Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions issues/toyos-has-its-own-network-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions toyos-net-ip/src/arp.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions toyos-net-ip/src/iface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &Address> + '_ {
self.addresses.iter().filter(|a| a.usable())
Expand All @@ -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<Ipv4Addr> {
self.usable().find(|a| a.cidr.contains(toward)).or_else(|| self.usable().next()).map(|a| a.cidr.addr())
Expand Down
4 changes: 2 additions & 2 deletions toyos-net-ip/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
13 changes: 10 additions & 3 deletions toyos-net-ip/src/route.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)? })
}
Expand Down
122 changes: 122 additions & 0 deletions toyos-net-ip/tests/addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&eth(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 }));
}
Loading
Loading