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/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 2b967a14952..e05edcd90b1 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. 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 socket's datagrams that wait for a next hop wait in the one queue it has, `toyos_net_udp::limits::TX_DATAGRAMS` of them: with all 16 waiting for one next hop, the socket's sends to every other destination are refused `udp.tx-queue-full`, `Refused::ResourceExhausted` to a client, until that hop answers or [ip] gives it up, 3 s after its first request left (`BROADCAST_SOLICIT` requests a `RETRANS` apart). It is no regression by this track's earlier record of what ships, that smoltcp kept each datagram in its own socket until the neighbour answered. What a closed socket had accepted waits in the closed sender's `CLOSED_DATAGRAMS` the same way, so 16 datagrams of closed sockets that wait for one silent host have the next closes' datagrams discarded, `udp.tx-discarded-on-close`, for those 3 s: the line on US-53 among stage 3's departures. `s_udp_us_025_what_waits_for_a_next_hop_is_bounded_by_its_sockets_queue` holds the first as it stands. Owner: the worker of the move, with which a program first reaches this queue. Exit, by the move's first run on the T14 or in a guest: it shows a program refused for it, and a socket's bound is counted per next hop; or the owner rules the one queue is the bound. - A link that goes down drops no waiting datagram by itself: every waiting one is handed to [ip] again at the next transmit opportunity, which refuses it for want of a route (`route.no-source-address`), and one for which the link is back before that opportunity waits for its next hop anew, where the readers' link-down rule (NUD-23) drops what waits at once. `s_udp_us_024_shard_a_waiting_datagram_whose_route_goes_is_dropped` holds the first as it stands, and `a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves`, beside it in `toyos-net-shard/tests/udp.rs` and under no scenario's id, the second. Owner: the worker of the move. Exit, before the move: that second test's last assertion is inverted and finds the datagram that waited gone, or the specifications have it wait. - `toyos_dns::Lookup::on_datagram` still takes a reply's source address and port, which the node's connected sockets have already matched and the node hands it from its own record of the query: the two parameters are read only by netd's resolver on smoltcp. Exit, at the move: they go with that resolver. diff --git a/tests/toyos.rs b/tests/toyos.rs index 7db775abb22..323ed4bbf0a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2962,18 +2962,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 66b16f21031..08dce48654e 100644 --- a/userland/netstack/mdns/src/lib.rs +++ b/userland/netstack/mdns/src/lib.rs @@ -8,7 +8,74 @@ //! `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 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 +//! 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 "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 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, 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", 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: 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): //! //! - 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 +84,25 @@ //! 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.** 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 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 +112,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}; @@ -69,14 +147,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 +207,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 +216,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 +253,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 +264,83 @@ 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, + /// §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 +/// 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: 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; + /// §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 a link is held. +#[derive(Clone, Copy, Debug)] +enum Claim { + /// 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 }, + Held, + Lost, +} + /// 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 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::owed`] has handed it over. #[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, + /// When the name last met a conflict. + conflict_ms: 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,91 +351,179 @@ 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, 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 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 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; }; - if self.link.is_none_or(|was| was.addr != link.addr) { - self.owed_ms = Some(now_ms); + match self.link.replace(link) { + 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(_) => {} + } + } + + /// 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, 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)), None); + } + self.claim = Claim::Held; + 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) { - return None; + if !matches!(self.claim, Claim::Held) || !self.owed_ms.is_some_and(|at| now_ms >= at) { + 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)) + (Some(announcement(self.host, link.addr)), event) } - /// 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. - 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; + /// 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; + } + + /// 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 } } - /// 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) + /// 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 is next owed to the group, on the caller's clock. + /// 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 [`Self::owed`] 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 { from_ms } => Some(from_ms), + Claim::Probing { at_ms, .. } => Some(at_ms), + Claim::Held => self.owed_ms, + Claim::Lost => None, + } } - /// 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 { - let link = self.link?; - if !link.holds(asker.addr) { - return None; + /// `message` arrived from `from` at `now_ms`: the answer it is owed now, + /// 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, 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 { - return None; + if flags & QR != 0 { + return (None, self.response(message, from, link, now_ms)); } - 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), None), + Claim::Probing { sent, .. } if sent > 0 => { + self.rival(message, link, now_ms); + (None, None) } + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => (None, None), } - let unicast = asked?; - if asker.port != PORT { + } + + /// 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) -> Option { + let probing = match self.claim { + Claim::Probing { sent, .. } if sent > 0 => true, + Claim::Held => false, + Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => return None, + }; + if from.port != PORT || !conflicts(message, self.host, link.addr).unwrap_or(false) { + return None; + } + let wait = self.conflict(now_ms); + if probing { + self.release(Claim::Lost); + return Some(Event::Lost); + } + self.release(Claim::Owed { from_ms: now_ms.saturating_add(wait) }); + None + } + + /// 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 { + let wait = self.conflict(now_ms).max(DEFER_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 +531,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..a0b6543ad48 --- /dev/null +++ b/userland/netstack/mdns/src/tests.rs @@ -0,0 +1,939 @@ +//! 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") +} + +/// 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 +/// 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 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.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) -> 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) -> 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.said(), r.owed_at()), (vec![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 Machine, 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 = 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"); + 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 = Machine::new(); + 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 = 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.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.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.said(), [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 = 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); + 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.said(), []); + 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.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); + 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.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.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"); + 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 = 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.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); + 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.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)); +} + +/// 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.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"); + 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.said(), [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.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)); +} + +/// 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)); +} + +/// 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." "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_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_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 +/// 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.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); + 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.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 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 +/// 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.said(), [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.said(), []); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), claim_of(ADDR, 49 * 600 + 40_000)[3..]); + assert_eq!(r.said(), [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.said(), r.owed_at()), (vec![Event::Lost], None)); + assert_eq!(run(&mut r, LINK, 3_600_000, 0), []); +} + +/// 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_one_probing_in_five_seconds_and_the_name_is_held_when_they_stop() { + let mut r = held(10_000); + 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_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::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 +/// 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.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.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.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); + 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.said(), [Event::Lost]); +} + +/// 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 = 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.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.said(), []); + + 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.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), 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 +/// 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 4a68d7b9bdb..077eae0a870 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, and alone in -//! [`Node::transmit`]: 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, and alone in [`Node::transmit`], which serves no +//! name: an id and then a port for each query they send, and nothing else. #![no_std] #![forbid(unsafe_code)] @@ -73,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 { @@ -80,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, @@ -106,6 +111,7 @@ impl Node { stack, client, name: None, + up: false, resolver: resolve::Resolver::new(), counters: Counters::default(), events: Vec::new(), @@ -168,14 +174,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); } @@ -218,7 +222,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..1816734468e 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. -//! - 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 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 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. //! -//! **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,19 +28,19 @@ use alloc::vec; use alloc::vec::Vec; use core::net::Ipv4Addr; -use toyos_mdns::{Asker, Host, Link, Responder, 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, 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,60 +63,63 @@ 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] }) - } - - /// The link came back: the record of a lease that outlived it is owed to the group again. - pub(crate) fn link_returned(&mut self, now: Instant) { - self.record.link_returned(millis(now)); + Ok(Self { socket, record: Responder::new(host), message: vec![0; toyos_net_udp::limits::MAX_PAYLOAD] }) } 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 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 { - 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(record) = self.record.on(link, ms) { - unsent = unsent.saturating_add(send(stack, now, self.socket, group, PORT, &record)); - } + self.record.on(link, ms); 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, + Ok(None) => break, 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 (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: 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 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); + self.serve_name(now, &mut draw); Ok(()) } /// 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, 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 f219c3ce10d..823054774ee 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}; 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,76 @@ 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). +/// 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()); + 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!(said(&mut lan), [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 +118,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 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)]) } -/// A response to the group. +/// 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 +174,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 +242,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!(said(&mut lan), [], "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!(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!(said(&mut lan), [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 +324,136 @@ 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!(said(&mut lan), []); + + until(&mut lan, from + 5, Duration::from_secs(3)); + claimed(&lan, from, back); + 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 + 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))); +// 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 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); + assert!(!lan.run_until(Duration::from_secs(5), |_| false), "every deadline of five seconds"); + 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); - 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, 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.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: "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 an_announcement_due_with_the_link_down_is_counted_and_the_links_return_owes_both() { +fn a_conflicting_response_received_as_the_probing_ends_takes_the_name() { 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, 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), []); +} - 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 §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 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!(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!(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!(said(&mut lan), [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); + 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!(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 +// 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!(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 @@ -273,13 +485,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 +518,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 +540,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 +599,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 a62d57b80a6..6082031628a 100644 --- a/userland/netstack/node/tests/resolve.rs +++ b/userland/netstack/node/tests/resolve.rs @@ -898,7 +898,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/main.rs b/userland/netstack/src/main.rs index 3b63bfcae2d..b0c27ccc70f 100644 --- a/userland/netstack/src/main.rs +++ b/userland/netstack/src/main.rs @@ -1511,7 +1511,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 b88b38bcf43..f148e52bccb 100644 --- a/userland/netstack/src/mdns.rs +++ b/userland/netstack/src/mdns.rs @@ -3,10 +3,18 @@ //! 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 +//! **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. @@ -16,15 +24,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, Source, To, 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 +43,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,27 +52,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), 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. - 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(record) = self.record.on(link, now_ms) { - send(socket, &record, group); - } - while let Ok((query, meta)) = socket.recv() { + self.record.on(link, now_ms); + 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 (answer, event) = self.record.heard(message, from, now_ms); + said(self.host, event); + let Some(answer) = answer else { continue; }; let to = match answer.to { @@ -72,9 +83,14 @@ impl Responder { }; send(socket, &answer.bytes, to); } + 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); + } } - /// 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)))) } @@ -84,6 +100,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(()) => {} @@ -109,19 +134,21 @@ 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 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_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.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))); } }