Repository navigation
netstack runs every link a driver brings: ranked, with failover and return (P5) - #1122
Conversation
Review of #1122 at ad04673 (P5), round 1ArchitectureThe shape is right, and it is what the owner approved (#876 ruling 3). There is one
Net growth: production +463, tests +1008. Most of the test growth is mechanical signature updates. The problem is that some per-link state is still keyed by an IPv4 address alone. With one interface that was unique. With several links it often is not: the same home gateway can be reached over wired and Wi-Fi, two consumer routers both use 192.168.1.1, and a wired and wireless link can be bridged onto one LAN. Blockers
Evidence
Suggestions
SEND BACK |
…es no other, one LAN on two links - A next hop is its link's: TCP's reachability advice carries the connection's local address and ip advises the flow's own link's entry (Source::Bound); a UDP datagram waits on and is failed by its next hop on its interface, so one link's dead gateway drops no datagram waiting for the same address on another. - ip's control queue bounds each interface's share: a link that takes nothing fills only its own, and every other link's ARP, ICMP, probes and announcements still queue and leave. - One LAN on two links (RFC 6762 §14's bridged laptop): ip already refuses a datagram from any of its interfaces' addresses (`ip.own-source`), so no responder hears this machine's own mDNS; name.rs says so. A server's reply addressed to another link's MAC is that link's, and the node drops it before this link's client logs it (`node.dhcp-other-link`; toyos_dhcp::addressed_to reads the reply's chaddr). A host test boots the bridged case. - The asked link is sticky: with no link up, lookups keep asking the link they asked; tested whichever link goes down last. - Serving before a link made comments false: the ULP row gates on the lease, netstack_answers goes, sshserver's and netstack_gone_mid_bind's comments say what is true; a detached doc comment is reattached. - Each link's DHCP client has its own refusal log; link lines say `link <n>`; drivers' pending hellos are capped at the link cap; the two-NIC profile's networks are restrict=on, the harness reached through each network's guestfwd `.100:7`. - issues/two-links-leased-one-address-the-second-holds-none.md records the two networks that lease one address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With restrict=on the failover job's first datagram off both networks was refused for want of a route, and the default route is what the test is of; the harness stays the peer at each network's QEMU host address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ad04673 to
d328d25
Compare
…ntrols, the T14 rows judged
Round 2: review r1Blockers
Suggestions
README revision: this round's index names each run's revision exactly. Checks at d328d25 (on main 24a538c): |
… what they hold - i219_leaves_ulp_unarmed's comment: it reads the driver's lines once its job has seen the lease, which follows the bring-up. - acd.rs: the helper is `answering()`, if0 holding ANSWERING, whose replies fill if0's own share of the queue; its callers name if0, the comment says so, and the two asserts check the frames that led are ARP replies. - Headers: udp and the shard say a datagram waits on the hop on its interface; name.rs notes the bridged message that crosses as its link loses its address; lease.rs's long line wrapped; the failover comment says it depends on no public network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 3 at a478fe8, on main 24a538c: both required items fixed.
Checks: |
…ink credit, a lost address's flows ended The P5 build's first half (issues/netstack-drives-one-card.md); the node and netstack follow, and do not build against this commit alone. - link: a hello says its medium, Wired or Wireless; i219 and virtio-net say Wired. OneLink goes: a stack refuses a link Open (its address's is alive) or Full. - ip: an interface is added with a Rank, and every choice between interfaces goes by rank and then arrival, where it went by index alone. A transmit opportunity takes Credit, room per interface, which a count still is for every interface alike; the control queue hands out the oldest frame whose link has room. A UDP datagram may name its interface, and send_udp answers NoRoom for one whose link has none. - udp: a received datagram says the interface it came by; the acquisition socket's send names an Origin, its source and its interface. - tcp: lose_address ends every connection from an address the host gave up, Failure::AddressGone, and sends nothing. - shard: no interface until the shell adds one per link; frames come in and go out with their interface, and a flow whose route leaves by a link with no room waits on that link. A removed address, or one ip lost to another host, ends its TCP connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s name's responder The P5 build's second half below netstack itself, which follows. - node: built with no link and serving from there; add_link gives each link its own DHCP client, lease state and mDNS responder, under one host name. Frames come in and go out by link, and one transmit opportunity takes each link's credit. - lease: one state per link, written by this module alone as before. The links' one acquisition socket hands each datagram to the client of the link it came by, and each client's message leaves by its own link. Lookups ask the asked link's resolvers: of the links holding a lease, one up before one down, then by rank and arrival. One cache holds their answers, emptied with every state written, every link down and every change of the link asked. - name: one socket on 5353, one responder per link; a message is its arrival link's responder's, and what a responder sends leaves by its link alone (udp's send_via). A name lost on one link is that link's. - A stream from an address the machine lost ends ADDRESS_GONE; the SDK's Refused::AddressGone, std's AddrNotAvailable and libc's EADDRNOTAVAIL name it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
netstack holds up to eight links, each the node's interface for the boot and ranked by its hello's medium, wired before wireless, then by arrival. A hello with a new address adds a link; one with an ended link's address is opened on it again; one with an open link's address is refused Open, and one past eight Full. The node is built before any link and serves from there, so LINK_BOUND and its exit are gone: a machine with no NIC driver keeps its netstack and says, at the lease bound, that it has no address. One transmit opportunity offers the node every open link's ring room. The lease lines name their link; inspect puts each link under net.link.<n> (its driver, medium, state, lease, netstack's own count of frames sent and the driver's counters) and the asked resolvers' link as net.dns.link. osbench sums the links' card counters and takes a connection's medium from the link holding its address. A guest test boots two virtio NICs on two QEMU user networks (Profile::HeadlessTwoNics) and has QMP set the first link down and up: traffic to TEST-NET-1 leaves by the first, by the second and none by the first while it is down, and by the first once it is back. issues/netstack-drives-one-card.md is closed: its rule, the owner's ruling that netstack runs every link wired first with failover, is P5's in the track. The NIC leaves first-match-selection-that-remains's virtio bullet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two-NIC guest test found QMP's set_link reaching nothing: virtio-net accepted no VIRTIO_NET_F_STATUS, so its link was Unreported and netstack took it as up whatever QEMU said. The driver now accepts the feature where the device offers it and reads `status` (§5.1.4) each pass; a change of it is a configuration change (§5.1.4.2), which raises the one vector, so a pass follows it. toyos-virtio's live device reads a 16-bit field of its structure, bounded and aligned as its 32-bit ones (§4.1.3.1). The link word gains UpWithoutSpeed, its free fourth tag: a device that reports its carrier and no speed, as virtio does without VIRTIO_NET_F_SPEED_DUPLEX. inspect reads it as up. The failover job waits on netstack's inspect for both leases: netcase gives its jobs no log port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them files.rs round-trips every stream refusal through the C interface; its count moves from seven to eight with P5's AddressGone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es no other, one LAN on two links - A next hop is its link's: TCP's reachability advice carries the connection's local address and ip advises the flow's own link's entry (Source::Bound); a UDP datagram waits on and is failed by its next hop on its interface, so one link's dead gateway drops no datagram waiting for the same address on another. - ip's control queue bounds each interface's share: a link that takes nothing fills only its own, and every other link's ARP, ICMP, probes and announcements still queue and leave. - One LAN on two links (RFC 6762 §14's bridged laptop): ip already refuses a datagram from any of its interfaces' addresses (`ip.own-source`), so no responder hears this machine's own mDNS; name.rs says so. A server's reply addressed to another link's MAC is that link's, and the node drops it before this link's client logs it (`node.dhcp-other-link`; toyos_dhcp::addressed_to reads the reply's chaddr). A host test boots the bridged case. - The asked link is sticky: with no link up, lookups keep asking the link they asked; tested whichever link goes down last. - Serving before a link made comments false: the ULP row gates on the lease, netstack_answers goes, sshserver's and netstack_gone_mid_bind's comments say what is true; a detached doc comment is reattached. - Each link's DHCP client has its own refusal log; link lines say `link <n>`; drivers' pending hellos are capped at the link cap; the two-NIC profile's networks are restrict=on, the harness reached through each network's guestfwd `.100:7`. - issues/two-links-leased-one-address-the-second-holds-none.md records the two networks that lease one address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With restrict=on the failover job's first datagram off both networks was refused for want of a route, and the default route is what the test is of; the harness stays the peer at each network's QEMU host address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's new job read net.lease.held, which P5 puts under each link as net.link.<n>.lease.held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… what they hold - i219_leaves_ulp_unarmed's comment: it reads the driver's lines once its job has seen the lease, which follows the bring-up. - acd.rs: the helper is `answering()`, if0 holding ANSWERING, whose replies fill if0's own share of the queue; its callers name if0, the comment says so, and the two asserts check the frames that led are ARP replies. - Headers: udp and the shard say a datagram waits on the hop on its interface; name.rs notes the bridged message that crosses as its link loses its address; lease.rs's long line wrapped; the failover comment says it depends on no public network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a478fe8 to
e71bcf0
Compare
std's TcpStream::local_addr and libc's getsockname answered QEMU's user network address with the stream's port, whatever the machine leased. Since #1122 osbench names a connection's link by that address, so on the T14 osbench_lan refused its group: "no netstack link holds 10.0.2.15". netstack's connect and accept answers and its ADOPT now carry the address the stream left from with its port: the node's StreamEvent::Connected, Accepted and export give the connection's local Endpoint, the tuple [tcp] holds. The SDK takes it only as a stream's end can be, a unicast address and a port; a connect or accept answer naming another is refused as a protocol error, and an ADOPT naming one as malformed. stream::endpoints returns it, std's local_addr and libc's getsockname answer it, and getsockname answers a listener's or datagram socket's bound address rather than the literal. Connected and Accepted lose their unread local_port. The guest jobs that play netstack (stream_handles, libc_recv_beside_close) name a TEST-NET-1 local address in their answers. The node's host tests assert the lease's address on connect, accept and export. link_failover opens a stream to the host on each of its two QEMU user networks and asserts each one's local address is its link's lease. The two leases differ, so one is not 10.0.2.15 and the value is netstack's: with std's literal restored the job fails, whichever order the links came up in. issues/getsockname-answers-an-address-nobody-asked-netstack-for.md is closed, and its references go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
P5 of
issues/there-is-no-wifi.md; its defect isissues/netstack-drives-one-card.md. netstack opens only the first link a driver brings and refuses every other withOneLink. Its node is one interface with one DHCP client, built from that link's address. As a result nothing is served before a driver's hello, and a machine with no NIC driver waits 30 s (LINK_BOUND) and then has no netstack at all. The owner ruled that netstack runs every link a driver brings, wired first, with failover (#876, ruling 3).Design
One stack over several interfaces.
toyos-net-ipalready holds several interfaces and routes over them under the strong host model. OneIp, oneTcpand oneUdpstay as they are, so routing, listeners and bound sockets keep a single owner. Each layer above them gains a per-link part:linksays its medium. A hello carriesWiredorWirelessafter the address;i219andvirtio-netsayWired. The two refusals are nowOpen(a link of this address is open and alive) andFull(the stack drives its most links).OneLinkgoes.ipranks interfaces. Each interface is added with a rank, and arrival is its index. Every choice among eligible interfaces now goes by rank and then arrival, where it used to go by index alone: the default gateway, a tie between equally long connected prefixes, the multicast fallback and a link-local neighbour. netstack gives wired rank 0 and wireless rank 1.Failover and return come from that rule. A link that goes down leaves the lookup (
upis already checked), so new flows go out on the next-ranked link with a route. When it comes back, it ranks first again. A lease outlives its link going down, as it does today.A flow bound to a lost address ends. Under the strong host model a TCP connection cannot move to another address. When an interface loses an address, because its lease ends, is replaced by another address, or is taken by another host, every connection from that address ends at once with
AddressGone. No reset is sent, since nothing can leave from that address, and the client readsADDRESS_GONEon its stream. Until now such a flow retransmitted until it timed out. A UDP socket bound to the address stays: each datagram it sends is refused byipfor want of a source, as before. A link that only goes down ends nothing: its address is still held.The shard holds several interfaces. Interfaces are added as links arrive, and each frame arrives tagged with its interface. One transmit opportunity serves every link with room:
ip's control queue hands out the oldest frame whose link has room, and bounds each interface's share, so a link that takes nothing holds back no other link's ARP, ICMP, probes or announcements;The round stays one deficit round-robin over every flow, so fairness between flows does not depend on which link carries them.
The node holds one link part per link. Each part has its own lease state, DHCP client (sending bound to its interface) and mDNS responder, all under one host name. A name lost to another host on one link is that link's alone: its responder probes for the same name again later, and the other links keep it. On one LAN two links share (a wired and a wireless link bridged, RFC 6762 §14),
ipalready refuses a datagram from any address its interfaces hold, so the machine never hears its own mDNS. A server's reply addressed to another link's MAC is dropped before this link's client sees it. An arriving datagram says which interface it came by, so a responder hears only its own link. The node is built before any link and serves at once; with no link, a connect is refused for want of a route.LINK_BOUNDgoes.Resolvers. A lookup asks the resolvers of the asked link's lease: of the links up and holding a lease, the first by rank and arrival. With none up, it keeps the link it asked while that link still holds its lease, since a down link keeps it; failing that, it takes the best-ranked held lease. One cache holds the answers. It is emptied whenever any link's state is written, a link goes down, or the asked link changes. A lookup ends with its lease's word, as it does now.
netstack. It keeps up to
MAX_LINKSsessions. A driver that ends and comes back with its address gets its interface back.inspectand the lease lines are per link.A flow on a down link. Under the strong host model it does not fail over. An established connection keeps retrying until its link returns. A SYN with no route ends unreachable, as on one link today.
Rejected: one shard per link. It would duplicate the routing choice above
ip(longest prefix across shards, then rank) and fan out every wildcard listener and socket to every shard, including shards that arrive later.What changes
Eight commits; the last three answer review r1 and the rebase onto main.
lib/link: the hello'sMedium, and theOpenandFullrefusals.ip:Rank; the ordered lookup;Creditwith a per-link control queue;NoRoom; a datagram's interface.udp: the arrival interface;Origin;send_via.tcp:lose_addressandFailure::AddressGone.ADDRESS_GONEcarried through the SDK, std (AddrNotAvailable) and libc (EADDRNOTAVAIL).inspectundernet.link.<n>: driver, medium, state, lease, and netstack's ownsentcount.LINK_BOUND, noOneLink.set_linkreaching nothing: the driver accepted noVIRTIO_NET_F_STATUS, so netstack took its link as up whatever QEMU said.status(§5.1.4) each pass. A change is a configuration change, which raises its one vector.toyos-virtio's live device reads a 16-bit field of its structure.UpWithoutSpeed, its free fourth tag: a carrier with no speed.files.rs's count of stream refusals that cross the C interface moves from seven to eight.Who changes behaviour:
inspect's NIC keys moved undernet.link.<n>, and osbench is their only reader.netstack: DHCP: leaseprefix and names its link after the address.test_rs_netstack_answersis gone: it no longer ordered anything.i219_leaves_ulp_unarmedgates on the lease, which follows the card's bring-up.Checks
Round 2, at d328d25 on origin/main 24a538c:
cargo run -- --ci host: exit 0, 107 steps.a_client_killed_mid_connect_gives_its_place_back. That job readnet.lease.held, now per link, and is updated.i219_carries_netstacks_linkandi219_leaves_ulp_unarmedboth pass.ip: advice landing on its own link's gateway where two links share one, and a stuck link holding back none of another's frames;addressed_to.Source::Any;iplooking advice up withSource::Any; failures dropping every interface's waiters; one shared queue bound; another link's reply reaching this client; asked falling back by rank.Round 1's checks: https://github.com/ToyOSOrg/evidence/blob/9750fbe47a4dcb5849c364f5aa8599a3220471a3/p5-r1/README.md
Integration tests and where they run
traffic_leaves_by_the_second_link_while_the_first_is_downruns on QEMU x86-64, with the newProfile::HeadlessTwoNics: two virtio NICs on two user networks. Its claim is what host tests can't establish: two drivers' links, a NIC's carrier set from QMP and netstack's routes, across three processes and two devices. It checks order and counts only. Its datagrams go to TEST-NET-1, and the harness is the peer on the host's loopback, so it needs no public network. No metal machine has two NICs ToyOS drives; P5's T14 row, wired and wireless both leasing, is W6's.The T14's existing I219 rows (
i219_carries_netstacks_link,i219_leaves_ulp_unarmed) passed on one boot. The I219 now says a medium in its hello, and netstack's lines name their link.Risks
Full.issues/two-links-leased-one-address-the-second-holds-none.md).🤖 Generated with Claude Code