diff --git a/Cargo.lock b/Cargo.lock index 8fe763bf323..0540986edef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5919,6 +5919,7 @@ version = "0.1.0" dependencies = [ "etherparse", "toyos-dhcp", + "toyos-dns", "toyos-mdns", "toyos-net-ip", "toyos-net-shard", diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index e95e20e367f..0e239de0d8a 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -17,7 +17,7 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade - Then multi-core, netring (blocked on the owner's ABI ruling), TCP and IP hardening, IPv6, offloads, soak. - This track owns the T14's outbound rows, the machine reaching its router and the internet on the I219, judged from the stick: they arrive on this stack and not on smoltcp (owner: "no smoltcp."), and until they do no T14 row reads the wired card (`issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`). -Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets and its mDNS name. Still to build on it: the resolver, streams, listeners; then the move. +Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets, its mDNS name and its resolver. Still to build on it: streams, listeners; then the move. What the node does not yet meet: @@ -30,6 +30,10 @@ What the node does not yet meet: - 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), as `toyos-mdns` records of itself. It is not announced again when the link comes back, which §8.3 asks and netd does today, where link-up resets its DHCP client and the address is new again: here the lease outlives the link. Exit: `toyos-mdns` does both, on the node's `link`. - An answer to a query from a 169.254/16 source leaves in a frame to the router: `toyos-mdns` takes a link-local source for one on the link, and [ip]'s route lookup has no way to 169.254/16 on a leased interface but the gateway, where RFC 3927 §2.6.2 sends it on the link and §7 never to a router. `an_answer_to_a_link_local_asker_leaves_by_the_router` holds the present state. Exit: [ip] sends to 169.254/16 on the link, and that test finds the answer in a frame to the asker's own link address. +- A query that does not reach its resolver is waited out for `toyos_dns::WAIT_MS` like one nobody answered, whether [udp] refused it for want of a route (`node.query-unsent`) or the network reported it back, its resolver unreachable or its port refused (`node.query-failed`): `toyos_dns::Lookup` has no word for either. Exit, before the move: `Lookup` is told and asks the next resolver at once, `a_query_udp_refuses_is_counted_holds_no_port_and_is_waited_out` finds its answer at 0 ms, and `a_query_the_network_reports_unreachable_is_counted_and_waited_out` finds its lookup ended at 3 s, [ip]'s first report of its one resolver, and not at `ROUNDS` × `WAIT_MS`. +- Lookups started together at a resolver whose link address is not yet known lose all but the newest `PENDING_PER_NEIGHBOUR` queries to [ip]'s queue for an unresolved next hop (`nb.pending-overflow`, RFC 4861 §7.2.2), and each lost one costs its lookup a `WAIT_MS`; smoltcp kept each query in its own socket until the neighbour answered. `a_burst_at_a_resolver_not_yet_resolved_leaves_ips_queue_of_queries_and_the_rest_a_wait_later` holds it as it stands. It is a regression against what ships and would ship at the move. Exit, before the move: that test finds every query of the burst on the wire once the resolver answers ARP. +- `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. +- No test of `toyos-net-ip` holds that a next hop which never answers ARP holds up no datagram to another, which the node's resolver rests on when the lease's first resolver is down (`issues/netstack-resolver-asks-a-dead-primary-first-on-every-lookup.md`, its second cost). Exit, before the move: a scenario in `toyos-net-ip/tests/nud.rs` sends to a silent neighbour and to an answering one and finds the second datagram on the wire at once. - `node.address-refused` has no test: `toyos-dhcp` accepts no address or prefix [ip] refuses, by the same `toyos-net-wire` checks in both, so the refusal cannot be reached from the wire. Exit: the client hands [ip] a type that carries the check, and the counter goes. The listener defects are this track's: `issues/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept. diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs index 288232af7ae..1f46cdaa91e 100644 --- a/toyos-dns/src/lib.rs +++ b/toyos-dns/src/lib.rs @@ -2,7 +2,7 @@ //! IPv4 addresses, which datagram answers that question, what the answer says, //! and when the question is asked again. Pure: `core` and `alloc`, no //! `unsafe`, no I/O, and no clock or randomness of its own. The caller hands in -//! the time, every query ID and every datagram with its source. +//! the time, the draw of every query's ID and every datagram with its source. //! //! **A reply has crossed a trust boundary.** Anyone on the path can send a //! datagram to the port a query left from, so a reply is read only if it @@ -471,8 +471,10 @@ struct Sent { /// One name's lookup, from its first query to its answer. /// -/// **Each query carries an ID the caller drew for it**, which the caller takes -/// from a source an off-path sender cannot predict (RFC 5452 §9.2). A reply to +/// **Each query carries an ID the caller draws for it**, from a source an +/// off-path sender cannot predict (RFC 5452 §9.2). Every call that may send a +/// query takes the draw, and calls it once for a query it sends and not at all +/// otherwise. A reply to /// any query this lookup sent for the name now asked is read, so an answer /// late past its query's wait still ends the lookup; it is read only on the /// query's own port, with the query's ID, from the server the query went to. @@ -499,8 +501,8 @@ pub struct Lookup { impl Lookup { /// A lookup of `name` through `servers`, and its first query, sent at - /// `now` with ID `id`. `None` when there is no server to ask. - pub fn start(name: Name, servers: &[[u8; 4]], now: u64, id: u16) -> Option<(Self, Step)> { + /// `now` with the ID `id` draws. `None` when there is no server to ask. + pub fn start(name: Name, servers: &[[u8; 4]], now: u64, id: impl FnOnce() -> u16) -> Option<(Self, Step)> { if servers.is_empty() { return None; } @@ -530,7 +532,7 @@ impl Lookup { } /// The next query for `name`, or the end where every one has been sent. - fn ask(&mut self, now: u64, id: u16) -> Step { + fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step { if self.asked == ROUNDS * self.servers.len() { return Step::Done(Err(match self.failed { Some(rcode) => Failure::ServerFailed(rcode), @@ -541,14 +543,15 @@ impl Lookup { self.asked += 1; let asked = Asked(self.next); self.next += 1; + let id = id(); self.sent.push(Sent { asked, id, to }); self.due = now + WAIT_MS; Step::Ask { asked, to, query: query(id, &self.name) } } /// The time is `now`: the newest query has had its [`WAIT_MS`] where it - /// is due. `id` is for the query this may send. - pub fn on_time(&mut self, now: u64, id: u16) -> Step { + /// is due. `id` draws the ID of the query this sends. + pub fn on_time(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step { if now < self.due { return Step::Wait; } @@ -556,8 +559,16 @@ impl Lookup { } /// `msg` arrived at `now` from `port` of `from`, on the port `asked` was - /// sent from. `id` is for the query this may send. - pub fn on_datagram(&mut self, asked: Asked, from: [u8; 4], port: u16, msg: &[u8], now: u64, id: u16) -> Step { + /// sent from. `id` draws the ID of the query this sends. + pub fn on_datagram( + &mut self, + asked: Asked, + from: [u8; 4], + port: u16, + msg: &[u8], + now: u64, + id: impl FnOnce() -> u16, + ) -> Step { let Some(carried) = msg.get(..2).map(|b| u16::from_be_bytes([b[0], b[1]])) else { return Step::Wait; }; diff --git a/toyos-dns/src/tests.rs b/toyos-dns/src/tests.rs index 6977135d0fc..9dd5631c916 100644 --- a/toyos-dns/src/tests.rs +++ b/toyos-dns/src/tests.rs @@ -645,14 +645,14 @@ fn waiting(lookup: &Lookup) -> Vec { #[test] fn rfc1035_4_2_1_every_server_is_asked_before_any_again_and_the_lookup_ends_after_its_rounds() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, 100).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 100).unwrap(); assert_eq!(to(&first), (S1, 100)); assert_eq!(lookup.due(), WAIT_MS); - assert_eq!(lookup.on_time(WAIT_MS - 1, 999), Step::Wait, "before its wait is up"); + assert_eq!(lookup.on_time(WAIT_MS - 1, || 999), Step::Wait, "before its wait is up"); let mut now = WAIT_MS; let mut sent = vec![asked(&first)]; loop { - match lookup.on_time(now, 101 + sent.len() as u16) { + match lookup.on_time(now, || 101 + sent.len() as u16) { Step::Done(result) => { assert_eq!(result, Err(Failure::TimedOut)); break; @@ -671,72 +671,72 @@ fn rfc1035_4_2_1_every_server_is_asked_before_any_again_and_the_lookup_ends_afte #[test] fn rfc5452_9_1_only_the_asked_server_port_and_id_are_read() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, 0x4242).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 0x4242).unwrap(); let (q, ..) = asked(&first); assert_eq!(to(&first), (S1, 0x4242)); let good = reply(0x4242, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]); - assert_eq!(lookup.on_datagram(q, S2, PORT, &good, 5, 1), Step::Wait, "another server"); - assert_eq!(lookup.on_datagram(q, S1, 5353, &good, 5, 1), Step::Wait, "another port"); - assert_eq!(lookup.on_datagram(Asked(1), S1, PORT, &good, 5, 1), Step::Wait, "another query's port"); + assert_eq!(lookup.on_datagram(q, S2, PORT, &good, 5, || 1), Step::Wait, "another server"); + assert_eq!(lookup.on_datagram(q, S1, 5353, &good, 5, || 1), Step::Wait, "another port"); + assert_eq!(lookup.on_datagram(Asked(1), S1, PORT, &good, 5, || 1), Step::Wait, "another query's port"); let forged = reply(0x4243, OK, "www.example", &[a("www.example", [6, 6, 6, 6])]); - assert_eq!(lookup.on_datagram(q, S1, PORT, &forged, 5, 1), Step::Wait, "another ID"); - assert_eq!(lookup.on_datagram(q, S1, PORT, &[0x42], 5, 1), Step::Wait, "a byte"); + assert_eq!(lookup.on_datagram(q, S1, PORT, &forged, 5, || 1), Step::Wait, "another ID"); + assert_eq!(lookup.on_datagram(q, S1, PORT, &[0x42], 5, || 1), Step::Wait, "a byte"); let elsewhere = reply(0x4242, OK, "bank.example", &[a("bank.example", [6, 6, 6, 6])]); - assert_eq!(lookup.on_datagram(q, S1, PORT, &elsewhere, 5, 1), Step::Wait, "another question"); - assert_eq!(lookup.on_datagram(q, S1, PORT, &good, 5, 1), Step::Done(Ok(vec![[1, 2, 3, 4]]))); + assert_eq!(lookup.on_datagram(q, S1, PORT, &elsewhere, 5, || 1), Step::Wait, "another question"); + assert_eq!(lookup.on_datagram(q, S1, PORT, &good, 5, || 1), Step::Done(Ok(vec![[1, 2, 3, 4]]))); } #[test] fn a_late_answer_to_an_earlier_query_still_ends_the_lookup() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, 1).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap(); let (q1, ..) = asked(&first); - let second = lookup.on_time(WAIT_MS, 2); + let second = lookup.on_time(WAIT_MS, || 2); assert_eq!(to(&second), (S2, 2)); let (q2, ..) = asked(&second); assert_eq!(waiting(&lookup), [q1, q2], "the first query is still answered"); let late = reply(1, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]); - assert_eq!(lookup.on_datagram(q1, S2, PORT, &late, WAIT_MS + 1, 3), Step::Wait, "ID 1 went to the first server"); - assert_eq!(lookup.on_datagram(q2, S1, PORT, &late, WAIT_MS + 1, 3), Step::Wait, "ID 1 left from the first query's port"); - assert_eq!(lookup.on_datagram(q1, S1, PORT, &late, WAIT_MS + 1, 3), Step::Done(Ok(vec![[1, 2, 3, 4]]))); + assert_eq!(lookup.on_datagram(q1, S2, PORT, &late, WAIT_MS + 1, || 3), Step::Wait, "ID 1 went to the first server"); + assert_eq!(lookup.on_datagram(q2, S1, PORT, &late, WAIT_MS + 1, || 3), Step::Wait, "ID 1 left from the first query's port"); + assert_eq!(lookup.on_datagram(q1, S1, PORT, &late, WAIT_MS + 1, || 3), Step::Done(Ok(vec![[1, 2, 3, 4]]))); } #[test] fn a_server_failure_asks_the_next_server_at_once() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, 1).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap(); let (q1, ..) = asked(&first); let servfail = reply(1, OK | 2, "www.example", &[]); - let second = lookup.on_datagram(q1, S1, PORT, &servfail, 10, 2); + let second = lookup.on_datagram(q1, S1, PORT, &servfail, 10, || 2); assert_eq!(to(&second), (S2, 2)); let (q2, ..) = asked(&second); assert_eq!(waiting(&lookup), [q2], "the failed query is answered no more"); assert_eq!(lookup.due(), 10 + WAIT_MS); // The same failure again is no longer a query of this lookup's. - assert_eq!(lookup.on_datagram(q1, S1, PORT, &servfail, 11, 3), Step::Wait); + assert_eq!(lookup.on_datagram(q1, S1, PORT, &servfail, 11, || 3), Step::Wait); // An older query's failure while the newest still waits asks nobody. - let third = lookup.on_time(10 + WAIT_MS, 3); + let third = lookup.on_time(10 + WAIT_MS, || 3); assert_eq!(to(&third), (S1, 3)); let (q3, ..) = asked(&third); let old = reply(2, OK | 5, "www.example", &[]); - assert_eq!(lookup.on_datagram(q2, S2, PORT, &old, 10 + WAIT_MS + 1, 4), Step::Wait); + assert_eq!(lookup.on_datagram(q2, S2, PORT, &old, 10 + WAIT_MS + 1, || 4), Step::Wait); assert_eq!(waiting(&lookup), [q3]); let ok = reply(3, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]); - assert_eq!(lookup.on_datagram(q3, S1, PORT, &ok, 10 + WAIT_MS + 2, 4), Step::Done(Ok(vec![[1, 2, 3, 4]]))); + assert_eq!(lookup.on_datagram(q3, S1, PORT, &ok, 10 + WAIT_MS + 2, || 4), Step::Done(Ok(vec![[1, 2, 3, 4]]))); } #[test] fn a_lookup_whose_every_server_failed_says_how() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, 0).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 0).unwrap(); let (mut q, _, mut id) = asked(&first); let mut now = 0; for _ in 0..ROUNDS - 1 { - let step = lookup.on_datagram(q, S1, PORT, &reply(id, OK | 2, "www.example", &[]), now, id + 1); + let step = lookup.on_datagram(q, S1, PORT, &reply(id, OK | 2, "www.example", &[]), now, || id + 1); assert_eq!(to(&step), (S1, id + 1)); q = asked(&step).0; id += 1; now += 1; } assert_eq!( - lookup.on_datagram(q, S1, PORT, &reply(id, OK | 2, "www.example", &[]), now, id + 1), + lookup.on_datagram(q, S1, PORT, &reply(id, OK | 2, "www.example", &[]), now, || id + 1), Step::Done(Err(Failure::ServerFailed(2))) ); } @@ -747,16 +747,16 @@ fn a_lookup_whose_every_server_failed_says_how() { /// timed out. #[test] fn a_server_failure_before_an_alias_is_not_the_aliases_failure() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, 1).unwrap(); - let second = lookup.on_datagram(asked(&first).0, S1, PORT, &reply(1, OK | 2, "www.example", &[]), 10, 2); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap(); + let second = lookup.on_datagram(asked(&first).0, S1, PORT, &reply(1, OK | 2, "www.example", &[]), 10, || 2); assert_eq!(to(&second), (S2, 2)); let alias = reply(2, OK, "www.example", &[cname("www.example", "cdn.example")]); - let restart = lookup.on_datagram(asked(&second).0, S2, PORT, &alias, 20, 3); + let restart = lookup.on_datagram(asked(&second).0, S2, PORT, &alias, 20, || 3); assert_eq!(to(&restart), (S1, 3), "the alias is asked afresh"); let mut now = 20; let ended = loop { now += WAIT_MS; - match lookup.on_time(now, 4) { + match lookup.on_time(now, || 4) { Step::Done(result) => break result, step => { asked(&step); @@ -768,58 +768,88 @@ fn a_server_failure_before_an_alias_is_not_the_aliases_failure() { #[test] fn a_truncated_reply_ends_the_lookup_by_name() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, 9).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 9).unwrap(); let cut = reply(9, OK | TC, "www.example", &[a("www.example", [1, 2, 3, 4])]); - assert_eq!(lookup.on_datagram(asked(&first).0, S1, PORT, &cut, 1, 10), Step::Done(Err(Failure::Truncated))); + assert_eq!(lookup.on_datagram(asked(&first).0, S1, PORT, &cut, 1, || 10), Step::Done(Err(Failure::Truncated))); } #[test] fn rfc2308_nxdomain_and_nodata_end_the_lookup_apart() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, 9).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 9).unwrap(); assert_eq!( - lookup.on_datagram(asked(&first).0, S1, PORT, &reply(9, OK | 3, "www.example", &[]), 1, 10), + lookup.on_datagram(asked(&first).0, S1, PORT, &reply(9, OK | 3, "www.example", &[]), 1, || 10), Step::Done(Err(Failure::NoSuchName)) ); - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, 9).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 9).unwrap(); assert_eq!( - lookup.on_datagram(asked(&first).0, S1, PORT, &reply(9, OK, "www.example", &[]), 1, 10), + lookup.on_datagram(asked(&first).0, S1, PORT, &reply(9, OK, "www.example", &[]), 1, || 10), Step::Done(Err(Failure::NoAddress)) ); } #[test] fn rfc1034_5_3_3_a_chain_that_ends_without_an_address_is_asked_again_at_its_end() { - let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, 1).unwrap(); + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap(); let (q1, ..) = asked(&first); let alias = reply(1, OK, "www.example", &[cname("www.example", "cdn.example")]); - let step = lookup.on_datagram(q1, S1, PORT, &alias, 50, 2); + let step = lookup.on_datagram(q1, S1, PORT, &alias, 50, || 2); let (q2, ..) = asked(&step); assert_eq!(step, Step::Ask { asked: q2, to: S1, query: query(2, &name("cdn.example")) }, "the first server, afresh"); assert_eq!(waiting(&lookup), [q2], "no query for the old name is answered any more"); assert_eq!(lookup.due(), 50 + WAIT_MS); - assert_eq!(lookup.on_datagram(q1, S1, PORT, &alias, 51, 3), Step::Wait, "the old name's query is over"); + assert_eq!(lookup.on_datagram(q1, S1, PORT, &alias, 51, || 3), Step::Wait, "the old name's query is over"); let done = reply(2, OK, "cdn.example", &[a("cdn.example", [4, 3, 2, 1])]); - assert_eq!(lookup.on_datagram(q2, S1, PORT, &done, 52, 3), Step::Done(Ok(vec![[4, 3, 2, 1]]))); + assert_eq!(lookup.on_datagram(q2, S1, PORT, &done, 52, || 3), Step::Done(Ok(vec![[4, 3, 2, 1]]))); } #[test] fn aliases_are_counted_across_the_names_a_lookup_asks() { - let (mut lookup, first) = Lookup::start(name("h0.example"), &[S1], 0, 0).unwrap(); + let (mut lookup, first) = Lookup::start(name("h0.example"), &[S1], 0, || 0).unwrap(); let (mut q, _, mut id) = asked(&first); for i in 0..MAX_ALIASES { let from = std::format!("h{i}.example"); let to_name = std::format!("h{}.example", i + 1); - let step = lookup.on_datagram(q, S1, PORT, &reply(id, OK, &from, &[cname(&from, &to_name)]), 0, id + 1); + let step = lookup.on_datagram(q, S1, PORT, &reply(id, OK, &from, &[cname(&from, &to_name)]), 0, || id + 1); assert_eq!(to(&step), (S1, id + 1), "alias {i}"); q = asked(&step).0; id += 1; } let last = std::format!("h{MAX_ALIASES}.example"); let one_more = reply(id, OK, &last, &[cname(&last, "end.example")]); - assert_eq!(lookup.on_datagram(q, S1, PORT, &one_more, 0, id + 1), Step::Done(Err(Failure::TooManyAliases))); + assert_eq!(lookup.on_datagram(q, S1, PORT, &one_more, 0, || id + 1), Step::Done(Err(Failure::TooManyAliases))); } #[test] fn a_lookup_with_no_server_does_not_start() { - assert!(Lookup::start(name("www.example"), &[], 0, 0).is_none()); + assert!(Lookup::start(name("www.example"), &[], 0, || 0).is_none()); +} + +/// A draw that must not be made. +fn undrawn() -> u16 { + panic!("no query is sent, so no ID is drawn") +} + +#[test] +fn rfc5452_9_2_an_id_is_drawn_for_a_query_that_is_sent_and_for_nothing_else() { + let mut drawn = 0u16; + let mut draw = || { + drawn += 1; + drawn + }; + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, &mut draw).unwrap(); + let (q, _, id) = asked(&first); + assert_eq!(id, 1, "the first query carries the first draw"); + assert_eq!(lookup.on_time(WAIT_MS - 1, undrawn), Step::Wait, "a wait that is not over"); + assert_eq!(lookup.on_datagram(q, S2, PORT, &reply(1, OK, "www.example", &[]), 5, undrawn), Step::Wait, "a stray"); + for round in 1..ROUNDS { + let step = lookup.on_time(round as u64 * WAIT_MS, &mut draw); + assert_eq!(asked(&step).2, round as u16 + 1, "each later query carries the next draw"); + } + assert_eq!(lookup.on_time(ROUNDS as u64 * WAIT_MS, undrawn), Step::Done(Err(Failure::TimedOut)), "the end sends nothing"); + assert_eq!(drawn, ROUNDS as u16, "one draw a query"); + + let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 7).unwrap(); + let answer = reply(7, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]); + assert_eq!(lookup.on_datagram(asked(&first).0, S1, PORT, &answer, 1, undrawn), Step::Done(Ok(vec![[1, 2, 3, 4]])), "an answer"); + assert!(Lookup::start(name("www.example"), &[], 0, undrawn).is_none(), "a lookup that does not start"); } diff --git a/userland/netstack/node/Cargo.toml b/userland/netstack/node/Cargo.toml index ca6ae5068a2..b2e5663d211 100644 --- a/userland/netstack/node/Cargo.toml +++ b/userland/netstack/node/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] toyos-dhcp = { path = "../../../toyos-dhcp" } +toyos-dns = { path = "../../../toyos-dns" } toyos-mdns = { path = "../mdns" } toyos-net-ip = { path = "../../../toyos-net-ip" } toyos-net-shard = { path = "../../../toyos-net-shard" } diff --git a/userland/netstack/node/src/lease.rs b/userland/netstack/node/src/lease.rs index 06e5450cb9b..625ad8624bc 100644 --- a/userland/netstack/node/src/lease.rs +++ b/userland/netstack/node/src/lease.rs @@ -140,6 +140,12 @@ impl Stack { Ok((id, self.shard.udp_port(id)?)) } + /// Connects a socket to one peer: from here [udp] delivers it that peer's datagrams for the + /// address the socket then sends from, and no other. + pub(crate) fn connect(&mut self, now: Instant, id: SocketId, peer: Ipv4Addr, port: u16) -> Result<(), toyos_net_udp::Error> { + self.shard.udp_connect(now, id, peer, port) + } + /// Queues a datagram; accepted means queued, not sent. pub(crate) fn send_to(&mut self, now: Instant, id: SocketId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), toyos_net_udp::Error> { self.shard.send_to(now, id, destination, port, payload) diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs index 23338f8e92f..ae7ef358aad 100644 --- a/userland/netstack/node/src/lib.rs +++ b/userland/netstack/node/src/lib.rs @@ -8,8 +8,9 @@ //! interface holds is `lease`'s to write and nobody else's: see that module for the three rules //! its types keep. //! -//! A client's datagram sockets are `datagram`'s and the machine's `.local` name is `name`'s: -//! both read the lease and write none of it. +//! A client's datagram sockets are `datagram`'s, the machine's `.local` name is `name`'s +//! and the lookups of other machines' names are `resolve`'s: each reads the lease and writes none +//! of it. //! //! **Untrusted input.** A received frame is never read here: every byte goes through //! `toyos-net-wire`'s parsers inside the shard, and a DHCP payload through the client's. What @@ -17,7 +18,8 @@ //! [`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. +//! exchange draws its transaction id first. The lookups in flight draw after it: an id and then a +//! port for each query they send, and nothing else. #![no_std] #![forbid(unsafe_code)] @@ -35,6 +37,7 @@ extern crate alloc; mod datagram; mod lease; mod name; +mod resolve; use alloc::vec; use alloc::vec::Vec; @@ -45,12 +48,15 @@ use toyos_net_wire::Instant; pub use datagram::{Datagram, DatagramId, Refused}; use lease::{Report, Stack, Verified}; +pub use resolve::{Ended, LookupId, NotStarted, Resolved}; toyos_net_wire::counters! { DhcpUnsent = "node.dhcp-unsent"; AddressRefused = "node.address-refused"; RouterRefused = "node.router-refused"; NameUnsent = "node.name-unsent"; + QueryUnsent = "node.query-unsent"; + QueryFailed = "node.query-failed"; } /// A line for the log. @@ -66,6 +72,7 @@ pub struct Node { client: Client, /// The responder for the machine's name, once [`Self::answer_as`] started it. name: Option, + resolver: resolve::Resolver, counters: Counters, events: Vec, /// Room for the largest message the client accepts. @@ -83,6 +90,7 @@ impl Node { stack, client, name: None, + resolver: resolve::Resolver::new(), counters: Counters::default(), events: Vec::new(), datagram: vec![0; usize::from(toyos_dhcp::limits::MAX_MESSAGE)], @@ -141,7 +149,7 @@ impl Node { pub fn next_deadline(&self) -> Option { let name = self.name.as_ref().and_then(name::Name::next_deadline); - self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).min() + self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).chain(self.resolver.next_deadline()).min() } /// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`]. @@ -154,8 +162,8 @@ impl Node { } /// Hands the client what the shard reported and what reached its socket, and carries out - /// what it answers, until neither has more; then the name is served, against the lease as - /// that left it. + /// what it answers, until neither has more; then the name is served and the lookups are + /// carried on, against the lease as that left it. fn settle(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) { loop { let (out, verified) = if let Some(report) = self.stack.report() { @@ -176,6 +184,7 @@ impl Node { self.carry_out(now, out, verified, draw); } self.serve_name(now); + self.resolver.pass(now, &mut self.stack, &mut self.counters, draw); } /// What one call of the client's asked for: the lease first, so a message leaves from the diff --git a/userland/netstack/node/src/name.rs b/userland/netstack/node/src/name.rs index 20c1632431b..0591302ce86 100644 --- a/userland/netstack/node/src/name.rs +++ b/userland/netstack/node/src/name.rs @@ -36,8 +36,8 @@ pub(crate) struct Name { query: Vec, } -/// `now` on the responder's clock: whole milliseconds of the node's. -fn millis(now: Instant) -> u64 { +/// `now` on the clock `toyos-mdns` and `toyos-dns` keep: whole milliseconds of the node's. +pub(crate) fn millis(now: Instant) -> u64 { u64::try_from(now.since(Instant::from_millis(0)).as_millis()).unwrap_or(u64::MAX) } diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs new file mode 100644 index 00000000000..d8a5cfc23dd --- /dev/null +++ b/userland/netstack/node/src/resolve.rs @@ -0,0 +1,251 @@ +//! The machine's one resolver: a name's IPv4 addresses, asked of the resolvers the held lease +//! names. Every decision about a question and every byte of a reply is `toyos_dns::Lookup`'s; +//! this is its sockets on [udp], its clock and its draws. +//! +//! - **Each query leaves from a socket of its own, connected to its resolver's port 53**, on an +//! ephemeral port [udp] picks from one draw, and carries an id that is another draw (RFC 5452 +//! §9.2): an off-path sender has to guess both to be read at all. [udp] delivers a connected +//! socket only what its peer's address and port sent to the address the socket sends from +//! (§9.1), so no reply is read from another source, at a broadcast or group address, or for a +//! query other than the one whose socket it reached. +//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets +//! the query go, ends, or is let go itself, and its port is free from then. +//! - **A query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits +//! its wait out as for any query nobody answered. So does one the network reports back, its +//! resolver unreachable or its port refused: counted, and waited out. +//! - **A lookup asks the resolvers of the lease it started under, and ends with that lease's +//! word**: when the held lease names other resolvers, or none is held. +//! - **A wait is a deadline of the node's** ([`Resolver::next_deadline`]); a reply is a frame. +//! - **At most `toyos_dns::MAX_LOOKUPS` lookups are held**, an ended one until +//! [`Node::take_resolved`] hands its answer over. +//! +//! **Untrusted input.** A reply's bytes are read by the lookup's reader alone, cut to the largest +//! datagram [udp] delivers. + +use alloc::vec; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_dns::{Asked, Failure, Lookup, Name, Step, MAX_LOOKUPS, PORT}; +use toyos_net_udp::{Error, SocketId}; +use toyos_net_wire::Instant; + +use crate::lease::Stack; +use crate::name::millis; +use crate::{Counter, Counters, Node}; + +/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LookupId(u64); + +/// Why a lookup was not started, in the pipe ABI's words (`toyos::net`'s `ERR_*`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NotStarted { + /// No lease is held, or the held one names no resolver. + NotConnected, + /// `toyos_dns::MAX_LOOKUPS` are held, or no ephemeral port is free: the same call succeeds + /// later. + ResourceExhausted, +} + +/// How a lookup ended without an address. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ended { + /// What the resolvers' answers, or their silence, came to. + Failed(Failure), + /// Every ephemeral port is held, so the next query had none to leave from. + NoPort, + /// The lease the lookup started under went, or names other resolvers now. + LeaseChanged, +} + +/// A lookup that ended: the name's addresses, at least one, or why none. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Resolved { + pub id: LookupId, + pub result: Result, Ended>, +} + +/// One query whose answer is still read. +#[derive(Clone, Copy)] +struct Query { + asked: Asked, + socket: SocketId, + /// The resolver the socket is connected to. + to: [u8; 4], +} + +struct Asking { + id: LookupId, + lookup: Lookup, + /// The resolvers of the lease the lookup started under. + resolvers: Vec, + queries: Vec, +} + +pub(crate) struct Resolver { + asking: Vec, + /// Ended, and not yet taken. + ended: Vec, + /// The id the next lookup gets. + next: u64, + /// Room for the largest datagram [udp] delivers. + reply: Vec, +} + +/// A query's id: the low half of one draw. +fn id(draw: &mut impl FnMut() -> u32) -> u16 { + let [_, _, high, low] = draw().to_be_bytes(); + u16::from_be_bytes([high, low]) +} + +fn close(stack: &mut Stack, now: Instant, socket: SocketId) { + if stack.close(now, socket).is_err() { + unreachable!("a query's socket is closed once, by the lookup that bound it"); + } +} + +/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the +/// query it is for. An error the network reported against a query is counted and read past. +fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> { + for query in queries { + loop { + match stack.recv_from(query.socket, reply) { + Ok(Some(received)) => return Some((*query, received.len)), + Ok(None) => break, + Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1), + Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"), + } + } + } + None +} + +/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer +/// reads an answer for. Returns how the lookup ended, if it did. +fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option, Ended>> { + let done = match step { + // An any-address bind that names no port is refused only for want of a free one. + Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) { + Ok((socket, _)) => { + let resolver = Ipv4Addr::from(to); + if stack.connect(now, socket, resolver, PORT).is_ok() && stack.send_to(now, socket, resolver, PORT, &query).is_ok() { + asking.queries.push(Query { asked, socket, to }); + } else { + counters.add(Counter::QueryUnsent, 1); + close(stack, now, socket); + } + None + } + Err(_) => Some(Err(Ended::NoPort)), + }, + Step::Wait => None, + Step::Done(result) => Some(result.map_err(Ended::Failed)), + }; + let lookup = &asking.lookup; + asking.queries.retain(|query| { + let read = lookup.waiting().any(|waiting| waiting == query.asked); + if !read { + close(stack, now, query.socket); + } + read + }); + done +} + +impl Resolver { + pub(crate) fn new() -> Self { + Self { asking: Vec::new(), ended: Vec::new(), next: 0, reply: vec![0; toyos_net_udp::limits::MAX_PAYLOAD] } + } + + /// When the soonest lookup's wait is over. + pub(crate) fn next_deadline(&self) -> Option { + self.asking.iter().map(|asking| Instant::from_millis(asking.lookup.due())).min() + } + + fn start(&mut self, now: Instant, name: Name, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Result { + let resolvers = stack.lease().map(|lease| lease.dns.clone()).unwrap_or_default(); + if resolvers.is_empty() { + return Err(NotStarted::NotConnected); + } + if self.asking.len().saturating_add(self.ended.len()) >= MAX_LOOKUPS { + return Err(NotStarted::ResourceExhausted); + } + let servers: Vec<[u8; 4]> = resolvers.iter().map(Ipv4Addr::octets).collect(); + let Some((lookup, step)) = Lookup::start(name, &servers, millis(now), || id(&mut *draw)) else { unreachable!("the lease names a resolver") }; + let mut asking = Asking { id: LookupId(self.next), lookup, resolvers, queries: Vec::new() }; + match act(&mut asking, step, now, stack, counters, &mut *draw) { + None => { + self.next = self.next.wrapping_add(1); + let started = asking.id; + self.asking.push(asking); + Ok(started) + } + Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted), + Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"), + } + } + + /// Ends every lookup whose lease went or names other resolvers, reads every reply that + /// reached a query's socket, and ends every wait that is over. A lookup that ended closes + /// its sockets and waits to be taken. + pub(crate) fn pass(&mut self, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) { + let Self { asking: lookups, ended, reply, .. } = self; + let ms = millis(now); + lookups.retain_mut(|asking| { + let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers); + let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) }; + while done.is_none() { + let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break }; + let message = reply.get(..len).unwrap_or_default(); + // The socket is connected: [udp] delivered this from port 53 of the resolver the + // query went to, or not at all. + let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw)); + done = act(asking, step, now, stack, counters, &mut *draw); + } + if done.is_none() { + let step = asking.lookup.on_time(ms, || id(&mut *draw)); + done = act(asking, step, now, stack, counters, &mut *draw); + } + let Some(result) = done else { return true }; + for query in &asking.queries { + close(stack, now, query.socket); + } + ended.push(Resolved { id: asking.id, result }); + false + }); + } + + fn let_go(&mut self, now: Instant, id: LookupId, stack: &mut Stack) { + self.asking.retain(|asking| { + if asking.id != id { + return true; + } + for query in &asking.queries { + close(stack, now, query.socket); + } + false + }); + self.ended.retain(|resolved| resolved.id != id); + } +} + +impl Node { + /// Starts looking `name` up at the resolvers the held lease names, which spends two draws: + /// the first query's id, then its port. A call refused for want of a port has spent both; + /// any other refused call spends none. + pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result { + self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw) + } + + /// The lookups that ended since the last call, each once. + pub fn take_resolved(&mut self) -> Vec { + core::mem::take(&mut self.resolver.ended) + } + + /// Nobody waits for `id`'s answer any more: its sockets close and its place is another's at + /// once, and an answer not yet taken goes with it. + pub fn let_go(&mut self, now: Instant, id: LookupId) { + self.resolver.let_go(now, id, &mut self.stack); + } +} diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs new file mode 100644 index 00000000000..01fbfc20871 --- /dev/null +++ b/userland/netstack/node/tests/resolve.rs @@ -0,0 +1,752 @@ +//! The resolver on the node, against a far end played here frame by frame: what is judged is +//! what leaves the node, not what it queued. No scenario ids: the reader's rules are +//! `toyos-dns`'s own tests; these are the node's sockets, clock and draws under it. +//! +//! What is not ours: `etherparse` reads every frame the node emits (`lan::outside`); every DNS +//! message here, a reply's and the query the node is expected to emit, is spelled by hand from +//! RFC 1035 §4.1's layouts and never by `toyos-dns`; each test names the RFC its expectation is +//! read from. + +mod common; +mod lan; + +use std::net::Ipv4Addr; +use std::time::Duration; + +use common::{arp, from_server, message, message_of, terms, xid, A, ACK, DNS, MAC, MAC_B, NAK, R}; +use lan::{udp, Lan, Seen, Udp, B}; +use toyos_dns::{Failure, Name, MAX_LOOKUPS, ROUNDS, WAIT_MS}; +use toyos_net_node::{Counter, Ended, LookupId, NotStarted, Refused, Resolved}; +use toyos_net_udp::limits::{EPHEMERAL_COUNT, EPHEMERAL_FIRST}; +use toyos_net_udp::Counter as Rule; +use toyos_net_wire::{Instant, Port}; + +const ANY: Ipv4Addr = Ipv4Addr::UNSPECIFIED; +/// On the link, answering ARP and every query its zone holds an answer for. +const ANSWERS: Ipv4Addr = DNS; +const MAC_S: [u8; 6] = [2, 0, 0, 0, 0, 0x53]; +/// On the link, and nothing there answers ARP: a LAN's resolver that is down. +const SILENT: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 52); +/// Off the link, asked of a lease that names no router. +const UNROUTED: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 53); +const ADDRESS: [u8; 4] = [203, 0, 113, 7]; +/// The address every reply that must not be read carries. +const FORGED: [u8; 4] = [203, 0, 113, 66]; +/// RFC 1035 §4.1.1: a response (QR), recursion desired and available, RCODE 0. +const RESPONSE: u16 = 0x8180; +/// RFC 1035 §3.2.2. +const TYPE_A: u16 = 1; +const TYPE_CNAME: u16 = 5; +/// No lookup has ended. +const NONE: [Resolved; 0] = []; + +fn name(text: &str) -> Name { + Name::parse(text).expect("a host name") +} + +/// The draw of a call that draws nothing. +fn undrawn() -> u32 { + panic!("this call draws nothing") +} + +/// These draws in order, and no more. +fn sequence(draws: [u32; N]) -> impl FnMut() -> u32 { + let mut rest = draws.into_iter(); + move || rest.next().expect("the call draws no more than it was given") +} + +fn counter(draws: &mut u32) -> impl FnMut() -> u32 + '_ { + move || { + *draws += 1; + *draws + } +} + +/// RFC 1035 §3.1: each label behind its length octet, then the root's zero. +fn labels(name: &str) -> Vec { + let mut out = Vec::new(); + for label in name.split('.') { + out.push(u8::try_from(label.len()).unwrap()); + out.extend_from_slice(label.as_bytes()); + } + out.push(0); + out +} + +/// RFC 1035 §4.1.1 and §4.1.2: a standard query, recursion desired, for `name`'s A records in IN. +fn question(id: u16, name: &str) -> Vec { + let mut out = id.to_be_bytes().to_vec(); + out.extend_from_slice(&[0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0]); + out.extend_from_slice(&labels(name)); + out.extend_from_slice(&[0, 1, 0, 1]); + out +} + +/// The dotted name a query asks, read by hand from octet 12 on. +fn asked_name(query: &[u8]) -> String { + let mut found = Vec::new(); + let mut at = 12; + while query[at] != 0 { + let len = usize::from(query[at]); + found.push(std::str::from_utf8(&query[at + 1..at + 1 + len]).unwrap().to_string()); + at += 1 + len; + } + found.join(".") +} + +/// A reply to `query` (RFC 1035 §4.1): §4.1.1's header with the query's id, `flags`, one question +/// and one answer a record; §4.1.2's question as the query spelled it; then §4.1.3's records, each +/// owned by a §4.1.4 pointer to the question's name at octet 12, in class IN with a TTL of 60. +fn reply(query: &[u8], flags: u16, records: &[(u16, Vec)]) -> Vec { + let mut out = query[..2].to_vec(); + out.extend_from_slice(&flags.to_be_bytes()); + out.extend_from_slice(&[0, 1]); + out.extend_from_slice(&u16::try_from(records.len()).unwrap().to_be_bytes()); + out.extend_from_slice(&[0, 0, 0, 0]); + out.extend_from_slice(&query[12..]); + for (rtype, data) in records { + out.extend_from_slice(&[0xc0, 12]); + out.extend_from_slice(&rtype.to_be_bytes()); + out.extend_from_slice(&[0, 1]); + out.extend_from_slice(&60u32.to_be_bytes()); + out.extend_from_slice(&u16::try_from(data.len()).unwrap().to_be_bytes()); + out.extend_from_slice(data); + } + out +} + +/// A reply to `query` that gives `address`. +fn gives(query: &[u8], address: [u8; 4]) -> Vec { + reply(query, RESPONSE, &[(TYPE_A, address.to_vec())]) +} + +/// What [`ANSWERS`] says for a name. +enum Says { + Address([u8; 4]), + /// A CNAME to this name and nothing else, which the resolver asks again at its end. + Alias(&'static str), +} + +/// A query that reached the wire. +#[derive(Clone, Debug)] +struct Query { + at: Instant, + udp: Udp, + id: u16, + name: String, +} + +struct Net { + lan: Lan, + /// When the lease was held: the origin of every time a test names. + born: Instant, + /// How many of `lan.sent` the far end has seen. + seen: usize, + /// [`ANSWERS`]'s zone: a name, the time before which its answer is held back, and what it + /// says. + zone: Vec<(&'static str, u64, Says)>, + /// Frames [`ANSWERS`] has sent and the wire has not yet delivered, with the time they arrive. + held: Vec<(Instant, Vec)>, + /// Every query that left, in order. + queried: Vec, + ended: Vec, + /// The last draw [`Self::resolve`] handed out: its ids are 0x8000 and up, and those the node + /// draws for itself from `lan`'s count are below it. + draws: u32, +} + +impl Net { + /// The node holding a day's lease of `A`/24 that names `servers`, through `router`, at a whole + /// millisecond of its clock. + fn leased(servers: &[Ipv4Addr], router: Option) -> Self { + let mut options = terms(86_400, router); + options.retain(|(code, _)| *code != 6); + options.push((6, servers.iter().flat_map(Ipv4Addr::octets).collect())); + let mut lan = Lan::new(); + lan.lease_on(&options); + assert_eq!(lan.node.lease().expect("held").dns, servers, "the premise: the lease names these resolvers"); + // `toyos-dns` keeps whole milliseconds and conflict detection's waits are drawn finer: the + // origin is the next whole one, so a wait counted from it ends on the millisecond named. + let whole = u64::try_from(lan.now.since(Instant::from_millis(0)).as_millis()).unwrap() + 1; + lan.fire(Instant::from_millis(whole)); + Self { + born: lan.now, + seen: lan.sent.len(), + lan, + zone: Vec::new(), + held: Vec::new(), + queried: Vec::new(), + ended: Vec::new(), + draws: 0x7c00_8000, + } + } + + fn at(&self, ms: u64) -> Instant { + self.born.after(Duration::from_millis(ms)) + } + + fn ms_of(&self, at: Instant) -> u64 { + u64::try_from(at.since(self.born).as_millis()).unwrap() + } + + /// Milliseconds since the lease was held. + fn ms(&self) -> u64 { + self.ms_of(self.lan.now) + } + + fn resolve(&mut self, text: &str) -> Result { + self.lan.node.resolve(self.lan.now, name(text), counter(&mut self.draws)) + } + + /// `message` from the resolver's port 53 to the node's `to`. + fn resolver_says(&mut self, to: u16, message: &[u8]) { + self.lan.deliver(&udp(MAC, MAC_S, (ANSWERS, 53), (A, to), message)); + } + + /// Whether a socket holds `port`, by binding it: a port is one socket's. + fn port_held(&mut self, port: u16) -> bool { + match self.lan.node.udp_bind(ANY, Port::new(port), undrawn) { + Ok((id, _)) => { + self.lan.node.udp_close(self.lan.now, id).unwrap(); + false + } + Err(Refused::AddrInUse) => true, + Err(other) => panic!("binding port {port}: {other:?}"), + } + } + + /// What the far end does with one frame of the node's. + fn far_end(&mut self, at: Instant, seen: &Seen) { + match seen { + Seen::Arp { request: true, target, .. } if *target == ANSWERS => self.lan.deliver(&arp(MAC, false, MAC_S, ANSWERS, A)), + Seen::Udp(udp) if udp.port == 53 => { + assert_eq!((udp.to, udp.destination), (MAC_S, ANSWERS), "a query left for a server the wire cannot reach"); + let query = Query { at, udp: udp.clone(), id: u16::from_be_bytes([udp.payload[0], udp.payload[1]]), name: asked_name(&udp.payload) }; + if let Some((_, not_before, says)) = self.zone.iter().find(|(name, ..)| *name == query.name) { + let record = match says { + Says::Address(address) => (TYPE_A, address.to_vec()), + Says::Alias(target) => (TYPE_CNAME, labels(target)), + }; + let message = reply(&udp.payload, RESPONSE, &[record]); + let due = self.at(*not_before).max(self.lan.now); + self.held.push((due, lan::udp(MAC, MAC_S, (ANSWERS, 53), (A, udp.source_port), &message))); + } + self.queried.push(query); + } + _ => {} + } + } + + /// The frames due now delivered, every frame the node then has to send sent, and the far + /// end's turn, until both are silent. The clock does not move. + fn pass(&mut self) { + for _ in 0..10_000 { + let now = self.lan.now; + let (due, later): (Vec<_>, Vec<_>) = std::mem::take(&mut self.held).into_iter().partition(|(at, _)| *at <= now); + self.held = later; + for (_, frame) in &due { + self.lan.deliver(frame); + } + self.lan.pump(); + let fresh = self.lan.sent[self.seen..].to_vec(); + self.seen = self.lan.sent.len(); + for (at, seen) in &fresh { + self.far_end(*at, seen); + } + if due.is_empty() && fresh.is_empty() { + return; + } + } + panic!("the node and its far end never fell silent at {} ms", self.ms()); + } + + /// When the node is next woken: its own next deadline, or the next frame on the wire. + fn next_wake(&self) -> Instant { + let frame = self.held.iter().map(|(at, _)| *at).min(); + self.lan.node.next_deadline().into_iter().chain(frame).min().expect("a held lease has a deadline") + } + + /// Passes at each of the node's wakes up to `ms`, and at `ms`. + fn until(&mut self, ms: u64) { + let end = self.at(ms); + for _ in 0..10_000 { + self.pass(); + if self.lan.now >= end { + return; + } + self.lan.fire(self.next_wake().min(end)); + } + panic!("10,000 wakes and the clock stands at {} ms: a deadline that never passes", self.ms()); + } + + /// Passes at each of the node's wakes until lookup `id` has ended, or the next wake is past + /// `until_ms`. Nothing but the node's own deadline carries a lookup's wait. + fn run(&mut self, id: LookupId, until_ms: u64) -> Option, Ended>> { + let end = self.at(until_ms); + for _ in 0..10_000 { + self.pass(); + self.ended.extend(self.lan.node.take_resolved()); + if let Some(found) = self.ended.iter().position(|resolved| resolved.id == id) { + return Some(self.ended.remove(found).result); + } + let wake = self.next_wake(); + if wake > end { + return None; + } + self.lan.fire(wake); + } + panic!("10,000 wakes and the clock stands at {} ms: a deadline that never passes", self.ms()); + } +} + +// RFC 1035 §4.1.1: ID, then RD alone among the flags, QDCOUNT 1 and the other counts 0. §4.1.2: +// the name as labels, QTYPE A (1), QCLASS IN (1). §4.2.1: to the server's port 53. RFC 5452 §9.2: +// the id and the source port are each drawn. RFC 6056 §3.3.1: the port is the draw's offset into +// 49152 to 65535. +#[test] +fn a_query_is_rfc_1035s_octets_from_a_port_and_with_an_id_of_its_own_draws() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + net.lan.node.resolve(net.lan.now, name("www.example"), sequence([0xabcd_1234, 7])).expect("two draws: the id, then the port"); + net.pass(); + #[rustfmt::skip] + let payload = vec![ + 0x12, 0x34, 0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0, + 3, b'w', b'w', b'w', 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 0, + 0, 1, 0, 1, + ]; + let sent: Vec<&Udp> = net.queried.iter().map(|query| &query.udp).collect(); + assert_eq!(sent, [&Udp { to: MAC_S, source: A, source_port: 49_152 + 7, destination: ANSWERS, port: 53, ttl: 64, payload }]); +} + +// RFC 5452 §9.1: a reply is accepted only from the address the query was sent to. The query's +// socket is connected to it, so another host's datagram finds no socket (RFC 1122 §4.1.3.1). +#[test] +fn a_reply_is_read_only_from_the_resolver_its_query_went_to() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + let nobodys = net.lan.counted(Rule::RxNoSocket); + net.lan.deliver(&udp(MAC, MAC_B, (B, 53), (A, asked.udp.source_port), &gives(&asked.udp.payload, FORGED))); + assert_eq!(net.lan.node.take_resolved(), NONE, "another host's port 53 answered, with the query's id and question"); + assert_eq!(net.lan.counted(Rule::RxNoSocket), nobodys + 1, "and its datagram reached no socket"); + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// RFC 5452 §9.1: and only from the port it was sent to, 53. +#[test] +fn a_reply_is_read_only_from_port_53() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + let reached = net.lan.counted(Rule::Rx); + net.lan.deliver(&udp(MAC, MAC_S, (ANSWERS, 5_353), (A, asked.udp.source_port), &gives(&asked.udp.payload, FORGED))); + assert_eq!(net.lan.counted(Rule::Rx), reached + 1, "the premise: the datagram reached [udp]"); + assert_eq!(net.lan.node.take_resolved(), NONE, "the resolver's port 5353 answered"); + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// RFC 768: a source port of zero is no port, and so not port 53. +#[test] +fn a_reply_from_port_0_is_not_read() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + let portless = net.lan.counted(Rule::RxSrcPortZero); + net.lan.deliver(&udp(MAC, MAC_S, (ANSWERS, 0), (A, asked.udp.source_port), &gives(&asked.udp.payload, FORGED))); + assert_eq!(net.lan.counted(Rule::RxSrcPortZero), portless + 1, "the premise: the datagram reached [udp], which read it as from port 0"); + assert_eq!(net.lan.node.take_resolved(), NONE, "the resolver's port 0 answered"); + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// RFC 5452 §9.2: the port a query left from is half of what a forger must guess, so a reply is +// read for the query whose port it reached. A reply to the first query, id and all, on the second +// query's port is no reply; on its own port it is one still, late as it is. +#[test] +fn a_reply_on_another_querys_port_is_not_read() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.until(WAIT_MS); + let (first, second) = (net.queried[0].clone(), net.queried[1].clone()); + assert_eq!(net.queried.len(), 2, "the premise: a second query left when the first one's wait ended"); + assert!(first.id != second.id && first.udp.source_port != second.udp.source_port, "the premise: {first:?} {second:?}"); + net.resolver_says(second.udp.source_port, &gives(&first.udp.payload, FORGED)); + assert_eq!(net.lan.node.take_resolved(), NONE, "the first query's reply was read on the second query's port"); + net.resolver_says(first.udp.source_port, &gives(&first.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// RFC 1035 §4.1.1 (the id, QR), §4.1.2 (the question), §4.1.4 (a pointer names a prior +// occurrence): a message that is not the answer to the query ends nothing, whatever it is made +// of, and the node reads the next one. RFC 5452 §9.1: the id and the question must match. +#[test] +fn nothing_but_its_answer_ends_a_lookup() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + let forged = gives(&asked.udp.payload, FORGED); + let after_question = asked.udp.payload.len(); + + let mut hostile: Vec<(String, Vec)> = Vec::new(); + let mut another_id = forged.clone(); + another_id[1] ^= 1; + hostile.push(("another id".into(), another_id)); + hostile.push(("another question".into(), gives(&question(asked.id, "other.example"), FORGED))); + hostile.push(("a query, not a response".into(), reply(&asked.udp.payload, 0x0100, &[(TYPE_A, FORGED.to_vec())]))); + // The question's name is a pointer to itself. + let mut looped = forged[..12].to_vec(); + looped.extend_from_slice(&[0xc0, 12, 0, 1, 0, 1]); + looped.extend_from_slice(&forged[after_question..]); + hostile.push(("a question that points at itself".into(), looped)); + // The answer's owner is a pointer to itself, and one to an octet past the message's end. + for target in [after_question, 0x3fff] { + let mut pointed = forged.clone(); + pointed[after_question..after_question + 2].copy_from_slice(&(0xc000 | u16::try_from(target).unwrap()).to_be_bytes()); + hostile.push((format!("an owner that points to octet {target}"), pointed)); + } + for len in 0..forged.len() { + hostile.push((format!("the first {len} octets of an answer"), forged[..len].to_vec())); + } + // The largest datagram a frame carries: the query's id, a response, and pointers to its end. + let mut largest = forged[..12].to_vec(); + largest.resize(1_472, 0xc0); + hostile.push(("1,472 octets of pointers".into(), largest)); + + for (what, message) in &hostile { + net.resolver_says(asked.udp.source_port, message); + assert_eq!(net.lan.node.take_resolved(), NONE, "{what} ended the lookup"); + } + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }], "the answer, after all of them"); +} + +// RFC 1035 §4.2.1: a resolver that does not answer is given up on after a wait and the next is +// asked. No route leads to the first: [udp] refuses its query's socket that peer (`udp.no-route`), +// and the query is counted, holds no port, and is waited out like one nobody answered. +#[test] +fn a_query_udp_refuses_is_counted_holds_no_port_and_is_waited_out() { + let mut net = Net::leased(&[UNROUTED, ANSWERS], None); + net.zone.push(("www.example", 0, Says::Address(ADDRESS))); + let refused = net.lan.counted(Rule::NoRoute); + let id = net.lan.node.resolve(net.lan.now, name("www.example"), sequence([0x8001, 9])).expect("a lookup whose first query cannot leave"); + assert_eq!(net.lan.counted(Rule::NoRoute), refused + 1, "the premise: [udp] refused the query for want of a route"); + assert_eq!(net.lan.node.counters().get(Counter::QueryUnsent), 1); + assert!(!net.port_held(49_152 + 9), "the refused query's socket"); + let ended = net.run(id, 25_000); + assert_eq!(ended, Some(Ok(vec![ADDRESS])), "the second resolver heard {:?}", net.queried); + assert_eq!(net.ms(), WAIT_MS); + assert_eq!(net.lan.node.counters().get(Counter::QueryUnsent), 1); +} + +// RFC 1034 §5.3.3: an alias with no address is asked again at its end, every query afresh. The +// first resolver answers only after six queries have left, three of them to it, and with an alias +// alone: the queries for the old name are let go, so the two other answers to them, arriving +// with the first, find no socket (RFC 1122 §4.1.3.1), and the alias's address ends the lookup. +#[test] +fn an_alias_answered_late_restarts_the_lookup_and_lets_the_old_names_queries_go() { + let mut net = Net::leased(&[ANSWERS, SILENT], Some(R)); + net.zone.push(("www.example", 10_500, Says::Alias("cdn.example"))); + net.zone.push(("cdn.example", 0, Says::Address(ADDRESS))); + let nobodys = net.lan.counted(Rule::RxNoSocket); + let id = net.resolve("www.example").unwrap(); + let ended = net.run(id, 40_000); + assert_eq!(ended, Some(Ok(vec![ADDRESS])), "the resolver heard {:?}", net.queried); + assert_eq!(net.ms(), 10_500); + let names: Vec<&str> = net.queried.iter().map(|query| query.name.as_str()).collect(); + assert_eq!(names, ["www.example", "www.example", "www.example", "cdn.example"]); + assert_eq!(net.lan.counted(Rule::RxNoSocket), nobodys + 2, "the old name's other two answers"); +} + +// `toyos_dns::MAX_LOOKUPS` are held and one more is refused as `toyos::net`'s +// ERR_RESOURCE_EXHAUSTED; an ended lookup holds its place until its answer is taken. +#[test] +fn the_lookup_past_the_cap_is_refused_until_an_answer_is_taken() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + net.zone.push(("one.example", 1_000, Says::Address(ADDRESS))); + let first = net.resolve("one.example").unwrap(); + net.pass(); + for held in 1..MAX_LOOKUPS { + net.resolve("www.example").unwrap_or_else(|why| panic!("lookup {held} was refused: {why:?}")); + } + assert_eq!(net.lan.node.resolve(net.lan.now, name("www.example"), undrawn), Err(NotStarted::ResourceExhausted)); + net.until(1_000); + assert_eq!(net.lan.node.resolve(net.lan.now, name("www.example"), undrawn), Err(NotStarted::ResourceExhausted), "an answer nobody took holds its place"); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id: first, result: Ok(vec![ADDRESS]) }]); + net.resolve("www.example").expect("a taken answer's place is another's"); +} + +// A lookup is asked of the held lease's resolvers and of nothing else: with none to ask it is +// refused as `toyos::net`'s ERR_NOT_CONNECTED, and draws nothing. +#[test] +fn a_lookup_with_no_resolver_to_ask_is_refused_as_not_connected() { + let mut lan = Lan::new(); + assert_eq!(lan.node.resolve(lan.now, name("www.example"), undrawn), Err(NotStarted::NotConnected), "before a lease"); + let mut options = terms(3_600, Some(R)); + options.retain(|(code, _)| *code != 6); + lan.lease_on(&options); + assert_eq!(lan.node.resolve(lan.now, name("www.example"), undrawn), Err(NotStarted::NotConnected), "a lease that names no resolver"); + + let mut lan = Lan::new(); + lan.lease(600); + lan.node.resolve(lan.now, name("www.example"), sequence([1, 2])).expect("the premise: a held lease's resolver is asked"); + assert!(lan.run_until(Duration::from_secs(700), |lan| lan.node.lease().is_none()), "the lease runs out"); + assert_eq!(lan.node.resolve(lan.now, name("www.example"), undrawn), Err(NotStarted::NotConnected), "after the lease"); +} + +// A lookup nobody waits for is let go at once: the ports its queries left from are free, and its +// place takes another lookup. +#[test] +fn a_lookup_let_go_frees_its_ports_and_its_place_at_once() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let names: Vec = (0..MAX_LOOKUPS).map(|n| format!("n{n}.example")).collect(); + let mut ids = Vec::new(); + for asked in &names { + ids.push(net.resolve(asked).unwrap()); + net.pass(); + } + net.until(WAIT_MS); + assert_eq!(net.lan.node.take_resolved(), NONE, "nothing answers, so nothing has ended"); + assert_eq!(net.queried.len(), 2 * MAX_LOOKUPS, "two queries of each left, neither answered"); + let ports = |net: &Net, asked: &str| -> Vec { net.queried.iter().filter(|query| query.name == asked).map(|query| query.udp.source_port).collect() }; + let (gone, stays) = (ports(&net, &names[3]), ports(&net, &names[4])); + assert_eq!((gone.len(), stays.len()), (2, 2)); + assert_eq!(net.resolve("www.example"), Err(NotStarted::ResourceExhausted), "the premise: every place is held"); + + net.lan.node.let_go(net.lan.now, ids[3]); + for port in gone { + assert!(!net.port_held(port), "port {port} of the lookup let go"); + } + for port in stays { + assert!(net.port_held(port), "port {port} of a lookup still waited for"); + } + net.resolve("www.example").expect("its place takes another lookup"); +} + +// And an answer nobody took goes with it: the id is nobody's from then on. +#[test] +fn a_lookup_let_go_after_it_ended_takes_its_answer_with_it() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + net.zone.push(("www.example", 0, Says::Address(ADDRESS))); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let left = net.queried[0].udp.source_port; + assert!(!net.port_held(left), "the premise: the lookup was answered and ended, its port closed"); + net.lan.node.let_go(net.lan.now, id); + assert_eq!(net.lan.node.take_resolved(), NONE); +} + +// RFC 1035 §4.2.1 leaves a late answer readable: a query's port is held while its answer would +// still be read, past its own wait, and every port is free once the lookup has ended. +#[test] +fn a_query_holds_its_port_while_its_answer_is_read_and_an_ended_lookup_holds_none() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + net.until(WAIT_MS); + assert_eq!(net.queried.len(), 2); + let first = net.queried[0].udp.source_port; + assert!(net.port_held(first), "the first query's answer is still read after its wait"); + net.zone.push(("www.example", 0, Says::Address(ADDRESS))); + assert_eq!(net.run(id, 5 * WAIT_MS), Some(Ok(vec![ADDRESS])), "the third query's answer"); + assert_eq!(net.queried.len(), 3); + let left: Vec = net.queried.iter().map(|query| query.udp.source_port).collect(); + for port in left { + assert!(!net.port_held(port), "port {port} of an ended lookup"); + } +} + +// RFC 6335 §6: the dynamic ports are 49152 to 65535. With every one held a lookup is refused as +// `toyos::net`'s ERR_RESOURCE_EXHAUSTED, and one in flight ends the moment its next query has no +// port to leave from, by that name. +#[test] +fn a_query_with_no_port_to_leave_from_ends_its_lookup_by_name() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let last = u32::from(EPHEMERAL_COUNT) - 1; + for offset in 0..last { + net.lan.node.udp_bind(ANY, None, || offset).unwrap(); + } + let id = net.lan.node.resolve(net.lan.now, name("www.example"), sequence([0x8001, last])).expect("the one port left"); + assert_eq!(net.lan.node.resolve(net.lan.now, name("other.example"), sequence([0x8002, 0])), Err(NotStarted::ResourceExhausted)); + assert_eq!(net.run(id, 10 * WAIT_MS), Some(Err(Ended::NoPort))); + assert_eq!(net.ms(), WAIT_MS); + let left: Vec = net.queried.iter().map(|query| query.udp.source_port).collect(); + assert_eq!(left, [EPHEMERAL_FIRST + (EPHEMERAL_COUNT - 1)]); +} + +// RFC 4861 §7.2.2: [ip] holds a bounded queue for a next hop it is resolving, and keeps the +// newest. Lookups started together at a resolver whose link address is not yet known hand [ip] +// one query each: those past its queue never leave, and each is asked again when its wait ends. +// The track records this against the node (`issues/toyos-has-its-own-network-stack.md`). +#[test] +fn a_burst_at_a_resolver_not_yet_resolved_leaves_ips_queue_of_queries_and_the_rest_a_wait_later() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let names: Vec = (0..MAX_LOOKUPS).map(|n| format!("n{n}.example")).collect(); + for asked in &names { + net.resolve(asked).unwrap(); + } + net.pass(); + let queue = toyos_net_ip::limits::nud::PENDING_PER_NEIGHBOUR; + let mut left: Vec<&str> = net.queried.iter().map(|query| query.name.as_str()).collect(); + let mut newest: Vec<&str> = names[MAX_LOOKUPS - queue..].iter().map(String::as_str).collect(); + left.sort_unstable(); + newest.sort_unstable(); + assert_eq!(left, newest, "the newest {queue} queries left once the resolver answered ARP"); + net.until(WAIT_MS); + assert_eq!(net.queried.len(), queue + MAX_LOOKUPS, "and every lookup's second query, a wait later"); +} + +// RFC 1035 §4.2.1: each resolver is asked ROUNDS times, WAIT_MS apart. A wait is a deadline of +// the node's: a resolver that is reached and never answers is asked again the moment each wait +// ends, and the lookup ends timed out the moment its last one does, with nothing but the node's +// own next deadline to carry it there. +#[test] +fn a_resolver_that_never_answers_is_asked_at_each_waits_end() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.resolve("www.example").unwrap(); + let ended = net.run(id, 10 * WAIT_MS); + assert_eq!(ended, Some(Err(Ended::Failed(Failure::TimedOut)))); + let rounds = u64::try_from(ROUNDS).unwrap(); + assert_eq!(net.ms(), rounds * WAIT_MS, "the lookup ended late"); + let left: Vec = net.queried.iter().map(|query| net.ms_of(query.at)).collect(); + assert_eq!(left, (0..rounds).map(|round| round * WAIT_MS).collect::>()); +} + +// A lookup's wait is its own: the node's deadline is the soonest of them, so a second lookup +// started half a wait behind the first pushes neither's schedule onto the other. +#[test] +fn a_lookup_is_not_carried_by_a_later_ones_schedule() { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let first = net.resolve("www.example").unwrap(); + net.until(WAIT_MS / 2); + let second = net.resolve("other.example").unwrap(); + let rounds = u64::try_from(ROUNDS).unwrap(); + assert_eq!(net.run(first, 10 * WAIT_MS), Some(Err(Ended::Failed(Failure::TimedOut)))); + assert_eq!(net.ms(), rounds * WAIT_MS, "the first lookup waited on the second's schedule"); + assert_eq!(net.run(second, 10 * WAIT_MS), Some(Err(Ended::Failed(Failure::TimedOut)))); + assert_eq!(net.ms(), WAIT_MS / 2 + rounds * WAIT_MS, "the second lookup waited on the first's schedule"); +} + +// RFC 5452 §9.1: a reply is matched on the address it was sent to as well, which is the one its +// query left from. The limited broadcast, the subnet's broadcast and a group the node has joined +// each reach [udp] at the query's port, which is the premise, and none is the query's answer. +#[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(); + let id = net.resolve("www.example").unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + let group = (Ipv4Addr::new(224, 0, 0, 251), [0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb]); + for (destination, mac) in [(Ipv4Addr::BROADCAST, common::BROADCAST), (Ipv4Addr::new(192, 0, 2, 255), common::BROADCAST), group] { + let reached = net.lan.counted(Rule::Rx); + net.lan.deliver(&udp(mac, MAC_S, (ANSWERS, 53), (destination, asked.udp.source_port), &gives(&asked.udp.payload, FORGED))); + assert_eq!(net.lan.counted(Rule::Rx), reached + 1, "the premise: the datagram to {destination} reached [udp]"); + assert_eq!(net.lan.node.take_resolved(), NONE, "a reply sent to {destination} was read"); + } + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// `toyos-dns/src/tests.rs` holds this reply as `REAL_GOOGLE`: what a public resolver answered for +// `dns.google` to a query with id 0x1001, 60 octets, recorded and not written here. Its two +// addresses come out of the node in the reply's order. +#[test] +fn a_recorded_reply_of_a_public_resolver_answers_the_nodes_query() { + #[rustfmt::skip] + const RECORDED: [u8; 60] = [ + 0x10, 0x01, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x64, 0x6e, 0x73, 0x06, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x02, 0xcf, 0x00, 0x04, 0x08, 0x08, 0x04, 0x04, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0xcf, + 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, + ]; + let mut net = Net::leased(&[ANSWERS], Some(R)); + let id = net.lan.node.resolve(net.lan.now, name("dns.google"), sequence([0x1001, 3])).unwrap(); + net.pass(); + let asked = net.queried[0].clone(); + assert_eq!((asked.id, &asked.udp.payload[12..]), (0x1001, &RECORDED[12..28]), "the premise: the node's query carries the reply's id and question"); + net.resolver_says(asked.udp.source_port, &RECORDED); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![[8, 8, 4, 4], [8, 8, 8, 8]]) }]); +} + +// RFC 1122 §3.3.1.1 and §4.1.3.3: a datagram whose next hop never answers ARP is reported to the +// socket it left from. Each of a lookup's queries to such a resolver is counted as the network +// reported it, and waited out like one nobody answered. +#[test] +fn a_query_the_network_reports_unreachable_is_counted_and_waited_out() { + let mut net = Net::leased(&[SILENT], Some(R)); + let id = net.resolve("www.example").unwrap(); + let rounds = u64::try_from(ROUNDS).unwrap(); + assert_eq!(net.run(id, 10 * WAIT_MS), Some(Err(Ended::Failed(Failure::TimedOut)))); + assert_eq!(net.ms(), rounds * WAIT_MS); + assert!(net.queried.is_empty(), "the premise: no query reached the wire: {:?}", net.queried); + // The first two queries wait in [ip] until the neighbour fails, at three of its requests; + // the third leaves after that and is refused at once, because [ip] holds a failed neighbour + // down for longer than the second it would take to be asked again. + let refused_at_once = net.lan.node.shard().ip().counters().get(toyos_net_ip::Counter::NbFailedRefused); + assert_eq!(refused_at_once, 1, "the premise: [ip] refused one query on a failed neighbour"); + assert_eq!(net.lan.node.counters().get(Counter::QueryFailed), rounds); + assert_eq!(net.lan.node.counters().get(Counter::QueryUnsent), 0); +} + +/// A lookup in flight as the lease's first renewal leaves: the net, the lookup and the renewal's +/// transaction id. +fn renewing_under_a_lookup() -> (Net, LookupId, u32) { + let mut net = Net::leased(&[ANSWERS], Some(R)); + let sent = net.lan.udp().iter().filter(|udp| udp.port == 67).count(); + assert!(net.lan.run_until(Duration::from_secs(50_000), |lan| lan.udp().iter().filter(|udp| udp.port == 67).count() > sent), "the renewal leaves"); + let renewal = xid(&net.lan.udp().iter().rfind(|udp| udp.port == 67).expect("a renewal").payload); + net.seen = net.lan.sent.len(); + let id = net.resolve("www.example").unwrap(); + net.pass(); + assert_eq!(net.queried.len(), 1, "the premise: the lookup's first query left"); + (net, id, renewal) +} + +/// The options of a day's lease through the router that names `resolver`. +fn naming(resolver: Ipv4Addr) -> Vec<(u8, Vec)> { + let mut options = terms(86_400, Some(R)); + options.retain(|(code, _)| *code != 6); + options.push((6, resolver.octets().to_vec())); + options +} + +// The resolvers are the held lease's and come and go with it. A lookup asks the ones its lease +// named: a renewal that names the same ones leaves it be, and its answer still ends it. +#[test] +fn a_lookup_outlives_a_renewal_that_names_the_same_resolvers() { + let (mut net, id, renewal) = renewing_under_a_lookup(); + let expiry = |net: &Net| net.lan.node.lease().and_then(|lease| lease.timers).map(|timers| timers.expiry); + let before = expiry(&net); + net.lan.deliver(&from_server(MAC, A, &message_of(ACK, renewal, &naming(ANSWERS)))); + assert_ne!(expiry(&net), before, "the premise: the renewal was taken, and the lease runs from it"); + assert_eq!(net.lan.node.take_resolved(), NONE); + let asked = net.queried[0].clone(); + net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS)); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]); +} + +// A renewal that names another resolver ends the lookup by name, at once: no further query goes +// to an address the held lease does not name, its answer is not read, and its port is free. +#[test] +fn a_lookup_ends_when_its_leases_resolvers_change() { + let (mut net, id, renewal) = renewing_under_a_lookup(); + net.lan.deliver(&from_server(MAC, A, &message_of(ACK, renewal, &naming(SILENT)))); + assert_eq!(net.lan.node.lease().expect("held").dns, [SILENT], "the premise: the renewed lease names another resolver"); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Err(Ended::LeaseChanged) }]); + let left = net.queried[0].udp.source_port; + assert!(!net.port_held(left), "port {left} of the ended lookup"); +} + +// RFC 2131 §3.1: a DHCPNAK takes the lease, and the lookup under it ends by the same name. +#[test] +fn a_lookup_ends_when_its_lease_is_lost() { + let (mut net, id, renewal) = renewing_under_a_lookup(); + net.lan.deliver(&from_server(common::BROADCAST, Ipv4Addr::BROADCAST, &message(NAK, renewal, Ipv4Addr::UNSPECIFIED, &[(54, R.octets().to_vec())]))); + assert_eq!(net.lan.node.lease(), None, "the premise: the lease is gone"); + assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Err(Ended::LeaseChanged) }]); +} diff --git a/userland/netstack/src/resolve.rs b/userland/netstack/src/resolve.rs index 190af55443f..526316c8787 100644 --- a/userland/netstack/src/resolve.rs +++ b/userland/netstack/src/resolve.rs @@ -145,7 +145,7 @@ impl u16> Resolver { if self.lookups.len() == MAX_LOOKUPS { return Err((client, Refused::Full)); } - let (lookup, step) = Lookup::start(name.clone(), &self.servers, self.ms(now), (self.draw)()) + let (lookup, step) = Lookup::start(name.clone(), &self.servers, self.ms(now), &mut self.draw) .expect("the server list was checked not empty"); let mut pending = Pending { client, name, lookup, queries: Vec::new() }; match act(&mut pending, step, socket_set, &mut self.draw) { @@ -185,7 +185,7 @@ impl u16> Resolver { unreachable!("netstack: recv hands back the whole datagram and truncates nothing") } }; - let step = pending.lookup.on_datagram(asked, from, port, &reply, now_ms, (self.draw)()); + let step = pending.lookup.on_datagram(asked, from, port, &reply, now_ms, &mut self.draw); done = act(pending, step, socket_set, &mut self.draw); // The step may have let this query go: its socket is read // again from wherever it now is, or every socket from the @@ -193,7 +193,7 @@ impl u16> Resolver { q = pending.queries.iter().position(|&(a, _)| a == asked).unwrap_or(0); } if done.is_none() { - let step = pending.lookup.on_time(now_ms, (self.draw)()); + let step = pending.lookup.on_time(now_ms, &mut self.draw); done = act(pending, step, socket_set, &mut self.draw); } match done {