From db786202740707933740121bf2f3877aa5b7c041 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 10:20:32 +0200 Subject: [PATCH 1/3] toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome and its bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 6762 §8 asks a responder to probe for a unique name before it announces it, at start-up and whenever its connectivity may have changed, and §9 to defend it. The responder announced `.local` unprobed on every new address, and the node on every return of its link too, each time with the cache-flush bit, so two machines named alike each flushed the other's record. The responder is still a pure state machine and now holds the claim: - an address after none and a link's return owe a probing; `on` draws its delay from the caller (0 to 250 ms), sends three probes 250 ms apart, each `ANY`, QU, with the proposed record in the Authority Section, and holds the name 250 ms after the third. Only a held name is announced or answered with. - a conflicting response under the probe loses the name; one at a held name sends it back to probing; another host's probe heard under ours is compared by class, type and data, and the earlier waits a second. - a held name answers another host's probe as soon as 250 ms after the record's last multicast (§6's one exception). - a lost name is not replaced by one the responder makes up: the name was moved in by its owner, and the responder reports the loss and holds none until the link returns or an address comes after none. - fifteen conflicts in ten seconds put five seconds before each further attempt (§8.1), and the fifteenth conflict since the link last returned loses the name, so no peer's packets keep the responder probing. `answer` is `heard`, since it reads responses and probes too, and `Asker` is `Source`. The node hands the name its caller's draw from `settle`, which is one line of `lib.rs`, and `answer_as` takes the draw of the probing it may start. The netstack that ships draws from the kernel's random source and logs what became of the name. Closes issues/netstack-answers-for-its-name-without-claiming-it.md; what is still true of the name is the track's line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...nswers-for-its-name-without-claiming-it.md | 42 - issues/toyos-has-its-own-network-stack.md | 2 +- userland/netstack/mdns/src/lib.rs | 835 +++++++++------- userland/netstack/mdns/src/tests.rs | 906 ++++++++++++++++++ userland/netstack/node/src/lib.rs | 7 +- userland/netstack/node/src/name.rs | 73 +- userland/netstack/node/tests/name.rs | 375 ++++++-- userland/netstack/node/tests/resolve.rs | 2 +- userland/netstack/src/mdns.rs | 77 +- 9 files changed, 1763 insertions(+), 556 deletions(-) delete mode 100644 issues/netstack-answers-for-its-name-without-claiming-it.md create mode 100644 userland/netstack/mdns/src/tests.rs diff --git a/issues/netstack-answers-for-its-name-without-claiming-it.md b/issues/netstack-answers-for-its-name-without-claiming-it.md deleted file mode 100644 index 9cb223e1c9c..00000000000 --- a/issues/netstack-answers-for-its-name-without-claiming-it.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-24 ---- - -# netstack answers for its name without first claiming it on the network - -`toyos-mdns` (`userland/netstack/mdns/src/lib.rs`) announces `.local` -and answers for it, and does neither of the things RFC 6762 asks of a -responder before it uses a name: probing for it (§8.1) and defending it against -another host's answer (§9). §8.3 announces a unique record that has "completed -the probing step"; this one has not, and each announcement and each multicast -answer carries the cache-flush bit (§10.2), which tells every cache on the link -to drop any other record of the name. - -Every machine this tree boots is named `toyos-t14` (`dhcp::HOSTNAME`, -`userland/netstack/src/dhcp.rs`), so two of them on one network both answer for -that name, each flushes the other's record, and a resolver reaches whichever -spoke last. - -The record is announced, unprobed, on every new address (the responder that -ships, called by `userland/netstack/src/mdns.rs`) and, in `toyos-net-node`, -which ships in nothing yet, also on every return of the link under a held lease -(`Responder::link_returned`): §8 asks for both steps there, probing first, and -only the announcing is built. What a returning link multiplies is the -occasions: each one is two more multicasts of the unprobed record, a second -apart, so two machines named alike each flush the other's record whenever -either's link returns, where on what ships it happens once an address. - -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; probing -is the next stage of `toyos-mdns`, not part of the node's stages. - -## Exit condition - -`toyos-mdns` probes for its name before it announces it, at start-up and when -its link returns (§8, §8.1), defends it (§9), and gives it up, saying so in its -log, when another host holds it; its tests hold the three probes 250 ms apart, -the announcement only after them, a conflicting answer under the probe taking -the name away, and a link's return probing again before it announces. Or each -machine's name is its own, and a test with two guests on one network finds each -by its name. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 6af94f946c7..891900dbc4d 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 node's name is not probed for or defended (RFC 6762 §8.1, §9): `issues/netstack-answers-for-its-name-without-claiming-it.md`, which is `toyos-mdns`'s and so the node's and the shipped netstack's alike. A link that returns under a held lease is announced on (§8.3, `a_held_name_is_announced_again_when_the_link_returns`, `userland/netstack/node/tests/name.rs`) and not probed on, which §8 asks first, so each return is two more multicasts of an unprobed record. Exit: that issue's. +- 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. Netstack on smoltcp tells its responder of no link's return: `userland/netstack/src/main.rs` keeps a lease across a flap and says nothing to `mdns`, so there a returning link is neither probed nor announced on, and a lost name is probed for again only with an address after none. The node hands the responder a probe's delay and no word that the link is down: a probe that falls due then is counted `node.name-unsent` and held sent, so the responder says the name claimed on no wire, until the link's return probes for it (`what_the_name_is_owed_with_the_link_down_is_counted_and_the_links_return_probes_for_it`, `userland/netstack/node/tests/name.rs`). What became of the name leaves the node by `Node::name_event`, a second way out beside `Node::drain_events`. A response is read however it was addressed, where §6 reads a unicast one only within two seconds of a question that asked for one: the caller does not say how a message arrived. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; netd runs on the node, whose `Node::link` tells the name of a link's return; the responder is told the link went down and probes for nothing on it; the name's line leaves by `Node::drain_events`; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name. - A query [ip] refuses at the transmit opportunity that would have carried it, its next hop held failed or its table full, is reported to its socket inside `Node::transmit`, which carries no lookup on: the resolver reads the report at the node's next `receive`, `fire` or `link`, which at the latest is the end of the query's wait, where a query [udp] refuses in the resolver's own call, or one reported in a frame or at a deadline, ends its wait at once. `a_query_ip_refuses_at_its_transmit_opportunity_is_read_at_the_nodes_next_wake` holds it as it stands. It is no regression: what ships waits every such query out. Until that read, an ICMP error that quotes the query's addresses and ports takes [ip]'s word's place on the socket, [udp] holding one pending error a socket, the latest: the query is then waited out and not let go, which costs the early end and never an answer. Owner: the slice in which datagram senders wait on the hop (`Node::transmit` is the node's `lib.rs`). Exit: the node carries its lookups on after the stack's transmit, or [udp] refuses in the call a datagram whose next hop [ip] holds failed, so that no report waits on a socket to be displaced, and that test finds its lookup ended at the 4 s it started. - [ip]'s queue for a next hop it is resolving is every sender's: `PENDING_PER_NEIGHBOUR` is 16, a stopgap which the node's resolver asserts is no fewer than `toyos_dns::MAX_LOOKUPS`, so every held lookup's first query waits there whole, but a datagram a client sends to the same next hop in that window, which for everything off the link is the router, takes the oldest one's place (`nb.pending-overflow`, RFC 4861 §7.2.2), and a query lost so costs its lookup a `WAIT_MS`; smoltcp kept each datagram in its own socket until the neighbour answered. `a_clients_datagram_behind_the_burst_takes_the_oldest_querys_place_in_ips_queue` holds it as it stands. It is a regression against what ships for more than 16 datagrams at one next hop not yet resolved, and would ship at the move; and it sizes a limit of [ip]'s by a constant of the resolver's, which leaves four unresolved next hops filling `PENDING_TOTAL` where eight did. Owner: the slice in which datagram senders wait on the hop, where [udp]'s datagram calls already go through `Stack` and `Shard`. Its design: [udp]'s `serve` asks [ip] the next-hop question before it hands a datagram over, as [tcp]'s exit does with `Hop` (NUD-25, IP-D5); a socket whose oldest datagram's hop is pending keeps it in its own queue, which `TX_DATAGRAMS` and `TX_BYTES` already bound and refuse in the call, and is offered again when the hop resolves and told when it fails; [ip] then queues only its own datagrams. Exit, before the move: that test finds every query on the wire once the resolver answers ARP; `PENDING_PER_NEIGHBOUR` is `limits.ip.nud.pending_per_neighbour`'s 8 again; and the resolver's compile-time assertion, the change to `s_ip_nud_004_overflow_drops_the_oldest` and stage 3's departure line about it are deleted. - `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 66b16f21031..e542cdbd58f 100644 --- a/userland/netstack/mdns/src/lib.rs +++ b/userland/netstack/mdns/src/lib.rs @@ -8,7 +8,62 @@ //! `ANY`; every other question is somebody else's and gets silence, which is //! what RFC 6762 §6 asks of a responder that has no answer. //! -//! Where the answer goes follows the question (§5.4, §6.7): +//! **The name is claimed before it is used** (§8: "Probing (Section 8.1) and +//! Announcing (Section 8.3)"), on every address that comes after none and +//! whenever the caller says its link came back ([`Responder::link_returned`]): +//! +//! - §8.1: after a delay the caller draws, uniform in 0 to 250 ms, three +//! probes 250 ms apart, each the question `ANY` for the name with the +//! unicast-response bit and the proposed record in the Authority Section; +//! 250 ms after the third the name is held, and only then is the record +//! announced (§8.3) or answered with. A new address under a held name is +//! announced and not probed for (§8.4). +//! - §8.1: "if any conflicting Multicast DNS response is received, then the +//! probing host MUST defer to the existing host": the name is lost +//! ([`Event::Lost`]). A response conflicts when it carries, in any section, a +//! record of this name of any type other than this record itself (§9: +//! "resource records with identical rdata are never considered +//! inconsistent"); one "received *before* the first probe packet is sent MUST +//! be silently ignored". +//! - §8.2: another host's probe for the name heard under ours is compared +//! with ours, class, then type, then data as unsigned bytes, "and the +//! lexicographically later data wins": the earlier "defers to the winning +//! host by waiting one second, and then begins probing for this record +//! again". +//! - §9: a conflicting response to a held name means it "MUST immediately +//! reset its conflicted unique record to probing state", and what the probe +//! then hears decides it. A held name is defended by answering: another +//! host's probe for it is a question like any other, multicast as soon as +//! 250 ms after the record's last multicast (§6) or unicast at once. +//! +//! **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 +//! the link to return or an address after none. 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. +//! +//! **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 and +//! the times of its last [`CONFLICTS`] conflicts. +//! - One conflict costs at most three probes and two announcements. +//! - §8.1: "If fifteen conflicts occur within any ten-second period, then the +//! host MUST wait at least five seconds before each successive additional +//! probe attempt", for as long as conflicts keep coming less than ten +//! seconds apart. +//! - Between two events that are no message read here, the link's return or +//! an address after none, a name is contested [`CONFLICTS`] times at most: +//! the conflict that makes it so loses the name ([`Lost::Contested`]), so +//! no peer keeps this responder probing, and the most its messages have +//! cost is that many rounds of three probes and two announcements. +//! - A lost name sends nothing and reads nothing until one of those two +//! events. An address after none is whoever leases this machine its address +//! to cause, which decides what the record says anyway. +//! +//! Where an answer goes follows the question (§5.4, §6.7): //! //! - a query from port 5353 is a full responder's: the answer is multicast to //! the group, unless it set the unicast-response bit, which asks for it back @@ -17,18 +72,21 @@ //! back to its address and port, carrying its ID and its question, with no //! cache-flush bit and a TTL of at most ten seconds. //! -//! A query whose source is not on this link is ignored (§11), and the record -//! is multicast at most once a second (§6) — announcements included — with a -//! query inside that second answered when it ends ([`Responder`]). +//! A message whose source is not on this link is ignored (§11), as is one +//! with an opcode or a response code other than zero (§18.3, §18.11), and the +//! record is multicast at most once a second (§6) — announcements included — +//! with a query inside that second answered when it ends ([`Responder`]). //! -//! **Not implemented, and so not claimed:** probing for the name before using -//! it (§8.1) and defending it against another host's (§9). Two machines named -//! alike both answer, and a resolver may see either. +//! **Not implemented, and so not claimed:** §6's rule that a unicast response +//! is read only within two seconds of a question that asked for one. The +//! caller does not say how a message was addressed, so a response is read +//! however it came; a host that can send one can multicast it too. //! -//! A question is read off the wire by `toyos-dns`, the reader a resolver's +//! A message is read off the wire by `toyos-dns`, the reader a resolver's //! reply is read by. //! -//! Pure: `core` and `alloc`, no `unsafe`, no I/O. +//! Pure: `core` and `alloc`, no `unsafe`, no I/O, no clock and no randomness +//! of its own. #![no_std] #![forbid(unsafe_code)] @@ -38,7 +96,11 @@ extern crate alloc; #[cfg(test)] extern crate std; +#[cfg(test)] +mod tests; + use alloc::vec::Vec; +use core::cmp::Ordering; use toyos_dns::{name_at, u16_at, Name}; @@ -59,6 +121,10 @@ pub const TTL: u32 = 120; /// §6.7: an answer to a legacy resolver carries a TTL of at most ten seconds. pub const LEGACY_TTL: u32 = 10; +/// §8.1: "If fifteen conflicts occur within any ten-second period". It is +/// also how often a name is contested before it is given up. +pub const CONFLICTS: usize = 15; + /// The domain every name here is under (§3). const LOCAL: &[u8] = b"local"; @@ -69,14 +135,19 @@ const CLASS_IN: u16 = 1; const UNICAST_RESPONSE: u16 = 0x8000; /// The top bit of an answer's class: every cached record of this name is /// replaced by this one (§10.2). Set on every multicast answer, because this -/// responder is the only owner of the name it answers for. +/// responder answers only with a name it holds. const CACHE_FLUSH: u16 = 0x8000; /// `QR` and `AA`: a response, authoritative (§18.2, §18.4). const RESPONSE_FLAGS: u16 = 0x8400; -/// §18.3: the opcode of any query this answers is zero. +/// §18.3, §18.11: the opcode and the response code of any message read here +/// are zero. const OPCODE_MASK: u16 = 0x7800; +const RCODE_MASK: u16 = 0x000f; const QR: u16 = 0x8000; +/// A message's header (RFC 1035 §4.1.1): its sections begin behind it. +const HEADER: usize = 12; + /// The longest label (RFC 1035 §2.3.4). const MAX_LABEL: usize = 63; @@ -124,7 +195,7 @@ impl<'a> Host<'a> { out.push(0); } - /// Whether `name`, as a question spelled it, is this host's, whatever the + /// Whether `name`, as a message spelled it, is this host's, whatever the /// case of its letters. fn is(&self, name: &Name) -> bool { let mut ours = Vec::with_capacity(self.0.len() + LOCAL.len() + 3); @@ -133,16 +204,30 @@ impl<'a> Host<'a> { } } -/// The unsolicited answer a responder sends when its address is new (§8.3). -pub fn announcement(host: Host, addr: [u8; 4]) -> Vec { - let mut out = header(0, RESPONSE_FLAGS, 0, 1); +/// The unsolicited answer a responder sends for a name it has claimed (§8.3). +fn announcement(host: Host, addr: [u8; 4]) -> Vec { + let mut out = header(0, RESPONSE_FLAGS, [0, 1, 0]); answer_record(&mut out, host, addr, CLASS_IN | CACHE_FLUSH, TTL); out } -/// Where a query came from: the source address and port of its packet. +/// §8.1: "All probe queries SHOULD be done using the desired resource record +/// name and class (usually class 1, "Internet"), and query type "ANY" (255)", +/// "as "QU" questions with the unicast-response bit set". §8.2: "each host +/// populates the query message's Authority Section with the record or records +/// with the rdata that it would be proposing to use". +fn probe(host: Host, addr: [u8; 4]) -> Vec { + let mut out = header(0, 0, [1, 0, 1]); + host.put_name(&mut out); + out.extend_from_slice(&TYPE_ANY.to_be_bytes()); + out.extend_from_slice(&(CLASS_IN | UNICAST_RESPONSE).to_be_bytes()); + answer_record(&mut out, host, addr, CLASS_IN, TTL); + out +} + +/// Where a message came from: the source address and port of its packet. #[derive(Clone, Copy, Debug)] -pub struct Asker { +pub struct Source { pub addr: [u8; 4], pub port: u16, } @@ -156,7 +241,7 @@ pub struct Link { } impl Link { - /// Whether a query from `addr` is not from off this link, which is what + /// Whether a message from `addr` is not from off this link, which is what /// §11 refuses: in this subnet, or link-local (RFC 3927). A loopback /// source is off every link (RFC 1122 §3.2.1.3: a host silently discards /// a datagram carrying one), so it is refused wherever it arrived. @@ -167,27 +252,149 @@ impl Link { } } +/// What became of the name, for its owner's log. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Event { + /// Probing ended with no host answering: the name is this host's, and is + /// announced and answered with from here on. + Claimed, + /// The name is not this host's, and nothing is announced or answered + /// until the link returns or an address comes after none. + Lost(Lost), +} + +/// Why the name was lost. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Lost { + /// §8.1: another host answered for the name under the probe. + Answered, + /// The name was contested [`CONFLICTS`] times since the link last + /// returned or an address came after none. + Contested, +} + +/// §8.1: "the host should first wait for a short random delay time, uniformly +/// distributed in the range 0-250 ms." +const PROBE_DELAY_MS: u32 = 250; + +/// §8.1: "250 ms after the first query, the host should send a second; then, +/// 250 ms after that, a third. If, by 250 ms after the third probe, no +/// conflicting Multicast DNS responses have been received, the host may move +/// to the next step, announcing." +const PROBE_EVERY_MS: u64 = 250; +const PROBES: u8 = 3; + +/// §8.2: the loser of a simultaneous probe "defers to the winning host by +/// waiting one second, and then begins probing for this record again." +const DEFER_MS: u64 = 1_000; + +/// §8.1: fifteen conflicts "within any ten-second period", and then "at least +/// five seconds before each successive additional probe attempt." +const CONFLICT_WINDOW_MS: u64 = 10_000; +const LIMITED_WAIT_MS: u64 = 5_000; + /// §6: a record is multicast on an interface at most once a second. const GROUP_EVERY_MS: u64 = 1_000; +/// §6: "when responding via multicast to a probe, a Multicast DNS responder +/// is only required to delay its transmission as necessary to ensure an +/// interval of at least 250 ms since the last time the record was multicast +/// on that interface." +const PROBE_ANSWER_EVERY_MS: u64 = 250; + /// §8.3: "The Multicast DNS responder MUST send at least two unsolicited /// responses, one second apart." const ANNOUNCE_AGAIN_MS: u64 = 1_000; +/// Whose the name is, read only while an address is held. +#[derive(Clone, Copy, Debug)] +enum Claim { + /// Probing is owed since `since_ms`: the next [`Responder::on`] draws its + /// delay. + Owed { since_ms: u64 }, + /// `sent` probes have left; at `at_ms` the next does, or after the third + /// the name is held. A message conflicts only once `sent` is not zero. + Probing { sent: u8, at_ms: u64 }, + Held, + Lost, +} + +/// The conflicts over the name. +#[derive(Debug)] +struct Contest { + /// The times of the last [`CONFLICTS`] conflicts, the oldest at `next`. + times: [u64; CONFLICTS], + next: usize, + /// How many of `times` came each within [`CONFLICT_WINDOW_MS`] of the one + /// before, up to now. + counted: usize, + last_ms: Option, + /// [`CONFLICTS`] of them have fallen within one window. + limited: bool, + /// The conflicts since the link returned or an address came after none. + since_return: usize, +} + +impl Contest { + const fn new() -> Self { + Self { times: [0; CONFLICTS], next: 0, counted: 0, last_ms: None, limited: false, since_return: 0 } + } + + /// A conflict at `now_ms`. Returns whether the name has now been contested + /// [`CONFLICTS`] times since the link returned or an address came after none. + fn conflict(&mut self, now_ms: u64) -> bool { + if !self.running(now_ms) { + (self.counted, self.limited) = (0, false); + } + self.last_ms = Some(now_ms); + self.times[self.next] = now_ms; + self.next = (self.next + 1) % CONFLICTS; + self.counted = (self.counted + 1).min(CONFLICTS); + if self.counted == CONFLICTS && now_ms.saturating_sub(self.times[self.next]) <= CONFLICT_WINDOW_MS { + self.limited = true; + } + self.since_return = (self.since_return + 1).min(CONFLICTS); + self.since_return == CONFLICTS + } + + /// Whether a conflict at `now_ms` would be one more of this contest. + fn running(&self, now_ms: u64) -> bool { + self.last_ms.is_some_and(|last| now_ms.saturating_sub(last) <= CONFLICT_WINDOW_MS) + } + + /// What §8.1's limit puts before a probe attempt scheduled at `now_ms`. + fn wait_ms(&self, now_ms: u64) -> u64 { + if self.limited && self.running(now_ms) { + LIMITED_WAIT_MS + } else { + 0 + } + } +} + /// This host's one record on its link, on the caller's monotonic clock in -/// milliseconds: announced on every new address and whenever the link comes -/// back ([`Responder::link_returned`]), each time 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`]. +/// milliseconds: probed for on every address after none and whenever the link +/// comes back ([`Responder::link_returned`]), 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 message counts as sent once [`Responder::on`] has handed it over. A +/// caller whose link was down for it says so by [`Responder::link_returned`] +/// when the link is back, which probes again. #[derive(Debug)] pub struct Responder<'a> { host: Host<'a>, - /// The link the record was last announced on. + /// The link the address is held on. link: Option, + claim: Claim, + contest: Contest, + /// The latest change of the claim nobody took. + event: Option, last_group_ms: Option, - /// When the record is next owed to the group: an announcement, or an + /// When a held record is next owed to the group: an announcement, or an /// answer §6 delayed. One multicast of the record answers every query it /// held. owed_ms: Option, @@ -198,25 +405,55 @@ pub struct Responder<'a> { impl<'a> Responder<'a> { pub const fn new(host: Host<'a>) -> Self { - Self { host, link: None, last_group_ms: None, owed_ms: None, again: false } + Self { + host, + link: None, + claim: Claim::Lost, + contest: Contest::new(), + event: 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 - /// address: the multicast the record is owed now, if any — an - /// announcement of a new address or of a link that came back, its - /// second, or an answer §6 delayed. - pub fn on(&mut self, link: Option, now_ms: u64) -> Option> { + /// address: the multicast owed now, if any — a probe, an announcement of + /// a name just claimed or of a new address under a held one (§8.4), its + /// second, or an answer §6 delayed. `delay` is drawn once when this call + /// starts probing, and not otherwise. + pub fn on(&mut self, link: Option, now_ms: u64, delay: impl FnOnce() -> u32) -> Option> { let Some(link) = link else { self.link = None; self.owed_ms = None; return None; }; - if self.link.is_none_or(|was| was.addr != link.addr) { - self.owed_ms = Some(now_ms); + match self.link.replace(link) { + None => self.begin(now_ms), + Some(was) if was.addr != link.addr && matches!(self.claim, Claim::Held) => { + self.owed_ms = Some(now_ms); + self.again = true; + } + Some(_) => {} + } + if let Claim::Owed { .. } = self.claim { + let wait = u64::from(delay() % (PROBE_DELAY_MS + 1)) + self.contest.wait_ms(now_ms); + self.claim = Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) }; + } + if let Claim::Probing { sent, at_ms } = self.claim { + if now_ms < at_ms { + return None; + } + if sent < PROBES { + self.claim = Claim::Probing { sent: sent + 1, at_ms: now_ms.saturating_add(PROBE_EVERY_MS) }; + return Some(probe(self.host, link.addr)); + } + self.claim = Claim::Held; + self.event = Some(Event::Claimed); + self.owed_ms = Some(self.group_free_ms(now_ms, GROUP_EVERY_MS)); self.again = true; } - self.link = Some(link); - if !self.owed_ms.is_some_and(|at| now_ms >= at) { + if !matches!(self.claim, Claim::Held) || !self.owed_ms.is_some_and(|at| now_ms >= at) { return None; } self.owed_ms = core::mem::take(&mut self.again).then(|| now_ms.saturating_add(ANNOUNCE_AGAIN_MS)); @@ -224,65 +461,141 @@ impl<'a> Responder<'a> { Some(announcement(self.host, link.addr)) } - /// The link this host's address is on came back at `now_ms`: its - /// connectivity may have changed, so the record is announced again as a - /// new address's is (§8), the first time as soon as §6 lets the record - /// be multicast. With no address held nothing is owed: the one that - /// comes is new. + /// The link this host's address is on came back at `now_ms`, or the + /// caller has another reason to think its connectivity changed (§8): the + /// name is probed for again, whoever held it, and announced only if that + /// probe goes unanswered. With no address held nothing is owed: the one + /// that comes is probed on. pub fn link_returned(&mut self, now_ms: u64) { if self.link.is_some() { - self.owed_ms = Some(self.group_free_ms(now_ms)); - self.again = true; + self.begin(now_ms); } } - /// When §6 next lets the record be multicast: `now_ms`, or the end of - /// the second its last multicast began. - fn group_free_ms(&self, now_ms: u64) -> u64 { - self.last_group_ms.map_or(now_ms, |last| last.saturating_add(GROUP_EVERY_MS)).max(now_ms) + /// Probing is owed for a reason that is no message read here. + fn begin(&mut self, now_ms: u64) { + self.contest.since_return = 0; + self.release(Claim::Owed { since_ms: now_ms }); + } + + /// The name is no longer held: nothing of the record is owed. + fn release(&mut self, claim: Claim) { + self.claim = claim; + self.owed_ms = None; + self.again = false; + } + + fn lose(&mut self, why: Lost) { + self.release(Claim::Lost); + self.event = Some(Event::Lost(why)); + } + + /// When the record was last multicast plus `every_ms`, or `now_ms` if + /// that is later: when §6 next lets it be multicast. + fn group_free_ms(&self, now_ms: u64, every_ms: u64) -> u64 { + self.last_group_ms.map_or(now_ms, |last| last.saturating_add(every_ms)).max(now_ms) } - /// When the record is next owed to the group, on the caller's clock. + /// When [`Self::on`] is next owed a call, on the caller's clock. pub fn owed_at(&self) -> Option { - self.owed_ms + self.link?; + match self.claim { + Claim::Owed { since_ms } => Some(since_ms), + Claim::Probing { at_ms, .. } => Some(at_ms), + Claim::Held => self.owed_ms, + Claim::Lost => None, + } + } + + /// The latest of what became of the name since the last call. + pub fn take_event(&mut self) -> Option { + self.event.take() } - /// The answer `query`, from `asker`, is owed now, or `None` where it asks - /// nothing this host answers — no address held, a source off the link, a - /// response, a query with a nonzero opcode, a question for another name or - /// type, or bytes that are not a message at all — or where §6 delays it - /// to [`Responder::owed_at`]. - pub fn answer(&mut self, query: &[u8], asker: Asker, now_ms: u64) -> Option { + /// `message` arrived from `from` at `now_ms`: the answer it is owed now, + /// if it is a query for a held name; a response or another host's probe + /// is read for what it says of the name and answered nothing. `None` too + /// where it asks nothing this host answers — no address held, a source + /// off the link, an opcode or a response code other than zero, a question + /// for another name or type, or bytes that are not a message at all — or + /// where §6 delays the answer to [`Responder::owed_at`]. + pub fn heard(&mut self, message: &[u8], from: Source, now_ms: u64) -> Option { let link = self.link?; - if !link.holds(asker.addr) { + if !link.holds(from.addr) { return None; } - let host = self.host; - let id = u16_at(query, 0).ok()?; - let flags = u16_at(query, 2).ok()?; - if flags & (QR | OPCODE_MASK) != 0 { + let id = u16_at(message, 0).ok()?; + let flags = u16_at(message, 2).ok()?; + if flags & (OPCODE_MASK | RCODE_MASK) != 0 { + return None; + } + if flags & QR != 0 { + self.response(message, from, link, now_ms); return None; } - let questions = u16_at(query, 4).ok()?; - let mut at = 12; - let mut asked = None; - for _ in 0..questions { - let (name, after) = name_at(query, at).ok()?; - let kind = u16_at(query, after).ok()?; - let class = u16_at(query, after + 2).ok()?; - at = after + 4; - if host.is(&name) - && matches!(kind, TYPE_A | TYPE_ANY) - && class & !UNICAST_RESPONSE == CLASS_IN - { - asked = Some(class & UNICAST_RESPONSE != 0); - break; + match self.claim { + Claim::Held => self.answer(message, id, from, link, now_ms), + Claim::Probing { sent, .. } if sent > 0 => { + self.rival(message, link, now_ms); + None } + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => 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). + fn response(&mut self, message: &[u8], from: Source, link: Link, now_ms: u64) { + let probing = match self.claim { + Claim::Probing { sent, .. } if sent > 0 => true, + Claim::Held => false, + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => return, + }; + if from.port != PORT || !conflicts(message, self.host, link.addr).unwrap_or(false) { + return; + } + let over = self.contest.conflict(now_ms); + if probing { + self.lose(Lost::Answered); + } else if over { + self.lose(Lost::Contested); + } else { + self.release(Claim::Owed { since_ms: now_ms }); + } + } + + /// A query heard under this host's probe: §8.2's comparison if it is + /// another host's probe for the name. + 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 }; + // §8.2.1: equal as far as this host's one record goes, "the list with + // records remaining is deemed to have won"; with none remaining "there + // is, in fact, no conflict." + let later = match theirs.cmp(&(CLASS_IN, TYPE_A, &link.addr[..])) { + Ordering::Greater => true, + Ordering::Equal => more, + Ordering::Less => false, + }; + if !later { + return; } - let unicast = asked?; - if asker.port != PORT { + if self.contest.conflict(now_ms) { + self.lose(Lost::Contested); + } else { + let wait = DEFER_MS + self.contest.wait_ms(now_ms); + self.release(Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) }); + } + } + + /// A query for a held name. + fn answer(&mut self, query: &[u8], id: u16, from: Source, link: Link, now_ms: u64) -> Option { + let host = self.host; + let (kind, unicast) = asked(query, host)?; + if from.port != PORT { // §6.7: the asker's ID and question, and a record no cache keeps long. - let mut out = header(id, RESPONSE_FLAGS, 1, 1); + let mut out = header(id, RESPONSE_FLAGS, [1, 1, 0]); host.put_name(&mut out); out.extend_from_slice(&TYPE_A.to_be_bytes()); out.extend_from_slice(&CLASS_IN.to_be_bytes()); @@ -290,299 +603,131 @@ impl<'a> Responder<'a> { return Some(Answer { to: To::Asker, bytes: out }); } if !unicast { - let free_ms = self.group_free_ms(now_ms); + // §6: "A probe query can be distinguished from a normal query by + // the fact that a probe query contains a proposed record in the + // Authority Section that answers the question". + let every_ms = if proposed(query, host, kind).is_some() { PROBE_ANSWER_EVERY_MS } else { GROUP_EVERY_MS }; + let free_ms = self.group_free_ms(now_ms, every_ms); if now_ms < free_ms { - self.owed_ms = Some(free_ms); + self.owed_ms = Some(self.owed_ms.map_or(free_ms, |owed| owed.min(free_ms))); return None; } self.last_group_ms = Some(now_ms); } - let mut out = header(0, RESPONSE_FLAGS, 0, 1); - answer_record(&mut out, host, link.addr, CLASS_IN | CACHE_FLUSH, TTL); - Some(Answer { to: if unicast { To::Asker } else { To::Group }, bytes: out }) + Some(Answer { to: if unicast { To::Asker } else { To::Group }, bytes: announcement(host, link.addr) }) } } -fn header(id: u16, flags: u16, questions: u16, answers: u16) -> Vec { - let mut out = Vec::with_capacity(64); - for word in [id, flags, questions, answers, 0, 0] { - out.extend_from_slice(&word.to_be_bytes()); +/// The first question of `query` this host's record answers: its type, and +/// whether it asks for a unicast response (§5.4). +fn asked(query: &[u8], host: Host) -> Option<(u16, bool)> { + let mut at = HEADER; + for _ in 0..u16_at(query, 4).ok()? { + let (name, after) = name_at(query, at).ok()?; + let kind = u16_at(query, after).ok()?; + let class = u16_at(query, after + 2).ok()?; + at = after + 4; + if host.is(&name) && matches!(kind, TYPE_A | TYPE_ANY) && class & !UNICAST_RESPONSE == CLASS_IN { + return Some((kind, class & UNICAST_RESPONSE != 0)); + } } - out + None } -fn answer_record(out: &mut Vec, host: Host, addr: [u8; 4], class: u16, ttl: u32) { - host.put_name(out); - out.extend_from_slice(&TYPE_A.to_be_bytes()); - out.extend_from_slice(&class.to_be_bytes()); - out.extend_from_slice(&ttl.to_be_bytes()); - out.extend_from_slice(&4u16.to_be_bytes()); - out.extend_from_slice(&addr); +/// One resource record (RFC 1035 §4.1.3) and where the bytes after it begin. +struct Record<'m> { + name: Name, + kind: u16, + class: u16, + data: &'m [u8], + end: usize, } -#[cfg(test)] -mod tests { - use super::*; - use std::vec; - - const ADDR: [u8; 4] = [192, 168, 1, 49]; - - fn host() -> Host<'static> { - Host::new("toyos-t14").expect("a label") - } - - const LINK: Link = Link { addr: ADDR, prefix: 24 }; - const NEIGHBOUR: [u8; 4] = [192, 168, 1, 7]; - - /// A responder that announced [`LINK`]'s address at 0. - fn announced() -> Responder<'static> { - let mut r = Responder::new(host()); - assert!(r.on(Some(LINK), 0).is_some(), "a new address is announced"); - r - } - - /// One query from a neighbour on the link, long after the announcement. - fn ask(query: &[u8], port: u16) -> Option { - announced().answer(query, Asker { addr: NEIGHBOUR, port }, 10_000) - } - - /// A query as RFC 1035 §4.1 lays one out, spelled byte by byte here rather - /// than by this crate's own writer. - fn query(id: u16, name: &[&str], kind: u16, class: u16) -> Vec { - let mut q = vec![(id >> 8) as u8, id as u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; - for label in name { - q.push(label.len() as u8); - q.extend_from_slice(label.as_bytes()); - } - q.push(0); - q.extend_from_slice(&kind.to_be_bytes()); - q.extend_from_slice(&class.to_be_bytes()); - q - } - - /// The answer a multicast query is owed, byte for byte: RFC 6762's header - /// (§18: ID zero, `QR|AA`, no question), one `A` record with the - /// cache-flush bit and a 120 s TTL. - #[test] - fn a_query_for_this_name_is_answered_to_the_group() { - let got = ask(&query(0, &["toyos-t14", "local"], 1, 1), PORT) - .expect("an answer"); - assert_eq!(got.to, To::Group); - let mut want = vec![0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]; - want.extend_from_slice(b"\x09toyos-t14\x05local\x00"); - want.extend_from_slice(&[0, 1, 0x80, 1, 0, 0, 0, 120, 0, 4, 192, 168, 1, 49]); - assert_eq!(got.bytes, want); - assert_eq!(announcement(host(), ADDR), want, "the announcement is the same answer, unasked"); - } - - #[test] - fn a_name_is_matched_whatever_its_case_and_any_asks_for_it_too() { - let q = query(0, &["ToyOS-T14", "LOCAL"], 255, 1); - assert!(ask(&q, PORT).is_some()); - } - - #[test] - fn the_unicast_bit_sends_the_answer_back_to_the_asker() { - let got = ask(&query(0, &["toyos-t14", "local"], 1, 0x8001), PORT) - .expect("an answer"); - assert_eq!(got.to, To::Asker); - } - - /// §6.7: a resolver that is not a responder gets its ID, its question and - /// a short TTL, and no cache-flush bit. - #[test] - fn a_legacy_resolver_gets_its_id_its_question_and_a_short_ttl() { - let got = ask(&query(0xBEEF, &["toyos-t14", "local"], 1, 1), 53_000) - .expect("an answer"); - assert_eq!(got.to, To::Asker); - let mut want = vec![0xBE, 0xEF, 0x84, 0x00, 0, 1, 0, 1, 0, 0, 0, 0]; - want.extend_from_slice(b"\x09toyos-t14\x05local\x00\x00\x01\x00\x01"); - want.extend_from_slice(b"\x09toyos-t14\x05local\x00"); - want.extend_from_slice(&[0, 1, 0, 1, 0, 0, 0, 10, 0, 4, 192, 168, 1, 49]); - assert_eq!(got.bytes, want); - } - - #[test] - fn a_question_that_is_not_this_hosts_is_not_answered() { - for (name, kind, class) in [ - (&["other", "local"][..], 1, 1), - (&["toyos-t14", "lan"][..], 1, 1), - (&["toyos-t14"][..], 1, 1), - (&["toyos-t14", "local", "x"][..], 1, 1), - (&["toyos-t14", "local"][..], 28, 1), - (&["toyos-t14", "local"][..], 1, 3), - ] { - assert_eq!(ask(&query(0, name, kind, class), PORT), None, "{name:?}"); - } - let mut response = query(0, &["toyos-t14", "local"], 1, 1); - response[2] = 0x84; - assert_eq!(ask(&response, PORT), None, "a response is not a question"); - let mut opcode = query(0, &["toyos-t14", "local"], 1, 1); - opcode[2] = 0x08; - assert_eq!(ask(&opcode, PORT), None, "a nonzero opcode is not a query"); - } +fn record_at(message: &[u8], at: usize) -> Option> { + let (name, after) = name_at(message, at).ok()?; + let kind = u16_at(message, after).ok()?; + let class = u16_at(message, after + 2).ok()?; + let end = after + 10 + usize::from(u16_at(message, after + 8).ok()?); + Some(Record { name, kind, class, data: message.get(after + 10..end)?, end }) +} - /// A second question may name the first's labels by pointer (RFC 1035 - /// §4.1.4), as a responder asking several things at once does. - #[test] - fn a_question_spelled_by_a_pointer_is_read() { - let mut q = query(0, &["other", "local"], 1, 1); - q[5] = 2; - // `toyos-t14`, then a pointer to `local` at offset 12 + 6. - q.extend_from_slice(b"\x09toyos-t14\xC0\x12\x00\x01\x00\x01"); - assert_eq!(ask(&q, PORT).map(|a| a.to), Some(To::Group)); +/// Where the records of `message` begin: behind its questions. +fn records_at(message: &[u8]) -> Option { + let mut at = HEADER; + for _ in 0..u16_at(message, 4).ok()? { + at = name_at(message, at).ok()?.1 + 4; } + Some(at) +} - /// Nothing a peer sends can hold the parser: a short message, a label past - /// the end, a pointer forwards or to itself are each no question. - #[test] - fn a_malformed_message_is_no_question() { - let whole = query(0, &["toyos-t14", "local"], 1, 1); - for cut in 0..whole.len() { - assert_eq!(ask(&whole[..cut], PORT), None, "cut at {cut}"); +/// Whether `response` carries, in any of its three record sections (§9), a +/// record of `host`'s name other than its `A` record for `addr`. `None` where +/// it stops being a message before one is found. +fn conflicts(response: &[u8], host: Host, addr: [u8; 4]) -> Option { + let mut at = records_at(response)?; + let records = [6, 8, 10].into_iter().map(|count| u16_at(response, count).map(u32::from)); + for _ in 0..records.sum::>().ok()? { + let record = record_at(response, at)?; + at = record.end; + let ours = record.kind == TYPE_A && record.class & !CACHE_FLUSH == CLASS_IN && record.data == addr; + if host.is(&record.name) && !ours { + return Some(true); } - let mut forward = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; - forward.extend_from_slice(&[0xC0, 12, 0, 1, 0, 1]); - assert_eq!(ask(&forward, PORT), None, "a pointer to itself"); - let mut ahead = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; - ahead.extend_from_slice(&[0xC0, 20, 0, 1, 0, 1, 0, 0]); - assert_eq!(ask(&ahead, PORT), None, "a pointer forwards"); - let mut reserved = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; - reserved.extend_from_slice(&[0x40, 0, 0, 1, 0, 1]); - assert_eq!(ask(&reserved, PORT), None, "a reserved label type"); - } - - /// RFC 6762 §6: "a Multicast DNS responder MUST NOT (except in the one - /// special case of answering probe queries) multicast a record on a given - /// interface until at least one second has elapsed since the last time that - /// record was multicast on that particular interface." An announcement is a - /// multicast of the record too. A query inside the second is answered when - /// it ends, by one multicast of the record, and not before or never. - #[test] - fn the_record_is_multicast_at_most_once_a_second_and_a_query_inside_it_waits() { - let q = query(0, &["toyos-t14", "local"], 1, 1); - let from = Asker { addr: NEIGHBOUR, port: PORT }; - let mut r = Responder::new(host()); - let record = r.on(Some(LINK), 20_000).expect("a new address is announced"); - assert_eq!(r.owed_at(), Some(21_000), "§8.3: the second announcement"); - assert_eq!(r.answer(&q, from, 20_500), None, "the record was announced 500 ms ago"); - let legacy = Asker { addr: NEIGHBOUR, port: 53_000 }; - assert_eq!( - r.answer(&q, legacy, 20_500).map(|a| a.to), - Some(To::Asker), - "a unicast answer is no multicast of the record" - ); - assert_eq!(r.on(Some(LINK), 20_999), None); - assert_eq!(r.on(Some(LINK), 21_000).as_ref(), Some(&record), "announced again, which answers the query"); - assert_eq!(r.owed_at(), None); - - assert_eq!(r.answer(&q, from, 21_400), None, "the record was multicast 400 ms ago"); - assert_eq!(r.answer(&q, from, 21_600), None); - assert_eq!(r.owed_at(), Some(22_000), "both queries are owed one multicast when the second ends"); - assert_eq!(r.on(Some(LINK), 21_999), None); - assert_eq!(r.on(Some(LINK), 22_000).as_ref(), Some(&record)); - assert_eq!(r.on(Some(LINK), 22_001), None, "owed once"); - - let later = r.answer(&q, from, 23_000).map(|a| a.to); - assert_eq!(later, Some(To::Group), "a second has passed"); - assert_eq!(r.answer(&q, from, 23_999), None); - assert_eq!(r.owed_at(), Some(24_000)); - } - - /// Nothing is answered without an address; an address after none, or a - /// different one, is new and announced at once with what it is. - #[test] - fn an_address_is_announced_when_new_and_none_is_answered_for() { - let q = query(0, &["toyos-t14", "local"], 1, 1); - let from = Asker { addr: NEIGHBOUR, port: 53_000 }; - let mut r = Responder::new(host()); - assert_eq!(r.answer(&q, from, 0), None, "no address yet"); - assert!(r.on(Some(LINK), 0).is_some()); - assert_eq!(r.on(Some(LINK), 500), None); - assert_eq!(r.on(None, 600), None); - assert_eq!(r.owed_at(), None, "nothing is owed for an address no longer held"); - assert_eq!(r.answer(&q, from, 600), None, "no address any more"); - assert!(r.on(Some(LINK), 700).is_some(), "an address after none is new"); - let moved = Link { addr: [192, 168, 1, 50], prefix: 24 }; - let announced = r.on(Some(moved), 800).expect("a different address is new"); - assert_eq!(announced[announced.len() - 4..], [192, 168, 1, 50]); } + Some(false) +} - /// RFC 6762 §8: "Whenever a Multicast DNS responder starts up, wakes up - /// from sleep, receives an indication of a network interface "Link Change" - /// event, or has any other reason to believe that its network connectivity - /// may have changed in some relevant way, it MUST perform the two startup - /// steps below: Probing (Section 8.1) and Announcing (Section 8.3)." §8.3: - /// "at least two unsolicited responses, one second apart." §6 still holds: - /// a link back inside the second of the record's last multicast is - /// announced when that second ends. With no address there is no record, - /// and the address that comes is announced as new, twice and not four - /// times. - #[test] - fn a_link_that_returns_is_announced_twice_a_second_apart_and_no_sooner_than_a_second_after_the_last_multicast() { - let mut r = announced(); - let record = r.on(Some(LINK), 1_000).expect("the first address's second announcement"); - assert_eq!((r.on(Some(LINK), 60_000), r.owed_at()), (None, None), "nothing is owed a link that stayed"); - - r.link_returned(60_000); - assert_eq!(r.owed_at(), Some(60_000)); - assert_eq!(r.on(Some(LINK), 60_000).as_ref(), Some(&record), "announced as the link returns"); - assert_eq!(r.owed_at(), Some(61_000), "§8.3: and a second later"); - assert_eq!(r.on(Some(LINK), 60_999), None); - assert_eq!(r.on(Some(LINK), 61_000).as_ref(), Some(&record)); - assert_eq!((r.on(Some(LINK), 62_000), r.owed_at()), (None, None), "twice"); - - r.link_returned(61_300); - assert_eq!(r.owed_at(), Some(62_000), "§6: the record was multicast 300 ms ago"); - assert_eq!(r.on(Some(LINK), 61_300), None); - assert_eq!(r.on(Some(LINK), 62_000).as_ref(), Some(&record)); - assert_eq!(r.owed_at(), Some(63_000)); - assert_eq!(r.on(Some(LINK), 63_000).as_ref(), Some(&record)); - assert_eq!(r.owed_at(), None); - - r.link_returned(70_000); - assert_eq!(r.on(None, 70_000), None, "the address went with the link's return"); - assert_eq!(r.owed_at(), None); - assert!(r.on(Some(LINK), 71_000).is_some(), "an address after none is new"); - assert_eq!(r.owed_at(), Some(72_000)); - assert!(r.on(Some(LINK), 72_000).is_some()); - assert_eq!((r.on(Some(LINK), 73_000), r.owed_at()), (None, None), "its two announcements and no third"); - - let mut unaddressed = Responder::new(host()); - unaddressed.link_returned(5_000); - assert_eq!(unaddressed.owed_at(), None, "no address, no record to announce"); - assert!(unaddressed.on(Some(LINK), 9_000).is_some()); - assert_eq!(unaddressed.owed_at(), Some(10_000)); +/// A record as §8.2 orders it: "first comparing the record class (excluding +/// the cache-flush bit described in Section 10.2), then the record type, then +/// raw comparison of the binary content of the rdata", its bytes "as +/// eight-bit UNSIGNED values", the shorter of two that agree the earlier. +/// +/// The data is compared as the message carries it, and §8.2 has a name in it +/// uncompressed first: none is, because data is reached only between two +/// records of one class and type, this host's is its `A` record, and an +/// address holds no name. +type Proposed<'m> = (u16, u16, &'m [u8]); + +/// The records `query` proposes in its Authority Section for `host`'s name +/// that answer a question of type `kind`: the first of them as §8.2.1 sorts +/// them, and whether there is another. `None` where it proposes none, which +/// is what makes a query no probe (§6), or is no message that far. +fn proposed<'m>(query: &'m [u8], host: Host, kind: u16) -> Option<(Proposed<'m>, bool)> { + let mut at = records_at(query)?; + for _ in 0..u16_at(query, 6).ok()? { + at = record_at(query, at)?.end; } - - /// 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 - /// silently discarded. - #[test] - fn a_query_from_off_the_link_is_not_answered() { - let q = query(0, &["toyos-t14", "local"], 1, 1); - for addr in [[10, 0, 0, 7], [192, 168, 2, 7], [8, 8, 8, 8], [127, 0, 0, 1], [127, 1, 2, 3]] { - for port in [PORT, 53_000] { - assert_eq!(announced().answer(&q, Asker { addr, port }, 10_000), None, "{addr:?}:{port}"); + let mut first: Option = None; + let mut more = false; + for _ in 0..u16_at(query, 8).ok()? { + let record = record_at(query, at)?; + at = record.end; + if host.is(&record.name) && (kind == TYPE_ANY || kind == record.kind) { + let theirs = (record.class & !CACHE_FLUSH, record.kind, record.data); + more = first.is_some(); + if first.is_none_or(|first| theirs < first) { + first = Some(theirs); } } - for addr in [[192, 168, 1, 254], [169, 254, 3, 4]] { - let asked = announced().answer(&q, Asker { addr, port: PORT }, 10_000); - assert!(asked.is_some(), "{addr:?}"); - } } + first.map(|first| (first, more)) +} - #[test] - fn a_host_name_is_one_label() { - assert!(Host::new("toyos-t14").is_ok()); - for bad in ["", "a.b", "-a", "a-", "a b", "é", &"a".repeat(64)] { - assert!(Host::new(bad).is_err(), "{bad:?}"); - } +/// A header (RFC 1035 §4.1.1) with `counts` questions, answers and authority +/// records, and no additional one. +fn header(id: u16, flags: u16, counts: [u16; 3]) -> Vec { + let mut out = Vec::with_capacity(96); + for word in [id, flags, counts[0], counts[1], counts[2], 0] { + out.extend_from_slice(&word.to_be_bytes()); } + out +} - #[test] - fn the_group_address_maps_to_its_ethernet_address() { - assert_eq!(GROUP_MAC, [0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb]); - } +fn answer_record(out: &mut Vec, host: Host, addr: [u8; 4], class: u16, ttl: u32) { + host.put_name(out); + out.extend_from_slice(&TYPE_A.to_be_bytes()); + out.extend_from_slice(&class.to_be_bytes()); + out.extend_from_slice(&ttl.to_be_bytes()); + out.extend_from_slice(&4u16.to_be_bytes()); + out.extend_from_slice(&addr); } diff --git a/userland/netstack/mdns/src/tests.rs b/userland/netstack/mdns/src/tests.rs new file mode 100644 index 00000000000..0fa2cc40727 --- /dev/null +++ b/userland/netstack/mdns/src/tests.rs @@ -0,0 +1,906 @@ +//! Every message here, sent or expected, is spelled from RFC 1035 §4.1's +//! layout and never by the responder's own writers; each test names the +//! section of RFC 6762 whose sentence it holds. + +use super::*; +use std::vec; + +const ADDR: [u8; 4] = [192, 168, 1, 49]; +const MOVED: [u8; 4] = [192, 168, 1, 50]; +const LINK: Link = Link { addr: ADDR, prefix: 24 }; +const NEIGHBOUR: [u8; 4] = [192, 168, 1, 7]; +/// Another responder on the link. +const PEER: Source = Source { addr: NEIGHBOUR, port: PORT }; +const NAME: &[&str] = &["toyos-t14", "local"]; +/// §18.2, §18.4: a response, authoritative. +const RESPONSE: u16 = 0x8400; + +fn host() -> Host<'static> { + Host::new("toyos-t14").expect("a label") +} + +/// The draw of a call that starts no probing. +fn undrawn() -> u32 { + panic!("no probing starts here, so no delay is drawn") +} + +/// The probe for `addr`. §18.1, §18.2: ID zero and QR clear. §8.1: one +/// question, the name, type `ANY` (255), class 1 with the unicast-response +/// bit. §8.2: the proposed record in the Authority Section, which is the +/// header's third count: the name, type `A`, class 1 with no cache-flush bit +/// (§10.2 sets it in responses), §10's 120 s, four bytes of address. +fn probe_for(addr: [u8; 4]) -> Vec { + let mut want = vec![0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]; + want.extend_from_slice(b"\x09toyos-t14\x05local\x00\x00\xff\x80\x01"); + want.extend_from_slice(b"\x09toyos-t14\x05local\x00\x00\x01\x00\x01\x00\x00\x00\x78\x00\x04"); + want.extend_from_slice(&addr); + want +} + +/// The record for `addr` as a multicast carries it, asked for or announced: +/// §18's header (ID zero, `QR|AA`, no question), one `A` record with the +/// cache-flush bit (§10.2) and a 120 s TTL (§10). +fn record_of(addr: [u8; 4]) -> Vec { + let mut want = vec![0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]; + want.extend_from_slice(b"\x09toyos-t14\x05local\x00"); + want.extend_from_slice(&[0, 1, 0x80, 1, 0, 0, 0, 120, 0, 4]); + want.extend_from_slice(&addr); + want +} + +fn put_name(out: &mut Vec, labels: &[&str]) { + for label in labels { + out.push(label.len() as u8); + out.extend_from_slice(label.as_bytes()); + } + out.push(0); +} + +/// A record's name, type, class and data. +type Rr<'a> = (&'a [&'a str], u16, u16, &'a [u8]); + +/// A message as RFC 1035 §4.1 lays one out: the header, the questions (name, +/// type, class), then the answer, authority and additional sections. +fn message(id: u16, flags: u16, questions: &[(&[&str], u16, u16)], sections: [&[Rr]; 3]) -> Vec { + let mut m = Vec::new(); + for word in [id, flags, questions.len() as u16, sections[0].len() as u16, sections[1].len() as u16, sections[2].len() as u16] { + m.extend_from_slice(&word.to_be_bytes()); + } + for (name, kind, class) in questions { + put_name(&mut m, name); + m.extend_from_slice(&kind.to_be_bytes()); + m.extend_from_slice(&class.to_be_bytes()); + } + for (name, kind, class, data) in sections.into_iter().flatten() { + put_name(&mut m, name); + m.extend_from_slice(&kind.to_be_bytes()); + m.extend_from_slice(&class.to_be_bytes()); + m.extend_from_slice(&120u32.to_be_bytes()); + m.extend_from_slice(&(data.len() as u16).to_be_bytes()); + m.extend_from_slice(data); + } + m +} + +fn query(id: u16, name: &[&str], kind: u16, class: u16) -> Vec { + message(id, 0, &[(name, kind, class)], [&[], &[], &[]]) +} + +/// Another host's response naming `addr` for this host's name. +fn says(addr: &[u8]) -> Vec { + message(0, RESPONSE, &[], [&[(NAME, 1, 0x8001, addr)], &[], &[]]) +} + +/// Another host's probe for this host's name, proposing `proposed`; `class` +/// is its question's. +fn probe_of(class: u16, proposed: &[Rr]) -> Vec { + message(0, 0, &[(NAME, 255, class)], [&[], proposed, &[]]) +} + +/// Every multicast `r` sends at its own deadlines up to `until`, each with +/// its time. Nothing is due a millisecond before a deadline, and `delay` is +/// drawn by the call that starts probing and by no other. +fn run(r: &mut Responder, link: Link, until: u64, delay: u32) -> Vec<(u64, Vec)> { + let mut sent = Vec::new(); + for _ in 0..10_000 { + let Some(at) = r.owed_at().filter(|at| *at <= until) else { return sent }; + let starts = matches!(r.claim, Claim::Owed { .. }); + if !starts && at > 0 { + assert_eq!(r.on(Some(link), at - 1, undrawn), None, "nothing is due before {at}"); + } + let mut drawn = false; + let owed = r.on(Some(link), at, || { + drawn = true; + delay + }); + assert_eq!(drawn, starts, "a delay is drawn by the call that starts probing, and by no other"); + sent.extend(owed.map(|bytes| (at, bytes))); + } + panic!("10,000 deadlines before {until}"); +} + +/// Three probes for `addr` from `first`, 250 ms apart, and the two +/// announcements from 250 ms after the third, a second apart. +fn claim_of(addr: [u8; 4], first: u64) -> Vec<(u64, Vec)> { + let probes = [0, 250, 500].map(|after| (first + after, probe_for(addr))); + let announcements = [750, 1_750].map(|after| (first + after, record_of(addr))); + probes.into_iter().chain(announcements).collect() +} + +/// A responder one probe into claiming `link`'s address: the probe left at +/// 1,000. +fn probing(link: Link) -> Responder<'static> { + let mut r = Responder::new(host()); + assert_eq!(r.on(Some(link), 1_000, || 0), Some(probe_for(link.addr))); + r +} + +/// A responder that holds its name on [`LINK`], its second announcement out +/// at `at`. +fn held(at: u64) -> Responder<'static> { + let mut r = Responder::new(host()); + assert_eq!(r.on(Some(LINK), at - 1_750, || 0), Some(probe_for(ADDR))); + assert_eq!(run(&mut r, LINK, at, 0), claim_of(ADDR, at - 1_750)[1..]); + assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Claimed), None)); + r +} + +/// One query from a neighbour on the link, long after the announcements. +fn ask(query: &[u8], port: u16) -> Option { + held(10_000).heard(query, Source { addr: NEIGHBOUR, port }, 100_000) +} + +/// Whether `r` answers for its name at `now`, to a legacy resolver, which §6 +/// never delays. +fn answers(r: &mut Responder, now: u64) -> bool { + r.heard(&query(1, NAME, 1, 1), Source { addr: NEIGHBOUR, port: 53_000 }, now).is_some() +} + +#[test] +fn a_query_for_this_name_is_answered_to_the_group() { + let got = ask(&query(0, NAME, 1, 1), PORT).expect("an answer"); + assert_eq!(got, Answer { to: To::Group, bytes: record_of(ADDR) }); +} + +#[test] +fn a_name_is_matched_whatever_its_case_and_any_asks_for_it_too() { + let q = query(0, &["ToyOS-T14", "LOCAL"], 255, 1); + assert!(ask(&q, PORT).is_some()); +} + +#[test] +fn the_unicast_bit_sends_the_answer_back_to_the_asker() { + let got = ask(&query(0, NAME, 1, 0x8001), PORT).expect("an answer"); + assert_eq!(got.to, To::Asker); +} + +/// §6.7: a resolver that is not a responder gets its ID, its question and +/// a short TTL, and no cache-flush bit. +#[test] +fn a_legacy_resolver_gets_its_id_its_question_and_a_short_ttl() { + let got = ask(&query(0xBEEF, NAME, 1, 1), 53_000).expect("an answer"); + assert_eq!(got.to, To::Asker); + let mut want = vec![0xBE, 0xEF, 0x84, 0x00, 0, 1, 0, 1, 0, 0, 0, 0]; + want.extend_from_slice(b"\x09toyos-t14\x05local\x00\x00\x01\x00\x01"); + want.extend_from_slice(b"\x09toyos-t14\x05local\x00"); + want.extend_from_slice(&[0, 1, 0, 1, 0, 0, 0, 10, 0, 4, 192, 168, 1, 49]); + assert_eq!(got.bytes, want); +} + +/// §18.3: "Multicast DNS messages received with an OPCODE other than zero +/// MUST be silently ignored." §18.11: "Multicast DNS messages received with +/// non-zero Response Codes MUST be silently ignored." +#[test] +fn a_question_that_is_not_this_hosts_is_not_answered() { + for (name, kind, class) in [ + (&["other", "local"][..], 1, 1), + (&["toyos-t14", "lan"][..], 1, 1), + (&["toyos-t14"][..], 1, 1), + (&["toyos-t14", "local", "x"][..], 1, 1), + (NAME, 28, 1), + (NAME, 1, 3), + ] { + assert_eq!(ask(&query(0, name, kind, class), PORT), None, "{name:?}"); + } + for (flags, why) in [(0x0800, "a nonzero opcode"), (0x0003, "a nonzero response code")] { + for port in [PORT, 53_000] { + assert_eq!(ask(&message(0, flags, &[(NAME, 1, 1)], [&[], &[], &[]]), port), None, "{why}"); + } + } + assert_eq!(ask(&message(0, RESPONSE, &[(NAME, 1, 1)], [&[], &[], &[]]), PORT), None, "a response is not a question"); +} + +/// A second question may name the first's labels by pointer (RFC 1035 +/// §4.1.4), as a responder asking several things at once does. +#[test] +fn a_question_spelled_by_a_pointer_is_read() { + let mut q = query(0, &["other", "local"], 1, 1); + q[5] = 2; + // `toyos-t14`, then a pointer to `local` at offset 12 + 6. + q.extend_from_slice(b"\x09toyos-t14\xC0\x12\x00\x01\x00\x01"); + assert_eq!(ask(&q, PORT).map(|a| a.to), Some(To::Group)); +} + +/// Nothing a peer sends can hold the parser: a short message, a label past +/// the end, a pointer forwards or to itself are each no question. +#[test] +fn a_malformed_message_is_no_question() { + let whole = query(0, NAME, 1, 1); + for cut in 0..whole.len() { + assert_eq!(ask(&whole[..cut], PORT), None, "cut at {cut}"); + } + let mut forward = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; + forward.extend_from_slice(&[0xC0, 12, 0, 1, 0, 1]); + assert_eq!(ask(&forward, PORT), None, "a pointer to itself"); + let mut ahead = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; + ahead.extend_from_slice(&[0xC0, 20, 0, 1, 0, 1, 0, 0]); + assert_eq!(ask(&ahead, PORT), None, "a pointer forwards"); + let mut reserved = vec![0u8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; + reserved.extend_from_slice(&[0x40, 0, 0, 1, 0, 1]); + assert_eq!(ask(&reserved, PORT), None, "a reserved label type"); +} + +/// RFC 6762 §6: "a Multicast DNS responder MUST NOT (except in the one +/// special case of answering probe queries) multicast a record on a given +/// interface until at least one second has elapsed since the last time that +/// record was multicast on that particular interface." An announcement is a +/// multicast of the record too. A query inside the second is answered when +/// it ends, by one multicast of the record, and not before or never. +#[test] +fn the_record_is_multicast_at_most_once_a_second_and_a_query_inside_it_waits() { + let q = query(0, NAME, 1, 1); + let mut r = Responder::new(host()); + assert_eq!(r.on(Some(LINK), 19_250, || 0), Some(probe_for(ADDR))); + assert_eq!(run(&mut r, LINK, 20_000, 0), claim_of(ADDR, 19_250)[1..4], "claimed, and first announced at 20,000"); + assert_eq!(r.owed_at(), Some(21_000), "§8.3: the second announcement"); + assert_eq!(r.heard(&q, PEER, 20_500), None, "the record was announced 500 ms ago"); + let legacy = Source { addr: NEIGHBOUR, port: 53_000 }; + assert_eq!(r.heard(&q, legacy, 20_500).map(|a| a.to), Some(To::Asker), "a unicast answer is no multicast of the record"); + assert_eq!(r.on(Some(LINK), 20_999, undrawn), None); + assert_eq!(r.on(Some(LINK), 21_000, undrawn), Some(record_of(ADDR)), "announced again, which answers the query"); + assert_eq!(r.owed_at(), None); + + assert_eq!(r.heard(&q, PEER, 21_400), None, "the record was multicast 400 ms ago"); + assert_eq!(r.heard(&q, PEER, 21_600), None); + assert_eq!(r.owed_at(), Some(22_000), "both queries are owed one multicast when the second ends"); + assert_eq!(run(&mut r, LINK, 30_000, 0), [(22_000, record_of(ADDR))], "owed once"); + + assert_eq!(r.heard(&q, PEER, 23_000).map(|a| a.to), Some(To::Group), "a second has passed"); + assert_eq!(r.heard(&q, PEER, 23_999), None); + assert_eq!(r.owed_at(), Some(24_000)); +} + +/// RFC 6762 §8.1: "All probe queries SHOULD be done using the desired +/// resource record name and class (usually class 1, "Internet"), and query +/// type "ANY" (255)"; "the probes SHOULD be sent as "QU" questions with the +/// unicast-response bit set". §8.2: "each host populates the query message's +/// Authority Section with the record or records with the rdata that it would +/// be proposing to use, should its probing be successful." +#[test] +fn rfc6762_8_1_a_probe_asks_any_for_the_name_with_the_unicast_bit_and_proposes_its_record_in_the_authority_section() { + let mut r = Responder::new(host()); + let probe = r.on(Some(LINK), 0, || 0).expect("no delay, so the first probe"); + #[rustfmt::skip] + let want = [ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, + 9, b't', b'o', b'y', b'o', b's', b'-', b't', b'1', b'4', 5, b'l', b'o', b'c', b'a', b'l', 0, + 0, 255, 0x80, 1, + 9, b't', b'o', b'y', b'o', b's', b'-', b't', b'1', b'4', 5, b'l', b'o', b'c', b'a', b'l', 0, + 0, 1, 0, 1, 0, 0, 0, 120, 0, 4, 192, 168, 1, 49, + ]; + assert_eq!(probe, want); + assert_eq!(probe_for(ADDR), want, "which is what every other test expects of a probe"); + let sent = run(&mut r, LINK, 600, 0); + assert_eq!(sent, [(250, want.to_vec()), (500, want.to_vec())], "each of the three is that message"); +} + +/// RFC 6762 §8.1: "the host should first wait for a short random delay time, +/// uniformly distributed in the range 0-250 ms." "250 ms after the first +/// query, the host should send a second; then, 250 ms after that, a third. +/// If, by 250 ms after the third probe, no conflicting Multicast DNS +/// responses have been received, the host may move to the next step, +/// announcing." §8.3: "at least two unsolicited responses, one second apart." +#[test] +fn rfc6762_8_1_three_probes_250_ms_apart_follow_the_drawn_delay_and_the_name_is_announced_250_ms_after_the_third() { + for (draw, delay) in [(0, 0), (1, 1), (137, 137), (250, 250), (251, 0), (1_000, 247), (u32::MAX, u64::from(u32::MAX % 251))] { + let mut r = Responder::new(host()); + let mut sent: Vec<_> = r.on(Some(LINK), 5_000, || draw).map(|probe| (5_000, probe)).into_iter().collect(); + assert_eq!(sent.is_empty(), delay != 0, "draw {draw}"); + assert_eq!(r.take_event(), None); + sent.extend(run(&mut r, LINK, 5_000 + delay + 749, draw)); + assert_eq!(sent, claim_of(ADDR, 5_000 + delay)[..3], "draw {draw}: three probes, and nothing announced so far"); + assert_eq!(r.take_event(), None, "the name is not held 249 ms after the third probe"); + assert_eq!(run(&mut r, LINK, 60_000, draw), claim_of(ADDR, 5_000 + delay)[3..], "draw {draw}: announced twice, and nothing after"); + assert_eq!(r.take_event(), Some(Event::Claimed)); + } +} + +/// RFC 6762 §8.3 announces "unique records that have completed the probing +/// step", and §6 has only "an authoritative source for a given record" answer +/// with it: under the probe nothing is answered, to anyone. +#[test] +fn a_name_under_probe_is_answered_to_nobody() { + let mut r = Responder::new(host()); + assert_eq!(r.on(Some(LINK), 1_000, || 100), None); + for now in [1_000, 1_099, 1_100, 1_350, 1_600, 1_849] { + run(&mut r, LINK, now, 0); + for (class, port) in [(1, PORT), (0x8001, PORT), (1, 53_000)] { + let asked = r.heard(&query(7, NAME, 1, class), Source { addr: NEIGHBOUR, port }, now); + assert_eq!(asked, None, "at {now}, class {class:#x} from port {port}"); + } + assert_eq!(r.owed_at().map(|at| at > now), Some(true), "and no answer is held for later"); + } + assert_eq!(run(&mut r, LINK, 1_850, 0), [(1_850, record_of(ADDR))]); + assert!(answers(&mut r, 1_850), "held, and answered"); +} + +/// Whether a message heard from `from`, 100 ms after the first probe for +/// `link`'s address, made the responder defer for §8.2's second. +fn deferred(link: Link, from: [u8; 4], heard: &[u8]) -> bool { + let mut r = probing(link); + assert_eq!(r.heard(heard, Source { addr: from, port: PORT }, 1_100), None); + assert_eq!(r.take_event(), None); + match r.owed_at() { + Some(1_250) => false, + Some(2_100) => true, + other => panic!("neither the second probe nor a second's wait: {other:?}"), + } +} + +/// 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 it defers +/// to the winning host by waiting one second, and then begins probing for +/// this record again." Its example: "MyPrinter.local. A 169.254.99.200" and +/// "MyPrinter.local. A 169.254.200.50": "169.254.200.50 is lexicographically +/// later (the third byte, with value 200, is greater than its counterpart +/// with value 99), so it is deemed the winner", which a signed comparison +/// gets wrong. +#[test] +fn rfc6762_8_2_the_lexicographically_later_data_wins_a_simultaneous_probe_and_the_earlier_waits_a_second() { + let (early, late) = ([169, 254, 99, 200], [169, 254, 200, 50]); + let on = |addr| Link { addr, prefix: 16 }; + let proposing = |addr: [u8; 4]| probe_of(0x8001, &[(NAME, 1, 1, &addr)]); + assert!(deferred(on(early), late, &proposing(late)), "the earlier defers"); + assert!(!deferred(on(late), early, &proposing(early)), "the later ignores the other's probe"); + + let mut r = probing(on(early)); + assert_eq!(r.heard(&proposing(late), Source { addr: late, port: PORT }, 1_100), None); + assert_eq!(run(&mut r, on(early), 60_000, 0), claim_of(early, 2_100), "a second's wait, then the probing whole"); + assert_eq!(r.take_event(), Some(Event::Claimed), "nobody answered the second probing: a stale packet's name is claimed"); + + let mut r = probing(on(late)); + assert_eq!(r.heard(&proposing(early), Source { addr: early, port: PORT }, 1_100), None); + assert_eq!(run(&mut r, on(late), 60_000, 0), claim_of(late, 1_000)[1..], "the probing goes on as it was"); +} + +/// RFC 6762 §8.2: "first comparing the record class (excluding the +/// cache-flush bit described in Section 10.2), then the record type, then raw +/// comparison of the binary content of the rdata"; a record that "runs out of +/// rdata" first is the earlier. §8.2.1: the other host's records "are each +/// sorted into order, and then compared pairwise"; "If either list of records +/// runs out of records before any difference is found, then the list with +/// records remaining is deemed to have won the tiebreak. If both lists run +/// out of records at the same time without any difference being found, then +/// ... there is, in fact, no conflict." §8.2 consults "the Authority Section +/// of that query" for records "which answers the query". +#[test] +fn rfc6762_8_2_a_probe_is_compared_by_class_then_type_then_data_and_a_longer_list_wins() { + let (lower, higher, longer) = ([192, 168, 1, 48], [192, 168, 1, 50], [192, 168, 1, 49, 0]); + let cases: [(&str, &[Rr], bool); 15] = [ + ("later data", &[(NAME, 1, 1, &higher)], true), + ("earlier data", &[(NAME, 1, 1, &lower)], false), + ("the same record", &[(NAME, 1, 1, &ADDR)], false), + ("a greater class with earlier data", &[(NAME, 1, 2, &lower)], true), + ("a lesser class with later data", &[(NAME, 1, 0, &higher)], false), + ("a greater class with a lesser type", &[(NAME, 0, 2, &lower)], true), + ("the cache-flush bit is no part of the class", &[(NAME, 1, 0x8001, &lower)], false), + ("a greater type with earlier data", &[(NAME, 28, 1, &[0; 16])], true), + ("a lesser type with later data", &[(NAME, 0, 1, &higher)], false), + ("data that runs out first", &[(NAME, 1, 1, &ADDR[..3])], false), + ("data with more remaining", &[(NAME, 1, 1, &longer)], true), + ("the same record and one more", &[(NAME, 28, 1, &[0; 16]), (NAME, 1, 1, &ADDR)], true), + ("its earliest record is earlier", &[(NAME, 28, 1, &[0xff; 16]), (NAME, 1, 1, &lower)], false), + ("another name's record", &[(&["other", "local"], 1, 1, &higher)], false), + ("the same record in another case", &[(&["TOYOS-T14", "Local"], 1, 1, &higher)], true), + ]; + for (what, theirs, loses) in cases { + assert_eq!(deferred(LINK, NEIGHBOUR, &probe_of(1, theirs)), loses, "{what}"); + } + let later: &[Rr] = &[(NAME, 1, 1, &higher)]; + let known = message(0, 0, &[(NAME, 255, 1)], [later, &[], &[]]); + assert!(!deferred(LINK, NEIGHBOUR, &known), "a known answer proposes nothing"); + let elsewhere = message(0, 0, &[(&["other", "local"], 255, 1)], [&[], later, &[]]); + assert!(!deferred(LINK, NEIGHBOUR, &elsewhere), "a probe for another name"); + let aaaa = message(0, 0, &[(NAME, 1, 1)], [&[], &[(NAME, 28, 1, &[0xff; 16])], &[]]); + assert!(!deferred(LINK, NEIGHBOUR, &aaaa), "a record of another type answers no question for A"); + assert!(!deferred(LINK, [10, 0, 0, 7], &probe_of(1, later)), "§11: a probe from off the link"); + assert!(deferred(LINK, NEIGHBOUR, &probe_of(1, later)), "and the one all of those were not"); +} + +/// What a peer may send about the name, and whether it conflicts. +fn about_the_name() -> Vec<(&'static str, Vec, Source, bool)> { + let other: &[Rr] = &[(NAME, 1, 0x8001, &MOVED)]; + let in_section = |at: usize| { + let mut sections: [&[Rr]; 3] = [&[], &[], &[]]; + sections[at] = other; + message(0, RESPONSE, &[], sections) + }; + let mut by_pointer = message(0, RESPONSE, &[(NAME, 1, 1)], [&[], &[], &[]]); + by_pointer[7] = 1; + by_pointer.extend_from_slice(&[0xC0, 12, 0, 1, 0x80, 1, 0, 0, 0, 120, 0, 4, 192, 168, 1, 50]); + let mut counted_long = says(&MOVED); + counted_long[6..12].copy_from_slice(&[0xff; 6]); + let mut counted_alone = message(0, RESPONSE, &[], [&[], &[], &[]]); + counted_alone[6..12].copy_from_slice(&[0xff; 6]); + let mut overlong = says(&MOVED); + let len = overlong.len(); + overlong[len - 5] = 5; + vec![ + ("another address", says(&MOVED), PEER, true), + ("the name in another case", message(0, RESPONSE, &[], [&[(&["ToyOS-T14", "LOCAL"], 1, 1, &[10, 0, 0, 1])], &[], &[]]), PEER, true), + ("§8.1: a record with that name, of any type", message(0, RESPONSE, &[], [&[(NAME, 28, 1, &[0; 16])], &[], &[]]), PEER, true), + ("another class", message(0, RESPONSE, &[], [&[(NAME, 1, 3, &ADDR)], &[], &[]]), PEER, true), + ("no data", message(0, RESPONSE, &[], [&[(NAME, 1, 1, &[])], &[], &[]]), PEER, true), + ("§9: in the answer section", in_section(0), PEER, true), + ("§9: in the authority section", in_section(1), PEER, true), + ("§9: in the additional section", in_section(2), PEER, true), + ("behind another host's record", message(0, RESPONSE, &[], [&[(&["other", "local"], 1, 1, &MOVED), (NAME, 1, 1, &MOVED)], &[], &[]]), PEER, true), + ("named by a pointer, behind a question §6 has ignored", by_pointer, PEER, true), + ("with counts of records that are not there", counted_long, PEER, true), + ("from a link-local source", says(&MOVED), Source { addr: [169, 254, 3, 4], port: PORT }, true), + ("§9: identical rdata is never inconsistent", says(&ADDR), PEER, false), + ("this record without the cache-flush bit", message(0, RESPONSE, &[], [&[(NAME, 1, 1, &ADDR)], &[], &[]]), PEER, false), + ("another name's record", message(0, RESPONSE, &[], [&[(&["other", "local"], 1, 1, &MOVED)], &[], &[]]), PEER, false), + ("a question and no record", message(0, RESPONSE, &[(NAME, 255, 1)], [&[], &[], &[]]), PEER, false), + ("counts and no record", counted_alone, PEER, false), + ("data longer than the message", overlong, PEER, false), + ("§6: a response from a port other than 5353", says(&MOVED), Source { addr: NEIGHBOUR, port: 5_354 }, false), + ("§11: a response from off the link", says(&MOVED), Source { addr: [10, 0, 0, 7], port: PORT }, false), + ("§18.11: a response code", message(0, RESPONSE | 3, &[], [other, &[], &[]]), PEER, false), + ("§18.3: an opcode", message(0, RESPONSE | 0x0800, &[], [other, &[], &[]]), PEER, false), + ("a query's known answer", message(0, 0, &[], [other, &[], &[]]), PEER, false), + ] +} + +/// RFC 6762 §8.1: "During probing, from the time the first probe packet is +/// sent until 250 ms after the third probe, if any conflicting Multicast DNS +/// response is received, then the probing host MUST defer to the existing +/// host"; "any answer containing a record with that name, of any type, MUST +/// be considered a conflicting response". §9: "The protocol used in the +/// Probing phase will determine a winner and a loser, and the loser MUST +/// cease using the name". +#[test] +fn rfc6762_8_1_a_conflicting_response_under_the_probe_takes_the_name_and_nothing_else_does() { + for (what, heard, from, conflicts) in about_the_name() { + for (probes, now) in [(1, 1_001), (2, 1_300), (3, 1_500), (3, 1_749)] { + let mut r = probing(LINK); + assert_eq!(run(&mut r, LINK, now, 0).len(), probes - 1); + assert_eq!(r.heard(&heard, from, now), None, "{what}"); + if !conflicts { + assert_eq!(r.take_event(), None, "{what}, after {probes} probes"); + assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 1_000)[probes..], "{what}: the probing goes on"); + continue; + } + assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered)), "{what}, after {probes} probes"); + assert_eq!((r.take_event(), r.owed_at()), (None, 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"); + for class in [1, 0x8001] { + assert_eq!(r.heard(&query(0, NAME, 255, class), PEER, 3_600_000), None); + } + } + } +} + +/// RFC 6762 §8.1: "Apparently conflicting Multicast DNS responses received +/// *before* the first probe packet is sent MUST be silently ignored (see +/// discussion of stale probe packets in Section 8.2, "Simultaneous Probe +/// Tiebreaking", below)." A probe heard then is as stale. +#[test] +fn rfc6762_8_1_nothing_heard_before_the_first_probe_is_sent_is_a_conflict() { + let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); + let mut r = Responder::new(host()); + assert_eq!(r.on(Some(LINK), 1_000, || 200), None, "the delay"); + for heard in [says(&MOVED), later.clone()] { + assert_eq!(r.heard(&heard, PEER, 1_100), None); + } + assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_200))); + assert_eq!(r.on(Some(LINK), 1_200, undrawn), Some(probe_for(ADDR))); + + assert_eq!(r.heard(&later, PEER, 1_300), None); + assert_eq!(r.owed_at(), Some(2_300), "the first probe is out: a later probe is deferred to"); + for heard in [says(&MOVED), later.clone()] { + assert_eq!(r.heard(&heard, PEER, 2_000), None); + } + assert_eq!((r.take_event(), r.owed_at()), (None, Some(2_300)), "and in the second's wait no probe of the new probing is out"); + assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 2_300)); +} + +/// RFC 6762 §9: "Whenever a Multicast DNS responder receives any Multicast +/// DNS response (solicited or otherwise) containing a conflicting resource +/// record in any of the Resource Record Sections, the Multicast DNS responder +/// MUST immediately reset its conflicted unique record to probing state, and +/// go through the startup steps described above in Section 8". +#[test] +fn rfc6762_9_a_conflicting_response_resets_a_held_name_to_probing_and_nothing_else_does() { + for (what, heard, from, conflicts) in about_the_name() { + let mut r = held(10_000); + assert_eq!(r.heard(&heard, from, 50_000), None, "{what}"); + assert_eq!(r.take_event(), None, "{what}: the name is neither lost nor claimed yet"); + if !conflicts { + assert_eq!(r.owed_at(), None, "{what}"); + assert!(answers(&mut r, 50_000), "{what}: still held"); + continue; + } + assert_eq!(r.owed_at(), Some(50_000), "{what}: probing is owed at once"); + assert!(!answers(&mut r, 50_000), "{what}: and the name is answered to nobody"); + assert_eq!(r.heard(&query(0, NAME, 1, 1), PEER, 50_000), None); + assert_eq!(run(&mut r, LINK, 50_326, 77), claim_of(ADDR, 50_077)[..1], "{what}: probed after a drawn delay"); + assert!(!answers(&mut r, 50_326)); + assert_eq!(run(&mut r, LINK, 3_600_000, 77), claim_of(ADDR, 50_077)[1..], "{what}: unanswered, the name is announced again"); + assert_eq!(r.take_event(), Some(Event::Claimed)); + assert!(answers(&mut r, 60_000)); + } +} + +/// RFC 6762 §9: "The protocol used in the Probing phase will determine a +/// winner and a loser, and the loser MUST cease using the name". A host that +/// holds the name answers the probe §9 asks for, and that takes it (§8.1). +#[test] +fn rfc6762_9_a_held_name_another_host_answers_for_under_the_new_probe_is_lost() { + let mut r = held(10_000); + 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.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Answered)), None)); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); + assert!(!answers(&mut r, 3_600_000)); +} + +/// RFC 6762 §6: "In the special case of answering probe queries, because of +/// the limited time before the probing host will make its decision about +/// whether or not to use the name, a Multicast DNS responder MUST respond +/// quickly. In this special case only, when responding via multicast to a +/// probe, a Multicast DNS responder is only required to delay its +/// transmission as necessary to ensure an interval of at least 250 ms since +/// the last time the record was multicast on that interface." "A probe query +/// can be distinguished from a normal query by the fact that a probe query +/// contains a proposed record in the Authority Section that answers the +/// question". §8.1 has probes sent "QU" "to allow a defending host to respond +/// immediately via unicast". +#[test] +fn rfc6762_6_a_probe_for_a_held_name_is_answered_at_once_by_unicast_or_250_ms_after_the_records_last_multicast() { + let theirs: &[Rr] = &[(NAME, 1, 1, &NEIGHBOUR)]; + let (qu, qm, plain) = (probe_of(0x8001, theirs), probe_of(1, theirs), query(0, NAME, 255, 1)); + + let mut r = held(10_000); + let defended = r.heard(&qu, PEER, 10_001); + assert_eq!(defended, Some(Answer { to: To::Asker, bytes: record_of(ADDR) }), "a QU probe, a millisecond after an announcement"); + + assert_eq!(r.heard(&qm, PEER, 10_100), None, "a QM probe 100 ms after the record's last multicast"); + assert_eq!(r.owed_at(), Some(10_250), "is answered at 250 ms, inside the 750 ms its sender waits"); + assert_eq!(run(&mut r, LINK, 20_000, 0), [(10_250, record_of(ADDR))]); + + let mut r = held(10_000); + assert_eq!(r.heard(&plain, PEER, 10_100), None, "a query that is no probe"); + assert_eq!(r.owed_at(), Some(11_000), "waits out the second"); + assert_eq!(r.heard(&qm, PEER, 10_200), None); + assert_eq!(r.owed_at(), Some(10_250), "and a probe behind it is not held to the query's second"); + assert_eq!(r.heard(&plain, PEER, 10_240), None); + assert_eq!(run(&mut r, LINK, 20_000, 0), [(10_250, record_of(ADDR))], "one multicast answers all three"); + + let mut r = held(10_000); + assert_eq!(r.heard(&qm, PEER, 10_250).map(|a| a.to), Some(To::Group), "250 ms have passed"); + assert_eq!(r.heard(&qm, PEER, 10_499), None); + assert_eq!(r.owed_at(), Some(10_500)); + let known = message(0, 0, &[(NAME, 255, 1)], [theirs, &[], &[]]); + let mut r = held(10_000); + assert_eq!(r.heard(&known, PEER, 10_300), None, "a known answer is no proposed record"); + assert_eq!(r.owed_at(), Some(11_000)); +} + +/// `count` returns of the link `every` ms apart from 10,000, each probed on +/// at once and answered for by another host a millisecond later. Returns the +/// responder and when the last conflict was. +fn answered_on_every_return(count: u64, every: u64) -> (Responder<'static>, u64) { + let mut r = held(5_000); + let mut last = 0; + for i in 0..count { + let now = 10_000 + i * every; + r.link_returned(now); + assert_eq!(r.on(Some(LINK), now, || 0), Some(probe_for(ADDR)), "return {i}: not limited yet"); + assert_eq!(r.heard(&says(&MOVED), PEER, now + 1), None); + assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); + last = now + 1; + } + (r, last) +} + +/// RFC 6762 §8.1: "If fifteen conflicts occur within any ten-second period, +/// then the host MUST wait at least five seconds before each successive +/// additional probe attempt." +#[test] +fn rfc6762_8_1_fifteen_conflicts_in_ten_seconds_put_five_seconds_before_each_further_probe_attempt() { + // 14 x 714 ms is 9,996 ms: the fifteen fall within ten seconds. + let (mut r, last) = answered_on_every_return(15, 714); + let now = last + 700; + r.link_returned(now); + assert_eq!(r.on(Some(LINK), now, || 30), None); + assert_eq!(r.owed_at(), Some(now + 5_030), "five seconds, and the drawn delay"); + assert_eq!(run(&mut r, LINK, now + 5_100, 30), [(now + 5_030, probe_for(ADDR))]); + + assert_eq!(r.heard(&probe_of(1, &[(NAME, 1, 1, &MOVED)]), PEER, now + 5_100), None, "a later probe"); + assert_eq!(r.owed_at(), Some(now + 5_100 + 6_000), "§8.2's second, and the five"); + assert_eq!(run(&mut r, LINK, now + 11_200, 30), [(now + 11_100, probe_for(ADDR))]); + assert_eq!(r.heard(&says(&MOVED), PEER, now + 11_200), None); + assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); + + // The newest fifteen now span more than ten seconds, and each still came + // within ten of the one before: "each successive additional probe attempt". + r.link_returned(now + 12_000); + assert_eq!(r.on(Some(LINK), now + 12_000, || 0), None); + assert_eq!(r.owed_at(), Some(now + 17_000)); + + // Ten seconds with no conflict end it. + r.link_returned(now + 21_201); + assert_eq!(r.on(Some(LINK), now + 21_201, || 0), Some(probe_for(ADDR))); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, now + 21_201)[1..]); +} + +/// RFC 6762 §8.1's limit is of "fifteen conflicts" "within any ten-second +/// period": fourteen are not fifteen, and fifteen in 10.01 s are not within +/// ten. +#[test] +fn rfc6762_8_1_fourteen_conflicts_or_fifteen_in_more_than_ten_seconds_limit_nothing() { + for (count, every) in [(14, 100), (15, 715), (40, 750)] { + let (mut r, last) = answered_on_every_return(count, every); + r.link_returned(last + 700); + assert_eq!(r.on(Some(LINK), last + 700, || 0), Some(probe_for(ADDR)), "{count} conflicts {every} ms apart"); + } +} + +/// RFC 6762 §8: "Whenever a Multicast DNS responder starts up, wakes up from +/// sleep, receives an indication of a network interface "Link Change" event, +/// or has any other reason to believe that its network connectivity may have +/// changed in some relevant way, it MUST perform the two startup steps below: +/// Probing (Section 8.1) and Announcing (Section 8.3)." §6 still holds: the +/// record a query had multicast inside the last second is first announced +/// when that second ends. +#[test] +fn rfc6762_8_a_link_that_returns_is_probed_on_before_the_name_is_announced_or_answered_with_again() { + let mut r = held(10_000); + assert_eq!((r.on(Some(LINK), 60_000, undrawn), r.owed_at()), (None, None), "nothing is owed a link that stayed"); + + r.link_returned(60_000); + assert_eq!(r.owed_at(), Some(60_000)); + assert!(!answers(&mut r, 60_000), "not this host's until probed for again"); + assert_eq!(run(&mut r, LINK, 3_600_000, 120), claim_of(ADDR, 60_120)); + assert_eq!(r.take_event(), Some(Event::Claimed)); + + assert_eq!(r.heard(&query(0, NAME, 1, 1), PEER, 99_900).map(|a| a.to), Some(To::Group)); + r.link_returned(100_000); + let sent = run(&mut r, LINK, 3_600_000, 0); + assert_eq!(sent[..3], claim_of(ADDR, 100_000)[..3]); + assert_eq!(sent[3..], [(100_900, record_of(ADDR)), (101_900, record_of(ADDR))], "§6: a second after the answer at 99,900"); + + let mut lost = probing(LINK); + assert_eq!(lost.heard(&says(&MOVED), PEER, 1_100), None); + assert_eq!(lost.take_event(), Some(Event::Lost(Lost::Answered))); + lost.link_returned(9_000); + assert_eq!(run(&mut lost, LINK, 3_600_000, 5), claim_of(ADDR, 9_005), "a lost name is probed for on the link's return"); + assert_eq!(lost.take_event(), Some(Event::Claimed)); + + let mut unaddressed = Responder::new(host()); + unaddressed.link_returned(5_000); + assert_eq!(unaddressed.owed_at(), None, "no address, no record to probe for"); + assert_eq!(unaddressed.on(Some(LINK), 9_000, || 0), Some(probe_for(ADDR)), "the address that comes is probed on once"); + assert_eq!(run(&mut unaddressed, LINK, 3_600_000, 0), claim_of(ADDR, 9_000)[1..]); +} + +/// A link that returns again under the probe starts it over: each return is +/// a reason of its own to doubt the name (§8), nothing is announced until one +/// probing has run whole, and a return costs no more than its three probes. +#[test] +fn a_flapping_link_restarts_the_probe_and_nothing_is_announced_until_one_runs_whole() { + let mut r = held(10_000); + let mut sent = Vec::new(); + for flap in 0..50 { + let now = 20_000 + flap * 100; + r.link_returned(now); + sent.extend(run(&mut r, LINK, now + 99, 150)); + assert!(!answers(&mut r, now + 99)); + } + assert_eq!(sent, [], "a return inside the last one's delay sends nothing"); + assert_eq!(run(&mut r, LINK, 3_600_000, 150), claim_of(ADDR, 24_900 + 150)); + assert_eq!(r.take_event(), Some(Event::Claimed)); + + let mut sent = Vec::new(); + for flap in 0..50 { + let now = 40_000 + flap * 600; + r.link_returned(now); + sent.extend(run(&mut r, LINK, now + 599, 0)); + assert!(!answers(&mut r, now + 599)); + } + let probes: Vec<_> = (0..50).flat_map(|flap| claim_of(ADDR, 40_000 + flap * 600).into_iter().take(3)).collect(); + assert_eq!(sent, probes, "three probes a return, and no announcement between two returns 600 ms apart"); + assert_eq!(r.take_event(), None); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, 49 * 600 + 40_000)[3..]); + assert_eq!(r.take_event(), Some(Event::Claimed)); +} + +/// 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. +#[test] +fn a_storm_of_forged_answers_costs_one_probe_and_the_name() { + let mut r = held(10_000); + let mut sent = Vec::new(); + for ms in 0..10_000 { + sent.extend(run(&mut r, LINK, 50_000 + ms, 0)); + assert_eq!(r.heard(&says(&MOVED), PEER, 50_000 + ms), None); + } + assert_eq!(sent, [(50_000, probe_for(ADDR))]); + assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Answered)), None)); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); +} + +/// Forged answers timed to spare every probe, one before each second +/// announcement of a name held again: each costs a probing (§9), and the +/// fifteenth since the link last returned costs the name, so the forger's +/// packets buy fourteen probings and no more, however long it goes on. A +/// return of the link is no peer's packet, and the count starts over at it. +#[test] +fn forged_answers_that_spare_every_probe_cost_fourteen_probings_and_then_the_name() { + let mut r = held(10_000); + let (mut now, mut sent, mut forged) = (50_000, Vec::new(), 0); + while !matches!(r.claim, Claim::Lost) { + assert_eq!(r.heard(&says(&MOVED), PEER, now), None); + forged += 1; + let Some(at) = r.owed_at() else { break }; + // The name is held again and announced once; the forger strikes before the second. + let round = run(&mut r, LINK, at + 1_749, 0); + assert_eq!(round, claim_of(ADDR, at)[..4], "forged answer {forged}"); + assert_eq!(r.take_event(), Some(Event::Claimed)); + sent.extend(round); + now = at + 1_749; + } + assert_eq!((forged, sent.len()), (CONFLICTS, 14 * 4), "fourteen probings: three probes and an announcement each"); + assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Contested)), None)); + for later in 0..1_000 { + assert_eq!(r.heard(&says(&MOVED), PEER, now + later), None); + } + assert_eq!(run(&mut r, LINK, 3_600_000, 0), [], "and nothing more, whatever it sends"); + + r.link_returned(4_000_000); + assert_eq!(run(&mut r, LINK, 5_000_000, 0), claim_of(ADDR, 4_000_000)); + for forged in 0..14 { + let now = 5_000_000 + forged * 20_000; + assert_eq!(r.heard(&says(&MOVED), PEER, now), None); + assert_eq!(run(&mut r, LINK, now + 19_999, 0), claim_of(ADDR, now), "fourteen more are fourteen since the link returned"); + } + r.link_returned(6_000_000); + assert_eq!(run(&mut r, LINK, 6_100_000, 0), claim_of(ADDR, 6_000_000)); + assert_eq!(r.heard(&says(&MOVED), PEER, 6_100_000), None); + assert_eq!(run(&mut r, LINK, 6_200_000, 0), claim_of(ADDR, 6_100_000), "and the fifteenth is the first since the next return"); +} + +/// Forged probes that win every tiebreak, one behind each first probe: each +/// costs a second's wait and one probe (§8.2), and the fifteenth costs the +/// name. A probe a second never reaches §8.1's fifteen in ten seconds. +#[test] +fn forged_probes_that_win_every_tiebreak_cost_fifteen_probes_and_then_the_name() { + let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); + let mut r = probing(LINK); + let mut sent = vec![1_000]; + for forged in 1..CONFLICTS as u64 { + assert_eq!(r.heard(&later, PEER, 1_000 * forged + 1), None); + assert_eq!(r.take_event(), None); + let probe = run(&mut r, LINK, 1_000 * forged + 1_100, 0); + assert_eq!(probe, [(1_000 * forged + 1_001, probe_for(ADDR))], "a second after forged probe {forged}"); + sent.push(probe[0].0); + } + assert_eq!(r.heard(&later, PEER, 16_000), None); + assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Contested)), None)); + assert_eq!(sent.len(), CONFLICTS); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); +} + +/// Nothing a peer sends is trusted to be a message: no cut of a conflicting +/// response or of a winning probe is either, under the probe or at a held +/// name, and the whole one behind them still is. +#[test] +fn no_cut_of_a_conflicting_response_or_of_a_winning_probe_is_one() { + let (response, later) = (says(&MOVED), probe_of(1, &[(NAME, 1, 1, &MOVED)])); + for cut in 0..response.len() { + let mut r = probing(LINK); + assert_eq!(r.heard(&response[..cut], PEER, 1_100), None); + assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_250)), "under the probe, cut at {cut}"); + let mut r = held(10_000); + assert_eq!(r.heard(&response[..cut], PEER, 50_000), None); + assert_eq!((r.take_event(), r.owed_at()), (None, None), "held, cut at {cut}"); + } + for cut in 0..later.len() { + let mut r = probing(LINK); + assert_eq!(r.heard(&later[..cut], PEER, 1_100), None); + assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_250)), "cut at {cut}"); + } + let mut r = probing(LINK); + assert_eq!(r.heard(&later, PEER, 1_100), None); + assert_eq!(r.owed_at(), Some(2_100)); + assert_eq!(r.heard(&response, PEER, 2_100), None, "in the wait"); + assert_eq!(run(&mut r, LINK, 2_100, 0), [(2_100, probe_for(ADDR))]); + assert_eq!(r.heard(&response, PEER, 2_101), None); + assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); +} + +/// RFC 6762 §8.4: "if any of a host's IP addresses change, it MUST +/// re-announce those address records. The host does not need to repeat the +/// Probing step because it has already established unique ownership of that +/// name." §10.2: a host that "has not been continuously connected and +/// participating on the network link" "MUST first probe": an address after +/// none is probed on. Nothing is answered without an address. +#[test] +fn an_address_after_none_is_probed_for_and_a_new_one_under_a_held_name_is_announced() { + let mut r = Responder::new(host()); + assert!(!answers(&mut r, 0), "no address yet"); + assert_eq!(r.on(None, 0, undrawn), None); + assert_eq!(r.owed_at(), None); + assert_eq!(r.on(Some(LINK), 0, || 0), Some(probe_for(ADDR))); + assert_eq!(run(&mut r, LINK, 5_000, 0), claim_of(ADDR, 0)[1..]); + assert_eq!(r.take_event(), Some(Event::Claimed)); + + let moved = Link { addr: MOVED, prefix: 24 }; + assert_eq!(r.on(Some(moved), 9_000, undrawn), Some(record_of(MOVED)), "announced at once, and not probed for"); + assert_eq!(run(&mut r, moved, 60_000, 0), [(10_000, record_of(MOVED))], "§8.3: and a second later"); + assert_eq!(r.take_event(), None); + + assert_eq!(r.on(Some(moved), 60_500, undrawn), None); + assert_eq!(r.on(None, 60_600, undrawn), None); + assert_eq!(r.owed_at(), None, "nothing is owed for an address no longer held"); + assert!(!answers(&mut r, 60_600), "no address any more"); + assert_eq!(r.on(Some(LINK), 60_700, || 9), None); + assert_eq!(run(&mut r, LINK, 3_600_000, 9), claim_of(ADDR, 60_709), "an address after none is probed on"); + + let mut r = probing(LINK); + 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.take_event(), Some(Event::Lost(Lost::Answered))); + 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(None, 3_000, undrawn), None); + assert_eq!(r.on(Some(LINK), 4_000, || 0), Some(probe_for(ADDR)), "but by one after none"); +} + +/// 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 +/// silently discarded. +#[test] +fn a_query_from_off_the_link_is_not_answered() { + let q = query(0, NAME, 1, 1); + for addr in [[10, 0, 0, 7], [192, 168, 2, 7], [8, 8, 8, 8], [127, 0, 0, 1], [127, 1, 2, 3]] { + for port in [PORT, 53_000] { + assert_eq!(held(10_000).heard(&q, Source { addr, port }, 100_000), None, "{addr:?}:{port}"); + } + } + for addr in [[192, 168, 1, 254], [169, 254, 3, 4]] { + let asked = held(10_000).heard(&q, Source { addr, port: PORT }, 100_000); + assert!(asked.is_some(), "{addr:?}"); + } +} + +#[test] +fn a_host_name_is_one_label() { + assert!(Host::new("toyos-t14").is_ok()); + for bad in ["", "a.b", "-a", "a-", "a b", "é", &"a".repeat(64)] { + assert!(Host::new(bad).is_err(), "{bad:?}"); + } +} + +#[test] +fn the_group_address_maps_to_its_ethernet_address() { + assert_eq!(GROUP_MAC, [0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb]); +} diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs index 3a2ca566cb3..1827d42bcb9 100644 --- a/userland/netstack/node/src/lib.rs +++ b/userland/netstack/node/src/lib.rs @@ -20,8 +20,9 @@ //! [`Node::drain_events`]. //! //! **Draws.** Each `draw` is handed to the client, whose order is its own: a call that starts an -//! exchange draws its transaction id first. The lookups in flight draw after it: an id and then a -//! port for each query they send, and nothing else. +//! exchange draws its transaction id first. The name draws after it, the delay of each probing it +//! starts. The lookups in flight draw after both: an id and then a port for each query they send, +//! and nothing else. #![no_std] #![forbid(unsafe_code)] @@ -210,7 +211,7 @@ impl Node { }; self.carry_out(now, out, verified, draw); } - self.serve_name(now); + self.serve_name(now, draw); self.resolver.pass(now, &mut self.stack, &mut self.counters, draw); } diff --git a/userland/netstack/node/src/name.rs b/userland/netstack/node/src/name.rs index 329db3b1aba..ba8f18c883e 100644 --- a/userland/netstack/node/src/name.rs +++ b/userland/netstack/node/src/name.rs @@ -1,23 +1,25 @@ //! This machine's name on its link: `.local` answers with the held lease's address -//! (`toyos-mdns`, RFC 6762). Every decision about a query, and every byte of an answer, is -//! `toyos_mdns::Responder`'s; this is its socket on the group's port, its membership of the -//! group, and its clock. +//! (`toyos-mdns`, RFC 6762). Every decision about a message, and every byte of a probe or an +//! answer, is `toyos_mdns::Responder`'s; this is its socket on the group's port, its membership +//! of the group, its clock and its draws. //! //! - The record is the held lease's: its address and prefix are what the responder is told, and -//! it is told there is none from the moment the lease goes. Nothing is announced or answered -//! for an address still under probe. +//! it is told there is none from the moment the lease goes. Nothing is probed for, announced or +//! answered for an address still under probe. //! - A link that comes back under a held lease is the responder's to hear of: the lease outlives -//! the link, so no new address would tell it (§8). -//! - What the record is owed later, the second announcement (§8.3) or an answer §6 held back, -//! is a deadline of the node's ([`Name::next_deadline`]). -//! - An announcement that falls due with the link down is [udp]'s to refuse and is counted -//! unsent, and the responder is not told: the link's return owes both announcements again -//! (§8), and holding the unsent one a multicast only ever delays the first of them (§6). -//! - Every response leaves with TTL 255 (§11). +//! the link, so no new address would tell it (§8). It probes for the name again. +//! - 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`]). +//! - 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 the caller's to log ([`Node::name_event`]). +//! - A probe or an announcement that falls due with the link down is [udp]'s to refuse and is +//! counted unsent, and the responder is not told: it holds a name it claimed on no wire until +//! the link's return, which it hears of and probes on, and until then nothing can ask for it. +//! - Every message leaves with TTL 255 (§11). //! - An answer goes where the responder says: to the group on its port, or to the asker's own //! address and port. One [udp] refuses is counted and dropped: the asker asks again. //! -//! **Untrusted input.** A query's bytes are read by the responder's parser alone. Its source +//! **Untrusted input.** A message's bytes are read by the responder's parser alone. Its source //! address and port are the wire's, and are a destination only once the responder took the //! source for one on this link, which is the lease's prefix or 169.254/16, and [udp] accepted it //! as one. @@ -26,7 +28,7 @@ use alloc::vec; use alloc::vec::Vec; use core::net::Ipv4Addr; -use toyos_mdns::{Asker, Host, Link, Responder, To, GROUP, PORT}; +use toyos_mdns::{Event, Host, Link, Responder, Source, To, GROUP, PORT}; use toyos_net_udp::SocketId; use toyos_net_wire::ipv4::{MulticastAddr, Ttl}; use toyos_net_wire::{Instant, Port}; @@ -38,7 +40,7 @@ pub(crate) struct Name { socket: SocketId, record: Responder<'static>, /// Room for the largest datagram [udp] delivers. - query: Vec, + message: Vec, } /// `now` on the clock `toyos-mdns` and `toyos-dns` keep: whole milliseconds of the node's. @@ -61,10 +63,10 @@ impl Name { let (socket, _) = stack.bind(Ipv4Addr::UNSPECIFIED, Port::new(PORT), || 0)?; stack.set_ttl(socket, ttl, ttl)?; stack.join(now, group); - Ok(Self { socket, record: Responder::new(host), query: vec![0; toyos_net_udp::limits::MAX_PAYLOAD] }) + Ok(Self { socket, record: Responder::new(host), message: vec![0; toyos_net_udp::limits::MAX_PAYLOAD] }) } - /// The link came back: the record of a lease that outlived it is owed to the group again. + /// The link came back: the name of a lease that outlived it is probed for again. pub(crate) fn link_returned(&mut self, now: Instant) { self.record.link_returned(millis(now)); } @@ -73,25 +75,26 @@ impl Name { self.record.owed_at().map(Instant::from_millis) } - /// Tells the responder what lease is held, sends what the record is owed now, then answers - /// every query that arrived. Returns how many messages [udp] refused. - fn pass(&mut self, now: Instant, stack: &mut Stack) -> u64 { + /// Tells the responder what lease is held, sends what the name is owed now, then hands it + /// every message that arrived and sends what each is answered. Returns how many messages + /// [udp] refused. + fn pass(&mut self, now: Instant, stack: &mut Stack, draw: &mut impl FnMut() -> u32) -> u64 { let link = stack.lease().map(|lease| Link { addr: lease.address.octets(), prefix: lease.prefix_len }); let (ms, group) = (millis(now), Ipv4Addr::from(GROUP)); let mut unsent = 0u64; - if let Some(record) = self.record.on(link, ms) { - unsent = unsent.saturating_add(send(stack, now, self.socket, group, PORT, &record)); + if let Some(owed) = self.record.on(link, ms, draw) { + unsent = unsent.saturating_add(send(stack, now, self.socket, group, PORT, &owed)); } loop { - let received = match stack.recv_from(self.socket, &mut self.query) { + let received = match stack.recv_from(self.socket, &mut self.message) { Ok(Some(received)) => received, Ok(None) => return unsent, Err(_) => unreachable!("the responder's socket is never closed and never connected"), }; - let Some(query) = self.query.get(..received.len) else { continue }; + let Some(message) = self.message.get(..received.len) else { continue }; // Port 0 is no port to answer at: [udp] refuses the answer. let port = received.source_port.map_or(0, Port::get); - let Some(answer) = self.record.answer(query, Asker { addr: received.source.octets(), port }, ms) else { continue }; + let Some(answer) = self.record.heard(message, Source { addr: received.source.octets(), port }, ms) else { continue }; let (to, port) = match answer.to { To::Group => (group, PORT), To::Asker => (received.source, port), @@ -102,19 +105,25 @@ impl Name { } impl Node { - /// Answers for `.local` from here on: the node joins the multicast DNS group and holds - /// its port, and a lease already held is announced by this call. Refused when the port is - /// taken, by a client's socket or by an earlier call. - pub fn answer_as(&mut self, now: Instant, host: Host<'static>) -> Result<(), toyos_net_udp::Error> { + /// Answers for `.local` from here on, once it is claimed: the node joins the multicast + /// DNS group and holds its port, and a lease already held is probed on by this call, which + /// then spends one draw. Refused when the port is taken, by a client's socket or by an + /// earlier call. + pub fn answer_as(&mut self, now: Instant, host: Host<'static>, mut draw: impl FnMut() -> u32) -> Result<(), toyos_net_udp::Error> { self.name = Some(Name::new(now, &mut self.stack, host)?); - self.serve_name(now); + self.serve_name(now, &mut draw); Ok(()) } + /// The latest of what became of the name since the last call: a line for the log. + pub fn name_event(&mut self) -> Option { + self.name.as_mut()?.record.take_event() + } + /// The name's pass, if it has been started, and the count of what [udp] refused it. - pub(crate) fn serve_name(&mut self, now: Instant) { + pub(crate) fn serve_name(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) { if let Some(name) = &mut self.name { - let unsent = name.pass(now, &mut self.stack); + let unsent = name.pass(now, &mut self.stack, draw); self.counters.add(crate::Counter::NameUnsent, unsent); } } diff --git a/userland/netstack/node/tests/name.rs b/userland/netstack/node/tests/name.rs index f219c3ce10d..df8dcff2036 100644 --- a/userland/netstack/node/tests/name.rs +++ b/userland/netstack/node/tests/name.rs @@ -1,9 +1,9 @@ -//! The machine's `.local` name on the node. No scenario ids: a query's reading and an -//! answer's bytes are `toyos-mdns`'s own tests; these are the node's part, the group, the port, -//! the clock, the lease and where an answer is sent. What is not ours: `etherparse` reads every -//! frame the node emits (`lan::outside`), and every message here, asked or expected, is written -//! out from RFC 1035 §4.1's layout and RFC 6762's rules, by the section each test names, and -//! never by `toyos-mdns`. +//! The machine's `.local` name on the node. No scenario ids: a message's reading, the +//! claim's rules and an answer's bytes are `toyos-mdns`'s own tests; these are the node's part, +//! the group, the port, the clock, the draws, the lease and where a message is sent. What is not +//! ours: `etherparse` reads every frame the node emits (`lan::outside`), and every message here, +//! heard or expected, is written out from RFC 1035 §4.1's layout and RFC 6762's rules, by the +//! section each test names, and never by `toyos-mdns`. mod common; mod lan; @@ -11,11 +11,11 @@ mod lan; use std::net::Ipv4Addr; use std::time::Duration; -use common::{arp, sum, terms, A, BROADCAST, MAC_B, MAC_R, R}; +use common::{arp, sum, terms, A, BROADCAST, MAC, MAC_B, MAC_R, R}; use lan::{udp, Lan, Seen, Udp, B, OFF_LINK}; -use toyos_mdns::Host; +use toyos_mdns::{Event, Host, Lost}; use toyos_net_node::{Counter, Refused}; -use toyos_net_wire::Port; +use toyos_net_wire::{Instant, Port}; /// RFC 6762 §3: the group, and the port every responder listens on. const GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); @@ -25,31 +25,67 @@ const GROUP_MAC: [u8; 6] = [0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb]; /// `toyos.local`, as RFC 1035 §3.1 spells a name: each label behind its length, then the root. const NAME: &[u8] = b"\x05toyos\x05local\x00"; const TYPE_A: u16 = 1; +/// RFC 6762 §8.1: a probe's question is of type "ANY" (255). +const TYPE_ANY: u16 = 255; const CLASS_IN: u16 = 1; /// RFC 6762 §5.4: the top bit of a question's class asks for a unicast response. const UNICAST_RESPONSE: u16 = 0x8000; /// RFC 6762 §10.2: the top bit of a record's class is cache-flush. const CACHE_FLUSH: u16 = 0x8000; +/// RFC 6762 §8.1: the delay before the first probe is at most this, and the probes are this far +/// apart. +const QUARTER: Duration = Duration::from_millis(250); +const SECOND: Duration = Duration::from_secs(1); + +/// The draw of a call that starts no probing. +fn undrawn() -> u32 { + panic!("no probing starts here, so no delay is drawn") +} /// A node told its name, its link down. fn named() -> Lan { let mut lan = Lan::new(); - lan.node.answer_as(lan.now, Host::new("toyos").unwrap()).unwrap(); + lan.node.answer_as(lan.now, Host::new("toyos").unwrap(), undrawn).unwrap(); lan } -/// [`named`], its lease held, both announcements out and two quiet seconds after them: a query -/// from here is answered at once (RFC 6762 §6). +/// The node's datagrams until there are `count` of them, which is at most `limit` away. +fn until(lan: &mut Lan, count: usize, limit: Duration) { + assert!(lan.run_until(limit, |lan| lan.datagrams().len() >= count), "{count} datagrams: {:?}", lan.datagrams()); + assert_eq!(lan.datagrams().len(), count); +} + +/// [`named`], its lease held, its three probes and both announcements out and two quiet seconds +/// after them: a query from here is answered at once (RFC 6762 §6). fn settled() -> Lan { let mut lan = named(); lan.lease(3_600); - assert!(lan.run_until(Duration::from_secs(2), |lan| lan.datagrams().len() == 2), "both announcements"); + until(&mut lan, 5, Duration::from_secs(3)); + assert_eq!(lan.node.name_event(), Some(Event::Claimed)); let quiet = lan.now.after(Duration::from_secs(2)); lan.fire(quiet); - assert_eq!(lan.datagrams().len(), 2); + assert_eq!(lan.datagrams().len(), 5); lan } +/// A message (RFC 1035 §4.1.1): the header, with `counts` questions, answers and authority +/// records, and then `sections` as they are. +fn message(id: u16, flags: u16, counts: [u16; 3], sections: &[&[u8]]) -> Vec { + let mut message = Vec::new(); + for word in [id, flags, counts[0], counts[1], counts[2], 0] { + message.extend_from_slice(&word.to_be_bytes()); + } + for section in sections { + message.extend_from_slice(section); + } + message +} + +/// A question (RFC 1035 §4.1.2) for this machine's name: QNAME, QTYPE, QCLASS. +fn question(qtype: u16, qclass: u16) -> Vec { + [NAME, &qtype.to_be_bytes(), &qclass.to_be_bytes()].concat() +} + /// A query (RFC 1035 §4.1.1, §4.1.2): the header with one question, then QNAME, QTYPE, QCLASS. fn query(id: u16, labels: &[&str], qtype: u16, qclass: u16) -> Vec { let mut query = [&id.to_be_bytes()[..], &[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]].concat(); @@ -73,26 +109,48 @@ fn to_group(from: [u8; 6], source: (Ipv4Addr, u16), payload: &[u8]) -> Vec { udp(GROUP_MAC, from, source, (GROUP, MDNS), payload) } -/// The address record (RFC 1035 §4.1.3): NAME, TYPE, CLASS, TTL, RDLENGTH 4, the address. +/// An address record (RFC 1035 §4.1.3) of this machine's name: NAME, TYPE, CLASS, TTL, RDLENGTH +/// 4, the address. +fn record_of(address: Ipv4Addr, class: u16, ttl: u32) -> Vec { + [NAME, &TYPE_A.to_be_bytes(), &class.to_be_bytes(), &ttl.to_be_bytes(), &[0, 4], &address.octets()].concat() +} + +/// This machine's address record. fn record(class: u16, ttl: u32) -> Vec { - [NAME, &TYPE_A.to_be_bytes(), &class.to_be_bytes(), &ttl.to_be_bytes(), &[0, 4], &A.octets()].concat() + record_of(A, class, ttl) } /// The response RFC 6762 gives a multicast query, and unasked as an announcement: ID 0 and QR /// and AA alone (§18.1 to §18.4), no question (§6), one answer with cache-flush (§10.2) and the /// 120 s of a record naming a host (§10). fn response() -> Vec { - [&[0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0][..], &record(CLASS_IN | CACHE_FLUSH, 120)].concat() + message(0, 0x8400, [0, 1, 0], &[&record(CLASS_IN | CACHE_FLUSH, 120)]) } /// The response RFC 6762 §6.7 gives a legacy resolver: its ID, its question, and a record /// without cache-flush that lives at most ten seconds. fn legacy_response(id: u16) -> Vec { - let question = [NAME, &TYPE_A.to_be_bytes(), &CLASS_IN.to_be_bytes()].concat(); - [&id.to_be_bytes()[..], &[0x84, 0x00, 0, 1, 0, 1, 0, 0, 0, 0], &question, &record(CLASS_IN, 10)].concat() + message(id, 0x8400, [1, 1, 0], &[&question(TYPE_A, CLASS_IN), &record(CLASS_IN, 10)]) } -/// A response to the group. +/// A probe for this machine's name proposing `address` (RFC 6762 §8.1, §8.2): ID 0 and no flag +/// (§18.1, §18.2), the question `ANY` with `qclass`, and the proposed record in the Authority +/// Section, its class without cache-flush, which §10.2 sets in responses. +fn probe_of(address: Ipv4Addr, qclass: u16) -> Vec { + message(0, 0, [1, 0, 1], &[&question(TYPE_ANY, qclass), &record_of(address, CLASS_IN, 120)]) +} + +/// The node's probe: §8.1 has it sent "QU". +fn probe() -> Vec { + probe_of(A, CLASS_IN | UNICAST_RESPONSE) +} + +/// Another host's response naming `address` for this machine's name. +fn says(address: Ipv4Addr) -> Vec { + message(0, 0x8400, [0, 1, 0], &[&record_of(address, CLASS_IN | CACHE_FLUSH, 120)]) +} + +/// A message to the group. fn multicast(payload: Vec) -> Udp { Udp { to: GROUP_MAC, source: A, source_port: MDNS, destination: GROUP, port: MDNS, ttl: 255, payload } } @@ -107,6 +165,32 @@ fn since(lan: &Lan, from: usize) -> Vec { lan.datagrams().into_iter().skip(from).map(|(_, udp)| udp.clone()).collect() } +/// When each of the node's datagrams from `from` on left. +fn times(lan: &Lan, from: usize) -> Vec { + lan.datagrams().into_iter().skip(from).map(|(at, _)| at).collect() +} + +/// Whether `later` is `apart` after `earlier`, on a clock of whole milliseconds, which is the +/// responder's: its deadlines are whole, and the instant it starts from need not be. +fn after(earlier: Instant, later: Instant, apart: Duration) -> bool { + let waited = later.since(earlier); + waited <= apart && waited + Duration::from_millis(1) > apart +} + +/// The claim RFC 6762 §8 asks for, as the node's five datagrams from `from` on, the first probe +/// within §8.1's 250 ms of `start`: three probes 250 ms apart (§8.1), then, 250 ms after the +/// third, the two announcements a second apart (§8.3). +fn claimed(lan: &Lan, from: usize, start: Instant) { + let probes = [probe(), probe(), probe()].map(multicast); + let announcements = [response(), response()].map(multicast); + assert_eq!(since(lan, from), [&probes[..], &announcements].concat()); + let at = times(lan, from); + assert!(at[0] >= start && at[0].since(start) <= QUARTER, "the first probe, {:?} after", at[0].since(start)); + for (earlier, later, apart) in [(at[0], at[1], QUARTER), (at[1], at[2], QUARTER), (at[2], at[3], QUARTER), (at[3], at[4], SECOND)] { + assert!(after(earlier, later, apart), "{:?}, and {apart:?} is asked", later.since(earlier)); + } +} + // RFC 9776 §4: every IGMP message has TTL 1, internetwork-control precedence (ToS 0xC0) and the // Router Alert option (RFC 2113: 0x94, length 4, value 0). §4.2: a version 3 report is type // 0x22, a reserved octet, the checksum, two reserved octets and the count of group records; a @@ -149,40 +233,78 @@ fn a_query_in_a_frame_to_the_groups_address_reaches_the_responder() { assert_eq!(since(&lan, from), [multicast(response())]); } -// RFC 6762 §8.3: "The Multicast DNS responder MUST send at least two unsolicited responses, one -// second apart." The node's clock is the test's, and the second is a deadline the node names. -// Nothing is announced for an address still under probe (RFC 5227 §2.1.1). +// RFC 6762 §8: "it MUST perform the two startup steps below: Probing (Section 8.1) and +// Announcing (Section 8.3)." §8.1: "250 ms after the first query, the host should send a second; +// then, 250 ms after that, a third. If, by 250 ms after the third probe, no conflicting +// Multicast DNS responses have been received, the host may move to the next step, announcing." +// §8.3: "at least two unsolicited responses, one second apart." The node's clock is the test's, +// and each step is a deadline the node names. Nothing is probed for under an address still under +// probe itself (RFC 5227 §2.1.1), and what became of the name is the node's to say. #[test] -fn a_held_lease_is_announced_at_once_and_a_second_later() { +fn a_held_lease_is_probed_for_three_times_and_its_name_announced_only_then() { let mut lan = named(); lan.acknowledge(&terms(3_600, Some(R))); + assert!(lan.datagrams().is_empty(), "an address under probe has no name yet"); assert!(lan.run_until(Duration::from_secs(10), |lan| lan.node.lease().is_some()), "the lease is held"); let held = lan.now; - assert_eq!(lan.datagrams(), [(held, &multicast(response()))], "announced as the lease is held, and not before"); + assert_eq!(lan.node.name_event(), None, "the name is not this machine's yet"); + let asked = to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN)); - let owed = lan.node.next_deadline().expect("the second announcement is owed"); - assert!(owed <= held.after(Duration::from_secs(1)), "and is a deadline of the node's"); - assert!(lan.run_until(Duration::from_secs(2), |lan| lan.datagrams().len() == 2), "the second announcement"); - let (second, again) = lan.datagrams()[1]; - assert_eq!(again, &multicast(response())); - // The responder's clock is whole milliseconds. - let apart = second.since(held); - assert!(apart <= Duration::from_secs(1) && apart > Duration::from_millis(999), "{apart:?} apart"); + until(&mut lan, 3, SECOND); + assert_eq!(lan.node.name_event(), None); + lan.deliver(&asked); + assert_eq!(since(&lan, 0), [probe(), probe(), probe()].map(multicast), "three probes, and no answer under them"); + until(&mut lan, 5, Duration::from_secs(2)); + claimed(&lan, 0, held); + assert_eq!(lan.node.name_event(), Some(Event::Claimed)); let later = lan.now.after(Duration::from_secs(10)); lan.fire(later); - assert_eq!(lan.datagrams().len(), 2, "owed twice, sent twice"); + assert_eq!(lan.datagrams().len(), 5, "owed five, sent five"); + lan.deliver(&asked); + assert_eq!(since(&lan, 5), [unicast(53_000, legacy_response(7))], "and the name is answered with"); +} + +// RFC 6762 §8.1: "the host should first wait for a short random delay time, uniformly +// distributed in the range 0-250 ms." The delay is a draw of the node's caller, taken by the call +// that starts the probing and reduced to that range by the responder: 1,000 is 247 ms. A lease +// held before the name is told is probed on by `answer_as`; one that outlives its link, by the +// call that reports the link's return, behind its client's own draws. +#[test] +fn the_first_probe_follows_the_delay_the_callers_draw_gives() { + let delay = Duration::from_millis(1_000 % 251); + let mut lan = Lan::new(); + lan.lease(3_600); + assert!(lan.datagrams().is_empty()); + let told = lan.now; + lan.node.answer_as(told, Host::new("toyos").unwrap(), || 1_000).unwrap(); + lan.pump(); + assert!(lan.datagrams().is_empty(), "nothing before the delay is over"); + let first = lan.node.next_deadline().expect("the first probe is owed"); + assert!(after(told, first, delay), "{:?} after the name is told", first.since(told)); + until(&mut lan, 5, Duration::from_secs(3)); + assert_eq!(times(&lan, 0)[0], first); + assert_eq!(lan.datagrams()[0].1, &multicast(probe())); + + let quiet = lan.now.after(Duration::from_secs(5)); + lan.fire(quiet); + lan.link(false); + let back = lan.now; + lan.node.link(back, true, || 1_000); + lan.pump(); + assert_eq!(lan.datagrams().len(), 5, "nothing before the delay is over"); + until(&mut lan, 6, SECOND); + assert!(after(back, times(&lan, 5)[0], delay), "{:?} after the link returned", times(&lan, 5)[0].since(back)); } // RFC 6762 §8: "Whenever a Multicast DNS responder starts up, wakes up from sleep, receives an // indication of a network interface "Link Change" event, or has any other reason to believe that // its network connectivity may have changed in some relevant way, it MUST perform the two startup -// steps below: Probing (Section 8.1) and Announcing (Section 8.3)." §8.3: "at least two -// unsolicited responses, one second apart." The node keeps its lease across the link, so the -// address is not new and only the link's return says the name is owed again. §6: a return inside -// the second of the record's last multicast is announced when that second ends. +// steps below: Probing (Section 8.1) and Announcing (Section 8.3)." The node keeps its lease +// across the link, so the address is not new and only the link's return says the name is in +// doubt: it is answered to nobody until it has been probed for again. #[test] -fn a_held_name_is_announced_again_when_the_link_returns() { +fn a_held_name_is_probed_for_again_when_the_link_returns_and_announced_only_then() { let mut lan = settled(); let from = lan.datagrams().len(); lan.link(false); @@ -193,55 +315,119 @@ fn a_held_name_is_announced_again_when_the_link_returns() { lan.link(true); let back = lan.now; assert!(lan.node.lease().is_some(), "the lease outlived the link"); - assert_eq!(lan.datagrams()[from..], [(back, &multicast(response()))], "announced as the link returns"); - let owed = lan.node.next_deadline().expect("the second announcement is owed"); - assert!(owed <= back.after(Duration::from_secs(1)), "and is a deadline of the node's"); - assert!(lan.run_until(Duration::from_secs(2), |lan| lan.datagrams().len() == from + 2), "the second announcement"); - let (second, again) = lan.datagrams()[from + 1]; - assert_eq!(again, &multicast(response())); - // The responder's clock is whole milliseconds. - let apart = second.since(back); - assert!(apart <= Duration::from_secs(1) && apart > Duration::from_millis(999), "{apart:?} apart"); + until(&mut lan, from + 1, SECOND); + lan.deliver(&to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN))); + assert_eq!(since(&lan, from), [multicast(probe())], "probed for, and answered to nobody under the probe"); + assert_eq!(lan.node.name_event(), None); + + until(&mut lan, from + 5, Duration::from_secs(3)); + claimed(&lan, from, back); + assert_eq!(lan.node.name_event(), Some(Event::Claimed)); let quiet = lan.now.after(Duration::from_secs(10)); lan.fire(quiet); - assert_eq!(lan.datagrams().len(), from + 2, "owed twice, sent twice"); + assert_eq!(lan.datagrams().len(), from + 5, "owed five, sent five"); +} - let asked = lan.now; - lan.deliver(&to_group(MAC_B, (B, MDNS), &ours(0, CLASS_IN))); +// A probe or an announcement that falls due with the link down: [udp] has no route for it, so it +// is counted and gone, and the responder holds it sent. The name it then claims was probed for +// on no wire, and nobody can ask for it there; the link's return is told to the responder, which +// probes for it again (RFC 6762 §8) before a host on the link hears it announced. +#[test] +fn what_the_name_is_owed_with_the_link_down_is_counted_and_the_links_return_probes_for_it() { + let mut lan = named(); + lan.lease(3_600); lan.link(false); + let left = lan.datagrams().len(); + assert!(!lan.run_until(Duration::from_secs(5), |_| false), "every deadline of five seconds"); + assert_eq!(lan.datagrams().len(), left, "nothing leaves a link that is down"); + let unsent = lan.node.counters().get(Counter::NameUnsent); + assert_eq!(u64::try_from(left).unwrap() + unsent, 5, "three probes and two announcements, each sent or counted"); + assert!(unsent >= 4, "all but a probe that left before the link went"); + assert_eq!(lan.node.name_event(), Some(Event::Claimed), "held, on no wire"); + + lan.now = lan.now.after(Duration::from_secs(5)); lan.link(true); - assert_eq!(lan.datagrams()[from + 2..], [(asked, &multicast(response()))], "the answer, and no announcement in its second"); - assert!(lan.run_until(Duration::from_secs(3), |lan| lan.datagrams().len() == from + 5), "both announcements"); - let after: Vec = lan.datagrams()[from + 3..].iter().map(|(at, _)| at.since(asked)).collect(); - assert!(after[0] <= Duration::from_secs(1) && after[0] > Duration::from_millis(999), "{after:?}"); - assert!(after[1] <= Duration::from_secs(2) && after[1] > Duration::from_millis(1_999), "{after:?}"); + let back = lan.now; + until(&mut lan, left + 5, Duration::from_secs(3)); + claimed(&lan, left, back); + assert_eq!(lan.node.name_event(), Some(Event::Claimed)); + assert_eq!(lan.node.counters().get(Counter::NameUnsent), unsent); } -// RFC 6762 §8.3's second announcement falling due with the link down: [udp] has no route for it, -// so it is counted and gone, and the responder holds it sent. The link's return owes both again -// (§8, §8.3: two, a second apart), the first when the second of the unsent one ends: §6's -// interval is kept with time to spare and never cut short. +// RFC 6762 §8.1: "During probing, from the time the first probe packet is sent until 250 ms after +// the third probe, if any conflicting Multicast DNS response is received, then the probing host +// MUST defer to the existing host". The node's probes are "QU", so the host that holds the name +// may answer at the node's own address as well as to the group, and either reaches the +// responder. The name is then nobody's here: not announced, not answered with, and said lost. +// The link's return is a reason to probe for it again (§8). #[test] -fn an_announcement_due_with_the_link_down_is_counted_and_the_links_return_owes_both() { +fn another_hosts_answer_under_the_probe_takes_the_name_and_the_node_says_so() { + let to_the_group = to_group(MAC_B, (B, MDNS), &says(B)); + let to_the_node = udp(MAC, MAC_B, (B, MDNS), (A, MDNS), &says(B)); + for defended in [to_the_group, to_the_node] { + let mut lan = named(); + lan.lease(3_600); + until(&mut lan, 1, SECOND); + lan.deliver(&defended); + assert_eq!(lan.node.name_event(), Some(Event::Lost(Lost::Answered))); + let later = lan.now.after(Duration::from_secs(60)); + 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))); + assert_eq!(since(&lan, 0), [multicast(probe())], "one probe, and then no probe, no announcement and no answer"); + assert_eq!(lan.node.name_event(), None, "said once"); + + lan.link(false); + lan.link(true); + let back = lan.now; + until(&mut lan, 6, Duration::from_secs(3)); + claimed(&lan, 1, back); + assert_eq!(lan.node.name_event(), Some(Event::Claimed), "the other host is gone, and the name is this machine's"); + } +} + +// 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 +// it defers to the winning host by waiting one second, and then begins probing for this record +// again." The node is 192.0.2.1: B's 192.0.2.7 is later, and 192.0.2.0 earlier. +#[test] +fn a_later_probe_of_another_hosts_makes_the_node_wait_a_second_and_probe_again() { let mut lan = named(); lan.lease(3_600); - let held = lan.now; - assert_eq!(lan.datagrams(), [(held, &multicast(response()))], "the first announcement"); - lan.link(false); - let due = held.after(Duration::from_secs(1)); - lan.fire(due); - assert_eq!((lan.datagrams().len(), lan.node.counters().get(Counter::NameUnsent)), (1, 1), "the second has no link to leave on"); + until(&mut lan, 1, SECOND); + let first = times(&lan, 0)[0]; + lan.deliver(&to_group(MAC_B, (B, MDNS), &probe_of(Ipv4Addr::new(192, 0, 2, 0), CLASS_IN | UNICAST_RESPONSE))); + until(&mut lan, 2, SECOND); + assert!(after(first, times(&lan, 0)[1], QUARTER), "an earlier probe is ignored"); + + let heard = lan.now; + lan.deliver(&to_group(MAC_B, (B, MDNS), &probe_of(B, CLASS_IN | UNICAST_RESPONSE))); + until(&mut lan, 3, Duration::from_secs(2)); + assert!(after(heard, times(&lan, 0)[2], SECOND), "a later one is deferred to for a second: {:?}", times(&lan, 0)[2].since(heard)); + until(&mut lan, 7, Duration::from_secs(3)); + claimed(&lan, 2, times(&lan, 0)[2]); + assert_eq!(lan.node.name_event(), Some(Event::Claimed), "nobody answered the second probing"); +} - lan.now = due.after(Duration::from_millis(300)); - lan.link(true); - assert_eq!(lan.datagrams().len(), 1, "300 ms after the unsent one"); - assert!(lan.run_until(Duration::from_secs(3), |lan| lan.datagrams().len() == 3), "both announcements"); - let after: Vec = lan.datagrams()[1..].iter().map(|(at, _)| at.since(due)).collect(); - assert!(after[0] <= Duration::from_secs(1) && after[0] > Duration::from_millis(999), "{after:?}"); - assert!(after[1] <= Duration::from_secs(2) && after[1] > Duration::from_millis(1_999), "{after:?}"); - let quiet = lan.now.after(Duration::from_secs(10)); - lan.fire(quiet); - assert_eq!((lan.datagrams().len(), lan.node.counters().get(Counter::NameUnsent)), (3, 1), "owed twice, sent twice"); +// RFC 6762 §9: a response that conflicts with a held name means the responder "MUST immediately +// reset its conflicted unique record to probing state, and go through the startup steps described +// above in Section 8". §8.1: another host's probe is answered "to defend that name immediately", +// by unicast where it asked "QU". +#[test] +fn a_held_name_is_defended_and_probed_for_again_when_another_host_answers_for_it() { + let mut lan = settled(); + let from = lan.datagrams().len(); + lan.deliver(&to_group(MAC_B, (B, MDNS), &probe_of(B, CLASS_IN | UNICAST_RESPONSE))); + assert_eq!(since(&lan, from), [unicast(MDNS, response())], "B's probe is answered at B's address, at once"); + + let contested = lan.now; + lan.deliver(&to_group(MAC_B, (B, MDNS), &says(B))); + lan.deliver(&to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN))); + assert_eq!(lan.datagrams().len(), from + 1, "the name is answered to nobody"); + until(&mut lan, from + 6, Duration::from_secs(3)); + claimed(&lan, from + 1, contested); + assert_eq!(lan.node.name_event(), Some(Event::Claimed), "B said no more: the name is held again"); } // RFC 6762 §6.7: a query from a port other than 5353 is a legacy resolver's, and its answer goes @@ -273,13 +459,11 @@ fn a_second_query_inside_a_second_waits_for_the_second_to_end() { assert_eq!(lan.datagrams()[from..], [(asked, &multicast(response()))], "the first is answered at once, the others not yet"); let owed = lan.node.next_deadline().expect("the held answer is owed"); - assert!(owed <= asked.after(Duration::from_secs(1)), "and is a deadline of the node's"); + assert!(owed <= asked.after(SECOND), "and is a deadline of the node's"); assert!(lan.run_until(Duration::from_secs(3), |lan| lan.datagrams().len() == from + 2), "the held answer leaves"); let (answered, again) = lan.datagrams()[from + 1]; assert_eq!(again, &multicast(response())); - // The responder's clock is whole milliseconds. - let apart = answered.since(asked); - assert!(apart <= Duration::from_secs(1) && apart > Duration::from_millis(999), "{apart:?} after the first"); + assert!(after(asked, answered, SECOND), "{:?} after the first", answered.since(asked)); let later = lan.now.after(Duration::from_secs(10)); lan.fire(later); @@ -308,30 +492,15 @@ fn an_answer_to_a_link_local_asker_goes_to_its_own_link_address() { assert_eq!(since(&lan, from), [on_the_link]); } -// A lease held before the name is told is as new to the responder as one that comes after: -// announced by the call (RFC 6762 §8.3), its second a deadline from then. -#[test] -fn a_lease_already_held_is_announced_as_the_name_is_told() { - let mut lan = Lan::new(); - lan.lease(3_600); - assert!(lan.datagrams().is_empty()); - let told = lan.now; - lan.node.answer_as(told, Host::new("toyos").unwrap()).unwrap(); - let owed = lan.node.next_deadline().expect("the second announcement is owed"); - assert!(owed <= told.after(Duration::from_secs(1))); - lan.pump(); - assert_eq!(lan.datagrams(), [(told, &multicast(response()))]); -} - // RFC 6762 §11: "All Multicast DNS responses (including responses sent via unicast) SHOULD be -// sent with IP TTL set to 255." +// sent with IP TTL set to 255." The probes leave the socket the responses do. #[test] -fn every_response_leaves_with_ttl_255() { +fn every_message_leaves_with_ttl_255() { let mut lan = settled(); lan.deliver(&to_group(MAC_B, (B, 53_000), &ours(1, CLASS_IN))); lan.deliver(&to_group(MAC_B, (B, MDNS), &ours(0, CLASS_IN))); let ttls: Vec = lan.datagrams().iter().map(|(_, udp)| udp.ttl).collect(); - assert_eq!(ttls, [255; 4], "two announcements, a unicast answer and a multicast one"); + assert_eq!(ttls, [255; 7], "three probes, two announcements, a unicast answer and a multicast one"); } // The record is the held lease's: no answer is made up for an address under probe (RFC 5227 @@ -345,9 +514,9 @@ fn the_name_is_answered_only_while_its_lease_is_held() { assert!(lan.datagrams().is_empty(), "under probe"); assert!(lan.run_until(Duration::from_secs(10), |lan| lan.node.lease().is_some()), "the lease is held"); - let from = lan.datagrams().len(); + until(&mut lan, 5, Duration::from_secs(3)); lan.deliver(&asked); - assert_eq!(since(&lan, from), [unicast(53_000, legacy_response(7))], "held"); + assert_eq!(since(&lan, 5), [unicast(53_000, legacy_response(7))], "held, and its name claimed"); assert!(lan.run_until(Duration::from_secs(700), |lan| lan.node.lease().is_none()), "the lease runs out"); let from = lan.datagrams().len(); @@ -404,12 +573,12 @@ fn the_names_port_is_held_by_one_socket() { let mdns = Port::new(MDNS); let mut lan = named(); assert_eq!(lan.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, || 0).map(|(_, port)| port), Err(Refused::AddrInUse)); - let again = lan.node.answer_as(lan.now, Host::new("toyos").unwrap()); + let again = lan.node.answer_as(lan.now, Host::new("toyos").unwrap(), undrawn); assert_eq!(again, Err(toyos_net_udp::Error::Refused(toyos_net_udp::Counter::PortInUse))); let mut taken = Lan::new(); taken.node.udp_bind(Ipv4Addr::UNSPECIFIED, mdns, || 0).unwrap(); - let refused = taken.node.answer_as(taken.now, Host::new("toyos").unwrap()); + let refused = taken.node.answer_as(taken.now, Host::new("toyos").unwrap(), undrawn); assert_eq!(refused, Err(toyos_net_udp::Error::Refused(toyos_net_udp::Counter::PortInUse))); taken.lease(3_600); assert!(!taken.sent.iter().any(|(_, seen)| matches!(seen, Seen::Igmp { .. })), "no group was joined"); diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs index 0d61d790988..880b4a0cefa 100644 --- a/userland/netstack/node/tests/resolve.rs +++ b/userland/netstack/node/tests/resolve.rs @@ -889,7 +889,7 @@ fn a_lookup_is_not_carried_by_a_later_ones_schedule() { #[test] fn a_reply_is_read_only_at_the_address_its_query_left_from() { let mut net = Net::leased(&[ANSWERS], Some(R)); - net.lan.node.answer_as(net.lan.now, toyos_mdns::Host::new("toyos").unwrap()).unwrap(); + net.lan.node.answer_as(net.lan.now, toyos_mdns::Host::new("toyos").unwrap(), || 0).unwrap(); let id = net.resolve("www.example").unwrap(); net.pass(); let asked = net.queried[0].clone(); diff --git a/userland/netstack/src/mdns.rs b/userland/netstack/src/mdns.rs index b88b38bcf43..bedc1cb6077 100644 --- a/userland/netstack/src/mdns.rs +++ b/userland/netstack/src/mdns.rs @@ -3,12 +3,17 @@ //! the network reaches it by name with nothing configured on either — the //! development host finds the T14's served log this way. //! -//! **Answered only while an address is held**, and announced on every new -//! one. Every decision is `toyos_mdns::Responder`'s; this is the socket and -//! the clock. What the record is owed later — the second announcement (§8.3), -//! or an answer §6 held back — is a wake of netstack's own loop -//! ([`Responder::wake_in`]) rather than a sleep, because the protocol names -//! the interval and nothing on the wire says when it has passed. +//! **Answered only while an address is held and the name is claimed**: every +//! address after none is probed on first (§8.1), which takes the name's first +//! answer three quarters of a second and a drawn delay past the lease, and a +//! name another host answers for is not this machine's. Every decision is +//! `toyos_mdns::Responder`'s; this is the socket, the clock, the kernel's +//! random source for the delay the responder asks a draw for, and the log +//! line for what became of the name. What the name is owed later — a probe, +//! an announcement (§8.3), or an answer §6 held back — is a wake of +//! netstack's own loop ([`Responder::wake_in`]) rather than a sleep, because +//! the protocol names the interval and nothing on the wire says when it has +//! passed. use std::net::Ipv4Addr; use std::time::{Duration, Instant}; @@ -16,15 +21,16 @@ use std::time::{Duration, Instant}; use smoltcp::iface::{Interface, SocketHandle, SocketSet}; use smoltcp::socket::udp; use smoltcp::wire::{IpAddress, IpCidr, IpEndpoint}; -use toyos_mdns::{Asker, Host, Link, To, GROUP, PORT}; +use toyos_mdns::{Event, Host, Link, Lost, Source, To, CONFLICTS, GROUP, PORT}; -/// A query is a few hundred bytes; this holds a handful of them between two -/// passes, and a query past it is dropped by the socket, which is what the -/// asker's own retry is for. +/// 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 +/// sender's own retry is for. const BUFFER: usize = 4096; pub struct Responder { handle: SocketHandle, + host: &'static str, record: toyos_mdns::Responder<'static>, /// The origin of the responder's clock. born: Instant, @@ -34,7 +40,7 @@ impl Responder { /// Join the group and bind its port. `host` is the name this machine asks /// its network to record for it (`dhcp::HOSTNAME`). pub fn new(host: &'static str, iface: &mut Interface, socket_set: &mut SocketSet<'static>) -> Self { - let host = Host::new(host).unwrap_or_else(|_| panic!("netstack: {host:?} is no host name")); + let label = Host::new(host).unwrap_or_else(|_| panic!("netstack: {host:?} is no host name")); iface .join_multicast_group(IpAddress::Ipv4(Ipv4Addr::from(GROUP))) .expect("netstack: the multicast DNS group is the one group this interface joins"); @@ -43,11 +49,12 @@ impl Responder { }; let mut socket = udp::Socket::new(buffer(), buffer()); socket.bind(PORT).expect("netstack: nothing else binds the multicast DNS port"); - Self { handle: socket_set.add(socket), record: toyos_mdns::Responder::new(host), born: Instant::now() } + Self { handle: socket_set.add(socket), host, record: toyos_mdns::Responder::new(label), born: Instant::now() } } - /// After each poll: send what the record is owed now, then answer every - /// query that arrived. + /// After each poll: send what the name is owed now, hand the responder + /// every message that arrived and send what each is answered, then say + /// what became of the name. pub fn pass(&mut self, iface: &Interface, socket_set: &mut SocketSet<'_>, now: Instant) { let socket = socket_set.get_mut::(self.handle); // IPv4 is the one protocol this netstack is built with, so every address is one. @@ -57,13 +64,13 @@ impl Responder { }); let now_ms = self.ms(now); let group = IpEndpoint::new(IpAddress::Ipv4(Ipv4Addr::from(GROUP)), PORT); - if let Some(record) = self.record.on(link, now_ms) { - send(socket, &record, group); + if let Some(owed) = self.record.on(link, now_ms, || u32::from(crate::resolve::random_u16())) { + send(socket, &owed, group); } - while let Ok((query, meta)) = socket.recv() { + while let Ok((message, meta)) = socket.recv() { let IpAddress::Ipv4(from) = meta.endpoint.addr; - let asker = Asker { addr: from.octets(), port: meta.endpoint.port }; - let Some(answer) = self.record.answer(query, asker, now_ms) else { + let from = Source { addr: from.octets(), port: meta.endpoint.port }; + let Some(answer) = self.record.heard(message, from, now_ms) else { continue; }; let to = match answer.to { @@ -72,9 +79,20 @@ impl Responder { }; send(socket, &answer.bytes, to); } + let host = self.host; + match self.record.take_event() { + Some(Event::Claimed) => crate::say!("netstack: mDNS: no host answered for {host}.local; this machine answers as it"), + Some(Event::Lost(Lost::Answered)) => crate::say!( + "netstack: mDNS: another host answered for {host}.local; this machine answers to no name" + ), + Some(Event::Lost(Lost::Contested)) => crate::say!( + "netstack: mDNS: {host}.local was contested {CONFLICTS} times; this machine answers to no name" + ), + None => {} + } } - /// When the loop must wake for what the record is owed, if anything is. + /// When the loop must wake for what the name is owed, if anything is. pub fn wake_in(&self, now: Instant) -> Option { self.record.owed_at().map(|at| Duration::from_millis(at.saturating_sub(self.ms(now)))) } @@ -109,19 +127,20 @@ mod tests { let buffer = || udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0u8; 64]); let mut set = SocketSet::new(Vec::new()); let handle = set.add(udp::Socket::new(buffer(), buffer())); - Responder { handle, record: toyos_mdns::Responder::new(Host::new("t14").unwrap()), born: Instant::now() } + Responder { handle, host: "t14", record: toyos_mdns::Responder::new(Host::new("t14").unwrap()), born: Instant::now() } } - /// **A wake is asked for exactly what the record owes.** Nothing before - /// an address is held; the §8.3 second announcement's own instant once - /// `on` schedules it. A responder that never asks for this wake answers a - /// query §6 held back only on some other, unrelated one. + /// **A wake is asked for exactly what the name owes.** Nothing before an + /// address is held; the first probe's own instant, the drawn delay after + /// the address, once `on` schedules it. A responder that never asks for + /// this wake sends that probe, and every one after it, only on some + /// other, unrelated wake. #[test] - fn wake_in_asks_for_what_the_record_owes_and_nothing_else() { + fn wake_in_asks_for_what_the_name_owes_and_nothing_else() { let mut r = responder(); assert_eq!(r.wake_in(r.born), None, "nothing is owed before an address is held"); - r.record.on(Some(LINK), 0); - let owed_ms = r.record.owed_at().expect("§8.3 owes the second announcement"); - assert_eq!(r.wake_in(r.born), Some(Duration::from_millis(owed_ms))); + assert_eq!(r.record.on(Some(LINK), 0, || 100), None, "§8.1: the delay before the first probe"); + assert_eq!(r.record.owed_at(), Some(100)); + assert_eq!(r.wake_in(r.born), Some(Duration::from_millis(100))); } } From 03eb72f9497059c90666fd40764c3fa69cee98cd Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 11:49:18 +0200 Subject: [PATCH 2/3] =?UTF-8?q?The=20name's=20conflict=20bound=20is=20RFC?= =?UTF-8?q?=206762=20=C2=A78.1's,=20the=20shipped=20netstack=20tells=20its?= =?UTF-8?q?=20responder=20of=20its=20link,=20and=20the=20name's=20events?= =?UTF-8?q?=20ride=20the=20node's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review of the probing stage sent it back on four things. The bound on a peer was a count: the fifteenth conflict since the link's return dropped a held name without the probing §9 requires, never decayed and defended nothing. It is §8.1's now, the simple-device form: every probe attempt that would begin within ten seconds of a conflict waits five seconds first, whatever begins it. A conflict with none in the ten seconds before it is probed on at once, as §9 asks. A peer buys at most one probing in five seconds, and the name is held again when it stops. The times of the last fifteen conflicts, the count since the link's return, Lost::Contested and its log line are gone; the state a conflict writes is the claim and one time. The netstack that ships never told its responder the link returned, though main.rs holds that edge. A responder's link is now an address on a link that is up: both callers say they have none while the link is down, so the link's return is a link after none, which is probed on. Responder::link_returned, the node's Name::link_returned and the residual of a name said claimed on no wire go together. A pass was "what is owed, then what arrived", so a conflicting response already received when a probing's last 250 ms ended was read after the name was claimed and announced. `on` is split: the link, then every message, then what is owed, in both callers. What became of the name left the node by Node::name_event, a second way out that kept only the latest. The responder's calls return their events, and the node's are a variant of its Event, drained with the rest. The shell's responder takes its draw as the resolver does, which with smoltcp's loopback device is the seam its three host tests drive. The netcase boot waits for the claim line by event, so a shipped responder whose probing never ends turns two guest tests red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- issues/toyos-has-its-own-network-stack.md | 2 +- tests/toyos.rs | 9 +- userland/netstack/mdns/src/lib.rs | 333 ++++++++------------- userland/netstack/mdns/src/tests.rs | 347 ++++++++++++---------- userland/netstack/node/src/lib.rs | 13 +- userland/netstack/node/src/name.rs | 66 ++-- userland/netstack/node/tests/name.rs | 80 +++-- userland/netstack/src/main.rs | 4 +- userland/netstack/src/mdns.rs | 202 +++++++++---- 9 files changed, 572 insertions(+), 484 deletions(-) diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 891900dbc4d..888bbd7964a 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. Netstack on smoltcp tells its responder of no link's return: `userland/netstack/src/main.rs` keeps a lease across a flap and says nothing to `mdns`, so there a returning link is neither probed nor announced on, and a lost name is probed for again only with an address after none. The node hands the responder a probe's delay and no word that the link is down: a probe that falls due then is counted `node.name-unsent` and held sent, so the responder says the name claimed on no wire, until the link's return probes for it (`what_the_name_is_owed_with_the_link_down_is_counted_and_the_links_return_probes_for_it`, `userland/netstack/node/tests/name.rs`). What became of the name leaves the node by `Node::name_event`, a second way out beside `Node::drain_events`. A response is read however it was addressed, where §6 reads a unicast one only within two seconds of a question that asked for one: the caller does not say how a message arrived. Exit: each machine's name is its own, and a test with two guests on one network finds each by its name; netd runs on the node, whose `Node::link` tells the name of a link's return; the responder is told the link went down and probes for nothing on it; the name's line leaves by `Node::drain_events`; and the caller says how a message was addressed, with a test in which a unicast response nobody asked for takes no name. +- 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. 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. - A query [ip] refuses at the transmit opportunity that would have carried it, its next hop held failed or its table full, is reported to its socket inside `Node::transmit`, which carries no lookup on: the resolver reads the report at the node's next `receive`, `fire` or `link`, which at the latest is the end of the query's wait, where a query [udp] refuses in the resolver's own call, or one reported in a frame or at a deadline, ends its wait at once. `a_query_ip_refuses_at_its_transmit_opportunity_is_read_at_the_nodes_next_wake` holds it as it stands. It is no regression: what ships waits every such query out. Until that read, an ICMP error that quotes the query's addresses and ports takes [ip]'s word's place on the socket, [udp] holding one pending error a socket, the latest: the query is then waited out and not let go, which costs the early end and never an answer. Owner: the slice in which datagram senders wait on the hop (`Node::transmit` is the node's `lib.rs`). Exit: the node carries its lookups on after the stack's transmit, or [udp] refuses in the call a datagram whose next hop [ip] holds failed, so that no report waits on a socket to be displaced, and that test finds its lookup ended at the 4 s it started. - [ip]'s queue for a next hop it is resolving is every sender's: `PENDING_PER_NEIGHBOUR` is 16, a stopgap which the node's resolver asserts is no fewer than `toyos_dns::MAX_LOOKUPS`, so every held lookup's first query waits there whole, but a datagram a client sends to the same next hop in that window, which for everything off the link is the router, takes the oldest one's place (`nb.pending-overflow`, RFC 4861 §7.2.2), and a query lost so costs its lookup a `WAIT_MS`; smoltcp kept each datagram in its own socket until the neighbour answered. `a_clients_datagram_behind_the_burst_takes_the_oldest_querys_place_in_ips_queue` holds it as it stands. It is a regression against what ships for more than 16 datagrams at one next hop not yet resolved, and would ship at the move; and it sizes a limit of [ip]'s by a constant of the resolver's, which leaves four unresolved next hops filling `PENDING_TOTAL` where eight did. Owner: the slice in which datagram senders wait on the hop, where [udp]'s datagram calls already go through `Stack` and `Shard`. Its design: [udp]'s `serve` asks [ip] the next-hop question before it hands a datagram over, as [tcp]'s exit does with `Hop` (NUD-25, IP-D5); a socket whose oldest datagram's hop is pending keeps it in its own queue, which `TX_DATAGRAMS` and `TX_BYTES` already bound and refuse in the call, and is offered again when the hop resolves and told when it fails; [ip] then queues only its own datagrams. Exit, before the move: that test finds every query on the wire once the resolver answers ARP; `PENDING_PER_NEIGHBOUR` is `limits.ip.nud.pending_per_neighbour`'s 8 again; and the resolver's compile-time assertion, the change to `s_ip_nud_004_overflow_drops_the_oldest` and stage 3's departure line about it are deleted. - `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/tests/toyos.rs b/tests/toyos.rs index e67da326128..47ab2dc28d8 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2959,18 +2959,23 @@ fn holding_server() -> Result { Ok(port) } -/// Boot `tests/netcase` with these binaries staged, to netstack's lease: its -/// jobs name their peer by an address. +/// Boot `tests/netcase` with these binaries staged, to netstack's lease, since +/// its jobs name their peer by an address, and then to the claim of its name: +/// a responder whose probing never ends leaves the machine nameless, and +/// nothing else on a boot reads the name. fn boot_netcase( c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)], options: BootOptions, ) -> Result { const LEASED: &str = "netstack: DHCP: lease "; + /// `userland/netstack/src/mdns.rs`'s line for a probing no host answered. + const CLAIMED: &str = "netstack: mDNS: no host answered for "; let case = compile::repo_root().join("tests/netcase"); let mut qemu = QemuInstance::boot_with_options(&case, c_bins, rust_bins, options); let mut console = qemu.boot_log().to_string(); await_marker(&mut qemu, &mut console, LEASED, "netstack's lease").map_err(|e| format!("{e}\n{console}"))?; + await_marker(&mut qemu, &mut console, CLAIMED, "netstack to claim its name").map_err(|e| format!("{e}\n{console}"))?; Ok(qemu) } diff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs index e542cdbd58f..7465e68b5d5 100644 --- a/userland/netstack/mdns/src/lib.rs +++ b/userland/netstack/mdns/src/lib.rs @@ -9,8 +9,10 @@ //! what RFC 6762 §6 asks of a responder that has no answer. //! //! **The name is claimed before it is used** (§8: "Probing (Section 8.1) and -//! Announcing (Section 8.3)"), on every address that comes after none and -//! whenever the caller says its link came back ([`Responder::link_returned`]): +//! Announcing (Section 8.3)"), on every link that comes after none +//! ([`Responder::on`]). A link is an address on a link that is up: the caller +//! says it has none while it holds no address and while its link is down, so +//! an address after none and a link's return are one event here. //! //! - §8.1: after a delay the caller draws, uniform in 0 to 250 ms, three //! probes 250 ms apart, each the question `ANY` for the name with the @@ -31,37 +33,44 @@ //! host by waiting one second, and then begins probing for this record //! again". //! - §9: a conflicting response to a held name means it "MUST immediately -//! reset its conflicted unique record to probing state", and what the probe -//! then hears decides it. A held name is defended by answering: another -//! host's probe for it is a question like any other, multicast as soon as -//! 250 ms after the record's last multicast (§6) or unicast at once. +//! reset its conflicted unique record to probing state", and "the protocol +//! used in the Probing phase will determine a winner and a loser". A held +//! name is defended by answering: another host's probe for it is a question +//! like any other, multicast as soon as 250 ms after the record's last +//! multicast (§6) or unicast at once. +//! +//! **The caller reads before it asks what is owed.** A pass is +//! [`Responder::on`], then [`Responder::heard`] for every message that has +//! arrived, then [`Responder::owed`]: a conflicting response already received +//! 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 -//! the link to return or an address after none. 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. +//! 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 +//! 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. //! //! **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 and -//! the times of its last [`CONFLICTS`] conflicts. -//! - One conflict costs at most three probes and two announcements. +//! - It keeps nothing a message brought: its state is its fixed fields, of +//! which a conflict writes the claim 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 //! host MUST wait at least five seconds before each successive additional -//! probe attempt", for as long as conflicts keep coming less than ten -//! seconds apart. -//! - Between two events that are no message read here, the link's return or -//! an address after none, a name is contested [`CONFLICTS`] times at most: -//! the conflict that makes it so loses the name ([`Lost::Contested`]), so -//! no peer keeps this responder probing, and the most its messages have -//! cost is that many rounds of three probes and two announcements. -//! - A lost name sends nothing and reads nothing until one of those two -//! events. An address after none is whoever leases this machine its address -//! to cause, which decides what the record says anyway. +//! probe attempt", and "a valid way to comply with this requirement is to +//! always wait five seconds after any failed probe attempt before trying +//! again." Here every probe attempt that would begin within ten seconds of +//! a conflict waits five seconds first, whatever begins it: a conflict at a +//! held name, a lost tiebreak or a link after none. A conflict with none in +//! 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. //! //! Where an answer goes follows the question (§5.4, §6.7): //! @@ -77,10 +86,14 @@ //! record is multicast at most once a second (§6) — announcements included — //! with a query inside that second answered when it ends ([`Responder`]). //! -//! **Not implemented, and so not claimed:** §6's rule that a unicast response -//! is read only within two seconds of a question that asked for one. The -//! caller does not say how a message was addressed, so a response is read -//! however it came; a host that can send one can multicast it too. +//! **Not implemented, and so not claimed.** The caller does not say how a +//! message was addressed, so two rules that turn on it are not applied. §6: a +//! unicast response is read only within two seconds of a question that asked +//! for one; here a response is read however it came, and a host that can send +//! one can multicast it too. §11: a message sent to the group is on the link +//! "regardless of source IP address"; here one from a source outside the +//! subnet is ignored however it was addressed, so a host of this name on +//! another subnet of the same link is never a conflict. //! //! A message is read off the wire by `toyos-dns`, the reader a resolver's //! reply is read by. @@ -121,10 +134,6 @@ pub const TTL: u32 = 120; /// §6.7: an answer to a legacy resolver carries a TTL of at most ten seconds. pub const LEGACY_TTL: u32 = 10; -/// §8.1: "If fifteen conflicts occur within any ten-second period". It is -/// also how often a name is contested before it is given up. -pub const CONFLICTS: usize = 15; - /// The domain every name here is under (§3). const LOCAL: &[u8] = b"local"; @@ -258,19 +267,10 @@ pub enum Event { /// Probing ended with no host answering: the name is this host's, and is /// announced and answered with from here on. Claimed, - /// The name is not this host's, and nothing is announced or answered - /// until the link returns or an address comes after none. - Lost(Lost), -} - -/// Why the name was lost. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Lost { - /// §8.1: another host answered for the name under the probe. - Answered, - /// The name was contested [`CONFLICTS`] times since the link last - /// returned or an address came after none. - Contested, + /// §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. + Lost, } /// §8.1: "the host should first wait for a short random delay time, uniformly @@ -288,8 +288,8 @@ const PROBES: u8 = 3; /// waiting one second, and then begins probing for this record again." const DEFER_MS: u64 = 1_000; -/// §8.1: fifteen conflicts "within any ten-second period", and then "at least -/// five seconds before each successive additional probe attempt." +/// §8.1: conflicts "within any ten-second period", and "at least five seconds +/// before each successive additional probe attempt." const CONFLICT_WINDOW_MS: u64 = 10_000; const LIMITED_WAIT_MS: u64 = 5_000; @@ -306,12 +306,12 @@ const PROBE_ANSWER_EVERY_MS: u64 = 250; /// responses, one second apart." const ANNOUNCE_AGAIN_MS: u64 = 1_000; -/// Whose the name is, read only while an address is held. +/// Whose the name is, read only while a link is held. #[derive(Clone, Copy, Debug)] enum Claim { - /// Probing is owed since `since_ms`: the next [`Responder::on`] draws its - /// delay. - Owed { since_ms: u64 }, + /// Probing is owed from `from_ms` on: the next [`Responder::owed`] draws + /// its delay. + Owed { from_ms: u64 }, /// `sent` probes have left; at `at_ms` the next does, or after the third /// the name is held. A message conflicts only once `sent` is not zero. Probing { sent: u8, at_ms: u64 }, @@ -319,80 +319,23 @@ enum Claim { Lost, } -/// The conflicts over the name. -#[derive(Debug)] -struct Contest { - /// The times of the last [`CONFLICTS`] conflicts, the oldest at `next`. - times: [u64; CONFLICTS], - next: usize, - /// How many of `times` came each within [`CONFLICT_WINDOW_MS`] of the one - /// before, up to now. - counted: usize, - last_ms: Option, - /// [`CONFLICTS`] of them have fallen within one window. - limited: bool, - /// The conflicts since the link returned or an address came after none. - since_return: usize, -} - -impl Contest { - const fn new() -> Self { - Self { times: [0; CONFLICTS], next: 0, counted: 0, last_ms: None, limited: false, since_return: 0 } - } - - /// A conflict at `now_ms`. Returns whether the name has now been contested - /// [`CONFLICTS`] times since the link returned or an address came after none. - fn conflict(&mut self, now_ms: u64) -> bool { - if !self.running(now_ms) { - (self.counted, self.limited) = (0, false); - } - self.last_ms = Some(now_ms); - self.times[self.next] = now_ms; - self.next = (self.next + 1) % CONFLICTS; - self.counted = (self.counted + 1).min(CONFLICTS); - if self.counted == CONFLICTS && now_ms.saturating_sub(self.times[self.next]) <= CONFLICT_WINDOW_MS { - self.limited = true; - } - self.since_return = (self.since_return + 1).min(CONFLICTS); - self.since_return == CONFLICTS - } - - /// Whether a conflict at `now_ms` would be one more of this contest. - fn running(&self, now_ms: u64) -> bool { - self.last_ms.is_some_and(|last| now_ms.saturating_sub(last) <= CONFLICT_WINDOW_MS) - } - - /// What §8.1's limit puts before a probe attempt scheduled at `now_ms`. - fn wait_ms(&self, now_ms: u64) -> u64 { - if self.limited && self.running(now_ms) { - LIMITED_WAIT_MS - } else { - 0 - } - } -} - /// This host's one record on its link, on the caller's monotonic clock in -/// milliseconds: probed for on every address after none and whenever the link -/// comes back ([`Responder::link_returned`]), 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 +/// 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. /// -/// A message counts as sent once [`Responder::on`] has handed it over. A -/// caller whose link was down for it says so by [`Responder::link_returned`] -/// when the link is back, which probes again. +/// A message counts as sent once [`Responder::owed`] has handed it over. #[derive(Debug)] pub struct Responder<'a> { host: Host<'a>, /// The link the address is held on. link: Option, claim: Claim, - contest: Contest, - /// The latest change of the claim nobody took. - event: Option, + /// When the name last met a conflict. + conflict_ms: Option, last_group_ms: Option, /// When a held record is next owed to the group: an announcement, or an /// answer §6 delayed. One multicast of the record answers every query it @@ -405,77 +348,60 @@ pub struct Responder<'a> { impl<'a> Responder<'a> { pub const fn new(host: Host<'a>) -> Self { - Self { - host, - link: None, - claim: Claim::Lost, - contest: Contest::new(), - event: None, - last_group_ms: None, - owed_ms: None, - again: false, - } + Self { host, link: None, claim: Claim::Lost, 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 - /// address: the multicast owed now, if any — a probe, an announcement of - /// a name just claimed or of a new address under a held one (§8.4), its - /// second, or an answer §6 delayed. `delay` is drawn once when this call - /// starts probing, and not otherwise. - pub fn on(&mut self, link: Option, now_ms: u64, delay: impl FnOnce() -> u32) -> Option> { + /// address or its link is down. A link after none owes a probing (§8, + /// §10.2), whoever held the name; a new address under a held name owes + /// its announcement (§8.4). + pub fn on(&mut self, link: Option, now_ms: u64) { let Some(link) = link else { self.link = None; self.owed_ms = None; - return None; + return; }; match self.link.replace(link) { - None => self.begin(now_ms), + None => self.release(Claim::Owed { from_ms: now_ms.saturating_add(self.limit_ms(now_ms)) }), Some(was) if was.addr != link.addr && matches!(self.claim, Claim::Held) => { self.owed_ms = Some(now_ms); self.again = true; } Some(_) => {} } - if let Claim::Owed { .. } = self.claim { - let wait = u64::from(delay() % (PROBE_DELAY_MS + 1)) + self.contest.wait_ms(now_ms); - self.claim = Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) }; + } + + /// The multicast owed at `now_ms`, if any — a probe, an announcement of a + /// name just claimed or of a new address under a held one, its second, or + /// an answer §6 delayed — and whether this call claimed the name. Asked + /// after every message that has arrived was [`Self::heard`]. `delay` is + /// drawn once when this call starts a probing, and not otherwise. + pub fn owed(&mut self, now_ms: u64, delay: impl FnOnce() -> u32) -> (Option>, Option) { + let Some(link) = self.link else { return (None, None) }; + if let Claim::Owed { from_ms } = self.claim { + let wait = u64::from(delay() % (PROBE_DELAY_MS + 1)); + self.claim = Claim::Probing { sent: 0, at_ms: from_ms.max(now_ms).saturating_add(wait) }; } + let mut event = None; if let Claim::Probing { sent, at_ms } = self.claim { if now_ms < at_ms { - return None; + return (None, None); } if sent < PROBES { self.claim = Claim::Probing { sent: sent + 1, at_ms: now_ms.saturating_add(PROBE_EVERY_MS) }; - return Some(probe(self.host, link.addr)); + return (Some(probe(self.host, link.addr)), None); } self.claim = Claim::Held; - self.event = Some(Event::Claimed); + event = Some(Event::Claimed); self.owed_ms = Some(self.group_free_ms(now_ms, GROUP_EVERY_MS)); self.again = true; } if !matches!(self.claim, Claim::Held) || !self.owed_ms.is_some_and(|at| now_ms >= at) { - return None; + return (None, event); } self.owed_ms = core::mem::take(&mut self.again).then(|| now_ms.saturating_add(ANNOUNCE_AGAIN_MS)); self.last_group_ms = Some(now_ms); - Some(announcement(self.host, link.addr)) - } - - /// The link this host's address is on came back at `now_ms`, or the - /// caller has another reason to think its connectivity changed (§8): the - /// name is probed for again, whoever held it, and announced only if that - /// probe goes unanswered. With no address held nothing is owed: the one - /// that comes is probed on. - pub fn link_returned(&mut self, now_ms: u64) { - if self.link.is_some() { - self.begin(now_ms); - } - } - - /// Probing is owed for a reason that is no message read here. - fn begin(&mut self, now_ms: u64) { - self.contest.since_return = 0; - self.release(Claim::Owed { since_ms: now_ms }); + (Some(announcement(self.host, link.addr)), event) } /// The name is no longer held: nothing of the record is owed. @@ -485,9 +411,23 @@ impl<'a> Responder<'a> { self.again = false; } - fn lose(&mut self, why: Lost) { - self.release(Claim::Lost); - self.event = Some(Event::Lost(why)); + /// What §8.1's limit puts before a probe attempt that would begin at + /// `now_ms`: five seconds within ten of a conflict. + fn limit_ms(&self, now_ms: u64) -> u64 { + let limited = self.conflict_ms.is_some_and(|last| now_ms.saturating_sub(last) <= CONFLICT_WINDOW_MS); + if limited { + LIMITED_WAIT_MS + } else { + 0 + } + } + + /// A conflict at `now_ms`: what §8.1's limit puts before the probe + /// attempt that follows it. + fn conflict(&mut self, now_ms: u64) -> u64 { + let wait = self.limit_ms(now_ms); + self.conflict_ms = Some(now_ms); + wait } /// When the record was last multicast plus `every_ms`, or `now_ms` if @@ -496,73 +436,63 @@ impl<'a> Responder<'a> { self.last_group_ms.map_or(now_ms, |last| last.saturating_add(every_ms)).max(now_ms) } - /// When [`Self::on`] is next owed a call, on the caller's clock. + /// When [`Self::owed`] is next owed a call, on the caller's clock. pub fn owed_at(&self) -> Option { self.link?; match self.claim { - Claim::Owed { since_ms } => Some(since_ms), + Claim::Owed { from_ms } => Some(from_ms), Claim::Probing { at_ms, .. } => Some(at_ms), Claim::Held => self.owed_ms, Claim::Lost => None, } } - /// The latest of what became of the name since the last call. - pub fn take_event(&mut self) -> Option { - self.event.take() - } - /// `message` arrived from `from` at `now_ms`: the answer it is owed now, - /// if it is a query for a held name; a response or another host's probe - /// is read for what it says of the name and answered nothing. `None` too - /// where it asks nothing this host answers — no address held, a source - /// off the link, an opcode or a response code other than zero, a question - /// for another name or type, or bytes that are not a message at all — or - /// where §6 delays the answer to [`Responder::owed_at`]. - pub fn heard(&mut self, message: &[u8], from: Source, now_ms: u64) -> Option { - let link = self.link?; - if !link.holds(from.addr) { - return None; - } - let id = u16_at(message, 0).ok()?; - let flags = u16_at(message, 2).ok()?; + /// if it is a query for a held name, and whether it took the name; a + /// response or another host's probe is read for what it says of the name + /// and answered nothing. No answer too where it asks nothing this host + /// answers — no link held, a source off the link, an opcode or a response + /// code other than zero, a question for another name or type, or bytes + /// that are not a message at all — or where §6 delays the answer to + /// [`Responder::owed_at`]. + pub fn heard(&mut self, message: &[u8], from: Source, now_ms: u64) -> (Option, Option) { + let Some(link) = self.link.filter(|link| link.holds(from.addr)) else { return (None, None) }; + let (Ok(id), Ok(flags)) = (u16_at(message, 0), u16_at(message, 2)) else { return (None, None) }; if flags & (OPCODE_MASK | RCODE_MASK) != 0 { - return None; + return (None, None); } if flags & QR != 0 { - self.response(message, from, link, now_ms); - return None; + return (None, self.response(message, from, link, now_ms)); } match self.claim { - Claim::Held => self.answer(message, id, from, link, now_ms), + Claim::Held => (self.answer(message, id, from, link, now_ms), None), Claim::Probing { sent, .. } if sent > 0 => { self.rival(message, link, now_ms); - None + (None, None) } - Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => None, + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => (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). - fn response(&mut self, message: &[u8], from: Source, link: Link, now_ms: u64) { + 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, + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => return None, }; if from.port != PORT || !conflicts(message, self.host, link.addr).unwrap_or(false) { - return; + return None; } - let over = self.contest.conflict(now_ms); + let wait = self.conflict(now_ms); if probing { - self.lose(Lost::Answered); - } else if over { - self.lose(Lost::Contested); - } else { - self.release(Claim::Owed { since_ms: now_ms }); + self.release(Claim::Lost); + return 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 @@ -578,13 +508,8 @@ impl<'a> Responder<'a> { Ordering::Equal => more, Ordering::Less => false, }; - if !later { - return; - } - if self.contest.conflict(now_ms) { - self.lose(Lost::Contested); - } else { - let wait = DEFER_MS + self.contest.wait_ms(now_ms); + if later { + let wait = self.conflict(now_ms).max(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 0fa2cc40727..a0b6543ad48 100644 --- a/userland/netstack/mdns/src/tests.rs +++ b/userland/netstack/mdns/src/tests.rs @@ -24,6 +24,48 @@ fn undrawn() -> u32 { panic!("no probing starts here, so no delay is drawn") } +/// A responder driven as a caller drives it, and every event its calls +/// returned since [`Machine::said`] was last asked, in order. +struct Machine { + r: Responder<'static>, + said: Vec, +} + +impl Machine { + fn new() -> Self { + Self { r: Responder::new(host()), said: Vec::new() } + } + + /// A pass with nothing to read: the link, then what is owed. + fn on(&mut self, link: Option, now: u64, delay: impl FnOnce() -> u32) -> Option> { + self.r.on(link, now); + let (owed, event) = self.r.owed(now, delay); + self.said.extend(event); + owed + } + + fn heard(&mut self, message: &[u8], from: Source, now: u64) -> Option { + let (answer, event) = self.r.heard(message, from, now); + self.said.extend(event); + answer + } + + fn owed_at(&self) -> Option { + self.r.owed_at() + } + + /// [`LINK`] went down and is back at `now`: the caller had no link, and + /// has one. + fn link_returned(&mut self, now: u64) { + self.r.on(None, now); + self.r.on(Some(LINK), now); + } + + fn said(&mut self) -> Vec { + std::mem::take(&mut self.said) + } +} + /// The probe for `addr`. §18.1, §18.2: ID zero and QR clear. §8.1: one /// question, the name, type `ANY` (255), class 1 with the unicast-response /// bit. §8.2: the proposed record in the Authority Section, which is the @@ -100,11 +142,11 @@ fn probe_of(class: u16, proposed: &[Rr]) -> Vec { /// Every multicast `r` sends at its own deadlines up to `until`, each with /// its time. Nothing is due a millisecond before a deadline, and `delay` is /// drawn by the call that starts probing and by no other. -fn run(r: &mut Responder, link: Link, until: u64, delay: u32) -> Vec<(u64, Vec)> { +fn run(r: &mut Machine, link: Link, until: u64, delay: u32) -> Vec<(u64, Vec)> { let mut sent = Vec::new(); for _ in 0..10_000 { let Some(at) = r.owed_at().filter(|at| *at <= until) else { return sent }; - let starts = matches!(r.claim, Claim::Owed { .. }); + let starts = matches!(r.r.claim, Claim::Owed { .. }); if !starts && at > 0 { assert_eq!(r.on(Some(link), at - 1, undrawn), None, "nothing is due before {at}"); } @@ -129,19 +171,19 @@ fn claim_of(addr: [u8; 4], first: u64) -> Vec<(u64, Vec)> { /// A responder one probe into claiming `link`'s address: the probe left at /// 1,000. -fn probing(link: Link) -> Responder<'static> { - let mut r = Responder::new(host()); +fn probing(link: Link) -> Machine { + let mut r = Machine::new(); assert_eq!(r.on(Some(link), 1_000, || 0), Some(probe_for(link.addr))); r } /// A responder that holds its name on [`LINK`], its second announcement out /// at `at`. -fn held(at: u64) -> Responder<'static> { - let mut r = Responder::new(host()); +fn held(at: u64) -> Machine { + let mut r = Machine::new(); assert_eq!(r.on(Some(LINK), at - 1_750, || 0), Some(probe_for(ADDR))); assert_eq!(run(&mut r, LINK, at, 0), claim_of(ADDR, at - 1_750)[1..]); - assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Claimed), None)); + assert_eq!((r.said(), r.owed_at()), (vec![Event::Claimed], None)); r } @@ -152,7 +194,7 @@ fn ask(query: &[u8], port: u16) -> Option { /// Whether `r` answers for its name at `now`, to a legacy resolver, which §6 /// never delays. -fn answers(r: &mut Responder, now: u64) -> bool { +fn answers(r: &mut Machine, now: u64) -> bool { r.heard(&query(1, NAME, 1, 1), Source { addr: NEIGHBOUR, port: 53_000 }, now).is_some() } @@ -249,7 +291,7 @@ fn a_malformed_message_is_no_question() { #[test] fn the_record_is_multicast_at_most_once_a_second_and_a_query_inside_it_waits() { let q = query(0, NAME, 1, 1); - let mut r = Responder::new(host()); + let mut r = Machine::new(); assert_eq!(r.on(Some(LINK), 19_250, || 0), Some(probe_for(ADDR))); assert_eq!(run(&mut r, LINK, 20_000, 0), claim_of(ADDR, 19_250)[1..4], "claimed, and first announced at 20,000"); assert_eq!(r.owed_at(), Some(21_000), "§8.3: the second announcement"); @@ -278,7 +320,7 @@ fn the_record_is_multicast_at_most_once_a_second_and_a_query_inside_it_waits() { /// be proposing to use, should its probing be successful." #[test] fn rfc6762_8_1_a_probe_asks_any_for_the_name_with_the_unicast_bit_and_proposes_its_record_in_the_authority_section() { - let mut r = Responder::new(host()); + let mut r = Machine::new(); let probe = r.on(Some(LINK), 0, || 0).expect("no delay, so the first probe"); #[rustfmt::skip] let want = [ @@ -303,15 +345,15 @@ fn rfc6762_8_1_a_probe_asks_any_for_the_name_with_the_unicast_bit_and_proposes_i #[test] fn rfc6762_8_1_three_probes_250_ms_apart_follow_the_drawn_delay_and_the_name_is_announced_250_ms_after_the_third() { for (draw, delay) in [(0, 0), (1, 1), (137, 137), (250, 250), (251, 0), (1_000, 247), (u32::MAX, u64::from(u32::MAX % 251))] { - let mut r = Responder::new(host()); + let mut r = Machine::new(); let mut sent: Vec<_> = r.on(Some(LINK), 5_000, || draw).map(|probe| (5_000, probe)).into_iter().collect(); assert_eq!(sent.is_empty(), delay != 0, "draw {draw}"); - assert_eq!(r.take_event(), None); + assert_eq!(r.said(), []); sent.extend(run(&mut r, LINK, 5_000 + delay + 749, draw)); assert_eq!(sent, claim_of(ADDR, 5_000 + delay)[..3], "draw {draw}: three probes, and nothing announced so far"); - assert_eq!(r.take_event(), None, "the name is not held 249 ms after the third probe"); + assert_eq!(r.said(), [], "the name is not held 249 ms after the third probe"); assert_eq!(run(&mut r, LINK, 60_000, draw), claim_of(ADDR, 5_000 + delay)[3..], "draw {draw}: announced twice, and nothing after"); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); } } @@ -320,7 +362,7 @@ fn rfc6762_8_1_three_probes_250_ms_apart_follow_the_drawn_delay_and_the_name_is_ /// with it: under the probe nothing is answered, to anyone. #[test] fn a_name_under_probe_is_answered_to_nobody() { - let mut r = Responder::new(host()); + let mut r = Machine::new(); assert_eq!(r.on(Some(LINK), 1_000, || 100), None); for now in [1_000, 1_099, 1_100, 1_350, 1_600, 1_849] { run(&mut r, LINK, now, 0); @@ -339,7 +381,7 @@ fn a_name_under_probe_is_answered_to_nobody() { fn deferred(link: Link, from: [u8; 4], heard: &[u8]) -> bool { let mut r = probing(link); assert_eq!(r.heard(heard, Source { addr: from, port: PORT }, 1_100), None); - assert_eq!(r.take_event(), None); + assert_eq!(r.said(), []); match r.owed_at() { Some(1_250) => false, Some(2_100) => true, @@ -368,7 +410,7 @@ fn rfc6762_8_2_the_lexicographically_later_data_wins_a_simultaneous_probe_and_th let mut r = probing(on(early)); assert_eq!(r.heard(&proposing(late), Source { addr: late, port: PORT }, 1_100), None); assert_eq!(run(&mut r, on(early), 60_000, 0), claim_of(early, 2_100), "a second's wait, then the probing whole"); - assert_eq!(r.take_event(), Some(Event::Claimed), "nobody answered the second probing: a stale packet's name is claimed"); + assert_eq!(r.said(), [Event::Claimed], "nobody answered the second probing: a stale packet's name is claimed"); let mut r = probing(on(late)); assert_eq!(r.heard(&proposing(early), Source { addr: early, port: PORT }, 1_100), None); @@ -479,12 +521,12 @@ fn rfc6762_8_1_a_conflicting_response_under_the_probe_takes_the_name_and_nothing assert_eq!(run(&mut r, LINK, now, 0).len(), probes - 1); assert_eq!(r.heard(&heard, from, now), None, "{what}"); if !conflicts { - assert_eq!(r.take_event(), None, "{what}, after {probes} probes"); + assert_eq!(r.said(), [], "{what}, after {probes} probes"); assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 1_000)[probes..], "{what}: the probing goes on"); continue; } - assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered)), "{what}, after {probes} probes"); - assert_eq!((r.take_event(), r.owed_at()), (None, None), "said once, and nothing is owed"); + 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"); @@ -502,12 +544,12 @@ fn rfc6762_8_1_a_conflicting_response_under_the_probe_takes_the_name_and_nothing #[test] fn rfc6762_8_1_nothing_heard_before_the_first_probe_is_sent_is_a_conflict() { let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); - let mut r = Responder::new(host()); + let mut r = Machine::new(); assert_eq!(r.on(Some(LINK), 1_000, || 200), None, "the delay"); for heard in [says(&MOVED), later.clone()] { assert_eq!(r.heard(&heard, PEER, 1_100), None); } - assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_200))); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(1_200))); assert_eq!(r.on(Some(LINK), 1_200, undrawn), Some(probe_for(ADDR))); assert_eq!(r.heard(&later, PEER, 1_300), None); @@ -515,7 +557,7 @@ fn rfc6762_8_1_nothing_heard_before_the_first_probe_is_sent_is_a_conflict() { for heard in [says(&MOVED), later.clone()] { assert_eq!(r.heard(&heard, PEER, 2_000), None); } - assert_eq!((r.take_event(), r.owed_at()), (None, Some(2_300)), "and in the second's wait no probe of the new probing is out"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(2_300)), "and in the second's wait no probe of the new probing is out"); assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 2_300)); } @@ -529,7 +571,7 @@ fn rfc6762_9_a_conflicting_response_resets_a_held_name_to_probing_and_nothing_el for (what, heard, from, conflicts) in about_the_name() { let mut r = held(10_000); assert_eq!(r.heard(&heard, from, 50_000), None, "{what}"); - assert_eq!(r.take_event(), None, "{what}: the name is neither lost nor claimed yet"); + assert_eq!(r.said(), [], "{what}: the name is neither lost nor claimed yet"); if !conflicts { assert_eq!(r.owed_at(), None, "{what}"); assert!(answers(&mut r, 50_000), "{what}: still held"); @@ -541,7 +583,7 @@ fn rfc6762_9_a_conflicting_response_resets_a_held_name_to_probing_and_nothing_el assert_eq!(run(&mut r, LINK, 50_326, 77), claim_of(ADDR, 50_077)[..1], "{what}: probed after a drawn delay"); assert!(!answers(&mut r, 50_326)); assert_eq!(run(&mut r, LINK, 3_600_000, 77), claim_of(ADDR, 50_077)[1..], "{what}: unanswered, the name is announced again"); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); assert!(answers(&mut r, 60_000)); } } @@ -555,7 +597,7 @@ 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.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Answered)), None)); + 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)); } @@ -602,64 +644,71 @@ fn rfc6762_6_a_probe_for_a_held_name_is_answered_at_once_by_unicast_or_250_ms_af assert_eq!(r.owed_at(), Some(11_000)); } -/// `count` returns of the link `every` ms apart from 10,000, each probed on -/// at once and answered for by another host a millisecond later. Returns the -/// responder and when the last conflict was. -fn answered_on_every_return(count: u64, every: u64) -> (Responder<'static>, u64) { - let mut r = held(5_000); - let mut last = 0; - for i in 0..count { - let now = 10_000 + i * every; - r.link_returned(now); - assert_eq!(r.on(Some(LINK), now, || 0), Some(probe_for(ADDR)), "return {i}: not limited yet"); - assert_eq!(r.heard(&says(&MOVED), PEER, now + 1), None); - assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); - last = now + 1; - } - (r, last) -} - /// RFC 6762 §8.1: "If fifteen conflicts occur within any ten-second period, /// then the host MUST wait at least five seconds before each successive -/// additional probe attempt." +/// additional probe attempt." "For very simple devices, a valid way to comply +/// with this requirement is to always wait five seconds after any failed +/// probe attempt before trying again." Every probe attempt that would begin +/// within ten seconds of a conflict waits the five, whatever begins it; §9's +/// "MUST immediately reset its conflicted unique record to probing state" is +/// kept whole for a conflict that follows none. #[test] -fn rfc6762_8_1_fifteen_conflicts_in_ten_seconds_put_five_seconds_before_each_further_probe_attempt() { - // 14 x 714 ms is 9,996 ms: the fifteen fall within ten seconds. - let (mut r, last) = answered_on_every_return(15, 714); - let now = last + 700; - r.link_returned(now); - assert_eq!(r.on(Some(LINK), now, || 30), None); - assert_eq!(r.owed_at(), Some(now + 5_030), "five seconds, and the drawn delay"); - assert_eq!(run(&mut r, LINK, now + 5_100, 30), [(now + 5_030, probe_for(ADDR))]); - - assert_eq!(r.heard(&probe_of(1, &[(NAME, 1, 1, &MOVED)]), PEER, now + 5_100), None, "a later probe"); - assert_eq!(r.owed_at(), Some(now + 5_100 + 6_000), "§8.2's second, and the five"); - assert_eq!(run(&mut r, LINK, now + 11_200, 30), [(now + 11_100, probe_for(ADDR))]); - assert_eq!(r.heard(&says(&MOVED), PEER, now + 11_200), None); - assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); - - // The newest fifteen now span more than ten seconds, and each still came - // within ten of the one before: "each successive additional probe attempt". - r.link_returned(now + 12_000); - assert_eq!(r.on(Some(LINK), now + 12_000, || 0), None); - assert_eq!(r.owed_at(), Some(now + 17_000)); - - // Ten seconds with no conflict end it. - r.link_returned(now + 21_201); - assert_eq!(r.on(Some(LINK), now + 21_201, || 0), Some(probe_for(ADDR))); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, now + 21_201)[1..]); -} - -/// RFC 6762 §8.1's limit is of "fifteen conflicts" "within any ten-second -/// period": fourteen are not fifteen, and fifteen in 10.01 s are not within -/// ten. +fn rfc6762_8_1_a_probe_attempt_within_ten_seconds_of_a_conflict_waits_five_seconds_first() { + let mut r = held(10_000); + assert_eq!(r.heard(&says(&MOVED), PEER, 50_000), None); + assert_eq!(r.owed_at(), Some(50_000), "§9: no conflict came before this one, and it is probed on at once"); + assert_eq!(run(&mut r, LINK, 54_999, 0), claim_of(ADDR, 50_000)); + + assert_eq!(r.heard(&says(&MOVED), PEER, 55_000), None, "five seconds after the last"); + assert_eq!(r.owed_at(), Some(60_000), "five seconds before the probing"); + assert_eq!(r.on(Some(LINK), 55_001, || 30), None, "a pass inside the five seconds draws the delay and sends nothing"); + assert_eq!(r.owed_at(), Some(60_030), "the five seconds, then the drawn delay"); + assert!(!answers(&mut r, 59_999), "§9: in probing state meanwhile, and answered to nobody"); + assert_eq!(run(&mut r, LINK, 64_999, 0), claim_of(ADDR, 60_030), "and then the probing whole"); + + assert_eq!(r.heard(&says(&MOVED), PEER, 65_000), None, "ten seconds after the last is within ten seconds"); + assert_eq!(r.owed_at(), Some(70_000)); + assert_eq!(run(&mut r, LINK, 75_000, 0), claim_of(ADDR, 70_000)); + assert_eq!(r.heard(&says(&MOVED), PEER, 75_001), None, "and 10,001 ms after it is not"); + assert_eq!(r.owed_at(), Some(75_001)); + assert_eq!(run(&mut r, LINK, 80_000, 0), claim_of(ADDR, 75_001)); + assert_eq!(r.said(), [Event::Claimed; 4]); + + // §8.2's second is the wait of a lost tiebreak that follows no conflict. + let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); + let mut r = probing(LINK); + assert_eq!(r.heard(&later, PEER, 1_100), None); + assert_eq!(run(&mut r, LINK, 2_100, 0), [(2_100, probe_for(ADDR))]); + assert_eq!(r.heard(&later, PEER, 2_200), None); + assert_eq!(r.owed_at(), Some(7_200), "the five seconds, which hold §8.2's one"); + assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 7_200)); + + // A link after none begins a probe attempt like any other. + let mut r = probing(LINK); + assert_eq!(r.heard(&says(&MOVED), PEER, 1_100), None); + assert_eq!(r.said(), [Event::Lost]); + r.link_returned(4_000); + assert_eq!(r.owed_at(), Some(9_000)); + r.link_returned(11_100); + assert_eq!(r.owed_at(), Some(16_100), "ten seconds after the conflict"); + r.link_returned(11_101); + assert_eq!(r.owed_at(), Some(11_101)); + assert_eq!(run(&mut r, LINK, 60_000, 0), claim_of(ADDR, 11_101)); +} + +/// RFC 6762 §8.1: "If, by 250 ms after the third probe, no conflicting +/// Multicast DNS responses have been received, the host may move to the next +/// step, announcing." A response that has arrived when the 250 ms end is +/// heard before what is owed is asked, and takes the name. #[test] -fn rfc6762_8_1_fourteen_conflicts_or_fifteen_in_more_than_ten_seconds_limit_nothing() { - for (count, every) in [(14, 100), (15, 715), (40, 750)] { - let (mut r, last) = answered_on_every_return(count, every); - r.link_returned(last + 700); - assert_eq!(r.on(Some(LINK), last + 700, || 0), Some(probe_for(ADDR)), "{count} conflicts {every} ms apart"); - } +fn rfc6762_8_1_a_conflicting_response_received_as_the_last_250_ms_end_takes_the_name() { + let mut r = probing(LINK); + assert_eq!(run(&mut r, LINK, 1_749, 0), claim_of(ADDR, 1_000)[1..3]); + 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), []); } /// RFC 6762 §8: "Whenever a Multicast DNS responder starts up, wakes up from @@ -678,7 +727,7 @@ fn rfc6762_8_a_link_that_returns_is_probed_on_before_the_name_is_announced_or_an assert_eq!(r.owed_at(), Some(60_000)); assert!(!answers(&mut r, 60_000), "not this host's until probed for again"); assert_eq!(run(&mut r, LINK, 3_600_000, 120), claim_of(ADDR, 60_120)); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); assert_eq!(r.heard(&query(0, NAME, 1, 1), PEER, 99_900).map(|a| a.to), Some(To::Group)); r.link_returned(100_000); @@ -688,16 +737,17 @@ fn rfc6762_8_a_link_that_returns_is_probed_on_before_the_name_is_announced_or_an let mut lost = probing(LINK); assert_eq!(lost.heard(&says(&MOVED), PEER, 1_100), None); - assert_eq!(lost.take_event(), Some(Event::Lost(Lost::Answered))); - lost.link_returned(9_000); - assert_eq!(run(&mut lost, LINK, 3_600_000, 5), claim_of(ADDR, 9_005), "a lost name is probed for on the link's return"); - assert_eq!(lost.take_event(), Some(Event::Claimed)); + assert_eq!(lost.said(), [Event::Lost]); + lost.link_returned(12_000); + assert_eq!(run(&mut lost, LINK, 3_600_000, 5), claim_of(ADDR, 12_005), "a lost name is probed for on the link's return"); + assert_eq!(lost.said(), [Event::Claimed]); - let mut unaddressed = Responder::new(host()); - unaddressed.link_returned(5_000); - assert_eq!(unaddressed.owed_at(), None, "no address, no record to probe for"); - assert_eq!(unaddressed.on(Some(LINK), 9_000, || 0), Some(probe_for(ADDR)), "the address that comes is probed on once"); - assert_eq!(run(&mut unaddressed, LINK, 3_600_000, 0), claim_of(ADDR, 9_000)[1..]); + let mut down = held(10_000); + down.r.on(None, 20_000); + assert_eq!(down.owed_at(), None, "a link that is down is owed nothing"); + assert!(!answers(&mut down, 20_000), "and nothing is answered on it"); + assert_eq!(down.on(None, 30_000, undrawn), None); + assert_eq!(down.on(Some(LINK), 40_000, || 0), Some(probe_for(ADDR)), "and its return is probed on"); } /// A link that returns again under the probe starts it over: each return is @@ -715,7 +765,7 @@ fn a_flapping_link_restarts_the_probe_and_nothing_is_announced_until_one_runs_wh } assert_eq!(sent, [], "a return inside the last one's delay sends nothing"); assert_eq!(run(&mut r, LINK, 3_600_000, 150), claim_of(ADDR, 24_900 + 150)); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); let mut sent = Vec::new(); for flap in 0..50 { @@ -726,9 +776,9 @@ fn a_flapping_link_restarts_the_probe_and_nothing_is_announced_until_one_runs_wh } let probes: Vec<_> = (0..50).flat_map(|flap| claim_of(ADDR, 40_000 + flap * 600).into_iter().take(3)).collect(); assert_eq!(sent, probes, "three probes a return, and no announcement between two returns 600 ms apart"); - assert_eq!(r.take_event(), None); + assert_eq!(r.said(), []); assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, 49 * 600 + 40_000)[3..]); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); } /// A storm of forged answers, one a millisecond for ten seconds, at a held @@ -744,69 +794,52 @@ 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.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Answered)), None)); + assert_eq!((r.said(), r.owed_at()), (vec![Event::Lost], None)); assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); } -/// Forged answers timed to spare every probe, one before each second -/// announcement of a name held again: each costs a probing (§9), and the -/// fifteenth since the link last returned costs the name, so the forger's -/// packets buy fourteen probings and no more, however long it goes on. A -/// return of the link is no peer's packet, and the count starts over at it. +/// Forged answers, one a millisecond for a minute, that spare every probe: +/// each strikes a name that is not under one. The first is probed on at once +/// (§9) and every later one waits §8.1's five seconds, so the forger buys one +/// probing in 5,750 ms, three probes and the announcement the next forged +/// answer follows, for as long as it sends; nothing it sent is kept, and when +/// it stops the probing it last bought holds the name. #[test] -fn forged_answers_that_spare_every_probe_cost_fourteen_probings_and_then_the_name() { +fn forged_answers_that_spare_every_probe_cost_one_probing_in_five_seconds_and_the_name_is_held_when_they_stop() { let mut r = held(10_000); - let (mut now, mut sent, mut forged) = (50_000, Vec::new(), 0); - while !matches!(r.claim, Claim::Lost) { - assert_eq!(r.heard(&says(&MOVED), PEER, now), None); - forged += 1; - let Some(at) = r.owed_at() else { break }; - // The name is held again and announced once; the forger strikes before the second. - let round = run(&mut r, LINK, at + 1_749, 0); - assert_eq!(round, claim_of(ADDR, at)[..4], "forged answer {forged}"); - assert_eq!(r.take_event(), Some(Event::Claimed)); - sent.extend(round); - now = at + 1_749; - } - assert_eq!((forged, sent.len()), (CONFLICTS, 14 * 4), "fourteen probings: three probes and an announcement each"); - assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Contested)), None)); - for later in 0..1_000 { - assert_eq!(r.heard(&says(&MOVED), PEER, now + later), None); - } - assert_eq!(run(&mut r, LINK, 3_600_000, 0), [], "and nothing more, whatever it sends"); - - r.link_returned(4_000_000); - assert_eq!(run(&mut r, LINK, 5_000_000, 0), claim_of(ADDR, 4_000_000)); - for forged in 0..14 { - let now = 5_000_000 + forged * 20_000; - assert_eq!(r.heard(&says(&MOVED), PEER, now), None); - assert_eq!(run(&mut r, LINK, now + 19_999, 0), claim_of(ADDR, now), "fourteen more are fourteen since the link returned"); - } - r.link_returned(6_000_000); - assert_eq!(run(&mut r, LINK, 6_100_000, 0), claim_of(ADDR, 6_000_000)); - assert_eq!(r.heard(&says(&MOVED), PEER, 6_100_000), None); - assert_eq!(run(&mut r, LINK, 6_200_000, 0), claim_of(ADDR, 6_100_000), "and the fifteenth is the first since the next return"); -} - -/// Forged probes that win every tiebreak, one behind each first probe: each -/// costs a second's wait and one probe (§8.2), and the fifteenth costs the -/// name. A probe a second never reaches §8.1's fifteen in ten seconds. + let mut sent = Vec::new(); + for now in 50_000..110_000 { + sent.extend(run(&mut r, LINK, now, 0)); + if !matches!(r.r.claim, Claim::Probing { sent: 1.., .. }) { + assert_eq!(r.heard(&says(&MOVED), PEER, now), None); + } + } + let probings: Vec<_> = (0..11).flat_map(|nth| claim_of(ADDR, 50_000 + nth * 5_750).into_iter().take(4)).collect(); + assert_eq!(sent, probings, "eleven probings in the minute, each 5,750 ms after the one before"); + assert_eq!(r.said(), [Event::Claimed; 11], "and the name is lost to none of them"); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, 113_250), "the forger stops, and its last conflict is probed on"); + assert_eq!(r.said(), [Event::Claimed]); + assert!(answers(&mut r, 3_600_000)); +} + +/// Forged probes that win every tiebreak, one a millisecond for a minute: +/// the first costs §8.2's second and every later one §8.1's five, so the +/// forger buys one probe in five seconds, and when it stops the name is +/// claimed. #[test] -fn forged_probes_that_win_every_tiebreak_cost_fifteen_probes_and_then_the_name() { +fn forged_probes_that_win_every_tiebreak_cost_one_probe_in_five_seconds_and_the_name_is_claimed_when_they_stop() { let later = probe_of(1, &[(NAME, 1, 1, &MOVED)]); let mut r = probing(LINK); - let mut sent = vec![1_000]; - for forged in 1..CONFLICTS as u64 { - assert_eq!(r.heard(&later, PEER, 1_000 * forged + 1), None); - assert_eq!(r.take_event(), None); - let probe = run(&mut r, LINK, 1_000 * forged + 1_100, 0); - assert_eq!(probe, [(1_000 * forged + 1_001, probe_for(ADDR))], "a second after forged probe {forged}"); - sent.push(probe[0].0); - } - assert_eq!(r.heard(&later, PEER, 16_000), None); - assert_eq!((r.take_event(), r.owed_at()), (Some(Event::Lost(Lost::Contested)), None)); - assert_eq!(sent.len(), CONFLICTS); - assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); + let mut sent = Vec::new(); + for now in 1_001..61_000 { + sent.extend(run(&mut r, LINK, now, 0)); + assert_eq!(r.heard(&later, PEER, now), None); + } + let probes: Vec<_> = (0..12).map(|nth| (2_001 + nth * 5_000, probe_for(ADDR))).collect(); + assert_eq!(sent, probes, "a second after the first forged probe, and then one probe in five seconds"); + assert_eq!(r.said(), []); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, 62_001), "the forger stops, and the probing runs whole"); + assert_eq!(r.said(), [Event::Claimed]); } /// Nothing a peer sends is trusted to be a message: no cut of a conflicting @@ -818,15 +851,15 @@ fn no_cut_of_a_conflicting_response_or_of_a_winning_probe_is_one() { for cut in 0..response.len() { let mut r = probing(LINK); assert_eq!(r.heard(&response[..cut], PEER, 1_100), None); - assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_250)), "under the probe, cut at {cut}"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(1_250)), "under the probe, cut at {cut}"); let mut r = held(10_000); assert_eq!(r.heard(&response[..cut], PEER, 50_000), None); - assert_eq!((r.take_event(), r.owed_at()), (None, None), "held, cut at {cut}"); + assert_eq!((r.said(), r.owed_at()), (vec![], None), "held, cut at {cut}"); } for cut in 0..later.len() { let mut r = probing(LINK); assert_eq!(r.heard(&later[..cut], PEER, 1_100), None); - assert_eq!((r.take_event(), r.owed_at()), (None, Some(1_250)), "cut at {cut}"); + assert_eq!((r.said(), r.owed_at()), (vec![], Some(1_250)), "cut at {cut}"); } let mut r = probing(LINK); assert_eq!(r.heard(&later, PEER, 1_100), None); @@ -834,7 +867,7 @@ fn no_cut_of_a_conflicting_response_or_of_a_winning_probe_is_one() { assert_eq!(r.heard(&response, PEER, 2_100), None, "in the wait"); assert_eq!(run(&mut r, LINK, 2_100, 0), [(2_100, probe_for(ADDR))]); assert_eq!(r.heard(&response, PEER, 2_101), None); - assert_eq!(r.take_event(), Some(Event::Lost(Lost::Answered))); + assert_eq!(r.said(), [Event::Lost]); } /// RFC 6762 §8.4: "if any of a host's IP addresses change, it MUST @@ -845,18 +878,18 @@ fn no_cut_of_a_conflicting_response_or_of_a_winning_probe_is_one() { /// none is probed on. Nothing is answered without an address. #[test] fn an_address_after_none_is_probed_for_and_a_new_one_under_a_held_name_is_announced() { - let mut r = Responder::new(host()); + let mut r = Machine::new(); assert!(!answers(&mut r, 0), "no address yet"); assert_eq!(r.on(None, 0, undrawn), None); assert_eq!(r.owed_at(), None); assert_eq!(r.on(Some(LINK), 0, || 0), Some(probe_for(ADDR))); assert_eq!(run(&mut r, LINK, 5_000, 0), claim_of(ADDR, 0)[1..]); - assert_eq!(r.take_event(), Some(Event::Claimed)); + assert_eq!(r.said(), [Event::Claimed]); let moved = Link { addr: MOVED, prefix: 24 }; assert_eq!(r.on(Some(moved), 9_000, undrawn), Some(record_of(MOVED)), "announced at once, and not probed for"); assert_eq!(run(&mut r, moved, 60_000, 0), [(10_000, record_of(MOVED))], "§8.3: and a second later"); - assert_eq!(r.take_event(), None); + assert_eq!(r.said(), []); assert_eq!(r.on(Some(moved), 60_500, undrawn), None); assert_eq!(r.on(None, 60_600, undrawn), None); @@ -868,10 +901,10 @@ fn an_address_after_none_is_probed_for_and_a_new_one_under_a_held_name_is_announ let mut r = probing(LINK); 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.take_event(), Some(Event::Lost(Lost::Answered))); + 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(None, 3_000, undrawn), None); - assert_eq!(r.on(Some(LINK), 4_000, || 0), Some(probe_for(ADDR)), "but by one after none"); + assert_eq!(r.on(Some(LINK), 12_000, || 0), Some(probe_for(ADDR)), "but by one after none"); } /// RFC 6762 §11: a query whose source is not on this link is ignored; a diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs index 1827d42bcb9..798b0c94ced 100644 --- a/userland/netstack/node/src/lib.rs +++ b/userland/netstack/node/src/lib.rs @@ -74,6 +74,8 @@ pub enum Event { /// A refusal of the stack's, and how many of its rule it stands for beyond itself. Stack { refusal: toyos_net_shard::Refusal, suppressed: u64 }, Dhcp(toyos_dhcp::Refusal), + /// What became of the machine's name. + Name(toyos_mdns::Event), } pub struct Node { @@ -81,6 +83,8 @@ pub struct Node { client: Client, /// The responder for the machine's name, once [`Self::answer_as`] started it. name: Option, + /// The link as [`Self::link`] last reported it. + up: bool, resolver: resolve::Resolver, counters: Counters, events: Vec, @@ -107,6 +111,7 @@ impl Node { stack, client, name: None, + up: false, resolver: resolve::Resolver::new(), counters: Counters::default(), events: Vec::new(), @@ -161,14 +166,12 @@ impl Node { } /// The link came up or went down; the caller reports a change, not a state. Down, a held - /// lease stays; up, the client verifies a lease it holds and otherwise starts over, and the - /// machine's name is announced again for a lease that stayed. + /// lease stays and the machine's name has no link; up, the client verifies a lease it holds + /// and otherwise starts over, and the name of a lease that stayed is probed for again. pub fn link(&mut self, now: Instant, up: bool, mut draw: impl FnMut() -> u32) { self.stack.link(now, up); + self.up = up; if up { - if let Some(name) = &mut self.name { - name.link_returned(now); - } let out = self.client.link_up(now, &mut draw); self.carry_out(now, out, None, &mut draw); } diff --git a/userland/netstack/node/src/name.rs b/userland/netstack/node/src/name.rs index ba8f18c883e..1816734468e 100644 --- a/userland/netstack/node/src/name.rs +++ b/userland/netstack/node/src/name.rs @@ -3,18 +3,18 @@ //! answer, is `toyos_mdns::Responder`'s; this is its socket on the group's port, its membership //! of the group, its clock and its draws. //! -//! - The record is the held lease's: its address and prefix are what the responder is told, and -//! it is told there is none from the moment the lease goes. Nothing is probed for, announced or -//! answered for an address still under probe. -//! - A link that comes back under a held lease is the responder's to hear of: the lease outlives -//! the link, so no new address would tell it (§8). It probes for the name again. +//! - The record is the held lease's on a link that is up: its address and prefix are what the +//! responder is told, and it is told it has no link from the moment the lease goes or the link +//! does. Nothing is probed for, announced or answered for an address still under probe, or on a +//! link that is down. +//! - A link that comes back under a held lease is a link after none: the lease outlives the link, +//! 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`]). //! - 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 the caller's to log ([`Node::name_event`]). -//! - A probe or an announcement that falls due with the link down is [udp]'s to refuse and is -//! counted unsent, and the responder is not told: it holds a name it claimed on no wire until -//! the link's return, which it hears of and probes on, and until then nothing can ask for it. +//! - What became of the name, claimed or lost, is a line of [`Node::drain_events`]. //! - Every message leaves with TTL 255 (§11). //! - An answer goes where the responder says: to the group on its port, or to the asker's own //! address and port. One [udp] refuses is counted and dropped: the asker asks again. @@ -28,13 +28,13 @@ use alloc::vec; use alloc::vec::Vec; use core::net::Ipv4Addr; -use toyos_mdns::{Event, Host, Link, Responder, Source, To, GROUP, PORT}; +use toyos_mdns::{Host, Link, Responder, Source, To, GROUP, PORT}; use toyos_net_udp::SocketId; use toyos_net_wire::ipv4::{MulticastAddr, Ttl}; use toyos_net_wire::{Instant, Port}; use crate::lease::Stack; -use crate::Node; +use crate::{Event, Node}; pub(crate) struct Name { socket: SocketId, @@ -66,64 +66,60 @@ impl Name { Ok(Self { socket, record: Responder::new(host), message: vec![0; toyos_net_udp::limits::MAX_PAYLOAD] }) } - /// The link came back: the name of a lease that outlived it is probed for again. - pub(crate) fn link_returned(&mut self, now: Instant) { - self.record.link_returned(millis(now)); - } - pub(crate) fn next_deadline(&self) -> Option { self.record.owed_at().map(Instant::from_millis) } - /// Tells the responder what lease is held, sends what the name is owed now, then hands it - /// every message that arrived and sends what each is answered. Returns how many messages - /// [udp] refused. - fn pass(&mut self, now: Instant, stack: &mut Stack, draw: &mut impl FnMut() -> u32) -> u64 { - let link = stack.lease().map(|lease| Link { addr: lease.address.octets(), prefix: lease.prefix_len }); + /// Tells the responder what lease is held and whether the link is `up`, hands it every + /// message that arrived and sends what each is answered, then sends what the name is owed + /// now. What became of the name goes to `events`. Returns how many messages [udp] refused. + fn pass(&mut self, now: Instant, stack: &mut Stack, up: bool, events: &mut Vec, draw: &mut impl FnMut() -> u32) -> u64 { + let link = stack.lease().filter(|_| up).map(|lease| Link { addr: lease.address.octets(), prefix: lease.prefix_len }); let (ms, group) = (millis(now), Ipv4Addr::from(GROUP)); let mut unsent = 0u64; - if let Some(owed) = self.record.on(link, ms, draw) { - unsent = unsent.saturating_add(send(stack, now, self.socket, group, PORT, &owed)); - } + self.record.on(link, ms); loop { let received = match stack.recv_from(self.socket, &mut self.message) { Ok(Some(received)) => received, - Ok(None) => return unsent, + Ok(None) => break, Err(_) => unreachable!("the responder's socket is never closed and never connected"), }; let Some(message) = self.message.get(..received.len) else { continue }; // Port 0 is no port to answer at: [udp] refuses the answer. let port = received.source_port.map_or(0, Port::get); - let Some(answer) = self.record.heard(message, Source { addr: received.source.octets(), port }, ms) else { continue }; + let (answer, event) = self.record.heard(message, Source { addr: received.source.octets(), port }, ms); + events.extend(event.map(Event::Name)); + let Some(answer) = answer else { continue }; let (to, port) = match answer.to { To::Group => (group, PORT), To::Asker => (received.source, port), }; unsent = unsent.saturating_add(send(stack, now, self.socket, to, port, &answer.bytes)); } + let (owed, event) = self.record.owed(ms, draw); + events.extend(event.map(Event::Name)); + if let Some(owed) = owed { + unsent = unsent.saturating_add(send(stack, now, self.socket, group, PORT, &owed)); + } + unsent } } impl Node { /// Answers for `.local` from here on, once it is claimed: the node joins the multicast - /// DNS group and holds its port, and a lease already held is probed on by this call, which - /// then spends one draw. Refused when the port is taken, by a client's socket or by an - /// earlier call. + /// DNS group and holds its port, and a lease already held on a link that is up is probed on + /// by this call, which then spends one draw. Refused when the port is taken, by a client's + /// socket or by an earlier call. pub fn answer_as(&mut self, now: Instant, host: Host<'static>, mut draw: impl FnMut() -> u32) -> Result<(), toyos_net_udp::Error> { self.name = Some(Name::new(now, &mut self.stack, host)?); self.serve_name(now, &mut draw); Ok(()) } - /// The latest of what became of the name since the last call: a line for the log. - pub fn name_event(&mut self) -> Option { - self.name.as_mut()?.record.take_event() - } - /// The name's pass, if it has been started, and the count of what [udp] refused it. pub(crate) fn serve_name(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) { if let Some(name) = &mut self.name { - let unsent = name.pass(now, &mut self.stack, draw); + let unsent = name.pass(now, &mut self.stack, self.up, &mut self.events, draw); self.counters.add(crate::Counter::NameUnsent, unsent); } } diff --git a/userland/netstack/node/tests/name.rs b/userland/netstack/node/tests/name.rs index df8dcff2036..823054774ee 100644 --- a/userland/netstack/node/tests/name.rs +++ b/userland/netstack/node/tests/name.rs @@ -13,7 +13,7 @@ use std::time::Duration; use common::{arp, sum, terms, A, BROADCAST, MAC, MAC_B, MAC_R, R}; use lan::{udp, Lan, Seen, Udp, B, OFF_LINK}; -use toyos_mdns::{Event, Host, Lost}; +use toyos_mdns::{Event, Host}; use toyos_net_node::{Counter, Refused}; use toyos_net_wire::{Instant, Port}; @@ -49,6 +49,15 @@ fn named() -> Lan { lan } +/// What the node logged of its name since the last call, in order. +fn said(lan: &mut Lan) -> Vec { + let name = |event| match event { + toyos_net_node::Event::Name(event) => Some(event), + _ => None, + }; + lan.node.drain_events().filter_map(name).collect() +} + /// The node's datagrams until there are `count` of them, which is at most `limit` away. fn until(lan: &mut Lan, count: usize, limit: Duration) { assert!(lan.run_until(limit, |lan| lan.datagrams().len() >= count), "{count} datagrams: {:?}", lan.datagrams()); @@ -61,7 +70,7 @@ fn settled() -> Lan { let mut lan = named(); lan.lease(3_600); until(&mut lan, 5, Duration::from_secs(3)); - assert_eq!(lan.node.name_event(), Some(Event::Claimed)); + assert_eq!(said(&mut lan), [Event::Claimed]); let quiet = lan.now.after(Duration::from_secs(2)); lan.fire(quiet); assert_eq!(lan.datagrams().len(), 5); @@ -247,17 +256,17 @@ fn a_held_lease_is_probed_for_three_times_and_its_name_announced_only_then() { assert!(lan.datagrams().is_empty(), "an address under probe has no name yet"); assert!(lan.run_until(Duration::from_secs(10), |lan| lan.node.lease().is_some()), "the lease is held"); let held = lan.now; - assert_eq!(lan.node.name_event(), None, "the name is not this machine's yet"); + assert_eq!(said(&mut lan), [], "the name is not this machine's yet"); let asked = to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN)); until(&mut lan, 3, SECOND); - assert_eq!(lan.node.name_event(), None); + assert_eq!(said(&mut lan), []); lan.deliver(&asked); assert_eq!(since(&lan, 0), [probe(), probe(), probe()].map(multicast), "three probes, and no answer under them"); until(&mut lan, 5, Duration::from_secs(2)); claimed(&lan, 0, held); - assert_eq!(lan.node.name_event(), Some(Event::Claimed)); + assert_eq!(said(&mut lan), [Event::Claimed]); let later = lan.now.after(Duration::from_secs(10)); lan.fire(later); assert_eq!(lan.datagrams().len(), 5, "owed five, sent five"); @@ -318,40 +327,57 @@ fn a_held_name_is_probed_for_again_when_the_link_returns_and_announced_only_then until(&mut lan, from + 1, SECOND); lan.deliver(&to_group(MAC_B, (B, 53_000), &ours(7, CLASS_IN))); assert_eq!(since(&lan, from), [multicast(probe())], "probed for, and answered to nobody under the probe"); - assert_eq!(lan.node.name_event(), None); + assert_eq!(said(&mut lan), []); until(&mut lan, from + 5, Duration::from_secs(3)); claimed(&lan, from, back); - assert_eq!(lan.node.name_event(), Some(Event::Claimed)); + assert_eq!(said(&mut lan), [Event::Claimed]); let quiet = lan.now.after(Duration::from_secs(10)); lan.fire(quiet); assert_eq!(lan.datagrams().len(), from + 5, "owed five, sent five"); } -// A probe or an announcement that falls due with the link down: [udp] has no route for it, so it -// is counted and gone, and the responder holds it sent. The name it then claims was probed for -// on no wire, and nobody can ask for it there; the link's return is told to the responder, which -// probes for it again (RFC 6762 §8) before a host on the link hears it announced. +// The responder is told it has no link while the link is down: nothing of the name falls due +// there, nothing is counted unsent, and no name is said claimed on no wire. The link's return is +// a link after none, probed on (RFC 6762 §8) before a host on it hears the name announced. #[test] -fn what_the_name_is_owed_with_the_link_down_is_counted_and_the_links_return_probes_for_it() { +fn a_name_has_no_link_while_the_link_is_down_and_the_links_return_is_probed_on() { let mut lan = named(); lan.lease(3_600); + until(&mut lan, 1, SECOND); lan.link(false); - let left = lan.datagrams().len(); assert!(!lan.run_until(Duration::from_secs(5), |_| false), "every deadline of five seconds"); - assert_eq!(lan.datagrams().len(), left, "nothing leaves a link that is down"); - let unsent = lan.node.counters().get(Counter::NameUnsent); - assert_eq!(u64::try_from(left).unwrap() + unsent, 5, "three probes and two announcements, each sent or counted"); - assert!(unsent >= 4, "all but a probe that left before the link went"); - assert_eq!(lan.node.name_event(), Some(Event::Claimed), "held, on no wire"); + assert_eq!(since(&lan, 0), [multicast(probe())], "the probe that left before the link went, and nothing after"); + assert_eq!(lan.node.counters().get(Counter::NameUnsent), 0, "nothing of the name fell due on a link that is down"); + assert_eq!(said(&mut lan), [], "and no name is claimed on no wire"); lan.now = lan.now.after(Duration::from_secs(5)); lan.link(true); let back = lan.now; - until(&mut lan, left + 5, Duration::from_secs(3)); - claimed(&lan, left, back); - assert_eq!(lan.node.name_event(), Some(Event::Claimed)); - assert_eq!(lan.node.counters().get(Counter::NameUnsent), unsent); + until(&mut lan, 6, Duration::from_secs(3)); + claimed(&lan, 1, back); + assert_eq!(said(&mut lan), [Event::Claimed]); + assert_eq!(lan.node.counters().get(Counter::NameUnsent), 0); +} + +// RFC 6762 §8.1: "If, by 250 ms after the third probe, no conflicting Multicast DNS responses +// have been received, the host may move to the next step, announcing." A response the node +// receives at the instant those 250 ms end has been received by then: it is handed to the +// responder before the responder is asked what it owes, and the name is lost and never announced. +#[test] +fn a_conflicting_response_received_as_the_probing_ends_takes_the_name() { + let mut lan = named(); + lan.lease(3_600); + until(&mut lan, 3, SECOND); + let ends = times(&lan, 0)[2].after(QUARTER); + assert_eq!(lan.node.next_deadline(), Some(ends), "the name is held at the node's next deadline"); + 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)); + lan.fire(later); + assert_eq!(since(&lan, 0), [probe(), probe(), probe()].map(multicast), "three probes, and no announcement"); + assert_eq!(said(&mut lan), []); } // RFC 6762 §8.1: "During probing, from the time the first probe packet is sent until 250 ms after @@ -369,20 +395,20 @@ fn another_hosts_answer_under_the_probe_takes_the_name_and_the_node_says_so() { lan.lease(3_600); until(&mut lan, 1, SECOND); lan.deliver(&defended); - assert_eq!(lan.node.name_event(), Some(Event::Lost(Lost::Answered))); + assert_eq!(said(&mut lan), [Event::Lost]); let later = lan.now.after(Duration::from_secs(60)); 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))); assert_eq!(since(&lan, 0), [multicast(probe())], "one probe, and then no probe, no announcement and no answer"); - assert_eq!(lan.node.name_event(), None, "said once"); + assert_eq!(said(&mut lan), [], "said once"); lan.link(false); lan.link(true); let back = lan.now; until(&mut lan, 6, Duration::from_secs(3)); claimed(&lan, 1, back); - assert_eq!(lan.node.name_event(), Some(Event::Claimed), "the other host is gone, and the name is this machine's"); + assert_eq!(said(&mut lan), [Event::Claimed], "the other host is gone, and the name is this machine's"); } } @@ -407,7 +433,7 @@ fn a_later_probe_of_another_hosts_makes_the_node_wait_a_second_and_probe_again() assert!(after(heard, times(&lan, 0)[2], SECOND), "a later one is deferred to for a second: {:?}", times(&lan, 0)[2].since(heard)); until(&mut lan, 7, Duration::from_secs(3)); claimed(&lan, 2, times(&lan, 0)[2]); - assert_eq!(lan.node.name_event(), Some(Event::Claimed), "nobody answered the second probing"); + assert_eq!(said(&mut lan), [Event::Claimed], "nobody answered the second probing"); } // RFC 6762 §9: a response that conflicts with a held name means the responder "MUST immediately @@ -427,7 +453,7 @@ fn a_held_name_is_defended_and_probed_for_again_when_another_host_answers_for_it assert_eq!(lan.datagrams().len(), from + 1, "the name is answered to nobody"); until(&mut lan, from + 6, Duration::from_secs(3)); claimed(&lan, from + 1, contested); - assert_eq!(lan.node.name_event(), Some(Event::Claimed), "B said no more: the name is held again"); + assert_eq!(said(&mut lan), [Event::Claimed], "B said no more: the name is held again"); } // RFC 6762 §6.7: a query from a port other than 5353 is a legacy resolver's, and its answer goes diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs index bb763e574e1..1c6d337f5d6 100644 --- a/userland/netstack/src/main.rs +++ b/userland/netstack/src/main.rs @@ -1435,7 +1435,7 @@ fn main() { if let Card::Intel(nic) = &device.nic { nic.accept_multicast(toyos_mdns::GROUP_MAC); } - let mut mdns = mdns::Responder::new(dhcp::HOSTNAME, &mut iface, &mut socket_set); + let mut mdns = mdns::Responder::new(dhcp::HOSTNAME, &mut iface, &mut socket_set, resolve::random_u16); let total_mem = total_memory(); let max_piped = max_piped_connections(total_mem); @@ -1497,7 +1497,7 @@ fn main() { ); } - mdns.pass(&iface, &mut socket_set, Instant::now()); + mdns.pass(&iface, &mut socket_set, link_up, Instant::now()); daemon.bridge_piped(&mut socket_set); diff --git a/userland/netstack/src/mdns.rs b/userland/netstack/src/mdns.rs index bedc1cb6077..886a8b20f7d 100644 --- a/userland/netstack/src/mdns.rs +++ b/userland/netstack/src/mdns.rs @@ -3,17 +3,20 @@ //! the network reaches it by name with nothing configured on either — the //! development host finds the T14's served log this way. //! -//! **Answered only while an address is held and the name is claimed**: every -//! address after none is probed on first (§8.1), which takes the name's first -//! answer three quarters of a second and a drawn delay past the lease, and a -//! name another host answers for is not this machine's. Every decision is -//! `toyos_mdns::Responder`'s; this is the socket, the clock, the kernel's -//! random source for the delay the responder asks a draw for, and the log -//! line for what became of the name. What the name is owed later — a probe, -//! an announcement (§8.3), or an answer §6 held back — is a wake of -//! netstack's own loop ([`Responder::wake_in`]) rather than a sleep, because -//! the protocol names the interval and nothing on the wire says when it has -//! passed. +//! **Answered only while an address is held on a link that is up, and the +//! name is claimed**: every address after none and every return of the link +//! is probed on first (§8, §8.1), which takes the name's first answer three +//! quarters of a second and a drawn delay past it, and a name another host +//! answers for is not this machine's. Every decision is +//! `toyos_mdns::Responder`'s; this is the socket, the clock, the draw of the +//! delay the responder asks for, and the log line for what became of the +//! name. A pass tells the responder its link, hands it every message that +//! has arrived, and only then asks what the name is owed: a conflicting +//! response received as a probing ends takes the name before it is claimed. +//! What the name is owed later — a probe, an announcement (§8.3), or an +//! answer §6 held back — is a wake of netstack's own loop +//! ([`Responder::wake_in`]) rather than a sleep, because the protocol names +//! the interval and nothing on the wire says when it has passed. use std::net::Ipv4Addr; use std::time::{Duration, Instant}; @@ -21,25 +24,28 @@ 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, Lost, Source, To, CONFLICTS, GROUP, PORT}; +use toyos_mdns::{Event, Host, Link, Source, To, GROUP, PORT}; /// 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 /// sender's own retry is for. const BUFFER: usize = 4096; -pub struct Responder { +pub struct Responder { handle: SocketHandle, host: &'static str, record: toyos_mdns::Responder<'static>, /// The origin of the responder's clock. born: Instant, + /// The delay before each probing's first probe. + draw: D, } -impl Responder { +impl u16> Responder { /// Join the group and bind its port. `host` is the name this machine asks - /// its network to record for it (`dhcp::HOSTNAME`). - pub fn new(host: &'static str, iface: &mut Interface, socket_set: &mut SocketSet<'static>) -> Self { + /// its network to record for it (`dhcp::HOSTNAME`), and `draw` the source + /// of each probing's delay. + pub fn new(host: &'static str, iface: &mut Interface, socket_set: &mut SocketSet<'static>, draw: D) -> Self { let label = Host::new(host).unwrap_or_else(|_| panic!("netstack: {host:?} is no host name")); iface .join_multicast_group(IpAddress::Ipv4(Ipv4Addr::from(GROUP))) @@ -49,28 +55,29 @@ impl Responder { }; let mut socket = udp::Socket::new(buffer(), buffer()); socket.bind(PORT).expect("netstack: nothing else binds the multicast DNS port"); - Self { handle: socket_set.add(socket), host, record: toyos_mdns::Responder::new(label), born: Instant::now() } + Self { handle: socket_set.add(socket), host, record: toyos_mdns::Responder::new(label), born: Instant::now(), draw } } - /// After each poll: send what the name is owed now, hand the responder - /// every message that arrived and send what each is answered, then say - /// what became of the name. - pub fn pass(&mut self, iface: &Interface, socket_set: &mut SocketSet<'_>, now: Instant) { + /// After each poll: tell the responder its link, which is the interface's + /// address while the card's link is up; hand it every message that + /// arrived and send what each is answered; then send what the name is + /// owed now. What became of the name is said as it happens. + pub fn pass(&mut self, iface: &Interface, socket_set: &mut SocketSet<'_>, link_up: bool, now: Instant) { let socket = socket_set.get_mut::(self.handle); // IPv4 is the one protocol this netstack is built with, so every address is one. - let link = iface.ip_addrs().first().map(|&IpCidr::Ipv4(cidr)| Link { + let link = iface.ip_addrs().first().filter(|_| link_up).map(|&IpCidr::Ipv4(cidr)| Link { addr: cidr.address().octets(), prefix: cidr.prefix_len(), }); let now_ms = self.ms(now); let group = IpEndpoint::new(IpAddress::Ipv4(Ipv4Addr::from(GROUP)), PORT); - if let Some(owed) = self.record.on(link, now_ms, || u32::from(crate::resolve::random_u16())) { - send(socket, &owed, group); - } + self.record.on(link, now_ms); while let Ok((message, meta)) = socket.recv() { let IpAddress::Ipv4(from) = meta.endpoint.addr; let from = Source { addr: from.octets(), port: meta.endpoint.port }; - let Some(answer) = self.record.heard(message, from, now_ms) else { + let (answer, event) = self.record.heard(message, from, now_ms); + said(self.host, event); + let Some(answer) = answer else { continue; }; let to = match answer.to { @@ -79,16 +86,10 @@ impl Responder { }; send(socket, &answer.bytes, to); } - let host = self.host; - match self.record.take_event() { - Some(Event::Claimed) => crate::say!("netstack: mDNS: no host answered for {host}.local; this machine answers as it"), - Some(Event::Lost(Lost::Answered)) => crate::say!( - "netstack: mDNS: another host answered for {host}.local; this machine answers to no name" - ), - Some(Event::Lost(Lost::Contested)) => crate::say!( - "netstack: mDNS: {host}.local was contested {CONFLICTS} times; this machine answers to no name" - ), - None => {} + let (owed, event) = self.record.owed(now_ms, || u32::from((self.draw)())); + said(self.host, event); + if let Some(owed) = owed { + send(socket, &owed, group); } } @@ -102,6 +103,15 @@ impl Responder { } } +/// The log line for what became of `host`'s name. +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"), + None => {} + } +} + fn send(socket: &mut udp::Socket, bytes: &[u8], to: IpEndpoint) { match socket.send_slice(bytes, to) { Ok(()) => {} @@ -117,30 +127,120 @@ fn send(socket: &mut udp::Socket, bytes: &[u8], to: IpEndpoint) { #[cfg(test)] mod tests { use super::*; - use smoltcp::iface::SocketSet; + use smoltcp::iface::{Config, PollResult}; + use smoltcp::phy::{Device, Loopback, Medium, TxToken}; + use smoltcp::time::Instant as PollInstant; + use smoltcp::wire::{EthernetAddress, HardwareAddress, Ipv4Address}; - const LINK: Link = Link { addr: [10, 0, 2, 15], prefix: 24 }; + /// The bytes of a probe for `t14.local` (RFC 6762 §8.1, RFC 1035 §4.1): a + /// 12-byte header, the 11-byte name with its type and class, and the + /// proposed record, the name again and 14 bytes behind it. + const PROBE: usize = 12 + 11 + 4 + 11 + 14; + /// The bytes of an announcement: the header and the record. + const ANNOUNCEMENT: usize = 12 + 11 + 14; - /// A `Responder` over a socket taken from a set of its own — `wake_in` - /// touches neither, only `record` and `born`. - fn responder() -> Responder { - let buffer = || udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0u8; 64]); - let mut set = SocketSet::new(Vec::new()); - let handle = set.add(udp::Socket::new(buffer(), buffer())); - Responder { handle, host: "t14", record: toyos_mdns::Responder::new(Host::new("t14").unwrap()), born: Instant::now() } + /// The responder as `main` runs it, over smoltcp's loopback device: a + /// frame handed to the device is the next one the interface receives. + struct Machine { + device: Loopback, + iface: Interface, + sockets: SocketSet<'static>, + mdns: Responder u16>, + } + + impl Machine { + /// 10.0.2.15/24, its delay drawn as zero. + fn new() -> Self { + let mut device = Loopback::new(Medium::Ethernet); + let config = Config::new(HardwareAddress::Ethernet(EthernetAddress([2, 0, 0, 0, 0, 1]))); + let mut iface = Interface::new(config, &mut device, PollInstant::from_millis(0)); + iface.update_ip_addrs(|addrs| addrs.push(IpCidr::new(IpAddress::Ipv4(Ipv4Address::new(10, 0, 2, 15)), 24)).unwrap()); + let mut sockets = SocketSet::new(Vec::new()); + let mdns = Responder::new("t14", &mut iface, &mut sockets, (|| 0) as fn() -> u16); + Self { device, iface, sockets, mdns } + } + + /// One pass of `main`'s loop `ms` after the responder was born: the + /// poll, then the name's pass. Returns how many bytes of messages the + /// pass left in the responder's socket. + fn pass(&mut self, ms: u64, link_up: bool) -> usize { + let poll = PollInstant::from_millis(ms as i64); + while self.iface.poll(poll, &mut self.device, &mut self.sockets) != PollResult::None {} + let before = self.sockets.get::(self.mdns.handle).send_queue(); + self.mdns.pass(&self.iface, &mut self.sockets, link_up, self.mdns.born + Duration::from_millis(ms)); + self.sockets.get::(self.mdns.handle).send_queue() - before + } + + fn wake_in(&self, ms: u64) -> Option { + self.mdns.wake_in(self.mdns.born + Duration::from_millis(ms)) + } + + /// A neighbour at 10.0.2.7 multicasts `message` from the group's + /// port: Ethernet II to RFC 1112 §6.4's address, RFC 791's header + /// with TTL 255 and RFC 768's, their checksums zero, which the + /// loopback device does not ask for. + fn hears(&mut self, message: &[u8]) { + let udp = 8 + message.len() as u16; + let total = 20 + udp; + let mut frame = vec![0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb, 2, 0, 0, 0, 0, 7, 0x08, 0x00]; + frame.extend_from_slice(&[0x45, 0, (total >> 8) as u8, total as u8, 0, 0, 0, 0, 255, 17, 0, 0, 10, 0, 2, 7, 224, 0, 0, 251]); + frame.extend_from_slice(&[0x14, 0xe9, 0x14, 0xe9, (udp >> 8) as u8, udp as u8, 0, 0]); + frame.extend_from_slice(message); + let token = self.device.transmit(PollInstant::from_millis(0)).expect("the loopback device always has room"); + token.consume(frame.len(), |buffer| buffer.copy_from_slice(&frame)); + } } /// **A wake is asked for exactly what the name owes.** Nothing before an /// address is held; the first probe's own instant, the drawn delay after - /// the address, once `on` schedules it. A responder that never asks for + /// the address, once a pass schedules it. A responder that never asks for /// this wake sends that probe, and every one after it, only on some /// other, unrelated wake. #[test] fn wake_in_asks_for_what_the_name_owes_and_nothing_else() { - let mut r = responder(); - assert_eq!(r.wake_in(r.born), None, "nothing is owed before an address is held"); - assert_eq!(r.record.on(Some(LINK), 0, || 100), None, "§8.1: the delay before the first probe"); - assert_eq!(r.record.owed_at(), Some(100)); - assert_eq!(r.wake_in(r.born), Some(Duration::from_millis(100))); + let mut m = Machine::new(); + assert_eq!(m.wake_in(0), None, "nothing is owed before an address is held"); + m.mdns.draw = || 100; + assert_eq!(m.pass(0, true), 0, "§8.1: the delay before the first probe"); + assert_eq!(m.wake_in(0), Some(Duration::from_millis(100))); + assert_eq!(m.wake_in(40), Some(Duration::from_millis(60))); + } + + /// RFC 6762 §8: "Whenever a Multicast DNS responder ... receives an + /// indication of a network interface "Link Change" event ... it MUST + /// perform the two startup steps below: Probing (Section 8.1) and + /// Announcing (Section 8.3)." The interface keeps its address across the + /// card's link, so `main`'s word that the link is down is what tells the + /// responder: it owes a link that is down nothing, and probes for the + /// name from the start when the link is back. + #[test] + fn a_link_that_is_down_is_owed_nothing_and_its_return_is_probed_on() { + let mut m = Machine::new(); + assert_eq!((m.pass(0, true), m.pass(250, true), m.pass(500, true)), (PROBE, PROBE, PROBE)); + assert_eq!((m.pass(750, true), m.pass(1_750, true)), (ANNOUNCEMENT, ANNOUNCEMENT), "§8.3: claimed, and announced twice"); + assert_eq!(m.wake_in(1_750), None); + + assert_eq!(m.pass(5_000, false), 0); + assert_eq!(m.wake_in(5_000), None, "nothing is owed a link that is down"); + assert_eq!(m.pass(9_000, true), PROBE, "the link is back, and probed on"); + assert_eq!((m.pass(9_250, true), m.pass(9_500, true), m.pass(9_750, true)), (PROBE, PROBE, ANNOUNCEMENT), "and announced only then"); + } + + /// RFC 6762 §8.1: "If, by 250 ms after the third probe, no conflicting + /// Multicast DNS responses have been received, the host may move to the + /// next step, announcing." A response in the socket when the pass that + /// ends the 250 ms begins has been received: it takes the name, and + /// nothing is announced. The message is RFC 1035 §4.1's: a response + /// (`QR|AA`) with one answer, `t14.local A 10.0.2.99`. + #[test] + fn a_conflicting_response_received_as_the_probing_ends_takes_the_name() { + let mut said = vec![0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]; + said.extend_from_slice(b"\x03t14\x05local\x00\x00\x01\x80\x01\x00\x00\x00\x78\x00\x04\x0a\x00\x02\x63"); + + let mut m = Machine::new(); + assert_eq!((m.pass(0, true), m.pass(250, true), m.pass(500, true)), (PROBE, PROBE, PROBE)); + m.hears(&said); + assert_eq!(m.pass(750, true), 0, "no announcement"); + assert_eq!(m.wake_in(750), None, "and nothing more is owed: the name is another host's"); } } From 29d0b6c0118255591f7f48677dcb618325a6985b Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 12:20:53 +0200 Subject: [PATCH 3/3] The shipped netstack's name is tested without smoltcp, draws its own delay, and the track says what a lost name does The round-2 review's two BLOCKERs and its NOTEs. The shell's two tests over smoltcp's loopback device, and the `Machine` they stood on, are deleted: the owner's ruling is "no smoltcp.", no new test built on the old stack, and the shell leaves at the move. `main`'s `wake_in` test stays in `main`'s shape, a handle from a set of its own and the record driven directly. What the two deleted tests held, that a pass says no link while the card's is down and reads before it asks what is owed, now rests on reading `pass`, with the responder's and the node's tests holding the mechanism. `Responder` and `new`'s `draw` had one value outside those tests, so both go: `pass` calls `resolve::random_u16`. The Intel driver reports a link only where a pass finds it different, so a link that goes and returns inside one pass reaches the name as no change: filed, owner the driver, not fixed here. The track's name line and the responder's header say that a lost name stays lost until a link after none, what that costs a peer, and the owner's open question of probing again on an interval; the line records what stays unmeasured, the claim on the T14's wired card and the link's return on metal, each with its exit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...e-intel-driver-is-reported-as-no-change.md | 40 +++++ issues/toyos-has-its-own-network-stack.md | 2 +- userland/netstack/mdns/src/lib.rs | 5 +- userland/netstack/src/main.rs | 2 +- userland/netstack/src/mdns.rs | 138 +++--------------- 5 files changed, 69 insertions(+), 118 deletions(-) create mode 100644 issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md diff --git a/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md b/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md new file mode 100644 index 00000000000..646b8e2e750 --- /dev/null +++ b/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md @@ -0,0 +1,40 @@ +--- +status: open +kind: defect +opened: 2026-10-09 +--- + +# A link that goes and returns inside one pass of the Intel driver is reported as no change + +`toyos-i219`'s `begin_pass` (`toyos-i219/src/lib.rs`) reads the link only on +a pass that acknowledged `LSC`, and answers `link_changed: self.link != +before`: the state after the pass compared with the state before it. `LSC` is +one bit, so a link that went down and came back at the same speed and duplex +before one pass acknowledged it is read as the link it was, and the pass +answers no change. `Nic::begin_pass` (`userland/netstack/src/i219.rs`) hands +netstack a link only where that is true, so `main`'s `link_up` stays true and +no pass tells the name's responder it had no link. + +RFC 6762 §8 asks for probing and announcing whenever a responder "receives an indication of a network interface "Link Change" +event". The part gave that indication, the `LSC` cause, and the driver held +it and dropped it: the name is neither probed for nor announced on that +link, which may be another network's. + +## What is known and what is not + +Read from the source, and not measured. Not known: whether a real link can +go and return between two passes. A pass begins on the `LSC` +interrupt, so it needs netstack kept off the CPU for a whole renegotiation. +No machine has been read across a link change at all +(`issues/no-machine-has-read-an-intel-nic-across-a-link-change.md`). + +## Owner + +The driver's `Pass`, under stage 2 of +`issues/the-lan-is-not-yet-production-grade.md`. + +## Exit + +A host test in `toyos-i219` in which a pass that acknowledged `LSC` with +`STATUS` as it was before reports the change, and netstack's name has no +link on that pass. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 8fd644bd5dc..7c16a7c7c75 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. 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. +- 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`. - A query [ip] refuses at the transmit opportunity that would have carried it, its next hop held failed or its table full, is reported to its socket inside `Node::transmit`, which carries no lookup on: the resolver reads the report at the node's next `receive`, `fire` or `link`, which at the latest is the end of the query's wait, where a query [udp] refuses in the resolver's own call, or one reported in a frame or at a deadline, ends its wait at once. `a_query_ip_refuses_at_its_transmit_opportunity_is_read_at_the_nodes_next_wake` holds it as it stands. It is no regression: what ships waits every such query out. Until that read, an ICMP error that quotes the query's addresses and ports takes [ip]'s word's place on the socket, [udp] holding one pending error a socket, the latest: the query is then waited out and not let go, which costs the early end and never an answer. Owner: the slice in which datagram senders wait on the hop (`Node::transmit` is the node's `lib.rs`). Exit: the node carries its lookups on after the stack's transmit, or [udp] refuses in the call a datagram whose next hop [ip] holds failed, so that no report waits on a socket to be displaced, and that test finds its lookup ended at the 4 s it started. - [ip]'s queue for a next hop it is resolving is every sender's: `PENDING_PER_NEIGHBOUR` is 16, a stopgap which the node's resolver asserts is no fewer than `toyos_dns::MAX_LOOKUPS`, so every held lookup's first query waits there whole, but a datagram a client sends to the same next hop in that window, which for everything off the link is the router, takes the oldest one's place (`nb.pending-overflow`, RFC 4861 §7.2.2), and a query lost so costs its lookup a `WAIT_MS`; smoltcp kept each datagram in its own socket until the neighbour answered. `a_clients_datagram_behind_the_burst_takes_the_oldest_querys_place_in_ips_queue` holds it as it stands. It is a regression against what ships for more than 16 datagrams at one next hop not yet resolved, and would ship at the move; and it sizes a limit of [ip]'s by a constant of the resolver's, which leaves four unresolved next hops filling `PENDING_TOTAL` where eight did. Owner: the slice in which datagram senders wait on the hop, where [udp]'s datagram calls already go through `Stack` and `Shard`. Its design: [udp]'s `serve` asks [ip] the next-hop question before it hands a datagram over, as [tcp]'s exit does with `Hop` (NUD-25, IP-D5); a socket whose oldest datagram's hop is pending keeps it in its own queue, which `TX_DATAGRAMS` and `TX_BYTES` already bound and refuse in the call, and is offered again when the hop resolves and told when it fails; [ip] then queues only its own datagrams. Exit, before the move: that test finds every query on the wire once the resolver answers ARP; `PENDING_PER_NEIGHBOUR` is `limits.ip.nud.pending_per_neighbour`'s 8 again; and the resolver's compile-time assertion, the change to `s_ip_nud_004_overflow_drops_the_oldest` and stage 3's departure line about it are deleted. - `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 7465e68b5d5..08dce48654e 100644 --- a/userland/netstack/mdns/src/lib.rs +++ b/userland/netstack/mdns/src/lib.rs @@ -70,7 +70,10 @@ //! 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. +//! - 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. //! //! Where an answer goes follows the question (§5.4, §6.7): //! diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs index 7aeb988cee6..b0c27ccc70f 100644 --- a/userland/netstack/src/main.rs +++ b/userland/netstack/src/main.rs @@ -1449,7 +1449,7 @@ fn main() { if let Card::Intel(nic) = &device.nic { nic.accept_multicast(toyos_mdns::GROUP_MAC); } - let mut mdns = mdns::Responder::new(dhcp::HOSTNAME, &mut iface, &mut socket_set, resolve::random_u16); + let mut mdns = mdns::Responder::new(dhcp::HOSTNAME, &mut iface, &mut socket_set); let total_mem = total_memory(); let max_piped = max_piped_connections(total_mem); diff --git a/userland/netstack/src/mdns.rs b/userland/netstack/src/mdns.rs index 886a8b20f7d..f148e52bccb 100644 --- a/userland/netstack/src/mdns.rs +++ b/userland/netstack/src/mdns.rs @@ -31,21 +31,18 @@ use toyos_mdns::{Event, Host, Link, Source, To, GROUP, PORT}; /// sender's own retry is for. const BUFFER: usize = 4096; -pub struct Responder { +pub struct Responder { handle: SocketHandle, host: &'static str, record: toyos_mdns::Responder<'static>, /// The origin of the responder's clock. born: Instant, - /// The delay before each probing's first probe. - draw: D, } -impl u16> Responder { +impl Responder { /// Join the group and bind its port. `host` is the name this machine asks - /// its network to record for it (`dhcp::HOSTNAME`), and `draw` the source - /// of each probing's delay. - pub fn new(host: &'static str, iface: &mut Interface, socket_set: &mut SocketSet<'static>, draw: D) -> Self { + /// its network to record for it (`dhcp::HOSTNAME`). + pub fn new(host: &'static str, iface: &mut Interface, socket_set: &mut SocketSet<'static>) -> Self { let label = Host::new(host).unwrap_or_else(|_| panic!("netstack: {host:?} is no host name")); iface .join_multicast_group(IpAddress::Ipv4(Ipv4Addr::from(GROUP))) @@ -55,7 +52,7 @@ impl u16> Responder { }; let mut socket = udp::Socket::new(buffer(), buffer()); socket.bind(PORT).expect("netstack: nothing else binds the multicast DNS port"); - Self { handle: socket_set.add(socket), host, record: toyos_mdns::Responder::new(label), born: Instant::now(), draw } + Self { handle: socket_set.add(socket), host, record: toyos_mdns::Responder::new(label), born: Instant::now() } } /// After each poll: tell the responder its link, which is the interface's @@ -86,7 +83,7 @@ impl u16> Responder { }; send(socket, &answer.bytes, to); } - let (owed, event) = self.record.owed(now_ms, || u32::from((self.draw)())); + let (owed, event) = self.record.owed(now_ms, || u32::from(crate::resolve::random_u16())); said(self.host, event); if let Some(owed) = owed { send(socket, &owed, group); @@ -127,120 +124,31 @@ fn send(socket: &mut udp::Socket, bytes: &[u8], to: IpEndpoint) { #[cfg(test)] mod tests { use super::*; - use smoltcp::iface::{Config, PollResult}; - use smoltcp::phy::{Device, Loopback, Medium, TxToken}; - use smoltcp::time::Instant as PollInstant; - use smoltcp::wire::{EthernetAddress, HardwareAddress, Ipv4Address}; + use smoltcp::iface::SocketSet; - /// The bytes of a probe for `t14.local` (RFC 6762 §8.1, RFC 1035 §4.1): a - /// 12-byte header, the 11-byte name with its type and class, and the - /// proposed record, the name again and 14 bytes behind it. - const PROBE: usize = 12 + 11 + 4 + 11 + 14; - /// The bytes of an announcement: the header and the record. - const ANNOUNCEMENT: usize = 12 + 11 + 14; + const LINK: Link = Link { addr: [10, 0, 2, 15], prefix: 24 }; - /// The responder as `main` runs it, over smoltcp's loopback device: a - /// frame handed to the device is the next one the interface receives. - struct Machine { - device: Loopback, - iface: Interface, - sockets: SocketSet<'static>, - mdns: Responder u16>, - } - - impl Machine { - /// 10.0.2.15/24, its delay drawn as zero. - fn new() -> Self { - let mut device = Loopback::new(Medium::Ethernet); - let config = Config::new(HardwareAddress::Ethernet(EthernetAddress([2, 0, 0, 0, 0, 1]))); - let mut iface = Interface::new(config, &mut device, PollInstant::from_millis(0)); - iface.update_ip_addrs(|addrs| addrs.push(IpCidr::new(IpAddress::Ipv4(Ipv4Address::new(10, 0, 2, 15)), 24)).unwrap()); - let mut sockets = SocketSet::new(Vec::new()); - let mdns = Responder::new("t14", &mut iface, &mut sockets, (|| 0) as fn() -> u16); - Self { device, iface, sockets, mdns } - } - - /// One pass of `main`'s loop `ms` after the responder was born: the - /// poll, then the name's pass. Returns how many bytes of messages the - /// pass left in the responder's socket. - fn pass(&mut self, ms: u64, link_up: bool) -> usize { - let poll = PollInstant::from_millis(ms as i64); - while self.iface.poll(poll, &mut self.device, &mut self.sockets) != PollResult::None {} - let before = self.sockets.get::(self.mdns.handle).send_queue(); - self.mdns.pass(&self.iface, &mut self.sockets, link_up, self.mdns.born + Duration::from_millis(ms)); - self.sockets.get::(self.mdns.handle).send_queue() - before - } - - fn wake_in(&self, ms: u64) -> Option { - self.mdns.wake_in(self.mdns.born + Duration::from_millis(ms)) - } - - /// A neighbour at 10.0.2.7 multicasts `message` from the group's - /// port: Ethernet II to RFC 1112 §6.4's address, RFC 791's header - /// with TTL 255 and RFC 768's, their checksums zero, which the - /// loopback device does not ask for. - fn hears(&mut self, message: &[u8]) { - let udp = 8 + message.len() as u16; - let total = 20 + udp; - let mut frame = vec![0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb, 2, 0, 0, 0, 0, 7, 0x08, 0x00]; - frame.extend_from_slice(&[0x45, 0, (total >> 8) as u8, total as u8, 0, 0, 0, 0, 255, 17, 0, 0, 10, 0, 2, 7, 224, 0, 0, 251]); - frame.extend_from_slice(&[0x14, 0xe9, 0x14, 0xe9, (udp >> 8) as u8, udp as u8, 0, 0]); - frame.extend_from_slice(message); - let token = self.device.transmit(PollInstant::from_millis(0)).expect("the loopback device always has room"); - token.consume(frame.len(), |buffer| buffer.copy_from_slice(&frame)); - } + /// A `Responder` over a socket taken from a set of its own — `wake_in` + /// touches neither, only `record` and `born`. + fn responder() -> Responder { + let buffer = || udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0u8; 64]); + let mut set = SocketSet::new(Vec::new()); + let handle = set.add(udp::Socket::new(buffer(), buffer())); + Responder { handle, host: "t14", record: toyos_mdns::Responder::new(Host::new("t14").unwrap()), born: Instant::now() } } /// **A wake is asked for exactly what the name owes.** Nothing before an /// address is held; the first probe's own instant, the drawn delay after - /// the address, once a pass schedules it. A responder that never asks for - /// this wake sends that probe, and every one after it, only on some + /// the address, once the record schedules it. A responder that never asks + /// for this wake sends that probe, and every one after it, only on some /// other, unrelated wake. #[test] fn wake_in_asks_for_what_the_name_owes_and_nothing_else() { - let mut m = Machine::new(); - assert_eq!(m.wake_in(0), None, "nothing is owed before an address is held"); - m.mdns.draw = || 100; - assert_eq!(m.pass(0, true), 0, "§8.1: the delay before the first probe"); - assert_eq!(m.wake_in(0), Some(Duration::from_millis(100))); - assert_eq!(m.wake_in(40), Some(Duration::from_millis(60))); - } - - /// RFC 6762 §8: "Whenever a Multicast DNS responder ... receives an - /// indication of a network interface "Link Change" event ... it MUST - /// perform the two startup steps below: Probing (Section 8.1) and - /// Announcing (Section 8.3)." The interface keeps its address across the - /// card's link, so `main`'s word that the link is down is what tells the - /// responder: it owes a link that is down nothing, and probes for the - /// name from the start when the link is back. - #[test] - fn a_link_that_is_down_is_owed_nothing_and_its_return_is_probed_on() { - let mut m = Machine::new(); - assert_eq!((m.pass(0, true), m.pass(250, true), m.pass(500, true)), (PROBE, PROBE, PROBE)); - assert_eq!((m.pass(750, true), m.pass(1_750, true)), (ANNOUNCEMENT, ANNOUNCEMENT), "§8.3: claimed, and announced twice"); - assert_eq!(m.wake_in(1_750), None); - - assert_eq!(m.pass(5_000, false), 0); - assert_eq!(m.wake_in(5_000), None, "nothing is owed a link that is down"); - assert_eq!(m.pass(9_000, true), PROBE, "the link is back, and probed on"); - assert_eq!((m.pass(9_250, true), m.pass(9_500, true), m.pass(9_750, true)), (PROBE, PROBE, ANNOUNCEMENT), "and announced only then"); - } - - /// RFC 6762 §8.1: "If, by 250 ms after the third probe, no conflicting - /// Multicast DNS responses have been received, the host may move to the - /// next step, announcing." A response in the socket when the pass that - /// ends the 250 ms begins has been received: it takes the name, and - /// nothing is announced. The message is RFC 1035 §4.1's: a response - /// (`QR|AA`) with one answer, `t14.local A 10.0.2.99`. - #[test] - fn a_conflicting_response_received_as_the_probing_ends_takes_the_name() { - let mut said = vec![0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]; - said.extend_from_slice(b"\x03t14\x05local\x00\x00\x01\x80\x01\x00\x00\x00\x78\x00\x04\x0a\x00\x02\x63"); - - let mut m = Machine::new(); - assert_eq!((m.pass(0, true), m.pass(250, true), m.pass(500, true)), (PROBE, PROBE, PROBE)); - m.hears(&said); - assert_eq!(m.pass(750, true), 0, "no announcement"); - assert_eq!(m.wake_in(750), None, "and nothing more is owed: the name is another host's"); + let mut r = responder(); + assert_eq!(r.wake_in(r.born), None, "nothing is owed before an address is held"); + r.record.on(Some(LINK), 0); + assert_eq!(r.record.owed(0, || 100), (None, None), "§8.1: the delay before the first probe"); + assert_eq!(r.wake_in(r.born), Some(Duration::from_millis(100))); + assert_eq!(r.wake_in(r.born + Duration::from_millis(40)), Some(Duration::from_millis(60))); } }