diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 0e239de0d8a..03e83a2490a 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -29,11 +29,9 @@ What the node does not yet meet: - A refusal [udp] logs against a client's call waits in the stack until the node's next `receive`, `fire` or `link`, because only those pass over the stack's reports. Exit, at the move and not after it: the datagram calls end in that pass, or netd drains it once per turn of its loop. - 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. @@ -45,10 +43,12 @@ What stage 3 departs from its specifications: - US-43 expects `ip.not-for-us` for a datagram to an unjoined group in a frame to that group's MAC; the frame filter refuses it first, as `eth.not-for-us` (ETH-23). Exit: the scenario names the frame filter. - DH-64 expects an announce request from the client on link-up with a lease held; `toyos-net-ip` announces the held address itself on link-up, by the owner's ruling to announce rather than probe again, so the client asks nothing. Exit: the scenario drops the request, or [ip] stops announcing on its own. - The readers' UDP specification lets every datagram a closed socket had accepted leave; `toyos-net-udp` holds at most one socket's queue of them together and refuses the rest `udp.tx-discarded-on-close`, because the readers' stage 4 design holds nothing in the shard without a bound. Exit: `s_udp_us_053_accepted_datagrams_outlive_close` finds `udp.tx-discarded-on-close` at 0, every datagram a closed socket accepted leaving. +- The readers' IP specification has no scenario for 169.254/16 on an interface that holds a routable address: its routing scenarios are RTE-01 to RTE-17 and its ARP reception scenarios NBR-01 to NBR-18, none names that prefix, and NBR-12's `arp.sender-off-link` is expected of a sender in no prefix at all, which its 198.51.100.9 still is. Its input scenarios are IN-01 to IN-09, of which none names 169.254 or a link-local source: IN-05, the nearest, refuses a source that is no host or an edge of a held prefix, and [ip]'s refusal of a source at an edge of 169.254/16 on every interface, `ip.invalid-source`, is an addition to that step's list. It has none either for two next hops of which one never answers: NUD-04 overflows one neighbour's queue and NUD-05 fills the interface's, and in neither does a second neighbour answer. The tests that hold the two rules therefore name their RFC section and carry no id: the `rfc_3927_…` tests in `toyos-net-ip/tests/addr.rs` and `nbr.rs`, and `rfc_4861_7_2_2_a_silent_next_hop_holds_up_no_other` in `tests/nud.rs`. Exit: the readers add a scenario for each, and each test carries its id. What stage 3 does not yet meet: - Its only oracles are the readers' own: the specifications' scenarios and byte vectors, and the tests' own RFC 1071 sum. No behaviour of `toyos-net-ip`, `toyos-net-udp` or `toyos-dhcp` is checked against anything the readers did not write. Exit: exchanges captured from slirp and the T14 (ARP, DHCP, ICMP, IGMP) replay through them at stage 5 and match. +- A datagram from a 169.254/16 source to a destination outside that prefix is handed to the router where the interface has one and refused `route.none` where it has none; RFC 3927 §2.6.2 has the host resolve the destination itself and send to it on the link, and with §2.7 never to a router. [ip] sends on the link by the destination alone. `rfc_3927_2_6_2_a_link_local_source_still_sends_by_the_router_or_nowhere` holds both cases as they stand. An interface that held such an address would also owe §2.5, every ARP packet of its own sent to the link's broadcast address, replies included. No interface holds one today, since `toyos-dhcp` refuses one a server offers and nothing configures one by itself, and no stage of this track is planned that gives an interface one. Exit: [ip] refuses a 169.254/16 address at `Ip::add_address` and that test finds the refusal, or the stage that configures one turns that test to the destination's own link address in both cases and adds §2.5's. What stage 4 has not yet built: the segment-script runner, the resets owed when an address is lost, each routed by its destination alone, or the remaining [net] scenarios at frame level: NET-02, NET-05 and NET-07–16 still run on `toyos-net-tcp`'s own two-node network, which goes with the last of them. Exit: each lands, or the specification moves it to a later stage. diff --git a/toyos-net-ip/src/arp.rs b/toyos-net-ip/src/arp.rs index 4c7236053b8..08a6093b839 100644 --- a/toyos-net-ip/src/arp.rs +++ b/toyos-net-ip/src/arp.rs @@ -1,6 +1,6 @@ //! ARP reception: the sender's MAC and address are checked, then the packet classified, in //! `receive`'s order. Only our own -//! need to send, and a request for one of our addresses from an on-link host, create an entry; +//! need to send, and a request for one of our addresses from a neighbour, create an entry; //! any other packet can confirm, assert a MAC into STALE, or be ignored, and a MAC change is //! always logged. @@ -9,7 +9,7 @@ use toyos_net_wire::arp::{Arp, Operation}; use toyos_net_wire::ethernet::MacClass; use crate::counters::Counter; -use crate::iface::{Cx, Interface}; +use crate::iface::{link_local_edge, Cx, Interface}; use crate::{acd, egress, nud}; pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) { @@ -24,7 +24,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) { if mac == i.mac.get() { return cx.log.count(Counter::ArpOwnSender); } - if i.usable().any(|a| a.cidr.is_edge(sender)) { + if link_local_edge(sender) || i.usable().any(|a| a.cidr.is_edge(sender)) { return cx.log.count(Counter::ArpInvalidSenderAddress); } if !probe && i.owns(sender) { @@ -36,7 +36,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) { let for_us = arp.operation == Operation::Request && i.is_usable(target); let mut noticed = for_us; if !probe { - if !i.on_link(sender) { + if !i.is_neighbour(sender) { cx.log.count(Counter::ArpSenderOffLink); noticed = true; } else if i.neighbours.contains_key(&sender) { diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs index 148ac72e39c..4a6f3d038c8 100644 --- a/toyos-net-ip/src/iface.rs +++ b/toyos-net-ip/src/iface.rs @@ -50,6 +50,11 @@ impl Cx<'_> { } } +/// The network or the broadcast address of 169.254/16: no host's, on any interface. +pub(crate) const fn link_local_edge(addr: Ipv4Addr) -> bool { + matches!(addr.octets(), [169, 254, 0, 0] | [169, 254, 255, 255]) +} + impl Interface { pub fn usable(&self) -> impl Iterator + '_ { self.addresses.iter().filter(|a| a.usable()) @@ -68,6 +73,12 @@ impl Interface { self.usable().any(|a| a.cidr.contains(addr)) } + /// Reached without a gateway: on the link, or a host of 169.254/16, which is this link's + /// whatever address the interface holds (RFC 3927 §2.6.2). + pub fn is_neighbour(&self, addr: Ipv4Addr) -> bool { + self.on_link(addr) || (addr.is_link_local() && !link_local_edge(addr) && self.usable().next().is_some()) + } + /// A usable address whose prefix holds `toward`, else the first usable one. pub fn source_for(&self, toward: Ipv4Addr) -> Option { self.usable().find(|a| a.cidr.contains(toward)).or_else(|| self.usable().next()).map(|a| a.cidr.addr()) diff --git a/toyos-net-ip/src/input.rs b/toyos-net-ip/src/input.rs index d2769ea10d0..bb5749451d1 100644 --- a/toyos-net-ip/src/input.rs +++ b/toyos-net-ip/src/input.rs @@ -14,7 +14,7 @@ use toyos_net_wire::udp::UdpDatagram; use toyos_net_wire::Instant; use crate::counters::Counter; -use crate::iface::Interface; +use crate::iface::{link_local_edge, Interface}; use crate::{arp, igmp, Arrival, Cast, Delivery, IfIndex, Ip, Peer}; const IPV6: u16 = 0x86DD; @@ -84,7 +84,7 @@ fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packe return Err(Counter::IpNotForUs); }; let unspecified_igmp = source == Ipv4Addr::UNSPECIFIED && packet.protocol() == Protocol::Igmp; - if !unspecified_igmp && (!is_host(source) || i.usable().any(|a| a.cidr.is_edge(source))) { + if !unspecified_igmp && (!is_host(source) || link_local_edge(source) || i.usable().any(|a| a.cidr.is_edge(source))) { return Err(Counter::IpInvalidSource); } if ifaces.iter().any(|j| j.owns(source)) { diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs index 79f1f91a0ee..fc05654905b 100644 --- a/toyos-net-ip/src/route.rs +++ b/toyos-net-ip/src/route.rs @@ -1,7 +1,8 @@ //! The host routing table: the connected prefixes of usable addresses and an ordered list of -//! on-link gateways per interface; longest prefix first, then the active gateway, and no -//! forwarding. A lookup considers only interfaces that are up, and with a bound source only the -//! interface holding it (RFC 1122 §3.3.4.2, the strong model). +//! on-link gateways per interface; longest prefix first, then 169.254/16 on the link itself (RFC +//! 3927 §2.6.2), then the active gateway, and no forwarding. A lookup considers only interfaces +//! that are up, and with a bound source only the interface holding it (RFC 1122 §3.3.4.2, the +//! strong model). use core::net::Ipv4Addr; @@ -92,6 +93,12 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) }; return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? }); } + // RFC 3927 §2.6.2, §2.7: a host of 169.254/16 is resolved and sent to on the link itself, and + // nothing to that prefix is a router's: not its two edges either, which are no host. + if destination.is_link_local() { + let (index, i) = candidates().find(|(_, i)| i.is_neighbour(destination)).ok_or(Counter::RouteNone)?; + return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? }); + } let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?; Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(gateway), source: pick_source(i, gateway)? }) } diff --git a/toyos-net-ip/tests/addr.rs b/toyos-net-ip/tests/addr.rs index e518c9e6f9b..a937698afa1 100644 --- a/toyos-net-ip/tests/addr.rs +++ b/toyos-net-ip/tests/addr.rs @@ -415,3 +415,125 @@ fn s_ip_rte_017_lookups_send_nothing() { } assert!(h.run(600_000).is_empty()); } + +const LINK_LOCAL: Ipv4Addr = Ipv4Addr::new(169, 254, 3, 4); +const MAC_L: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x4c]); +/// The network and the broadcast address of 169.254/16. +const LINK_LOCAL_EDGES: [Ipv4Addr; 2] = [Ipv4Addr::new(169, 254, 0, 0), Ipv4Addr::new(169, 254, 255, 255)]; + +// RFC 3927 §2.6.2: "If the destination address is in the 169.254/16 prefix (excluding the address +// 169.254.255.255, which is the IPv4 Link-Local subnet broadcast address), then the sender MUST +// ARP for the destination address and then send the packet directly to the destination on the +// same physical link. This MUST be done whether the interface is configured with a Link-Local +// or a routable IPv4 address." And: "The host MUST NOT send a packet with an IPv4 Link-Local +// destination address to any router for forwarding." §2.7 says the same of every such packet. +#[test] +fn rfc_3927_2_6_2_a_link_local_destination_is_on_the_link() { + let mut h = H::fixture_i(); + let on_the_link = Route { iface: h.if0, next_hop: NextHop::Neighbour(LINK_LOCAL), source: A }; + assert_eq!(route(&mut h, LINK_LOCAL), Ok(on_the_link), "with a routable address and a router"); + assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(R)), "the router is still the way off the link"); + h.ip.set_gateways(h.clock(), h.if0, &[]).unwrap(); + assert_eq!(route(&mut h, LINK_LOCAL), Ok(on_the_link), "and with no router at all"); + + let mut h = H::bare(); + assert_eq!(route(&mut h, LINK_LOCAL), Err(Counter::RouteNoSourceAddress), "an interface with no address sends nothing"); + + // The interface is the lowest one up, or the one a bound source names. + let mut h = H::raw(); + let (if0, if1) = (h.if0, h.add_if1()); + h.assign(if0, A, 24, 0); + h.assign(if1, ip4(198, 51, 100, 1), 24, 5_000); + h.settle(); + assert_eq!(route(&mut h, LINK_LOCAL).map(|r| (r.iface, r.source)), Ok((if0, A))); + let bound = h.ip.route(LINK_LOCAL, Source::Bound(ip4(198, 51, 100, 1)), None); + assert_eq!(bound, Ok(Route { iface: if1, next_hop: NextHop::Neighbour(LINK_LOCAL), source: ip4(198, 51, 100, 1) })); + h.ip.link_down(h.clock(), if0).unwrap(); + assert_eq!(route(&mut h, LINK_LOCAL).map(|r| r.iface), Ok(if1)); +} + +// The same rule on the wire: the request names the destination itself, its reply is a +// neighbour's, and no frame is the router's. RFC 3927 §2.5: a host sends every ARP packet whose +// sender is link-local, a reply too, to the link's broadcast address. +#[test] +fn rfc_3927_2_6_2_a_datagram_to_a_link_local_host_is_resolved_and_sent_to_it() { + let mut h = H::fixture_i_with(R, MAC_R); + assert_eq!(h.udp_to(LINK_LOCAL), Ok(None)); + let asked = h.out(); + assert_eq!(asked.len(), 1); + let request = asked[0].arp().unwrap(); + assert_eq!((asked[0].to(), request.sender_ip, request.target_ip), (MacAddr::BROADCAST, A, LINK_LOCAL)); + h.at(5); + h.frame(ð(MacAddr::BROADCAST, MAC_L, 0x0806, &arp_packet(2, MAC_L, LINK_LOCAL, MAC_A, A))); + assert!(h.is_reachable(LINK_LOCAL)); + assert_eq!(h.count(Counter::ArpSenderOffLink), 0); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!((out[0].to(), out[0].ip().unwrap().destination()), (MAC_L, LINK_LOCAL)); + assert!(matches!(h.udp_to(LINK_LOCAL), Ok(Some(frame)) if destination_of(&frame) == MAC_L)); +} + +// RFC 3927 §2.6.2 excludes 169.254.255.255, "the IPv4 Link-Local subnet broadcast address", from +// what is resolved, and 169.254.0.0 is that prefix's network address: neither is a host. On an +// interface whose own prefix is another they are nothing it may send to: no request asks for +// them, and by §2.7 no router is handed them, though the router is resolved and a frame to it +// could leave at once. +#[test] +fn rfc_3927_2_6_2_the_edges_of_the_link_local_prefix_are_no_destination() { + let mut h = H::fixture_i_with(R, MAC_R); + for edge in LINK_LOCAL_EDGES { + assert_eq!(route(&mut h, edge), Err(Counter::RouteNone), "{edge}"); + assert_eq!(h.udp_to(edge), Err(Counter::RouteNone), "{edge}"); + assert!(h.state(edge).is_none(), "{edge}"); + } + assert!(h.out().is_empty(), "no request, and no frame to the router"); + assert!(h.events.contains(&toyos_net_ip::Event::Unreachable(toyos_net_ip::Flow { + source: A, + source_port: toyos_net_wire::Port::new(5001).unwrap(), + destination: LINK_LOCAL_EDGES[1], + destination_port: toyos_net_wire::Port::new(5001).unwrap(), + }))); +} + +// RFC 1122 §3.2.1.3: a datagram whose source is a broadcast address is silently discarded, and +// RFC 3927 §2.6.2 names 169.254.255.255 the link-local prefix's broadcast address for every +// host, whatever it holds; 169.254.0.0 is that prefix's network address, and no host's either. +#[test] +fn rfc_3927_the_edges_of_the_link_local_prefix_are_no_source() { + for edge in LINK_LOCAL_EDGES { + let mut h = H::fixture_i(); + assert_eq!(cast_of(&h.datagram(&udp(edge, A, 5000, 5001, b"hi"))), None, "{edge}"); + assert_eq!(h.count(Counter::IpInvalidSource), 1, "{edge}"); + assert!(h.out().is_empty(), "{edge}: and nothing answers it"); + } + let mut h = H::fixture_i(); + assert_eq!(cast_of(&h.datagram(&udp(LINK_LOCAL, A, 5000, 5001, b"hi"))), Some(Cast::Unicast), "a host of the prefix is a source"); +} + +// RFC 2132 §3.5: the router option lists "routers on the client's subnet". A router is inside a +// prefix the interface holds: what RFC 3927 puts on the link is a neighbour, and no router. +#[test] +fn rfc_3927_a_link_local_address_is_no_gateway_of_a_routable_interface() { + let mut h = H::fixture_i(); + assert_eq!(h.ip.set_gateways(h.clock(), h.if0, &[ip4(169, 254, 0, 1)]), Err(Counter::RouteGatewayOffLink)); + assert_eq!(h.ip.gateways(h.if0), Some(&[R][..])); +} + +// Present state, and wrong: RFC 3927 §2.6.2 has a host that sends from a link-local source to a +// destination outside 169.254/16 "ARP for the destination address and then send the packet ... +// directly to its destination on the same physical link", and "MUST NOT send the packet to any +// router for forwarding". [ip] decides by the destination alone: with a gateway the datagram is +// the router's, and with none it is refused. The track records it; its exit turns both into the +// destination itself. +#[test] +fn rfc_3927_2_6_2_a_link_local_source_still_sends_by_the_router_or_nowhere() { + let ours = ip4(169, 254, 7, 7); + let router = ip4(169, 254, 0, 1); + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, ours, 16, 0); + h.settle(); + assert_eq!(route(&mut h, REMOTE), Err(Counter::RouteNone)); + h.ip.set_gateways(h.clock(), if0, &[router]).unwrap(); + assert_eq!(route(&mut h, REMOTE), Ok(Route { iface: if0, next_hop: NextHop::Neighbour(router), source: ours })); +} diff --git a/toyos-net-ip/tests/nbr.rs b/toyos-net-ip/tests/nbr.rs index 1f23c4f6c12..bfed86d03c9 100644 --- a/toyos-net-ip/tests/nbr.rs +++ b/toyos-net-ip/tests/nbr.rs @@ -153,6 +153,43 @@ fn s_ip_nbr_012_an_off_link_sender_is_answered_not_cached() { assert_eq!(h.count(Counter::ArpSenderOffLink), 1); } +// RFC 3927 §2.6.2: a host in 169.254/16 is on this link whatever address the interface holds, so +// its request is a neighbour's: answered, and cached to be verified before it is trusted. Before +// the interface has an address nothing is on its link. §2.5 has that host broadcast its request, +// as every request here is. +#[test] +fn rfc_3927_2_6_2_a_link_local_sender_is_a_neighbour() { + let asker = ip4(169, 254, 3, 4); + let mut h = H::fixture_i(); + h.frame(&request(MAC_B, asker, A)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!((out[0].to(), out[0].arp().unwrap().target_ip), (MAC_B, asker)); + assert!(h.is_stale(asker)); + assert_eq!(h.mac_of(asker), Some(MAC_B)); + assert_eq!(h.count(Counter::ArpSenderOffLink), 0); + + let mut h = H::bare(); + h.frame(&request(MAC_B, asker, A)); + assert!(h.out().is_empty() && h.state(asker).is_none()); + assert_eq!(h.count(Counter::ArpSenderOffLink), 1); +} + +// 169.254.0.0 and 169.254.255.255 are the network and the broadcast address of that prefix +// (RFC 3927 §2.6.2 names the second), and no host's on any interface: as a sender, in a request +// for our address or in a reply to a request of ours, each is refused as our own prefixes' edges +// are, answered nothing and cached nowhere. +#[test] +fn rfc_3927_the_edges_of_the_link_local_prefix_are_no_senders() { + for edge in [ip4(169, 254, 0, 0), ip4(169, 254, 255, 255)] { + let mut h = H::fixture_i(); + h.frame(&request(MAC_B, edge, A)); + h.frame(ð(MAC_A, MAC_B, 0x0806, &arp_packet(2, MAC_B, edge, MAC_A, A))); + assert_eq!(h.count(Counter::ArpInvalidSenderAddress), 2, "{edge}"); + assert!(h.out().is_empty() && h.state(edge).is_none(), "{edge}"); + } +} + #[test] fn s_ip_nbr_013_a_probe_is_answered_and_not_cached() { let mut h = H::fixture_i(); diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 8232e9804bc..a760b8811b1 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -886,3 +886,47 @@ fn s_ip_nud_031_a_request_queued_before_the_lifetime_is_not_idle() { assert_eq!(h.out_with(1).iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_REQ_R)]); assert_eq!(h.ip.next_deadline(), Some(H::instant(end + 3_000)), "3 s after the hand-off"); } + +// RFC 4861 §7.2.2: "While waiting for address resolution to complete, the sender MUST, for each +// neighbor, retain a small queue of packets waiting for address resolution to complete. ... When +// a queue overflows, the new arrival SHOULD replace the oldest entry." The queue is the next +// hop's: a router that never answers holds every datagram routed by it, drops only its own, and +// holds up none to a host on the link. +#[test] +fn rfc_4861_7_2_2_a_silent_next_hop_holds_up_no_other() { + let per = limits::nud::PENDING_PER_NEIGHBOUR as u8; + let mut h = H::fixture_i(); + let behind_r = |n: u8| Ipv4Addr::new(198, 51, 100, 1 + n); + for n in 0..=per { + assert_eq!(h.send(A, behind_r(n), 5001, 5001, &[b'r', n]), Ok(None)); + } + assert_eq!(h.count(Counter::NbPendingOverflow), 1, "one more than R's queue holds"); + for n in 0..per { + assert_eq!(h.send(A, B, 5001, 5001, &[b'b', n]), Ok(None)); + } + assert_eq!(h.count(Counter::NbPendingOverflow), 1, "B's datagrams wait in B's queue, and R's full one drops none of them"); + let queued = |h: &H, addr| match h.state(addr) { + Some(Nud::Incomplete(s)) => Some(s.pending.queued()), + _ => None, + }; + assert_eq!((queued(&h, R), queued(&h, B)), (Some(usize::from(per)), Some(usize::from(per)))); + let asked = h.out(); + assert!(asked.len() == 2 && asked[0].requests(R) && asked[1].requests(B), "one request each, and no datagram"); + + h.at(5); + h.frame(&hex(V_ARP_REPLY)); + let released: Vec<(MacAddr, Vec)> = h.out().iter().map(|o| (o.to(), payload(o))).collect(); + let all_of_bs: Vec<(MacAddr, Vec)> = (0..per).map(|n| (MAC_B, vec![b'b', n])).collect(); + assert_eq!(released, all_of_bs, "B answered: every datagram to B leaves, in order, while R is silent"); + assert_eq!(queued(&h, R), Some(usize::from(per)), "and R's wait on"); + assert!(matches!(h.send(A, B, 5001, 5001, b"now"), Ok(Some(frame)) if destination_of(&frame) == MAC_B)); + + let later = h.run(3_000); + assert!(later.iter().all(|o| o.requests(R)), "nothing but R's own requests leaves as R fails"); + assert!(matches!(h.state(R), Some(Nud::Failed))); + assert_eq!(h.count(Counter::NbPendingDropped), u64::from(per)); + let told: Vec = h.events.iter().filter(|e| matches!(e, Event::Unreachable(_))).copied().collect(); + let behind: Vec = (1..=per).map(|n| Event::Unreachable(flow_to(behind_r(n)))).collect(); + assert_eq!(told, behind, "only the flows routed by R are told, the oldest having given its place"); + assert!(matches!(h.send(A, B, 5001, 5001, b"still"), Ok(Some(frame)) if destination_of(&frame) == MAC_B)); +} diff --git a/userland/netstack/node/tests/name.rs b/userland/netstack/node/tests/name.rs index a7a245248f2..96e0681c9ba 100644 --- a/userland/netstack/node/tests/name.rs +++ b/userland/netstack/node/tests/name.rs @@ -11,7 +11,7 @@ mod lan; use std::net::Ipv4Addr; use std::time::Duration; -use common::{sum, terms, A, MAC_B, MAC_R, R}; +use common::{arp, sum, terms, A, BROADCAST, MAC_B, MAC_R, R}; use lan::{udp, Lan, Seen, Udp, B, OFF_LINK}; use toyos_mdns::Host; use toyos_net_node::{Counter, Refused}; @@ -216,19 +216,26 @@ fn a_second_query_inside_a_second_waits_for_the_second_to_end() { assert_eq!(lan.datagrams().len(), from + 2, "one multicast answered both"); } -// Present state, and wrong: RFC 3927 §2.6.2 has a host send to 169.254/16 directly on the link -// whatever address it holds, and §7 never through a router. The responder takes a link-local -// source for one on this link (RFC 6762 §11), and [ip], with no route for 169.254/16 on a leased -// interface, hands the answer to the router. The track records it; its exit turns `to` here into -// the asker's own link address. +// RFC 3927 §2.6.2: "If the destination address is in the 169.254/16 prefix ... then the sender +// MUST ARP for the destination address and then send the packet directly to the destination on +// the same physical link. This MUST be done whether the interface is configured with a +// Link-Local or a routable IPv4 address." And: "The host MUST NOT send a packet with an IPv4 +// Link-Local destination address to any router for forwarding" (§2.7 too). The responder takes +// a link-local source for one on this link (RFC 6762 §11); the node holds a routable lease and +// a router, asks the link for the asker itself, and answers it there. §2.5: the asker's ARP +// reply, its sender link-local, comes in a frame to the link's broadcast address. #[test] -fn an_answer_to_a_link_local_asker_leaves_by_the_router() { +fn an_answer_to_a_link_local_asker_goes_to_its_own_link_address() { let link_local = Ipv4Addr::new(169, 254, 3, 4); let mut lan = settled(); - let from = lan.datagrams().len(); + let (from, frames) = (lan.datagrams().len(), lan.sent.len()); lan.deliver(&to_group(MAC_B, (link_local, 53_000), &ours(5, CLASS_IN))); - let by_the_router = Udp { to: MAC_R, source: A, source_port: MDNS, destination: link_local, port: 53_000, ttl: 255, payload: legacy_response(5) }; - assert_eq!(since(&lan, from), [by_the_router]); + let asked: Vec<&Seen> = lan.sent[frames..].iter().map(|(_, seen)| seen).collect(); + assert_eq!(asked, [&Seen::Arp { request: true, sender: A, target: link_local }], "the asker is asked for, and nothing is the router's"); + + lan.deliver(&arp(BROADCAST, false, MAC_B, link_local, A)); + let on_the_link = Udp { to: MAC_B, source: A, source_port: MDNS, destination: link_local, port: 53_000, ttl: 255, payload: legacy_response(5) }; + 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: