diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index e05edcd90b1..e9ad5042a57 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -38,7 +38,7 @@ What the node does not yet meet: - The word each of [udp]'s refusals is answered in on the pipe ABI is `Refused` in `userland/netstack/node/src/datagram.rs`, tested by `each_refusal_is_answered_in_the_pipes_word_for_it` against no reader's scenario; `udp.no-ephemeral-port` has a word and no test that reaches it. Exit: the scenario owed below exists, and the test names its id. - A client's datagram to a broadcast address needs its socket's permission, where smoltcp sends it for every socket. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The pipe ABI carries the permission as far as netd: `MsgType::UdpSetOption` with `OPT_BROADCAST` (`toyos/src/net.rs`), sent by std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) and by libc's `setsockopt` of `SO_BROADCAST` (`userland/libc/src/socket.rs`), which keeps it for a socket not yet bound and sends it at `bind`; the word for a send refused for want of it is `ERR_PERMISSION_DENIED`, `ErrorKind::PermissionDenied` in std and `EACCES` in libc. The node enforces it and nothing that ships does: `Node::udp_set_broadcast` hands the permission to `Udp::set_broadcast`, and a send without it is refused `udp.broadcast-not-permitted` and answered `Refused::PermissionDenied` (`userland/netstack/node/src/datagram.rs`), which `a_broadcast_needs_its_permission` (US-21, `userland/netstack/node/tests/datagram.rs`) holds, with both broadcast addresses read off the wire in link-broadcast frames once the socket holds it and refused again once it is taken back, and `each_refusal_is_answered_in_the_pipes_word_for_it` holds the word. The permission of the call goes with the datagram to the route it leaves by: [udp] queues it with what its socket held, a closed socket's included, and `Ip::send_udp` refuses a link broadcast to one without it, `ip.broadcast-not-permitted`, counted and logged, so a prefix a DHCP server renews under a held address between the call and the frame makes no broadcast of a datagram accepted for a host (`s_udp_us_021_shard_a_datagram_accepted_for_a_host_is_no_broadcast_after_a_renewal_narrows_the_prefix` and its two neighbours in `toyos-net-shard/tests/udp.rs`, and from a server's ACK `a_datagram_accepted_for_a_host_is_no_broadcast_after_the_server_renews_a_narrower_prefix` in the node's tests); netd on smoltcp answers the request done and sends to a broadcast address for every socket. The search is run. `grep -rnI -E 'SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255|sendto|setsockopt|sys/socket\.h|socket\(' userland/doom/doomgeneric`, the one third-party C program an image builds, at the commit `userland/doom/build.rs` pins, exits 1 with no line; the same without `sys/socket\.h` over `tests/testcases` exits 1 with no line; `SO_BROADCAST|INADDR_BROADCAST|255\.255\.255\.255` over the trees of `src/libcxx.rs`'s `SOURCES` finds LLVM libc's own macro and its documentation and no caller; and in ToyOS's tree outside the stack's crates the option is named only by the two setters. The socket2 fork's setter reaches nobody: `issues/the-socket2-forks-so-broadcast-setter-does-nothing.md`. Exit, at the move: netd answers `MsgType::UdpSetOption`'s `OPT_BROADCAST` by `Node::udp_set_broadcast` and writes `ERR_PERMISSION_DENIED` for `Refused::PermissionDenied`. Exit, after the move, which keeps this line open until both are green on netd on the node, since no test can read the refusal on smoltcp: a guest test, `udp_broadcast_needs_its_permission`, reads `broadcast()` false, then true after `set_broadcast(true)`, on the socket and on a `try_clone` of it, which `broadcast()` answering a constant or a duplicate holding a value of its own turns red, and sends to the limited broadcast address before and after the set and is refused `PermissionDenied` only before, which std's setter reverted to `Ok(())` turns red; and a guest C case reads libc's setter, `socket`, `setsockopt(SO_BROADCAST)`, `bind`, `sendto` to the limited broadcast address, refused `EACCES` without the option and sent with it, and with the option set before `bind`, which `bind` no longer handing the kept option over turns red. The case runs the sequence without `bind` too, where the first `sendto` binds the socket and hands the kept option over; `tests/netcase/sendto_unbound.c` holds that send for a socket permitted to make it. - The machine's name is told to the node twice, as `toyos_dhcp::HostName` in `Node::new` and as `toyos_mdns::Host` in `Node::answer_as`: the first owns its bytes and the second borrows them. Exit: one type carries the label to both; due at the move if handing `answer_as` its `&'static str` takes a leak. -- The machine's name is probed for and defended (RFC 6762 §8, §9) by `toyos-mdns` (`userland/netstack/mdns`), on the node and in the netstack that ships. What that does not yet meet. Every machine is named `toyos-t14` (`dhcp::HOSTNAME`, `userland/netstack/src/dhcp.rs`) and a responder that loses its name holds none, so of two on one network the second answers to no name. A name lost under the probe stays lost after the other host has left, or a forger has stopped, until a link after none: two packets from any host on the link buy that, a response to take a held name back to probing and a response under the probe. No path back exists that a peer cannot hold shut, since a host that answers every probe is what a holder of the name is. The owner's open question, with nothing built for it: whether a lost name is probed for again on an interval. RFC 6762 permits probing again at §8.1's rate ("wait five seconds after any failed probe attempt before trying again") and does not require it; the owner's ruling so far is that of two same-named machines the second should "Hold no name, say so". The caller does not say how a message was addressed, so two rules that turn on it are not applied: a response is read however it came, where §6 reads a unicast one only within two seconds of a question that asked for one; and a message from a source outside the subnet is ignored even when it was sent to the group, where §11 deems that on the link "regardless of source IP address", so a host of the same name on another subnet of the link is never a conflict. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name and one in which a response sent to the group from another subnet does. Not measured, each with its exit: the shipped responder's claim on the T14's wired card, which no `system.toml` in the tree gives netstack (when the outbound rows land, their boot shows the lease line, then the claim line, and no loss line); and the link's return on metal (the orchestrator's attended session with the owner, never automated). A link that goes and returns inside one pass of the Intel driver reaches the name as no change: `issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md`. +- The machine's name is probed for and defended (RFC 6762 §8, §9) by `toyos-mdns` (`userland/netstack/mdns`), on the node and in the netstack that ships. What that does not yet meet. Every machine is named `toyos-t14` (`dhcp::HOSTNAME`, `userland/netstack/src/dhcp.rs`) and a responder that loses its name holds none, so of two on one network the second answers to no name: asked "When two machines on one network have the same ToyOS host name, what should the second one do?", the owner answered "Hold no name, say so (Recommended)". Asked on 2026-10-09 "A ToyOS machine that loses its .local name to a conflict stays nameless until its network link next returns, even after the other machine has left. Should it try for its name again by itself?", he answered "Retry on a slow interval (Recommended)". Built: a lost name is probed for again a minute after each loss (`RETRY_MS`, `userland/netstack/mdns/src/lib.rs`) and at once on a link after none, and is claimed by the first probing no host answers; RFC 6762 permits probing again at §8.1's rate ("wait five seconds after any failed probe attempt before trying again") and does not require it. Two packets from any host on the link still take a held name, a response to take it back to probing and a response under the probe, and one more a minute keeps it: no path back exists that a peer cannot hold shut, since a host that answers every probe is what a holder of the name is. The caller does not say how a message was addressed, so two rules that turn on it are not applied: a response is read however it came, where §6 reads a unicast one only within two seconds of a question that asked for one; and a message from a source outside the subnet is ignored even when it was sent to the group, where §11 deems that on the link "regardless of source IP address", so a host of the same name on another subnet of the link is never a conflict. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name and one in which a response sent to the group from another subnet does. Not measured, each with its exit: the shipped responder's claim on the T14's wired card, which no `system.toml` in the tree gives netstack (when the outbound rows land, their boot shows the lease line, then the claim line, and no loss line); and the link's return on metal (the orchestrator's attended session with the owner, never automated). A link that goes and returns inside one pass of the Intel driver reaches the name as no change: `issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md`. - A socket's datagrams that wait for a next hop wait in the one queue it has, `toyos_net_udp::limits::TX_DATAGRAMS` of them: with all 16 waiting for one next hop, the socket's sends to every other destination are refused `udp.tx-queue-full`, `Refused::ResourceExhausted` to a client, until that hop answers or [ip] gives it up, 3 s after its first request left (`BROADCAST_SOLICIT` requests a `RETRANS` apart). It is no regression by this track's earlier record of what ships, that smoltcp kept each datagram in its own socket until the neighbour answered. What a closed socket had accepted waits in the closed sender's `CLOSED_DATAGRAMS` the same way, so 16 datagrams of closed sockets that wait for one silent host have the next closes' datagrams discarded, `udp.tx-discarded-on-close`, for those 3 s: the line on US-53 among stage 3's departures. `s_udp_us_025_what_waits_for_a_next_hop_is_bounded_by_its_sockets_queue` holds the first as it stands. Owner: the worker of the move, with which a program first reaches this queue. Exit, by the move's first run on the T14 or in a guest: it shows a program refused for it, and a socket's bound is counted per next hop; or the owner rules the one queue is the bound. - A link that goes down drops no waiting datagram by itself: every waiting one is handed to [ip] again at the next transmit opportunity, which refuses it for want of a route (`route.no-source-address`), and one for which the link is back before that opportunity waits for its next hop anew, where the readers' link-down rule (NUD-23) drops what waits at once. `s_udp_us_024_shard_a_waiting_datagram_whose_route_goes_is_dropped` holds the first as it stands, and `a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves`, beside it in `toyos-net-shard/tests/udp.rs` and under no scenario's id, the second. Owner: the worker of the move. Exit, before the move: that second test's last assertion is inverted and finds the datagram that waited gone, or the specifications have it wait. - `toyos_dns::Lookup::on_datagram` still takes a reply's source address and port, which the node's connected sockets have already matched and the node hands it from its own record of the query: the two parameters are read only by netd's resolver on smoltcp. Exit, at the move: they go with that resolver. diff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs index 08dce48654e..c52c9a80a8c 100644 --- a/userland/netstack/mdns/src/lib.rs +++ b/userland/netstack/mdns/src/lib.rs @@ -45,19 +45,27 @@ //! when the last 250 ms end is heard under the probe and takes the name, //! where the other order would claim and announce it first. //! -//! **A lost name is not replaced.** §9 recommends a responder change its name -//! and probe again; this one holds none, says so to its owner and waits for a -//! link after none. The name was moved in by the owner, who also asks the +//! **A lost name is not replaced, and is probed for again.** §9 recommends a +//! responder change its name and probe again; this one holds none and says so +//! to its owner once. The name was moved in by the owner, who also asks the //! network to record it with the lease: a responder that picked another would //! answer to a name nothing else on the machine knows, that no storage keeps //! for the next boot (§9's third step), and that any host on the link could -//! move again by answering for it. +//! move again by answering for it. It probes for the same name again +//! [`RETRY_MS`] after each loss, and at once on a link after none: a probing +//! no host answers claims and announces the name as at start-up, and one the +//! holder answers waits the interval again and says nothing. §9's loser "MUST +//! cease using the name", and a probe uses none: it is a question, and +//! nothing is announced or answered between a loss and a claim. §8.1 lets a +//! failed probe be tried again five seconds later and asks for no retry at +//! all. //! //! **What a peer can make this responder do.** Every byte read is a peer's, //! from a source on this link (§11); none is trusted, and none is kept. //! //! - It keeps nothing a message brought: its state is its fixed fields, of -//! which a conflict writes the claim and one time. +//! which a conflict writes the claim, whether its loss was said and one +//! time. //! - One conflict costs at most one probing: three probes and two //! announcements. //! - §8.1: "If fifteen conflicts occur within any ten-second period, then the @@ -70,10 +78,25 @@ //! the ten seconds before it is probed on at once, as §9 asks. So a peer's //! messages buy at most one probing in five seconds, for as long as it //! sends them, and the name is held again when it stops. -//! - A lost name sends nothing and reads nothing until a link after none: it -//! stays lost after the other host has left or a forger has stopped. Two -//! packets from any host on the link buy that, a response that takes a held -//! name back to probing and a response under the probe. +//! - A lost name's probing begins at least [`RETRY_MS`] after the one +//! before, whatever arrives: between two nothing is read, since no probe is +//! out for a message to conflict with or tie; under a probe a conflicting +//! response and a probe that wins the tiebreak each put the next probing +//! the interval later, where §8.2's second would let forged probes buy one +//! in five seconds. So a peer's messages buy at most three probes in the +//! interval from a host that holds no name, no announcement and no word to +//! its owner. +//! - A link after none is probed on at once, whoever holds the name, in place +//! of the retry that was owed and never beside it. Under a lost name it +//! costs what it costs under any, its three probes, and §8.1's five seconds +//! first within ten of a conflict: one probing in five seconds while a +//! holder answers each. +//! - Two packets from any host on the link take a held name, a response that +//! takes it back to probing and a response under the probe, and one more in +//! each interval keeps it: a host that answers every probe is what a holder +//! of the name is. When it stops, the next probing claims the name. One +//! that lets each retry claim the name and then takes it buys the owner two +//! words in the interval, claimed and lost. //! //! Where an answer goes follows the question (§5.4, §6.7): //! @@ -271,8 +294,9 @@ pub enum Event { /// announced and answered with from here on. Claimed, /// §8.1: another host answered for the name under the probe. It is not - /// this host's, and nothing is announced or answered until a link comes - /// after none. + /// this host's, and nothing is announced or answered until it is + /// [`Event::Claimed`]. Said once: a later probing that host answers too + /// says nothing. Lost, } @@ -296,6 +320,13 @@ const DEFER_MS: u64 = 1_000; const CONFLICT_WINDOW_MS: u64 = 10_000; const LIMITED_WAIT_MS: u64 = 5_000; +/// How long after losing the name it is probed for again: a minute. A host +/// that holds the name answers one probe a minute for each host that wants +/// it, twelve times fewer than §8.1's five seconds would ask of it, and a +/// name whose holder has left is back within the minute and the second a +/// probing takes. §8.1's five seconds after a failed attempt are inside it. +pub const RETRY_MS: u64 = 60_000; + /// §6: a record is multicast on an interface at most once a second. const GROUP_EVERY_MS: u64 = 1_000; @@ -319,16 +350,16 @@ enum Claim { /// the name is held. A message conflicts only once `sent` is not zero. Probing { sent: u8, at_ms: u64 }, Held, - Lost, } /// This host's one record on its link, on the caller's monotonic clock in -/// milliseconds: probed for on every link after none, then announced twice a -/// second apart (§8, §8.3), answered to whoever asks, and multicast at most -/// once a second (§6), so no host can turn the queries it sends into a -/// multicast to every host on the link at its own rate. A query §6 holds back -/// is answered when the second ends, not dropped: the caller wakes at -/// [`Responder::owed_at`], for that and for every probe. +/// milliseconds: probed for on every link after none and [`RETRY_MS`] after +/// each loss, then announced twice a second apart (§8, §8.3), answered to +/// whoever asks, and multicast at most once a second (§6), so no host can +/// turn the queries it sends into a multicast to every host on the link at +/// its own rate. A query §6 holds back is answered when the second ends, not +/// dropped: the caller wakes at [`Responder::owed_at`], for that and for +/// every probe, a retry's included. /// /// A message counts as sent once [`Responder::owed`] has handed it over. #[derive(Debug)] @@ -337,6 +368,8 @@ pub struct Responder<'a> { /// The link the address is held on. link: Option, claim: Claim, + /// [`Event::Lost`] was said, and [`Event::Claimed`] not since. + lost: bool, /// When the name last met a conflict. conflict_ms: Option, last_group_ms: Option, @@ -351,7 +384,7 @@ pub struct Responder<'a> { impl<'a> Responder<'a> { pub const fn new(host: Host<'a>) -> Self { - Self { host, link: None, claim: Claim::Lost, conflict_ms: None, last_group_ms: None, owed_ms: None, again: false } + Self { host, link: None, claim: Claim::Owed { from_ms: 0 }, lost: false, conflict_ms: None, last_group_ms: None, owed_ms: None, again: false } } /// This host is on `link` at `now_ms`, or on none while it holds no @@ -395,6 +428,7 @@ impl<'a> Responder<'a> { return (Some(probe(self.host, link.addr)), None); } self.claim = Claim::Held; + self.lost = false; event = Some(Event::Claimed); self.owed_ms = Some(self.group_free_ms(now_ms, GROUP_EVERY_MS)); self.again = true; @@ -446,7 +480,6 @@ impl<'a> Responder<'a> { Claim::Owed { from_ms } => Some(from_ms), Claim::Probing { at_ms, .. } => Some(at_ms), Claim::Held => self.owed_ms, - Claim::Lost => None, } } @@ -473,33 +506,35 @@ impl<'a> Responder<'a> { self.rival(message, link, now_ms); (None, None) } - Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => (None, None), + Claim::Owed { .. } | Claim::Probing { .. } => (None, None), } } /// A response: §6 has one from a port other than 5353 silently ignored, - /// and one that conflicts takes a name under probe (§8.1) and sends a - /// held one back to probing (§9). + /// and one that conflicts takes a name under probe (§8.1), which is + /// probed for again [`RETRY_MS`] later, and sends a held one back to + /// probing (§9). fn response(&mut self, message: &[u8], from: Source, link: Link, now_ms: u64) -> Option { let probing = match self.claim { Claim::Probing { sent, .. } if sent > 0 => true, Claim::Held => false, - Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => return None, + Claim::Owed { .. } | Claim::Probing { .. } => return None, }; if from.port != PORT || !conflicts(message, self.host, link.addr).unwrap_or(false) { return None; } let wait = self.conflict(now_ms); if probing { - self.release(Claim::Lost); - return Some(Event::Lost); + self.release(Claim::Owed { from_ms: now_ms.saturating_add(RETRY_MS) }); + return (!core::mem::replace(&mut self.lost, true)).then_some(Event::Lost); } self.release(Claim::Owed { from_ms: now_ms.saturating_add(wait) }); None } /// A query heard under this host's probe: §8.2's comparison if it is - /// another host's probe for the name. + /// another host's probe for the name. A lost name defers for + /// [`RETRY_MS`], as it does to an answer. fn rival(&mut self, query: &[u8], link: Link, now_ms: u64) { let Some((kind, _)) = asked(query, self.host) else { return }; let Some((theirs, more)) = proposed(query, self.host, kind) else { return }; @@ -512,7 +547,7 @@ impl<'a> Responder<'a> { Ordering::Less => false, }; if later { - let wait = self.conflict(now_ms).max(DEFER_MS); + let wait = self.conflict(now_ms).max(if self.lost { RETRY_MS } else { DEFER_MS }); self.release(Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) }); } } diff --git a/userland/netstack/mdns/src/tests.rs b/userland/netstack/mdns/src/tests.rs index a0b6543ad48..1e592c07d7c 100644 --- a/userland/netstack/mdns/src/tests.rs +++ b/userland/netstack/mdns/src/tests.rs @@ -526,12 +526,11 @@ fn rfc6762_8_1_a_conflicting_response_under_the_probe_takes_the_name_and_nothing continue; } assert_eq!(r.said(), [Event::Lost], "{what}, after {probes} probes"); - assert_eq!((r.said(), r.owed_at()), (vec![], None), "said once, and nothing is owed"); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), [], "{what}: no probe and no announcement"); - assert_eq!(r.on(Some(LINK), 3_600_000, undrawn), None); - assert!(!answers(&mut r, 3_600_000), "{what}: and no answer"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(now + 60_000)), "said once, and nothing is owed for a minute"); + assert_eq!(run(&mut r, LINK, now + 59_999, 0), [], "{what}: no probe and no announcement"); + assert!(!answers(&mut r, now + 59_999), "{what}: and no answer"); for class in [1, 0x8001] { - assert_eq!(r.heard(&query(0, NAME, 255, class), PEER, 3_600_000), None); + assert_eq!(r.heard(&query(0, NAME, 255, class), PEER, now + 59_999), None); } } } @@ -597,9 +596,9 @@ fn rfc6762_9_a_held_name_another_host_answers_for_under_the_new_probe_is_lost() assert_eq!(r.heard(&says(&MOVED), PEER, 50_000), None); assert_eq!(run(&mut r, LINK, 50_100, 0), [(50_000, probe_for(ADDR))]); assert_eq!(r.heard(&says(&MOVED), PEER, 50_100), None, "the holder defends"); - assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], None)); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); - assert!(!answers(&mut r, 3_600_000)); + assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], Some(110_100))); + assert_eq!(run(&mut r, LINK, 110_099, 0), []); + assert!(!answers(&mut r, 110_099)); } /// RFC 6762 §6: "In the special case of answering probe queries, because of @@ -707,8 +706,8 @@ fn rfc6762_8_1_a_conflicting_response_received_as_the_last_250_ms_end_takes_the_ r.r.on(Some(LINK), 1_750); assert_eq!(r.heard(&says(&MOVED), PEER, 1_750), None); assert_eq!(r.said(), [Event::Lost]); - assert_eq!(r.r.owed(1_750, undrawn), (None, None), "and nothing is announced"); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); + assert_eq!(r.r.owed(1_750, || 0), (None, None), "and nothing is announced"); + assert_eq!(run(&mut r, LINK, 61_749, 0), []); } /// RFC 6762 §8: "Whenever a Multicast DNS responder starts up, wakes up from @@ -784,7 +783,7 @@ fn a_flapping_link_restarts_the_probe_and_nothing_is_announced_until_one_runs_wh /// A storm of forged answers, one a millisecond for ten seconds, at a held /// name: the first resets it to probing (§9), the next after its first probe /// takes it (§8.1), and the other 9,998 are read by a responder that holds no -/// name and sends nothing. +/// name and sends nothing for a minute. #[test] fn a_storm_of_forged_answers_costs_one_probe_and_the_name() { let mut r = held(10_000); @@ -794,8 +793,8 @@ fn a_storm_of_forged_answers_costs_one_probe_and_the_name() { assert_eq!(r.heard(&says(&MOVED), PEER, 50_000 + ms), None); } assert_eq!(sent, [(50_000, probe_for(ADDR))]); - assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], None)); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); + assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], Some(110_001))); + assert_eq!(run(&mut r, LINK, 110_000, 0), []); } /// Forged answers, one a millisecond for a minute, that spare every probe: @@ -902,11 +901,149 @@ fn an_address_after_none_is_probed_for_and_a_new_one_under_a_held_name_is_announ assert_eq!(r.on(Some(moved), 1_250, undrawn), Some(probe_for(MOVED)), "a probe proposes the address held as it leaves"); assert_eq!(r.heard(&says(&ADDR), PEER, 1_300), None, "and the address given up is another's"); assert_eq!(r.said(), [Event::Lost]); - assert_eq!((r.on(Some(LINK), 2_000, undrawn), r.owed_at()), (None, None), "a lost name is not claimed by a new address"); + assert_eq!((r.on(Some(LINK), 2_000, || 0), r.owed_at()), (None, Some(61_300)), "a lost name is not probed for on a new address"); assert_eq!(r.on(None, 3_000, undrawn), None); assert_eq!(r.on(Some(LINK), 12_000, || 0), Some(probe_for(ADDR)), "but by one after none"); } +/// A responder that lost its name at 1,100, to a response under the first +/// probe of its start-up; the retry's delay is not yet drawn. +fn lost() -> Machine { + let mut r = probing(LINK); + assert_eq!(r.heard(&says(&MOVED), PEER, 1_100), None); + assert_eq!(r.said(), [Event::Lost]); + r +} + +/// RFC 6762 §9: "the loser MUST cease using the name"; §8.1 lets a failed +/// probe attempt be tried again after five seconds and asks for none. A lost +/// name is probed for again a minute after it was lost, behind a delay drawn +/// as any probing's is (§8.1), and until then nothing is sent or answered. +#[test] +fn a_lost_name_is_probed_for_again_a_minute_after_the_loss_and_not_before() { + for (draw, delay) in [(0, 0), (40, 40), (250, 250), (1_000, 247)] { + let mut r = lost(); + assert_eq!(r.on(Some(LINK), 1_100, || draw), None, "draw {draw}"); + assert_eq!(r.owed_at(), Some(61_100 + delay), "a minute, then the drawn delay"); + for now in 1_101..61_100 + delay { + assert_eq!(r.on(Some(LINK), now, undrawn), None, "draw {draw}: nothing at {now}"); + } + assert!(!answers(&mut r, 61_099 + delay), "the name is nobody's here meanwhile"); + assert_eq!(r.on(Some(LINK), 61_100 + delay, undrawn), Some(probe_for(ADDR)), "draw {draw}"); + assert_eq!(r.said(), [], "a probe is no claim"); + } + let moved = Link { addr: MOVED, prefix: 24 }; + let mut r = lost(); + assert_eq!(r.on(Some(moved), 30_000, || 0), None, "a new address under a lost name"); + assert_eq!(run(&mut r, moved, 61_100, 0), [(61_100, probe_for(MOVED))], "is the one the retry proposes"); +} + +/// The host that holds the name answers the retry's probe (§8.1), early or +/// late in the probing: the name is lost again, a minute from that answer is +/// waited again, and the owner, told once that the name is lost, is told +/// nothing more. +#[test] +fn a_retry_the_holder_answers_loses_again_and_the_next_is_a_minute_after_it() { + let mut r = lost(); + assert_eq!(run(&mut r, LINK, 61_100, 0), [(61_100, probe_for(ADDR))]); + assert_eq!(r.heard(&says(&MOVED), PEER, 61_130), None, "the holder defends"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(121_130)), "lost as before, and said once"); + assert_eq!(run(&mut r, LINK, 121_129, 0), [], "nothing for a minute"); + assert!(!answers(&mut r, 121_129)); + + assert_eq!(run(&mut r, LINK, 121_849, 9), claim_of(ADDR, 121_139)[..3], "the next retry, three probes unanswered so far"); + assert_eq!(r.heard(&says(&MOVED), PEER, 121_849), None, "an answer in the last 250 ms"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(181_849))); + assert_eq!(run(&mut r, LINK, 181_848, 0), [], "no announcement, and nothing for a minute"); + assert_eq!(run(&mut r, LINK, 181_849, 0), [(181_849, probe_for(ADDR))]); + assert_eq!(r.said(), []); +} + +/// The host that held the name has left: the retry's probing runs +/// unanswered, and the name is claimed and announced as at start-up (§8.1, +/// §8.3). A name claimed is one that can be lost, and said lost, again. +#[test] +fn a_retry_no_host_answers_claims_and_announces_the_name() { + let mut r = lost(); + assert_eq!(run(&mut r, LINK, 3_600_000, 25), claim_of(ADDR, 61_125), "three probes, two announcements, and nothing after"); + assert_eq!(r.said(), [Event::Claimed]); + assert!(answers(&mut r, 3_600_000)); + + assert_eq!(r.heard(&says(&MOVED), PEER, 4_000_000), None); + assert_eq!(run(&mut r, LINK, 4_000_000, 0), [(4_000_000, probe_for(ADDR))], "§9: a held name is probed for at once"); + assert_eq!(r.heard(&says(&MOVED), PEER, 4_000_100), None); + assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], Some(4_060_100)), "lost anew, and said"); +} + +/// Forged messages, each a millisecond for five minutes, at a host that +/// holds no name: a conflicting response, a probe that wins every tiebreak +/// and a query. Between two probings no probe is out and none is read; the +/// response that follows a retry's first probe takes the name again. The +/// forger buys one probe a minute and the drawn delay, no announcement, and +/// no word to the owner. +#[test] +fn a_storm_of_forged_conflicts_at_a_lost_name_costs_one_probe_a_minute() { + let forged = [says(&MOVED), probe_of(1, &[(NAME, 1, 1, &MOVED)]), query(0, NAME, 255, 1)]; + let mut r = lost(); + let mut sent = Vec::new(); + for now in 1_101..301_101 { + sent.extend(run(&mut r, LINK, now, 7)); + for message in &forged { + assert_eq!(r.heard(message, PEER, now), None); + } + } + let probes: Vec<_> = (0..4).map(|nth| (61_107 + nth * 60_007, probe_for(ADDR))).collect(); + assert_eq!(sent, probes, "four probes in the five minutes, each a minute and its delay after the one before"); + assert_eq!(r.said(), [], "lost was said before the storm, and is not said again"); + assert_eq!(run(&mut r, LINK, 3_600_000, 7), claim_of(ADDR, 301_135), "the forger stops, and the next retry claims the name"); + assert_eq!(r.said(), [Event::Claimed]); +} + +/// RFC 6762 §8.2 has the loser of a tiebreak wait a second and probe again, +/// which forged probes turn into a probe in five seconds (§8.1) at a host +/// that holds no name for as long as they are sent. A lost name defers to a +/// later probe as it does to an answer, for a minute: one probe a minute, +/// and the name when the forger stops. +#[test] +fn forged_probes_that_win_every_tiebreak_at_a_lost_name_cost_one_probe_a_minute() { + let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); + let mut r = lost(); + let mut sent = Vec::new(); + for now in 1_101..301_101 { + sent.extend(run(&mut r, LINK, now, 7)); + assert_eq!(r.heard(&later, PEER, now), None); + } + let probes: Vec<_> = (0..4).map(|nth| (61_107 + nth * 60_000, probe_for(ADDR))).collect(); + assert_eq!(sent, probes, "four probes in the five minutes, a minute apart"); + assert_eq!(r.said(), []); + assert_eq!(run(&mut r, LINK, 3_600_000, 7), claim_of(ADDR, 301_107), "the forger stops, and the next retry claims the name"); + assert_eq!(r.said(), [Event::Claimed]); +} + +/// RFC 6762 §8: a link's return is probed on at once, under a lost name as +/// under any. It takes the place of the retry that was owed: one probing, +/// and none a minute after the loss. A holder that answers it is deferred to +/// for a minute from that answer. +#[test] +fn a_links_return_under_a_lost_name_probes_at_once_in_place_of_the_retry() { + let mut r = lost(); + assert_eq!(r.on(Some(LINK), 1_100, || 0), None); + assert_eq!(r.owed_at(), Some(61_100), "the retry"); + r.link_returned(30_000); + assert_eq!(r.owed_at(), Some(30_000), "at once"); + assert_eq!(run(&mut r, LINK, 3_600_000, 3), claim_of(ADDR, 30_003), "one probing, and no second at 61,100"); + assert_eq!(r.said(), [Event::Claimed]); + + let mut r = lost(); + assert_eq!(r.on(Some(LINK), 1_100, || 0), None); + r.link_returned(30_000); + assert_eq!(run(&mut r, LINK, 30_000, 0), [(30_000, probe_for(ADDR))]); + assert_eq!(r.heard(&says(&MOVED), PEER, 30_050), None, "the holder is still there"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(90_050)), "a minute from this loss, and not from the first"); + assert_eq!(run(&mut r, LINK, 90_049, 0), [], "no probe at 61,100"); + assert_eq!(run(&mut r, LINK, 90_050, 0), [(90_050, probe_for(ADDR))]); +} + /// RFC 6762 §11: a query whose source is not on this link is ignored; a /// link-local source (RFC 3927) is on every link. RFC 1122 §3.2.1.3: a /// loopback source is never on a wire, and a datagram carrying one is diff --git a/userland/netstack/node/src/name.rs b/userland/netstack/node/src/name.rs index 1816734468e..9b06bd50a47 100644 --- a/userland/netstack/node/src/name.rs +++ b/userland/netstack/node/src/name.rs @@ -11,8 +11,8 @@ //! so no new address would say the name is in doubt (§8). The responder probes for it again. //! - Every message that has arrived is handed over before the responder is asked what it owes: a //! conflicting response received as a probing ends takes the name before it is claimed (§8.1). -//! - What the name is owed later, a probe, an announcement (§8.3) or an answer §6 held back, is a -//! deadline of the node's ([`Name::next_deadline`]). +//! - What the name is owed later, a probe, a lost name's next among them, an announcement (§8.3) +//! or an answer §6 held back, is a deadline of the node's ([`Name::next_deadline`]). //! - Each probing the responder starts spends one draw, the delay before its first probe (§8.1). //! - What became of the name, claimed or lost, is a line of [`Node::drain_events`]. //! - Every message leaves with TTL 255 (§11). diff --git a/userland/netstack/node/tests/name.rs b/userland/netstack/node/tests/name.rs index 823054774ee..a6d2d1e7adf 100644 --- a/userland/netstack/node/tests/name.rs +++ b/userland/netstack/node/tests/name.rs @@ -36,6 +36,8 @@ const CACHE_FLUSH: u16 = 0x8000; /// apart. const QUARTER: Duration = Duration::from_millis(250); const SECOND: Duration = Duration::from_secs(1); +/// How long after losing its name the responder probes for it again. +const MINUTE: Duration = Duration::from_secs(60); /// The draw of a call that starts no probing. fn undrawn() -> u32 { @@ -374,7 +376,7 @@ fn a_conflicting_response_received_as_the_probing_ends_takes_the_name() { lan.now = ends; lan.deliver(&to_group(MAC_B, (B, MDNS), &says(B))); assert_eq!(said(&mut lan), [Event::Lost]); - let later = lan.now.after(Duration::from_secs(60)); + let later = lan.now.after(Duration::from_secs(59)); lan.fire(later); assert_eq!(since(&lan, 0), [probe(), probe(), probe()].map(multicast), "three probes, and no announcement"); assert_eq!(said(&mut lan), []); @@ -396,7 +398,7 @@ fn another_hosts_answer_under_the_probe_takes_the_name_and_the_node_says_so() { until(&mut lan, 1, SECOND); lan.deliver(&defended); assert_eq!(said(&mut lan), [Event::Lost]); - let later = lan.now.after(Duration::from_secs(60)); + let later = lan.now.after(Duration::from_secs(59)); lan.fire(later); lan.deliver(&to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN))); lan.deliver(&to_group(MAC_B, (B, MDNS), &ours(0, CLASS_IN))); @@ -412,6 +414,77 @@ fn another_hosts_answer_under_the_probe_takes_the_name_and_the_node_says_so() { } } +/// [`named`], its lease held, and its name lost to B's answer under the first probe. Returns +/// when. +fn lost() -> (Lan, Instant) { + let mut lan = named(); + lan.lease(3_600); + until(&mut lan, 1, SECOND); + lan.deliver(&to_group(MAC_B, (B, MDNS), &says(B))); + assert_eq!(said(&mut lan), [Event::Lost]); + let at = lan.now; + (lan, at) +} + +/// Whether `probed` is where a probing owed `MINUTE` after `lost` puts its first probe: behind +/// the minute and at most §8.1's 250 ms of delay, on the responder's clock of whole milliseconds. +fn retried(lost: Instant, probed: Instant) -> bool { + let waited = probed.since(lost); + waited + Duration::from_millis(1) > MINUTE && waited <= MINUTE + QUARTER +} + +// A lost name is probed for again a minute after each loss, and that probe is a deadline of the +// node's: nothing but its own deadlines moves the clock here. RFC 6762 §9 has the loser "cease +// using the name", and between the loss and a claim nothing is announced or answered. The host +// that holds the name answers the first retry (§8.1): lost again, said to nobody, and a minute +// from that answer. It has left by the second: three probes unanswered, and the name is claimed +// and announced as at start-up (§8.1, §8.3). +#[test] +fn a_lost_name_is_probed_for_again_a_minute_after_each_loss_and_claimed_once_no_host_answers() { + let (mut lan, first) = lost(); + let asked = to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN)); + assert!(!lan.run_until(Duration::from_secs(59), |lan| lan.datagrams().len() > 1), "every deadline of 59 seconds, and no probe"); + lan.fire(first.after(Duration::from_secs(59))); + lan.deliver(&asked); + assert_eq!(lan.datagrams().len(), 1, "and no answer"); + until(&mut lan, 2, Duration::from_secs(2)); + assert!(retried(first, times(&lan, 1)[0]), "{:?} after the loss", times(&lan, 1)[0].since(first)); + + lan.deliver(&to_group(MAC_B, (B, MDNS), &says(B))); + let second = lan.now; + assert_eq!(said(&mut lan), [], "the name was said lost, and is not said so again"); + assert!(!lan.run_until(Duration::from_secs(59), |lan| lan.datagrams().len() > 2), "every deadline of 59 seconds, and no probe"); + lan.fire(second.after(Duration::from_secs(59))); + assert_eq!(since(&lan, 0), [probe(), probe()].map(multicast), "one probe a loss, and no announcement"); + + until(&mut lan, 7, Duration::from_secs(4)); + let probed = times(&lan, 2)[0]; + assert!(retried(second, probed), "{:?} after the second loss", probed.since(second)); + claimed(&lan, 2, probed); + assert_eq!(said(&mut lan), [Event::Claimed], "B is gone, and the name is this machine's"); + lan.deliver(&asked); + assert_eq!(since(&lan, 7), [unicast(53_000, legacy_response(7))], "and is answered with"); +} + +// RFC 6762 §8: the link's return is probed on at once, under a lost name too, and in place of the +// retry that was owed: five datagrams claim the name, and nothing follows them a minute after the +// loss. +#[test] +fn a_links_return_under_a_lost_name_is_probed_on_at_once_and_no_retry_follows_it() { + let (mut lan, _) = lost(); + let half = lan.now.after(Duration::from_secs(30)); + lan.fire(half); + assert_eq!(lan.datagrams().len(), 1, "nothing in half a minute"); + lan.link(false); + lan.link(true); + let back = lan.now; + until(&mut lan, 6, Duration::from_secs(3)); + claimed(&lan, 1, back); + assert_eq!(said(&mut lan), [Event::Claimed]); + assert!(!lan.run_until(Duration::from_secs(120), |lan| lan.datagrams().len() > 6), "every deadline of two minutes"); + assert_eq!(lan.datagrams().len(), 6, "one probing, and no second where the retry was owed"); +} + // RFC 6762 §8.2: "The two records are compared and the lexicographically later data wins. This // means that if the host finds that its own data is lexicographically later, it simply ignores // the other host's probe. If the host finds that its own data is lexicographically earlier, then diff --git a/userland/netstack/src/mdns.rs b/userland/netstack/src/mdns.rs index f148e52bccb..9c0d66a77a9 100644 --- a/userland/netstack/src/mdns.rs +++ b/userland/netstack/src/mdns.rs @@ -24,7 +24,7 @@ use std::time::{Duration, Instant}; use smoltcp::iface::{Interface, SocketHandle, SocketSet}; use smoltcp::socket::udp; use smoltcp::wire::{IpAddress, IpCidr, IpEndpoint}; -use toyos_mdns::{Event, Host, Link, Source, To, GROUP, PORT}; +use toyos_mdns::{Event, Host, Link, Source, To, GROUP, PORT, RETRY_MS}; /// A message is a few hundred bytes; this holds a handful of them between two /// passes, and one past it is dropped by the socket, which is what its @@ -104,7 +104,10 @@ impl Responder { fn said(host: &str, event: Option) { match event { Some(Event::Claimed) => crate::say!("netstack: mDNS: no host answered for {host}.local; this machine answers as it"), - Some(Event::Lost) => crate::say!("netstack: mDNS: another host answered for {host}.local; this machine answers to no name"), + Some(Event::Lost) => crate::say!( + "netstack: mDNS: another host answered for {host}.local; this machine answers to no name and asks for {host}.local again every {} s", + RETRY_MS / 1000 + ), None => {} } }