From 3c913fde32c58024fd192b0eb1b4e7db17e2857d Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 21:46:39 +0200 Subject: [PATCH 01/20] toyos-net-wire: the clock, the keyed function and the counters every net crate shares Stage 3 adds three crates (toyos-net-ip, toyos-net-udp, toyos-dhcp) that need the same injected Instant, the same SipHash-2-4 keyed function and the same counter declaration toyos-net-tcp carried privately. Each moves to toyos-net-wire, the crate every net crate already depends on, so the shard that composes them hands one Instant to all of them and no crate carries a second copy of either. toyos-net-tcp re-exports Instant, siphash24 and Key, so its API is unchanged; the TSval of an instant becomes a free function, and its counters are declared through toyos_net_wire::counters!. Its suite is the same 654 tests, green, before and after. Co-Authored-By: Claude Opus 5.5 --- toyos-net-tcp/src/conn.rs | 4 +- toyos-net-tcp/src/counters.rs | 69 +----------- toyos-net-tcp/src/lib.rs | 40 ++----- toyos-net-tcp/src/open.rs | 6 +- toyos-net-wire/src/lib.rs | 104 ++++++++++++++++++ .../src/siphash.rs | 4 +- 6 files changed, 118 insertions(+), 109 deletions(-) rename {toyos-net-tcp => toyos-net-wire}/src/siphash.rs (89%) diff --git a/toyos-net-tcp/src/conn.rs b/toyos-net-tcp/src/conn.rs index ee5327b86e5..c301ad6497e 100644 --- a/toyos-net-tcp/src/conn.rs +++ b/toyos-net-tcp/src/conn.rs @@ -221,13 +221,13 @@ impl Ts { /// What a segment sent now carries: TSval now, TSecr TS.Recent. pub fn option(&self, now: Instant) -> Timestamps { - Timestamps { value: now.tsval(self.offset), echo: self.recent } + Timestamps { value: crate::tsval(now, self.offset), echo: self.recent } } /// RFC 7323 §4.1: the round trip an echoed TSval measures, unless it is one we never sent or /// one from the future. pub fn echo_rtt(&self, echo: u32, now: Instant) -> Option { - let age = Stamp(now.tsval(self.offset)).since(Stamp(echo)); + let age = Stamp(crate::tsval(now, self.offset)).since(Stamp(echo)); (age < 1 << 31 && Stamp(echo).at_or_after(Stamp(self.first))).then(|| Duration::from_millis(u64::from(age))) } } diff --git a/toyos-net-tcp/src/counters.rs b/toyos-net-tcp/src/counters.rs index 485732fc66c..4b09374d926 100644 --- a/toyos-net-tcp/src/counters.rs +++ b/toyos-net-tcp/src/counters.rs @@ -8,56 +8,7 @@ use core::time::Duration; use crate::{limits, Endpoint, Event, Instant, Tuple}; -macro_rules! counters { - ($($variant:ident = $name:literal $(, $logged:ident)?;)*) => { - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Counter { - $($variant,)* - } - - impl Counter { - pub const ALL: &'static [Counter] = &[$(Counter::$variant,)*]; - - pub const fn name(self) -> &'static str { - match self { - $(Self::$variant => $name,)* - } - } - - /// A refusal the log names: legacy or insecure input ToyOS does not implement. - pub const fn logged(self) -> bool { - match self { - $(Self::$variant => counters!(@logged $($logged)?),)* - } - } - } - - /// One `T` per counter. - #[allow(non_snake_case)] - #[derive(Clone, Debug, Default)] - struct PerCounter { - $($variant: T,)* - } - - impl PerCounter { - fn get(&self, counter: Counter) -> &T { - match counter { - $(Counter::$variant => &self.$variant,)* - } - } - - fn get_mut(&mut self, counter: Counter) -> &mut T { - match counter { - $(Counter::$variant => &mut self.$variant,)* - } - } - } - }; - (@logged logged) => { true }; - (@logged) => { false }; -} - -counters! { +toyos_net_wire::counters! { ClosedRst = "tcp.closed-rst"; ClosedRstLimited = "tcp.closed-rst-limited"; SynRst = "tcp.syn-rst", logged; @@ -118,24 +69,6 @@ counters! { EventOverflow = "tcp.event-overflow"; } -#[derive(Clone, Debug, Default)] -pub struct Counters(PerCounter); - -impl Counters { - pub fn get(&self, counter: Counter) -> u64 { - *self.0.get(counter) - } - - fn add(&mut self, counter: Counter, n: u64) { - let value = self.0.get_mut(counter); - *value = value.saturating_add(n); - } - - pub fn iter(&self) -> impl Iterator + '_ { - Counter::ALL.iter().map(|&c| (c.name(), self.get(c))) - } -} - /// The counters and the events the shell has not drained: a refusal is both. #[derive(Debug, Default)] pub struct Log { diff --git a/toyos-net-tcp/src/lib.rs b/toyos-net-tcp/src/lib.rs index 8c560599c60..69364eb82f0 100644 --- a/toyos-net-tcp/src/lib.rs +++ b/toyos-net-tcp/src/lib.rs @@ -40,51 +40,25 @@ mod ring; mod rtt; mod rx; mod seq; -mod siphash; mod stack; mod tx; use core::net::Ipv4Addr; use core::time::Duration; +use toyos_net_wire::siphash; use toyos_net_wire::Port; pub use counters::{Counter, Counters, Refusal, RefusalLog, REFUSAL_LOG_INTERVAL}; pub use seq::Seq; -pub use siphash::{siphash24, Key}; pub use stack::{ConnId, Info, ListenerId, Outgoing, Tcp}; +pub use toyos_net_wire::siphash::{siphash24, Key}; +pub use toyos_net_wire::Instant; -/// A point on the caller's monotonic clock, in nanoseconds. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Instant(u64); - -impl Instant { - pub const fn from_nanos(ns: u64) -> Self { - Self(ns) - } - - pub const fn from_millis(ms: u64) -> Self { - Self(ms.saturating_mul(1_000_000)) - } - - pub const fn nanos(self) -> u64 { - self.0 - } - - pub fn after(self, d: Duration) -> Self { - Self(self.0.saturating_add(u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))) - } - - /// Zero when `earlier` is not earlier. - pub const fn since(self, earlier: Self) -> Duration { - Duration::from_nanos(self.0.saturating_sub(earlier.0)) - } - - /// The TSval a 4-tuple with `offset` sends now: one tick per millisecond (RFC 7323 §5.4). - const fn tsval(self, offset: u32) -> u32 { - let [a, b, c, d, ..] = (self.0 / 1_000_000).to_le_bytes(); - u32::from_le_bytes([a, b, c, d]).wrapping_add(offset) - } +/// The TSval a 4-tuple with `offset` sends at `now`: one tick per millisecond (RFC 7323 §5.4). +const fn tsval(now: Instant, offset: u32) -> u32 { + let [a, b, c, d, ..] = (now.nanos() / 1_000_000).to_le_bytes(); + u32::from_le_bytes([a, b, c, d]).wrapping_add(offset) } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/toyos-net-tcp/src/open.rs b/toyos-net-tcp/src/open.rs index 8a9167b4ac8..6a06827a2a8 100644 --- a/toyos-net-tcp/src/open.rs +++ b/toyos-net-tcp/src/open.rs @@ -141,7 +141,7 @@ impl Retransmit { fn syn_options(local: &Local, peer: Option<&Negotiated>, now: Instant) -> SynOptions { let timestamps = match peer { - None => Some(Timestamps { value: now.tsval(local.ts_offset), echo: 0 }), + None => Some(Timestamps { value: crate::tsval(now, local.ts_offset), echo: 0 }), Some(n) => n.ts.map(|ts| ts.option(now)), }; SynOptions { @@ -237,7 +237,7 @@ impl SynSent { if !self.timer.owed { return None; } - let tsval = now.tsval(local.ts_offset); + let tsval = crate::tsval(now, local.ts_offset); self.timer.handed_off(now, tsval, true); Some(Out { seq: self.iss, kind: Kind::Syn(syn_options(local, None, now)), window: local.window, ts: None, sack: NO_BLOCKS, data: (0, 0) }) } @@ -394,7 +394,7 @@ impl SynRcvd { if let Some(rst) = self.answer.take() { return Some(Out::rst(&rst)); } - let tsval = now.tsval(local.ts_offset); + let tsval = crate::tsval(now, local.ts_offset); let ts = self.negotiated.ts.map(|ts| ts.option(now)); let dup = core::mem::replace(&mut self.dup_answer, false); if self.timer.owed || dup { diff --git a/toyos-net-wire/src/lib.rs b/toyos-net-wire/src/lib.rs index c42cd96f540..7be1971e2c0 100644 --- a/toyos-net-wire/src/lib.rs +++ b/toyos-net-wire/src/lib.rs @@ -1,4 +1,7 @@ //! Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP, parsed in place and built: a parse refuses by the first rule its input breaks, a build refuses what it cannot represent. +//! +//! It is also what every net crate above it shares: the caller's clock, the keyed function, and +//! the declaration of a crate's counters. #![no_std] #![forbid(unsafe_code)] @@ -48,6 +51,79 @@ macro_rules! reasons { }; } +/// Declares a crate's counters: `Counter`, each variant the name inspect reads it by and, marked +/// `logged`, a refusal of legacy or insecure input the log names; `Counters`, one value each; and +/// `PerCounter`, one `T` each. +#[macro_export] +macro_rules! counters { + ($($variant:ident = $name:literal $(, $logged:ident)?;)*) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Counter { + $($variant,)* + } + + impl Counter { + pub const ALL: &'static [Counter] = &[$(Counter::$variant,)*]; + + pub const fn name(self) -> &'static str { + match self { + $(Self::$variant => $name,)* + } + } + + pub const fn logged(self) -> bool { + match self { + $(Self::$variant => $crate::counters!(@logged $($logged)?),)* + } + } + } + + #[allow(non_snake_case)] + #[derive(Clone, Debug, Default)] + struct PerCounter { + $($variant: T,)* + } + + impl PerCounter { + fn get(&self, counter: Counter) -> &T { + match counter { + $(Counter::$variant => &self.$variant,)* + } + } + + fn get_mut(&mut self, counter: Counter) -> &mut T { + match counter { + $(Counter::$variant => &mut self.$variant,)* + } + } + } + + #[derive(Clone, Debug, Default)] + pub struct Counters(PerCounter); + + impl Counters { + pub fn get(&self, counter: Counter) -> u64 { + *self.0.get(counter) + } + + pub(crate) fn add(&mut self, counter: Counter, n: u64) { + let value = self.0.get_mut(counter); + *value = value.saturating_add(n); + } + + pub fn iter(&self) -> impl Iterator + '_ { + Counter::ALL.iter().map(|&c| (c.name(), self.get(c))) + } + } + }; + (@logged logged) => { + true + }; + (@logged) => { + false + }; +} + pub mod arp; pub mod checksum; mod emit; @@ -55,11 +131,39 @@ pub mod ethernet; pub mod icmp; pub mod igmp; pub mod ipv4; +pub mod siphash; pub mod tcp; pub mod udp; pub use emit::BuildError; +/// A point on the caller's monotonic clock, in nanoseconds. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Instant(u64); + +impl Instant { + pub const fn from_nanos(ns: u64) -> Self { + Self(ns) + } + + pub const fn from_millis(ms: u64) -> Self { + Self(ms.saturating_mul(1_000_000)) + } + + pub const fn nanos(self) -> u64 { + self.0 + } + + pub fn after(self, d: core::time::Duration) -> Self { + Self(self.0.saturating_add(u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))) + } + + /// Zero when `earlier` is not earlier. + pub const fn since(self, earlier: Self) -> core::time::Duration { + core::time::Duration::from_nanos(self.0.saturating_sub(earlier.0)) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Port(core::num::NonZeroU16); diff --git a/toyos-net-tcp/src/siphash.rs b/toyos-net-wire/src/siphash.rs similarity index 89% rename from toyos-net-tcp/src/siphash.rs rename to toyos-net-wire/src/siphash.rs index 04aef5762a0..d62032a6a70 100644 --- a/toyos-net-tcp/src/siphash.rs +++ b/toyos-net-wire/src/siphash.rs @@ -1,6 +1,5 @@ //! SipHash-2-4 (Aumasson and Bernstein, "SipHash: a fast short-input PRF"): the keyed function -//! behind initial sequence numbers, timestamp offsets and port choice (RFC 6528 §3, RFC 6056 -//! §3.3.4), each with its own key. +//! behind every value a net crate keeps unpredictable, each purpose with its own key. pub type Key = [u8; 16]; @@ -42,7 +41,6 @@ pub fn siphash24(key: &Key, data: &[u8]) -> u64 { v[0] ^ v[1] ^ v[2] ^ v[3] } -/// The low 32 bits, which every use here takes. pub fn low32(key: &Key, data: &[u8]) -> u32 { let [a, b, c, d, ..] = siphash24(key, data).to_le_bytes(); u32::from_le_bytes([a, b, c, d]) From 3fa6d3b71d49ee01163914aaaad499151075452e Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 23:10:11 +0200 Subject: [PATCH 02/20] toyos-net-ip: the IPv4 host layer of ToyOS's own stack, first cut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 3's first crate, written clean-room from the stage-3 IP specification and the RFCs: per-interface addresses and the host routing table, frame dispatch and the IPv4 input policy, the reachability machine over ARP (RFC 4861 §7.3 with RFC 7048's UNREACHABLE), RFC 5227 conflict detection on the owner's 200 ms schedule, ICMP echo and errors under the two-level limiter, inbound errors attributed for the transports, and the IGMPv3 host with its IGMPv2 compatibility mode. Pull egress throughout: [ip]'s own frames wait in one FIFO and are built when they leave; every timer they feed starts at the hand-off; a UDP datagram is written straight into the caller's frame or held here for its next hop, and a full ring spends no datagram. toyos-net-wire gains what W-1 needs (IGMPv3 IS_IN (B), RFC 9776 §5.2 Table 5), `MulticastAddr: Ord`, and the counter macro takes an expression for a name and generates the per-rule refusal limiter, which toyos-net-tcp now uses instead of its own copy. Scenario tests so far: CLK, ADDR, RTE, IN, OUT, NUD, and the wire specification's owed [ip] and [shell] rows (IPP, ETH, IP, IPO, UDP-22). Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 7 + Cargo.toml | 1 + toyos-net-ip/Cargo.toml | 10 + toyos-net-ip/src/acd.rs | 187 ++++++++ toyos-net-ip/src/addr.rs | 90 ++++ toyos-net-ip/src/arp.rs | 58 +++ toyos-net-ip/src/config.rs | 181 +++++++ toyos-net-ip/src/counters.rs | 144 ++++++ toyos-net-ip/src/draw.rs | 67 +++ toyos-net-ip/src/egress.rs | 340 +++++++++++++ toyos-net-ip/src/icmp.rs | 151 ++++++ toyos-net-ip/src/iface.rs | 95 ++++ toyos-net-ip/src/igmp.rs | 518 ++++++++++++++++++++ toyos-net-ip/src/input.rs | 171 +++++++ toyos-net-ip/src/lib.rs | 470 ++++++++++++++++++ toyos-net-ip/src/limiter.rs | 86 ++++ toyos-net-ip/src/nud.rs | 498 +++++++++++++++++++ toyos-net-ip/src/route.rs | 174 +++++++ toyos-net-ip/src/timers.rs | 76 +++ toyos-net-ip/tests/addr.rs | 417 ++++++++++++++++ toyos-net-ip/tests/clk.rs | 229 +++++++++ toyos-net-ip/tests/common/mod.rs | 789 +++++++++++++++++++++++++++++++ toyos-net-ip/tests/io.rs | 518 ++++++++++++++++++++ toyos-net-ip/tests/nud.rs | 476 +++++++++++++++++++ toyos-net-tcp/src/counters.rs | 22 +- toyos-net-tcp/src/lib.rs | 3 +- toyos-net-tcp/tests/modern.rs | 4 +- toyos-net-wire/src/igmp.rs | 47 +- toyos-net-wire/src/ipv4.rs | 2 +- toyos-net-wire/src/lib.rs | 26 +- toyos-net-wire/tests/igmp.rs | 31 +- 31 files changed, 5835 insertions(+), 53 deletions(-) create mode 100644 toyos-net-ip/Cargo.toml create mode 100644 toyos-net-ip/src/acd.rs create mode 100644 toyos-net-ip/src/addr.rs create mode 100644 toyos-net-ip/src/arp.rs create mode 100644 toyos-net-ip/src/config.rs create mode 100644 toyos-net-ip/src/counters.rs create mode 100644 toyos-net-ip/src/draw.rs create mode 100644 toyos-net-ip/src/egress.rs create mode 100644 toyos-net-ip/src/icmp.rs create mode 100644 toyos-net-ip/src/iface.rs create mode 100644 toyos-net-ip/src/igmp.rs create mode 100644 toyos-net-ip/src/input.rs create mode 100644 toyos-net-ip/src/lib.rs create mode 100644 toyos-net-ip/src/limiter.rs create mode 100644 toyos-net-ip/src/nud.rs create mode 100644 toyos-net-ip/src/route.rs create mode 100644 toyos-net-ip/src/timers.rs create mode 100644 toyos-net-ip/tests/addr.rs create mode 100644 toyos-net-ip/tests/clk.rs create mode 100644 toyos-net-ip/tests/common/mod.rs create mode 100644 toyos-net-ip/tests/io.rs create mode 100644 toyos-net-ip/tests/nud.rs diff --git a/Cargo.lock b/Cargo.lock index f22b64e4ae1..feb58be41aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,6 +1129,13 @@ dependencies = [ name = "toyos-mixer" version = "0.1.0" +[[package]] +name = "toyos-net-ip" +version = "0.1.0" +dependencies = [ + "toyos-net-wire", +] + [[package]] name = "toyos-net-tcp" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 57ebd52d1eb..a3e0443aeaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "toyos-manifest", "toyos-mdns", "toyos-mixer", + "toyos-net-ip", "toyos-net-tcp", "toyos-net-wire", "toyos-pci", diff --git a/toyos-net-ip/Cargo.toml b/toyos-net-ip/Cargo.toml new file mode 100644 index 00000000000..56ea7b03248 --- /dev/null +++ b/toyos-net-ip/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "toyos-net-ip" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "ToyOS's IPv4 host layer, pure: addressing, routing, ARP under one reachability machine, conflict detection, ICMP and the IGMP host." +publish = false + +[dependencies] +toyos-net-wire = { path = "../toyos-net-wire" } diff --git a/toyos-net-ip/src/acd.rs b/toyos-net-ip/src/acd.rs new file mode 100644 index 00000000000..4a87fe676ea --- /dev/null +++ b/toyos-net-ip/src/acd.rs @@ -0,0 +1,187 @@ +//! Address conflict detection (RFC 5227, §8). A new address probes on RFC 5227's schedule scaled +//! by the owner's ruling (`limits::acd`), is usable from its first announcement, and is then +//! defended once per DEFEND_INTERVAL and given up on a second conflict inside it (§2.4 (b)): one +//! forged packet cannot take an address away. Every interval starts when its frame leaves. + +use core::net::Ipv4Addr; + +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::Instant; + +use crate::addr::{Address, Phase}; +use crate::counters::Counter; +use crate::draw::Purpose; +use crate::egress::Item; +use crate::iface::{Cx, Interface}; +use crate::limits::acd::{ + ANNOUNCE_INTERVAL, ANNOUNCE_NUM, ANNOUNCE_WAIT, CONFLICT_RESET, DEFEND_INTERVAL, MAX_CONFLICTS, PROBE_MAX, PROBE_MIN, + PROBE_NUM, PROBE_WAIT, RATE_LIMIT_INTERVAL, +}; +use crate::timers::Timer; +use crate::{igmp, route, Event, Peer}; + +/// An interface's conflict history, for the rate limit on new candidates (§8.3). +#[derive(Debug, Default)] +pub(crate) struct Conflicts { + count: u32, + last: Option, + /// When the last new address's first probe left. + started: Option, +} + +fn timer(cx: &Cx<'_>, addr: Ipv4Addr) -> Timer { + Timer::Acd(cx.iface, addr) +} + +fn record(i: &mut Interface, addr: Ipv4Addr) -> Option<&mut Address> { + i.addresses.iter_mut().find(|a| a.cidr.addr == addr) +} + +/// Schedules a tentative address's first probe: a draw in [0, PROBE_WAIT], deferred past +/// RATE_LIMIT_INTERVAL after the last probe start once MAX_CONFLICTS conflicts were seen. +pub(crate) fn start(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let now = cx.now; + if !i.up { + return; + } + if i.acd.last.is_some_and(|at| now.since(at) >= CONFLICT_RESET) { + i.acd.count = 0; + } + let mut at = now.after(cx.draws.between(Purpose::Acd, core::time::Duration::ZERO, PROBE_WAIT)); + if i.acd.count >= MAX_CONFLICTS { + if let Some(permitted) = i.acd.started.map(|s| s.after(RATE_LIMIT_INTERVAL)).filter(|p| *p > at) { + at = permitted; + cx.log.count(Counter::AcdRateLimited); + } + } + cx.timers.arm(timer(cx, addr), at); +} + +fn announce(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, owed: bool) { + if let Some(Address { phase: Phase::Usable { queued, .. }, .. }) = record(i, addr).filter(|_| owed) { + *queued = true; + } + if !cx.control.push(Item::Announce { iface: cx.iface, addr, owed }, cx.log) { + announced(i, cx, addr, owed); + } +} + +/// A deadline of `addr`: the next probe, the end of ANNOUNCE_WAIT, or an owed announcement. +pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let first = i.usable().next().is_none(); + let Some(a) = record(i, addr) else { return }; + match a.phase { + Phase::Tentative { probes, queued: false } if probes < PROBE_NUM => { + a.phase = Phase::Tentative { probes, queued: true }; + if !cx.control.push(Item::Probe { iface: cx.iface, addr }, cx.log) { + probed(i, cx, addr); + } + } + Phase::Tentative { probes, .. } if probes >= PROBE_NUM => { + a.phase = Phase::Usable { assigned: false, owed: ANNOUNCE_NUM, queued: false }; + announce(i, cx, addr, true); + cx.log.count(Counter::AcdVerified); + cx.log.event(Event::Verified { iface: cx.iface, addr }); + cx.bump(); + route::refresh_active(i, cx); + if first { + igmp::rejoin(i, cx); + } + } + Phase::Usable { owed, queued: false, .. } if owed > 0 => announce(i, cx, addr, true), + Phase::Tentative { .. } | Phase::Usable { .. } => {} + } +} + +/// A probe for `addr` left, or was lost. +pub(crate) fn probed(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let now = cx.now; + let Some(Address { phase: Phase::Tentative { probes, queued: queued @ true }, .. }) = record(i, addr) else { return }; + *queued = false; + *probes = probes.saturating_add(1); + let wait = if *probes < PROBE_NUM { cx.draws.between(Purpose::Acd, PROBE_MIN, PROBE_MAX) } else { ANNOUNCE_WAIT }; + if *probes == 1 { + i.acd.started = Some(now); + } + cx.timers.arm(timer(cx, addr), now.after(wait)); +} + +/// An announcement of `addr` left, or was lost; a defence owes nothing. +pub(crate) fn announced(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, owed: bool) { + let now = cx.now; + let Some(Address { phase: Phase::Usable { assigned, owed: left, queued: queued @ true }, .. }) = record(i, addr).filter(|_| owed) else { return }; + *queued = false; + *left = left.saturating_sub(1); + if *left > 0 { + cx.timers.arm(timer(cx, addr), now.after(ANNOUNCE_INTERVAL)); + } else { + *assigned = true; + } +} + +/// Removes `addr` after a conflict, and tells the shell which kind. +fn lose(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + let now = cx.now; + let Some(index) = i.addresses.iter().position(|a| a.cidr.addr == addr) else { return }; + let removed = i.addresses.remove(index); + cx.timers.cancel(timer(cx, addr)); + cx.log.refuse(Counter::AcdConflict, cx.iface, Peer::Arp { ip: addr, mac }); + i.acd.count = i.acd.count.saturating_add(1); + i.acd.last = Some(now); + if removed.usable() { + cx.log.event(Event::Lost { iface: cx.iface, addr, mac }); + route::withdraw_off_link(i, cx); + cx.bump(); + } else { + cx.log.event(Event::Conflict { iface: cx.iface, addr, mac }); + } +} + +/// An ARP packet from another MAC naming `addr`, one of ours, as its sender; or a probe for it +/// while it is tentative (§7.2 (3), RFC 5227 §2.1.1, §2.4). +pub(crate) fn conflict(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + let now = cx.now; + let Some(a) = record(i, addr) else { return }; + match a.phase { + Phase::Tentative { .. } => lose(i, cx, addr, mac), + Phase::Usable { .. } if a.defended.is_some_and(|at| now.since(at) <= DEFEND_INTERVAL) => lose(i, cx, addr, mac), + Phase::Usable { .. } => { + a.defended = Some(now); + cx.log.refuse(Counter::AcdDefended, cx.iface, Peer::Arp { ip: addr, mac }); + announce(i, cx, addr, false); + } + } +} + +/// The link came up: held addresses stay usable and are announced twice, and probing starts for +/// any address added while it was down (§8.6). +pub(crate) fn link_up(i: &mut Interface, cx: &mut Cx<'_>) { + let addrs: alloc::vec::Vec<(Ipv4Addr, bool)> = i.addresses.iter().map(|a| (a.cidr.addr, a.usable())).collect(); + for (addr, usable) in addrs { + if usable { + if let Some(Address { phase: Phase::Usable { owed, .. }, .. }) = record(i, addr) { + *owed = ANNOUNCE_NUM; + } + announce(i, cx, addr, true); + } else { + start(i, cx, addr); + } + } +} + +/// The link went down: probing is abandoned and its addresses removed, unverified (§6.10). +pub(crate) fn link_down(i: &mut Interface, cx: &mut Cx<'_>) { + let iface = cx.iface; + for a in &mut i.addresses { + cx.timers.cancel(Timer::Acd(iface, a.cidr.addr)); + if let Phase::Usable { owed, queued, .. } = &mut a.phase { + *owed = 0; + *queued = false; + } + } + let tentative = i.addresses.iter().filter(|a| !a.usable()).map(|a| a.cidr.addr); + for addr in tentative.collect::>() { + cx.log.event(Event::NotVerified { iface, addr }); + } + i.addresses.retain(Address::usable); +} diff --git a/toyos-net-ip/src/addr.rs b/toyos-net-ip/src/addr.rs new file mode 100644 index 00000000000..b25fac3fcd6 --- /dev/null +++ b/toyos-net-ip/src/addr.rs @@ -0,0 +1,90 @@ +//! An interface's address records, and the address classes every rule reads (RFC 1122 +//! §3.2.1.3, RFC 6890, RFC 3021). + +use core::net::Ipv4Addr; + +use toyos_net_wire::Instant; + +/// Not 0.0.0.0/8, 127.0.0.0/8, 224.0.0.0/4 or 240.0.0.0/4 (which holds 255.255.255.255): an +/// address that can name one host. +pub(crate) const fn is_host(addr: Ipv4Addr) -> bool { + let [first, ..] = addr.octets(); + !(first == 0 || first == 127 || first >= 224) +} + +/// 240.0.0.0/4 but the limited broadcast. +pub(crate) const fn is_class_e(addr: Ipv4Addr) -> bool { + let [first, ..] = addr.octets(); + first >= 240 && !addr.is_broadcast() +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Cidr { + pub addr: Ipv4Addr, + /// 1 to 32. + pub len: u8, +} + +impl Cidr { + fn mask(self) -> u32 { + u32::MAX.checked_shl(u32::from(32u8.saturating_sub(self.len))).unwrap_or(0) + } + + pub fn contains(self, other: Ipv4Addr) -> bool { + (u32::from(self.addr) ^ u32::from(other)) & self.mask() == 0 + } + + pub fn network(self) -> Ipv4Addr { + Ipv4Addr::from(u32::from(self.addr) & self.mask()) + } + + /// Host bits all one; a /31 or /32 has none (RFC 3021 §2.2.1). + pub fn broadcast(self) -> Option { + (self.len <= 30).then(|| Ipv4Addr::from(u32::from(self.addr) | !self.mask())) + } + + /// The network or directed-broadcast address of a prefix of /30 or shorter, which no host + /// may use. + pub fn is_edge(self, addr: Ipv4Addr) -> bool { + self.len <= 30 && self.contains(addr) && (addr == self.network() || Some(addr) == self.broadcast()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AddrState { + /// Probed for conflicts: not a source, not for us, answers no ARP. + Tentative, + /// Usable from its first announcement (RFC 5227 §2.3). + Announcing, + Assigned, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Phase { + /// Probing: probes gone so far, and whether one waits to leave. + Tentative { probes: u8, queued: bool }, + /// Usable, with the announcements still owed and whether one waits to leave. + Usable { assigned: bool, owed: u8, queued: bool }, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Address { + pub cidr: Cidr, + pub phase: Phase, + /// The last conflicting packet this address was defended against (RFC 5227 §2.4 (b)). + pub defended: Option, +} + +impl Address { + pub fn state(&self) -> AddrState { + match self.phase { + Phase::Tentative { .. } => AddrState::Tentative, + Phase::Usable { assigned: false, .. } => AddrState::Announcing, + Phase::Usable { assigned: true, .. } => AddrState::Assigned, + } + } + + pub fn usable(&self) -> bool { + matches!(self.phase, Phase::Usable { .. }) + } +} diff --git a/toyos-net-ip/src/arp.rs b/toyos-net-ip/src/arp.rs new file mode 100644 index 00000000000..35c0958136f --- /dev/null +++ b/toyos-net-ip/src/arp.rs @@ -0,0 +1,58 @@ +//! ARP reception (§7): `wire.md` §4.3's policy, then §7.2's classification in order. Only our own +//! need to send, and a request for one of our addresses from an on-link host, create an entry; +//! any other packet can confirm, assert a MAC into STALE, or be ignored, and a MAC change is +//! always logged (§7.3). + +use toyos_net_wire::arp::{Arp, Operation}; +use toyos_net_wire::ethernet::MacClass; + +use crate::addr::is_host; +use crate::counters::Counter; +use crate::iface::{Cx, Interface}; +use crate::{acd, egress, nud}; + +pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) { + let (sender, mac, target) = (arp.sender_ip, arp.sender_mac, arp.target_ip); + if mac.class() != MacClass::Individual { + return cx.log.count(Counter::ArpGroupSenderHardware); + } + let probe = arp.operation == Operation::Request && sender.is_unspecified(); + if !(probe || is_host(sender)) { + return cx.log.count(Counter::ArpInvalidSenderAddress); + } + if mac == i.mac.get() { + return cx.log.count(Counter::ArpOwnSender); + } + if i.usable().any(|a| a.cidr.is_edge(sender)) { + return cx.log.count(Counter::ArpInvalidSenderAddress); + } + if !probe && i.owns(sender) { + return acd::conflict(i, cx, sender, mac); + } + if probe && i.owns(target) && !i.is_usable(target) { + return acd::conflict(i, cx, target, mac); + } + let for_us = arp.operation == Operation::Request && i.is_usable(target); + let mut noticed = for_us; + if !probe { + if !i.on_link(sender) { + cx.log.count(Counter::ArpSenderOffLink); + noticed = true; + } else if i.neighbours.contains_key(&sender) { + if arp.operation == Operation::Reply && nud::solicits(i, sender) && i.is_usable(target) { + nud::confirm(i, cx, sender, mac); + } else { + nud::assert(i, cx, sender, mac); + } + noticed = true; + } else if for_us { + nud::learn(i, cx, sender, mac); + } + } + if for_us { + egress::reply(cx, arp, target); + } + if !noticed { + cx.log.count(Counter::ArpNotForUs); + } +} diff --git a/toyos-net-ip/src/config.rs b/toyos-net-ip/src/config.rs new file mode 100644 index 00000000000..e823b7f9a6c --- /dev/null +++ b/toyos-net-ip/src/config.rs @@ -0,0 +1,181 @@ +//! What the shell and the transports tell [ip]: addresses, link state, group membership, +//! reachability advice and a flow's need for its next hop; and time. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_wire::ipv4::MulticastAddr; +use toyos_net_wire::Instant; + +use crate::addr::{is_host, Address, Cidr, Phase}; +use crate::counters::Counter; +use crate::nud::Link; +use crate::route::{self, NextHop, Source}; +use crate::timers::Timer; +use crate::{acd, igmp, limits, nud, Advice, IfIndex, IgmpMode, Ip, Resolution}; + +impl Ip { + fn unknown(&mut self) -> Counter { + self.log.count(Counter::UnknownInterface); + Counter::UnknownInterface + } + + /// Adds `addr/prefix_len` to `iface` as tentative: conflict detection runs before it is used + /// (RFC 5227 §2.1). The address already held with another prefix length takes the new one + /// and keeps its state, which is how a renewal arrives (§2.2 (4)). + pub fn add_address(&mut self, now: Instant, iface: IfIndex, addr: Ipv4Addr, prefix_len: u8) -> Result<(), Counter> { + let now = self.clock(now); + let cidr = Cidr { addr, len: prefix_len }; + let refusal = if !(1..=32).contains(&prefix_len) { + Some(Counter::AddrPrefixInvalid) + } else if !is_host(addr) || cidr.is_edge(addr) { + Some(Counter::AddrNotUnicast) + } else if self.ifaces.iter().enumerate().any(|(n, i)| n != iface.0 && i.owns(addr)) { + Some(Counter::AddrDuplicate) + } else { + None + }; + if let Some(refusal) = refusal { + self.log.count(refusal); + return Err(refusal); + } + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + if let Some(held) = i.addresses.iter_mut().find(|a| a.cidr.addr == addr) { + if held.cidr.len != prefix_len { + held.cidr.len = prefix_len; + if held.usable() { + route::withdraw_off_link(i, &mut cx); + cx.bump(); + } + } + return Ok(()); + } + if i.addresses.len() >= limits::ADDRS { + cx.log.count(Counter::AddrTooMany); + return Err(Counter::AddrTooMany); + } + i.addresses.push(Address { cidr, phase: Phase::Tentative { probes: 0, queued: false }, defended: None }); + acd::start(i, &mut cx, addr); + Ok(()) + } + + /// Removes one of the interface's addresses; gateways that leave every usable prefix go with + /// it (§3.2). + pub fn remove_address(&mut self, now: Instant, iface: IfIndex, addr: Ipv4Addr) -> Result<(), Counter> { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + let Some(index) = i.addresses.iter().position(|a| a.cidr.addr == addr) else { + cx.log.count(Counter::AddrUnknown); + return Err(Counter::AddrUnknown); + }; + let removed = i.addresses.remove(index); + cx.timers.cancel(Timer::Acd(iface, addr)); + if removed.usable() { + route::withdraw_off_link(i, &mut cx); + cx.bump(); + } + Ok(()) + } + + pub fn link_up(&mut self, now: Instant, iface: IfIndex) -> Result<(), Counter> { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + if !i.up { + i.up = true; + cx.bump(); + acd::link_up(i, &mut cx); + igmp::rejoin(i, &mut cx); + } + Ok(()) + } + + /// Every neighbour entry goes, probing is abandoned, IGMP stops and nothing waits to leave; + /// addresses and gateways stay (§6.10). + pub fn link_down(&mut self, now: Instant, iface: IfIndex) -> Result<(), Counter> { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + if i.up { + i.up = false; + cx.bump(); + nud::flush(i, &mut cx); + acd::link_down(i, &mut cx); + igmp::link_down(i, &mut cx); + cx.control.purge(iface); + route::refresh_active(i, &mut cx); + } + Ok(()) + } + + pub fn join(&mut self, now: Instant, iface: IfIndex, group: MulticastAddr) -> Result<(), Counter> { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + igmp::join(i, &mut cx, group) + } + + pub fn leave(&mut self, now: Instant, iface: IfIndex, group: MulticastAddr) -> Result<(), Counter> { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; + igmp::leave(i, &mut cx, group); + Ok(()) + } + + pub fn joined(&self, iface: IfIndex, group: MulticastAddr) -> bool { + self.ifaces.get(iface.0).is_some_and(|i| i.igmp.joined(group)) + } + + pub fn igmp_mode(&self, iface: IfIndex) -> Option { + self.ifaces.get(iface.0).map(|i| i.igmp.mode()) + } + + /// A transport's advice about its peer `remote`; it lands on the next hop's entry, the + /// gateway's for an off-link peer (§6.9). + pub fn advise(&mut self, now: Instant, remote: Ipv4Addr, advice: Advice) { + let now = self.clock(now); + let Ok(route) = route::lookup(&self.ifaces, remote, Source::Any, None) else { return }; + let NextHop::Neighbour(next_hop) = route.next_hop else { return }; + let Some((i, mut cx)) = self.split(now, route.iface) else { return }; + nud::advise(i, &mut cx, next_hop, advice == Advice::Confirmed); + } + + /// Whether a flow may build a segment for `next_hop` now (§6.7): a flow never parks one in + /// [ip], it waits for [`crate::Event::Resolved`] or [`crate::Event::Failed`]. + pub fn resolve(&mut self, now: Instant, iface: IfIndex, next_hop: Ipv4Addr) -> Resolution { + let now = self.clock(now); + let Some((i, mut cx)) = self.split(now, iface) else { return Resolution::Failed }; + if !i.up { + return Resolution::Pending; + } + match nud::send(i, &mut cx, next_hop, None) { + Link::Resolved(mac) => Resolution::Resolved(mac), + Link::Pending => Resolution::Pending, + Link::Failed(_) => Resolution::Failed, + } + } + + pub fn next_deadline(&self) -> Option { + self.timers.next() + } + + /// Processes every deadline at or before `now`, in deadline order, each state object once. + pub fn fire(&mut self, now: Instant) { + let now = self.clock(now); + let mut reports: BTreeMap> = BTreeMap::new(); + for timer in self.timers.due(now) { + let iface = match timer { + Timer::Neighbour(iface, _) | Timer::Acd(iface, _) | Timer::Igmp(iface, _) => iface, + }; + let Some((i, mut cx)) = self.split(now, iface) else { continue }; + match timer { + Timer::Neighbour(_, addr) => nud::fire(i, &mut cx, addr), + Timer::Acd(_, addr) => acd::fire(i, &mut cx, addr), + Timer::Igmp(_, t) => igmp::fire(i, &mut cx, t, reports.entry(iface).or_default()), + } + } + for (iface, records) in reports { + if let Some((i, mut cx)) = self.split(now, iface) { + igmp::flush(i, &mut cx, records); + } + } + } +} diff --git a/toyos-net-ip/src/counters.rs b/toyos-net-ip/src/counters.rs new file mode 100644 index 00000000000..c20e785e6e1 --- /dev/null +++ b/toyos-net-ip/src/counters.rs @@ -0,0 +1,144 @@ +//! Every refusal, every drop and every ordinary event has a counter named `.`. A +//! refusal of legacy or insecure input, and every change of a neighbour's MAC, is also an event +//! naming the rule and the peer. A rule a `toyos-net-wire` parse refused is counted under that +//! reason's own name. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use toyos_net_wire::icmp::IcmpError; + +use crate::{limits, Event, IfIndex, Peer, Refusal}; + +toyos_net_wire::counters! { + ClockRegressed = "clock.regressed"; + UnknownInterface = "ip.unknown-interface"; + AddrPrefixInvalid = "addr.prefix-invalid"; + AddrNotUnicast = "addr.not-unicast"; + AddrDuplicate = "addr.duplicate"; + AddrTooMany = "addr.too-many"; + AddrUnknown = "addr.unknown"; + RouteGatewayInvalid = "route.gateway-invalid"; + RouteGatewayIsLocal = "route.gateway-is-local"; + RouteGatewayOffLink = "route.gateway-off-link"; + RouteTooManyGateways = "route.too-many-gateways"; + RouteGatewayWithdrawn = "route.gateway-withdrawn"; + RouteGatewaySwitched = "route.gateway-switched"; + RouteInvalidDestination = "route.invalid-destination"; + RouteLocalDestination = "route.local-destination"; + RouteAmbiguousInterface = "route.ambiguous-interface"; + RouteNone = "route.none"; + RouteNoSourceAddress = "route.no-source-address"; + EthIpv6 = "eth.ipv6"; + EthUnknownType = "eth.unknown-type"; + EthNotForUs = "eth.not-for-us"; + EthVlan = "eth.vlan"; + EthOwnSource = "eth.own-source"; + IpUnicastInLinkBroadcast = "ip.unicast-in-link-broadcast"; + IpUnicastInLinkMulticast = "ip.unicast-in-link-multicast"; + IpMartianDestination = "ip.martian-destination"; + IpTentativeDestination = "ip.tentative-destination"; + IpNotForUs = "ip.not-for-us"; + IpInvalidSource = "ip.invalid-source"; + IpOwnSource = "ip.own-source"; + IpFragment = "ip.fragment", logged; + IpSourceRoute = "ip.source-route", logged; + IpProtocol = "ip.protocol"; + IpTcpNotUnicast = "ip.tcp-not-unicast"; + IpExceedsMtu = "ip.exceeds-mtu"; + IpControlQueueFull = "ip.control-queue-full"; + IpAcquisitionAdmitted = "ip.acquisition-admitted"; + IpEventOverflow = "ip.event-overflow"; + ArpOwnSender = "arp.own-sender"; + ArpInvalidSenderAddress = "arp.invalid-sender-address"; + ArpGroupSenderHardware = "arp.group-sender-hardware"; + ArpSenderOffLink = "arp.sender-off-link"; + ArpNotForUs = "arp.not-for-us"; + ArpMacChanged = "arp.mac-changed", logged; + ArpOverrideLocked = "arp.override-locked", logged; + ArpRequestsSent = "arp.requests-sent"; + ArpRepliesSent = "arp.replies-sent"; + NbPendingOverflow = "nb.pending-overflow"; + NbPendingFull = "nb.pending-full"; + NbFailedRefused = "nb.failed-refused"; + NbTableFull = "nb.table-full"; + NbResolved = "nb.resolved"; + NbFailed = "nb.failed"; + NbUnreachable = "nb.unreachable"; + AcdConflict = "acd.conflict", logged; + AcdDefended = "acd.defended", logged; + AcdRateLimited = "acd.rate-limited"; + AcdVerified = "acd.verified"; + IcmpEchoToGroup = "icmp.echo-to-group"; + IcmpErrorToGroup = "icmp.error-to-group"; + IcmpEchoReply = "icmp.echo-reply"; + IcmpEchoReplyDropped = "icmp.echo-reply-dropped"; + IcmpErrorSuppressed = "icmp.error-suppressed"; + IcmpErrorRateLimited = "icmp.error-rate-limited"; + IcmpQuoteNotOurs = "icmp.quote-not-ours"; + IcmpQuoteNonInitialFragment = "icmp.quote-non-initial-fragment"; + IcmpQuoteIcmp = "icmp.quote-icmp"; + IcmpQuoteIgmp = "icmp.quote-igmp"; + IcmpQuoteOtherProtocol = "icmp.quote-other-protocol"; + IcmpQuoteShort = "icmp.quote-short"; + IcmpRedirect = "icmp.redirect", logged; + IcmpTimestampRequest = IcmpError::TimestampRequest.name(), logged; + IcmpSourceQuench = IcmpError::SourceQuench.name(), logged; + IcmpEchoRepliesSent = "icmp.echo-replies-sent"; + IcmpErrorsSent = "icmp.errors-sent"; + IgmpV1Query = "igmp.v1-query", logged; + IgmpQueryNoRouterAlert = "igmp.query-no-router-alert", logged; + IgmpGeneralQueryDestination = "igmp.general-query-destination", logged; + IgmpQueryDestination = "igmp.query-destination"; + IgmpQueryOtherGroup = "igmp.query-other-group"; + IgmpTooManyGroups = "igmp.too-many-groups"; + IgmpIgnored = "igmp.ignored"; + IgmpReportsSent = "igmp.reports-sent"; + IgmpModeV2 = "igmp.mode-v2"; + IgmpModeV3 = "igmp.mode-v3"; +} + +#[derive(Debug, Default)] +pub(crate) struct Log { + pub counters: Counters, + pub wire: BTreeMap<&'static str, u64>, + events: Vec, + refusals: usize, +} + +impl Log { + pub fn count(&mut self, counter: Counter) { + self.counters.add(counter, 1); + } + + pub fn wire(&mut self, name: &'static str) { + let n = self.wire.entry(name).or_insert(0); + *n = n.saturating_add(1); + } + + /// Counts a refusal, and names it for the log when its rule is one the log carries. Past + /// [`limits::EVENTS`] undrained refusals one is counted and not named: a peer never grows the + /// list. + pub fn refuse(&mut self, rule: Counter, iface: IfIndex, peer: Peer) { + self.count(rule); + if !rule.logged() { + return; + } + if self.refusals >= limits::EVENTS { + self.count(Counter::IpEventOverflow); + return; + } + self.refusals = self.refusals.saturating_add(1); + self.events.push(Event::Refused(Refusal { rule, iface, peer })); + } + + /// A result the shell acts on; each is bounded by the work that made it. + pub fn event(&mut self, event: Event) { + self.events.push(event); + } + + pub fn drain(&mut self) -> alloc::vec::Drain<'_, Event> { + self.refusals = 0; + self.events.drain(..) + } +} diff --git a/toyos-net-ip/src/draw.rs b/toyos-net-ip/src/draw.rs new file mode 100644 index 00000000000..595ffefdd62 --- /dev/null +++ b/toyos-net-ip/src/draw.rs @@ -0,0 +1,67 @@ +//! Every random value [ip] needs is SipHash-2-4 over (secret, purpose, the purpose's counter) +//! (§1.3): one secret makes the layer deterministic, and without it no draw is predictable. + +use core::time::Duration; + +use toyos_net_wire::siphash::{siphash24, Key}; + +use crate::limits; + +#[derive(Clone, Copy)] +pub(crate) enum Purpose { + Reachable, + Acd, + Igmp, +} + +pub(crate) fn nanos(d: Duration) -> u64 { + u64::try_from(d.as_nanos()).unwrap_or(u64::MAX) +} + +pub(crate) struct Draws { + key: Key, + reachable: u64, + acd: u64, + igmp: u64, +} + +impl Draws { + pub fn new(key: Key) -> Self { + Self { key, reachable: 0, acd: 0, igmp: 0 } + } + + fn next(&mut self, purpose: Purpose) -> u64 { + let (byte, counter) = match purpose { + Purpose::Reachable => (1, &mut self.reachable), + Purpose::Acd => (2, &mut self.acd), + Purpose::Igmp => (3, &mut self.igmp), + }; + let [a, b, c, d, e, f, g, h] = counter.to_le_bytes(); + *counter = counter.wrapping_add(1); + siphash24(&self.key, &[byte, a, b, c, d, e, f, g, h]) + } + + /// Uniform over `[low, high]`. + pub fn between(&mut self, purpose: Purpose, low: Duration, high: Duration) -> Duration { + let low = nanos(low); + let span = nanos(high).saturating_sub(low).saturating_add(1); + Duration::from_nanos(low.saturating_add(self.next(purpose).checked_rem(span).unwrap_or(0))) + } + + /// Uniform over `(0, high]`: never at once. + pub fn after(&mut self, purpose: Purpose, high: Duration) -> Duration { + self.between(purpose, Duration::from_nanos(1), high.max(Duration::from_nanos(1))) + } + + /// RFC 4861 §6.3.2: 0.5 to 1.5 times BASE_REACHABLE_TIME. + pub fn reachable_time(&mut self) -> Duration { + self.between(Purpose::Reachable, limits::nud::REACHABLE_MIN, limits::nud::REACHABLE_MAX) + } + + /// The error limiter's key, derived so the two keyed functions reveal nothing of each other. + pub fn limiter_key(&self) -> Key { + let [a, b, c, d, e, f, g, h] = siphash24(&self.key, &[4, 0]).to_le_bytes(); + let [i, j, k, l, m, n, o, p] = siphash24(&self.key, &[4, 1]).to_le_bytes(); + [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] + } +} diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs new file mode 100644 index 00000000000..e8fae255b23 --- /dev/null +++ b/toyos-net-ip/src/egress.rs @@ -0,0 +1,340 @@ +//! Egress (§5). [ip]'s own frames wait in one FIFO the transmit opportunity drains ahead of the +//! data flows: an ARP or IGMP frame is built when it leaves, from the state of that moment, and +//! the timer it feeds starts then. The FIFO holds at most CONTROL_QUEUE of [ip]'s own frames, at +//! most ECHO_REPLIES of them echo replies; a frame past either is dropped and counted, and its +//! producer moves on as if it had left. A datagram released by resolution is bounded where it +//! was held and is never dropped here. +//! +//! Every datagram is atomic: DF set, identification 0 (`toyos-net-wire`'s one IPv4 form). + +use alloc::collections::VecDeque; +use alloc::vec; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_wire::arp::{Arp, Operation}; +use toyos_net_wire::ethernet::{FrameBuilder, FrameBody, IndividualMac, MacAddr}; +use toyos_net_wire::ipv4::{Ipv4Builder, Ipv4Source, Payload, TrafficClass, Ttl}; +use toyos_net_wire::udp::UdpBuilder; +use toyos_net_wire::Instant; + +use crate::counters::{Counter, Log}; +use crate::iface::Cx; +use crate::limits::{nud::PENDING_TOTAL, CONTROL_QUEUE, ECHO_REPLIES}; +use crate::nud::{self, Held, Link, Nud}; +use crate::route::{NextHop, Route, Source}; +use crate::{acd, igmp, Event, Flow, IfIndex, Ip, MTU}; + +/// The largest frame [ip] builds. +pub const FRAME: usize = toyos_net_wire::ethernet::HEADER_LEN + MTU; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FrameKind { + Echo, + Error, + Datagram, +} + +#[derive(Debug)] +pub(crate) enum Item { + Request { iface: IfIndex, target: Ipv4Addr, to: Option }, + Reply { iface: IfIndex, to: MacAddr, target: Ipv4Addr, ours: Ipv4Addr }, + Probe { iface: IfIndex, addr: Ipv4Addr }, + Announce { iface: IfIndex, addr: Ipv4Addr, owed: bool }, + Igmp { iface: IfIndex, report: igmp::Report }, + Frame { iface: IfIndex, frame: Vec, kind: FrameKind }, + Released { iface: IfIndex, frame: Vec, kind: FrameKind }, +} + +impl Item { + fn iface(&self) -> IfIndex { + match self { + Self::Request { iface, .. } + | Self::Reply { iface, .. } + | Self::Probe { iface, .. } + | Self::Announce { iface, .. } + | Self::Igmp { iface, .. } + | Self::Frame { iface, .. } + | Self::Released { iface, .. } => *iface, + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct Control { + items: VecDeque, + own: usize, + echoes: usize, +} + +impl Control { + /// Queues one of [ip]'s own frames; `false` when it was dropped and counted. + pub fn push(&mut self, item: Item, log: &mut Log) -> bool { + let echo = matches!(item, Item::Frame { kind: FrameKind::Echo, .. }); + if echo && self.echoes >= ECHO_REPLIES { + log.count(Counter::IcmpEchoReplyDropped); + return false; + } + if self.own >= CONTROL_QUEUE { + log.count(Counter::IpControlQueueFull); + return false; + } + self.own = self.own.saturating_add(1); + if echo { + self.echoes = self.echoes.saturating_add(1); + } + self.items.push_back(item); + true + } + + /// A datagram resolution released, in the order it was held. + pub fn release(&mut self, iface: IfIndex, frame: Vec, kind: FrameKind) { + self.items.push_back(Item::Released { iface, frame, kind }); + } + + fn pop(&mut self) -> Option { + let item = self.items.pop_front()?; + self.forget(&item); + Some(item) + } + + fn forget(&mut self, item: &Item) { + match item { + Item::Released { .. } => {} + Item::Frame { kind: FrameKind::Echo, .. } => { + self.own = self.own.saturating_sub(1); + self.echoes = self.echoes.saturating_sub(1); + } + _ => self.own = self.own.saturating_sub(1), + } + } + + /// Drops everything waiting for `iface`, whose link went down. + pub fn purge(&mut self, iface: IfIndex) { + let (gone, kept): (Vec, Vec) = self.items.drain(..).partition(|item| item.iface() == iface); + for item in &gone { + self.forget(item); + } + self.items = kept.into(); + } +} + +/// A UDP datagram a transport hands [ip] at a transmit opportunity. +#[derive(Clone, Copy, Debug)] +pub struct UdpOut<'a> { + /// The socket's address, or 0.0.0.0 for the acquisition socket's broadcasts. + pub source: Ipv4Addr, + pub destination: Ipv4Addr, + pub ttl: Ttl, + pub datagram: UdpBuilder<'a>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Sent { + /// This many bytes of the caller's buffer are the frame: one credit spent. + Frame(usize), + /// Held for its next hop; it leaves from [`Ip::transmit`] once resolved. + Held, +} + +fn build(source: IndividualMac, destination: MacAddr, body: &B, out: &mut [u8]) -> Option { + FrameBuilder { destination, source }.emit(body, out).ok().map(<[u8]>::len) +} + +fn build_vec(source: IndividualMac, destination: MacAddr, body: &B) -> Option> { + let mut frame = vec![0; FRAME]; + let len = build(source, destination, body, &mut frame)?; + frame.truncate(len); + Some(frame) +} + +impl Ip { + /// A transmit opportunity with room for `credit` frames: [ip]'s own frames and the datagrams + /// resolution released, in the order they were queued, each handed to `sink` with its + /// interface. Returns how many left. + pub fn transmit(&mut self, now: Instant, credit: usize, mut sink: impl FnMut(IfIndex, &[u8])) -> usize { + let now = self.clock(now); + let mut sent = 0usize; + while sent < credit { + let Some(item) = self.control.pop() else { break }; + if self.emit(now, item, &mut sink) { + sent = sent.saturating_add(1); + } + } + sent + } + + /// Builds one item into a frame and hands it over; `false` when the state it was queued for + /// is gone and nothing left. + fn emit(&mut self, now: Instant, item: Item, sink: &mut impl FnMut(IfIndex, &[u8])) -> bool { + let iface = item.iface(); + let Some((i, mut cx)) = self.split(now, iface) else { return false }; + let mac = i.mac; + let mut arp = |arp: &Arp, to: MacAddr| -> bool { + let mut frame = [0u8; 60]; + match build(mac, to, arp, &mut frame).and_then(|len| frame.get(..len)) { + Some(frame) => { + sink(iface, frame); + true + } + None => false, + } + }; + match item { + Item::Request { target, to, .. } => { + let Some(n) = i.neighbours.get(&target).filter(|n| n.queued) else { return false }; + let sender = n.hint.filter(|h| i.is_usable(*h)).or_else(|| i.source_for(target)); + nud::request_left(i, &mut cx, target); + let left = sender.is_some_and(|s| arp(&Arp::request(mac, s, target), to.unwrap_or(MacAddr::BROADCAST))); + if left { + cx.log.count(Counter::ArpRequestsSent); + } + left + } + Item::Reply { to, target, ours, .. } => { + if !i.is_usable(ours) { + return false; + } + let reply = Arp { operation: Operation::Reply, sender_mac: mac.get(), sender_ip: ours, target_mac: to, target_ip: target }; + let left = arp(&reply, to); + if left { + cx.log.count(Counter::ArpRepliesSent); + } + left + } + Item::Probe { addr, .. } => { + let queued = i.addresses.iter().any(|a| a.cidr.addr == addr && matches!(a.phase, crate::addr::Phase::Tentative { queued: true, .. })); + if !queued { + return false; + } + acd::probed(i, &mut cx, addr); + arp(&Arp::probe(mac, addr), MacAddr::BROADCAST) + } + Item::Announce { addr, owed, .. } => { + if !i.is_usable(addr) { + return false; + } + acd::announced(i, &mut cx, addr, owed); + arp(&Arp::announcement(mac, addr), MacAddr::BROADCAST) + } + Item::Igmp { report, .. } => { + let mut frame = [0u8; FRAME]; + match igmp::emit(i, &mut cx, &report, &mut frame).and_then(|len| frame.get(..len)) { + Some(frame) => { + sink(iface, frame); + true + } + None => false, + } + } + Item::Frame { frame, kind, .. } => { + sink(iface, &frame); + sent(&mut cx, kind); + true + } + Item::Released { frame, kind, .. } => { + i.held = i.held.saturating_sub(1); + sink(iface, &frame); + sent(&mut cx, kind); + true + } + } + } + + /// A UDP datagram at a transmit opportunity (§5): written into `frame` when its link + /// destination is known, or held here for its next hop, which spends no credit. A refusal is + /// counted and the datagram's flow is told it is unreachable. + pub fn send_udp(&mut self, now: Instant, out: &UdpOut<'_>, frame: &mut [u8; FRAME]) -> Result { + let now = self.clock(now); + let flow = Flow { + source: out.source, + source_port: out.datagram.source, + destination: out.destination, + destination_port: out.datagram.destination, + }; + let source = if out.source.is_unspecified() { Source::Unspecified } else { Source::Bound(out.source) }; + let sent = self.route(out.destination, source, None).and_then(|route| { + let builder = Ipv4Builder { + source: Ipv4Source::new(route.source).map_err(|_| Counter::RouteNoSourceAddress)?, + destination: out.destination, + ttl: out.ttl, + traffic_class: TrafficClass::ZERO, + options: &[], + payload: out.datagram, + }; + self.datagram(now, &route, &builder, FrameKind::Datagram, Some(flow), Some(frame)) + }); + if sent.is_err() { + self.log.event(Event::Unreachable(flow)); + } + sent + } + + /// Sends one of [ip]'s own ICMP messages along `route`, into the control queue or held for + /// its next hop. + pub(crate) fn own(&mut self, now: Instant, route: &Route, builder: &Ipv4Builder<'_, P>, kind: FrameKind) { + let _ = self.datagram(now, route, builder, kind, None, None); + } + + fn datagram( + &mut self, + now: Instant, + route: &Route, + builder: &Ipv4Builder<'_, P>, + kind: FrameKind, + flow: Option, + out: Option<&mut [u8; FRAME]>, + ) -> Result { + if builder.length().map_or(true, |len| len > MTU) { + self.log.count(Counter::IpExceedsMtu); + return Err(Counter::IpExceedsMtu); + } + let Some((i, mut cx)) = self.split(now, route.iface) else { return Err(Counter::UnknownInterface) }; + let (destination, hold) = match route.next_hop { + NextHop::Broadcast => (MacAddr::BROADCAST, None), + NextHop::Multicast(group) => (MacAddr::multicast(group), None), + NextHop::Neighbour(addr) => { + let holds = matches!(i.neighbours.get(&addr).map(|n| &n.state), None | Some(Nud::Incomplete(_))); + if holds && i.held >= PENDING_TOTAL { + cx.log.count(Counter::NbPendingFull); + return Err(Counter::NbPendingFull); + } + match nud::send(i, &mut cx, addr, Some(route.source)) { + Link::Resolved(mac) => (mac, None), + Link::Pending => (MacAddr::ZERO, Some(addr)), + Link::Failed(refusal) => return Err(refusal), + } + } + }; + let mac = i.mac; + if let Some(addr) = hold { + let frame = build_vec(mac, destination, builder).ok_or(Counter::IpExceedsMtu)?; + nud::hold(i, &mut cx, addr, Held { frame, kind, flow }); + return Ok(Sent::Held); + } + match out { + Some(out) => build(mac, destination, builder, out).map(Sent::Frame).ok_or(Counter::IpExceedsMtu), + None => { + let frame = build_vec(mac, destination, builder).ok_or(Counter::IpExceedsMtu)?; + if cx.control.push(Item::Frame { iface: route.iface, frame, kind }, cx.log) { + Ok(Sent::Held) + } else { + Err(Counter::IpControlQueueFull) + } + } + } + } +} + +fn sent(cx: &mut Cx<'_>, kind: FrameKind) { + match kind { + FrameKind::Echo => cx.log.count(Counter::IcmpEchoRepliesSent), + FrameKind::Error => cx.log.count(Counter::IcmpErrorsSent), + FrameKind::Datagram => {} + } +} + +/// Queues an ARP reply to `request`'s sender for `ours`, unicast to it (§6.4). +pub(crate) fn reply(cx: &mut Cx<'_>, request: &Arp, ours: Ipv4Addr) { + cx.control.push(Item::Reply { iface: cx.iface, to: request.sender_mac, target: request.sender_ip, ours }, cx.log); +} diff --git a/toyos-net-ip/src/icmp.rs b/toyos-net-ip/src/icmp.rs new file mode 100644 index 00000000000..2379c19ed1b --- /dev/null +++ b/toyos-net-ip/src/icmp.rs @@ -0,0 +1,151 @@ +//! ICMPv4 (§9). An echo request to one of our unicast addresses is answered and routed like any +//! datagram. An error is generated only about a datagram to one of our unicast addresses, never +//! about an error, a group or link-group destination, a non-initial fragment or a source that is +//! not one host, and only when the destination's bucket and the global one both have a token. +//! An inbound error is handed on only once its quote names one of our datagrams. + +use toyos_net_wire::ethernet::MacClass; +use toyos_net_wire::icmp::{Echo, EchoBuilder, HostUnreachable, IcmpError, IcmpMessage, IcmpPacket, Quote, UnreachableBuilder, UnreachableCode}; +use toyos_net_wire::ipv4::{Ecn, Ipv4Builder, Ipv4Packet, Ipv4Source, Protocol, TrafficClass, Ttl}; +use toyos_net_wire::{Instant, Port}; + +use crate::addr::is_host; +use crate::counters::Counter; +use crate::egress::FrameKind; +use crate::route::Source; +use crate::{Arrival, Cast, Delivery, ErrorKind, Flow, IfIndex, Ip, Peer, Transport, TransportError}; + +/// ICMP types that are errors: never answered with another (RFC 1122 §3.2.2). +const ERROR_TYPES: [u8; 5] = [3, 4, 5, 11, 12]; + +impl Ip { + pub(crate) fn icmp<'a>(&mut self, now: Instant, iface: IfIndex, cast: Cast, packet: Ipv4Packet<'a>) -> Option> { + let peer = Peer::Ip(packet.source()); + let message = match IcmpPacket::parse(packet.payload()) { + Ok(parsed) => parsed.message(), + Err(IcmpError::TimestampRequest) => { + self.log.refuse(Counter::IcmpTimestampRequest, iface, peer); + return None; + } + Err(IcmpError::SourceQuench) => { + self.log.refuse(Counter::IcmpSourceQuench, iface, peer); + return None; + } + Err(e) => { + self.log.wire(e.name()); + return None; + } + }; + let unicast = cast == Cast::Unicast; + let kind = match message { + IcmpMessage::EchoRequest(echo) if unicast => { + self.echo(now, iface, &packet, &echo); + return None; + } + IcmpMessage::EchoRequest(_) => return self.drop(Counter::IcmpEchoToGroup), + IcmpMessage::EchoReply(_) => return self.drop(Counter::IcmpEchoReply), + IcmpMessage::Redirect { .. } => { + self.log.refuse(Counter::IcmpRedirect, iface, peer); + return None; + } + IcmpMessage::DestinationUnreachable { .. } | IcmpMessage::TimeExceeded { .. } | IcmpMessage::ParameterProblem { .. } if !unicast => { + return self.drop(Counter::IcmpErrorToGroup); + } + IcmpMessage::DestinationUnreachable { code: UnreachableCode::FragmentationNeeded, next_hop_mtu, quote } => { + (ErrorKind::FragmentationNeeded { next_hop_mtu, quoted_length: quote.total_length() }, quote) + } + IcmpMessage::DestinationUnreachable { code, quote, .. } => (ErrorKind::Unreachable(code), quote), + IcmpMessage::TimeExceeded { code, quote } => (ErrorKind::TimeExceeded(code), quote), + IcmpMessage::ParameterProblem { code, pointer, quote } => (ErrorKind::ParameterProblem { code, pointer }, quote), + }; + self.attribute(iface, &packet, kind.0, &kind.1) + } + + fn drop<'a>(&mut self, counter: Counter) -> Option> { + self.log.count(counter); + None + } + + /// §9.5: the quote names one of our usable addresses, is not a later fragment, and carries a + /// TCP or UDP header from which the error's 4-tuple is read. + fn attribute<'a>(&mut self, iface: IfIndex, packet: &Ipv4Packet<'_>, kind: ErrorKind, quote: &Quote<'_>) -> Option> { + let i = self.ifaces.get(iface.0)?; + let refusal = if !i.is_usable(quote.source()) { + Some(Counter::IcmpQuoteNotOurs) + } else if quote.fragment_offset().units() != 0 { + Some(Counter::IcmpQuoteNonInitialFragment) + } else { + match quote.protocol() { + Protocol::Tcp | Protocol::Udp => None, + Protocol::Icmp => Some(Counter::IcmpQuoteIcmp), + Protocol::Igmp => Some(Counter::IcmpQuoteIgmp), + Protocol::Other(_) => Some(Counter::IcmpQuoteOtherProtocol), + } + }; + if let Some(refusal) = refusal { + return self.drop(refusal); + } + let transport = if quote.protocol() == Protocol::Tcp { Transport::Tcp } else { Transport::Udp }; + let Some(&[s0, s1, d0, d1, q0, q1, q2, q3]) = quote.transport() else { return self.drop(Counter::IcmpQuoteShort) }; + let ports = Port::new(u16::from_be_bytes([s0, s1])).zip(Port::new(u16::from_be_bytes([d0, d1]))); + let Some((source_port, destination_port)) = ports else { return self.drop(Counter::IcmpQuoteNotOurs) }; + let flow = Flow { source: quote.source(), source_port, destination: quote.destination(), destination_port }; + let sequence = (transport == Transport::Tcp).then_some(u32::from_be_bytes([q0, q1, q2, q3])); + Some(Delivery::Error(TransportError { iface, transport, flow, sequence, kind, reporter: packet.source() })) + } + + /// §9.2: the reply copies identifier, sequence, data and DSCP, carries no option, ECN 0. + fn echo(&mut self, now: Instant, iface: IfIndex, packet: &Ipv4Packet<'_>, echo: &Echo<'_>) { + let Ok(route) = self.route(packet.source(), Source::Bound(packet.destination()), Some(iface)) else { return }; + let Ok(source) = Ipv4Source::new(packet.destination()) else { return }; + let traffic_class = TrafficClass::new(packet.traffic_class().dscp(), Ecn::NotEct).unwrap_or(TrafficClass::ZERO); + let builder = Ipv4Builder { + source, + destination: packet.source(), + ttl: Ttl::DEFAULT, + traffic_class, + options: &[], + payload: EchoBuilder::reply_to(echo), + }; + self.own(now, &route, &builder, FrameKind::Echo); + } + + /// [udp] found no socket for `arrival`: a port unreachable, if suppression and the limiter + /// let one go (§9.3, §9.4). + pub fn port_unreachable(&mut self, now: Instant, arrival: &Arrival<'_>) { + let now = self.clock(now); + if arrival.cast != Cast::Unicast { + return self.log.count(Counter::IcmpErrorSuppressed); + } + self.error(now, arrival.iface, arrival.link, &arrival.packet, HostUnreachable::Port); + } + + pub(crate) fn error(&mut self, now: Instant, iface: IfIndex, link: MacClass, packet: &Ipv4Packet<'_>, code: HostUnreachable) { + let (source, destination) = (packet.source(), packet.destination()); + let about_error = packet.protocol() == Protocol::Icmp && packet.payload().first().is_some_and(|t| ERROR_TYPES.contains(t)); + let suppressed = about_error + || link != MacClass::Individual + || destination.is_broadcast() + || destination.is_multicast() + || self.is_directed_broadcast(destination) + || packet.fragment_offset().units() != 0 + || !is_host(source); + if suppressed { + return self.log.count(Counter::IcmpErrorSuppressed); + } + if !self.limiter.allow(now, source) { + return self.log.count(Counter::IcmpErrorRateLimited); + } + let Ok(route) = self.route(source, Source::Bound(destination), Some(iface)) else { return }; + let Ok(ours) = Ipv4Source::new(destination) else { return }; + let builder = Ipv4Builder { + source: ours, + destination: source, + ttl: Ttl::DEFAULT, + traffic_class: TrafficClass::ZERO, + options: &[], + payload: UnreachableBuilder { code, datagram: packet }, + }; + self.own(now, &route, &builder, FrameKind::Error); + } +} diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs new file mode 100644 index 00000000000..aadd24b77fc --- /dev/null +++ b/toyos-net-ip/src/iface.rs @@ -0,0 +1,95 @@ +//! One interface's state, and the context each operation on it borrows beside it. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use core::time::Duration; + +use toyos_net_wire::ethernet::IndividualMac; +use toyos_net_wire::Instant; + +use crate::addr::Address; +use crate::counters::Log; +use crate::draw::Draws; +use crate::egress::Control; +use crate::timers::Timers; +use crate::{acd, igmp, limits, nud, IfIndex, Ip}; + +pub(crate) struct Interface { + pub mac: IndividualMac, + pub up: bool, + /// In the order they were added. + pub addresses: Vec
, + pub gateways: Vec, + pub active: Option, + pub neighbours: BTreeMap, + /// Datagrams waiting in [ip] for this interface: held for resolution, or released and not + /// yet left. + pub held: usize, + pub reachable: Duration, + pub reachable_drawn: Instant, + pub acd: acd::Conflicts, + pub igmp: igmp::Igmp, +} + +pub(crate) struct Cx<'a> { + pub now: Instant, + pub iface: IfIndex, + pub timers: &'a mut Timers, + pub control: &'a mut Control, + pub log: &'a mut Log, + pub draws: &'a mut Draws, + pub generation: &'a mut u64, +} + +impl Cx<'_> { + pub fn bump(&mut self) { + *self.generation = self.generation.wrapping_add(1); + } +} + +impl Interface { + pub fn usable(&self) -> impl Iterator + '_ { + self.addresses.iter().filter(|a| a.usable()) + } + + pub fn is_usable(&self, addr: Ipv4Addr) -> bool { + self.usable().any(|a| a.cidr.addr == addr) + } + + pub fn owns(&self, addr: Ipv4Addr) -> bool { + self.addresses.iter().any(|a| a.cidr.addr == addr) + } + + /// Inside the prefix of a usable address: reachable without a gateway. + pub fn on_link(&self, addr: Ipv4Addr) -> bool { + self.usable().any(|a| a.cidr.contains(addr)) + } + + /// §3.5 rules 1 and 2: 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) + } + + /// The directed broadcast of one of its usable prefixes. + pub fn is_directed_broadcast(&self, addr: Ipv4Addr) -> bool { + self.usable().any(|a| a.cidr.broadcast() == Some(addr)) + } + + /// ReachableTime, redrawn once two hours have passed since the last draw (RFC 4861 §6.3.2). + pub fn reachable_time(&mut self, draws: &mut Draws, now: Instant) -> Duration { + if now.since(self.reachable_drawn) >= limits::nud::REACHABLE_REDRAW { + self.reachable = draws.reachable_time(); + self.reachable_drawn = now; + } + self.reachable + } +} + +impl Ip { + pub(crate) fn split(&mut self, now: Instant, iface: IfIndex) -> Option<(&mut Interface, Cx<'_>)> { + let Self { ifaces, timers, control, log, draws, generation, .. } = self; + let interface = ifaces.get_mut(iface.0)?; + Some((interface, Cx { now, iface, timers, control, log, draws, generation })) + } +} diff --git a/toyos-net-ip/src/igmp.rs b/toyos-net-ip/src/igmp.rs new file mode 100644 index 00000000000..423e4c3c9b2 --- /dev/null +++ b/toyos-net-ip/src/igmp.rs @@ -0,0 +1,518 @@ +//! The IGMP host (§10): IGMPv3 (RFC 9776) for any-source joins, so a joined group's state is +//! always EXCLUDE {} (RFC 5790 §4), with RFC 9776 §7.2.1's IGMPv2 compatibility mode (RFC 2236) +//! and no IGMPv1 mode: a v1 query is refused and logged. 224.0.0.1 is always joined and never +//! reported. Records due at one moment leave in one report; a state change's retransmissions +//! are timed from the hand-off of the one before. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use core::time::Duration; + +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::igmp::{self as wire, Deciseconds, GroupRecord, IgmpMessage, Query, QueryGroup, QueryVersion, ReportGroup, V2Builder, V2Kind, V3ReportBuilder}; +use toyos_net_wire::ipv4::{Ecn, Ipv4Option, Ipv4Packet, Ipv4Source, MulticastAddr, TrafficClass}; +use toyos_net_wire::Instant; + +use crate::counters::Counter; +use crate::draw::Purpose; +use crate::egress::Item; +use crate::iface::{Cx, Interface}; +use crate::limits::igmp::{QUERY_INTERVAL, ROBUSTNESS, UNSOLICITED_REPORT, V2_UNSOLICITED_REPORT}; +use crate::limits::{IGMP_GROUPS, IGMP_QUERY_SOURCES}; +use crate::timers; +use crate::{Peer, MTU}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Timer { + /// The response to a general query: the interface timer. + General, + /// A group-specific or group-and-source-specific response. + Response(MulticastAddr), + /// A state change's next transmission. + Change(MulticastAddr), + /// IGMPv2 mode: a group's report delay. + V2Report(MulticastAddr), + /// IGMPv2 mode ends. + V2Querier, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IgmpMode { + V3, + V2, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ChangeKind { + Join, + Leave, +} + +#[derive(Clone, Copy, Debug)] +struct Change { + kind: ChangeKind, + /// Transmissions still owed. + left: u8, + queued: bool, + seq: u32, +} + +#[derive(Debug, Default)] +struct Group { + refs: u32, + change: Option, + /// A pending v3 response's recorded sources: empty for a group response (§10.5). + response: Option>, + /// IGMPv2 mode: ours was the last report on the link (RFC 2236 §3). + last_reporter: bool, +} + +impl Group { + fn idle(&self) -> bool { + self.refs == 0 && self.change.is_none() + } +} + +#[derive(Debug)] +pub(crate) struct Igmp { + groups: BTreeMap, + /// IGMPv2 compatibility mode lasts until then. + v2_until: Option, + robustness: u8, + seq: u32, +} + +impl Default for Igmp { + fn default() -> Self { + Self { groups: BTreeMap::new(), v2_until: None, robustness: ROBUSTNESS, seq: 0 } + } +} + +impl Igmp { + pub fn joined(&self, group: MulticastAddr) -> bool { + group == MulticastAddr::ALL_HOSTS || self.groups.get(&group).is_some_and(|g| g.refs > 0) + } + + pub fn mode(&self) -> IgmpMode { + if self.v2_until.is_some() { + IgmpMode::V2 + } else { + IgmpMode::V3 + } + } + + fn joined_groups(&self) -> impl Iterator + '_ { + self.groups.iter().filter(|(_, g)| g.refs > 0).map(|(&group, _)| group) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Kind { + IsInclude, + IsExclude, + ToInclude, + ToExclude, +} + +#[derive(Clone, Debug)] +pub(crate) struct Record { + group: MulticastAddr, + kind: Kind, + sources: Vec, + /// A state change's transmission, accounted when it leaves. + change: Option, +} + +#[derive(Debug)] +pub(crate) enum Report { + V3(Vec), + /// A v2 report; a join's carries its change. + V2(MulticastAddr, Option), + Leave(MulticastAddr), +} + +fn timer(cx: &Cx<'_>, t: Timer) -> timers::Timer { + timers::Timer::Igmp(cx.iface, t) +} + +/// RFC 9776 §4 and W-7: internetwork control, TTL 1, Router Alert. +fn traffic_class() -> TrafficClass { + TrafficClass::new(48, Ecn::NotEct).unwrap_or(TrafficClass::ZERO) +} + +fn deciseconds(d: Deciseconds) -> Duration { + Duration::from_millis(u64::from(d.0).saturating_mul(100)) +} + +/// Room for records in one report: an MTU less the IPv4 header with its Router Alert. +const REPORT_ROOM: usize = MTU.saturating_sub(24).saturating_sub(wire::HEADER_LEN); + +/// Queues `records` in as few reports as fit the MTU. +pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>, records: Vec) { + let mut report = Vec::new(); + let mut room = REPORT_ROOM; + for record in records { + let size = record.sources.len().saturating_mul(4).saturating_add(8); + if size > room && !report.is_empty() { + let full = core::mem::take(&mut report); + enqueue(i, cx, Report::V3(full)); + room = REPORT_ROOM; + } + room = room.saturating_sub(size); + report.push(record); + } + if !report.is_empty() { + enqueue(i, cx, Report::V3(report)); + } +} + +fn enqueue(i: &mut Interface, cx: &mut Cx<'_>, report: Report) { + let changes: Vec<(MulticastAddr, u32)> = match &report { + Report::V3(records) => records.iter().filter_map(|r| r.change.map(|seq| (r.group, seq))).collect(), + Report::V2(group, Some(seq)) => alloc::vec![(*group, *seq)], + Report::V2(_, None) | Report::Leave(_) => Vec::new(), + }; + if !cx.control.push(Item::Igmp { iface: cx.iface, report }, cx.log) { + for (group, seq) in changes { + changed(i, cx, group, seq); + } + } +} + +/// A state change's transmission left, or was lost. +fn changed(i: &mut Interface, cx: &mut Cx<'_>, group: MulticastAddr, seq: u32) { + let now = cx.now; + let v2 = i.igmp.mode() == IgmpMode::V2; + let Some(g) = i.igmp.groups.get_mut(&group) else { return }; + let Some(change) = g.change.as_mut().filter(|c| c.seq == seq && c.queued) else { return }; + change.queued = false; + change.left = change.left.saturating_sub(1); + if change.left > 0 { + let interval = if v2 { V2_UNSOLICITED_REPORT } else { UNSOLICITED_REPORT }; + let at = now.after(cx.draws.after(Purpose::Igmp, interval)); + cx.timers.arm(timer(cx, Timer::Change(group)), at); + } else { + g.change = None; + if g.idle() { + i.igmp.groups.remove(&group); + } + } +} + +/// A new state for `group`: RV transmissions of its record, the first at once, replacing any +/// change still owed (RFC 9776 §5.1). +fn change(i: &mut Interface, cx: &mut Cx<'_>, group: MulticastAddr, kind: ChangeKind, batch: &mut Vec) { + let robustness = i.igmp.robustness; + let mode = i.igmp.mode(); + i.igmp.seq = i.igmp.seq.wrapping_add(1); + let seq = i.igmp.seq; + cx.timers.cancel(timer(cx, Timer::Change(group))); + let Some(g) = i.igmp.groups.get_mut(&group) else { return }; + match (mode, kind) { + (IgmpMode::V3, _) => { + g.change = Some(Change { kind, left: robustness, queued: true, seq }); + let kind = if kind == ChangeKind::Join { Kind::ToExclude } else { Kind::ToInclude }; + batch.push(Record { group, kind, sources: Vec::new(), change: Some(seq) }); + } + (IgmpMode::V2, ChangeKind::Join) => { + g.change = Some(Change { kind, left: 2, queued: true, seq }); + enqueue(i, cx, Report::V2(group, Some(seq))); + } + (IgmpMode::V2, ChangeKind::Leave) => { + let last = g.last_reporter; + g.change = None; + if g.idle() { + i.igmp.groups.remove(&group); + } + if last { + enqueue(i, cx, Report::Leave(group)); + } + } + } +} + +pub(crate) fn join(i: &mut Interface, cx: &mut Cx<'_>, group: MulticastAddr) -> Result<(), Counter> { + if group == MulticastAddr::ALL_HOSTS { + return Ok(()); + } + let joined = i.igmp.joined_groups().count(); + let g = i.igmp.groups.entry(group).or_default(); + if g.refs > 0 { + g.refs = g.refs.saturating_add(1); + return Ok(()); + } + if joined >= IGMP_GROUPS { + if g.idle() { + i.igmp.groups.remove(&group); + } + cx.log.count(Counter::IgmpTooManyGroups); + return Err(Counter::IgmpTooManyGroups); + } + g.refs = 1; + if i.up { + let mut batch = Vec::new(); + change(i, cx, group, ChangeKind::Join, &mut batch); + flush(i, cx, batch); + } + Ok(()) +} + +pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, group: MulticastAddr) { + let Some(g) = i.igmp.groups.get_mut(&group).filter(|g| g.refs > 0) else { return }; + g.refs = g.refs.saturating_sub(1); + if g.refs > 0 { + return; + } + g.response = None; + cx.timers.cancel(timer(cx, Timer::Response(group))); + cx.timers.cancel(timer(cx, Timer::V2Report(group))); + if i.up { + let mut batch = Vec::new(); + change(i, cx, group, ChangeKind::Leave, &mut batch); + flush(i, cx, batch); + } else { + i.igmp.groups.remove(&group); + } +} + +/// Every joined group's join again, as new: after the link came up, or when the interface's first +/// usable address appeared (§10.3 (6), §10.8). +pub(crate) fn rejoin(i: &mut Interface, cx: &mut Cx<'_>) { + let groups: Vec = i.igmp.joined_groups().collect(); + let mut batch = Vec::new(); + for group in groups { + change(i, cx, group, ChangeKind::Join, &mut batch); + } + flush(i, cx, batch); +} + +/// The link went down: every timer stops and the mode returns to v3 (§10.8). +pub(crate) fn link_down(i: &mut Interface, cx: &mut Cx<'_>) { + cx.timers.cancel_iface(cx.iface, |t| matches!(t, timers::Timer::Igmp(..))); + i.igmp.v2_until = None; + i.igmp.groups.retain(|_, g| g.refs > 0); + for g in i.igmp.groups.values_mut() { + g.change = None; + g.response = None; + g.last_reporter = false; + } +} + +fn router_alert(packet: &Ipv4Packet<'_>) -> bool { + packet.options().iter().any(|o| matches!(o, Ipv4Option::RouterAlert(_))) +} + +/// A received IGMP message the IP layer admitted (§10.4, §10.7). +pub(crate) fn input(i: &mut Interface, cx: &mut Cx<'_>, packet: &Ipv4Packet<'_>, message: IgmpMessage<'_>) { + match message { + IgmpMessage::Query(query) => self::query(i, cx, packet, &query), + IgmpMessage::V2Report(group) | IgmpMessage::V1Report(group) => { + let running = cx.timers.get(timer(cx, Timer::V2Report(group))).is_some(); + if i.igmp.mode() == IgmpMode::V2 && running { + cx.timers.cancel(timer(cx, Timer::V2Report(group))); + if let Some(g) = i.igmp.groups.get_mut(&group) { + g.last_reporter = false; + } + } else { + cx.log.count(Counter::IgmpIgnored); + } + } + IgmpMessage::Leave(_) => cx.log.count(Counter::IgmpIgnored), + } +} + +fn query(i: &mut Interface, cx: &mut Cx<'_>, packet: &Ipv4Packet<'_>, query: &Query<'_>) { + let now = cx.now; + let peer = Peer::Ip(packet.source()); + if query.version == QueryVersion::V1 { + return cx.log.refuse(Counter::IgmpV1Query, cx.iface, peer); + } + if !router_alert(packet) { + return cx.log.refuse(Counter::IgmpQueryNoRouterAlert, cx.iface, peer); + } + let destination = packet.destination(); + let to_us = i.is_usable(destination); + match query.group { + QueryGroup::General if !(destination == MulticastAddr::ALL_HOSTS.get() || to_us) => { + return cx.log.refuse(Counter::IgmpGeneralQueryDestination, cx.iface, peer); + } + QueryGroup::Specific(group) if !(destination == group.get() || to_us) => { + return cx.log.count(Counter::IgmpQueryDestination); + } + QueryGroup::Specific(group) if !i.igmp.joined(group) => return cx.log.count(Counter::IgmpQueryOtherGroup), + QueryGroup::General | QueryGroup::Specific(_) => {} + } + let max = deciseconds(query.max_response); + match (&query.version, query.group) { + (QueryVersion::V2, QueryGroup::General) => enter_v2(i, cx, max), + (QueryVersion::V3(v3), _) if v3.robustness != 0 => i.igmp.robustness = v3.robustness, + _ => {} + } + if i.igmp.mode() == IgmpMode::V2 { + let groups: Vec = match query.group { + QueryGroup::General => i.igmp.joined_groups().collect(), + QueryGroup::Specific(group) => alloc::vec![group], + }; + for group in groups { + let at = now.after(cx.draws.after(Purpose::Igmp, max)); + let key = timer(cx, Timer::V2Report(group)); + if cx.timers.get(key).is_none_or(|running| running > at) { + cx.timers.arm(key, at); + } + } + return; + } + let at = now.after(cx.draws.after(Purpose::Igmp, max)); + if cx.timers.get(timer(cx, Timer::General)).is_some_and(|general| general < at) { + return; + } + let group = match query.group { + QueryGroup::General => return cx.timers.arm(timer(cx, Timer::General), at), + QueryGroup::Specific(group) => group, + }; + let sources: Vec = match &query.version { + QueryVersion::V3(v3) => v3.sources().collect(), + QueryVersion::V1 | QueryVersion::V2 => Vec::new(), + }; + let key = timer(cx, Timer::Response(group)); + let Some(g) = i.igmp.groups.get_mut(&group) else { return }; + let recorded = match g.response.take() { + None => sources, + Some(list) if sources.is_empty() || list.is_empty() => Vec::new(), + Some(mut list) => { + list.extend(sources.into_iter().filter(|s| !list.contains(s)).collect::>()); + list + } + }; + g.response = Some(if recorded.len() > IGMP_QUERY_SOURCES { Vec::new() } else { recorded }); + let at = cx.timers.get(key).map_or(at, |pending| pending.min(at)); + cx.timers.arm(key, at); +} + +/// An IGMPv2 general query: v2 mode for the Older Version Querier Present Interval, 2 × 125 s +/// plus the query's Max Response Time (RFC 9776 §8.12; IP-D13), and every v3 response and +/// retransmission cancelled on entry (§7.2.1). +fn enter_v2(i: &mut Interface, cx: &mut Cx<'_>, max: Duration) { + let until = cx.now.after(QUERY_INTERVAL.saturating_mul(2).saturating_add(max)); + if i.igmp.v2_until.is_none() { + cx.log.count(Counter::IgmpModeV2); + cx.timers.cancel_iface(cx.iface, |t| matches!(t, timers::Timer::Igmp(_, Timer::General | Timer::Response(_) | Timer::Change(_)))); + for g in i.igmp.groups.values_mut() { + g.change = None; + g.response = None; + } + i.igmp.groups.retain(|_, g| g.refs > 0); + } + i.igmp.v2_until = Some(until); + cx.timers.arm(timer(cx, Timer::V2Querier), until); +} + +/// A deadline of the IGMP host; v3 records join `batch`, to leave together. +pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, t: Timer, batch: &mut Vec) { + match t { + Timer::General => { + let groups: Vec = i.igmp.joined_groups().collect(); + batch.extend(groups.into_iter().map(|group| Record { group, kind: Kind::IsExclude, sources: Vec::new(), change: None })); + } + Timer::Response(group) => { + let joined = i.igmp.joined(group); + let Some(sources) = i.igmp.groups.get_mut(&group).and_then(|g| g.response.take()) else { return }; + if joined { + let kind = if sources.is_empty() { Kind::IsExclude } else { Kind::IsInclude }; + batch.push(Record { group, kind, sources, change: None }); + } + } + Timer::Change(group) => { + let mode = i.igmp.mode(); + let Some(change) = i.igmp.groups.get_mut(&group).and_then(|g| g.change.as_mut()).filter(|c| !c.queued && c.left > 0) else { return }; + change.queued = true; + let seq = change.seq; + match (mode, change.kind) { + (IgmpMode::V3, ChangeKind::Join) => batch.push(Record { group, kind: Kind::ToExclude, sources: Vec::new(), change: Some(seq) }), + (IgmpMode::V3, ChangeKind::Leave) => batch.push(Record { group, kind: Kind::ToInclude, sources: Vec::new(), change: Some(seq) }), + (IgmpMode::V2, _) => enqueue(i, cx, Report::V2(group, Some(seq))), + } + } + Timer::V2Report(group) => { + if i.igmp.joined(group) { + enqueue(i, cx, Report::V2(group, None)); + } + } + Timer::V2Querier => { + i.igmp.v2_until = None; + cx.timers.cancel_iface(cx.iface, |t| matches!(t, timers::Timer::Igmp(_, Timer::V2Report(_)))); + cx.log.count(Counter::IgmpModeV3); + } + } +} + +/// Builds `report` as it leaves, from the interface's source of this moment (0.0.0.0 while it +/// has none, RFC 9776 §4.2.14); a change record overtaken by a newer change is left out. +pub(crate) fn emit(i: &mut Interface, cx: &mut Cx<'_>, report: &Report, out: &mut [u8]) -> Option { + let source = Ipv4Source::new(i.usable().next().map_or(Ipv4Addr::UNSPECIFIED, |a| a.cidr.addr)).ok()?; + let mac = i.mac; + let current = |i: &Interface, group: MulticastAddr, seq: u32| { + i.igmp.groups.get(&group).and_then(|g| g.change).is_some_and(|c| c.seq == seq && c.queued) + }; + let len = match report { + Report::V3(records) => { + let live: Vec<&Record> = records.iter().filter(|r| r.change.is_none_or(|seq| current(i, r.group, seq))).collect(); + let groups: Vec> = live + .iter() + .filter_map(|r| { + let group = ReportGroup::new(r.group).ok()?; + Some(match r.kind { + Kind::IsInclude => GroupRecord::IsInclude(group, &r.sources), + Kind::IsExclude => GroupRecord::IsExclude(group), + Kind::ToInclude => GroupRecord::ToInclude(group), + Kind::ToExclude => GroupRecord::ToExclude(group), + }) + }) + .collect(); + let len = if groups.is_empty() { + None + } else { + let datagram = wire::datagram(source, traffic_class(), V3ReportBuilder { records: &groups }); + let destination = MacAddr::multicast(MulticastAddr::IGMPV3_ROUTERS); + toyos_net_wire::ethernet::FrameBuilder { destination, source: mac }.emit(&datagram, out).ok().map(<[u8]>::len) + }; + for (group, seq) in live.iter().filter_map(|r| r.change.map(|seq| (r.group, seq))).collect::>() { + changed(i, cx, group, seq); + } + len + } + Report::V2(group, seq) => { + if seq.is_some_and(|seq| !current(i, *group, seq)) { + return None; + } + let message = V2Builder { kind: V2Kind::Report, group: ReportGroup::new(*group).ok()? }; + let datagram = wire::datagram(source, traffic_class(), message); + let len = toyos_net_wire::ethernet::FrameBuilder { destination: MacAddr::multicast(*group), source: mac }.emit(&datagram, out).ok().map(<[u8]>::len); + if let Some(g) = i.igmp.groups.get_mut(group) { + g.last_reporter = true; + } + if let Some(seq) = seq { + changed(i, cx, *group, *seq); + } + len + } + Report::Leave(group) => { + let message = V2Builder { kind: V2Kind::Leave, group: ReportGroup::new(*group).ok()? }; + let datagram = wire::datagram(source, traffic_class(), message); + let destination = MacAddr::multicast(MulticastAddr::ALL_ROUTERS); + toyos_net_wire::ethernet::FrameBuilder { destination, source: mac }.emit(&datagram, out).ok().map(<[u8]>::len) + } + }; + if len.is_some() { + cx.log.count(Counter::IgmpReportsSent); + } + len +} + +impl Igmp { + /// Whether a frame to `mac` is for a group joined here (RFC 1112 §6.4). + pub fn accepts(&self, mac: MacAddr) -> bool { + self.joined_groups().any(|group| MacAddr::multicast(group) == mac) + } +} diff --git a/toyos-net-ip/src/input.rs b/toyos-net-ip/src/input.rs new file mode 100644 index 00000000000..e57faf1ae5a --- /dev/null +++ b/toyos-net-ip/src/input.rs @@ -0,0 +1,171 @@ +//! Frame dispatch (`wire.md` §3.3) and IPv4 input (§4.1). The first failing step names the +//! refusal, so counters are deterministic, and a transport is handed only what passed them all. + +use core::net::Ipv4Addr; + +use toyos_net_wire::arp::Arp; +use toyos_net_wire::ethernet::{EtherType, Frame, MacAddr, MacClass, TagProtocol, Tags}; +use toyos_net_wire::icmp::HostUnreachable; +use toyos_net_wire::igmp::IgmpPacket; +use toyos_net_wire::ipv4::{Ipv4Option, Ipv4Packet, MulticastAddr, Protocol}; +use toyos_net_wire::tcp::TcpSegment; +use toyos_net_wire::udp::UdpDatagram; +use toyos_net_wire::Instant; + +use crate::addr::{is_class_e, is_host}; +use crate::counters::Counter; +use crate::iface::Interface; +use crate::{arp, igmp, Arrival, Cast, Delivery, IfIndex, Ip, Peer}; + +const IPV6: u16 = 0x86DD; +const LOOSE_SOURCE_ROUTE: u8 = 0x83; +const STRICT_SOURCE_ROUTE: u8 = 0x89; +/// The DHCP client's port, which the acquisition exception admits (RFC 2131 §2). +const ACQUISITION_PORT: u16 = 68; + +/// §3.3 steps 1 to 4 of `wire.md`: EtherType, destination, tags, own source. +fn dispatch(i: &Interface, frame: &Frame<'_>) -> Option { + let destination = frame.destination(); + let for_us = destination == i.mac.get() + || destination == MacAddr::BROADCAST + || destination == MacAddr::multicast(MulticastAddr::ALL_HOSTS) + || i.igmp.accepts(destination); + let untagged = match frame.tags() { + Tags::Untagged => true, + Tags::Single(tag) => tag.protocol() == TagProtocol::Customer && tag.vlan_id() == 0, + Tags::Double { .. } => false, + }; + match frame.ether_type() { + EtherType::Other(other) if other.value() == IPV6 => Some(Counter::EthIpv6), + EtherType::Other(_) => Some(Counter::EthUnknownType), + EtherType::Ipv4 | EtherType::Arp if !for_us => Some(Counter::EthNotForUs), + EtherType::Ipv4 | EtherType::Arp if !untagged => Some(Counter::EthVlan), + EtherType::Ipv4 | EtherType::Arp if frame.source() == i.mac => Some(Counter::EthOwnSource), + EtherType::Ipv4 | EtherType::Arp => None, + } +} + +/// The acquisition exception (ruling H-1): before the interface holds a usable address, a +/// unicast datagram to UDP port 68 in a frame to our MAC is admitted for the DHCP client alone. +fn acquisition(i: &Interface, link: MacClass, packet: &Ipv4Packet<'_>) -> bool { + link == MacClass::Individual + && i.usable().next().is_none() + && packet.protocol() == Protocol::Udp + && !packet.is_fragment() + && UdpDatagram::parse(packet).is_ok_and(|d| d.destination_port().get() == ACQUISITION_PORT) +} + +/// §4.1 steps 3 to 9. +fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packet<'_>) -> Result { + let (destination, source) = (packet.destination(), packet.source()); + let group = destination.is_broadcast() || destination.is_multicast() || i.is_directed_broadcast(destination); + match link { + MacClass::Broadcast if !group => return Err(Counter::IpUnicastInLinkBroadcast), + MacClass::Group if !group => return Err(Counter::IpUnicastInLinkMulticast), + MacClass::Individual | MacClass::Broadcast | MacClass::Group => {} + } + let [first, ..] = destination.octets(); + if first == 0 || first == 127 || is_class_e(destination) { + return Err(Counter::IpMartianDestination); + } + let cast = if i.is_usable(destination) { + Cast::Unicast + } else if destination.is_broadcast() { + Cast::LimitedBroadcast + } else if i.is_directed_broadcast(destination) { + Cast::SubnetBroadcast + } else if let Some(g) = MulticastAddr::new(destination).filter(|g| i.igmp.joined(*g)) { + Cast::Multicast(g) + } else if i.owns(destination) { + return Err(Counter::IpTentativeDestination); + } else if acquisition(i, link, packet) { + Cast::Acquisition + } else { + 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))) { + return Err(Counter::IpInvalidSource); + } + if ifaces.iter().any(|j| j.owns(source)) { + return Err(Counter::IpOwnSource); + } + if packet.is_fragment() { + return Err(Counter::IpFragment); + } + let source_routed = packet.options().iter().any(|o| { + matches!(o, Ipv4Option::Other { kind, .. } if kind.value() == LOOSE_SOURCE_ROUTE || kind.value() == STRICT_SOURCE_ROUTE) + }); + if source_routed { + return Err(Counter::IpSourceRoute); + } + Ok(cast) +} + +impl Ip { + /// A frame the device received on `iface`. What a transport must act on comes back borrowing + /// the frame; everything else — ARP, ICMP, IGMP, refusals — [ip] handles itself. + pub fn receive<'a>(&mut self, now: Instant, iface: IfIndex, frame: &'a [u8]) -> Option> { + let now = self.clock(now); + let frame = Frame::parse(frame).map_err(|e| self.log.wire(e.name())).ok()?; + let Some(i) = self.ifaces.get(iface.0) else { + self.log.count(Counter::UnknownInterface); + return None; + }; + if let Some(refusal) = dispatch(i, &frame) { + self.log.count(refusal); + return None; + } + if frame.ether_type() == EtherType::Arp { + let parsed = Arp::parse(frame.body()).map_err(|e| self.log.wire(e.name())).ok()?; + let (i, mut cx) = self.split(now, iface)?; + arp::receive(i, &mut cx, &parsed); + return None; + } + self.ipv4(now, iface, frame.destination().class(), frame.body()) + } + + fn ipv4<'a>(&mut self, now: Instant, iface: IfIndex, link: MacClass, body: &'a [u8]) -> Option> { + let packet = Ipv4Packet::parse(body).map_err(|e| self.log.wire(e.name())).ok()?; + let i = self.ifaces.get(iface.0)?; + let cast = match admit(&self.ifaces, i, link, &packet) { + Ok(cast) => cast, + Err(refusal) => { + self.log.refuse(refusal, iface, Peer::Ip(packet.source())); + return None; + } + }; + let arrival = Arrival { iface, packet, cast, link }; + match packet.protocol() { + Protocol::Icmp => self.icmp(now, iface, cast, packet), + Protocol::Igmp => { + let message = IgmpPacket::parse(packet.payload()).map_err(|e| self.log.wire(e.name())).ok()?; + let (i, mut cx) = self.split(now, iface)?; + igmp::input(i, &mut cx, &packet, message.message()); + None + } + Protocol::Tcp if cast != Cast::Unicast => { + self.log.count(Counter::IpTcpNotUnicast); + None + } + Protocol::Tcp => { + let segment = TcpSegment::parse(&packet).map_err(|e| self.log.wire(e.name())).ok()?; + Some(Delivery::Tcp(arrival, segment)) + } + Protocol::Udp => { + let datagram = UdpDatagram::parse(&packet).map_err(|e| self.log.wire(e.name())).ok()?; + if cast == Cast::Acquisition { + self.log.count(Counter::IpAcquisitionAdmitted); + } + Some(Delivery::Udp(arrival, datagram)) + } + Protocol::Other(_) => { + self.log.count(Counter::IpProtocol); + if cast == Cast::Unicast { + self.error(now, iface, link, &packet, HostUnreachable::Protocol); + } + None + } + } + } +} diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs new file mode 100644 index 00000000000..78cf7d0d9c8 --- /dev/null +++ b/toyos-net-ip/src/lib.rs @@ -0,0 +1,470 @@ +//! ToyOS's IPv4 host layer (RFC 1122), pure: per-interface addresses and the host routing table, +//! frame dispatch and the IPv4 input policy, neighbour resolution over ARP with one reachability +//! machine (RFC 4861 §7.3 with RFC 7048's UNREACHABLE), address conflict detection (RFC 5227), +//! ICMP (echo, errors under a two-level token bucket, inbound errors classified for the +//! transports), and the IGMPv3 host with its IGMPv2 compatibility mode (RFC 9776). The caller +//! hands in the time, the secret, frames, configuration and transport requests; nothing here +//! reads a clock, draws randomness or does I/O. +//! +//! **Pull egress.** [ip]'s own frames — ARP, IGMP, ICMP, and datagrams released by resolution — +//! wait in one FIFO that [`Ip::transmit`] drains with the credit the device offers, and a frame's +//! timers start when it leaves. A datagram a transport hands [`Ip::send_udp`] is written straight +//! into the device's buffer, or waits here for its next hop; a full ring never drops one. +//! +//! **Refusals are values.** Every refusal is a named [`Counter`]; one of legacy or insecure input +//! is also an [`Event::Refused`] naming the rule and the peer, which the shell logs through +//! [`RefusalLog`]. +//! +//! **Draws are keyed.** Every random delay, the neighbour reachable time and the error limiter's +//! slots and jitter come from SipHash-2-4 over (secret, purpose, counter): with one secret the +//! layer is deterministic, and without it no off-link host can predict a draw. + +#![cfg_attr(not(test), no_std)] +#![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] + +extern crate alloc; + +mod acd; +mod addr; +mod config; +mod arp; +mod counters; +mod draw; +mod egress; +mod icmp; +mod iface; +mod igmp; +mod input; +mod limiter; +mod nud; +mod route; +mod timers; + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use core::num::NonZeroU16; +use core::time::Duration; + +use toyos_net_wire::ethernet::{IndividualMac, MacAddr, MacClass}; +use toyos_net_wire::icmp::{ParameterProblemCode, TimeExceededCode, UnreachableCode}; +use toyos_net_wire::ipv4::{Ipv4Packet, MulticastAddr}; +use toyos_net_wire::siphash::Key; +use toyos_net_wire::tcp::TcpSegment; +use toyos_net_wire::udp::UdpDatagram; +use toyos_net_wire::Port; + +pub use addr::AddrState; +pub use counters::{Counter, Counters, RefusalLog}; +pub use egress::{Sent, UdpOut, FRAME}; +pub use igmp::IgmpMode; +pub use limiter::Limiter; +pub use nud::{Failed, Incomplete, Linked, Nud, Probing, Reachable, Unreachable}; +pub use route::{NextHop, Route, Source}; +pub use toyos_net_wire::Instant; + +/// An interface, named by the order it was added; the lower index wins a routing tie (§3.3). An +/// index names an interface of the [`Ip`] that returned it and nothing in another. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct IfIndex(usize); + +/// The IP MTU of every interface: an Ethernet body. +pub const MTU: usize = toyos_net_wire::ethernet::MAX_BODY; + +pub mod limits { + pub const ADDRS: usize = 8; + pub const GATEWAYS: usize = 4; + pub const CONTROL_QUEUE: usize = 64; + pub const ECHO_REPLIES: usize = 16; + pub const IGMP_GROUPS: usize = 32; + pub const IGMP_QUERY_SOURCES: usize = 64; + /// Refusal events held for the shell until it drains them. + pub const EVENTS: usize = 1_024; + + /// RFC 4861 §10 and RFC 7048 §3, over ARP. + pub mod nud { + use core::time::Duration; + + pub const RETRANS: Duration = Duration::from_secs(1); + pub const BROADCAST_SOLICIT: u8 = 3; + pub const UNICAST_SOLICIT: u8 = 3; + pub const REACHABLE_MIN: Duration = Duration::from_secs(15); + pub const REACHABLE_MAX: Duration = Duration::from_secs(45); + pub const REACHABLE_REDRAW: Duration = Duration::from_secs(7_200); + pub const DELAY_FIRST_PROBE: Duration = Duration::from_secs(5); + pub const BACKOFF_MULTIPLE: u32 = 3; + pub const MAX_RETRANS: Duration = Duration::from_secs(60); + pub const FAILED_HOLD: Duration = Duration::from_secs(20); + pub const LOCKTIME: Duration = Duration::from_secs(1); + pub const IDLE_LIFETIME: Duration = Duration::from_secs(600); + pub const PENDING_PER_NEIGHBOUR: usize = 8; + pub const PENDING_TOTAL: usize = 64; + pub const TABLE_MAX: usize = 512; + } + + /// RFC 5227 §1.1. The owner's ruling scales every probing interval by one factor, so the + /// longest probing — PROBE_WAIT, two PROBE_MAX gaps and ANNOUNCE_WAIT, 7 s by the RFC — takes + /// at most 200 ms; announcing, defence and the rate limit keep the RFC's values. + pub mod acd { + use core::time::Duration; + + const fn scaled(rfc_ms: u64) -> Duration { + Duration::from_nanos(rfc_ms.saturating_mul(1_000_000).saturating_mul(200) / 7_000) + } + + pub const PROBE_WAIT: Duration = scaled(1_000); + pub const PROBE_NUM: u8 = 3; + pub const PROBE_MIN: Duration = scaled(1_000); + pub const PROBE_MAX: Duration = scaled(2_000); + pub const ANNOUNCE_WAIT: Duration = scaled(2_000); + pub const ANNOUNCE_NUM: u8 = 2; + pub const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(2); + pub const MAX_CONFLICTS: u32 = 10; + pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(60); + pub const DEFEND_INTERVAL: Duration = Duration::from_secs(10); + /// The conflict count starts over after this long without a conflict. + pub const CONFLICT_RESET: Duration = Duration::from_secs(600); + } + + /// The error limiter: per destination, and one global bucket whose burst is redrawn every + /// second (RFC 4443 §2.4 (f)). + pub mod icmp { + pub const DEST_BURST: u64 = 10; + pub const DEST_PER_S: u64 = 10; + pub const DEST_SLOTS: usize = 256; + pub const GLOBAL_BURST_MIN: u64 = 75; + pub const GLOBAL_BURST_MAX: u64 = 100; + pub const GLOBAL_PER_S: u64 = 100; + } + + /// RFC 9776 §8 and RFC 2236 §8. + pub mod igmp { + use core::time::Duration; + + pub const ROBUSTNESS: u8 = 2; + pub const QUERY_INTERVAL: Duration = Duration::from_secs(125); + pub const UNSOLICITED_REPORT: Duration = Duration::from_secs(1); + pub const V2_UNSOLICITED_REPORT: Duration = Duration::from_secs(10); + } +} + +/// How a datagram reached us (§4.2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Cast { + /// To an announcing or assigned address of the receiving interface. + Unicast, + LimitedBroadcast, + /// To the directed broadcast of one of the interface's prefixes of /30 or shorter. + SubnetBroadcast, + Multicast(MulticastAddr), + /// Admitted by the acquisition exception alone: for the DHCP client's socket and no other. + Acquisition, +} + +/// What a transport is handed with a datagram. +#[derive(Clone, Copy, Debug)] +pub struct Arrival<'a> { + pub iface: IfIndex, + pub packet: Ipv4Packet<'a>, + pub cast: Cast, + /// How the frame that carried it was addressed. + pub link: MacClass, +} + +#[derive(Clone, Copy, Debug)] +pub enum Delivery<'a> { + Udp(Arrival<'a>, UdpDatagram<'a>), + /// Only ever to one of our unicast addresses, from a valid unicast source. + Tcp(Arrival<'a>, TcpSegment<'a>), + Error(TransportError), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Transport { + Tcp, + Udp, +} + +/// A transport 4-tuple, as a datagram of ours carried it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Flow { + pub source: Ipv4Addr, + pub source_port: Port, + pub destination: Ipv4Addr, + pub destination_port: Port, +} + +/// An ICMP error [ip] validated against one of our datagrams (§9.5). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TransportError { + pub iface: IfIndex, + pub transport: Transport, + /// The quoted datagram's 4-tuple: ours as its source. + pub flow: Flow, + /// The quoted TCP sequence number. + pub sequence: Option, + pub kind: ErrorKind, + /// The ICMP message's own source. + pub reporter: Ipv4Addr, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorKind { + /// Destination unreachable, any code but fragmentation needed. + Unreachable(UnreachableCode), + FragmentationNeeded { next_hop_mtu: Option, quoted_length: u16 }, + TimeExceeded(TimeExceededCode), + ParameterProblem { code: ParameterProblemCode, pointer: u8 }, +} + +/// §9.5's shared classification, the vocabulary the transports act on. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorClass { + /// Codes 2 and 3: the peer refused. + Refused, + /// Codes 9, 10 and 13: administratively prohibited. + Prohibited, + PathMtu, + Soft, +} + +impl ErrorKind { + pub const fn class(self) -> ErrorClass { + match self { + Self::Unreachable(UnreachableCode::Protocol | UnreachableCode::Port) => ErrorClass::Refused, + Self::Unreachable( + UnreachableCode::NetProhibited | UnreachableCode::HostProhibited | UnreachableCode::CommunicationProhibited, + ) => ErrorClass::Prohibited, + Self::FragmentationNeeded { .. } | Self::Unreachable(UnreachableCode::FragmentationNeeded) => ErrorClass::PathMtu, + Self::Unreachable(_) | Self::TimeExceeded(_) | Self::ParameterProblem { .. } => ErrorClass::Soft, + } + } +} + +/// Who a refusal names. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Peer { + Ip(Ipv4Addr), + Arp { ip: Ipv4Addr, mac: MacAddr }, + /// A neighbour's MAC, and the one an ARP packet named instead. + MacChange { ip: Ipv4Addr, old: MacAddr, new: MacAddr }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refusal { + pub rule: Counter, + pub iface: IfIndex, + pub peer: Peer, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Event { + Refused(Refusal), + /// Conflict detection finished: the address is usable. + Verified { iface: IfIndex, addr: Ipv4Addr }, + /// Another host used the address while it was probed; it was removed. + Conflict { iface: IfIndex, addr: Ipv4Addr, mac: MacAddr }, + /// The address was lost to another host after one defence (RFC 5227 §2.4 (b)) and removed: + /// every connection using it is to be reset. + Lost { iface: IfIndex, addr: Ipv4Addr, mac: MacAddr }, + /// The link went down while the address was probed; it was removed. + NotVerified { iface: IfIndex, addr: Ipv4Addr }, + /// A next hop resolved: flows waiting on it may send. + Resolved { iface: IfIndex, next_hop: Ipv4Addr }, + /// A next hop failed resolution: flows waiting on it are told "host unreachable". + Failed { iface: IfIndex, next_hop: Ipv4Addr }, + /// A UDP datagram [ip] took could not reach its next hop. + Unreachable(Flow), +} + +/// Upper-layer reachability advice (RFC 1122 §3.3.1.4, RFC 4861 §7.3.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Advice { + /// Forward progress: the next hop answers. + Confirmed, + /// No progress: re-verify the next hop. + Reverify, +} + +/// Whether a flow may build a segment for its next hop now (§6.7). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Resolution { + Resolved(MacAddr), + Pending, + Failed, +} + +pub struct Ip { + draws: draw::Draws, + ifaces: Vec, + timers: timers::Timers, + control: egress::Control, + limiter: Limiter, + log: counters::Log, + latest: Instant, + generation: u64, +} + + +impl Ip { + pub fn new(now: Instant, secret: Key) -> Self { + let draws = draw::Draws::new(secret); + let limiter = Limiter::new(draws.limiter_key()); + Self { + draws, + ifaces: Vec::new(), + timers: timers::Timers::default(), + control: egress::Control::default(), + limiter, + log: counters::Log::default(), + latest: now, + generation: 0, + } + } + + pub fn add_interface(&mut self, now: Instant, mac: IndividualMac) -> IfIndex { + let now = self.clock(now); + let reachable = self.draws.reachable_time(); + self.ifaces.push(iface::Interface { + mac, + up: false, + addresses: Vec::new(), + gateways: Vec::new(), + active: None, + neighbours: BTreeMap::new(), + held: 0, + reachable, + reachable_drawn: now, + acd: acd::Conflicts::default(), + igmp: igmp::Igmp::default(), + }); + IfIndex(self.ifaces.len().saturating_sub(1)) + } + + pub fn counters(&self) -> &Counters { + &self.log.counters + } + + /// How many refusals of the wire rule `name` (a `toyos_net_wire` reason's `name()`) [ip] met. + pub fn wire_refusals(&self, name: &str) -> u64 { + self.log.wire.get(name).copied().unwrap_or(0) + } + + /// Every wire refusal [ip] met, by name, for inspect. + pub fn wire_counters(&self) -> impl Iterator + '_ { + self.log.wire.iter().map(|(&name, &n)| (name, n)) + } + + /// Refusals, address results and resolution news since the last call. + pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> { + self.log.drain() + } + + /// Bumped by every change that can alter a route lookup (§3.6). + pub const fn generation(&self) -> u64 { + self.generation + } + + pub fn mac(&self, iface: IfIndex) -> Option { + self.ifaces.get(iface.0).map(|i| i.mac) + } + + pub fn neighbour(&self, iface: IfIndex, addr: Ipv4Addr) -> Option<&Nud> { + self.ifaces.get(iface.0)?.neighbours.get(&addr).map(|n| &n.state) + } + + pub fn address(&self, iface: IfIndex, addr: Ipv4Addr) -> Option { + self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr == addr).map(|a| a.state()) + } + + /// The prefix length of one of the interface's addresses. + pub fn prefix_len(&self, iface: IfIndex, addr: Ipv4Addr) -> Option { + self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr == addr).map(|a| a.cidr.len) + } + + pub fn reachable_time(&self, iface: IfIndex) -> Option { + self.ifaces.get(iface.0).map(|i| i.reachable) + } + + pub fn gateways(&self, iface: IfIndex) -> Option<&[Ipv4Addr]> { + self.ifaces.get(iface.0).map(|i| i.gateways.as_slice()) + } + + /// An announcing or assigned address of any interface: one a socket may bind and send from. + pub fn is_assigned(&self, addr: Ipv4Addr) -> bool { + self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr == addr && a.usable())) + } + + /// One of our addresses in any state. + pub fn is_local(&self, addr: Ipv4Addr) -> bool { + self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr == addr)) + } + + /// The directed broadcast of a usable prefix of /30 or shorter. + pub fn is_directed_broadcast(&self, addr: Ipv4Addr) -> bool { + self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.usable() && a.cidr.broadcast() == Some(addr))) + } + + /// A `now` before the latest one seen is the latest one: no deadline fires early and no + /// duration is negative, and the regression is counted (§1.4 (4)). + fn clock(&mut self, now: Instant) -> Instant { + if now < self.latest { + self.log.count(Counter::ClockRegressed); + self.latest + } else { + self.latest = now; + now + } + } +} + +// Each compile_fail block sits beside one that compiles, so a typo cannot pass it. +#[cfg(doctest)] +mod compile_fail { + /// NUD-28: a MAC exists only in a state that has one, and a pending queue only in INCOMPLETE. + /// + /// ``` + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) -> usize { + /// match n { + /// Nud::Incomplete(i) => i.queued(), + /// Nud::Reachable(r) => usize::from(r.mac().0[0]), + /// Nud::Failed(_) => 0, + /// _ => 1, + /// } + /// } + /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Incomplete(i) = n { let _ = i.mac(); } } + /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Failed(i) = n { let _ = i.mac(); } } + /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Reachable(r) = n { let _ = r.queued(); } } + /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Stale(s) = n { let _ = s.queued(); } } + /// ``` + #[allow(non_camel_case_types)] + pub struct s_ip_nud_028_a_state_holds_only_its_own_fields; +} diff --git a/toyos-net-ip/src/limiter.rs b/toyos-net-ip/src/limiter.rs new file mode 100644 index 00000000000..15d3dd95349 --- /dev/null +++ b/toyos-net-ip/src/limiter.rs @@ -0,0 +1,86 @@ +//! The error limiter (§9.4): a token bucket per destination in 256 keyed slots, and one global +//! bucket whose burst is redrawn every second from a keyed function. A bucket holds its tokens as +//! nanoseconds of refill, so fractions are carried and never rounded away, and a moved clock +//! refills it to its cap and no further. The TCP resets for segments nobody asked for draw from a +//! second instance: a shared budget is the side channel the per-destination buckets remove. + +use alloc::vec; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_wire::siphash::{siphash24, Key}; +use toyos_net_wire::Instant; + +use crate::draw::nanos; +use crate::limits::icmp::{DEST_BURST, DEST_PER_S, DEST_SLOTS, GLOBAL_BURST_MAX, GLOBAL_BURST_MIN, GLOBAL_PER_S}; + +const SECOND: u64 = 1_000_000_000; +const DEST_COST: u64 = SECOND / DEST_PER_S; +const GLOBAL_COST: u64 = SECOND / GLOBAL_PER_S; + +#[derive(Clone, Copy, Debug)] +struct Bucket { + level: u64, + at: Instant, +} + +impl Bucket { + fn refill(&mut self, now: Instant, cap: u64) { + self.level = self.level.saturating_add(nanos(now.since(self.at))).min(cap); + self.at = self.at.max(now); + } + + fn has(&self, cost: u64) -> bool { + self.level >= cost + } + + fn take(&mut self, cost: u64) { + self.level = self.level.saturating_sub(cost); + } +} + +#[derive(Clone, Debug)] +pub struct Limiter { + key: Key, + slots: Vec, + global: Bucket, +} + +impl Limiter { + /// Every bucket starts full. + pub fn new(key: Key) -> Self { + let full = |burst: u64, cost: u64| Bucket { level: burst.saturating_mul(cost), at: Instant::from_nanos(0) }; + Self { key, slots: vec![full(DEST_BURST, DEST_COST); DEST_SLOTS], global: full(GLOBAL_BURST_MAX, GLOBAL_COST) } + } + + /// The slot `destination`'s bucket lives in. + pub fn slot(&self, destination: Ipv4Addr) -> usize { + let [a, b, c, d] = destination.octets(); + let hash = siphash24(&self.key, &[1, a, b, c, d]); + usize::try_from(hash.checked_rem(u64::try_from(DEST_SLOTS).unwrap_or(1)).unwrap_or(0)).unwrap_or(0) + } + + /// The global bucket's burst during the second `now` falls in. + pub fn global_burst(&self, now: Instant) -> u64 { + let [a, b, c, d, e, f, g, h] = (now.nanos() / SECOND).to_le_bytes(); + let span = GLOBAL_BURST_MAX.saturating_sub(GLOBAL_BURST_MIN).saturating_add(1); + let draw = siphash24(&self.key, &[2, a, b, c, d, e, f, g, h]).checked_rem(span).unwrap_or(0); + GLOBAL_BURST_MIN.saturating_add(draw) + } + + /// Takes a token from `destination`'s bucket and one from the global bucket, or refuses and + /// takes neither. + pub fn allow(&mut self, now: Instant, destination: Ipv4Addr) -> bool { + let global_cap = self.global_burst(now).saturating_mul(GLOBAL_COST); + let index = self.slot(destination); + let Some(slot) = self.slots.get_mut(index) else { return false }; + slot.refill(now, DEST_BURST.saturating_mul(DEST_COST)); + self.global.refill(now, global_cap); + if !(slot.has(DEST_COST) && self.global.has(GLOBAL_COST)) { + return false; + } + slot.take(DEST_COST); + self.global.take(GLOBAL_COST); + true + } +} diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs new file mode 100644 index 00000000000..73be300cc84 --- /dev/null +++ b/toyos-net-ip/src/nud.rs @@ -0,0 +1,498 @@ +//! The reachability machine (§6): RFC 4861 §7.3's neighbour unreachability detection with RFC +//! 7048's UNREACHABLE, written over ARP so NDP can reuse it. A request is the probe; a solicited +//! reply or a transport's positive advice is a confirmation; any other ARP packet naming a +//! neighbour we hold is an assertion. Requests to one neighbour are never closer than RETRANS, +//! measured from each request's hand-off; a timer fires once per `fire` and its next deadline is +//! taken from that moment, so a jumped clock never replays missed periods. +//! +//! Each state carries exactly its own fields: a MAC only where one is known, a pending queue +//! only while INCOMPLETE. + +use alloc::collections::VecDeque; +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use core::time::Duration; + +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::Instant; + +use crate::counters::Counter; +use crate::egress::{FrameKind, Item}; +use crate::iface::{Cx, Interface}; +use crate::limits::nud::{ + BACKOFF_MULTIPLE, BROADCAST_SOLICIT, DELAY_FIRST_PROBE, FAILED_HOLD, IDLE_LIFETIME, LOCKTIME, MAX_RETRANS, + RETRANS, TABLE_MAX, UNICAST_SOLICIT, +}; +use crate::timers::Timer; +use crate::{route, Event, Flow, Peer}; + +#[derive(Debug)] +pub enum Nud { + Incomplete(Incomplete), + Reachable(Reachable), + Stale(Linked), + Delay(Linked), + Probe(Probing), + Unreachable(Unreachable), + Failed(Failed), +} + +#[derive(Debug)] +pub struct Incomplete { + requests: u8, + pending: VecDeque, +} + +impl Incomplete { + pub fn requests(&self) -> u8 { + self.requests + } + + /// Datagrams waiting for the answer. + pub fn queued(&self) -> usize { + self.pending.len() + } +} + +#[derive(Debug)] +pub struct Reachable { + mac: MacAddr, + confirmed: Instant, +} + +impl Reachable { + pub fn mac(&self) -> MacAddr { + self.mac + } + + pub fn confirmed(&self) -> Instant { + self.confirmed + } +} + +/// STALE or DELAY: a MAC and nothing else. +#[derive(Debug)] +pub struct Linked { + mac: MacAddr, +} + +impl Linked { + pub fn mac(&self) -> MacAddr { + self.mac + } +} + +#[derive(Debug)] +pub struct Probing { + mac: MacAddr, + requests: u8, +} + +impl Probing { + pub fn mac(&self) -> MacAddr { + self.mac + } + + pub fn requests(&self) -> u8 { + self.requests + } +} + +#[derive(Debug)] +pub struct Unreachable { + mac: MacAddr, + /// Broadcast requests this episode (RFC 7048 §4's k). + requests: u32, + /// A request left and its backoff runs. + backoff: bool, + /// A datagram went to the MAC since that request. + sent: bool, +} + +impl Unreachable { + pub fn mac(&self) -> MacAddr { + self.mac + } + + pub fn requests(&self) -> u32 { + self.requests + } + + /// No request pending: nothing is sent until the next datagram. + pub fn quiescent(&self) -> bool { + !self.backoff + } +} + +#[derive(Debug)] +pub struct Failed { + since: Instant, +} + +impl Failed { + pub fn since(&self) -> Instant { + self.since + } +} + +impl Nud { + pub fn mac(&self) -> Option { + match self { + Self::Reachable(r) => Some(r.mac), + Self::Stale(l) | Self::Delay(l) => Some(l.mac), + Self::Probe(p) => Some(p.mac), + Self::Unreachable(u) => Some(u.mac), + Self::Incomplete(_) | Self::Failed(_) => None, + } + } + + /// A request of ours may be outstanding: a reply now is solicited (§7.2 (5)). + fn solicits(&self) -> bool { + matches!(self, Self::Incomplete(_) | Self::Probe(_) | Self::Unreachable(_)) + } +} + +/// A frame [ip] built and holds for its next hop, and who is told if it never leaves. +#[derive(Debug)] +pub(crate) struct Held { + pub frame: Vec, + pub kind: FrameKind, + pub flow: Option, +} + +#[derive(Debug)] +pub(crate) struct Neighbour { + pub state: Nud, + pub last_request: Option, + /// A request waits in the control queue. + pub queued: bool, + /// When a datagram last went to it: eviction takes the entry unused longest. + pub used: Instant, + /// The source a request prefers: the prompting datagram's (§6.3). + pub hint: Option, +} + +/// Where a datagram goes now (§6.3's "send", §6.7's answer). +#[derive(Debug)] +pub(crate) enum Link { + Resolved(MacAddr), + Pending, + Failed(Counter), +} + +fn backoff(requests: u32) -> Duration { + BACKOFF_MULTIPLE.checked_pow(requests).and_then(|m| RETRANS.checked_mul(m)).map_or(MAX_RETRANS, |d| d.min(MAX_RETRANS)) +} + +fn timer(cx: &Cx<'_>, addr: Ipv4Addr) -> Timer { + Timer::Neighbour(cx.iface, addr) +} + +/// Queues a request to `addr`, broadcast or to `to`; one the full queue refuses counts as sent, +/// so the machine moves on and never stalls on a loss. +fn request(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, to: Option) { + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + n.queued = true; + if !cx.control.push(Item::Request { iface: cx.iface, target: addr, to }, cx.log) { + request_left(i, cx, addr); + } +} + +/// A request to `addr` left, or was lost: its spacing and its state's deadline start now. +pub(crate) fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let now = cx.now; + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + if !core::mem::replace(&mut n.queued, false) { + return; + } + n.last_request = Some(now); + let wait = match &mut n.state { + Nud::Incomplete(s) => { + s.requests = s.requests.saturating_add(1); + RETRANS + } + Nud::Probe(s) => { + s.requests = s.requests.saturating_add(1); + RETRANS + } + Nud::Unreachable(s) => { + s.requests = s.requests.saturating_add(1); + s.backoff = true; + s.sent = false; + backoff(s.requests) + } + Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed(_) => return, + }; + cx.timers.arm(timer(cx, addr), now.after(wait)); +} + +/// Makes room for one more entry, evicting FAILED, then quiescent UNREACHABLE, then STALE, each +/// the one unused longest (§6.8). +fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { + if i.neighbours.len() < TABLE_MAX { + return true; + } + let class = |n: &Neighbour| match &n.state { + Nud::Failed(_) => Some(0), + Nud::Unreachable(u) if u.quiescent() && !n.queued => Some(1), + Nud::Stale(_) => Some(2), + _ => None, + }; + let victim = i.neighbours.iter().filter_map(|(a, n)| class(n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a); + let Some(victim) = victim else { return false }; + i.neighbours.remove(&victim); + cx.timers.cancel(timer(cx, victim)); + true +} + +/// A datagram or a flow wants `addr` now (§6.3 "send"): creates INCOMPLETE, moves STALE to +/// DELAY, or asks a quiescent UNREACHABLE for a request. +pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Option) -> Link { + let now = cx.now; + let Some(n) = i.neighbours.get_mut(&addr) else { + if !make_room(i, cx) { + cx.log.count(Counter::NbTableFull); + return Link::Failed(Counter::NbTableFull); + } + let state = Nud::Incomplete(Incomplete { requests: 0, pending: VecDeque::new() }); + i.neighbours.insert(addr, Neighbour { state, last_request: None, queued: false, used: now, hint }); + request(i, cx, addr, None); + return Link::Pending; + }; + n.used = now; + if hint.is_some() { + n.hint = hint; + } + let queued = n.queued; + match &mut n.state { + Nud::Incomplete(_) => Link::Pending, + Nud::Reachable(Reachable { mac, .. }) | Nud::Delay(Linked { mac }) | Nud::Probe(Probing { mac, .. }) => Link::Resolved(*mac), + Nud::Stale(Linked { mac }) => { + let mac = *mac; + n.state = Nud::Delay(Linked { mac }); + cx.timers.arm(timer(cx, addr), now.after(DELAY_FIRST_PROBE)); + Link::Resolved(mac) + } + Nud::Unreachable(u) => { + let mac = u.mac; + if u.backoff { + u.sent = true; + } else if !queued { + request(i, cx, addr, None); + } + Link::Resolved(mac) + } + Nud::Failed(_) => { + cx.log.count(Counter::NbFailedRefused); + Link::Failed(Counter::NbFailedRefused) + } + } +} + +/// Holds a frame for an INCOMPLETE neighbour; the queue keeps the newest (RFC 4861 §7.2.2). +pub(crate) fn hold(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, held: Held) { + let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return }; + if s.pending.len() >= crate::limits::nud::PENDING_PER_NEIGHBOUR { + s.pending.pop_front(); + i.held = i.held.saturating_sub(1); + cx.log.count(Counter::NbPendingOverflow); + } + s.pending.push_back(held); + i.held = i.held.saturating_add(1); +} + +/// A deadline of `addr`'s entry. +pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let now = cx.now; + let reachable = i.reachable_time(cx.draws, now); + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + let spaced = n.last_request.map_or(now, |at| at.after(RETRANS)).max(now); + match &mut n.state { + Nud::Incomplete(s) if s.requests < BROADCAST_SOLICIT => request(i, cx, addr, None), + Nud::Incomplete(_) => fail(i, cx, addr), + Nud::Reachable(s) => { + let end = s.confirmed.after(reachable); + if now < end { + cx.timers.arm(timer(cx, addr), end); + } else { + n.state = Nud::Stale(Linked { mac: s.mac }); + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); + } + } + Nud::Delay(s) => { + n.state = Nud::Probe(Probing { mac: s.mac, requests: 0 }); + probe(i, cx, addr, spaced); + } + Nud::Probe(s) if s.requests < UNICAST_SOLICIT => { + let mac = s.mac; + request(i, cx, addr, Some(mac)); + } + Nud::Probe(s) => { + n.state = Nud::Unreachable(Unreachable { mac: s.mac, requests: 0, backoff: false, sent: false }); + cx.log.count(Counter::NbUnreachable); + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); + route::refresh_active(i, cx); + } + Nud::Unreachable(s) if s.backoff && s.sent => request(i, cx, addr, None), + Nud::Unreachable(s) if s.backoff => { + s.backoff = false; + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); + } + Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed(_) => { + i.neighbours.remove(&addr); + route::refresh_active(i, cx); + } + } +} + +/// PROBE's first unicast request, now or at the spacing boundary. +fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { + if at <= cx.now { + let mac = i.neighbours.get(&addr).and_then(|n| n.state.mac()); + request(i, cx, addr, mac); + } else { + cx.timers.arm(timer(cx, addr), at); + } +} + +/// INCOMPLETE gave up: every held datagram is dropped and its sender told (§6.5, §9.6). +fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let now = cx.now; + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + let Nud::Incomplete(s) = core::mem::replace(&mut n.state, Nud::Failed(Failed { since: now })) else { return }; + for held in s.pending { + i.held = i.held.saturating_sub(1); + if let Some(flow) = held.flow { + cx.log.event(Event::Unreachable(flow)); + } + } + cx.log.count(Counter::NbFailed); + cx.log.event(Event::Failed { iface: cx.iface, next_hop: addr }); + cx.timers.arm(timer(cx, addr), now.after(FAILED_HOLD)); + route::refresh_active(i, cx); +} + +/// INCOMPLETE learned `mac`: its datagrams leave in arrival order, ahead of any sent later (§6.5). +fn resolved(cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr, pending: VecDeque) { + for held in pending { + let mut frame = held.frame; + if let Some((destination, _)) = frame.split_first_chunk_mut::<6>() { + *destination = mac.0; + } + cx.control.release(cx.iface, frame, held.kind); + } + cx.log.count(Counter::NbResolved); + cx.log.event(Event::Resolved { iface: cx.iface, next_hop: addr }); +} + +fn mac_changed(cx: &mut Cx<'_>, addr: Ipv4Addr, old: MacAddr, new: MacAddr) { + cx.log.refuse(Counter::ArpMacChanged, cx.iface, Peer::MacChange { ip: addr, old, new }); +} + +/// Enters REACHABLE, confirmed now. +fn reach(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + let now = cx.now; + let reachable = i.reachable_time(cx.draws, now); + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + let old = core::mem::replace(&mut n.state, Nud::Reachable(Reachable { mac, confirmed: now })); + cx.timers.arm(timer(cx, addr), now.after(reachable)); + match old { + Nud::Incomplete(s) => resolved(cx, addr, mac, s.pending), + other => { + if let Some(was) = other.mac().filter(|was| *was != mac) { + mac_changed(cx, addr, was, mac); + } + } + } + route::refresh_active(i, cx); +} + +/// Enters STALE at `mac`. +fn stale(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + let now = cx.now; + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + let old = core::mem::replace(&mut n.state, Nud::Stale(Linked { mac })); + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); + match old { + Nud::Incomplete(s) => resolved(cx, addr, mac, s.pending), + other => { + if let Some(was) = other.mac().filter(|was| *was != mac) { + mac_changed(cx, addr, was, mac); + } + } + } + route::refresh_active(i, cx); +} + +/// Whether a reply from `addr` now answers a request of ours. +pub(crate) fn solicits(i: &Interface, addr: Ipv4Addr) -> bool { + i.neighbours.get(&addr).is_some_and(|n| n.state.solicits()) +} + +/// A solicited reply (§7.2 (5)): the host asked, so it overrides any MAC (RFC 4861 §7.2.5). +pub(crate) fn confirm(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + reach(i, cx, addr, mac); +} + +/// Any other ARP packet naming a neighbour we hold (§7.2 (6), §7.3): a new MAC is taken only into +/// STALE, and never within LOCKTIME of a confirmation. +pub(crate) fn assert(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + let now = cx.now; + let Some(n) = i.neighbours.get(&addr) else { return }; + match &n.state { + Nud::Reachable(r) if r.mac != mac && now.since(r.confirmed) < LOCKTIME => { + let old = r.mac; + cx.log.refuse(Counter::ArpOverrideLocked, cx.iface, Peer::MacChange { ip: addr, old, new: mac }); + } + state if state.mac() == Some(mac) => {} + _ => stale(i, cx, addr, mac), + } +} + +/// An ARP request for one of our addresses from an on-link host we hold nothing for: the reply +/// path needs it, and STALE makes it verified before it is trusted (§7.3). +pub(crate) fn learn(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { + if i.neighbours.contains_key(&addr) || !make_room(i, cx) { + return; + } + let now = cx.now; + i.neighbours.insert(addr, Neighbour { state: Nud::Stale(Linked { mac }), last_request: None, queued: false, used: now, hint: None }); + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); +} + +/// A transport's advice about the next hop `addr` (§6.9). +pub(crate) fn advise(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, confirmed: bool) { + let now = cx.now; + let Some(n) = i.neighbours.get_mut(&addr) else { return }; + let spaced = n.last_request.map_or(now, |at| at.after(RETRANS)).max(now); + match (&mut n.state, confirmed) { + (Nud::Reachable(r), true) => r.confirmed = now, + (state @ (Nud::Stale(_) | Nud::Delay(_) | Nud::Probe(_) | Nud::Unreachable(_)), true) => { + if let Some(mac) = state.mac() { + reach(i, cx, addr, mac); + } + } + (state @ (Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_)), false) => { + if let Some(mac) = state.mac() { + *state = Nud::Probe(Probing { mac, requests: 0 }); + probe(i, cx, addr, spaced); + } + } + _ => {} + } +} + +/// The link went down: every entry goes, and the senders of held datagrams are told (§6.10). +pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>) { + for (addr, n) in core::mem::take(&mut i.neighbours) { + cx.timers.cancel(timer(cx, addr)); + if let Nud::Incomplete(s) = n.state { + for held in s.pending { + if let Some(flow) = held.flow { + cx.log.event(Event::Unreachable(flow)); + } + } + } + } + i.held = 0; +} diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs new file mode 100644 index 00000000000..02e5aff1baf --- /dev/null +++ b/toyos-net-ip/src/route.rs @@ -0,0 +1,174 @@ +//! The host routing table (§3): 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). + +use core::net::Ipv4Addr; + +use toyos_net_wire::ipv4::MulticastAddr; +use toyos_net_wire::Instant; + +use crate::addr::{is_class_e, is_host}; +use crate::counters::Counter; +use crate::iface::{Cx, Interface}; +use crate::nud::Nud; +use crate::{limits, IfIndex, Ip}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NextHop { + Neighbour(Ipv4Addr), + Broadcast, + Multicast(MulticastAddr), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Route { + pub iface: IfIndex, + pub next_hop: NextHop, + pub source: Ipv4Addr, +} + +/// The source a datagram asks to be sent from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Source { + /// [ip] chooses (§3.5). + Any, + /// The socket's address: only the interface holding it is eligible. + Bound(Ipv4Addr), + /// 0.0.0.0: only to the limited broadcast, while acquiring an address (RFC 2131 §4.1). + Unspecified, +} + +pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source, bound: Option) -> Result { + let eligible = |index: usize, i: &Interface| { + i.up + && bound.is_none_or(|b| b.0 == index) + && match source { + Source::Bound(addr) => i.is_usable(addr), + Source::Any | Source::Unspecified => true, + } + }; + let candidates = || ifaces.iter().enumerate().filter(|&(n, i)| eligible(n, i)); + let pick_source = |i: &Interface, toward: Ipv4Addr| match source { + Source::Bound(addr) => Ok(addr), + Source::Unspecified => Err(Counter::RouteNoSourceAddress), + Source::Any => i.source_for(toward).ok_or(Counter::RouteNoSourceAddress), + }; + if destination.is_broadcast() { + let mut up = candidates(); + let (index, i) = up.next().ok_or(Counter::RouteNone)?; + if up.next().is_some() { + return Err(Counter::RouteAmbiguousInterface); + } + let source = match source { + Source::Unspecified => Ipv4Addr::UNSPECIFIED, + _ => pick_source(i, destination)?, + }; + return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Broadcast, source }); + } + let [first, ..] = destination.octets(); + if first == 0 || first == 127 || is_class_e(destination) { + return Err(Counter::RouteInvalidDestination); + } + if ifaces.iter().any(|i| i.owns(destination)) { + return Err(Counter::RouteLocalDestination); + } + if let Some(group) = MulticastAddr::new(destination) { + let (index, i) = candidates() + .find(|(_, i)| i.igmp.joined(group)) + .or_else(|| candidates().next()) + .ok_or(Counter::RouteNone)?; + return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Multicast(group), source: pick_source(i, destination)? }); + } + if !candidates().any(|(_, i)| i.usable().next().is_some()) { + return Err(Counter::RouteNoSourceAddress); + } + let connected = candidates() + .flat_map(|(index, i)| i.usable().filter(|a| a.cidr.contains(destination)).map(move |a| (index, i, a.cidr))) + .fold(None, |best: Option<(usize, &Interface, crate::addr::Cidr)>, (index, i, cidr)| match best { + Some((_, _, b)) if b.len >= cidr.len => best, + _ => Some((index, i, cidr)), + }); + if let Some((index, i, cidr)) = connected { + 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)? }); + } + 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)? }) +} + +/// §3.4: the first gateway not FAILED and not UNREACHABLE; with none, the first UNREACHABLE one, +/// which still has a MAC, else the first. +fn active(i: &Interface) -> Option { + let state = |g: &Ipv4Addr| i.neighbours.get(g).map(|n| &n.state); + i.gateways + .iter() + .find(|g| !matches!(state(g), Some(Nud::Failed(_) | Nud::Unreachable(_)))) + .or_else(|| i.gateways.iter().find(|g| matches!(state(g), Some(Nud::Unreachable(_))))) + .or_else(|| i.gateways.first()) + .copied() +} + +/// A neighbour's reachability changed: the active gateway may have too (§3.4). +pub(crate) fn refresh_active(i: &mut Interface, cx: &mut Cx<'_>) { + let now = active(i); + if now != i.active { + if i.active.is_some() && now.is_some() { + cx.log.count(Counter::RouteGatewaySwitched); + } + i.active = now; + cx.bump(); + } +} + +/// Withdraws every gateway no usable prefix holds any more (§3.2). +pub(crate) fn withdraw_off_link(i: &mut Interface, cx: &mut Cx<'_>) { + let before = i.gateways.len(); + let on_link: alloc::vec::Vec = i.gateways.iter().copied().filter(|g| i.on_link(*g)).collect(); + for _ in on_link.len()..before { + cx.log.count(Counter::RouteGatewayWithdrawn); + } + i.gateways = on_link; + i.active = active(i); +} + +impl Ip { + /// Where a datagram to `destination` goes (§3.3), or the refusal a transport reports. + pub fn route(&mut self, destination: Ipv4Addr, source: Source, iface: Option) -> Result { + lookup(&self.ifaces, destination, source, iface).inspect_err(|&refusal| self.log.count(refusal)) + } + + /// Installs the interface's gateways, in order of preference (RFC 2132 §3.5). A list with any + /// refused entry is refused whole and the old one kept (§3.2). + pub fn set_gateways(&mut self, now: Instant, iface: IfIndex, gateways: &[Ipv4Addr]) -> Result<(), Counter> { + let now = self.clock(now); + let local: alloc::vec::Vec = self.ifaces.iter().flat_map(|i| i.addresses.iter().map(|a| a.cidr.addr)).collect(); + let Some((i, mut cx)) = self.split(now, iface) else { + self.log.count(Counter::UnknownInterface); + return Err(Counter::UnknownInterface); + }; + let refusal = if gateways.len() > limits::GATEWAYS { + Some(Counter::RouteTooManyGateways) + } else { + gateways.iter().find_map(|&g| { + if !is_host(g) || i.usable().any(|a| a.cidr.is_edge(g)) { + Some(Counter::RouteGatewayInvalid) + } else if local.contains(&g) { + Some(Counter::RouteGatewayIsLocal) + } else if !i.on_link(g) { + Some(Counter::RouteGatewayOffLink) + } else { + None + } + }) + }; + if let Some(refusal) = refusal { + cx.log.count(refusal); + return Err(refusal); + } + i.gateways = gateways.to_vec(); + i.active = active(i); + cx.bump(); + Ok(()) + } +} diff --git a/toyos-net-ip/src/timers.rs b/toyos-net-ip/src/timers.rs new file mode 100644 index 00000000000..3ebfecdcdcb --- /dev/null +++ b/toyos-net-ip/src/timers.rs @@ -0,0 +1,76 @@ +//! One deadline per state object, fired in deadline order, ties by kind — neighbour, ACD, IGMP — +//! and then by interface and address (§1.2 (2)). + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_wire::Instant; + +use crate::{igmp, IfIndex}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Timer { + Neighbour(IfIndex, Ipv4Addr), + Acd(IfIndex, Ipv4Addr), + Igmp(IfIndex, igmp::Timer), +} + +#[derive(Debug, Default)] +pub(crate) struct Timers { + at: BTreeMap, + order: BTreeSet<(Instant, Timer)>, +} + +impl Timers { + pub fn arm(&mut self, timer: Timer, at: Instant) { + self.cancel(timer); + self.at.insert(timer, at); + self.order.insert((at, timer)); + } + + pub fn cancel(&mut self, timer: Timer) { + if let Some(at) = self.at.remove(&timer) { + self.order.remove(&(at, timer)); + } + } + + pub fn get(&self, timer: Timer) -> Option { + self.at.get(&timer).copied() + } + + pub fn next(&self) -> Option { + self.order.first().map(|&(at, _)| at) + } + + /// Every timer due at `now`, in order and disarmed, so what one re-arms fires in a later call: + /// a jumped clock fires each state object once (§1.4 (1)). + pub fn due(&mut self, now: Instant) -> Vec { + let mut due = Vec::new(); + while let Some(&(at, timer)) = self.order.first() { + if at > now { + break; + } + self.order.pop_first(); + self.at.remove(&timer); + due.push(timer); + } + due + } + + /// Cancels every timer of `iface`. + pub fn cancel_iface(&mut self, iface: IfIndex, kind: impl Fn(&Timer) -> bool) { + let doomed: Vec = self + .at + .keys() + .filter(|t| match t { + Timer::Neighbour(i, _) | Timer::Acd(i, _) | Timer::Igmp(i, _) => *i == iface, + }) + .filter(|t| kind(t)) + .copied() + .collect(); + for timer in doomed { + self.cancel(timer); + } + } +} diff --git a/toyos-net-ip/tests/addr.rs b/toyos-net-ip/tests/addr.rs new file mode 100644 index 00000000000..c78a8f30196 --- /dev/null +++ b/toyos-net-ip/tests/addr.rs @@ -0,0 +1,417 @@ +//! §2 and §3: interface addressing and the host routing table. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{limits, AddrState, Cast, Counter, NextHop, Nud, Route, Source}; +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::ipv4::Ttl; + +fn route(h: &mut H, destination: Ipv4Addr) -> Result { + h.ip.route(destination, Source::Any, None) +} + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +/// Every probing interval scaled as ruled: the first announcement, when the address becomes +/// usable, lies within 200 ms of the add. +#[test] +fn s_ip_addr_001_an_added_address_becomes_a_connected_route() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Tentative)); + assert_eq!(route(&mut h, B), Err(Counter::RouteNoSourceAddress)); + h.run(200); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + h.run(3_000); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); + assert_eq!(route(&mut h, ip4(192, 0, 2, 77)), Ok(Route { iface: h.if0, next_hop: NextHop::Neighbour(ip4(192, 0, 2, 77)), source: A })); +} + +#[test] +fn s_ip_addr_002_prefix_lengths_0_and_33() { + let mut h = H::bare(); + for len in [0, 33] { + assert_eq!(h.ip.add_address(h.clock(), h.if0, A, len), Err(Counter::AddrPrefixInvalid)); + } + assert_eq!(h.count(Counter::AddrPrefixInvalid), 2); + assert_eq!(h.ip.address(h.if0, A), None); +} + +#[test] +fn s_ip_addr_003_not_unicast() { + let mut h = H::bare(); + for (addr, len) in [(ip4(0, 0, 0, 0), 24), (ip4(0, 1, 2, 3), 24), (ip4(127, 0, 0, 1), 8), (ip4(224, 0, 0, 1), 24), (ip4(240, 0, 0, 1), 24), (ip4(255, 255, 255, 255), 32)] { + assert_eq!(h.ip.add_address(h.clock(), h.if0, addr, len), Err(Counter::AddrNotUnicast), "{addr}/{len}"); + } +} + +#[test] +fn s_ip_addr_004_network_and_broadcast_forms() { + let mut h = H::bare(); + for addr in [ip4(192, 0, 2, 0), ip4(192, 0, 2, 255)] { + assert_eq!(h.ip.add_address(h.clock(), h.if0, addr, 24), Err(Counter::AddrNotUnicast)); + } + for (addr, len) in [(ip4(192, 0, 2, 0), 31), (ip4(192, 0, 2, 1), 31), (ip4(192, 0, 2, 255), 32)] { + assert_eq!(h.ip.add_address(h.clock(), h.if0, addr, len), Ok(()), "{addr}/{len}"); + } +} + +#[test] +fn s_ip_addr_005_one_address_one_interface() { + let mut h = H::raw(); + let (if0, if1) = (h.if0, h.add_if1()); + h.assign(if0, A, 24, 0); + h.settle(); + assert_eq!(h.ip.add_address(h.clock(), if1, A, 24), Err(Counter::AddrDuplicate)); + assert_eq!(h.ip.add_address(h.clock(), if0, A, 25), Ok(())); + assert_eq!(h.ip.prefix_len(if0, A), Some(25)); + assert_eq!(h.ip.address(if0, A), Some(AddrState::Assigned)); +} + +#[test] +fn s_ip_addr_006_eight_addresses() { + let mut h = H::bare(); + for n in 0..limits::ADDRS as u8 { + h.ip.add_address(h.clock(), h.if0, ip4(192, 0, 2, 10 + n), 24).unwrap(); + } + assert_eq!(h.ip.add_address(h.clock(), h.if0, ip4(192, 0, 2, 30), 24), Err(Counter::AddrTooMany)); +} + +fn to(destination: Ipv4Addr) -> Vec { + udp(B, destination, 5000, 5001, b"hi") +} + +#[test] +fn s_ip_addr_007_what_is_for_us() { + let mut h = H::fixture_i(); + for (destination, frame, cast) in [ + (A, MAC_A, Cast::Unicast), + (ip4(192, 0, 2, 255), MacAddr::BROADCAST, Cast::SubnetBroadcast), + (ip4(255, 255, 255, 255), MacAddr::BROADCAST, Cast::LimitedBroadcast), + (ip4(224, 0, 0, 1), MacAddr([1, 0, 0x5e, 0, 0, 1]), Cast::Multicast(group(ip4(224, 0, 0, 1)))), + (MDNS, MacAddr([1, 0, 0x5e, 0, 0, 0xfb]), Cast::Multicast(group(MDNS))), + ] { + assert_eq!(cast_of(&h.datagram_to(frame, &to(destination))), Some(cast), "{destination}"); + } + for destination in [ip4(224, 0, 0, 252), ip4(192, 0, 2, 7)] { + assert_eq!(cast_of(&h.datagram_to(MAC_A, &to(destination))), None); + } + assert_eq!(h.count(Counter::IpNotForUs), 2); +} + +#[test] +fn s_ip_addr_008_slash_31_and_32() { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, ip4(192, 0, 2, 0), 31, 0); + h.settle(); + assert_eq!(cast_of(&h.datagram(&udp(ip4(192, 0, 2, 1), ip4(192, 0, 2, 0), 5000, 5001, b"hi"))), Some(Cast::Unicast)); + assert_eq!(cast_of(&h.datagram(&udp(ip4(198, 51, 100, 9), ip4(192, 0, 2, 1), 5000, 5001, b"hi"))), None); + assert_eq!(h.count(Counter::IpNotForUs), 1); + + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, A, 32, 0); + h.settle(); + assert_eq!(cast_of(&h.datagram(&to(A))), Some(Cast::Unicast)); + assert_eq!(cast_of(&h.datagram(&to(ip4(192, 0, 2, 255)))), None); + assert_eq!(h.count(Counter::IpNotForUs), 1); +} + +#[test] +fn s_ip_addr_009_the_strong_model() { + 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!(cast_of(&h.datagram(&to(ip4(198, 51, 100, 1)))), None); + assert_eq!(h.count(Counter::IpNotForUs), 1); +} + +#[test] +fn s_ip_addr_010_limited_broadcast_needs_no_address() { + let mut h = H::bare(); + let offer = udp(ip4(192, 0, 2, 254), ip4(255, 255, 255, 255), 67, 68, b"offer"); + assert_eq!(cast_of(&h.datagram_to(MacAddr::BROADCAST, &offer)), Some(Cast::LimitedBroadcast)); +} + +#[test] +fn s_ip_addr_011_a_tentative_address_is_not_ours_yet() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + assert_eq!(cast_of(&h.datagram(&to(A))), None); + assert_eq!(h.count(Counter::IpTentativeDestination), 1); + h.frame(&hex(V_ARP_REQ_B)); + assert!(h.out().iter().all(|o| o.arp().is_none_or(|a| a.operation != toyos_net_wire::arp::Operation::Reply))); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Tentative)); + assert_eq!(h.count(Counter::AcdConflict), 0); +} + +#[test] +fn s_ip_addr_012_no_classful_broadcast() { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, ip4(10, 1, 2, 3), 24, 0); + h.settle(); + for destination in [ip4(10, 255, 255, 255), ip4(10, 1, 2, 0)] { + assert_eq!(cast_of(&h.datagram(&udp(ip4(10, 1, 2, 9), destination, 5000, 5001, b"hi"))), None, "{destination}"); + } + assert_eq!(h.count(Counter::IpNotForUs), 2); + let delivered = h.datagram_to(MacAddr::BROADCAST, &udp(ip4(10, 1, 2, 9), ip4(10, 1, 2, 255), 5000, 5001, b"hi")); + assert_eq!(cast_of(&delivered), Some(Cast::SubnetBroadcast)); +} + +#[test] +fn s_ip_addr_013_announcing_is_usable() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + h.run(200); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + assert_eq!(cast_of(&h.datagram(&to(A))), Some(Cast::Unicast)); + h.frame(&hex(V_ARP_REQ_B)); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_REPLY_A)]); +} + +#[test] +fn s_ip_addr_014_removal_withdraws_route_and_gateway() { + let mut h = H::fixture_i(); + let generation = h.ip.generation(); + h.ip.remove_address(h.clock(), h.if0, A).unwrap(); + assert_eq!(h.count(Counter::RouteGatewayWithdrawn), 1); + assert_eq!(h.ip.gateways(h.if0), Some(&[][..])); + assert!(h.ip.generation() > generation); + assert_eq!(route(&mut h, B), Err(Counter::RouteNoSourceAddress)); + assert!(!h.ip.is_assigned(A)); +} + +#[test] +fn s_ip_rte_001_a_connected_destination() { + let mut h = H::fixture_i(); + assert_eq!(route(&mut h, B), Ok(Route { iface: h.if0, next_hop: NextHop::Neighbour(B), source: A })); +} + +#[test] +fn s_ip_rte_002_the_default_gateway() { + let mut h = H::fixture_i(); + assert_eq!(route(&mut h, REMOTE), Ok(Route { iface: h.if0, next_hop: NextHop::Neighbour(R), source: A })); +} + +#[test] +fn s_ip_rte_003_no_gateway() { + let mut h = H::fixture_i(); + h.ip.set_gateways(h.clock(), h.if0, &[]).unwrap(); + assert_eq!(route(&mut h, REMOTE), Err(Counter::RouteNone)); + assert_eq!(h.count(Counter::RouteNone), 1); +} + +#[test] +fn s_ip_rte_004_longest_prefix() { + let mut h = H::raw(); + let (if0, if1) = (h.if0, h.add_if1()); + h.assign(if0, A, 24, 0); + h.assign(if1, ip4(192, 0, 2, 129), 25, 5_000); + h.settle(); + assert_eq!(route(&mut h, ip4(192, 0, 2, 200)).map(|r| r.iface), Ok(if1)); + assert_eq!(route(&mut h, ip4(192, 0, 2, 100)).map(|r| r.iface), Ok(if0)); +} + +#[test] +fn s_ip_rte_005_gateways_are_on_link_unicast_hosts() { + let mut h = H::fixture_i(); + for gateway in [ip4(224, 0, 0, 1), ip4(192, 0, 2, 255), ip4(192, 0, 2, 0)] { + assert_eq!(h.ip.set_gateways(h.clock(), h.if0, &[gateway]), Err(Counter::RouteGatewayInvalid), "{gateway}"); + } + assert_eq!(h.ip.set_gateways(h.clock(), h.if0, &[A]), Err(Counter::RouteGatewayIsLocal)); + assert_eq!(h.ip.set_gateways(h.clock(), h.if0, &[ip4(198, 51, 100, 1)]), Err(Counter::RouteGatewayOffLink)); + assert_eq!(h.ip.gateways(h.if0), Some(&[R][..])); +} + +#[test] +fn s_ip_rte_006_invalid_and_local_destinations() { + let mut h = H::fixture_i(); + for destination in [ip4(127, 0, 0, 1), ip4(0, 0, 0, 0), ip4(0, 1, 2, 3), ip4(240, 0, 0, 1)] { + assert_eq!(route(&mut h, destination), Err(Counter::RouteInvalidDestination), "{destination}"); + } + assert_eq!(route(&mut h, A), Err(Counter::RouteLocalDestination)); +} + +#[test] +fn s_ip_rte_007_limited_broadcast_needs_one_interface() { + 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(); + let limited = ip4(255, 255, 255, 255); + assert_eq!(h.ip.route(limited, Source::Any, None), Err(Counter::RouteAmbiguousInterface)); + let bound = h.ip.route(limited, Source::Any, Some(if1)).unwrap(); + assert_eq!((bound.iface, bound.next_hop), (if1, NextHop::Broadcast)); + let frame = h.send(ip4(198, 51, 100, 1), limited, 5001, 5001, b"hi").unwrap().unwrap(); + assert_eq!(destination_of(&frame), MacAddr::BROADCAST); + h.ip.link_down(h.clock(), if1).unwrap(); + assert_eq!(h.ip.route(limited, Source::Any, None).map(|r| r.iface), Ok(if0)); +} + +#[test] +fn s_ip_rte_008_multicast_ttl_is_the_callers() { + let mut h = H::fixture_i(); + for ttl in [1, 255] { + let frame = h.send_ttl(A, MDNS, 5353, 5353, b"q", Ttl::new(ttl).unwrap()).unwrap().unwrap(); + assert_eq!(destination_of(&frame), MacAddr([1, 0, 0x5e, 0, 0, 0xfb])); + assert_eq!(ip_of(&frame).unwrap().ttl(), ttl); + } +} + +#[test] +fn s_ip_rte_009_subnet_broadcast_needs_no_arp() { + let mut h = H::fixture_i(); + let frame = h.udp_to(ip4(192, 0, 2, 255)).unwrap().unwrap(); + assert_eq!(destination_of(&frame), MacAddr::BROADCAST); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_rte_010_the_source_of_the_next_hops_prefix() { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, A, 24, 0); + h.assign(if0, ip4(198, 51, 100, 1), 24, 5_000); + h.ip.set_gateways(toyos_net_ip::Instant::from_millis(9_000), if0, &[R]).unwrap(); + h.settle(); + assert_eq!(route(&mut h, ip4(203, 0, 113, 9)).map(|r| r.source), Ok(A)); + assert_eq!(route(&mut h, ip4(198, 51, 100, 9)).map(|r| r.source), Ok(ip4(198, 51, 100, 1))); +} + +#[test] +fn s_ip_rte_011_a_bound_source_picks_its_interface() { + 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.ip.set_gateways(toyos_net_ip::Instant::from_millis(9_000), if0, &[R]).unwrap(); + h.settle(); + let bound = Source::Bound(ip4(198, 51, 100, 1)); + assert_eq!(h.ip.route(B, bound, None), Err(Counter::RouteNone)); + h.ip.set_gateways(h.clock(), if1, &[ip4(198, 51, 100, 254)]).unwrap(); + let r = h.ip.route(B, bound, None).unwrap(); + assert_eq!((r.iface, r.next_hop), (if1, NextHop::Neighbour(ip4(198, 51, 100, 254)))); +} + +#[test] +fn s_ip_rte_012_only_the_acquiring_source_is_unspecified() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + assert_eq!(h.udp_to(B), Err(Counter::RouteNoSourceAddress)); + let frame = h.send(ip4(0, 0, 0, 0), ip4(255, 255, 255, 255), 68, 67, b"discover").unwrap().unwrap(); + assert_eq!(ip_of(&frame).unwrap().source(), ip4(0, 0, 0, 0)); + assert_eq!(destination_of(&frame), MacAddr::BROADCAST); +} + +/// Gateways [192.0.2.254, 192.0.2.253], neither resolved. +fn two_gateways() -> H { + let mut h = H::fixture_i(); + h.ip.set_gateways(h.clock(), h.if0, &[R, ip4(192, 0, 2, 253)]).unwrap(); + h.rebase(); + h +} + +fn fail(h: &mut H, gateway: Ipv4Addr) { + let start = h.now; + let _ = h.ip.resolve(h.clock(), h.if0, gateway); + h.out(); + h.run(start + 3_000); + assert!(matches!(h.state(gateway), Some(Nud::Failed(_)))); +} + +#[test] +fn s_ip_rte_013_a_failed_gateway_is_passed_over() { + let mut h = two_gateways(); + let generation = h.ip.generation(); + fail(&mut h, R); + assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(ip4(192, 0, 2, 253)))); + assert_eq!(h.count(Counter::RouteGatewaySwitched), 1); + assert!(h.ip.generation() > generation); + h.run(3_000 + limits::nud::FAILED_HOLD.as_millis() as u64); + assert!(h.state(R).is_none()); + assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(R))); +} + +#[test] +fn s_ip_rte_014_with_no_good_gateway() { + let mut h = two_gateways(); + fail(&mut h, R); + fail(&mut h, ip4(192, 0, 2, 253)); + assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(R))); + + let mut h = two_gateways(); + let other = ip4(192, 0, 2, 253); + fail(&mut h, R); + h.stale(other, MAC_X); + h.udp_to(REMOTE).unwrap(); + let t = h.now; + h.run(t + 8_000); + assert!(matches!(h.state(other), Some(Nud::Unreachable(_)))); + assert!(matches!(h.state(R), Some(Nud::Failed(_)))); + assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(other))); +} + +#[test] +fn s_ip_rte_015_what_moves_the_generation() { + let mut h = H::fixture_i_with(R, MAC_R); + let mut last = h.ip.generation(); + let mut moved = |h: &H| { + let now = h.ip.generation(); + let changed = now != last; + last = now; + changed + }; + h.run(45_000); + assert!(h.is_stale(R)); + assert!(!moved(&h), "REACHABLE to STALE"); + h.ip.add_address(h.clock(), h.if0, ip4(192, 0, 2, 9), 24).unwrap(); + assert!(!moved(&h), "a tentative address alters no lookup"); + h.run(45_200); + assert!(moved(&h), "address added"); + h.ip.remove_address(h.clock(), h.if0, ip4(192, 0, 2, 9)).unwrap(); + assert!(moved(&h), "address removed"); + h.ip.set_gateways(h.clock(), h.if0, &[R, ip4(192, 0, 2, 253)]).unwrap(); + assert!(moved(&h), "gateway list changed"); + fail(&mut h, ip4(192, 0, 2, 50)); + assert!(!moved(&h), "a neighbour that is no gateway failing"); + h.udp_to(REMOTE).unwrap(); + let t = h.now; + h.run(t + 7_999); + assert!(!moved(&h), "STALE, DELAY and PROBE are usable"); + h.run(t + 8_000); + assert!(matches!(h.state(R), Some(Nud::Unreachable(_)))); + assert!(moved(&h), "active gateway changed"); + h.ip.link_down(h.clock(), h.if0).unwrap(); + assert!(moved(&h), "link down"); + h.ip.link_up(h.clock(), h.if0).unwrap(); + assert!(moved(&h), "link up"); +} + +#[test] +fn s_ip_rte_016_no_forwarding() { + let mut h = H::fixture_i(); + assert!(h.datagram(&udp(B, REMOTE, 5000, 5001, b"hi")).is_none()); + assert_eq!(h.count(Counter::IpNotForUs), 1); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_rte_017_lookups_send_nothing() { + let mut h = H::fixture_i(); + for t in (0..=600_000).step_by(1_000) { + h.at(t); + assert!(route(&mut h, REMOTE).is_ok()); + assert!(h.out().is_empty()); + } + assert!(h.run(600_000).is_empty()); +} diff --git a/toyos-net-ip/tests/clk.rs b/toyos-net-ip/tests/clk.rs new file mode 100644 index 00000000000..864ec22382a --- /dev/null +++ b/toyos-net-ip/tests/clk.rs @@ -0,0 +1,229 @@ +//! §1: the injected clock, deadlines, keyed draws and the moved clock. + +mod common; + +use common::*; +use toyos_net_ip::{limits, AddrState, Counter, Instant, Nud}; + +fn incomplete_b() -> H { + let mut h = H::fixture_i(); + assert_eq!(h.udp_to(B), Ok(None)); + assert!(h.out()[0].requests(B)); + h +} + +#[test] +fn s_ip_clk_001_a_send_arms_the_retransmission() { + let h = incomplete_b(); + assert!(matches!(h.state(B), Some(Nud::Incomplete(_)))); + assert_eq!(h.ip.next_deadline(), Some(H::instant(1_000))); +} + +#[test] +fn s_ip_clk_002_nothing_fires_before_its_deadline() { + let mut h = incomplete_b(); + assert!(h.fire(999).is_empty()); + let out = h.fire(1_000); + assert_eq!(out.len(), 1); + assert!(out[0].requests(B)); +} + +#[test] +fn s_ip_clk_003_firing_more_often_changes_nothing() { + let mut every_ms = incomplete_b(); + let mut sent_every_ms = vec![0]; + for t in 1..=3_000 { + if !every_ms.fire(t).is_empty() { + sent_every_ms.push(t); + } + } + let mut at_deadlines = incomplete_b(); + let sent_at_deadlines: Vec = std::iter::once(0).chain(at_deadlines.run(3_000).iter().map(|o| o.at)).collect(); + assert_eq!(sent_every_ms, [0, 1_000, 2_000]); + assert_eq!(sent_at_deadlines, sent_every_ms); + for h in [&every_ms, &at_deadlines] { + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert_eq!(h.count(Counter::NbFailed), 1); + } +} + +#[test] +fn s_ip_clk_004_a_jump_fires_a_reachable_entry_once() { + let mut h = H::fixture_i_with(R, MAC_R); + assert!(h.fire(3_600_000).is_empty()); + assert!(h.is_stale(R)); + assert!(matches!(h.udp_to(REMOTE), Ok(Some(frame)) if destination_of(&frame) == MAC_R)); + assert!(matches!(h.state(R), Some(Nud::Delay(_)))); +} + +/// R UNREACHABLE after PROBE's three unanswered polls, at the returned time. +fn unreachable_r() -> (H, u64) { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + h.udp_to(REMOTE).unwrap(); + h.run(8_000); + assert!(matches!(h.state(R), Some(Nud::Unreachable(_)))); + h.rebase(); + (h, 8_000) +} + +#[test] +fn s_ip_clk_005_a_jump_owes_one_request() { + let (mut h, t0) = unreachable_r(); + h.at(t0 + 100); + h.udp_to(REMOTE).unwrap(); + let first = h.out(); + assert_eq!(first.iter().filter(|o| o.requests(R)).count(), 1); + h.at(t0 + 200); + h.udp_to(REMOTE).unwrap(); + let jump = t0 + 200 + 600_000; + let out = h.fire(jump); + assert_eq!(out.iter().filter(|o| o.requests(R)).count(), 1, "one request at the jump"); + assert_eq!(h.ip.next_deadline(), Some(H::instant(jump + 9_000)), "RETRANS x 3^2 from the jump"); +} + +/// The trace of a seeded run: every frame, every event, every counter. +fn trace(secret: [u8; 16], seed: u64) -> (Vec<(u64, Vec)>, Vec, Vec, std::time::Duration) { + let mut ip = toyos_net_ip::Ip::new(Instant::from_nanos(0), secret); + let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_A)); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + let mut rng = Rng(seed); + let mut frames = Vec::new(); + let mut events = Vec::new(); + let inputs = [ + hex(V_ARP_REQ_B), + hex(V_ARP_REPLY_R), + hex(V_ARP_CONFLICT_B), + to_a(&hex(V_UDP_HI)), + to_a(&hex(V_ICMP_ECHO).iter().copied().collect::>()), + eth(MAC_A, MAC_R, 0x0800, &hex(V_IGMP3_QUERY_GEN)), + ]; + let mut now = 0u64; + ip.add_address(Instant::from_millis(0), if0, A, 24).unwrap(); + for _ in 0..400 { + now += rng.below(700); + let at = Instant::from_millis(now); + match rng.below(6) { + 0 => { + let input = &inputs[rng.below(inputs.len() as u64) as usize]; + let _ = ip.receive(at, if0, input); + } + 1 => { + let destination = std::net::Ipv4Addr::new(192, 0, 2, 2 + rng.below(4) as u8); + let mut frame = [0u8; toyos_net_ip::FRAME]; + let datagram = toyos_net_wire::udp::UdpBuilder { + source: toyos_net_wire::Port::new(5001).unwrap(), + destination: toyos_net_wire::Port::new(5001).unwrap(), + data: b"x", + }; + let out = toyos_net_ip::UdpOut { source: A, destination, ttl: toyos_net_wire::ipv4::Ttl::DEFAULT, datagram }; + if let Ok(toyos_net_ip::Sent::Frame(n)) = ip.send_udp(at, &out, &mut frame) { + frames.push((now, frame[..n].to_vec())); + } + } + 2 => ip.fire(at), + 3 => { + let _ = ip.join(at, if0, group(std::net::Ipv4Addr::new(239, 1, 1, rng.below(3) as u8))); + } + _ => { + ip.transmit(at, rng.below(4) as usize, |_, f| frames.push((now, f.to_vec()))); + } + } + events.extend(ip.drain_events().map(|e| format!("{e:?}"))); + } + let counters = Counter::ALL.iter().map(|&c| ip.counters().get(c)).collect(); + (frames, events, counters, ip.reachable_time(if0).unwrap()) +} + +#[test] +fn s_ip_clk_006_prop_one_secret_one_run() { + for seed in 1..=24 { + let first = trace(SECRET, seed); + let second = trace(SECRET, seed); + assert_eq!(first.0, second.0, "seed {seed}: frames"); + assert_eq!(first.1, second.1, "seed {seed}: events"); + assert_eq!(first.2, second.2, "seed {seed}: counters"); + let other = trace([0xa5; 16], seed); + for reachable in [first.3, other.3] { + assert!((limits::nud::REACHABLE_MIN..=limits::nud::REACHABLE_MAX).contains(&reachable)); + } + } + assert_ne!(trace(SECRET, 1).3, trace([0xa5; 16], 1).3, "the secret draws the reachable time"); +} + +#[test] +fn s_ip_clk_007_a_regressed_clock_is_the_latest() { + let mut h = incomplete_b(); + h.fire(5_000); + let deadline = h.ip.next_deadline(); + h.ip.fire(H::instant(4_000)); + assert_eq!(h.ip.transmit(H::instant(4_000), usize::MAX, |_, _| {}), 0); + assert_eq!(h.count(Counter::ClockRegressed), 2); + assert_eq!(h.ip.next_deadline(), deadline); + let mut again = incomplete_b(); + again.fire(5_000); + assert_eq!(h.fire(5_000).len(), again.fire(5_000).len()); + assert_eq!(format!("{:?}", h.state(B)), format!("{:?}", again.state(B))); +} + +#[test] +fn s_ip_clk_008_announce_wait_counts_from_the_hand_off() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + let mut probes = 0; + let mut third_left = 0; + while probes < 3 { + let deadline = h.ip.next_deadline().unwrap(); + let t = deadline.nanos() / 1_000_000 - BASE_MS; + h.ip.fire(deadline); + h.now = t; + let late = if probes == 2 { t + 500 } else { t }; + h.at(late); + let out = h.out(); + probes += out.iter().filter(|o| o.arp().is_some_and(|a| a.sender_ip.is_unspecified())).count(); + third_left = late; + } + let due = h.ip.next_deadline().unwrap(); + assert_eq!(due, H::instant(third_left).after(limits::acd::ANNOUNCE_WAIT)); + h.ip.fire(due); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); +} + +#[test] +fn s_ip_clk_009_a_jump_refills_a_bucket_to_its_cap() { + let mut h = H::fixture_i_with(B, MAC_B); + let error_to_b = |h: &mut H| { + let arrival = match h.datagram(&hex(V_UDP_HI)) { + Some(toyos_net_ip::Delivery::Udp(arrival, _)) => arrival, + other => panic!("{other:?}"), + }; + h.ip.port_unreachable(h.clock(), &arrival); + }; + for _ in 0..10 { + error_to_b(&mut h); + } + assert_eq!(h.out().len(), 10); + h.at(3_600_000); + for _ in 0..11 { + error_to_b(&mut h); + } + assert_eq!(h.out().iter().filter(|o| o.to() == MAC_B).count(), 10); + assert_eq!(h.count(Counter::IcmpErrorRateLimited), 1); +} + +#[test] +fn s_ip_clk_010_a_deadline_saturates() { + let mut h = H::fixture_i(); + let late = Instant::from_nanos(u64::MAX - 500); + let mut frame = [0u8; toyos_net_ip::FRAME]; + let datagram = toyos_net_wire::udp::UdpBuilder { + source: toyos_net_wire::Port::new(5001).unwrap(), + destination: toyos_net_wire::Port::new(5001).unwrap(), + data: b"hi", + }; + let out = toyos_net_ip::UdpOut { source: A, destination: B, ttl: toyos_net_wire::ipv4::Ttl::DEFAULT, datagram }; + assert_eq!(h.ip.send_udp(late, &out, &mut frame), Ok(toyos_net_ip::Sent::Held)); + assert_eq!(h.ip.transmit(late, usize::MAX, |_, _| {}), 1); + assert!(matches!(h.state(B), Some(Nud::Incomplete(_)))); + assert_eq!(h.ip.next_deadline(), Some(Instant::from_nanos(u64::MAX))); +} diff --git a/toyos-net-ip/tests/common/mod.rs b/toyos-net-ip/tests/common/mod.rs new file mode 100644 index 00000000000..b4a53ba6962 --- /dev/null +++ b/toyos-net-ip/tests/common/mod.rs @@ -0,0 +1,789 @@ +//! The scenario harness: fixtures reached through the public API only, a clock in scenario +//! milliseconds from the fixture's t = 0, frames out with the time they left, counters as deltas +//! from t = 0, and an RFC 1071 sum written here, independent of the crate's, for every +//! "(checksum fixed)" mutation. + +#![allow(dead_code)] + +use std::net::Ipv4Addr; + +use toyos_net_ip::{AddrState, Counter, Event, IfIndex, Instant, Ip, Nud, Sent, UdpOut, FRAME}; +use toyos_net_wire::ipv4::Ttl; +use toyos_net_wire::udp::UdpBuilder; +use toyos_net_wire::Port; +use toyos_net_wire::arp::Arp; +use toyos_net_wire::ethernet::{Frame, IndividualMac, MacAddr}; +use toyos_net_wire::ipv4::{Ipv4Packet, MulticastAddr}; + +pub const MAC_A: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0a]); +pub const MAC_B: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0b]); +pub const MAC_R: MacAddr = MacAddr([2, 0, 0, 0, 0, 0xfe]); +pub const MAC_X: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x66]); +pub const MAC_DNS: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x35]); +pub const A: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1); +pub const B: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 2); +pub const R: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 254); +pub const DNS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 53); +pub const REMOTE: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 7); +pub const MDNS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); +pub const SECRET: [u8; 16] = [0x5a; 16]; +/// Scenario t = 0, after every fixture's setup has settled. +pub const BASE_MS: u64 = 10_000; + +pub fn mac(m: MacAddr) -> IndividualMac { + IndividualMac::new(m).unwrap() +} + +pub fn group(a: Ipv4Addr) -> MulticastAddr { + MulticastAddr::new(a).unwrap() +} + +pub fn hex(text: &str) -> Vec { + text.split_whitespace().map(|b| u8::from_str_radix(b, 16).unwrap()).collect() +} + +/// RFC 1071's sum, big-endian words, odd byte padded: the oracle, not the crate's. +pub fn oracle_sum(data: &[u8]) -> u16 { + let mut sum: u32 = 0; + for chunk in data.chunks(2) { + let word = if chunk.len() == 2 { u32::from(chunk[0]) << 8 | u32::from(chunk[1]) } else { u32::from(chunk[0]) << 8 }; + sum += word; + while sum > 0xffff { + sum = (sum & 0xffff) + (sum >> 16); + } + } + sum as u16 +} + +pub fn fix_ip_header(ip: &mut [u8]) { + let ihl = usize::from(ip[0] & 0x0f) * 4; + ip[10] = 0; + ip[11] = 0; + let c = !oracle_sum(&ip[..ihl]); + ip[10..12].copy_from_slice(&c.to_be_bytes()); +} + +fn pseudo(ip: &[u8], protocol: u8, length: usize) -> Vec { + let mut p = ip[12..20].to_vec(); + p.extend_from_slice(&[0, protocol]); + p.extend_from_slice(&(length as u16).to_be_bytes()); + p +} + +/// Recomputes every checksum of an IPv4 datagram: header, then UDP, TCP, ICMP or IGMP. +pub fn fix(ip: &mut Vec) { + fix_ip_header(ip); + let ihl = usize::from(ip[0] & 0x0f) * 4; + let total = usize::from(u16::from_be_bytes([ip[2], ip[3]])); + let protocol = ip[9]; + let header = ip[..ihl].to_vec(); + let body = &mut ip[ihl..total]; + match protocol { + 1 | 2 => { + body[2] = 0; + body[3] = 0; + let c = !oracle_sum(body); + body[2..4].copy_from_slice(&c.to_be_bytes()); + } + 17 => { + let length = usize::from(u16::from_be_bytes([body[4], body[5]])); + if body[6] == 0 && body[7] == 0 { + return; + } + body[6] = 0; + body[7] = 0; + let mut all = pseudo(&header, 17, length); + all.extend_from_slice(&body[..length]); + let mut c = !oracle_sum(&all); + if c == 0 { + c = 0xffff; + } + body[6..8].copy_from_slice(&c.to_be_bytes()); + } + 6 => { + body[16] = 0; + body[17] = 0; + let mut all = pseudo(&header, 6, body.len()); + all.extend_from_slice(body); + let c = !oracle_sum(&all); + body[16..18].copy_from_slice(&c.to_be_bytes()); + } + _ => {} + } +} + +/// A datagram with its fields edited and every checksum fixed. +pub fn edit(mut ip: Vec, change: impl FnOnce(&mut Vec)) -> Vec { + change(&mut ip); + fix(&mut ip); + ip +} + +pub fn set_source(ip: &mut [u8], a: Ipv4Addr) { + ip[12..16].copy_from_slice(&a.octets()); +} + +pub fn set_destination(ip: &mut [u8], a: Ipv4Addr) { + ip[16..20].copy_from_slice(&a.octets()); +} + +/// A minimal IPv4 datagram: atomic, TTL 64, no options, checksums fixed. +pub fn ipv4(source: Ipv4Addr, destination: Ipv4Addr, protocol: u8, payload: &[u8]) -> Vec { + let total = 20 + payload.len(); + let mut ip = vec![0x45, 0, (total >> 8) as u8, total as u8, 0, 0, 0x40, 0, 64, protocol, 0, 0]; + ip.extend_from_slice(&source.octets()); + ip.extend_from_slice(&destination.octets()); + ip.extend_from_slice(payload); + fix(&mut ip); + ip +} + +pub fn udp(source: Ipv4Addr, destination: Ipv4Addr, sport: u16, dport: u16, data: &[u8]) -> Vec { + let length = 8 + data.len(); + let mut u = vec![(sport >> 8) as u8, sport as u8, (dport >> 8) as u8, dport as u8, (length >> 8) as u8, length as u8, 0xff, 0xff]; + u.extend_from_slice(data); + ipv4(source, destination, 17, &u) +} + +/// An Ethernet frame around `body`. +pub fn eth(destination: MacAddr, source: MacAddr, ether_type: u16, body: &[u8]) -> Vec { + let mut f = destination.0.to_vec(); + f.extend_from_slice(&source.0); + f.extend_from_slice(ðer_type.to_be_bytes()); + f.extend_from_slice(body); + f +} + +pub fn to_a(ip: &[u8]) -> Vec { + eth(MAC_A, MAC_B, 0x0800, ip) +} + +pub fn ip_of(frame: &[u8]) -> Option> { + let f = Frame::parse(frame).ok()?; + Ipv4Packet::parse(f.body()).ok() +} + +pub fn ip_bytes(frame: &[u8]) -> Vec { + ip_of(frame).unwrap().bytes().to_vec() +} + +pub fn arp_of(frame: &[u8]) -> Option { + let f = Frame::parse(frame).ok()?; + Arp::parse(f.body()).ok() +} + +pub fn destination_of(frame: &[u8]) -> MacAddr { + Frame::parse(frame).unwrap().destination() +} + +/// A frame left at scenario time `at` ms. +#[derive(Clone, Debug)] +pub struct Out { + pub at: u64, + pub iface: IfIndex, + pub frame: Vec, +} + +impl Out { + pub fn arp(&self) -> Option { + arp_of(&self.frame) + } + + pub fn ip(&self) -> Option> { + ip_of(&self.frame) + } + + pub fn to(&self) -> MacAddr { + destination_of(&self.frame) + } + + /// An ARP request for `target`. + pub fn requests(&self, target: Ipv4Addr) -> bool { + self.arp().is_some_and(|a| a.operation == toyos_net_wire::arp::Operation::Request && a.target_ip == target && !a.sender_ip.is_unspecified() && a.sender_ip != a.target_ip) + } +} + +pub struct H { + pub ip: Ip, + pub if0: IfIndex, + /// Scenario ms of the last call. + pub now: u64, + pub events: Vec, + counters: Vec<(Counter, u64)>, + wire: std::collections::BTreeMap<&'static str, u64>, +} + +impl H { + /// Interface `if0` with MAC A and its link up at time 0, nothing settled. + pub fn raw() -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), SECRET); + let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_A)); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: Default::default() } + } + + /// `if0` up with no address, at t = 0. + pub fn bare() -> Self { + let mut h = Self::raw(); + h.settle(); + h + } + + /// Scenario ms to the crate's clock. + pub fn instant(t: u64) -> Instant { + Instant::from_millis(BASE_MS + t) + } + + /// Setup time: every deadline up to `until`, each at its own time, with unlimited credit. + pub fn setup_until(&mut self, until: Instant) { + while let Some(d) = self.ip.next_deadline().filter(|d| *d <= until) { + self.ip.fire(d); + self.ip.transmit(d, usize::MAX, |_, _| {}); + } + self.ip.transmit(until, usize::MAX, |_, _| {}); + } + + /// Runs the setup to t = 0, then zeroes the scenario's view. + pub fn settle(&mut self) { + self.setup_until(Self::instant(0)); + self.now = 0; + self.rebase(); + } + + /// Counters and events count from here. + pub fn rebase(&mut self) { + self.counters = Counter::ALL.iter().map(|&c| (c, self.ip.counters().get(c))).collect(); + self.wire = self.ip.wire_counters().collect(); + self.ip.drain_events().for_each(drop); + self.events.clear(); + } + + /// Adds `addr/len` at setup time `at_ms` and runs conflict detection to assigned. + pub fn assign(&mut self, iface: IfIndex, addr: Ipv4Addr, len: u8, at_ms: u64) { + let at = Instant::from_millis(at_ms); + self.ip.add_address(at, iface, addr, len).unwrap(); + self.setup_until(Instant::from_millis(at_ms + 3_000)); + assert_eq!(self.ip.address(iface, addr), Some(AddrState::Assigned)); + } + + /// Fixture I's configuration, not yet settled. + pub fn raw_i() -> Self { + let mut h = Self::raw(); + let if0 = h.if0; + h.assign(if0, A, 24, 0); + let t = Instant::from_millis(3_000); + h.ip.set_gateways(t, if0, &[R]).unwrap(); + h.ip.join(t, if0, group(MDNS)).unwrap(); + h.setup_until(Instant::from_millis(5_000)); + h + } + + /// Fixture I: 192.0.2.1/24 assigned on if0 through conflict detection, gateway 192.0.2.254, + /// 224.0.0.251 joined, IGMPv3, every bucket full. + pub fn fixture_i() -> Self { + let mut h = Self::raw_i(); + h.settle(); + h + } + + pub fn clock(&self) -> Instant { + Self::instant(self.now) + } + + /// Fixture I with `addr` REACHABLE at `m`, confirmed at t = 0: resolved during setup, so no + /// request of ours is recent, and confirmed by advice at t = 0. + pub fn fixture_i_with(addr: Ipv4Addr, m: MacAddr) -> Self { + let mut h = Self::raw_i(); + let setup = Instant::from_millis(5_000); + let _ = h.ip.resolve(setup, h.if0, addr); + h.ip.transmit(setup, usize::MAX, |_, _| {}); + let reply = eth(MAC_A, m, 0x0806, &arp_packet(2, m, addr, MAC_A, A)); + let _ = h.ip.receive(setup, h.if0, &reply); + h.settle(); + h.ip.advise(h.clock(), addr, toyos_net_ip::Advice::Confirmed); + assert!(matches!(h.ip.neighbour(h.if0, addr), Some(Nud::Reachable(r)) if r.confirmed() == H::instant(0))); + h.rebase(); + h + } + + /// Resolves `addr` on if0 by a request and its reply, now. + pub fn reach(&mut self, addr: Ipv4Addr, m: MacAddr) { + let now = self.clock(); + let _ = self.ip.resolve(now, self.if0, addr); + self.ip.transmit(now, usize::MAX, |_, _| {}); + self.reply_from(addr, m); + self.ip.transmit(now, usize::MAX, |_, _| {}); + assert!(matches!(self.ip.neighbour(self.if0, addr), Some(Nud::Reachable(_))), "{addr} did not resolve"); + } + + /// `addr` at `m` answers a request of A's. + pub fn reply_from(&mut self, addr: Ipv4Addr, m: MacAddr) { + self.frame(ð(MAC_A, m, 0x0806, &arp_packet(2, m, addr, MAC_A, A))); + } + + pub fn count(&self, counter: Counter) -> u64 { + let base = self.counters.iter().find(|(c, _)| *c == counter).map_or(0, |(_, n)| *n); + self.ip.counters().get(counter) - base + } + + pub fn wire(&self, name: &str) -> u64 { + self.ip.wire_refusals(name) - self.wire.get(name).copied().unwrap_or(0) + } + + /// A frame received on if0 at the current time. + pub fn frame(&mut self, frame: &[u8]) -> Option> { + let leaked: &'static [u8] = Box::leak(frame.to_vec().into_boxed_slice()); + let delivery = self.ip.receive(self.clock(), self.if0, leaked); + self.collect(); + delivery + } + + /// An IPv4 datagram from B's MAC to A's. + pub fn datagram(&mut self, ip: &[u8]) -> Option> { + self.frame(&to_a(ip)) + } + + /// Moves the clock to `t` without firing anything. + pub fn at(&mut self, t: u64) { + self.now = t; + } + + /// Fires every deadline up to and including `t`, each at its own time, with unlimited credit + /// after each: a live loop. Returns every frame that left. + pub fn run(&mut self, t: u64) -> Vec { + let mut out = self.out(); + while let Some(d) = self.ip.next_deadline().filter(|d| *d <= Self::instant(t)) { + self.now = (d.nanos() / 1_000_000).saturating_sub(BASE_MS).max(self.now); + self.ip.fire(d); + out.extend(self.drain_at(d)); + } + self.now = t; + out.extend(self.out()); + out + } + + /// One `fire` at `t`, then a transmit opportunity. + pub fn fire(&mut self, t: u64) -> Vec { + self.now = t; + self.ip.fire(self.clock()); + self.out() + } + + /// A transmit opportunity with unlimited credit now. + pub fn out(&mut self) -> Vec { + let now = self.clock(); + self.drain_at(now) + } + + /// A transmit opportunity with room for `credit` frames. + pub fn out_with(&mut self, credit: usize) -> Vec { + let at = self.now; + let mut frames = Vec::new(); + self.ip.transmit(self.clock(), credit, |iface, f| frames.push(Out { at, iface, frame: f.to_vec() })); + self.collect(); + frames + } + + fn drain_at(&mut self, now: Instant) -> Vec { + let at = (now.nanos() / 1_000_000).saturating_sub(BASE_MS); + let mut frames = Vec::new(); + self.ip.transmit(now, usize::MAX, |iface, f| frames.push(Out { at, iface, frame: f.to_vec() })); + self.collect(); + frames + } + + pub fn collect(&mut self) { + self.events.extend(self.ip.drain_events()); + } + + pub fn state(&self, addr: Ipv4Addr) -> Option<&Nud> { + self.ip.neighbour(self.if0, addr) + } + + pub fn is_reachable(&self, addr: Ipv4Addr) -> bool { + matches!(self.state(addr), Some(Nud::Reachable(_))) + } + + pub fn is_stale(&self, addr: Ipv4Addr) -> bool { + matches!(self.state(addr), Some(Nud::Stale(_))) + } + + pub fn mac_of(&self, addr: Ipv4Addr) -> Option { + self.state(addr).and_then(Nud::mac) + } + + pub fn refusals(&self, rule: Counter) -> Vec { + self.events.iter().filter_map(|e| match e { + Event::Refused(r) if r.rule == rule => Some(*r), + _ => None, + }).collect() + } +} + +pub fn arp_frame(destination: MacAddr, source: MacAddr, arp: &Arp) -> Vec { + let builder = toyos_net_wire::ethernet::FrameBuilder { destination, source: IndividualMac::new(source).unwrap() }; + let mut out = vec![0u8; 60]; + builder.emit(arp, &mut out).unwrap().to_vec() +} + +/// An ARP packet as the frame body of a 60-byte frame, edited: operation, sender MAC, sender IP, +/// target MAC, target IP, frame destination. +pub fn arp_packet(op: u16, sender_mac: MacAddr, sender_ip: Ipv4Addr, target_mac: MacAddr, target_ip: Ipv4Addr) -> Vec { + let mut a = vec![0, 1, 8, 0, 6, 4]; + a.extend_from_slice(&op.to_be_bytes()); + a.extend_from_slice(&sender_mac.0); + a.extend_from_slice(&sender_ip.octets()); + a.extend_from_slice(&target_mac.0); + a.extend_from_slice(&target_ip.octets()); + a +} + +pub const V_ARP_REQ: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0a c0 00 02 01 +00 00 00 00 00 00 c0 00 02 02 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REPLY: &str = " +02 00 00 00 00 0a 02 00 00 00 00 0b 08 06 00 01 +08 00 06 04 00 02 02 00 00 00 00 0b c0 00 02 02 +02 00 00 00 00 0a c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_PROBE: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_ANNOUNCE: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0a c0 00 02 01 +00 00 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ETH_8021Q: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 81 00 00 64 +08 06 00 01 08 00 06 04 00 01 02 00 00 00 00 0a +c0 00 02 01 00 00 00 00 00 00 c0 00 02 02 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ETH_PRIO: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 81 00 a0 00 +08 06 00 01 08 00 06 04 00 01 02 00 00 00 00 0a +c0 00 02 01 00 00 00 00 00 00 c0 00 02 02 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ETH_QINQ: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 88 a8 00 0a +81 00 00 14 08 06 00 01 08 00 06 04 00 01 02 00 +00 00 00 0a c0 00 02 01 00 00 00 00 00 00 c0 00 +02 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00"; + +pub const V_UDP_DNS: &str = " +02 00 00 00 00 fe 02 00 00 00 00 0a 08 00 45 00 +00 39 00 00 40 00 40 11 b6 7d c0 00 02 01 c0 00 +02 35 c0 00 00 35 00 25 d9 95 12 34 01 00 00 01 +00 00 00 00 00 00 07 65 78 61 6d 70 6c 65 03 63 +6f 6d 00 00 01 00 01"; + +pub const V_ICMP_ECHO: &str = " +45 00 00 24 00 00 40 00 40 01 b6 d5 c0 00 02 01 +c0 00 02 02 08 00 66 68 00 01 00 01 61 62 63 64 +65 66 67 68"; + +pub const V_ICMP_REPLY: &str = " +45 00 00 24 00 00 40 00 40 01 b6 d5 c0 00 02 02 +c0 00 02 01 00 00 6e 68 00 01 00 01 61 62 63 64 +65 66 67 68"; + +pub const V_ICMP_PORT_UNREACH: &str = " +45 00 00 38 00 00 40 00 40 01 b6 8e c0 00 02 35 +c0 00 02 01 03 03 63 0c 00 00 00 00 45 00 00 39 +00 00 40 00 40 11 b6 7d c0 00 02 01 c0 00 02 35 +c0 00 00 35 00 25 d9 95"; + +pub const V_ICMP_FRAG_NEEDED: &str = " +45 00 00 38 00 00 40 00 40 01 b5 c5 c0 00 02 fe +c0 00 02 01 03 04 13 a5 00 00 05 78 45 00 05 dc +00 00 40 00 40 06 48 e0 c0 00 02 01 c6 33 64 07 +c0 01 01 bb 11 11 11 11"; + +pub const V_UDP_HI: &str = " +45 00 00 1e 00 00 40 00 40 11 b6 cb c0 00 02 02 +c0 00 02 01 13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_ICMP_PORT_UNREACH_GEN: &str = " +45 00 00 3a 00 00 40 00 40 01 b6 bf c0 00 02 01 +c0 00 02 02 03 03 81 1c 00 00 00 00 45 00 00 1e +00 00 40 00 40 11 b6 cb c0 00 02 02 c0 00 02 01 +13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_ICMP_TIME_EXCEEDED: &str = " +45 00 00 38 00 00 40 00 40 01 b5 c5 c0 00 02 fe +c0 00 02 01 0b 00 11 21 00 00 00 00 45 00 05 dc +00 00 40 00 40 06 48 e0 c0 00 02 01 c6 33 64 07 +c0 01 01 bb 11 11 11 11"; + +pub const V_ICMP_PARAM_PROBLEM: &str = " +0c 00 fc 20 14 00 00 00 45 00 05 dc 00 00 40 00 +40 06 48 e0 c0 00 02 01 c6 33 64 07 c0 01 01 bb +11 11 11 11"; + +pub const V_ICMP_TIMESTAMP_REQ: &str = " +0d 00 f1 f7 00 07 00 01 01 00 00 00 00 00 00 00 +00 00 00 00"; + +pub const V_ICMP_REDIRECT: &str = " +05 01 54 21 c0 00 02 fe 45 00 05 dc 00 00 40 00 +40 06 48 e0 c0 00 02 01 c6 33 64 07 c0 01 01 bb +11 11 11 11"; + +pub const V_IGMP_REPORT: &str = " +01 00 5e 00 00 fb 02 00 00 00 00 0a 08 00 46 00 +00 20 00 00 40 00 01 02 41 db c0 00 02 01 e0 00 +00 fb 94 04 00 00 16 00 09 04 e0 00 00 fb 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_IGMP_QUERY_V2: &str = " +46 00 00 20 00 00 00 00 01 02 81 d8 c0 00 02 fe +e0 00 00 01 94 04 00 00 11 64 ee 9b 00 00 00 00"; + +pub const V_IGMP_QUERY_V3_SRC: &str = " +11 64 87 0c e0 00 00 fb 02 7d 00 02 c0 00 02 09 +c0 00 02 0a"; + +pub const V_IGMP_LEAVE: &str = "17 00 08 04 e0 00 00 fb"; + +pub const V_IP_RR: &str = " +47 00 00 26 00 00 40 00 40 11 a9 bc c0 00 02 02 +c0 00 02 01 07 07 04 00 00 00 00 00 13 88 13 89 +00 0a ec 5b 68 69"; + +pub const V_IP_NOPS: &str = " +46 00 00 22 00 00 40 00 40 11 b3 c6 c0 00 02 02 +c0 00 02 01 01 01 01 00 13 88 13 89 00 0a ec 5b +68 69"; + +pub const V_IP_LSRR: &str = " +47 00 00 26 00 00 40 00 40 11 2e f9 c0 00 02 02 +c0 00 02 01 83 07 04 c0 00 02 fe 00 13 88 13 89 +00 0a ec 5b 68 69"; + +pub const V_IP_FRAG_FIRST: &str = " +45 00 00 1e 4d 2f 20 00 40 11 89 9c c0 00 02 02 +c0 00 02 01 13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_IP_FRAG_LAST: &str = " +45 00 00 18 4d 2f 00 b9 40 11 a8 e9 c0 00 02 02 +c0 00 02 01 74 61 69 6c"; + +pub const V_IP_DSCP: &str = " +45 bb 00 1e 00 00 40 00 40 11 b6 10 c0 00 02 02 +c0 00 02 01 13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_IP_MIN: &str = " +45 00 00 14 00 00 40 00 40 fd b5 e9 c0 00 02 02 +c0 00 02 01"; + +pub const V_IGMP3_JOIN: &str = " +01 00 5e 00 00 16 02 00 00 00 00 0a 08 00 46 c0 +00 28 00 00 40 00 01 02 41 f8 c0 00 02 01 e0 00 +00 16 94 04 00 00 22 00 f9 02 00 00 00 01 04 00 +00 00 e0 00 00 fb 00 00 00 00 00 00"; + +pub const V_IGMP3_LEAVE: &str = " +46 c0 00 28 00 00 40 00 01 02 41 f8 c0 00 02 01 +e0 00 00 16 94 04 00 00 22 00 fa 02 00 00 00 01 +03 00 00 00 e0 00 00 fb"; + +pub const V_IGMP3_CURRENT: &str = " +46 c0 00 28 00 00 40 00 01 02 41 f8 c0 00 02 01 +e0 00 00 16 94 04 00 00 22 00 fb 02 00 00 00 01 +02 00 00 00 e0 00 00 fb"; + +pub const V_IGMP3_JOIN_UNSPEC: &str = " +46 c0 00 28 00 00 40 00 01 02 03 fa 00 00 00 00 +e0 00 00 16 94 04 00 00 22 00 f9 02 00 00 00 01 +04 00 00 00 e0 00 00 fb"; + +pub const V_IGMP3_QUERY_GEN: &str = " +46 c0 00 24 00 00 40 00 01 02 41 14 c0 00 02 fe +e0 00 00 01 94 04 00 00 11 64 ec 1e 00 00 00 00 +02 7d 00 00"; + +pub const V_IGMP3_QUERY_NORA: &str = " +45 c0 00 20 00 00 40 00 01 02 d6 1c c0 00 02 fe +e0 00 00 01 11 64 ec 1e 00 00 00 00 02 7d 00 00"; + +pub const V_IGMP3_QUERY_GROUP: &str = " +46 c0 00 24 00 00 40 00 01 02 40 1a c0 00 02 fe +e0 00 00 fb 94 04 00 00 11 0a 0b 7d e0 00 00 fb +02 7d 00 00"; + +pub const V_IGMP1_QUERY: &str = " +45 00 00 1c 00 00 00 00 01 02 16 e1 c0 00 02 fe +e0 00 00 01 11 00 ee ff 00 00 00 00"; + +pub const V_ICMP_PROTO_UNREACH_GEN: &str = " +45 00 00 30 00 00 40 00 40 01 b6 c9 c0 00 02 01 +c0 00 02 02 03 02 fc fd 00 00 00 00 45 00 00 14 +00 00 40 00 40 fd b5 e9 c0 00 02 02 c0 00 02 01"; + +pub const V_ARP_REQ_B: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0b c0 00 02 02 +00 00 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REPLY_A: &str = " +02 00 00 00 00 0b 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 02 02 00 00 00 00 0a c0 00 02 01 +02 00 00 00 00 0b c0 00 02 02 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REQ_OTHER: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0b c0 00 02 02 +00 00 00 00 00 00 c0 00 02 07 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_PROBE_B: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0b 00 00 00 00 +00 00 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REPLY_TO_PROBE: &str = " +02 00 00 00 00 0b 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 02 02 00 00 00 00 0a c0 00 02 01 +02 00 00 00 00 0b 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_CONFLICT_B: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0b c0 00 02 01 +00 00 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_ROUTER_MOVED: &str = " +ff ff ff ff ff ff 02 00 00 00 00 66 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 66 c0 00 02 fe +00 00 00 00 00 00 c0 00 02 fe 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REQ_R: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0a c0 00 02 01 +00 00 00 00 00 00 c0 00 02 fe 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REPLY_R: &str = " +02 00 00 00 00 0a 02 00 00 00 00 fe 08 06 00 01 +08 00 06 04 00 02 02 00 00 00 00 fe c0 00 02 fe +02 00 00 00 00 0a c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_POLL_R: &str = " +02 00 00 00 00 fe 02 00 00 00 00 0a 08 06 00 01 +08 00 06 04 00 01 02 00 00 00 00 0a c0 00 02 01 +00 00 00 00 00 00 c0 00 02 fe 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ARP_REPLY_R_MOVED: &str = " +02 00 00 00 00 0a 02 00 00 00 00 66 08 06 00 01 +08 00 06 04 00 02 02 00 00 00 00 66 c0 00 02 fe +02 00 00 00 00 0a c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_FRAG_1: &str = " +45 00 00 24 1a 2b 20 00 40 11 bc 9a c0 00 02 02 +c0 00 02 01 13 88 13 89 00 20 7b b4 30 31 32 33 +34 35 36 37"; + +impl H { + /// A UDP datagram handed to [ip] now: the frame when it left at once, `None` when held. + pub fn send(&mut self, source: Ipv4Addr, destination: Ipv4Addr, sport: u16, dport: u16, data: &[u8]) -> Result>, Counter> { + self.send_ttl(source, destination, sport, dport, data, Ttl::DEFAULT) + } + + pub fn send_ttl(&mut self, source: Ipv4Addr, destination: Ipv4Addr, sport: u16, dport: u16, data: &[u8], ttl: Ttl) -> Result>, Counter> { + let mut frame = [0u8; FRAME]; + let datagram = UdpBuilder { source: Port::new(sport).unwrap(), destination: Port::new(dport).unwrap(), data }; + let out = UdpOut { source, destination, ttl, datagram }; + let sent = self.ip.send_udp(self.clock(), &out, &mut frame); + self.collect(); + sent.map(|s| match s { + Sent::Frame(n) => Some(frame[..n].to_vec()), + Sent::Held => None, + }) + } + + /// A UDP datagram from A to `destination`, 5001 → 5001. + pub fn udp_to(&mut self, destination: Ipv4Addr) -> Result>, Counter> { + self.send(A, destination, 5001, 5001, b"hi") + } + + /// `addr` at `m` asks who has A: learned into STALE, answered. + pub fn stale(&mut self, addr: Ipv4Addr, m: MacAddr) { + self.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, addr, MacAddr::ZERO, A))); + self.out(); + assert!(self.is_stale(addr), "{addr} is not STALE"); + } +} + +/// A seeded xorshift, for the property tests' generators. +pub struct Rng(pub u64); + +impl Rng { + pub fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + pub fn below(&mut self, n: u64) -> u64 { + self.next() % n + } +} + +pub const MAC_A1: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x1a]); + +impl H { + /// A second interface with its own MAC and link up, at setup time 0. + pub fn add_if1(&mut self) -> IfIndex { + let if1 = self.ip.add_interface(Instant::from_nanos(0), mac(MAC_A1)); + self.ip.link_up(Instant::from_nanos(0), if1).unwrap(); + if1 + } + + /// ToyOS as B: MAC B, 192.0.2.2/24, A resolved at setup. + pub fn as_b() -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), SECRET); + let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_B)); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + let mut h = Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: Default::default() }; + h.assign(if0, B, 24, 0); + let setup = Instant::from_millis(5_000); + let _ = h.ip.resolve(setup, if0, A); + h.ip.transmit(setup, usize::MAX, |_, _| {}); + let _ = h.ip.receive(setup, if0, ð(MAC_B, MAC_A, 0x0806, &arp_packet(2, MAC_A, A, MAC_B, B))); + h.settle(); + h + } + + /// A datagram in a frame to `destination` from B's MAC. + pub fn datagram_to(&mut self, destination: MacAddr, ip: &[u8]) -> Option> { + self.frame(ð(destination, MAC_B, 0x0800, ip)) + } +} + +/// The destination class and interface a datagram was delivered with, or `None`. +pub fn cast_of(d: &Option>) -> Option { + match d { + Some(toyos_net_ip::Delivery::Udp(arrival, _)) | Some(toyos_net_ip::Delivery::Tcp(arrival, _)) => Some(arrival.cast), + _ => None, + } +} diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs new file mode 100644 index 00000000000..dae66fd275a --- /dev/null +++ b/toyos-net-ip/tests/io.rs @@ -0,0 +1,518 @@ +//! §4 and §5 with `wire.md` §3.3–§3.4 and §5.4–§5.5: frame dispatch, the IPv4 input pipeline, +//! and what [ip] puts on the wire. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{limits, Cast, Counter, Delivery, MTU}; +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::ipv4::{Ecn, Ttl}; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +fn hi_to(destination: Ipv4Addr) -> Vec { + edit(hex(V_UDP_HI), |ip| set_destination(ip, destination)) +} + +fn hi_from(source: Ipv4Addr) -> Vec { + edit(hex(V_UDP_HI), |ip| set_source(ip, source)) +} + +const MDNS_MAC: MacAddr = MacAddr([1, 0, 0x5e, 0, 0, 0xfb]); + +#[test] +fn s_ip_in_001_frame_before_destination() { + let mut h = H::fixture_i(); + assert!(h.datagram_to(MacAddr::BROADCAST, &hi_to(ip4(192, 0, 2, 9))).is_none()); + assert_eq!(h.count(Counter::IpUnicastInLinkBroadcast), 1); + assert_eq!(h.count(Counter::IpNotForUs), 0); +} + +#[test] +fn s_ip_in_002_unicast_in_a_group_frame() { + let mut h = H::fixture_i(); + assert!(h.datagram_to(MDNS_MAC, &hex(V_UDP_HI)).is_none()); + assert_eq!(h.count(Counter::IpUnicastInLinkMulticast), 1); +} + +#[test] +fn s_ip_in_003_a_group_destination_in_our_frame() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_R, 0x0800, &hex(V_IGMP3_QUERY_GEN))); + let refused: u64 = Counter::ALL.iter().filter(|c| c.name().starts_with("ip.") || c.name().starts_with("igmp.")).map(|&c| h.count(c)).sum(); + assert_eq!(refused, 0); + assert!(h.run(10_000).iter().any(|o| ip_bytes(&o.frame) == hex(V_IGMP3_CURRENT))); +} + +#[test] +fn s_ip_in_004_martian_destinations() { + let mut h = H::fixture_i(); + for destination in [ip4(0, 1, 2, 3), ip4(240, 0, 0, 1)] { + assert!(h.datagram(&hi_to(destination)).is_none()); + } + assert_eq!(h.count(Counter::IpMartianDestination), 2); +} + +#[test] +fn s_ip_in_005_invalid_sources() { + let mut h = H::fixture_i(); + for source in [ip4(0, 1, 2, 3), ip4(192, 0, 2, 0)] { + assert!(h.datagram(&hi_from(source)).is_none()); + } + assert_eq!(h.count(Counter::IpInvalidSource), 2); + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, A, 31, 0); + h.settle(); + assert_eq!(cast_of(&h.datagram(&hi_from(ip4(192, 0, 2, 0)))), Some(Cast::Unicast)); +} + +#[test] +fn s_ip_in_006_destination_before_source() { + let mut h = H::fixture_i(); + let ip = edit(hex(V_UDP_HI), |ip| { + set_destination(ip, ip4(192, 0, 2, 9)); + set_source(ip, ip4(224, 0, 0, 5)); + }); + assert!(h.datagram(&ip).is_none()); + assert_eq!(h.count(Counter::IpNotForUs), 1); + assert_eq!(h.count(Counter::IpInvalidSource), 0); +} + +#[test] +fn s_ip_in_007_our_own_source_on_any_interface() { + 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!(h.datagram(&hi_from(ip4(198, 51, 100, 1))).is_none()); + assert_eq!(h.count(Counter::IpOwnSource), 1); +} + +#[test] +fn s_ip_in_008_fragments_before_options() { + let mut h = H::fixture_i(); + let ip = edit(hex(V_IP_LSRR), |ip| ip[6] |= 0x20); + assert!(h.datagram(&ip).is_none()); + assert_eq!(h.count(Counter::IpFragment), 1); + assert_eq!(h.count(Counter::IpSourceRoute), 0); +} + +#[test] +fn s_ip_in_009_the_delivery_carries_the_tos() { + let mut h = H::fixture_i(); + let Some(Delivery::Udp(arrival, datagram)) = h.datagram(&hex(V_IP_DSCP)) else { panic!("not delivered") }; + assert_eq!(arrival.packet.traffic_class().dscp(), 46); + assert_eq!(arrival.packet.traffic_class().ecn(), Ecn::Ce); + assert_eq!(arrival.cast, Cast::Unicast); + assert_eq!(arrival.iface, h.if0); + assert_eq!(datagram.destination_port().get(), 5001); +} + +#[test] +fn s_ipp_001_loose_source_route() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hex(V_IP_LSRR)).is_none()); + assert_eq!(h.count(Counter::IpSourceRoute), 1); + assert_eq!(h.refusals(Counter::IpSourceRoute)[0].peer, toyos_net_ip::Peer::Ip(B)); +} + +#[test] +fn s_ipp_002_strict_source_route() { + let mut h = H::fixture_i(); + assert!(h.datagram(&edit(hex(V_IP_LSRR), |ip| ip[20] = 0x89)).is_none()); + assert_eq!(h.count(Counter::IpSourceRoute), 1); +} + +#[test] +fn s_ipp_003_record_route_is_ignored() { + let mut h = H::fixture_i(); + assert!(matches!(h.datagram(&hex(V_IP_RR)), Some(Delivery::Udp(..)))); +} + +#[test] +fn s_ipp_004_our_address_and_another() { + let mut h = H::fixture_i(); + assert_eq!(cast_of(&h.datagram(&hi_to(A))), Some(Cast::Unicast)); + assert!(h.datagram(&hi_to(ip4(192, 0, 2, 7))).is_none()); + assert_eq!(h.count(Counter::IpNotForUs), 1); +} + +#[test] +fn s_ipp_005_broadcast_forms() { + let mut h = H::fixture_i(); + assert_eq!(cast_of(&h.datagram_to(MacAddr::BROADCAST, &hi_to(ip4(255, 255, 255, 255)))), Some(Cast::LimitedBroadcast)); + assert_eq!(cast_of(&h.datagram_to(MacAddr::BROADCAST, &hi_to(ip4(192, 0, 2, 255)))), Some(Cast::SubnetBroadcast)); + assert!(h.datagram(&hi_to(ip4(192, 0, 2, 0))).is_none()); + assert_eq!(h.count(Counter::IpNotForUs), 1); +} + +#[test] +fn s_ipp_006_joined_groups() { + let mut h = H::fixture_i(); + assert!(h.datagram_to(MDNS_MAC, &hi_to(MDNS)).is_some()); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + assert!(h.datagram(&hi_to(MDNS)).is_none()); + assert_eq!(h.count(Counter::IpNotForUs), 1); + assert!(h.datagram_to(MacAddr([1, 0, 0x5e, 0, 0, 1]), &hi_to(ip4(224, 0, 0, 1))).is_some()); +} + +#[test] +fn s_ipp_007_invalid_sources() { + let mut h = H::fixture_i(); + for source in [ip4(224, 0, 0, 5), ip4(255, 255, 255, 255), ip4(240, 0, 0, 1), ip4(127, 0, 0, 1), ip4(192, 0, 2, 255)] { + assert!(h.datagram(&hi_from(source)).is_none(), "{source}"); + } + assert_eq!(h.count(Counter::IpInvalidSource), 5); +} + +#[test] +fn s_ipp_008_unspecified_only_for_igmp() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hi_from(ip4(0, 0, 0, 0))).is_none()); + assert_eq!(h.count(Counter::IpInvalidSource), 1); + let report = edit(ip_bytes(&hex(V_IGMP_REPORT)), |ip| set_source(ip, ip4(0, 0, 0, 0))); + h.datagram_to(MDNS_MAC, &report); + assert_eq!(h.count(Counter::IpInvalidSource), 1); + assert_eq!(h.count(Counter::IgmpIgnored), 1, "accepted, then ignored as a report outside v2 mode"); +} + +#[test] +fn s_ipp_009_our_own_source() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hi_from(A)).is_none()); + assert_eq!(h.count(Counter::IpOwnSource), 1); +} + +#[test] +fn s_ipp_010_loopback_destination() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hi_to(ip4(127, 0, 0, 1))).is_none()); + assert_eq!(h.count(Counter::IpMartianDestination), 1); +} + +#[test] +fn s_ipp_011_unicast_in_a_broadcast_frame() { + let mut h = H::fixture_i(); + assert!(h.datagram_to(MacAddr::BROADCAST, &hex(V_UDP_HI)).is_none()); + assert_eq!(h.count(Counter::IpUnicastInLinkBroadcast), 1); +} + +#[test] +fn s_ipp_012_an_unknown_protocol_to_us() { + let mut h = H::fixture_i_with(B, MAC_B); + assert!(h.datagram(&hex(V_IP_MIN)).is_none()); + assert_eq!(h.count(Counter::IpProtocol), 1); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(ip_bytes(&out[0].frame), hex(V_ICMP_PROTO_UNREACH_GEN)); +} + +#[test] +fn s_ipp_013_an_unknown_protocol_to_all() { + let mut h = H::fixture_i_with(B, MAC_B); + let ip = edit(hex(V_IP_MIN), |ip| set_destination(ip, ip4(255, 255, 255, 255))); + assert!(h.datagram_to(MacAddr::BROADCAST, &ip).is_none()); + assert_eq!(h.count(Counter::IpProtocol), 1); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_002_minimal_datagram_is_an_unknown_protocol() { + let mut h = H::fixture_i(); + h.datagram(&hex(V_IP_MIN)); + assert_eq!(h.count(Counter::IpProtocol), 1); +} + +#[test] +fn s_ip_020_df_and_mf_is_a_fragment() { + let mut h = H::fixture_i(); + assert!(h.datagram(&edit(hex(V_UDP_HI), |ip| ip[6] = 0x60)).is_none()); + assert_eq!(h.count(Counter::IpFragment), 1); +} + +#[test] +fn s_ip_021_first_fragment() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hex(V_IP_FRAG_FIRST)).is_none()); + assert_eq!(h.count(Counter::IpFragment), 1); +} + +#[test] +fn s_ip_022_last_fragment() { + let mut h = H::fixture_i(); + assert!(h.datagram(&hex(V_IP_FRAG_LAST)).is_none()); + assert_eq!(h.count(Counter::IpFragment), 1); +} + +#[test] +fn s_ip_023_largest_offset() { + let mut h = H::fixture_i(); + assert!(h.datagram(&edit(hex(V_UDP_HI), |ip| { + ip[6] = 0x1f; + ip[7] = 0xff; + })) + .is_none()); + assert_eq!(h.count(Counter::IpFragment), 1); +} + +#[test] +fn s_ip_025_ttl_zero_is_delivered() { + let mut h = H::fixture_i(); + assert!(matches!(h.datagram(&edit(hex(V_UDP_HI), |ip| ip[8] = 0)), Some(Delivery::Udp(..)))); +} + +#[test] +fn s_ip_026_ttl_one_is_delivered() { + let mut h = H::fixture_i(); + assert!(matches!(h.datagram(&edit(hex(V_UDP_HI), |ip| ip[8] = 1)), Some(Delivery::Udp(..)))); +} + +#[test] +fn s_ip_029_other_protocols() { + let mut h = H::fixture_i(); + for protocol in [0u8, 41, 50, 132, 253, 255] { + h.datagram(&ipv4(B, A, protocol, &[])); + } + assert_eq!(h.count(Counter::IpProtocol), 6); +} + +#[test] +fn s_ip_035_the_mtu_bounds_a_send() { + let mut h = H::fixture_i_with(B, MAC_B); + assert!(h.send(A, B, 5001, 5001, &[0; 1_472]).unwrap().is_some()); + assert_eq!(h.send(A, B, 5001, 5001, &[0; 1_473]), Err(Counter::IpExceedsMtu)); +} + +#[test] +fn s_ipo_015_stream_id_is_delivered() { + let mut h = H::fixture_i(); + let ip = edit(hex(V_IP_NOPS), |ip| ip[20..24].copy_from_slice(&[0x88, 4, 0, 1])); + assert!(matches!(h.datagram(&ip), Some(Delivery::Udp(..)))); +} + +#[test] +fn s_ipo_016_security_option_is_delivered() { + let mut h = H::fixture_i(); + let mut udp_part = hex(V_UDP_HI)[20..].to_vec(); + let mut ip = vec![0x48, 0, 0, 0, 0, 0, 0x40, 0, 64, 17, 0, 0, 192, 0, 2, 2, 192, 0, 2, 1]; + ip.extend_from_slice(&[130, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + ip.append(&mut udp_part); + let total = ip.len() as u16; + ip[2..4].copy_from_slice(&total.to_be_bytes()); + fix(&mut ip); + assert!(matches!(h.datagram(&ip), Some(Delivery::Udp(..)))); +} + +#[test] +fn s_udp_022_the_mtu_refuses_1473_bytes() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(h.send(A, B, 5001, 5001, &[0; 1_473]), Err(Counter::IpExceedsMtu)); + assert_eq!(h.count(Counter::IpExceedsMtu), 1); + assert!(h.send(A, B, 5001, 5001, &[0; 1_472]).unwrap().is_some()); +} + +fn arp_request_for_b(ether_type_field: &[u8]) -> Vec { + let mut f = MacAddr::BROADCAST.0.to_vec(); + f.extend_from_slice(&MAC_B.0); + f.extend_from_slice(ether_type_field); + f.extend_from_slice(&arp_packet(1, MAC_B, B, MacAddr::ZERO, A)); + f +} + +#[test] +fn s_eth_010_type_0600_is_unknown() { + let mut h = H::fixture_i(); + h.frame(&arp_request_for_b(&[0x06, 0x00])); + assert_eq!(h.count(Counter::EthUnknownType), 1); +} + +#[test] +fn s_eth_011_ipv6() { + let mut h = H::fixture_i(); + h.frame(&arp_request_for_b(&[0x86, 0xdd])); + assert_eq!(h.count(Counter::EthIpv6), 1); +} + +#[test] +fn s_eth_012_lldp() { + let mut h = H::fixture_i(); + h.frame(&arp_request_for_b(&[0x88, 0xcc])); + assert_eq!(h.count(Counter::EthUnknownType), 1); +} + +#[test] +fn s_eth_014_a_priority_tag_is_untagged() { + let mut h = H::as_b(); + h.frame(&hex(V_ETH_PRIO)); + let out = h.out(); + assert_eq!(out.len(), 1, "the ARP inside is processed"); + assert_eq!(out[0].arp().unwrap().target_ip, A); +} + +#[test] +fn s_eth_019_9100_is_not_a_tag() { + let mut h = H::fixture_i(); + h.frame(&arp_request_for_b(&[0x91, 0x00])); + assert_eq!(h.count(Counter::EthUnknownType), 1); +} + +#[test] +fn s_eth_022_another_hosts_mac() { + let mut h = H::fixture_i(); + h.frame(ð(MacAddr([2, 0, 0, 0, 0, 0x99]), MAC_B, 0x0800, &hex(V_UDP_HI))); + assert_eq!(h.count(Counter::EthNotForUs), 1); +} + +#[test] +fn s_eth_023_group_macs_follow_membership() { + let mut h = H::fixture_i(); + assert!(h.datagram_to(MDNS_MAC, &hi_to(MDNS)).is_some()); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + assert!(h.datagram_to(MDNS_MAC, &hi_to(MDNS)).is_none()); + assert_eq!(h.count(Counter::EthNotForUs), 1); + assert!(h.datagram_to(MacAddr([1, 0, 0x5e, 0, 0, 1]), &hi_to(ip4(224, 0, 0, 1))).is_some()); +} + +#[test] +fn s_eth_024_vlans_are_refused() { + let mut h = H::as_b(); + h.frame(&hex(V_ETH_8021Q)); + h.frame(&hex(V_ETH_QINQ)); + assert_eq!(h.count(Counter::EthVlan), 2); + assert!(h.out().is_empty()); +} + +#[test] +fn s_eth_025_our_own_source() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_A, 0x0800, &hex(V_UDP_HI))); + assert_eq!(h.count(Counter::EthOwnSource), 1); +} + +#[test] +fn s_eth_032_broadcast_destinations_go_to_the_broadcast_mac() { + let mut h = H::fixture_i(); + for destination in [ip4(255, 255, 255, 255), ip4(192, 0, 2, 255)] { + let frame = h.udp_to(destination).unwrap().unwrap(); + assert_eq!(destination_of(&frame), MacAddr::BROADCAST); + } +} + +/// Every IPv4 frame [ip] emits, for datagrams to a spread of unicast destinations. +fn unicast_frames() -> Vec> { + let mut h = H::fixture_i_with(B, MAC_B); + let mut frames = Vec::new(); + for destination in [B, DNS, REMOTE, ip4(192, 0, 2, 77)] { + frames.extend(h.udp_to(destination).unwrap()); + } + h.frame(ð(MAC_A, MAC_DNS, 0x0806, &arp_packet(2, MAC_DNS, DNS, MAC_A, A))); + h.reply_from(R, MAC_R); + frames.extend(h.run(5_000).into_iter().map(|o| o.frame)); + frames +} + +#[test] +fn s_eth_033_a_unicast_destination_is_never_link_broadcast() { + let frames = unicast_frames(); + assert!(frames.iter().filter(|f| ip_of(f).is_some()).count() >= 3); + for frame in frames.iter().filter(|f| ip_of(f).is_some()) { + assert_ne!(destination_of(frame), MacAddr::BROADCAST); + } +} + +#[test] +fn s_ip_out_001_prop_every_datagram_is_atomic() { + let mut h = H::fixture_i_with(B, MAC_B); + let mut rng = Rng(0x0dd5); + let mut frames = Vec::new(); + for step in 0..300u64 { + h.at(step * 37); + match rng.below(5) { + 0 => frames.extend(h.udp_to([B, MDNS, ip4(255, 255, 255, 255), REMOTE][rng.below(4) as usize]).ok().flatten()), + 1 => { + h.datagram(&edit(hex(V_ICMP_ECHO), |ip| { + set_source(ip, B); + set_destination(ip, A); + })); + } + 2 => { + if let Some(Delivery::Udp(arrival, _)) = h.datagram(&hex(V_UDP_HI)) { + h.ip.port_unreachable(h.clock(), &arrival); + } + } + 3 => { + let g = group(ip4(239, 0, 0, rng.below(8) as u8 + 1)); + let _ = if rng.below(2) == 0 { h.ip.join(h.clock(), h.if0, g) } else { h.ip.leave(h.clock(), h.if0, g) }; + } + _ => { + h.datagram(&hex(V_IP_MIN)); + } + } + frames.extend(h.run(step * 37 + 36).into_iter().map(|o| o.frame)); + } + let datagrams: Vec<_> = frames.iter().filter_map(|f| ip_of(f)).collect(); + assert!(datagrams.len() > 200); + for ip in datagrams { + assert!(ip.dont_fragment() && !ip.more_fragments()); + assert_eq!((ip.fragment_offset().units(), ip.identification()), (0, 0)); + } +} + +#[test] +fn s_ip_out_002_ttl() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(ip_of(&h.udp_to(B).unwrap().unwrap()).unwrap().ttl(), 64); + let multicast = h.send_ttl(A, MDNS, 5353, 5353, b"q", Ttl::new(1).unwrap()).unwrap().unwrap(); + assert_eq!(ip_of(&multicast).unwrap().ttl(), 1); + h.ip.join(h.clock(), h.if0, group(ip4(239, 1, 2, 3))).unwrap(); + let report = h.out(); + assert_eq!(report[0].ip().unwrap().ttl(), 1); + assert_eq!(Ttl::new(0), Err(toyos_net_wire::BuildError::IpTtlZero)); +} + +#[test] +fn s_ip_out_003_tos() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(ip_of(&h.udp_to(B).unwrap().unwrap()).unwrap().traffic_class().byte(), 0); + h.ip.join(h.clock(), h.if0, group(ip4(239, 1, 2, 3))).unwrap(); + assert_eq!(h.out()[0].ip().unwrap().traffic_class().byte(), 0xc0); + let Some(Delivery::Udp(arrival, _)) = h.datagram(&hex(V_IP_DSCP)) else { panic!() }; + h.ip.port_unreachable(h.clock(), &arrival); + assert_eq!(h.out()[0].ip().unwrap().traffic_class().byte(), 0); +} + +#[test] +fn s_ip_out_004_the_mtu() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(MTU, 1_500); + assert_eq!(h.send(A, B, 5001, 5001, &[0; 1_473]), Err(Counter::IpExceedsMtu)); + let frame = h.send(A, B, 5001, 5001, &[0; 1_472]).unwrap().unwrap(); + assert_eq!(ip_of(&frame).unwrap().total_length(), 1_500); +} + +#[test] +fn s_ip_out_005_link_destinations() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(destination_of(&h.udp_to(ip4(192, 0, 2, 255)).unwrap().unwrap()), MacAddr::BROADCAST); + assert_eq!(destination_of(&h.udp_to(MDNS).unwrap().unwrap()), MDNS_MAC); + assert_eq!(destination_of(&h.udp_to(B).unwrap().unwrap()), MAC_B); + for frame in unicast_frames().iter().filter(|f| ip_of(f).is_some_and(|ip| !ip.destination().is_broadcast() && !ip.destination().is_multicast())) { + assert_ne!(destination_of(frame), MacAddr::BROADCAST); + } +} + +#[test] +fn s_ip_out_006_the_control_queue_is_bounded() { + let mut h = H::fixture_i(); + for n in 0..=limits::CONTROL_QUEUE as u8 { + let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); + } + assert_eq!(h.count(Counter::IpControlQueueFull), 1); + assert_eq!(h.out().len(), limits::CONTROL_QUEUE); +} diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs new file mode 100644 index 00000000000..6c1102d3a69 --- /dev/null +++ b/toyos-net-ip/tests/nud.rs @@ -0,0 +1,476 @@ +//! §6: the reachability machine. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{limits, Advice, Counter, Event, Flow, Nud, Resolution}; +use toyos_net_wire::Port; + +fn flow_to(destination: Ipv4Addr) -> Flow { + Flow { source: A, source_port: Port::new(5001).unwrap(), destination, destination_port: Port::new(5001).unwrap() } +} + +fn payload(out: &Out) -> Vec { + out.ip().unwrap().payload()[8..].to_vec() +} + +#[test] +fn s_ip_nud_001_resolution_releases_the_datagram() { + let mut h = H::fixture_i(); + assert_eq!(h.udp_to(B), Ok(None)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].frame, hex(V_ARP_REQ)); + assert!(matches!(h.state(B), Some(Nud::Incomplete(s)) if s.queued() == 1)); + h.at(5); + h.frame(&hex(V_ARP_REPLY)); + assert!(h.is_reachable(B)); + assert_eq!(h.mac_of(B), Some(MAC_B)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].to(), MAC_B); + assert_eq!(payload(&out[0]), b"hi"); +} + +#[test] +fn s_ip_nud_002_three_requests_then_failed() { + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + let mut out = h.out(); + out.extend(h.run(3_000)); + let times: Vec = out.iter().filter(|o| o.requests(B)).map(|o| o.at).collect(); + assert_eq!(times, [0, 1_000, 2_000]); + assert_eq!(out.len(), 3); + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert_eq!(h.count(Counter::NbFailed), 1); + assert!(h.events.contains(&Event::Unreachable(flow_to(B)))); + assert!(h.run(10_000).is_empty()); +} + +#[test] +fn s_ip_nud_003_release_keeps_arrival_order() { + let mut h = H::fixture_i(); + for data in [b"1", b"2", b"3"] { + h.send(A, B, 5001, 5001, data).unwrap(); + } + h.out(); + h.frame(&hex(V_ARP_REPLY)); + let fourth = h.send(A, B, 5001, 5001, b"4").unwrap().unwrap(); + let mut out: Vec> = h.out().iter().map(payload).collect(); + out.push(ip_of(&fourth).unwrap().payload()[8..].to_vec()); + assert_eq!(out, [b"1", b"2", b"3", b"4"]); +} + +#[test] +fn s_ip_nud_004_overflow_drops_the_oldest() { + let mut h = H::fixture_i(); + for n in 1..=10u8 { + h.send(A, B, 5001, 5001, &[b'0' + n % 10]).unwrap(); + } + h.out(); + h.frame(&hex(V_ARP_REPLY)); + let out: Vec> = h.out().iter().map(payload).collect(); + let expected: Vec> = (3..=10u8).map(|n| vec![b'0' + n % 10]).collect(); + assert_eq!(out, expected); + assert_eq!(h.count(Counter::NbPendingOverflow), 2); +} + +#[test] +fn s_ip_nud_005_the_interface_holds_at_most_64() { + let mut h = H::fixture_i(); + for n in 0..8u8 { + for _ in 0..8 { + assert_eq!(h.udp_to(Ipv4Addr::new(192, 0, 2, 10 + n)), Ok(None)); + } + } + assert_eq!(h.udp_to(Ipv4Addr::new(192, 0, 2, 30)), Err(Counter::NbPendingFull)); + assert_eq!(h.count(Counter::NbPendingFull), 1); + assert!(h.state(Ipv4Addr::new(192, 0, 2, 30)).is_none()); + assert!(h.events.contains(&Event::Unreachable(flow_to(Ipv4Addr::new(192, 0, 2, 30))))); +} + +#[test] +fn s_ip_nud_006_reachable_goes_stale_silently() { + let mut h = H::fixture_i_with(R, MAC_R); + assert!(h.run(14_999).is_empty()); + assert!(h.is_reachable(R)); + assert!(h.run(45_000).is_empty()); + assert!(h.is_stale(R)); +} + +#[test] +fn s_ip_nud_007_positive_advice_moves_the_confirmation() { + let mut h = H::fixture_i_with(R, MAC_R); + let reachable = h.ip.reachable_time(h.if0).unwrap(); + let ms = |d: std::time::Duration| d.as_millis() as u64; + h.at(10_000); + h.ip.advise(h.clock(), REMOTE, Advice::Confirmed); + h.run(ms(reachable) + 1); + assert!(h.is_reachable(R), "the original deadline re-arms"); + h.run(10_000 + ms(reachable) - 1); + assert!(h.is_reachable(R)); + h.run(10_000 + ms(reachable) + 1); + assert!(h.is_stale(R)); +} + +#[test] +fn s_ip_nud_008_delay_probe_unreachable() { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + assert!(matches!(h.udp_to(REMOTE), Ok(Some(frame)) if destination_of(&frame) == MAC_R)); + assert!(matches!(h.state(R), Some(Nud::Delay(_)))); + let out = h.run(8_000); + let polls: Vec = out.iter().filter(|o| o.frame == hex(V_ARP_POLL_R)).map(|o| o.at).collect(); + assert_eq!(polls, [5_000, 6_000, 7_000]); + assert_eq!(out.len(), 3); + assert!(matches!(h.state(R), Some(Nud::Unreachable(u)) if u.quiescent())); + assert_eq!(h.count(Counter::NbUnreachable), 1); +} + +#[test] +fn s_ip_nud_009_advice_ends_delay() { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + h.udp_to(REMOTE).unwrap(); + h.at(4_000); + h.ip.advise(h.clock(), REMOTE, Advice::Confirmed); + assert!(h.is_reachable(R)); + assert!(h.run(5_000).is_empty()); +} + +#[test] +fn s_ip_nud_010_a_reply_confirms_probe() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(100); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + assert!(matches!(h.state(R), Some(Nud::Probe(_)))); + h.out(); + h.frame(&hex(V_ARP_REPLY_R)); + assert!(h.is_reachable(R)); +} + +/// R UNREACHABLE after PROBE's three unanswered polls, at the returned time. +fn unreachable_r() -> (H, u64) { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + h.udp_to(REMOTE).unwrap(); + h.run(8_000); + assert!(matches!(h.state(R), Some(Nud::Unreachable(_)))); + h.rebase(); + (h, 8_000) +} + +#[test] +fn s_ip_nud_011_unreachable_backs_off_while_traffic_flows() { + let (mut h, t0) = unreachable_r(); + let mut requests = Vec::new(); + let mut t = t0; + while t <= t0 + 160_000 { + requests.extend(h.run(t).iter().filter(|o| o.requests(R)).map(|o| o.at - t0)); + h.at(t); + let frame = h.udp_to(REMOTE).unwrap().expect("sent to the cached MAC"); + assert_eq!(destination_of(&frame), MAC_R); + requests.extend(h.out().iter().filter(|o| o.requests(R)).map(|o| o.at - t0)); + t += 100; + } + assert_eq!(requests, [0, 3_000, 12_000, 39_000, 99_000, 159_000]); + assert!(h.out().iter().all(|o| o.frame != hex(V_ARP_REQ_R) || o.to() == MacAddr::BROADCAST)); +} + +use toyos_net_wire::ethernet::MacAddr; + +#[test] +fn s_ip_nud_012_unreachable_is_quiet_without_traffic() { + let (mut h, t0) = unreachable_r(); + assert!(h.run(t0 + 20_000).is_empty()); + let t1 = t0 + 20_000; + assert!(h.udp_to(REMOTE).unwrap().is_some()); + let out = h.out(); + assert_eq!(out.iter().filter(|o| o.frame == hex(V_ARP_REQ_R)).count(), 1); + assert!(h.run(t1 + 3_000).is_empty()); + assert!(matches!(h.state(R), Some(Nud::Unreachable(u)) if u.quiescent())); +} + +#[test] +fn s_ip_nud_013_a_moved_router_answers() { + let (mut h, t0) = unreachable_r(); + h.at(t0 + 100); + h.udp_to(REMOTE).unwrap(); + h.out(); + h.frame(&hex(V_ARP_REPLY_R_MOVED)); + assert!(h.is_reachable(R)); + assert_eq!(h.mac_of(R), Some(MAC_X)); + assert_eq!(h.count(Counter::ArpMacChanged), 1); + let logged = h.refusals(Counter::ArpMacChanged); + assert_eq!(logged.len(), 1); + assert_eq!(logged[0].peer, toyos_net_ip::Peer::MacChange { ip: R, old: MAC_R, new: MAC_X }); +} + +#[test] +fn s_ip_nud_014_failed_is_held_down() { + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + h.out(); + h.run(3_000); + h.at(5_000); + assert_eq!(h.udp_to(B), Err(Counter::NbFailedRefused)); + assert_eq!(h.count(Counter::NbFailedRefused), 1); + assert!(h.out().is_empty()); + assert!(h.run(22_999).is_empty()); + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + h.run(23_000); + assert!(h.state(B).is_none()); + h.at(23_001); + assert_eq!(h.udp_to(B), Ok(None)); + assert!(h.out()[0].requests(B)); +} + +#[test] +fn s_ip_nud_015_an_assertion_revives_failed() { + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + h.out(); + h.run(3_000); + h.at(4_000); + h.frame(&hex(V_ARP_REQ_B)); + assert!(h.is_stale(B)); + assert_eq!(h.mac_of(B), Some(MAC_B)); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_REPLY_A)]); +} + +#[test] +fn s_ip_nud_016_requests_are_spaced() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(100); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + let first = h.out(); + h.at(600); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + let second = h.out(); + let later = h.run(1_100); + assert_eq!(first.iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_POLL_R)]); + assert!(second.is_empty()); + assert_eq!(later.iter().map(|o| (o.at, o.frame.clone())).collect::>(), [(1_100, hex(V_ARP_POLL_R))]); +} + +#[test] +fn s_ip_nud_017_negative_advice_probes() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(100); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + assert!(matches!(h.state(R), Some(Nud::Probe(_)))); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_POLL_R)]); +} + +#[test] +fn s_ip_nud_018_negative_advice_leaves_probe_incomplete_and_failed() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(100); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + h.out(); + h.at(200); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + assert!(matches!(h.state(R), Some(Nud::Probe(p)) if p.requests() == 1)); + assert!(h.out().is_empty()); + + let mut h = H::fixture_i(); + h.ip.set_gateways(h.clock(), h.if0, &[]).unwrap(); + h.udp_to(B).unwrap(); + h.out(); + h.ip.advise(h.clock(), B, Advice::Reverify); + assert!(matches!(h.state(B), Some(Nud::Incomplete(s)) if s.requests() == 1)); + assert!(h.out().is_empty()); + h.run(3_000); + h.at(3_500); + h.ip.advise(h.clock(), B, Advice::Reverify); + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_nud_019_positive_advice_needs_a_mac() { + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + h.out(); + h.ip.advise(h.clock(), B, Advice::Confirmed); + assert!(matches!(h.state(B), Some(Nud::Incomplete(_)))); + h.run(3_000); + h.ip.advise(h.clock(), B, Advice::Confirmed); + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + let (mut h, t0) = unreachable_r(); + h.at(t0 + 10); + h.ip.advise(h.clock(), REMOTE, Advice::Confirmed); + assert!(h.is_reachable(R)); + assert_eq!(h.mac_of(R), Some(MAC_R)); +} + +#[test] +fn s_ip_nud_020_advice_lands_on_the_gateway() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(100); + h.ip.advise(h.clock(), REMOTE, Advice::Reverify); + assert!(matches!(h.state(R), Some(Nud::Probe(_)))); + assert!(h.state(REMOTE).is_none()); +} + +#[test] +fn s_ip_nud_021_stale_is_deleted_when_idle() { + let mut h = H::fixture_i(); + h.stale(B, MAC_B); + h.run(599_999); + assert!(h.is_stale(B)); + h.run(600_000); + assert!(h.state(B).is_none()); + let mut h = H::fixture_i(); + h.stale(B, MAC_B); + h.run(599_999); + h.udp_to(B).unwrap(); + assert!(matches!(h.state(B), Some(Nud::Delay(_)))); +} + +/// if0 at 10.0.0.1/16, with room for a full table of neighbours. +fn wide() -> H { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, Ipv4Addr::new(10, 0, 0, 1), 16, 0); + h.settle(); + h +} + +fn neighbour(n: u32) -> Ipv4Addr { + Ipv4Addr::from(u32::from(Ipv4Addr::new(10, 0, 1, 0)) + n) +} + +fn reach_wide(h: &mut H, addr: Ipv4Addr) { + let now = h.clock(); + let _ = h.ip.resolve(now, h.if0, addr); + h.out(); + let m = MacAddr([2, 1, 0, 0, (u32::from(addr) >> 8) as u8, u32::from(addr) as u8]); + h.frame(ð(MAC_A, m, 0x0806, &arp_packet(2, m, addr, MAC_A, Ipv4Addr::new(10, 0, 0, 1)))); + assert!(h.is_reachable(addr)); +} + +#[test] +fn s_ip_nud_022_a_full_table_evicts_failed_first() { + let mut h = wide(); + let failed = neighbour(0); + let _ = h.ip.resolve(h.clock(), h.if0, failed); + h.out(); + h.run(3_000); + assert!(matches!(h.state(failed), Some(Nud::Failed(_)))); + for n in 1..limits::nud::TABLE_MAX as u32 { + reach_wide(&mut h, neighbour(n)); + } + let new = neighbour(9_000); + assert_eq!(h.send(Ipv4Addr::new(10, 0, 0, 1), new, 5001, 5001, b"x"), Ok(None)); + assert!(h.state(failed).is_none()); + assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); + + let mut h = wide(); + for n in 0..limits::nud::TABLE_MAX as u32 { + reach_wide(&mut h, neighbour(n)); + } + let new = neighbour(9_000); + assert_eq!(h.send(Ipv4Addr::new(10, 0, 0, 1), new, 5001, 5001, b"x"), Err(Counter::NbTableFull)); + assert_eq!(h.count(Counter::NbTableFull), 1); + let asker = neighbour(9_001); + h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, asker, MacAddr::ZERO, Ipv4Addr::new(10, 0, 0, 1)))); + let out = h.out(); + assert_eq!(out.len(), 1); + assert!(out[0].arp().is_some_and(|a| a.target_ip == asker)); + assert!(h.state(asker).is_none()); +} + +#[test] +fn s_ip_nud_023_link_down_drops_the_table() { + let mut h = H::fixture_i(); + for _ in 0..2 { + h.udp_to(B).unwrap(); + } + h.udp_to(DNS).unwrap(); + h.out(); + h.ip.link_down(h.clock(), h.if0).unwrap(); + h.collect(); + assert!(h.state(B).is_none() && h.state(DNS).is_none()); + let told = h.events.iter().filter(|e| matches!(e, Event::Unreachable(_))).count(); + assert_eq!(told, 3); + h.ip.link_up(h.clock(), h.if0).unwrap(); + assert!(h.state(B).is_none() && h.state(DNS).is_none()); +} + +#[test] +fn s_ip_nud_024_requests_speak_from_the_prompting_source() { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, A, 24, 0); + h.assign(if0, Ipv4Addr::new(198, 51, 100, 1), 24, 5_000); + h.settle(); + h.send(Ipv4Addr::new(198, 51, 100, 1), Ipv4Addr::new(198, 51, 100, 9), 5001, 5001, b"x").unwrap(); + let out = h.out(); + assert_eq!(out[0].arp().unwrap().sender_ip, Ipv4Addr::new(198, 51, 100, 1)); + + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + assert_eq!(h.udp_to(B), Err(Counter::RouteNoSourceAddress)); + assert!(h.state(B).is_none()); + assert!(h.out().iter().all(|o| o.arp().is_none_or(|a| a.target_ip != B))); +} + +#[test] +fn s_ip_nud_025_a_flow_waits_outside() { + let mut h = H::fixture_i(); + assert_eq!(h.ip.resolve(h.clock(), h.if0, B), Resolution::Pending); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_REQ)]); + h.frame(&hex(V_ARP_REPLY)); + assert!(h.events.contains(&Event::Resolved { iface: h.if0, next_hop: B })); + assert_eq!(h.ip.resolve(h.clock(), h.if0, B), Resolution::Resolved(MAC_B)); + assert!(h.out().is_empty(), "no segment ever waited in [ip]"); + + let mut h = H::fixture_i(); + assert_eq!(h.ip.resolve(h.clock(), h.if0, B), Resolution::Pending); + h.out(); + h.run(2_999); + assert!(!h.events.iter().any(|e| matches!(e, Event::Failed { .. }))); + h.run(3_000); + assert!(h.events.contains(&Event::Failed { iface: h.if0, next_hop: B })); + assert_eq!(h.ip.resolve(h.clock(), h.if0, B), Resolution::Failed); +} + +#[test] +fn s_ip_nud_026_a_flow_moves_stale_to_delay() { + let mut h = H::fixture_i(); + h.stale(B, MAC_B); + assert_eq!(h.ip.resolve(h.clock(), h.if0, B), Resolution::Resolved(MAC_B)); + assert!(matches!(h.state(B), Some(Nud::Delay(_)))); +} + +#[test] +fn s_ip_nud_027_prop_reachable_time_is_drawn_every_two_hours() { + let two_hours = limits::nud::REACHABLE_REDRAW; + for seed in 1..=8u8 { + let mut h = H::fixture_i(); + let mut rng = Rng(u64::from(seed) * 7919); + let mut last = (h.ip.reachable_time(h.if0).unwrap(), h.clock()); + let mut changes = 0; + let mut t = 0; + while t < 10 * 3_600_000 { + t += 1 + rng.below(1_200_000); + h.at(t); + let addr = Ipv4Addr::new(192, 0, 2, 2 + (t % 200) as u8); + let _ = h.ip.resolve(h.clock(), h.if0, addr); + h.out(); + h.reply_from(addr, MAC_B); + h.fire(t); + let reachable = h.ip.reachable_time(h.if0).unwrap(); + assert!((limits::nud::REACHABLE_MIN..=limits::nud::REACHABLE_MAX).contains(&reachable)); + if reachable != last.0 { + assert!(h.clock().since(last.1) >= two_hours, "seed {seed}: redrawn early at {t}"); + last = (reachable, h.clock()); + changes += 1; + } + } + assert!((3..=5).contains(&changes), "seed {seed}: {changes} redraws in ten hours"); + } +} diff --git a/toyos-net-tcp/src/counters.rs b/toyos-net-tcp/src/counters.rs index 4b09374d926..1889ae7ffb8 100644 --- a/toyos-net-tcp/src/counters.rs +++ b/toyos-net-tcp/src/counters.rs @@ -4,9 +4,8 @@ use alloc::vec::Vec; use core::net::Ipv4Addr; -use core::time::Duration; -use crate::{limits, Endpoint, Event, Instant, Tuple}; +use crate::{limits, Endpoint, Event, Tuple}; toyos_net_wire::counters! { ClosedRst = "tcp.closed-rst"; @@ -121,22 +120,3 @@ pub struct Refusal { pub local: Endpoint, pub remote: Endpoint, } - -/// At most one line per rule in any 10 s, each carrying how many of that rule it stood for. -pub const REFUSAL_LOG_INTERVAL: Duration = Duration::from_secs(10); - -#[derive(Clone, Debug, Default)] -pub struct RefusalLog(PerCounter<(Option, u64)>); - -impl RefusalLog { - /// `Some(suppressed)` when this refusal is to be logged, with how many of its rule were not. - pub fn admit(&mut self, now: Instant, refusal: &Refusal) -> Option { - let (last, suppressed) = self.0.get_mut(refusal.rule); - if last.is_some_and(|at| now.since(at) < REFUSAL_LOG_INTERVAL) { - *suppressed = suppressed.saturating_add(1); - return None; - } - *last = Some(now); - Some(core::mem::replace(suppressed, 0)) - } -} diff --git a/toyos-net-tcp/src/lib.rs b/toyos-net-tcp/src/lib.rs index 69364eb82f0..51d2fe45e26 100644 --- a/toyos-net-tcp/src/lib.rs +++ b/toyos-net-tcp/src/lib.rs @@ -49,7 +49,8 @@ use core::time::Duration; use toyos_net_wire::siphash; use toyos_net_wire::Port; -pub use counters::{Counter, Counters, Refusal, RefusalLog, REFUSAL_LOG_INTERVAL}; +pub use counters::{Counter, Counters, Refusal, RefusalLog}; +pub use toyos_net_wire::REFUSAL_LOG_INTERVAL; pub use seq::Seq; pub use stack::{ConnId, Info, ListenerId, Outgoing, Tcp}; pub use toyos_net_wire::siphash::{siphash24, Key}; diff --git a/toyos-net-tcp/tests/modern.rs b/toyos-net-tcp/tests/modern.rs index 78b5ec54cd5..1ac1dc56680 100644 --- a/toyos-net-tcp/tests/modern.rs +++ b/toyos-net-tcp/tests/modern.rs @@ -65,7 +65,7 @@ fn s_mod_005_one_line_per_rule_per_10_s() { .refusals(Counter::SynFin) .into_iter() .zip(times) - .filter_map(|(r, t)| log.admit(Instant::from_millis(t), &r).map(|n| (r, n))) + .filter_map(|(r, t)| log.admit(Instant::from_millis(t), r.rule).map(|n| (r, n))) .collect(); assert_eq!(lines.len(), 2); assert_eq!((lines[0].0.remote, lines[0].1), (ep(Ipv4Addr::new(192, 0, 2, 10), 1234), 0)); @@ -118,7 +118,7 @@ fn s_mod_007_fast_open() { fn log_lines(h: &H, rule: Counter, times: &[i64]) -> usize { let mut log = RefusalLog::default(); - h.refusals(rule).into_iter().zip(times).filter(|(r, &t)| log.admit(Instant::from_millis(t as u64), r).is_some()).count() + h.refusals(rule).into_iter().zip(times).filter(|(r, &t)| log.admit(Instant::from_millis(t as u64), r.rule).is_some()).count() } #[test] diff --git a/toyos-net-wire/src/igmp.rs b/toyos-net-wire/src/igmp.rs index 4fc9aa6dd98..34950901ce1 100644 --- a/toyos-net-wire/src/igmp.rs +++ b/toyos-net-wire/src/igmp.rs @@ -209,22 +209,30 @@ impl IgmpBody for V2Builder { } } +/// A host's any-source records carry no sources; only the answer to a group-and-source query, +/// IS_IN (B), lists them (RFC 9776 §5.2, Table 5). #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RecordType { - IsExclude, - ToInclude, - ToExclude, +pub enum GroupRecord<'a> { + IsInclude(ReportGroup, &'a [Ipv4Addr]), + IsExclude(ReportGroup), + ToInclude(ReportGroup), + ToExclude(ReportGroup), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct GroupRecord { - pub group: ReportGroup, - pub record: RecordType, +impl GroupRecord<'_> { + fn parts(&self) -> (u8, ReportGroup, &[Ipv4Addr]) { + match *self { + Self::IsInclude(group, sources) => (1, group, sources), + Self::IsExclude(group) => (2, group, &[]), + Self::ToInclude(group) => (3, group, &[]), + Self::ToExclude(group) => (4, group, &[]), + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct V3ReportBuilder<'a> { - pub records: &'a [GroupRecord], + pub records: &'a [GroupRecord<'a>], } impl WritePayload for V3ReportBuilder<'_> { @@ -233,7 +241,10 @@ impl WritePayload for V3ReportBuilder<'_> { } fn length(&self, _header_len: usize) -> Result { - Ok(HEADER_LEN.saturating_add(self.records.len().saturating_mul(8))) + self.records.iter().try_fold(HEADER_LEN, |len, record| { + let (_, _, sources) = record.parts(); + len.checked_add(8)?.checked_add(sources.len().checked_mul(4)?) + }).ok_or(BuildError::IpTooLong) } fn write(&self, _pseudo: &PseudoHeader, out: &mut [u8]) -> Result<(), BuildError> { @@ -241,15 +252,17 @@ impl WritePayload for V3ReportBuilder<'_> { let (header, mut rest) = out.split_first_chunk_mut::().ok_or(BuildError::BufferTooSmall)?; let mut sum = Accumulator::new(); for record in self.records { - let kind = match record.record { - RecordType::IsExclude => 2, - RecordType::ToInclude => 3, - RecordType::ToExclude => 4, - }; - let [g0, g1, g2, g3] = record.group.0.get().octets(); - let fixed = [kind, 0, 0, 0, g0, g1, g2, g3]; + let (kind, group, sources) = record.parts(); + let [n0, n1] = u16::try_from(sources.len()).map_err(|_| BuildError::IpTooLong)?.to_be_bytes(); + let [g0, g1, g2, g3] = group.0.get().octets(); + let fixed = [kind, 0, n0, n1, g0, g1, g2, g3]; sum = sum.feed(&fixed); rest = put(rest, fixed)?; + for source in sources { + let octets = source.octets(); + sum = sum.feed(&octets); + rest = put(rest, octets)?; + } } let [a, b, n0, n1] = be16x2(0x2200, count); let [c0, c1] = sum.feed(&[a, b, 0, 0, 0, 0, n0, n1]).sum().checksum().to_be_bytes(); diff --git a/toyos-net-wire/src/ipv4.rs b/toyos-net-wire/src/ipv4.rs index 3546bd7c18b..877f624b40a 100644 --- a/toyos-net-wire/src/ipv4.rs +++ b/toyos-net-wire/src/ipv4.rs @@ -70,7 +70,7 @@ impl Protocol { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MulticastAddr(Ipv4Addr); impl MulticastAddr { diff --git a/toyos-net-wire/src/lib.rs b/toyos-net-wire/src/lib.rs index 7be1971e2c0..33c1c6434b7 100644 --- a/toyos-net-wire/src/lib.rs +++ b/toyos-net-wire/src/lib.rs @@ -52,11 +52,12 @@ macro_rules! reasons { } /// Declares a crate's counters: `Counter`, each variant the name inspect reads it by and, marked -/// `logged`, a refusal of legacy or insecure input the log names; `Counters`, one value each; and -/// `PerCounter`, one `T` each. +/// `logged`, a refusal of legacy or insecure input the log names; `Counters`, one value each; +/// `PerCounter`, one `T` each; and `RefusalLog`, which admits at most one log line per rule in +/// any [`REFUSAL_LOG_INTERVAL`]. #[macro_export] macro_rules! counters { - ($($variant:ident = $name:literal $(, $logged:ident)?;)*) => { + ($($variant:ident = $name:expr $(, $logged:ident)?;)*) => { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Counter { $($variant,)* @@ -115,6 +116,23 @@ macro_rules! counters { Counter::ALL.iter().map(|&c| (c.name(), self.get(c))) } } + + #[derive(Clone, Debug, Default)] + pub struct RefusalLog(PerCounter<(Option<$crate::Instant>, u64)>); + + impl RefusalLog { + /// `Some(suppressed)` when a refusal of `rule` at `now` is to be logged, with how many + /// of that rule were not since the last line. + pub fn admit(&mut self, now: $crate::Instant, rule: Counter) -> Option { + let (last, suppressed) = self.0.get_mut(rule); + if last.is_some_and(|at| now.since(at) < $crate::REFUSAL_LOG_INTERVAL) { + *suppressed = suppressed.saturating_add(1); + return None; + } + *last = Some(now); + Some(core::mem::replace(suppressed, 0)) + } + } }; (@logged logged) => { true @@ -137,6 +155,8 @@ pub mod udp; pub use emit::BuildError; +pub const REFUSAL_LOG_INTERVAL: core::time::Duration = core::time::Duration::from_secs(10); + /// A point on the caller's monotonic clock, in nanoseconds. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Instant(u64); diff --git a/toyos-net-wire/tests/igmp.rs b/toyos-net-wire/tests/igmp.rs index 9395901c88c..24bf54e0db2 100644 --- a/toyos-net-wire/tests/igmp.rs +++ b/toyos-net-wire/tests/igmp.rs @@ -5,7 +5,7 @@ use std::net::Ipv4Addr; use common::*; use toyos_net_wire::ethernet::{FrameBuilder, MacAddr}; use toyos_net_wire::igmp::{ - self, Deciseconds, GroupRecord, IgmpError, IgmpMessage, IgmpPacket, Query, QueryGroup, QueryVersion, RecordType, + self, Deciseconds, GroupRecord, IgmpError, IgmpMessage, IgmpPacket, Query, QueryGroup, QueryVersion, ReportGroup, V2Builder, V2Kind, V3ReportBuilder, }; use toyos_net_wire::ipv4::{Ecn, Ipv4Option, Ipv4Packet, Ipv4Source, MulticastAddr, TrafficClass}; @@ -233,13 +233,13 @@ fn emit_v3(source: Ipv4Addr, records: &[GroupRecord]) -> Vec { datagram.emit(&mut out).unwrap().to_vec() } -fn record(record: RecordType) -> GroupRecord { - GroupRecord { group: ReportGroup::new(mdns()).unwrap(), record } +fn group() -> ReportGroup { + ReportGroup::new(mdns()).unwrap() } #[test] fn w1_igmp3_join_frame() { - let records = [record(RecordType::ToExclude)]; + let records = [GroupRecord::ToExclude(group())]; let datagram = igmp::datagram(source_a(), network_control(), V3ReportBuilder { records: &records }); let builder = FrameBuilder { destination: MacAddr::multicast(MulticastAddr::IGMPV3_ROUTERS), source: mac_a() }; let mut out = junk(100); @@ -248,15 +248,15 @@ fn w1_igmp3_join_frame() { #[test] fn w1_igmp3_leave_current_and_unspecified_source() { - assert_eq!(emit_v3(IP_A, &[record(RecordType::ToInclude)]), hex(V_IGMP3_LEAVE)); - assert_eq!(emit_v3(IP_A, &[record(RecordType::IsExclude)]), hex(V_IGMP3_CURRENT)); - assert_eq!(emit_v3(Ipv4Addr::UNSPECIFIED, &[record(RecordType::ToExclude)]), hex(V_IGMP3_JOIN_UNSPEC)); + assert_eq!(emit_v3(IP_A, &[GroupRecord::ToInclude(group())]), hex(V_IGMP3_LEAVE)); + assert_eq!(emit_v3(IP_A, &[GroupRecord::IsExclude(group())]), hex(V_IGMP3_CURRENT)); + assert_eq!(emit_v3(Ipv4Addr::UNSPECIFIED, &[GroupRecord::ToExclude(group())]), hex(V_IGMP3_JOIN_UNSPEC)); } #[test] fn w1_igmp3_records_layout() { let other = ReportGroup::new(MulticastAddr::new(Ipv4Addr::new(239, 1, 2, 3)).unwrap()).unwrap(); - let records = [record(RecordType::ToExclude), GroupRecord { group: other, record: RecordType::IsExclude }]; + let records = [GroupRecord::ToExclude(group()), GroupRecord::IsExclude(other)]; let bytes = emit_v3(IP_A, &records); let ip = Ipv4Packet::parse(&bytes).unwrap(); let igmp = ip.payload(); @@ -267,3 +267,18 @@ fn w1_igmp3_records_layout() { assert_eq!(igmp[16..24], hex("02 00 00 00 ef 01 02 03")[..]); assert_eq!(igmp.len(), 24); } + +/// ip.md W-1: IS_IN (B) is type 1 with B's count and addresses after the group (RFC 9776 §4.2.4). +#[test] +fn w1_igmp3_is_include_lists_its_sources() { + let sources = [Ipv4Addr::new(192, 0, 2, 9), Ipv4Addr::new(192, 0, 2, 10)]; + let records = [GroupRecord::IsInclude(group(), &sources), GroupRecord::IsExclude(group())]; + let bytes = emit_v3(IP_A, &records); + let ip = Ipv4Packet::parse(&bytes).unwrap(); + let igmp = ip.payload(); + assert_eq!(oracle_sum(igmp), 0xFFFF); + assert_eq!(igmp[4..8], [0, 0, 0, 2]); + assert_eq!(igmp[8..24], hex("01 00 00 02 e0 00 00 fb c0 00 02 09 c0 00 02 0a")[..]); + assert_eq!(igmp[24..32], hex("02 00 00 00 e0 00 00 fb")[..]); + assert_eq!(igmp.len(), 32); +} From 0da64015044950173748fee560c264aa49fbaf34 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 23:29:16 +0200 Subject: [PATCH 03/20] toyos-net-ip: the NBR, ACD, ICMP and IGMP scenarios Every [5] scenario of the IP specification has its test but OUT-07, which needs the composed shard (stage 4): 185 of 186, plus the NUD-28 compile-fail doctests, and the wire specification's owed [ip] and [shell] rows except UDP-17 and UDP-18, which toyos-net-udp owns. Scenarios whose numbers the IP-D1 ruling moved are tested on the ruled schedule: probing scaled so the add-to-first-announcement worst case is 200 ms, ANNOUNCE_WAIT after the third probe's hand-off, announcements 2 s apart as RFC 5227 keeps them. Co-Authored-By: Claude Opus 5.5 --- toyos-net-ip/tests/acd.rs | 266 +++++++++++++ toyos-net-ip/tests/clk.rs | 6 +- toyos-net-ip/tests/common/mod.rs | 19 +- toyos-net-ip/tests/icmp.rs | 615 +++++++++++++++++++++++++++++++ toyos-net-ip/tests/igmp.rs | 536 +++++++++++++++++++++++++++ toyos-net-ip/tests/nbr.rs | 246 +++++++++++++ 6 files changed, 1683 insertions(+), 5 deletions(-) create mode 100644 toyos-net-ip/tests/acd.rs create mode 100644 toyos-net-ip/tests/icmp.rs create mode 100644 toyos-net-ip/tests/igmp.rs create mode 100644 toyos-net-ip/tests/nbr.rs diff --git a/toyos-net-ip/tests/acd.rs b/toyos-net-ip/tests/acd.rs new file mode 100644 index 00000000000..676740dd1a5 --- /dev/null +++ b/toyos-net-ip/tests/acd.rs @@ -0,0 +1,266 @@ +//! §8: address conflict detection, on RFC 5227's schedule scaled by the IP-D1 ruling. + +mod common; + +use std::net::Ipv4Addr; +use std::time::Duration; + +use common::*; +use toyos_net_ip::limits::acd::{ANNOUNCE_INTERVAL, ANNOUNCE_WAIT, PROBE_MAX, PROBE_MIN, PROBE_WAIT, RATE_LIMIT_INTERVAL}; +use toyos_net_ip::{AddrState, Counter, Event, Instant, Peer, Source}; +use toyos_net_wire::ethernet::MacAddr; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +/// Frames with their exact hand-off instants, from a loop firing each deadline at its own time. +fn run_exact(h: &mut H, until: Instant) -> Vec<(Instant, Vec)> { + let mut out = Vec::new(); + while let Some(d) = h.ip.next_deadline().filter(|d| *d <= until) { + h.ip.fire(d); + h.ip.transmit(d, usize::MAX, |_, f| out.push((d, f.to_vec()))); + h.collect(); + } + out +} + +fn probing() -> H { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + h +} + +/// Probing until the first probe left; returns when. +fn first_probe(h: &mut H) -> Instant { + let d = h.ip.next_deadline().unwrap(); + h.ip.fire(d); + assert_eq!(h.ip.transmit(d, usize::MAX, |_, _| {}), 1); + h.now = d.nanos() / 1_000_000 - BASE_MS; + d +} + +#[test] +fn s_ip_acd_001_the_ruled_schedule() { + let mut h = probing(); + let t0 = h.clock(); + let out = run_exact(&mut h, t0.after(Duration::from_secs(5))); + let probes: Vec = out.iter().filter(|(_, f)| *f == hex(V_ARP_PROBE)).map(|(at, _)| *at).collect(); + let announcements: Vec = out.iter().filter(|(_, f)| *f == hex(V_ARP_ANNOUNCE)).map(|(at, _)| *at).collect(); + assert_eq!(probes.len(), 3); + assert_eq!(announcements.len(), 2); + assert!(probes[0].since(t0) <= PROBE_WAIT); + for pair in probes.windows(2) { + assert!((PROBE_MIN..=PROBE_MAX).contains(&pair[1].since(pair[0]))); + } + assert_eq!(announcements[0].since(probes[2]), ANNOUNCE_WAIT); + assert_eq!(announcements[1].since(announcements[0]), ANNOUNCE_INTERVAL); + let usable = announcements[0].since(t0); + assert!(usable >= PROBE_MIN * 2 + ANNOUNCE_WAIT && usable <= Duration::from_millis(200), "{usable:?}"); + assert!(h.events.contains(&Event::Verified { iface: h.if0, addr: A })); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); +} + +#[test] +fn s_ip_acd_002_a_tentative_address_is_no_source() { + let mut h = probing(); + first_probe(&mut h); + assert_eq!(h.ip.route(B, Source::Bound(A), None), Err(Counter::RouteNoSourceAddress)); + assert_eq!(h.udp_to(B), Err(Counter::RouteNoSourceAddress)); + assert!(h.state(B).is_none()); +} + +fn conflict_while_probing(frame: Vec) { + let mut h = probing(); + first_probe(&mut h); + h.frame(&frame); + assert_eq!(h.ip.address(h.if0, A), None); + assert!(h.events.contains(&Event::Conflict { iface: h.if0, addr: A, mac: MAC_B })); + assert_eq!(h.refusals(Counter::AcdConflict)[0].peer, Peer::Arp { ip: A, mac: MAC_B }); + assert!(h.run(10_000).is_empty(), "no more probes"); +} + +#[test] +fn s_ip_acd_003_an_answer_to_our_probe() { + conflict_while_probing(eth(MAC_A, MAC_B, 0x0806, &arp_packet(2, MAC_B, A, MAC_A, ip4(0, 0, 0, 0)))); +} + +#[test] +fn s_ip_acd_004_an_announcement_of_our_candidate() { + conflict_while_probing(hex(V_ARP_CONFLICT_B)); +} + +#[test] +fn s_ip_acd_005_a_simultaneous_prober() { + conflict_while_probing(hex(V_ARP_PROBE_B)); +} + +#[test] +fn s_ip_acd_006_our_own_probe_reflected() { + let mut h = probing(); + first_probe(&mut h); + let mut reflected = hex(V_ARP_PROBE); + reflected[6..12].copy_from_slice(&MAC_X.0); + h.frame(&reflected); + assert_eq!(h.count(Counter::ArpOwnSender), 1); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Tentative)); +} + +/// Probing through the third probe; returns when it left. +fn third_probe(h: &mut H) -> Instant { + let mut probes = 0; + let mut last = h.clock(); + while probes < 3 { + let d = h.ip.next_deadline().unwrap(); + h.ip.fire(d); + probes += h.ip.transmit(d, usize::MAX, |_, _| {}); + last = d; + } + last +} + +#[test] +fn s_ip_acd_007_the_conflict_window_ends_with_announce_wait() { + let mut h = probing(); + let third = third_probe(&mut h); + let _ = h.ip.receive(third.after(ANNOUNCE_WAIT.saturating_sub(Duration::from_millis(1))), h.if0, &hex(V_ARP_CONFLICT_B)); + h.collect(); + assert_eq!(h.ip.address(h.if0, A), None); + assert!(h.events.contains(&Event::Conflict { iface: h.if0, addr: A, mac: MAC_B })); + + let mut h = probing(); + let third = third_probe(&mut h); + let at = third.after(ANNOUNCE_WAIT + Duration::from_millis(1)); + h.ip.fire(at); + h.ip.transmit(at, usize::MAX, |_, _| {}); + let _ = h.ip.receive(at, h.if0, &hex(V_ARP_CONFLICT_B)); + h.collect(); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + let mut out = Vec::new(); + h.ip.transmit(at, usize::MAX, |_, f| out.push(f.to_vec())); + assert_eq!(out, [hex(V_ARP_ANNOUNCE)], "defended with one announcement"); + assert_eq!(h.count(Counter::AcdDefended), 1); +} + +/// Ten addresses lost while probing on if0; returns when the tenth's first probe left. +fn ten_conflicts(h: &mut H) -> Instant { + let mut started = h.clock(); + for n in 0..10u8 { + let candidate = ip4(192, 0, 2, 10 + n); + h.ip.add_address(h.clock(), h.if0, candidate, 24).unwrap(); + started = first_probe(h); + let conflict = eth(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, MAC_B, candidate, MacAddr::ZERO, candidate)); + h.frame(&conflict); + assert_eq!(h.ip.address(h.if0, candidate), None); + } + assert_eq!(h.count(Counter::AcdConflict), 10); + started +} + +#[test] +fn s_ip_acd_008_ten_conflicts_rate_limit_new_candidates() { + let mut h = H::bare(); + let tenth = ten_conflicts(&mut h); + let added = tenth.after(Duration::from_secs(5)); + h.ip.add_address(added, h.if0, ip4(192, 0, 2, 40), 24).unwrap(); + assert_eq!(h.count(Counter::AcdRateLimited), 1); + assert_eq!(h.ip.next_deadline(), Some(tenth.after(RATE_LIMIT_INTERVAL))); +} + +#[test] +fn s_ip_acd_009_ten_quiet_minutes_lift_the_limit() { + let mut h = H::bare(); + ten_conflicts(&mut h); + let later = h.clock().after(Duration::from_secs(600)); + h.ip.add_address(later, h.if0, ip4(192, 0, 2, 40), 24).unwrap(); + assert_eq!(h.count(Counter::AcdRateLimited), 0); + assert!(h.ip.next_deadline().unwrap().since(later) <= PROBE_WAIT); +} + +#[test] +fn s_ip_acd_010_one_defence() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_CONFLICT_B)); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_ANNOUNCE)]); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); + assert_eq!(h.refusals(Counter::AcdDefended)[0].peer, Peer::Arp { ip: A, mac: MAC_B }); +} + +#[test] +fn s_ip_acd_011_a_second_conflict_inside_the_interval_loses() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_CONFLICT_B)); + h.out(); + h.at(9_999); + h.frame(&hex(V_ARP_CONFLICT_B)); + assert_eq!(h.ip.address(h.if0, A), None); + assert!(h.events.contains(&Event::Lost { iface: h.if0, addr: A, mac: MAC_B })); + assert_eq!(h.count(Counter::AcdConflict), 1); +} + +#[test] +fn s_ip_acd_012_a_second_conflict_after_the_interval_is_defended() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_CONFLICT_B)); + h.out(); + h.at(10_001); + h.frame(&hex(V_ARP_CONFLICT_B)); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); + assert_eq!(h.count(Counter::AcdDefended), 2); +} + +#[test] +fn s_ip_acd_013_an_announcing_address_answers_probes() { + let mut h = probing(); + third_probe(&mut h); + let d = h.ip.next_deadline().unwrap(); + h.ip.fire(d); + h.ip.transmit(d, usize::MAX, |_, _| {}); + h.now = d.nanos() / 1_000_000 - BASE_MS + 1; + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + h.frame(&hex(V_ARP_PROBE_B)); + assert_eq!(h.out().iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_REPLY_TO_PROBE)]); +} + +#[test] +fn s_ip_acd_014_a_renewal_is_not_probed() { + let mut h = H::fixture_i(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + assert!(h.run(10_000).is_empty()); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); +} + +#[test] +fn s_ip_acd_015_link_down_abandons_probing() { + let mut h = probing(); + first_probe(&mut h); + h.ip.link_down(h.clock(), h.if0).unwrap(); + h.collect(); + assert!(h.events.contains(&Event::NotVerified { iface: h.if0, addr: A })); + assert_eq!(h.ip.address(h.if0, A), None); +} + +#[test] +fn s_ip_acd_016_link_up_announces_without_probing() { + let mut h = H::fixture_i(); + h.ip.link_down(h.clock(), h.if0).unwrap(); + h.ip.link_up(h.clock(), h.if0).unwrap(); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); + let arp: Vec<(u64, Vec)> = h.run(5_000).into_iter().filter(|o| o.arp().is_some()).map(|o| (o.at, o.frame)).collect(); + assert_eq!(arp, [(0, hex(V_ARP_ANNOUNCE)), (2_000, hex(V_ARP_ANNOUNCE))]); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); +} + +#[test] +fn s_ip_acd_017_detection_starts_at_the_first_announcement() { + let mut h = probing(); + third_probe(&mut h); + let d = h.ip.next_deadline().unwrap(); + h.ip.fire(d); + h.ip.transmit(d, usize::MAX, |_, _| {}); + h.now = d.nanos() / 1_000_000 - BASE_MS + 1; + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + h.frame(&hex(V_ARP_CONFLICT_B)); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); + assert_eq!(h.count(Counter::AcdDefended), 1); +} diff --git a/toyos-net-ip/tests/clk.rs b/toyos-net-ip/tests/clk.rs index 864ec22382a..96fd6ab048d 100644 --- a/toyos-net-ip/tests/clk.rs +++ b/toyos-net-ip/tests/clk.rs @@ -83,7 +83,9 @@ fn s_ip_clk_005_a_jump_owes_one_request() { } /// The trace of a seeded run: every frame, every event, every counter. -fn trace(secret: [u8; 16], seed: u64) -> (Vec<(u64, Vec)>, Vec, Vec, std::time::Duration) { +type Trace = (Vec<(u64, Vec)>, Vec, Vec, std::time::Duration); + +fn trace(secret: [u8; 16], seed: u64) -> Trace { let mut ip = toyos_net_ip::Ip::new(Instant::from_nanos(0), secret); let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_A)); ip.link_up(Instant::from_nanos(0), if0).unwrap(); @@ -95,7 +97,7 @@ fn trace(secret: [u8; 16], seed: u64) -> (Vec<(u64, Vec)>, Vec, Vec< hex(V_ARP_REPLY_R), hex(V_ARP_CONFLICT_B), to_a(&hex(V_UDP_HI)), - to_a(&hex(V_ICMP_ECHO).iter().copied().collect::>()), + to_a(&hex(V_ICMP_ECHO)), eth(MAC_A, MAC_R, 0x0800, &hex(V_IGMP3_QUERY_GEN)), ]; let mut now = 0u64; diff --git a/toyos-net-ip/tests/common/mod.rs b/toyos-net-ip/tests/common/mod.rs index b4a53ba6962..30cfea5380e 100644 --- a/toyos-net-ip/tests/common/mod.rs +++ b/toyos-net-ip/tests/common/mod.rs @@ -71,7 +71,7 @@ fn pseudo(ip: &[u8], protocol: u8, length: usize) -> Vec { } /// Recomputes every checksum of an IPv4 datagram: header, then UDP, TCP, ICMP or IGMP. -pub fn fix(ip: &mut Vec) { +pub fn fix(ip: &mut [u8]) { fix_ip_header(ip); let ihl = usize::from(ip[0] & 0x0f) * 4; let total = usize::from(u16::from_be_bytes([ip[2], ip[3]])); @@ -219,7 +219,7 @@ impl H { let mut ip = Ip::new(Instant::from_nanos(0), SECRET); let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_A)); ip.link_up(Instant::from_nanos(0), if0).unwrap(); - Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: Default::default() } + Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: std::collections::BTreeMap::default() } } /// `if0` up with no address, at t = 0. @@ -764,7 +764,7 @@ impl H { let mut ip = Ip::new(Instant::from_nanos(0), SECRET); let if0 = ip.add_interface(Instant::from_nanos(0), mac(MAC_B)); ip.link_up(Instant::from_nanos(0), if0).unwrap(); - let mut h = Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: Default::default() }; + let mut h = Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: std::collections::BTreeMap::default() }; h.assign(if0, B, 24, 0); let setup = Instant::from_millis(5_000); let _ = h.ip.resolve(setup, if0, A); @@ -787,3 +787,16 @@ pub fn cast_of(d: &Option>) -> Option None, } } + +impl H { + /// A host with MAC `m` holding `addr/len`, assigned through conflict detection, nothing learned. + pub fn host(m: MacAddr, addr: Ipv4Addr, len: u8) -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), SECRET); + let if0 = ip.add_interface(Instant::from_nanos(0), mac(m)); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + let mut h = Self { ip, if0, now: 0, events: Vec::new(), counters: Vec::new(), wire: std::collections::BTreeMap::default() }; + h.assign(if0, addr, len, 0); + h.settle(); + h + } +} diff --git a/toyos-net-ip/tests/icmp.rs b/toyos-net-ip/tests/icmp.rs new file mode 100644 index 00000000000..01b32f3171d --- /dev/null +++ b/toyos-net-ip/tests/icmp.rs @@ -0,0 +1,615 @@ +//! §9: ICMPv4 — echo, error generation under the limiter, and inbound errors. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{Arrival, Cast, Counter, Delivery, ErrorClass, ErrorKind, Event, Instant, Limiter, Nud, Peer, Transport}; +use toyos_net_wire::ethernet::{MacAddr, MacClass}; +use toyos_net_wire::icmp::{IcmpError, ParameterProblemCode, TimeExceededCode, UnreachableCode}; +use toyos_net_wire::ipv4::Ipv4Packet; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +/// V-ICMP-ECHO from B to A. +fn echo_to_a() -> Vec { + edit(hex(V_ICMP_ECHO), |ip| { + set_source(ip, B); + set_destination(ip, A); + }) +} + +fn icmp_types(out: &[Out]) -> Vec<(u8, u8)> { + out.iter().filter_map(|o| o.ip()).filter(|ip| ip.protocol() == toyos_net_wire::ipv4::Protocol::Icmp).map(|ip| (ip.payload()[0], ip.payload()[1])).collect() +} + +/// The frames that left for a port unreachable about `datagram` from B's MAC. +fn closed_port(h: &mut H, datagram: &[u8]) -> Vec { + if let Some(Delivery::Udp(arrival, _)) = h.datagram(datagram) { + h.ip.port_unreachable(h.clock(), &arrival); + } + h.out() +} + +fn hi_from(source: Ipv4Addr) -> Vec { + edit(hex(V_UDP_HI), |ip| set_source(ip, source)) +} + +/// Fixture I with B and R both REACHABLE. +fn b_and_r() -> H { + let mut h = H::fixture_i_with(B, MAC_B); + h.reach(R, MAC_R); + h.rebase(); + h +} + +#[test] +fn s_ip_echo_001_the_mirror_of_the_request() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram(&echo_to_a()); + let out = h.out(); + assert_eq!(out.len(), 1); + let mirror = edit(hex(V_ICMP_REPLY), |ip| { + set_source(ip, A); + set_destination(ip, B); + }); + assert_eq!(ip_bytes(&out[0].frame), mirror); + assert_eq!(out[0].to(), MAC_B); + assert_eq!(h.count(Counter::IcmpEchoRepliesSent), 1); +} + +#[test] +fn s_ip_echo_002_dscp_copied_ecn_cleared() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram(&edit(echo_to_a(), |ip| ip[1] = 46 << 2 | 3)); + let reply = h.out(); + let tc = reply[0].ip().unwrap().traffic_class(); + assert_eq!((tc.dscp(), tc.ecn()), (46, toyos_net_wire::ipv4::Ecn::NotEct)); +} + +#[test] +fn s_ip_echo_003_an_off_link_requester_is_answered_through_the_gateway() { + let mut h = H::fixture_i(); + let request = edit(echo_to_a(), |ip| set_source(ip, REMOTE)); + h.frame(ð(MAC_A, MAC_R, 0x0800, &request)); + let first = h.out(); + assert_eq!(first.len(), 1); + assert!(first[0].requests(R), "an ARP request for R precedes the reply"); + h.reply_from(R, MAC_R); + let reply = h.out(); + assert_eq!(reply.len(), 1); + assert_eq!(reply[0].to(), MAC_R); + assert_eq!(reply[0].ip().unwrap().destination(), REMOTE); +} + +#[test] +fn s_ip_echo_004_the_reply_carries_no_options() { + let mut h = H::fixture_i_with(B, MAC_B); + let mut request = vec![0x47, 0, 0, 0, 0, 0, 0x40, 0, 64, 1, 0, 0, 192, 0, 2, 2, 192, 0, 2, 1, 7, 7, 4, 0, 0, 0, 0, 0]; + request.extend_from_slice(&hex(V_ICMP_ECHO)[20..]); + let total = request.len() as u16; + request[2..4].copy_from_slice(&total.to_be_bytes()); + fix(&mut request); + h.datagram(&request); + let reply = h.out(); + let ip = reply[0].ip().unwrap(); + assert_eq!(ip.header_len(), 20); + assert_eq!(ip.payload()[4..], hex(V_ICMP_ECHO)[24..]); +} + +#[test] +fn s_ip_echo_005_sixteen_replies_wait() { + let mut h = H::fixture_i_with(B, MAC_B); + for _ in 0..20 { + h.datagram(&echo_to_a()); + } + assert_eq!(h.count(Counter::IcmpEchoReplyDropped), 4); + assert_eq!(h.out().len(), 16); +} + +#[test] +fn s_ip_echo_006_an_echo_reply_is_counted() { + let mut h = H::fixture_i(); + h.datagram(&hex(V_ICMP_REPLY)); + assert_eq!(h.count(Counter::IcmpEchoReply), 1); + assert!(h.out().is_empty()); + let others: u64 = Counter::ALL.iter().filter(|&&c| c != Counter::IcmpEchoReply).map(|&c| h.count(c)).sum(); + assert_eq!(others, 0); +} + +#[test] +fn s_ip_echo_007_a_tentative_address_does_not_answer() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + h.datagram(&echo_to_a()); + assert_eq!(h.count(Counter::IpTentativeDestination), 1); + assert!(h.out().iter().all(|o| o.ip().is_none())); +} + +#[test] +fn s_ip_echo_008_echo_is_not_rate_limited() { + let mut h = H::fixture_i_with(B, MAC_B); + for _ in 0..10 { + closed_port(&mut h, &hex(V_UDP_HI)); + } + assert_eq!(closed_port(&mut h, &hex(V_UDP_HI)).len(), 0, "B's bucket is empty"); + h.datagram(&echo_to_a()); + assert_eq!(icmp_types(&h.out()), [(0, 0)]); +} + +#[test] +fn s_ip_icg_001_protocol_unreachable() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram(&hex(V_IP_MIN)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(ip_bytes(&out[0].frame), hex(V_ICMP_PROTO_UNREACH_GEN)); +} + +#[test] +fn s_ip_icg_002_port_unreachable() { + let mut h = H::fixture_i_with(B, MAC_B); + let out = closed_port(&mut h, &hex(V_UDP_HI)); + assert_eq!(out.len(), 1); + assert_eq!(ip_bytes(&out[0].frame), hex(V_ICMP_PORT_UNREACH_GEN)); + assert_eq!(h.count(Counter::IcmpErrorsSent), 1); +} + +/// ICG-03's four: to a group, a subnet broadcast, the limited broadcast, and about an ICMP error. +fn suppressed_requests(h: &mut H) { + for (frame_to, destination) in [(MacAddr([1, 0, 0x5e, 0, 0, 0xfb]), MDNS), (MacAddr::BROADCAST, ip4(192, 0, 2, 255)), (MacAddr::BROADCAST, ip4(255, 255, 255, 255))] { + let Some(Delivery::Udp(arrival, _)) = h.datagram_to(frame_to, &edit(hex(V_UDP_HI), |ip| set_destination(ip, destination))) else { panic!() }; + h.ip.port_unreachable(h.clock(), &arrival); + } + let about_error = edit(hex(V_ICMP_PORT_UNREACH), |ip| { + set_source(ip, B); + set_destination(ip, A); + }); + let leaked: &'static [u8] = Box::leak(about_error.into_boxed_slice()); + let arrival = Arrival { iface: h.if0, packet: Ipv4Packet::parse(leaked).unwrap(), cast: Cast::Unicast, link: MacClass::Individual }; + h.ip.port_unreachable(h.clock(), &arrival); +} + +#[test] +fn s_ip_icg_003_suppression_takes_no_token() { + let mut h = H::fixture_i_with(B, MAC_B); + suppressed_requests(&mut h); + assert!(h.out().is_empty()); + assert_eq!(h.count(Counter::IcmpErrorSuppressed), 4); + for _ in 0..10 { + assert_eq!(closed_port(&mut h, &hex(V_UDP_HI)).len(), 1, "no token was taken"); + } +} + +#[test] +fn s_ip_icg_004_refusals_are_silent() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram(&hex(V_IP_LSRR)); + h.datagram(&hex(V_IP_FRAG_FIRST)); + h.datagram(&edit(hex(V_UDP_HI), |ip| set_destination(ip, ip4(192, 0, 2, 7)))); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_icg_005_prop_only_echo_replies_and_unreachables() { + let mut h = b_and_r(); + let mut rng = Rng(0x1c95); + let inputs = [echo_to_a(), hex(V_IP_MIN), hex(V_UDP_HI), hex(V_IP_LSRR), hex(V_ICMP_PORT_UNREACH), hex(V_ICMP_REPLY), edit(hex(V_ICMP_TIME_EXCEEDED), |_| {})]; + let mut out = Vec::new(); + for step in 0..400u64 { + h.at(step * 13); + let input = &inputs[rng.below(inputs.len() as u64) as usize]; + if let Some(Delivery::Udp(arrival, _)) = h.datagram(input) { + h.ip.port_unreachable(h.clock(), &arrival); + } + out.extend(h.out()); + } + let types = icmp_types(&out); + assert!(types.len() > 50); + assert!(types.iter().all(|t| matches!(t, (0, 0) | (3, 2) | (3, 3))), "{types:?}"); +} + +#[test] +fn s_ip_icg_006_an_unknown_protocol_to_a_broadcast() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram_to(MacAddr::BROADCAST, &edit(hex(V_IP_MIN), |ip| set_destination(ip, ip4(192, 0, 2, 255)))); + assert_eq!(h.count(Counter::IpProtocol), 1); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_icg_007_short_datagrams_are_quoted_whole() { + let mut h = H::fixture_i_with(B, MAC_B); + let out = closed_port(&mut h, &hex(V_UDP_HI)); + assert_eq!(out[0].ip().unwrap().payload()[8..], hex(V_UDP_HI)[..]); + h.datagram(&hex(V_IP_MIN)); + let out = h.out(); + assert_eq!(out[0].ip().unwrap().payload()[8..], hex(V_IP_MIN)[..]); +} + +#[test] +fn s_ip_icg_008_a_malformed_router_alert_is_dropped_silently() { + let mut h = H::fixture_i_with(B, MAC_B); + let mut ip = vec![0x46, 0, 0, 0, 0, 0, 0x40, 0, 64, 17, 0, 0, 192, 0, 2, 2, 192, 0, 2, 1, 0x94, 3, 0, 0]; + ip.extend_from_slice(&hex(V_UDP_HI)[20..]); + let total = ip.len() as u16; + ip[2..4].copy_from_slice(&total.to_be_bytes()); + fix(&mut ip); + assert!(h.datagram(&ip).is_none()); + assert_eq!(h.wire("ip.router-alert-length"), 1); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_rl_001_ten_then_limited() { + let mut h = H::fixture_i_with(B, MAC_B); + let sent: usize = (0..15).map(|_| closed_port(&mut h, &hex(V_UDP_HI)).len()).sum(); + assert_eq!(sent, 10); + assert_eq!(h.count(Counter::IcmpErrorRateLimited), 5); +} + +#[test] +fn s_ip_rl_002_one_token_per_100_ms() { + let mut h = H::fixture_i_with(B, MAC_B); + for _ in 0..15 { + closed_port(&mut h, &hex(V_UDP_HI)); + } + let at = |h: &mut H, t: u64| { + h.at(t); + closed_port(h, &hex(V_UDP_HI)).len() + }; + assert_eq!([at(&mut h, 99), at(&mut h, 100), at(&mut h, 150), at(&mut h, 200)], [0, 1, 0, 1]); +} + +#[test] +fn s_ip_rl_003_another_destination_has_its_own_bucket() { + let mut h = b_and_r(); + for _ in 0..15 { + closed_port(&mut h, &hex(V_UDP_HI)); + } + let remote = edit(hex(V_UDP_HI), |ip| set_source(ip, REMOTE)); + let out = closed_port(&mut h, &remote); + assert_eq!(out.len(), 1); + assert_eq!(out[0].to(), MAC_R); +} + +fn two_hundred_sources() -> usize { + let mut h = b_and_r(); + (1..=200u32).map(|n| closed_port(&mut h, &hi_from(Ipv4Addr::from(u32::from(ip4(198, 51, 100, 0)) + n))).len()).sum() +} + +#[test] +fn s_ip_rl_004_the_global_burst_is_drawn() { + let sent = two_hundred_sources(); + assert!((75..=100).contains(&sent), "{sent}"); + assert_eq!(two_hundred_sources(), sent); +} + +#[test] +fn s_ip_rl_005_the_global_bucket_refills_at_100_a_second() { + let mut h = b_and_r(); + for n in 0..200u32 { + closed_port(&mut h, &hi_from(Ipv4Addr::from(u32::from(ip4(198, 51, 100, 0)) + n + 1))); + } + h.at(10); + assert_eq!(closed_port(&mut h, &hi_from(ip4(203, 0, 113, 1))).len(), 1); + h.at(15); + assert_eq!(closed_port(&mut h, &hi_from(ip4(203, 0, 113, 2))).len(), 0); +} + +#[test] +fn s_ip_rl_006_prop_colliding_destinations_share_a_bucket() { + let limiter = Limiter::new([1; 16]); + let base = ip4(203, 0, 113, 0); + let (first, second) = (0..4096u32) + .map(|n| Ipv4Addr::from(u32::from(base) + n)) + .find_map(|a| (1..4096u32).map(|m| Ipv4Addr::from(u32::from(a) + m)).find(|b| limiter.slot(*b) == limiter.slot(a)).map(|b| (a, b))) + .unwrap(); + let mut shared = Limiter::new([1; 16]); + let t = Instant::from_millis(1); + let passed = (0..20).filter(|n| shared.allow(t, if n % 2 == 0 { first } else { second })).count(); + assert_eq!(passed, 10, "one bucket of ten between them"); + let other = Limiter::new([2; 16]); + let colliding_elsewhere = (0..64u32).all(|n| { + let a = Ipv4Addr::from(u32::from(first) + n); + let b = Ipv4Addr::from(u32::from(second) + n); + (limiter.slot(a) == limiter.slot(b)) == (other.slot(a) == other.slot(b)) + }); + assert!(!colliding_elsewhere, "the collision set is the secret's"); +} + +#[test] +fn s_ip_rl_007_suppressed_requests_spend_nothing() { + let mut h = H::fixture_i_with(B, MAC_B); + for _ in 0..5 { + suppressed_requests(&mut h); + } + let sent: usize = (0..10).map(|_| closed_port(&mut h, &hex(V_UDP_HI)).len()).sum(); + assert_eq!(sent, 10); +} + +fn delivered_error(h: &mut H, datagram: &[u8]) -> toyos_net_ip::TransportError { + match h.frame(ð(MAC_A, MAC_R, 0x0800, datagram)) { + Some(Delivery::Error(e)) => e, + other => panic!("{other:?}"), + } +} + +#[test] +fn s_ip_icd_001_a_port_unreachable_for_udp() { + let mut h = H::fixture_i(); + let e = delivered_error(&mut h, &hex(V_ICMP_PORT_UNREACH)); + assert_eq!(e.transport, Transport::Udp); + assert_eq!(e.kind, ErrorKind::Unreachable(UnreachableCode::Port)); + assert_eq!((e.flow.source, e.flow.source_port.get(), e.flow.destination, e.flow.destination_port.get()), (A, 49152, DNS, 53)); + assert_eq!(e.reporter, DNS); +} + +#[test] +fn s_ip_icd_002_fragmentation_needed_for_tcp() { + let mut h = H::fixture_i(); + let generation = h.ip.generation(); + let e = delivered_error(&mut h, &hex(V_ICMP_FRAG_NEEDED)); + assert_eq!(e.transport, Transport::Tcp); + assert_eq!(e.kind, ErrorKind::FragmentationNeeded { next_hop_mtu: std::num::NonZeroU16::new(1_400), quoted_length: 1_500 }); + assert_eq!(e.sequence, Some(0x1111_1111)); + assert_eq!(e.reporter, R); + assert_eq!(h.ip.generation(), generation); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_icd_003_an_error_to_a_broadcast() { + let mut h = H::fixture_i(); + h.datagram_to(MacAddr::BROADCAST, &edit(hex(V_ICMP_PORT_UNREACH), |ip| set_destination(ip, ip4(192, 0, 2, 255)))); + assert_eq!(h.count(Counter::IcmpErrorToGroup), 1); +} + +#[test] +fn s_ip_icd_004_a_quote_from_a_tentative_address() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, ip4(192, 0, 2, 9), 24).unwrap(); + h.run(200); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + let e = edit(hex(V_ICMP_PORT_UNREACH), |ip| set_destination(ip, ip4(192, 0, 2, 9))); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &e)); + assert_eq!(h.count(Counter::IcmpQuoteNotOurs), 1); +} + +fn error_quoting(protocol: u8) -> Vec { + edit(hex(V_ICMP_PORT_UNREACH), |ip| { + ip[28 + 9] = protocol; + ip[28 + 10] = 0; + ip[28 + 11] = 0; + }) +} + +#[test] +fn s_ip_icd_005_a_quote_of_icmp() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &error_quoting(1))); + assert_eq!(h.count(Counter::IcmpQuoteIcmp), 1); +} + +#[test] +fn s_ip_icd_006_quotes_of_igmp_and_others() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &error_quoting(2))); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &error_quoting(253))); + assert_eq!((h.count(Counter::IcmpQuoteIgmp), h.count(Counter::IcmpQuoteOtherProtocol)), (1, 1)); +} + +#[test] +fn s_ip_icd_007_time_exceeded() { + let mut h = H::fixture_i(); + let e = delivered_error(&mut h, &hex(V_ICMP_TIME_EXCEEDED)); + assert_eq!((e.transport, e.kind), (Transport::Tcp, ErrorKind::TimeExceeded(TimeExceededCode::InTransit))); + assert_eq!((e.flow.source_port.get(), e.flow.destination, e.flow.destination_port.get()), (49153, REMOTE, 443)); +} + +#[test] +fn s_ip_icd_008_parameter_problem() { + let mut h = H::fixture_i(); + let e = delivered_error(&mut h, &ipv4(R, A, 1, &hex(V_ICMP_PARAM_PROBLEM))); + assert_eq!(e.kind, ErrorKind::ParameterProblem { code: ParameterProblemCode::Pointer, pointer: 20 }); +} + +#[test] +fn s_ip_icd_009_an_unassigned_code() { + let mut h = H::fixture_i(); + let e = delivered_error(&mut h, &edit(hex(V_ICMP_FRAG_NEEDED), |ip| ip[21] = 16)); + let ErrorKind::Unreachable(code) = e.kind else { panic!("{:?}", e.kind) }; + assert!(matches!(code, UnreachableCode::Unassigned(c) if c.value() == 16)); +} + +#[test] +fn s_ip_icd_010_a_redirect_moves_no_route() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_R, 0x0800, &ipv4(R, A, 1, &hex(V_ICMP_REDIRECT)))); + assert_eq!(h.refusals(Counter::IcmpRedirect)[0].peer, Peer::Ip(R)); + assert_eq!(h.ip.route(REMOTE, toyos_net_ip::Source::Any, None).map(|r| r.next_hop), Ok(toyos_net_ip::NextHop::Neighbour(R))); +} + +#[test] +fn s_ip_icd_011_prop_the_shared_classification() { + for code in 0..=16u8 { + let mut h = H::fixture_i(); + let e = delivered_error(&mut h, &edit(hex(V_ICMP_FRAG_NEEDED), |ip| ip[21] = code)); + let expected = match code { + 2 | 3 => ErrorClass::Refused, + 9 | 10 | 13 => ErrorClass::Prohibited, + 4 => ErrorClass::PathMtu, + _ => ErrorClass::Soft, + }; + assert_eq!(e.kind.class(), expected, "code {code}"); + } +} + +#[test] +fn s_ip_icd_012_a_resolution_failure_is_local() { + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + h.out(); + let out = h.run(3_000); + assert_eq!(out.len(), 2, "only the two retransmitted requests"); + assert!(h.events.iter().any(|e| matches!(e, Event::Unreachable(f) if f.destination == B))); + assert!(matches!(h.state(B), Some(Nud::Failed(_)))); +} + +#[test] +fn s_icmp_023_a_redirect_is_refused() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_R, 0x0800, &ipv4(R, A, 1, &hex(V_ICMP_REDIRECT)))); + assert_eq!(h.count(Counter::IcmpRedirect), 1); +} + +#[test] +fn s_icmp_024_a_timestamp_request_is_refused() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram(&ipv4(B, A, 1, &hex(V_ICMP_TIMESTAMP_REQ))); + assert_eq!(h.count(Counter::IcmpTimestampRequest), 1); + assert_eq!(h.refusals(Counter::IcmpTimestampRequest)[0].peer, Peer::Ip(B)); + assert!(h.out().is_empty()); +} + +#[test] +fn s_icmp_026_a_timestamp_reply_is_unsupported() { + let mut h = H::fixture_i(); + let mut reply = hex(V_ICMP_TIMESTAMP_REQ); + reply[0] = 14; + let mut ip = ipv4(B, A, 1, &reply); + fix(&mut ip); + h.datagram(&ip); + assert_eq!(h.wire(IcmpError::TimestampReply.name()), 1); +} + +#[test] +fn s_icmp_032_b_answers_a() { + let mut h = H::as_b(); + h.frame(ð(MAC_B, MAC_A, 0x0800, &hex(V_ICMP_ECHO))); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(ip_bytes(&out[0].frame), hex(V_ICMP_REPLY)); +} + +#[test] +fn s_icmp_033_all_the_data_comes_back() { + let mut h = H::fixture_i_with(B, MAC_B); + let mut message = vec![8, 0, 0, 0, 0, 1, 0, 1]; + message.extend((0..1_472u32).map(|n| n as u8)); + h.datagram(&ipv4(B, A, 1, &message)); + let out = h.out(); + assert_eq!(out[0].ip().unwrap().payload()[8..], message[8..]); +} + +#[test] +fn s_icmp_034_no_answer_to_a_group() { + let mut h = H::fixture_i_with(B, MAC_B); + h.datagram_to(MacAddr::BROADCAST, &edit(echo_to_a(), |ip| set_destination(ip, ip4(255, 255, 255, 255)))); + h.datagram_to(MacAddr([1, 0, 0x5e, 0, 0, 1]), &edit(echo_to_a(), |ip| set_destination(ip, ip4(224, 0, 0, 1)))); + assert_eq!(h.count(Counter::IcmpEchoToGroup), 2); + assert!(h.out().is_empty()); +} + +#[test] +fn s_icmp_035_record_route_is_not_returned() { + let mut h = H::fixture_i_with(B, MAC_B); + let mut request = vec![0x47, 0, 0, 0, 0, 0, 0x40, 0, 64, 1, 0, 0, 192, 0, 2, 2, 192, 0, 2, 1, 7, 7, 4, 0, 0, 0, 0, 0]; + request.extend_from_slice(&hex(V_ICMP_ECHO)[20..]); + let total = request.len() as u16; + request[2..4].copy_from_slice(&total.to_be_bytes()); + fix(&mut request); + h.datagram(&request); + assert!(h.out()[0].ip().unwrap().options().bytes().is_empty()); +} + +#[test] +fn s_icmp_036_a_quote_from_someone_else() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &edit(hex(V_ICMP_PORT_UNREACH), |ip| ip[40..44].copy_from_slice(&[192, 0, 2, 9])))); + assert_eq!(h.count(Counter::IcmpQuoteNotOurs), 1); +} + +#[test] +fn s_icmp_037_a_quote_of_a_later_fragment() { + let mut h = H::fixture_i(); + h.frame(ð(MAC_A, MAC_DNS, 0x0800, &edit(hex(V_ICMP_PORT_UNREACH), |ip| ip[34..36].copy_from_slice(&[0, 185])))); + assert_eq!(h.count(Counter::IcmpQuoteNonInitialFragment), 1); +} + +#[test] +fn s_icmp_038_each_error_goes_to_its_transport() { + let mut h = H::fixture_i(); + let udp = delivered_error(&mut h, &hex(V_ICMP_PORT_UNREACH)); + assert_eq!((udp.transport, udp.flow.source_port.get()), (Transport::Udp, 49152)); + let tcp = delivered_error(&mut h, &hex(V_ICMP_FRAG_NEEDED)); + assert_eq!((tcp.transport, tcp.flow.source_port.get(), tcp.flow.destination, tcp.flow.destination_port.get()), (Transport::Tcp, 49153, REMOTE, 443)); + assert!(matches!(tcp.kind, ErrorKind::FragmentationNeeded { next_hop_mtu: Some(m), .. } if m.get() == 1_400)); +} + +#[test] +fn s_icmp_039_the_generated_port_unreachable() { + let mut h = H::fixture_i_with(B, MAC_B); + assert_eq!(ip_bytes(&closed_port(&mut h, &hex(V_UDP_HI))[0].frame), hex(V_ICMP_PORT_UNREACH_GEN)); +} + +#[test] +fn s_icmp_040_576_bytes_at_most() { + let mut h = H::fixture_i_with(B, MAC_B); + let big = udp(B, A, 5000, 5001, &[7; 972]); + let out = closed_port(&mut h, &big); + let ip = out[0].ip().unwrap(); + assert_eq!(ip.total_length(), 576); + assert_eq!(ip.payload()[8..], big[..548]); +} + +#[test] +fn s_icmp_041_the_quote_is_as_received() { + let mut h = H::fixture_i_with(B, MAC_B); + let received = edit(hex(V_IP_RR), |ip| ip[8] = 3); + let out = closed_port(&mut h, &received); + assert_eq!(out[0].ip().unwrap().payload()[8..], received[..]); +} + +#[test] +fn s_icmp_042_an_error_for_no_socket_is_not_answered() { + let mut h = H::fixture_i(); + assert!(matches!(h.frame(ð(MAC_A, MAC_DNS, 0x0800, &hex(V_ICMP_PORT_UNREACH))), Some(Delivery::Error(_)))); + assert!(h.out().is_empty()); +} + +#[test] +fn s_icmp_043_no_error_about_a_broadcast() { + let mut h = H::fixture_i_with(B, MAC_B); + let broadcast = edit(hex(V_UDP_HI), |ip| set_destination(ip, ip4(255, 255, 255, 255))); + let Some(Delivery::Udp(arrival, _)) = h.datagram_to(MacAddr::BROADCAST, &broadcast) else { panic!("not delivered") }; + h.ip.port_unreachable(h.clock(), &arrival); + assert!(h.out().is_empty()); + assert_eq!(h.count(Counter::IcmpErrorSuppressed), 1); +} + +#[test] +fn s_icmp_044_no_error_about_a_link_broadcast() { + let mut h = H::fixture_i_with(B, MAC_B); + assert!(h.datagram_to(MacAddr::BROADCAST, &hex(V_UDP_HI)).is_none()); + assert!(h.out().is_empty()); +} + +#[test] +fn s_icmp_045_no_error_about_a_fragment() { + let mut h = H::fixture_i_with(B, MAC_B); + assert!(h.datagram(&hex(V_IP_FRAG_LAST)).is_none()); + assert!(h.out().is_empty()); +} + +#[test] +fn s_icmp_046_no_error_to_a_source_that_is_no_host() { + let mut h = H::fixture_i_with(B, MAC_B); + for source in [ip4(0, 0, 0, 0), ip4(127, 0, 0, 1), ip4(224, 0, 0, 5), ip4(240, 0, 0, 1)] { + assert!(closed_port(&mut h, &hi_from(source)).is_empty()); + } + assert_eq!(h.count(Counter::IpInvalidSource), 4); +} diff --git a/toyos-net-ip/tests/igmp.rs b/toyos-net-ip/tests/igmp.rs new file mode 100644 index 00000000000..7e7b49263f2 --- /dev/null +++ b/toyos-net-ip/tests/igmp.rs @@ -0,0 +1,536 @@ +//! §10: the IGMP host, plus fragments at stage 5 (§11.1) and refusal visibility (§13). + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{Counter, IgmpMode, Peer, RefusalLog}; +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::ipv4::MulticastAddr; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +fn group_mac(g: Ipv4Addr) -> MacAddr { + MacAddr::multicast(group(g)) +} + +/// One IGMPv3 record: type, group, sources. +type Record = (u8, Ipv4Addr, Vec); + +/// An IGMP message A sent, decoded by the test itself. +#[derive(Debug, PartialEq)] +enum Sent { + V3(Vec), + V2Report(Ipv4Addr), + V2Leave(Ipv4Addr), +} + +fn igmp(out: &Out) -> Option { + let ip = out.ip()?; + if ip.protocol() != toyos_net_wire::ipv4::Protocol::Igmp { + return None; + } + assert_eq!(oracle_sum(ip.payload()), 0xffff, "IGMP checksum"); + let p = ip.payload(); + let g = |at: usize| Ipv4Addr::new(p[at], p[at + 1], p[at + 2], p[at + 3]); + Some(match p[0] { + 0x22 => { + let n = usize::from(u16::from_be_bytes([p[6], p[7]])); + let mut at = 8; + let mut records = Vec::new(); + for _ in 0..n { + let sources = usize::from(u16::from_be_bytes([p[at + 2], p[at + 3]])); + let list = (0..sources).map(|s| g(at + 8 + 4 * s)).collect(); + records.push((p[at], g(at + 4), list)); + at += 8 + 4 * sources; + } + Sent::V3(records) + } + 0x16 => Sent::V2Report(g(4)), + 0x17 => Sent::V2Leave(g(4)), + other => panic!("type {other:#x}"), + }) +} + +fn reports(out: &[Out]) -> Vec<(u64, Sent)> { + out.iter().filter_map(|o| igmp(o).map(|s| (o.at, s))).collect() +} + +fn to_ex(g: Ipv4Addr) -> Sent { + Sent::V3(vec![(4, g, vec![])]) +} + +fn to_in(g: Ipv4Addr) -> Sent { + Sent::V3(vec![(3, g, vec![])]) +} + +fn is_ex(g: Ipv4Addr) -> Sent { + Sent::V3(vec![(2, g, vec![])]) +} + +/// Fixture I without 224.0.0.251 joined. +fn unjoined() -> H { + let mut h = H::raw(); + let if0 = h.if0; + h.assign(if0, A, 24, 0); + h.ip.set_gateways(toyos_net_ip::Instant::from_millis(3_000), if0, &[R]).unwrap(); + h.settle(); + h +} + +/// A query from R to `destination`, framed to its MAC. +fn query(h: &mut H, destination_mac: MacAddr, ip: &[u8]) { + h.frame(ð(destination_mac, MAC_R, 0x0800, ip)); +} + +fn general_v3(h: &mut H) { + query(h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP3_QUERY_GEN)); +} + +/// A query datagram from R: `igmp` inside an IPv4 header with Router Alert, TTL 1, TOS 0xC0. +fn query_datagram(destination: Ipv4Addr, igmp: &[u8]) -> Vec { + let mut ip = vec![0x46, 0xc0, 0, 0, 0, 0, 0x40, 0, 1, 2, 0, 0, 192, 0, 2, 254]; + ip.extend_from_slice(&destination.octets()); + ip.extend_from_slice(&[0x94, 4, 0, 0]); + ip.extend_from_slice(igmp); + let total = ip.len() as u16; + ip[2..4].copy_from_slice(&total.to_be_bytes()); + fix(&mut ip); + ip +} + +fn v3_query(max_code: u8, group: Ipv4Addr, qrv: u8, sources: &[Ipv4Addr]) -> Vec { + let mut q = vec![0x11, max_code, 0, 0]; + q.extend_from_slice(&group.octets()); + q.extend_from_slice(&[qrv, 125]); + q.extend_from_slice(&(sources.len() as u16).to_be_bytes()); + for s in sources { + q.extend_from_slice(&s.octets()); + } + q +} + +fn v2_query(max_code: u8, group: Ipv4Addr) -> Vec { + let mut q = vec![0x11, max_code, 0, 0]; + q.extend_from_slice(&group.octets()); + q +} + +#[test] +fn s_ip_igm_001_a_join_is_reported_twice() { + let mut h = unjoined(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + let sent = reports(&h.run(10_000)); + assert_eq!(sent.len(), 2); + assert_eq!(sent[0], (0, to_ex(MDNS))); + assert_eq!(sent[1].1, to_ex(MDNS)); + assert!(sent[1].0 > 0 && sent[1].0 <= 1_000); +} + +#[test] +fn s_ip_igm_002_joins_before_and_after_the_first_address() { + let mut h = H::bare(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + let before = h.run(2_000); + let unspecified: Vec> = before.iter().filter(|o| igmp(o).is_some()).map(|o| ip_bytes(&o.frame)).collect(); + assert_eq!(unspecified, [hex(V_IGMP3_JOIN_UNSPEC), hex(V_IGMP3_JOIN_UNSPEC)]); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + let after = h.run(3_000); + let joins: Vec<(u64, Vec)> = after.iter().filter(|o| igmp(o).is_some()).map(|o| (o.at, o.frame.clone())).collect(); + assert_eq!(joins.len(), 2); + assert!(joins.iter().all(|(_, f)| *f == hex(V_IGMP3_JOIN))); + assert!(joins[1].0 > joins[0].0 && joins[1].0 - joins[0].0 <= 1_000); +} + +#[test] +fn s_ip_igm_003_a_leave_is_reported_twice() { + let mut h = H::fixture_i(); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + let sent = reports(&h.run(10_000)); + assert_eq!(sent.len(), 2); + assert_eq!(sent[0], (0, to_in(MDNS))); + assert_eq!(sent[1].1, to_in(MDNS)); + assert!(sent[1].0 > 0 && sent[1].0 <= 1_000); +} + +#[test] +fn s_ip_igm_004_a_leave_replaces_the_join() { + let mut h = unjoined(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + let mut sent = reports(&h.out()); + h.at(10); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + sent.extend(reports(&h.run(10_000))); + assert_eq!(sent[0], (0, to_ex(MDNS))); + assert_eq!(sent[1], (10, to_in(MDNS))); + assert_eq!(sent.len(), 3); + assert_eq!(sent[2].1, to_in(MDNS)); + assert!(sent[2].0 > 10 && sent[2].0 <= 1_010); +} + +#[test] +fn s_ip_igm_005_membership_is_counted() { + let mut h = unjoined(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + assert_eq!(reports(&h.run(5_000)).len(), 2, "one join, sent twice"); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + assert!(reports(&h.run(10_000)).is_empty()); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + assert_eq!(reports(&h.run(15_000))[0].1, to_in(MDNS)); +} + +#[test] +fn s_ip_igm_006_all_hosts_is_never_reported() { + let mut h = H::fixture_i(); + h.ip.join(h.clock(), h.if0, group(ip4(224, 0, 0, 1))).unwrap(); + assert!(h.out().is_empty()); + general_v3(&mut h); + let sent = reports(&h.run(10_000)); + assert_eq!(sent.len(), 1); + assert_eq!(sent[0].1, is_ex(MDNS)); +} + +#[test] +fn s_ip_igm_007_a_general_query_is_answered_later() { + let mut h = H::fixture_i(); + general_v3(&mut h); + assert!(h.out().is_empty(), "never at once"); + let out = h.run(10_000); + let current: Vec<&Out> = out.iter().filter(|o| igmp(o).is_some()).collect(); + assert_eq!(current.len(), 1); + assert_eq!(ip_bytes(¤t[0].frame), hex(V_IGMP3_CURRENT)); + assert!(current[0].at > 0 && current[0].at <= 10_000); +} + +#[test] +fn s_ip_igm_008_a_sooner_general_response_suffices() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &query_datagram(ip4(224, 0, 0, 1), &v3_query(10, ip4(0, 0, 0, 0), 2, &[]))); + h.at(100); + general_v3(&mut h); + let sent = reports(&h.run(20_000)); + assert_eq!(sent.len(), 1); + assert!(sent[0].0 <= 1_000); +} + +#[test] +fn s_ip_igm_009_a_group_query() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(MDNS), &hex(V_IGMP3_QUERY_GROUP)); + let sent = reports(&h.run(5_000)); + assert_eq!(sent.len(), 1); + assert_eq!(sent[0].1, is_ex(MDNS)); + assert!(sent[0].0 > 0 && sent[0].0 <= 1_000); +} + +#[test] +fn s_ip_igm_010_a_query_for_a_group_not_joined() { + let mut h = H::fixture_i(); + query(&mut h, MAC_A, &query_datagram(A, &v3_query(10, ip4(224, 0, 0, 252), 2, &[]))); + assert_eq!(h.count(Counter::IgmpQueryOtherGroup), 1); + assert!(h.run(5_000).is_empty()); +} + +#[test] +fn s_ip_igm_011_a_group_and_source_query() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(MDNS), &query_datagram(MDNS, &hex(V_IGMP_QUERY_V3_SRC))); + let sent = reports(&h.run(10_000)); + assert_eq!(sent.len(), 1); + assert_eq!(sent[0].1, Sent::V3(vec![(1, MDNS, vec![ip4(192, 0, 2, 9), ip4(192, 0, 2, 10)])])); +} + +#[test] +fn s_ip_igm_012_too_many_sources_make_a_group_response() { + let mut h = H::fixture_i(); + let sources: Vec = (0..70u32).map(|n| Ipv4Addr::from(u32::from(ip4(198, 51, 100, 0)) + n)).collect(); + query(&mut h, group_mac(MDNS), &query_datagram(MDNS, &v3_query(100, MDNS, 2, &sources))); + assert_eq!(reports(&h.run(10_000)).into_iter().map(|(_, s)| s).collect::>(), [is_ex(MDNS)]); +} + +#[test] +fn s_ip_igm_013_queries_need_router_alert() { + let mut h = H::fixture_i(); + general_v3(&mut h); + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP3_QUERY_NORA)); + let mut v2 = hex(V_IGMP_QUERY_V2); + v2.drain(20..24); + v2[0] = 0x45; + v2[3] = 28; + fix(&mut v2); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &v2); + assert_eq!(h.count(Counter::IgmpQueryNoRouterAlert), 2); + assert_eq!(h.refusals(Counter::IgmpQueryNoRouterAlert)[0].peer, Peer::Ip(R)); + assert!(h.run(20_000).is_empty()); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V3)); +} + +#[test] +fn s_ip_igm_014_general_query_destinations() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(MDNS), &edit(hex(V_IGMP3_QUERY_GEN), |ip| set_destination(ip, MDNS))); + assert_eq!(h.refusals(Counter::IgmpGeneralQueryDestination).len(), 1); + assert!(h.run(20_000).is_empty()); + query(&mut h, MAC_A, &edit(hex(V_IGMP3_QUERY_GEN), |ip| set_destination(ip, A))); + assert_eq!(reports(&h.run(40_000)).len(), 1); +} + +#[test] +fn s_ip_igm_015_a_group_query_to_another_group() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &edit(hex(V_IGMP3_QUERY_GROUP), |ip| set_destination(ip, ip4(224, 0, 0, 1)))); + h.ip.join(h.clock(), h.if0, group(ip4(224, 0, 0, 252))).unwrap(); + h.run(5_000); + query(&mut h, group_mac(ip4(224, 0, 0, 252)), &edit(hex(V_IGMP3_QUERY_GROUP), |ip| set_destination(ip, ip4(224, 0, 0, 252)))); + assert_eq!(h.count(Counter::IgmpQueryDestination), 2); + assert!(h.run(20_000).is_empty()); +} + +#[test] +fn s_ip_igm_016_the_robustness_is_learned() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &query_datagram(ip4(224, 0, 0, 1), &v3_query(10, ip4(0, 0, 0, 0), 3, &[]))); + h.run(5_000); + h.ip.join(h.clock(), h.if0, group(ip4(239, 1, 2, 3))).unwrap(); + let joins = reports(&h.run(10_000)).into_iter().filter(|(_, s)| *s == to_ex(ip4(239, 1, 2, 3))).count(); + assert_eq!(joins, 3); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &query_datagram(ip4(224, 0, 0, 1), &v3_query(10, ip4(0, 0, 0, 0), 0, &[]))); + h.run(15_000); + h.ip.join(h.clock(), h.if0, group(ip4(239, 1, 2, 4))).unwrap(); + let joins = reports(&h.run(20_000)).into_iter().filter(|(_, s)| *s == to_ex(ip4(239, 1, 2, 4))).count(); + assert_eq!(joins, 3); +} + +#[test] +fn s_ip_igm_017_a_v2_querier() { + let mut h = H::fixture_i(); + general_v3(&mut h); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP_QUERY_V2)); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V2)); + assert_eq!(h.count(Counter::IgmpModeV2), 1); + let out = h.run(10_000); + let sent = reports(&out); + assert_eq!(sent.len(), 1, "the v3 response was cancelled"); + assert_eq!(sent[0].1, Sent::V2Report(MDNS)); + let report = out.iter().find(|o| igmp(o).is_some()).unwrap(); + let ip = report.ip().unwrap(); + assert_eq!((ip.destination(), ip.ttl(), ip.traffic_class().byte()), (MDNS, 1, 0xc0)); + assert!(matches!(ip.options().iter().next(), Some(toyos_net_wire::ipv4::Ipv4Option::RouterAlert(0)))); + assert!(sent[0].0 > 0 && sent[0].0 <= 10_000); +} + +fn v2_mode() -> H { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP_QUERY_V2)); + h.run(10_000); + h.rebase(); + h +} + +#[test] +fn s_ip_igm_018_a_join_in_v2_mode() { + let mut h = v2_mode(); + let t = h.now; + h.ip.join(h.clock(), h.if0, group(ip4(224, 0, 0, 252))).unwrap(); + let sent = reports(&h.run(t + 20_000)); + assert_eq!(sent.len(), 2); + assert_eq!(sent[0], (t, Sent::V2Report(ip4(224, 0, 0, 252)))); + assert_eq!(sent[1].1, Sent::V2Report(ip4(224, 0, 0, 252))); + assert!(sent[1].0 > t && sent[1].0 <= t + 10_000); +} + +#[test] +fn s_ip_igm_019_another_hosts_report_suppresses_ours() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP_QUERY_V2)); + let mut report = hex(V_IGMP_REPORT); + report[6..12].copy_from_slice(&MAC_B.0); + let mut ip = report[14..46].to_vec(); + set_source(&mut ip, B); + fix(&mut ip); + h.frame(ð(group_mac(MDNS), MAC_B, 0x0800, &ip)); + assert!(reports(&h.run(20_000)).is_empty(), "the delay was cancelled"); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + assert!(reports(&h.run(40_000)).is_empty(), "not the last reporter: no leave"); +} + +#[test] +fn s_ip_igm_020_a_v1_query_is_refused() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP1_QUERY)); + assert_eq!(h.refusals(Counter::IgmpV1Query).len(), 1); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V3)); + assert!(h.run(20_000).is_empty()); +} + +#[test] +fn s_ip_igm_021_v2_mode_ends() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &hex(V_IGMP_QUERY_V2)); + h.run(259_999); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V2)); + h.run(260_001); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V3)); + assert_eq!(h.count(Counter::IgmpModeV3), 1); + general_v3(&mut h); + let out = h.run(280_001); + assert!(out.iter().any(|o| igmp(o).is_some() && ip_bytes(&o.frame) == hex(V_IGMP3_CURRENT))); +} + +#[test] +fn s_ip_igm_022_a_v2_group_query_in_v3_mode() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(MDNS), &query_datagram(MDNS, &v2_query(10, MDNS))); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V3)); + let sent = reports(&h.run(5_000)); + assert_eq!(sent.len(), 1); + assert_eq!(sent[0].1, is_ex(MDNS)); + assert!(sent[0].0 > 0 && sent[0].0 <= 1_000); +} + +#[test] +fn s_ip_igm_023_the_last_reporter_leaves() { + let mut h = v2_mode(); + h.ip.leave(h.clock(), h.if0, group(MDNS)).unwrap(); + let out = h.out(); + assert_eq!(reports(&out).into_iter().map(|(_, s)| s).collect::>(), [Sent::V2Leave(MDNS)]); + let ip = out[0].ip().unwrap(); + assert_eq!((ip.destination(), ip.ttl()), (ip4(224, 0, 0, 2), 1)); + assert_eq!(ip.payload(), hex(V_IGMP_LEAVE)); +} + +#[test] +fn s_ip_igm_024_other_hosts_reports_are_ignored_in_v3() { + let mut h = H::fixture_i(); + general_v3(&mut h); + let v3_report = query_datagram(ip4(224, 0, 0, 22), &[0x22, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 224, 0, 0, 251]); + h.frame(ð(group_mac(ip4(224, 0, 0, 22)), MAC_B, 0x0800, &edit(v3_report, |ip| set_source(ip, B)))); + let v2 = edit(hex(V_IGMP_REPORT)[14..46].to_vec(), |ip| set_source(ip, B)); + h.frame(ð(group_mac(MDNS), MAC_B, 0x0800, &v2)); + let v1 = edit(v2, |ip| ip[24] = 0x12); + h.frame(ð(group_mac(MDNS), MAC_B, 0x0800, &v1)); + assert_eq!(h.count(Counter::IgmpIgnored), 2); + assert_eq!(h.count(Counter::EthNotForUs), 1, "a v3 report goes to 224.0.0.22, which a host never joins"); + assert_eq!(reports(&h.run(10_000)).into_iter().map(|(_, s)| s).collect::>(), [is_ex(MDNS)]); +} + +#[test] +fn s_ip_igm_025_thirty_two_groups() { + let mut h = H::fixture_i(); + for n in 1..32u8 { + h.ip.join(h.clock(), h.if0, group(ip4(239, 0, 0, n))).unwrap(); + } + assert_eq!(h.ip.join(h.clock(), h.if0, group(ip4(239, 0, 0, 99))), Err(Counter::IgmpTooManyGroups)); +} + +#[test] +fn s_ip_igm_026_link_flap() { + let mut h = H::fixture_i(); + general_v3(&mut h); + h.ip.link_down(h.clock(), h.if0).unwrap(); + h.ip.link_up(h.clock(), h.if0).unwrap(); + let sent = reports(&h.run(20_000)); + assert_eq!(sent.len(), 2, "the pending response is gone"); + assert_eq!(sent[0], (0, to_ex(MDNS))); + assert_eq!(sent[1].1, to_ex(MDNS)); + assert!(sent[1].0 > 0 && sent[1].0 <= 1_000); +} + +#[test] +fn s_ip_igm_027_one_report_for_every_group() { + let mut h = H::fixture_i(); + for g in [ip4(239, 1, 2, 3), ip4(239, 1, 2, 4)] { + h.ip.join(h.clock(), h.if0, group(g)).unwrap(); + } + h.run(5_000); + general_v3(&mut h); + let sent = reports(&h.run(20_000)); + assert_eq!(sent.len(), 1); + let Sent::V3(records) = &sent[0].1 else { panic!() }; + let mut groups: Vec<(u8, Ipv4Addr)> = records.iter().map(|r| (r.0, r.1)).collect(); + groups.sort(); + assert_eq!(groups, [(2, MDNS), (2, ip4(239, 1, 2, 3)), (2, ip4(239, 1, 2, 4))]); +} + +#[test] +fn s_ip_igm_028_the_join_byte_for_byte() { + let mut h = unjoined(); + h.ip.join(h.clock(), h.if0, group(MDNS)).unwrap(); + assert_eq!(h.out()[0].frame, hex(V_IGMP3_JOIN)); +} + +#[test] +fn s_igmp_023_no_router_alert_is_refused() { + let mut h = H::fixture_i(); + let mut v2 = hex(V_IGMP_QUERY_V2); + v2.drain(20..24); + v2[0] = 0x45; + v2[3] = 28; + fix(&mut v2); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &v2); + assert_eq!(h.count(Counter::IgmpQueryNoRouterAlert), 1); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V3)); +} + +#[test] +fn s_igmp_024_ttl_2_is_accepted() { + let mut h = H::fixture_i(); + query(&mut h, group_mac(ip4(224, 0, 0, 1)), &edit(hex(V_IGMP_QUERY_V2), |ip| ip[8] = 2)); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V2)); +} + +#[test] +fn s_igmp_025_a_general_query_to_our_address() { + let mut h = H::fixture_i(); + query(&mut h, MAC_A, &edit(hex(V_IGMP_QUERY_V2), |ip| set_destination(ip, A))); + assert_eq!(h.ip.igmp_mode(h.if0), Some(IgmpMode::V2)); + assert_eq!(reports(&h.run(20_000)).len(), 1); +} + +#[test] +fn s_igmp_029_joining_all_hosts_sends_nothing() { + let mut h = H::fixture_i(); + h.ip.join(h.clock(), h.if0, MulticastAddr::ALL_HOSTS).unwrap(); + assert!(h.run(5_000).is_empty()); +} + +#[test] +fn s_ip_frg_001_a_fragment_is_refused_and_logged() { + let mut h = H::fixture_i_with(B, MAC_B); + assert!(h.datagram(&hex(V_FRAG_1)).is_none()); + assert_eq!(h.refusals(Counter::IpFragment)[0].peer, Peer::Ip(B)); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_mod_001_one_line_per_rule_per_10_s() { + let mut h = H::fixture_i(); + for n in 0..100u64 { + h.at(n * 10); + h.datagram(&hex(V_IP_LSRR)); + } + h.at(10_001); + h.datagram(&hex(V_IP_LSRR)); + assert_eq!(h.count(Counter::IpSourceRoute), 101); + let refusals = h.refusals(Counter::IpSourceRoute); + assert!(refusals.iter().all(|r| r.peer == Peer::Ip(B))); + let mut log = RefusalLog::default(); + let times = (0..100u64).map(|n| n * 10).chain([10_001]); + let lines: Vec = refusals.iter().zip(times).filter_map(|(r, t)| log.admit(H::instant(t), r.rule)).collect(); + assert_eq!(lines, [0, 99]); +} + +#[test] +fn s_ip_mod_002_ordinary_refusals_are_not_logged() { + let mut h = H::fixture_i(); + for _ in 0..1_000 { + h.datagram(&edit(hex(V_UDP_HI), |ip| set_destination(ip, ip4(192, 0, 2, 7)))); + } + assert_eq!(h.count(Counter::IpNotForUs), 1_000); + assert!(h.events.iter().all(|e| !matches!(e, toyos_net_ip::Event::Refused(_)))); +} diff --git a/toyos-net-ip/tests/nbr.rs b/toyos-net-ip/tests/nbr.rs new file mode 100644 index 00000000000..6f3f35ba010 --- /dev/null +++ b/toyos-net-ip/tests/nbr.rs @@ -0,0 +1,246 @@ +//! §7: ARP reception, and what a modern host accepts. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{Counter, Nud, Peer, RefusalLog}; +use toyos_net_wire::ethernet::MacAddr; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +fn request(sender_mac: MacAddr, sender: Ipv4Addr, target: Ipv4Addr) -> Vec { + eth(MacAddr::BROADCAST, sender_mac, 0x0806, &arp_packet(1, sender_mac, sender, MacAddr::ZERO, target)) +} + +fn frames(out: &[Out]) -> Vec> { + out.iter().map(|o| o.frame.clone()).collect() +} + +#[test] +fn s_ip_nbr_001_our_own_mac_as_sender() { + let mut h = H::fixture_i(); + let mut frame = hex(V_ARP_REQ_B); + frame[22..28].copy_from_slice(&MAC_A.0); + h.frame(&frame); + assert_eq!(h.count(Counter::ArpOwnSender), 1); + assert!(h.out().is_empty() && h.state(B).is_none()); +} + +#[test] +fn s_ip_nbr_002_prefix_edges_are_no_senders() { + let mut h = H::fixture_i(); + for sender in [ip4(192, 0, 2, 255), ip4(192, 0, 2, 0)] { + h.frame(&request(MAC_B, sender, A)); + } + assert_eq!(h.count(Counter::ArpInvalidSenderAddress), 2); + assert!(h.out().is_empty()); +} + +#[test] +fn s_ip_nbr_003_a_request_for_us() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_REQ_B)); + assert_eq!(frames(&h.out()), [hex(V_ARP_REPLY_A)]); + assert!(h.is_stale(B)); + assert_eq!(h.mac_of(B), Some(MAC_B)); +} + +#[test] +fn s_ip_nbr_004_a_request_for_another_host() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_REQ_OTHER)); + assert!(h.out().is_empty() && h.state(B).is_none()); + assert_eq!(h.count(Counter::ArpNotForUs), 1); +} + +#[test] +fn s_ip_nbr_005_a_known_host_asking_for_another() { + let mut h = H::fixture_i_with(B, MAC_B); + h.frame(&hex(V_ARP_REQ_OTHER)); + assert!(h.is_reachable(B)); + assert_eq!(h.mac_of(B), Some(MAC_B)); +} + +#[test] +fn s_ip_nbr_006_an_unsolicited_reply() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_REPLY)); + assert!(h.state(B).is_none()); + assert_eq!(h.count(Counter::ArpNotForUs), 1); +} + +#[test] +fn s_ip_nbr_007_an_unknown_router_moving() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_ROUTER_MOVED)); + assert!(h.state(R).is_none()); +} + +#[test] +fn s_ip_nbr_008_a_known_router_moving() { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + h.frame(&hex(V_ARP_ROUTER_MOVED)); + assert!(h.is_stale(R)); + assert_eq!(h.mac_of(R), Some(MAC_X)); + assert_eq!(h.count(Counter::ArpMacChanged), 1); + assert_eq!(h.refusals(Counter::ArpMacChanged)[0].peer, Peer::MacChange { ip: R, old: MAC_R, new: MAC_X }); +} + +#[test] +fn s_ip_nbr_009_locktime() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(500); + h.frame(&hex(V_ARP_ROUTER_MOVED)); + assert!(h.is_reachable(R)); + assert_eq!(h.mac_of(R), Some(MAC_R)); + assert_eq!(h.refusals(Counter::ArpOverrideLocked).len(), 1); + h.at(1_001); + h.frame(&hex(V_ARP_ROUTER_MOVED)); + assert!(h.is_stale(R)); + assert_eq!(h.mac_of(R), Some(MAC_X)); +} + +#[test] +fn s_ip_nbr_010_a_second_answer_is_locked_out() { + let mut h = H::fixture_i(); + let _ = h.ip.resolve(h.clock(), h.if0, R); + h.out(); + h.at(5); + h.frame(&hex(V_ARP_REPLY_R)); + assert!(h.is_reachable(R)); + h.at(10); + h.frame(&hex(V_ARP_REPLY_R_MOVED)); + assert_eq!(h.mac_of(R), Some(MAC_R)); + assert_eq!(h.count(Counter::ArpOverrideLocked), 1); +} + +#[test] +fn s_ip_nbr_011_an_unsolicited_same_mac_extends_nothing() { + let mut h = H::fixture_i_with(R, MAC_R); + h.at(20_000); + let before = (format!("{:?}", h.state(R)), h.ip.next_deadline()); + h.frame(&hex(V_ARP_REPLY_R)); + assert_eq!((format!("{:?}", h.state(R)), h.ip.next_deadline()), before); +} + +#[test] +fn s_ip_nbr_012_an_off_link_sender_is_answered_not_cached() { + let mut h = H::fixture_i(); + let asker = ip4(198, 51, 100, 9); + h.frame(&request(MAC_B, asker, A)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].arp().unwrap().target_ip, asker); + assert!(h.state(asker).is_none()); + assert_eq!(h.count(Counter::ArpSenderOffLink), 1); +} + +#[test] +fn s_ip_nbr_013_a_probe_is_answered_and_not_cached() { + let mut h = H::fixture_i(); + h.frame(&hex(V_ARP_PROBE_B)); + assert_eq!(frames(&h.out()), [hex(V_ARP_REPLY_TO_PROBE)]); + assert!(h.state(ip4(0, 0, 0, 0)).is_none()); +} + +#[test] +fn s_ip_nbr_014_a_reply_for_another_target_asserts() { + let mut h = H::fixture_i(); + h.udp_to(REMOTE).unwrap(); + h.out(); + h.frame(ð(MAC_A, MAC_R, 0x0806, &arp_packet(2, MAC_R, R, MAC_A, ip4(192, 0, 2, 7)))); + assert!(h.is_stale(R)); + assert_eq!(h.mac_of(R), Some(MAC_R)); + let out = h.out(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].to(), MAC_R); +} + +#[test] +fn s_ip_nbr_015_a_broadcast_reply_is_still_solicited() { + let mut h = H::fixture_i(); + let _ = h.ip.resolve(h.clock(), h.if0, R); + h.out(); + let mut frame = hex(V_ARP_REPLY_R); + frame[..6].copy_from_slice(&MacAddr::BROADCAST.0); + h.frame(&frame); + assert!(h.is_reachable(R)); +} + +#[test] +fn s_ip_nbr_016_the_strong_model_for_arp() { + 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(); + let arp = request(MAC_X, ip4(198, 51, 100, 9), A); + let _ = h.ip.receive(h.clock(), if1, ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp[14..])); + assert_eq!(h.ip.transmit(h.clock(), usize::MAX, |_, _| {}), 0); + assert_eq!(h.count(Counter::ArpNotForUs), 1); +} + +#[test] +fn s_ip_nbr_017_a_tentative_address_is_not_answered() { + let mut h = H::bare(); + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + h.frame(&hex(V_ARP_REQ_B)); + assert!(h.out().iter().all(|o| o.arp().is_some_and(|a| a.sender_ip.is_unspecified()))); + assert_eq!(h.count(Counter::AcdConflict), 0); +} + +#[test] +fn s_ip_nbr_018_fifty_mac_changes_one_line() { + let mut h = H::fixture_i(); + h.stale(R, MAC_R); + for n in 0..50u8 { + h.at(u64::from(n) * 20); + let m = MacAddr([2, 0, 0, 0, 1, n]); + h.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, R, MacAddr::ZERO, R))); + } + assert_eq!(h.count(Counter::ArpMacChanged), 50); + let refusals = h.refusals(Counter::ArpMacChanged); + let mut log = RefusalLog::default(); + let lines: Vec = refusals.iter().enumerate().filter_map(|(n, r)| log.admit(H::instant(n as u64 * 20), r.rule)).collect(); + assert_eq!(lines, [0]); + assert_eq!(log.admit(H::instant(10_000), Counter::ArpMacChanged), Some(49)); +} + +#[test] +fn s_arp_016_the_sender_mac_is_what_is_learned() { + let mut h = H::host(MAC_B, B, 24); + let mut frame = hex(V_ARP_REQ); + frame[6..12].copy_from_slice(&MAC_X.0); + h.frame(&frame); + assert_eq!(h.mac_of(A), Some(MAC_A)); +} + +#[test] +fn s_arp_017_a_group_sender_mac() { + let mut h = H::fixture_i(); + for sender in [MacAddr::BROADCAST, MacAddr([1, 0, 0x5e, 0, 0, 1])] { + h.frame(ð(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, sender, B, MacAddr::ZERO, A))); + } + assert_eq!(h.count(Counter::ArpGroupSenderHardware), 2); + assert!(h.out().is_empty()); +} + +#[test] +fn s_arp_018_invalid_sender_addresses() { + let mut h = H::fixture_i(); + for sender in [ip4(255, 255, 255, 255), ip4(224, 0, 0, 1), ip4(127, 0, 0, 1), ip4(240, 0, 0, 1)] { + h.frame(&request(MAC_B, sender, A)); + } + h.frame(ð(MAC_A, MAC_B, 0x0806, &arp_packet(2, MAC_B, ip4(0, 0, 0, 0), MAC_A, A))); + assert_eq!(h.count(Counter::ArpInvalidSenderAddress), 5); + assert!(h.out().is_empty()); + h.frame(&request(MAC_B, ip4(0, 0, 0, 0), ip4(192, 0, 2, 7))); + assert_eq!(h.count(Counter::ArpInvalidSenderAddress), 5, "a probe is a request from 0.0.0.0"); + assert!(h.state(ip4(0, 0, 0, 0)).is_none()); + assert!(matches!(h.state(B), None | Some(Nud::Failed(_)))); +} From 9afdb3b1ddee18ef73bc88643c43352a1233f274 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 13:12:27 +0200 Subject: [PATCH 04/20] toyos-net-udp: UDP for ToyOS's own stack Stage 3's second crate, written clean-room from Part U of the stage-3 UDP/DHCP specification and the RFCs: sockets bound to any address, one address or one peer, at most one socket per port whatever the address (U-1), RFC 6056 Algorithm 1 over 49152-65535 with one draw per bind (U-2), bounded queues both ways (16 datagrams, 64 KiB) whose transmit side refuses back to the caller, broadcast only by permission (U-4), 0.0.0.0 as a source only for the acquisition socket and only to the limited broadcast, and ICMP errors for connected sockets alone (U-5). Egress is pulled: a datagram leaves only when the transmit opportunity offers it to toyos-net-ip, which writes it into the device's frame or holds it for its next hop; a held one spends no credit, so a socket's next datagram is never behind it (US-26). Accepted datagrams of a closed socket still leave (U-9). toyos-net-ip now counts every datagram it held and dropped (nb.pending-dropped): at resolution failure, and at link-down, where a datagram resolution had released but not yet sent is dropped and its sender told too. Every Part U scenario has its test but US-57, the pipe ABI mapping, which is netd's (stage 5); UDP-17 and UDP-18 are here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- Cargo.lock | 8 + Cargo.toml | 1 + toyos-net-ip/src/config.rs | 1 - toyos-net-ip/src/counters.rs | 1 + toyos-net-ip/src/egress.rs | 25 +- toyos-net-ip/src/nud.rs | 30 +- toyos-net-ip/tests/nud.rs | 13 + toyos-net-udp/Cargo.toml | 11 + toyos-net-udp/src/lib.rs | 683 ++++++++++++++++++++++++++++++ toyos-net-udp/tests/bind.rs | 115 +++++ toyos-net-udp/tests/common/mod.rs | 434 +++++++++++++++++++ toyos-net-udp/tests/recv.rs | 453 ++++++++++++++++++++ toyos-net-udp/tests/send.rs | 289 +++++++++++++ 13 files changed, 2042 insertions(+), 22 deletions(-) create mode 100644 toyos-net-udp/Cargo.toml create mode 100644 toyos-net-udp/src/lib.rs create mode 100644 toyos-net-udp/tests/bind.rs create mode 100644 toyos-net-udp/tests/common/mod.rs create mode 100644 toyos-net-udp/tests/recv.rs create mode 100644 toyos-net-udp/tests/send.rs diff --git a/Cargo.lock b/Cargo.lock index feb58be41aa..b261e4e4585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,14 @@ dependencies = [ "toyos-net-wire", ] +[[package]] +name = "toyos-net-udp" +version = "0.1.0" +dependencies = [ + "toyos-net-ip", + "toyos-net-wire", +] + [[package]] name = "toyos-net-wire" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a3e0443aeaf..3f57fa1df60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "toyos-mixer", "toyos-net-ip", "toyos-net-tcp", + "toyos-net-udp", "toyos-net-wire", "toyos-pci", "toyos-pcid", diff --git a/toyos-net-ip/src/config.rs b/toyos-net-ip/src/config.rs index e823b7f9a6c..8319a29c78c 100644 --- a/toyos-net-ip/src/config.rs +++ b/toyos-net-ip/src/config.rs @@ -101,7 +101,6 @@ impl Ip { nud::flush(i, &mut cx); acd::link_down(i, &mut cx); igmp::link_down(i, &mut cx); - cx.control.purge(iface); route::refresh_active(i, &mut cx); } Ok(()) diff --git a/toyos-net-ip/src/counters.rs b/toyos-net-ip/src/counters.rs index c20e785e6e1..c24b6a2ea27 100644 --- a/toyos-net-ip/src/counters.rs +++ b/toyos-net-ip/src/counters.rs @@ -60,6 +60,7 @@ toyos_net_wire::counters! { ArpRepliesSent = "arp.replies-sent"; NbPendingOverflow = "nb.pending-overflow"; NbPendingFull = "nb.pending-full"; + NbPendingDropped = "nb.pending-dropped"; NbFailedRefused = "nb.failed-refused"; NbTableFull = "nb.table-full"; NbResolved = "nb.resolved"; diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs index e8fae255b23..381f51859de 100644 --- a/toyos-net-ip/src/egress.rs +++ b/toyos-net-ip/src/egress.rs @@ -43,7 +43,7 @@ pub(crate) enum Item { Announce { iface: IfIndex, addr: Ipv4Addr, owed: bool }, Igmp { iface: IfIndex, report: igmp::Report }, Frame { iface: IfIndex, frame: Vec, kind: FrameKind }, - Released { iface: IfIndex, frame: Vec, kind: FrameKind }, + Released { iface: IfIndex, held: Held }, } impl Item { @@ -88,8 +88,8 @@ impl Control { } /// A datagram resolution released, in the order it was held. - pub fn release(&mut self, iface: IfIndex, frame: Vec, kind: FrameKind) { - self.items.push_back(Item::Released { iface, frame, kind }); + pub fn release(&mut self, iface: IfIndex, held: Held) { + self.items.push_back(Item::Released { iface, held }); } fn pop(&mut self) -> Option { @@ -109,13 +109,20 @@ impl Control { } } - /// Drops everything waiting for `iface`, whose link went down. - pub fn purge(&mut self, iface: IfIndex) { + /// Drops everything waiting for `iface`, whose link went down, and hands back the datagrams + /// resolution had released. + pub fn purge(&mut self, iface: IfIndex) -> Vec { let (gone, kept): (Vec, Vec) = self.items.drain(..).partition(|item| item.iface() == iface); + self.items = kept.into(); for item in &gone { self.forget(item); } - self.items = kept.into(); + gone.into_iter() + .filter_map(|item| match item { + Item::Released { held, .. } => Some(held), + _ => None, + }) + .collect() } } @@ -232,10 +239,10 @@ impl Ip { sent(&mut cx, kind); true } - Item::Released { frame, kind, .. } => { + Item::Released { held, .. } => { i.held = i.held.saturating_sub(1); - sink(iface, &frame); - sent(&mut cx, kind); + sink(iface, &held.frame); + sent(&mut cx, held.kind); true } } diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 73be300cc84..c695ae2a3d4 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -355,16 +355,22 @@ fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { } } -/// INCOMPLETE gave up: every held datagram is dropped and its sender told (§6.5, §9.6). +/// A held datagram that will never leave: counted, and its sender told (§6.5, §9.6). +pub(crate) fn drop_held(cx: &mut Cx<'_>, held: Held) { + cx.log.count(Counter::NbPendingDropped); + if let Some(flow) = held.flow { + cx.log.event(Event::Unreachable(flow)); + } +} + +/// INCOMPLETE gave up: every held datagram is dropped. fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let now = cx.now; let Some(n) = i.neighbours.get_mut(&addr) else { return }; let Nud::Incomplete(s) = core::mem::replace(&mut n.state, Nud::Failed(Failed { since: now })) else { return }; for held in s.pending { i.held = i.held.saturating_sub(1); - if let Some(flow) = held.flow { - cx.log.event(Event::Unreachable(flow)); - } + drop_held(cx, held); } cx.log.count(Counter::NbFailed); cx.log.event(Event::Failed { iface: cx.iface, next_hop: addr }); @@ -374,12 +380,11 @@ fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { /// INCOMPLETE learned `mac`: its datagrams leave in arrival order, ahead of any sent later (§6.5). fn resolved(cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr, pending: VecDeque) { - for held in pending { - let mut frame = held.frame; - if let Some((destination, _)) = frame.split_first_chunk_mut::<6>() { + for mut held in pending { + if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { *destination = mac.0; } - cx.control.release(cx.iface, frame, held.kind); + cx.control.release(cx.iface, held); } cx.log.count(Counter::NbResolved); cx.log.event(Event::Resolved { iface: cx.iface, next_hop: addr }); @@ -482,17 +487,18 @@ pub(crate) fn advise(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, confirm } } -/// The link went down: every entry goes, and the senders of held datagrams are told (§6.10). +/// The link went down: every entry goes, and with it every datagram held here (§6.10). pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>) { for (addr, n) in core::mem::take(&mut i.neighbours) { cx.timers.cancel(timer(cx, addr)); if let Nud::Incomplete(s) = n.state { for held in s.pending { - if let Some(flow) = held.flow { - cx.log.event(Event::Unreachable(flow)); - } + drop_held(cx, held); } } } + for held in cx.control.purge(cx.iface) { + drop_held(cx, held); + } i.held = 0; } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 6c1102d3a69..bfe995dc498 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -45,6 +45,7 @@ fn s_ip_nud_002_three_requests_then_failed() { assert_eq!(out.len(), 3); assert!(matches!(h.state(B), Some(Nud::Failed(_)))); assert_eq!(h.count(Counter::NbFailed), 1); + assert_eq!(h.count(Counter::NbPendingDropped), 1); assert!(h.events.contains(&Event::Unreachable(flow_to(B)))); assert!(h.run(10_000).is_empty()); } @@ -396,8 +397,20 @@ fn s_ip_nud_023_link_down_drops_the_table() { assert!(h.state(B).is_none() && h.state(DNS).is_none()); let told = h.events.iter().filter(|e| matches!(e, Event::Unreachable(_))).count(); assert_eq!(told, 3); + assert_eq!(h.count(Counter::NbPendingDropped), 3); h.ip.link_up(h.clock(), h.if0).unwrap(); assert!(h.state(B).is_none() && h.state(DNS).is_none()); + + let mut h = H::fixture_i(); + h.udp_to(B).unwrap(); + h.out(); + h.frame(&hex(V_ARP_REPLY)); + h.ip.link_down(h.clock(), h.if0).unwrap(); + h.collect(); + assert_eq!(h.count(Counter::NbPendingDropped), 1, "released, never sent"); + assert!(h.events.contains(&Event::Unreachable(flow_to(B)))); + h.ip.link_up(h.clock(), h.if0).unwrap(); + assert!(h.out().iter().all(|o| o.ip().is_none_or(|ip| ip.protocol() != toyos_net_wire::ipv4::Protocol::Udp))); } #[test] diff --git a/toyos-net-udp/Cargo.toml b/toyos-net-udp/Cargo.toml new file mode 100644 index 00000000000..9fdc8eeaf94 --- /dev/null +++ b/toyos-net-udp/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "toyos-net-udp" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "ToyOS's UDP, pure: sockets, binding, demultiplexing, bounded queues and ICMP errors over toyos-net-ip's addressing." +publish = false + +[dependencies] +toyos-net-ip = { path = "../toyos-net-ip" } +toyos-net-wire = { path = "../toyos-net-wire" } diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs new file mode 100644 index 00000000000..d646940f04e --- /dev/null +++ b/toyos-net-udp/src/lib.rs @@ -0,0 +1,683 @@ +//! ToyOS's UDP (RFC 768, RFC 8085), pure: sockets bound to any address, one address or one peer, +//! at most one socket per port, ephemeral ports by RFC 6056 Algorithm 1, the demultiplexer, and +//! ICMP errors for connected sockets. Addressing is `toyos-net-ip`'s: a call that needs it reads +//! the [`Ip`] it is handed. The caller hands in the time, the draws, arrivals and user calls; +//! nothing here reads a clock, draws randomness or does I/O. +//! +//! **Back-pressure is a refusal.** Each socket holds at most `limits::TX_DATAGRAMS` accepted +//! datagrams: a send past it is refused and the caller keeps its datagram. A datagram leaves only +//! when [`Udp::transmit`] offers it to the device and is built then; one [ip] holds for its next +//! hop has left this crate and spends no credit, so a socket's next datagram is never behind it. +//! +//! **Refusals are values.** Every refusal is a named [`Counter`] and the [`Error`] the call +//! returns; one of legacy or insecure input is also an [`Event`] naming the socket and the peer. + +#![cfg_attr(not(test), no_std)] +#![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] + +extern crate alloc; + +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_ip::{Arrival, Cast, ErrorClass, Flow, Ip, Source, Transport, TransportError, UdpOut}; +use toyos_net_wire::ethernet::MacClass; +use toyos_net_wire::ipv4::Ttl; +use toyos_net_wire::udp::{UdpBuilder, UdpChecksum, UdpDatagram}; +use toyos_net_wire::{Instant, Port}; + +toyos_net_wire::counters! { + BindAddressNotLocal = "udp.bind-address-not-local"; + PortInUse = "udp.port-in-use"; + NoEphemeralPort = "udp.no-ephemeral-port"; + ConnectUnspecified = "udp.connect-unspecified"; + ConnectGroup = "udp.connect-group"; + ConnectPortZero = "udp.connect-port-zero"; + SendLoopback = "udp.send-loopback"; + SendToSelf = "udp.send-to-self"; + SendInvalidDestination = "udp.send-invalid-destination"; + SendPortZero = "udp.send-port-zero"; + SendUnspecifiedDestination = "udp.send-unspecified-destination", logged; + BroadcastNotPermitted = "udp.broadcast-not-permitted", logged; + ConnectedDestinationMismatch = "udp.connected-destination-mismatch"; + NotConnected = "udp.not-connected"; + NoSourceAddress = "udp.no-source-address"; + NoRoute = "udp.no-route"; + SourceAddressNotAssigned = "udp.source-address-not-assigned"; + SourceNotPermitted = "udp.source-not-permitted"; + ExceedsMtu = "udp.exceeds-mtu"; + TxQueueFull = "udp.tx-queue-full"; + Rx = "udp.rx"; + RxDelivered = "udp.rx-delivered"; + RxNoSocket = "udp.rx-no-socket"; + RxNoSocketGroup = "udp.rx-no-socket-group"; + RxQueueFull = "udp.rx-queue-full"; + RxNoChecksum = "udp.rx-no-checksum"; + RxSrcPortZero = "udp.rx-src-port-zero"; + RxTruncated = "udp.rx-truncated"; + RxDroppedOnConnect = "udp.rx-dropped-on-connect"; + RxDiscardedOnClose = "udp.rx-discarded-on-close"; + Tx = "udp.tx"; + IcmpErrorDelivered = "udp.icmp-error-delivered"; + IcmpErrorSoft = "udp.icmp-error-soft"; + IcmpErrorUnconnected = "udp.icmp-error-unconnected"; + IcmpErrorNoSocket = "udp.icmp-error-no-socket"; + EventOverflow = "udp.event-overflow"; +} + +pub mod limits { + pub const RX_DATAGRAMS: usize = 16; + pub const RX_BYTES: usize = 65_536; + pub const TX_DATAGRAMS: usize = 16; + pub const TX_BYTES: usize = 65_536; + /// IANA's dynamic range, 49152 to 65535 (RFC 6335 §6). + pub const EPHEMERAL_FIRST: u16 = 49_152; + pub const EPHEMERAL_COUNT: u16 = 16_384; + /// The interface MTU less the IPv4 and UDP headers: nothing is fragmented (DF on every datagram). + pub const MAX_PAYLOAD: usize = toyos_net_ip::MTU.saturating_sub(28); + /// Refusal events held for the shell until it drains them. + pub const EVENTS: usize = 1_024; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SocketId { + index: u32, + generation: u32, +} + +/// Where a socket is bound (§U2.1): a peer only once connected. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Binding { + Any, + Specific(Ipv4Addr), + Connected { local: Ipv4Addr, peer: Ipv4Addr, peer_port: Port }, +} + +/// An error the network reported against a connected socket: its next call returns it once +/// (§U8.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SocketError { + Refused, + Unreachable, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Error { + NoSuchSocket, + /// Refused by the rule its counter names. + Refused(Counter), + /// The socket's pending error, now cleared; the call did nothing else. + Failed(SocketError), +} + +/// A datagram `recv` delivered (§U5.6). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Received { + /// Bytes written: the payload, cut to the buffer. + pub len: usize, + pub source: Ipv4Addr, + /// Absent when the peer sent port 0: it cannot be answered. + pub source_port: Option, + pub destination: Ipv4Addr, + pub ttl: u8, + pub link_broadcast: bool, + pub at: Instant, +} + +/// What [`Udp::receive`] did with an arrival. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Verdict { + /// A socket took it, or dropped it for a full queue: the receiver exists. + Delivered, + /// No socket accepts this unicast datagram: [ip] may answer with a port unreachable. + NoSocket, + /// No socket, and nothing is owed: a group, broadcast or acquisition datagram. + Ignored, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refusal { + pub rule: Counter, + /// The socket's address, 0.0.0.0 while bound to any, and its port. + pub local: (Ipv4Addr, Port), + /// The destination the call asked for. + pub peer: (Ipv4Addr, u16), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Event { + Refused(Refusal), +} + +#[derive(Debug)] +struct Stored { + payload: Vec, + source: Ipv4Addr, + source_port: Option, + destination: Ipv4Addr, + ttl: u8, + link_broadcast: bool, + at: Instant, +} + +/// A datagram accepted for sending, with the addressing it was accepted with. +#[derive(Debug)] +struct Queued { + source: Ipv4Addr, + destination: Ipv4Addr, + port: Port, + ttl: Ttl, + payload: Vec, +} + +#[derive(Debug)] +struct Socket { + port: Port, + binding: Binding, + /// Bound to any before its first connect: a reconnect selects its address again. + from_any: bool, + rx: VecDeque, + rx_bytes: usize, + rx_full: u64, + tx: VecDeque, + tx_bytes: usize, + active: bool, + pending: Option, + ttl: Ttl, + multicast_ttl: Ttl, + broadcast: bool, + acquisition: bool, +} + +#[derive(Debug)] +struct Slot { + generation: u32, + socket: Option, +} + +#[derive(Debug, Default)] +pub struct Udp { + slots: Vec, + free: Vec, + ports: BTreeMap, + active: VecDeque, + /// Datagrams a closed socket had accepted: they still leave (§U9 (3)). + orphans: VecDeque<(Port, Queued)>, + counters: Counters, + events: Vec, +} + +/// Not a destination any datagram may name: 0.0.0.0/8 but 0.0.0.0 itself, and 240.0.0.0/4 but the +/// limited broadcast. +fn invalid(addr: Ipv4Addr) -> bool { + let [first, ..] = addr.octets(); + (first == 0 && !addr.is_unspecified()) || (first >= 240 && !addr.is_broadcast()) +} + +fn route_refusal(refusal: toyos_net_ip::Counter) -> Counter { + match refusal { + toyos_net_ip::Counter::RouteNoSourceAddress => Counter::NoSourceAddress, + _ => Counter::NoRoute, + } +} + +impl Udp { + pub fn new() -> Self { + Self::default() + } + + pub fn counters(&self) -> &Counters { + &self.counters + } + + /// Refusals to log since the last call. + pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> { + self.events.drain(..) + } + + pub fn socket_count(&self) -> usize { + self.ports.len() + } + + fn count(&mut self, counter: Counter) { + self.counters.add(counter, 1); + } + + fn refuse(&mut self, rule: Counter, local: (Ipv4Addr, Port), peer: (Ipv4Addr, u16)) -> Result { + self.count(rule); + if rule.logged() { + if self.events.len() >= limits::EVENTS { + self.count(Counter::EventOverflow); + } else { + self.events.push(Event::Refused(Refusal { rule, local, peer })); + } + } + Err(Error::Refused(rule)) + } + + fn socket(&mut self, id: SocketId) -> Result<&mut Socket, Error> { + let slot = usize::try_from(id.index).ok().and_then(|i| self.slots.get_mut(i)).ok_or(Error::NoSuchSocket)?; + slot.socket.as_mut().filter(|_| slot.generation == id.generation).ok_or(Error::NoSuchSocket) + } + + fn at(&mut self, index: u32) -> Option<&mut Socket> { + self.slots.get_mut(usize::try_from(index).ok()?)?.socket.as_mut() + } + + /// RFC 6056 Algorithm 1 (§U2.4): the first candidate is uniform over the range, then each + /// next port in turn, until a free one or every port was tried. + fn ephemeral(&self, draw: u32) -> Option { + let offset = u16::try_from(draw.checked_rem(u32::from(limits::EPHEMERAL_COUNT)).unwrap_or(0)).unwrap_or(0); + (0..limits::EPHEMERAL_COUNT) + .map(|n| limits::EPHEMERAL_FIRST.wrapping_add(offset.wrapping_add(n).checked_rem(limits::EPHEMERAL_COUNT).unwrap_or(0))) + .filter_map(Port::new) + .find(|p| !self.ports.contains_key(p)) + } + + /// Binds a socket to `addr`, 0.0.0.0 meaning any, and `port`, or an ephemeral one when + /// `None`, which spends the one draw (§U2). + pub fn bind(&mut self, ip: &Ip, addr: Ipv4Addr, port: Option, draw: impl FnOnce() -> u32) -> Result { + let binding = if addr.is_unspecified() { + Binding::Any + } else if ip.is_assigned(addr) { + Binding::Specific(addr) + } else { + self.count(Counter::BindAddressNotLocal); + return Err(Error::Refused(Counter::BindAddressNotLocal)); + }; + let port = match port { + Some(port) if self.ports.contains_key(&port) => { + self.count(Counter::PortInUse); + return Err(Error::Refused(Counter::PortInUse)); + } + Some(port) => port, + None => match self.ephemeral(draw()) { + Some(port) => port, + None => { + self.count(Counter::NoEphemeralPort); + return Err(Error::Refused(Counter::NoEphemeralPort)); + } + }, + }; + let socket = Socket { + port, + binding, + from_any: binding == Binding::Any, + rx: VecDeque::new(), + rx_bytes: 0, + rx_full: 0, + tx: VecDeque::new(), + tx_bytes: 0, + active: false, + pending: None, + ttl: Ttl::DEFAULT, + multicast_ttl: Ttl::LINK, + broadcast: false, + acquisition: false, + }; + let index = match self.free.pop() { + Some(index) => index, + None => { + self.slots.push(Slot { generation: 0, socket: None }); + u32::try_from(self.slots.len().saturating_sub(1)).unwrap_or(u32::MAX) + } + }; + let slot = self.slots.get_mut(usize::try_from(index).unwrap_or(usize::MAX)).ok_or(Error::NoSuchSocket)?; + slot.socket = Some(socket); + let generation = slot.generation; + self.ports.insert(port, index); + Ok(SocketId { index, generation }) + } + + pub fn binding(&mut self, id: SocketId) -> Result<(Binding, Port), Error> { + self.socket(id).map(|s| (s.binding, s.port)) + } + + /// Datagrams this socket's full queue dropped. + pub fn dropped(&mut self, id: SocketId) -> Result { + self.socket(id).map(|s| s.rx_full) + } + + /// Only netd's own sockets: POSIX's `SO_BROADCAST` (§U4.3). + pub fn set_broadcast(&mut self, id: SocketId, permitted: bool) -> Result<(), Error> { + self.socket(id).map(|s| s.broadcast = permitted) + } + + pub fn set_ttl(&mut self, id: SocketId, unicast: Ttl, multicast: Ttl) -> Result<(), Error> { + self.socket(id).map(|s| { + s.ttl = unicast; + s.multicast_ttl = multicast; + }) + } + + /// Marks netd's DHCP client's socket: the only one that may send from 0.0.0.0, and the only + /// one the acquisition exception delivers to (§U4.4 (3), §U5.3). + pub fn set_acquisition(&mut self, id: SocketId) -> Result<(), Error> { + self.socket(id).map(|s| s.acquisition = true) + } + + /// The destination classes of §U4.2 and §U3 (2) that no send or connect may name. + fn unusable(ip: &Ip, addr: Ipv4Addr) -> Option { + if addr.is_loopback() { + Some(Counter::SendLoopback) + } else if ip.is_local(addr) { + Some(Counter::SendToSelf) + } else if invalid(addr) { + Some(Counter::SendInvalidDestination) + } else { + None + } + } + + /// Connects to one unicast peer; the local address is fixed now, and datagrams already + /// queued from anyone else are dropped (§U3). + pub fn connect(&mut self, ip: &mut Ip, id: SocketId, peer: Ipv4Addr, peer_port: u16) -> Result<(), Error> { + let socket = self.socket(id)?; + let (binding, from_any, local_port) = (socket.binding, socket.from_any, socket.port); + let local = match binding { + Binding::Specific(a) | Binding::Connected { local: a, .. } => a, + Binding::Any => Ipv4Addr::UNSPECIFIED, + }; + let refusal = if peer.is_unspecified() { + Some(Counter::ConnectUnspecified) + } else if peer.is_broadcast() || peer.is_multicast() || ip.is_directed_broadcast(peer) { + Some(Counter::ConnectGroup) + } else { + Self::unusable(ip, peer) + }; + let refusal = refusal.or((peer_port == 0).then_some(Counter::ConnectPortZero)); + if let Some(rule) = refusal { + return self.refuse(rule, (local, local_port), (peer, peer_port)); + } + let Some(peer_port) = Port::new(peer_port) else { return Err(Error::Refused(Counter::ConnectPortZero)) }; + let source = if from_any { Source::Any } else { Source::Bound(local) }; + let local = match ip.route(peer, source, None) { + Ok(route) => route.source, + Err(refusal) => return self.refuse(route_refusal(refusal), (local, local_port), (peer, peer_port.get())), + }; + let socket = self.socket(id)?; + socket.binding = Binding::Connected { local, peer, peer_port }; + let before = socket.rx.len(); + socket.rx.retain(|d| d.source == peer && d.source_port == Some(peer_port)); + socket.rx_bytes = socket.rx.iter().map(|d| d.payload.len()).sum(); + let dropped = before.saturating_sub(socket.rx.len()); + self.counters.add(Counter::RxDroppedOnConnect, u64::try_from(dropped).unwrap_or(u64::MAX)); + Ok(()) + } + + /// Queues `payload` for `destination:port` (§U4). Accepted means queued, not sent. + pub fn send_to(&mut self, ip: &mut Ip, id: SocketId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Error> { + self.submit(ip, id, None, destination, port, payload) + } + + /// Queues `payload` for the connected peer. + pub fn send(&mut self, ip: &mut Ip, id: SocketId, payload: &[u8]) -> Result<(), Error> { + let socket = self.socket(id)?; + match socket.binding { + Binding::Connected { peer, peer_port, .. } => self.submit(ip, id, None, peer, peer_port.get(), payload), + Binding::Any | Binding::Specific(_) => { + self.count(Counter::NotConnected); + Err(Error::Refused(Counter::NotConnected)) + } + } + } + + /// The acquisition socket's send, naming its source: 0.0.0.0 to the limited broadcast, or an + /// assigned address (§U4.4 (3)). + pub fn send_from(&mut self, ip: &mut Ip, id: SocketId, source: Ipv4Addr, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Error> { + self.submit(ip, id, Some(source), destination, port, payload) + } + + fn submit(&mut self, ip: &mut Ip, id: SocketId, named: Option, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Error> { + let socket = self.socket(id)?; + if let Some(error) = socket.pending.take() { + return Err(Error::Failed(error)); + } + let (binding, local_port, broadcast_permitted, acquisition) = (socket.binding, socket.port, socket.broadcast, socket.acquisition); + let (ttl, multicast_ttl) = (socket.ttl, socket.multicast_ttl); + let local = match binding { + Binding::Specific(a) | Binding::Connected { local: a, .. } => a, + Binding::Any => Ipv4Addr::UNSPECIFIED, + }; + let me = (local, local_port); + let peer = (destination, port); + if let Binding::Connected { peer: p, peer_port: pp, .. } = binding { + if (p, pp.get()) != peer { + return self.refuse(Counter::ConnectedDestinationMismatch, me, peer); + } + } + let broadcast = destination.is_broadcast() || ip.is_directed_broadcast(destination); + let refusal = if port == 0 { + Some(Counter::SendPortZero) + } else if destination.is_unspecified() { + Some(Counter::SendUnspecifiedDestination) + } else if let Some(rule) = Self::unusable(ip, destination) { + Some(rule) + } else if broadcast && !broadcast_permitted { + Some(Counter::BroadcastNotPermitted) + } else if payload.len() > limits::MAX_PAYLOAD { + Some(Counter::ExceedsMtu) + } else { + None + }; + if let Some(rule) = refusal { + return self.refuse(rule, me, peer); + } + let (source, lookup) = match (named, binding) { + (Some(_), _) if !acquisition => return self.refuse(Counter::SourceNotPermitted, me, peer), + (Some(named), _) if named.is_unspecified() && !destination.is_broadcast() => { + return self.refuse(Counter::SourceNotPermitted, me, peer); + } + (Some(named), _) if named.is_unspecified() => (named, Source::Unspecified), + (Some(named), _) | (None, Binding::Specific(named) | Binding::Connected { local: named, .. }) => { + if !ip.is_assigned(named) { + return self.refuse(Counter::SourceAddressNotAssigned, me, peer); + } + (named, Source::Bound(named)) + } + (None, Binding::Any) => (Ipv4Addr::UNSPECIFIED, Source::Any), + }; + let source = match ip.route(destination, lookup, None) { + Ok(route) if lookup == Source::Any => route.source, + Ok(_) => source, + Err(refusal) => return self.refuse(route_refusal(refusal), me, peer), + }; + let socket = self.socket(id)?; + let bytes = socket.tx_bytes.saturating_add(payload.len()); + if socket.tx.len() >= limits::TX_DATAGRAMS || bytes > limits::TX_BYTES { + return self.refuse(Counter::TxQueueFull, me, peer); + } + let Some(port) = Port::new(port) else { return Err(Error::Refused(Counter::SendPortZero)) }; + let ttl = if destination.is_multicast() { multicast_ttl } else { ttl }; + socket.tx.push_back(Queued { source, destination, port, ttl, payload: payload.to_vec() }); + socket.tx_bytes = bytes; + if !core::mem::replace(&mut socket.active, true) { + self.active.push_back(id.index); + } + Ok(()) + } + + /// The oldest datagram, cut to `out` (the rest discarded, U-7), or `None` when none waits. + pub fn recv(&mut self, id: SocketId, out: &mut [u8]) -> Result, Error> { + let socket = self.socket(id)?; + if let Some(error) = socket.pending.take() { + return Err(Error::Failed(error)); + } + let Some(d) = socket.rx.pop_front() else { return Ok(None) }; + socket.rx_bytes = socket.rx_bytes.saturating_sub(d.payload.len()); + let len = out.len().min(d.payload.len()); + if let (Some(to), Some(from)) = (out.get_mut(..len), d.payload.get(..len)) { + to.copy_from_slice(from); + } + if len < d.payload.len() { + self.count(Counter::RxTruncated); + } + Ok(Some(Received { + len, + source: d.source, + source_port: d.source_port, + destination: d.destination, + ttl: d.ttl, + link_broadcast: d.link_broadcast, + at: d.at, + })) + } + + /// Releases the port at once: received datagrams are discarded, accepted ones still leave. + pub fn close(&mut self, id: SocketId) -> Result<(), Error> { + self.socket(id)?; + let Some(slot) = usize::try_from(id.index).ok().and_then(|i| self.slots.get_mut(i)) else { return Err(Error::NoSuchSocket) }; + let Some(socket) = slot.socket.take() else { return Err(Error::NoSuchSocket) }; + slot.generation = slot.generation.wrapping_add(1); + self.free.push(id.index); + self.ports.remove(&socket.port); + self.counters.add(Counter::RxDiscardedOnClose, u64::try_from(socket.rx.len()).unwrap_or(u64::MAX)); + self.orphans.extend(socket.tx.into_iter().map(|q| (socket.port, q))); + Ok(()) + } + + /// A datagram [ip] admitted (§U5). + pub fn receive(&mut self, now: Instant, arrival: &Arrival<'_>, datagram: &UdpDatagram<'_>) -> Verdict { + self.count(Counter::Rx); + if datagram.checksum() == UdpChecksum::Absent { + self.count(Counter::RxNoChecksum); + } + if datagram.source_port().is_none() { + self.count(Counter::RxSrcPortZero); + } + let (source, destination) = (arrival.packet.source(), arrival.packet.destination()); + let source_port = datagram.source_port(); + let accepts = |s: &Socket| match (arrival.cast, s.binding) { + (Cast::Acquisition, _) => s.acquisition, + (Cast::Unicast, Binding::Any) => true, + (Cast::Unicast, Binding::Specific(local)) => destination == local, + (Cast::Unicast, Binding::Connected { local, peer, peer_port }) => destination == local && source == peer && source_port == Some(peer_port), + (Cast::LimitedBroadcast | Cast::SubnetBroadcast | Cast::Multicast(_), binding) => binding == Binding::Any, + }; + let index = self.ports.get(&datagram.destination_port()).copied(); + let Some(socket) = index.and_then(|i| self.at(i)).filter(|s| accepts(s)) else { + return match arrival.cast { + Cast::Unicast => { + self.count(Counter::RxNoSocket); + Verdict::NoSocket + } + Cast::Acquisition => { + self.count(Counter::RxNoSocket); + Verdict::Ignored + } + Cast::LimitedBroadcast | Cast::SubnetBroadcast | Cast::Multicast(_) => { + self.count(Counter::RxNoSocketGroup); + Verdict::Ignored + } + }; + }; + let payload = datagram.payload(); + let bytes = socket.rx_bytes.saturating_add(payload.len()); + if socket.rx.len() >= limits::RX_DATAGRAMS || bytes > limits::RX_BYTES { + socket.rx_full = socket.rx_full.saturating_add(1); + self.count(Counter::RxQueueFull); + return Verdict::Delivered; + } + socket.rx_bytes = bytes; + socket.rx.push_back(Stored { + payload: payload.to_vec(), + source, + source_port, + destination, + ttl: arrival.packet.ttl(), + link_broadcast: arrival.link == MacClass::Broadcast, + at: now, + }); + self.count(Counter::RxDelivered); + Verdict::Delivered + } + + fn connected(&mut self, flow: &Flow) -> Option<&mut Socket> { + let index = *self.ports.get(&flow.source_port)?; + self.at(index).filter(|s| { + s.binding == Binding::Connected { local: flow.source, peer: flow.destination, peer_port: flow.destination_port } + }) + } + + /// An ICMP error [ip] attributed to a UDP datagram of ours (§U8): only the connected socket + /// whose whole 4-tuple it quotes hears it. + pub fn icmp_error(&mut self, error: &TransportError) { + if error.transport != Transport::Udp { + return; + } + let flow = error.flow; + let class = error.kind.class(); + let holder = self.ports.get(&flow.source_port).copied().and_then(|i| self.at(i)).map(|s| s.binding); + let Some(socket) = self.connected(&flow) else { + let counter = match holder { + Some(Binding::Any | Binding::Specific(_)) => Counter::IcmpErrorUnconnected, + Some(Binding::Connected { .. }) | None => Counter::IcmpErrorNoSocket, + }; + return self.count(counter); + }; + let pending = match class { + ErrorClass::Refused => SocketError::Refused, + ErrorClass::Prohibited => SocketError::Unreachable, + ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft), + }; + socket.pending = Some(pending); + self.count(Counter::IcmpErrorDelivered); + } + + /// [ip] could not reach the next hop of a datagram this crate handed it (§U8.3 (4)). + pub fn unreachable(&mut self, flow: &Flow) { + if let Some(socket) = self.connected(flow) { + socket.pending = Some(SocketError::Unreachable); + } + } + + /// A transmit opportunity with room for `credit` frames: closed sockets' datagrams first, + /// then one datagram per socket in turn, each built as `sink` takes it. `sink` answers whether + /// it spent a frame; one [ip] holds for its next hop spends none. Returns the frames spent. + pub fn transmit(&mut self, credit: usize, mut sink: impl FnMut(&UdpOut<'_>) -> bool) -> usize { + let mut spent = 0usize; + while spent < credit { + let (port, queued) = if let Some(orphan) = self.orphans.pop_front() { + orphan + } else { + let Some(index) = self.active.pop_front() else { break }; + let Some(socket) = self.at(index) else { continue }; + let Some(queued) = socket.tx.pop_front() else { + socket.active = false; + continue; + }; + socket.tx_bytes = socket.tx_bytes.saturating_sub(queued.payload.len()); + let port = socket.port; + if socket.tx.is_empty() { + socket.active = false; + } else { + self.active.push_back(index); + } + (port, queued) + }; + self.count(Counter::Tx); + let out = UdpOut { + source: queued.source, + destination: queued.destination, + ttl: queued.ttl, + datagram: UdpBuilder { source: port, destination: queued.port, data: &queued.payload }, + }; + if sink(&out) { + spent = spent.saturating_add(1); + } + } + spent + } +} diff --git a/toyos-net-udp/tests/bind.rs b/toyos-net-udp/tests/bind.rs new file mode 100644 index 00000000000..e69fda70cbe --- /dev/null +++ b/toyos-net-udp/tests/bind.rs @@ -0,0 +1,115 @@ +//! §U2: sockets and binding. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_udp::{Binding, Counter, Error}; + +fn refused(counter: Counter) -> Result { + Err(Error::Refused(counter)) +} + +#[test] +fn s_udp_us_001_one_socket_per_port() { + let mut u = U::uf(); + let id = u.bind(ANY, 5001).unwrap(); + assert_eq!(u.udp.binding(id), Ok((Binding::Any, port(5001)))); + assert_eq!(u.bind(ANY, 5001), refused(Counter::PortInUse)); +} + +#[test] +fn s_udp_us_002_whatever_the_address() { + let mut u = U::uf(); + u.bind(ANY, 5001).unwrap(); + assert_eq!(u.bind(A, 5001), refused(Counter::PortInUse)); + let mut u = U::uf(); + u.bind(A, 5001).unwrap(); + assert_eq!(u.bind(ANY, 5001), refused(Counter::PortInUse)); +} + +#[test] +fn s_udp_us_003_only_an_assigned_address_binds() { + let mut u = U::uf(); + for (addr, p) in [ + (Ipv4Addr::new(192, 0, 2, 9), 5001), + (MDNS, 5353), + (LIMITED, 68), + (Ipv4Addr::new(192, 0, 2, 255), 5001), + (Ipv4Addr::new(127, 0, 0, 1), 5001), + ] { + assert_eq!(u.bind(addr, p), refused(Counter::BindAddressNotLocal), "{addr}"); + } + assert_eq!(u.count(Counter::BindAddressNotLocal), 5); +} + +#[test] +fn s_udp_us_004_any_needs_no_address() { + let mut u = U::bare(); + assert_eq!(u.bind(A, 5001), refused(Counter::BindAddressNotLocal)); + assert!(u.bind(ANY, 68).is_ok()); +} + +#[test] +fn s_udp_us_005_algorithm_1() { + let mut u = U::uf(); + u.draws.extend([10_000, 10_000]); + let first = u.bind(ANY, 0).unwrap(); + let second = u.bind(ANY, 0).unwrap(); + assert_eq!(u.udp.binding(first).unwrap().1, port(59_152)); + assert_eq!(u.udp.binding(second).unwrap().1, port(59_153)); +} + +#[test] +fn s_udp_us_006_the_candidates_wrap() { + let mut u = U::uf(); + u.draws.extend([16_383, 16_383]); + assert_eq!(u.ephemeral(), port(65_535)); + assert_eq!(u.ephemeral(), port(49_152)); +} + +#[test] +fn s_udp_us_007_the_draw_is_reduced() { + let mut u = U::uf(); + u.draws.extend([16_384, 0xffff_ffff]); + assert_eq!(u.ephemeral(), port(49_152)); + assert_eq!(u.ephemeral(), port(65_535)); +} + +#[test] +fn s_udp_us_008_exhaustion_spends_one_draw() { + let mut u = U::uf(); + for p in 49_152..=65_535 { + u.bind(ANY, p).unwrap(); + } + u.draws.extend([7, 8]); + assert_eq!(u.bind(ANY, 0), refused(Counter::NoEphemeralPort)); + assert_eq!(u.draws, [8], "exactly one draw"); +} + +#[test] +fn s_udp_us_009_a_numbered_bind_is_skipped() { + let mut u = U::uf(); + u.bind(ANY, 59_153).unwrap(); + u.draws.push_back(10_001); + assert_eq!(u.ephemeral(), port(59_154)); +} + +#[test] +fn s_udp_us_010_no_privileged_ports() { + let mut u = U::uf(); + let mdns = u.bind(ANY, 5353).unwrap(); + u.udp.set_ttl(mdns, toyos_net_wire::ipv4::Ttl::new(255).unwrap(), toyos_net_wire::ipv4::Ttl::new(255).unwrap()).unwrap(); + assert_eq!(u.bind(ANY, 5353), refused(Counter::PortInUse)); + assert!(u.bind(ANY, 1).is_ok()); +} + +#[test] +fn s_udp_us_011_close_frees_the_port_at_once() { + let mut u = U::uf(); + let id = u.bind(ANY, 5001).unwrap(); + u.udp.close(id).unwrap(); + assert!(u.bind(ANY, 5001).is_ok()); + assert_eq!(u.udp.binding(id), Err(Error::NoSuchSocket), "the old id names nothing"); +} diff --git a/toyos-net-udp/tests/common/mod.rs b/toyos-net-udp/tests/common/mod.rs new file mode 100644 index 00000000000..2cafb5122c3 --- /dev/null +++ b/toyos-net-udp/tests/common/mod.rs @@ -0,0 +1,434 @@ +//! Fixture UF over `toyos-net-ip`, built through its public API; a transmit opportunity composed +//! as the shard composes it; scripted draws; and an RFC 1071 sum written here for every +//! "(checksum fixed)" mutation. + +#![allow(dead_code)] + +use std::collections::VecDeque; +use std::net::Ipv4Addr; + +use toyos_net_ip::{Delivery, Event, IfIndex, Instant, Ip, Sent, FRAME}; +use toyos_net_udp::{Counter, SocketId, Udp, Verdict}; +use toyos_net_wire::ethernet::{Frame, IndividualMac, MacAddr}; +use toyos_net_wire::ipv4::{Ipv4Packet, MulticastAddr}; +use toyos_net_wire::Port; + +pub const MAC_A: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0a]); +pub const MAC_B: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0b]); +pub const MAC_DNS: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x35]); +pub const MAC_R: MacAddr = MacAddr([2, 0, 0, 0, 0, 0xfe]); +pub const A: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1); +pub const B: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 2); +pub const DNS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 53); +pub const R: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 254); +pub const MDNS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); +pub const ANY: Ipv4Addr = Ipv4Addr::UNSPECIFIED; +pub const LIMITED: Ipv4Addr = Ipv4Addr::BROADCAST; +pub const BASE_MS: u64 = 10_000; + +pub fn port(n: u16) -> Port { + Port::new(n).unwrap() +} + +pub fn hex(text: &str) -> Vec { + text.split_whitespace().map(|b| u8::from_str_radix(b, 16).unwrap()).collect() +} + +pub fn oracle_sum(data: &[u8]) -> u16 { + let mut sum: u32 = 0; + for chunk in data.chunks(2) { + sum += if chunk.len() == 2 { u32::from(chunk[0]) << 8 | u32::from(chunk[1]) } else { u32::from(chunk[0]) << 8 }; + while sum > 0xffff { + sum = (sum & 0xffff) + (sum >> 16); + } + } + sum as u16 +} + +/// Recomputes an IPv4 datagram's header checksum and its UDP or ICMP checksum; a UDP checksum +/// field of zero stays "none". +pub fn fix(ip: &mut [u8]) { + let ihl = usize::from(ip[0] & 0x0f) * 4; + ip[10] = 0; + ip[11] = 0; + let c = !oracle_sum(&ip[..ihl]); + ip[10..12].copy_from_slice(&c.to_be_bytes()); + let total = usize::from(u16::from_be_bytes([ip[2], ip[3]])); + let mut pseudo = ip[12..20].to_vec(); + let protocol = ip[9]; + let body = &mut ip[ihl..total]; + match protocol { + 17 => { + let length = usize::from(u16::from_be_bytes([body[4], body[5]])); + if body[6] == 0 && body[7] == 0 { + return; + } + body[6] = 0; + body[7] = 0; + pseudo.extend_from_slice(&[0, 17]); + pseudo.extend_from_slice(&(length as u16).to_be_bytes()); + pseudo.extend_from_slice(&body[..length]); + let mut c = !oracle_sum(&pseudo); + if c == 0 { + c = 0xffff; + } + body[6..8].copy_from_slice(&c.to_be_bytes()); + } + 1 => { + body[2] = 0; + body[3] = 0; + let c = !oracle_sum(body); + body[2..4].copy_from_slice(&c.to_be_bytes()); + } + _ => {} + } +} + +pub fn edit(mut ip: Vec, change: impl FnOnce(&mut Vec)) -> Vec { + change(&mut ip); + fix(&mut ip); + ip +} + +pub fn set_source(ip: &mut [u8], a: Ipv4Addr) { + ip[12..16].copy_from_slice(&a.octets()); +} + +pub fn set_destination(ip: &mut [u8], a: Ipv4Addr) { + ip[16..20].copy_from_slice(&a.octets()); +} + +pub fn set_ports(ip: &mut [u8], source: u16, destination: u16) { + ip[20..22].copy_from_slice(&source.to_be_bytes()); + ip[22..24].copy_from_slice(&destination.to_be_bytes()); +} + +/// A UDP datagram, atomic, TTL 64, checksummed. +pub fn udp(source: Ipv4Addr, sport: u16, destination: Ipv4Addr, dport: u16, data: &[u8]) -> Vec { + let length = 8 + data.len(); + let total = 20 + length; + let mut ip = vec![0x45, 0, (total >> 8) as u8, total as u8, 0, 0, 0x40, 0, 64, 17, 0, 0]; + ip.extend_from_slice(&source.octets()); + ip.extend_from_slice(&destination.octets()); + ip.extend_from_slice(&sport.to_be_bytes()); + ip.extend_from_slice(&dport.to_be_bytes()); + ip.extend_from_slice(&(length as u16).to_be_bytes()); + ip.extend_from_slice(&[0xff, 0xff]); + ip.extend_from_slice(data); + fix(&mut ip); + ip +} + +pub fn eth(destination: MacAddr, source: MacAddr, ether_type: u16, body: &[u8]) -> Vec { + let mut f = destination.0.to_vec(); + f.extend_from_slice(&source.0); + f.extend_from_slice(ðer_type.to_be_bytes()); + f.extend_from_slice(body); + f +} + +fn arp(op: u16, sender_mac: MacAddr, sender: Ipv4Addr, target_mac: MacAddr, target: Ipv4Addr) -> Vec { + let mut a = vec![0, 1, 8, 0, 6, 4]; + a.extend_from_slice(&op.to_be_bytes()); + a.extend_from_slice(&sender_mac.0); + a.extend_from_slice(&sender.octets()); + a.extend_from_slice(&target_mac.0); + a.extend_from_slice(&target.octets()); + a +} + +pub fn ip_of(frame: &[u8]) -> Option> { + Ipv4Packet::parse(Frame::parse(frame).ok()?.body()).ok() +} + +pub fn ip_bytes(frame: &[u8]) -> Vec { + ip_of(frame).unwrap().bytes().to_vec() +} + +pub fn destination_of(frame: &[u8]) -> MacAddr { + Frame::parse(frame).unwrap().destination() +} + +pub fn is_arp_request_for(frame: &[u8], target: Ipv4Addr) -> bool { + let f = Frame::parse(frame).unwrap(); + toyos_net_wire::arp::Arp::parse(f.body()).is_ok_and(|a| a.operation == toyos_net_wire::arp::Operation::Request && a.target_ip == target) +} + +pub struct U { + pub ip: Ip, + pub udp: Udp, + pub if0: IfIndex, + pub now: u64, + pub draws: VecDeque, + pub events: Vec, + base: Vec<(Counter, u64)>, + ip_base: Vec<(toyos_net_ip::Counter, u64)>, +} + +impl U { + pub fn instant(t: u64) -> Instant { + Instant::from_millis(BASE_MS + t) + } + + pub fn clock(&self) -> Instant { + Self::instant(self.now) + } + + fn setup_until(ip: &mut Ip, until: Instant) { + while let Some(d) = ip.next_deadline().filter(|d| *d <= until) { + ip.fire(d); + ip.transmit(d, usize::MAX, |_, _| {}); + } + ip.transmit(until, usize::MAX, |_, _| {}); + } + + /// A's interface up with MAC A and nothing configured. + pub fn bare() -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), [0x5a; 16]); + let if0 = ip.add_interface(Instant::from_nanos(0), IndividualMac::new(MAC_A).unwrap()); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + ip.join(Instant::from_nanos(0), if0, MulticastAddr::new(MDNS).unwrap()).unwrap(); + Self::setup_until(&mut ip, Self::instant(0)); + let mut u = Self { ip, udp: Udp::new(), if0, now: 0, draws: VecDeque::new(), events: Vec::new(), base: Vec::new(), ip_base: Vec::new() }; + u.rebase(); + u + } + + /// Fixture UF: 192.0.2.1/24 assigned, router 192.0.2.254, B, the DNS server and the router + /// resolved, 224.0.0.1 and 224.0.0.251 joined, no socket. + pub fn uf() -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), [0x5a; 16]); + let t = Instant::from_nanos(0); + let if0 = ip.add_interface(t, IndividualMac::new(MAC_A).unwrap()); + ip.link_up(t, if0).unwrap(); + ip.add_address(t, if0, A, 24).unwrap(); + Self::setup_until(&mut ip, Instant::from_millis(3_000)); + let t = Instant::from_millis(3_000); + ip.set_gateways(t, if0, &[R]).unwrap(); + ip.join(t, if0, MulticastAddr::new(MDNS).unwrap()).unwrap(); + let t = Instant::from_millis(5_000); + for (addr, m) in [(B, MAC_B), (DNS, MAC_DNS), (R, MAC_R)] { + let _ = ip.resolve(t, if0, addr); + ip.transmit(t, usize::MAX, |_, _| {}); + let _ = ip.receive(t, if0, ð(MAC_A, m, 0x0806, &arp(2, m, addr, MAC_A, A))); + } + Self::setup_until(&mut ip, Self::instant(0)); + let mut u = Self { ip, udp: Udp::new(), if0, now: 0, draws: VecDeque::new(), events: Vec::new(), base: Vec::new(), ip_base: Vec::new() }; + u.rebase(); + u + } + + pub fn rebase(&mut self) { + self.base = Counter::ALL.iter().map(|&c| (c, self.udp.counters().get(c))).collect(); + self.ip_base = toyos_net_ip::Counter::ALL.iter().map(|&c| (c, self.ip.counters().get(c))).collect(); + self.ip.drain_events().for_each(drop); + self.udp.drain_events().for_each(drop); + self.events.clear(); + } + + pub fn count(&self, counter: Counter) -> u64 { + self.udp.counters().get(counter) - self.base.iter().find(|(c, _)| *c == counter).map_or(0, |(_, n)| *n) + } + + pub fn ip_count(&self, counter: toyos_net_ip::Counter) -> u64 { + self.ip.counters().get(counter) - self.ip_base.iter().find(|(c, _)| *c == counter).map_or(0, |(_, n)| *n) + } + + /// Binds with the next scripted draw when a port is asked for. + pub fn bind(&mut self, addr: Ipv4Addr, port: u16) -> Result { + let draws = &mut self.draws; + self.udp.bind(&self.ip, addr, Port::new(port), || draws.pop_front().expect("a draw is scripted")) + } + + /// A frame arrives: [ip] admits it, [udp] takes it, and a unicast datagram nobody takes is + /// answered as the shard answers it. + pub fn frame(&mut self, frame: &[u8]) -> Option { + let now = self.clock(); + let verdict = match self.ip.receive(now, self.if0, frame) { + Some(Delivery::Udp(arrival, datagram)) => { + let verdict = self.udp.receive(now, &arrival, &datagram); + if verdict == Verdict::NoSocket { + self.ip.port_unreachable(now, &arrival); + } + Some(verdict) + } + Some(Delivery::Error(error)) => { + self.udp.icmp_error(&error); + None + } + Some(Delivery::Tcp(..)) | None => None, + }; + self.news(); + verdict + } + + /// A datagram from B's MAC to A's. + pub fn datagram(&mut self, ip: &[u8]) -> Option { + self.frame(ð(MAC_A, MAC_B, 0x0800, ip)) + } + + fn news(&mut self) { + let events: Vec = self.ip.drain_events().collect(); + for event in &events { + if let Event::Unreachable(flow) = event { + self.udp.unreachable(flow); + } + } + self.events.extend(events); + } + + /// A transmit opportunity with room for `credit` frames, composed as the shard composes it: + /// [ip]'s frames first, then UDP's, then what UDP's datagrams asked [ip] for. + pub fn out_with(&mut self, credit: usize) -> Vec> { + let now = self.clock(); + let mut frames = Vec::new(); + let mut spent = self.ip.transmit(now, credit, |_, f| frames.push(f.to_vec())); + let Self { ip, udp, .. } = self; + spent += udp.transmit(credit - spent, |out| { + let mut buf = [0u8; FRAME]; + match ip.send_udp(now, out, &mut buf) { + Ok(Sent::Frame(n)) => { + frames.push(buf[..n].to_vec()); + true + } + Ok(Sent::Held) | Err(_) => false, + } + }); + self.ip.transmit(now, credit - spent, |_, f| frames.push(f.to_vec())); + self.news(); + frames + } + + pub fn out(&mut self) -> Vec> { + self.out_with(usize::MAX) + } + + /// Fires [ip]'s deadlines up to `t`, each at its own time, transmitting after each. + pub fn run(&mut self, t: u64) -> Vec> { + let mut out = self.out(); + while let Some(d) = self.ip.next_deadline().filter(|d| *d <= Self::instant(t)) { + self.now = (d.nanos() / 1_000_000).saturating_sub(BASE_MS).max(self.now); + self.ip.fire(d); + out.extend(self.out()); + } + self.now = t; + out.extend(self.out()); + out + } + + pub fn recv(&mut self, id: SocketId) -> Result, toyos_net_udp::Received)>, toyos_net_udp::Error> { + let mut buf = [0u8; 65_536]; + self.udp.recv(id, &mut buf).map(|r| r.map(|r| (buf[..r.len].to_vec(), r))) + } +} + +pub const V_UDP_HI: &str = " +45 00 00 1e 00 00 40 00 40 11 b6 cb c0 00 02 02 +c0 00 02 01 13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_ICMP_PORT_UNREACH_GEN: &str = " +45 00 00 3a 00 00 40 00 40 01 b6 bf c0 00 02 01 +c0 00 02 02 03 03 81 1c 00 00 00 00 45 00 00 1e +00 00 40 00 40 11 b6 cb c0 00 02 02 c0 00 02 01 +13 88 13 89 00 0a ec 5b 68 69"; + +pub const V_IP_RR: &str = " +47 00 00 26 00 00 40 00 40 11 a9 bc c0 00 02 02 +c0 00 02 01 07 07 04 00 00 00 00 00 13 88 13 89 +00 0a ec 5b 68 69"; + +pub const V_IP_NOPS: &str = " +46 00 00 22 00 00 40 00 40 11 b3 c6 c0 00 02 02 +c0 00 02 01 01 01 01 00 13 88 13 89 00 0a ec 5b +68 69"; + +pub const V_UDP_MDNS: &str = " +01 00 5e 00 00 fb 02 00 00 00 00 0b 08 00 45 00 +00 39 00 00 40 00 ff 11 d8 b5 c0 00 02 02 e0 00 +00 fb 14 e9 14 e9 00 25 76 36 00 00 00 00 00 01 +00 00 00 00 00 00 05 74 6f 79 6f 73 05 6c 6f 63 +61 6c 00 00 01 00 01"; + +pub const V_UDP_BCAST: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 00 45 00 +00 1e 00 00 40 00 40 11 78 cd c0 00 02 02 ff ff +ff ff 13 88 13 89 00 0a ae 5d 68 69 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_UDP_SUBNET_BCAST: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0b 08 00 45 00 +00 1e 00 00 40 00 40 11 b5 cd c0 00 02 02 c0 00 +02 ff 13 88 13 89 00 0a eb 5d 68 69 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_UDP_EMPTY: &str = " +02 00 00 00 00 0a 02 00 00 00 00 0b 08 00 45 00 +00 1c 00 00 40 00 40 11 b6 cd c0 00 02 02 c0 00 +02 01 13 88 13 89 00 08 54 c9 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_UDP_A_OUT: &str = " +02 00 00 00 00 35 02 00 00 00 00 0a 08 00 45 00 +00 1e 00 00 40 00 40 11 b6 98 c0 00 02 01 c0 00 +02 35 c3 51 00 35 00 0a 4f b3 68 69 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00"; + +pub const V_ICMP_PU_A_OUT: &str = " +45 00 00 38 00 00 40 00 40 01 b6 8e c0 00 02 35 +c0 00 02 01 03 03 e9 b8 00 00 00 00 45 00 00 1e +00 00 40 00 40 11 b6 98 c0 00 02 01 c0 00 02 35 +c3 51 00 35 00 0a 4f b3"; + +pub const V_DHCP_DISCOVER: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 39 a6 00 00 00 00 ff ff +ff ff 00 44 00 43 01 34 13 19 01 01 06 00 9a 3c +21 f7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 01 3d 0f ff +00 00 00 0a 00 03 00 01 02 00 00 00 00 0a 39 02 +05 c0 37 06 01 03 06 33 3a 3b 0c 05 74 6f 79 6f +73 50 00 ff 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_OFFER_FRAME: &str = " +02 00 00 00 00 0a 02 00 00 00 00 fe 08 00 45 00 +01 2e 00 00 40 00 40 11 b4 bf c0 00 02 fe c0 00 +02 01 00 43 00 44 01 1a a8 44 02 01 06 00 9a 3c +21 f7 00 00 00 00 00 00 00 00 c0 00 02 01 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 02 36 04 c0 +00 02 fe 33 04 00 00 0e 10 01 04 ff ff ff 00 03 +04 c0 00 02 fe 06 04 c0 00 02 35 ff"; + +impl U { + /// Binds any address to an ephemeral port with the next scripted draw; the port it got. + pub fn ephemeral(&mut self) -> Port { + let id = self.bind(ANY, 0).unwrap(); + self.udp.binding(id).unwrap().1 + } +} diff --git a/toyos-net-udp/tests/recv.rs b/toyos-net-udp/tests/recv.rs new file mode 100644 index 00000000000..95b41865df7 --- /dev/null +++ b/toyos-net-udp/tests/recv.rs @@ -0,0 +1,453 @@ +//! §U5–§U9: receiving, ICMP errors, close, and the properties. + +mod common; + +use std::collections::{BTreeMap, BTreeSet}; +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{Cast, Delivery}; +use toyos_net_udp::{limits, Binding, Counter, Error, Received, SocketError, SocketId, Verdict}; +use toyos_net_wire::ethernet::MacAddr; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +fn hi() -> Vec { + hex(V_UDP_HI) +} + +fn received(u: &mut U, id: SocketId) -> (Vec, Received) { + u.recv(id).unwrap().expect("a datagram waits") +} + +fn frames(ip: &[u8]) -> Vec { + eth(MAC_A, MAC_B, 0x0800, ip) +} + +#[test] +fn s_udp_us_030_a_bound_any_socket_receives() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + assert_eq!(u.datagram(&hi()), Some(Verdict::Delivered)); + let (payload, r) = received(&mut u, id); + assert_eq!((payload.as_slice(), r.source, r.source_port, r.destination, r.ttl), (&b"hi"[..], B, Some(port(5_000)), A, 64)); + assert_eq!(u.count(Counter::RxDelivered), 1); +} + +#[test] +fn s_udp_us_031_a_specific_socket_hears_no_broadcast() { + let mut u = U::uf(); + let id = u.bind(A, 5_001).unwrap(); + assert_eq!(u.datagram(&hi()), Some(Verdict::Delivered)); + assert_eq!(received(&mut u, id).0, b"hi"); + assert_eq!(u.frame(&hex(V_UDP_BCAST)), Some(Verdict::Ignored)); + assert_eq!(u.count(Counter::RxNoSocketGroup), 1); + assert!(u.out().is_empty()); +} + +#[test] +fn s_udp_us_032_bound_any_hears_both_broadcasts() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.frame(&hex(V_UDP_BCAST)); + u.frame(&hex(V_UDP_SUBNET_BCAST)); + let first = received(&mut u, id).1; + let second = received(&mut u, id).1; + assert_eq!((first.destination, first.link_broadcast), (LIMITED, true)); + assert_eq!((second.destination, second.link_broadcast), (ip4(192, 0, 2, 255), true)); +} + +#[test] +fn s_udp_us_033_mdns_arrives_with_its_group() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_353).unwrap(); + u.frame(&hex(V_UDP_MDNS)); + let (payload, r) = received(&mut u, id); + assert_eq!(payload, hex(V_UDP_MDNS)[42..]); + assert_eq!((r.source, r.source_port, r.destination, r.ttl), (B, Some(port(5_353)), MDNS, 255)); +} + +#[test] +fn s_udp_us_034_no_socket_for_a_group_is_no_error() { + let mut u = U::uf(); + assert_eq!(u.frame(&hex(V_UDP_MDNS)), Some(Verdict::Ignored)); + assert_eq!(u.count(Counter::RxNoSocketGroup), 1); + assert!(u.out().is_empty()); +} + +#[test] +fn s_udp_us_035_no_socket_is_a_port_unreachable() { + let mut u = U::uf(); + assert_eq!(u.datagram(&hi()), Some(Verdict::NoSocket)); + assert_eq!(u.count(Counter::RxNoSocket), 1); + assert_eq!(u.out().iter().map(|f| ip_bytes(f)).collect::>(), [hex(V_ICMP_PORT_UNREACH_GEN)]); +} + +#[test] +fn s_udp_us_036_an_empty_datagram() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.frame(&hex(V_UDP_EMPTY)); + let (payload, r) = received(&mut u, id); + assert!(payload.is_empty() && r.len == 0); +} + +#[test] +fn s_udp_us_037_source_port_zero() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + let from_zero = edit(hi(), |ip| set_ports(ip, 0, 5_001)); + u.datagram(&from_zero); + let r = received(&mut u, id).1; + assert_eq!(r.source_port, None); + assert_eq!(u.count(Counter::RxSrcPortZero), 1); + assert_eq!(u.udp.send_to(&mut u.ip, id, B, 0, b"x"), Err(Error::Refused(Counter::SendPortZero))); + + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.udp.connect(&mut u.ip, id, B, 5_000).unwrap(); + assert_eq!(u.datagram(&from_zero), Some(Verdict::NoSocket)); + assert_eq!(u.out().len(), 1, "a port unreachable"); +} + +#[test] +fn s_udp_us_038_no_checksum_is_delivered() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + let mut unchecked = hi(); + unchecked[26] = 0; + unchecked[27] = 0; + u.datagram(&unchecked); + assert_eq!(received(&mut u, id).0, b"hi"); + assert_eq!(u.count(Counter::RxNoChecksum), 1); +} + +#[test] +fn s_udp_us_039_a_bad_checksum_is_malformed() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + let mut bad = hi(); + bad[26..28].copy_from_slice(&[0xec, 0x5c]); + assert_eq!(u.datagram(&bad), None); + assert_eq!(u.ip.wire_refusals("udp.checksum"), 1); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); + assert!(u.out().is_empty()); +} + +#[test] +fn s_udp_us_040_a_full_queue_drops_and_says_nothing() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + for _ in 0..limits::RX_DATAGRAMS { + u.datagram(&hi()); + } + assert_eq!(u.datagram(&hi()), Some(Verdict::Delivered)); + assert_eq!(u.count(Counter::RxQueueFull), 1); + assert_eq!(u.udp.dropped(id), Ok(1)); + assert!(u.out().is_empty()); + received(&mut u, id); + u.datagram(&hi()); + assert_eq!(u.count(Counter::RxQueueFull), 1); + assert_eq!(u.count(Counter::RxDelivered), 17); +} + +#[test] +fn s_udp_us_041_arrival_order() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + for data in [&b"a"[..], b"bb", b"ccc"] { + u.datagram(&udp(B, 5_000, A, 5_001, data)); + } + let got: Vec> = (0..3).map(|_| received(&mut u, id).0).collect(); + assert_eq!(got, [b"a".to_vec(), b"bb".to_vec(), b"ccc".to_vec()]); +} + +#[test] +fn s_udp_us_042_a_short_buffer_truncates() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.datagram(&udp(B, 5_000, A, 5_001, b"0123456789")); + u.datagram(&udp(B, 5_000, A, 5_001, b"next")); + let mut buf = [0u8; 4]; + let r = u.udp.recv(id, &mut buf).unwrap().unwrap(); + assert_eq!((r.len, &buf), (4, b"0123")); + assert_eq!(u.count(Counter::RxTruncated), 1); + assert_eq!(received(&mut u, id).0, b"next"); +} + +#[test] +fn s_udp_us_043_a_group_not_joined_never_reaches_udp() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_353).unwrap(); + let other = ip4(239, 1, 2, 3); + let ip = edit(hex(V_UDP_MDNS)[14..].to_vec(), |ip| set_destination(ip, other)); + let group_mac = MacAddr::multicast(toyos_net_wire::ipv4::MulticastAddr::new(other).unwrap()); + assert_eq!(u.frame(ð(group_mac, MAC_B, 0x0800, &ip)), None); + assert_eq!(u.ip_count(toyos_net_ip::Counter::EthNotForUs), 1, "the frame filter comes first"); + assert_eq!(u.frame(&frames(&ip)), None); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); + assert_eq!(u.count(Counter::Rx), 0); + assert_eq!(u.udp.recv(id, &mut [0; 64]), Ok(None)); +} + +#[test] +fn s_udp_us_044_the_acquisition_exception() { + let mut u = U::bare(); + let id = u.bind(ANY, 68).unwrap(); + u.udp.set_acquisition(id).unwrap(); + let offer = hex(V_DHCP_OFFER_FRAME); + let leaked: &'static [u8] = Box::leak(offer.clone().into_boxed_slice()); + let Some(Delivery::Udp(arrival, datagram)) = u.ip.receive(u.clock(), u.if0, leaked) else { panic!("not admitted") }; + assert_eq!(arrival.cast, Cast::Acquisition); + assert_eq!(u.udp.receive(u.clock(), &arrival, &datagram), Verdict::Delivered); + let (payload, r) = received(&mut u, id); + assert_eq!((payload.as_slice(), r.destination), (&offer[42..], A)); + + let mut u = U::bare(); + u.bind(ANY, 5_000).unwrap(); + let mut ip = offer[14..].to_vec(); + set_ports(&mut ip, 67, 5_000); + fix(&mut ip); + assert_eq!(u.frame(ð(MAC_A, MAC_R, 0x0800, &ip)), None); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); +} + +#[test] +fn s_udp_us_045_what_a_datagram_carries() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.now = 1_234; + u.frame(&hex(V_UDP_SUBNET_BCAST)); + let r = received(&mut u, id).1; + assert_eq!((r.destination, r.ttl, r.link_broadcast, r.at), (ip4(192, 0, 2, 255), 64, true, U::instant(1_234))); +} + +fn connected_to_dns() -> (U, SocketId) { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.connect(&mut u.ip, id, DNS, 53).unwrap(); + (u, id) +} + +fn with_code(code: u8) -> Vec { + edit(hex(V_ICMP_PU_A_OUT), |ip| ip[21] = code) +} + +#[test] +fn s_udp_us_046_refused_once() { + let (mut u, id) = connected_to_dns(); + u.datagram(&hex(V_ICMP_PU_A_OUT)); + assert_eq!(u.count(Counter::IcmpErrorDelivered), 1); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Err(Error::Failed(SocketError::Refused))); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); +} + +#[test] +fn s_udp_us_047_an_unconnected_socket_hears_no_error() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.datagram(&hex(V_ICMP_PU_A_OUT)); + assert_eq!(u.count(Counter::IcmpErrorUnconnected), 1); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); +} + +#[test] +fn s_udp_us_048_an_error_for_no_socket() { + let mut u = U::uf(); + u.datagram(&hex(V_ICMP_PU_A_OUT)); + assert_eq!(u.count(Counter::IcmpErrorNoSocket), 1); + assert!(u.out().is_empty()); +} + +#[test] +fn s_udp_us_049_hard_and_soft() { + let (mut u, id) = connected_to_dns(); + u.datagram(&with_code(13)); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Err(Error::Failed(SocketError::Unreachable))); + u.datagram(&with_code(1)); + let time_exceeded = edit(hex(V_ICMP_PU_A_OUT), |ip| { + ip[20] = 11; + ip[21] = 0; + }); + u.datagram(&time_exceeded); + assert_eq!(u.count(Counter::IcmpErrorSoft), 2); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); +} + +#[test] +fn s_udp_us_050_the_latest_error_wins() { + let (mut u, id) = connected_to_dns(); + u.datagram(&with_code(13)); + u.datagram(&with_code(3)); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Err(Error::Failed(SocketError::Refused))); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); +} + +#[test] +fn s_udp_us_051_a_send_takes_the_error() { + let (mut u, id) = connected_to_dns(); + u.datagram(&hex(V_ICMP_PU_A_OUT)); + assert_eq!(u.udp.send(&mut u.ip, id, b"x"), Err(Error::Failed(SocketError::Refused))); + assert!(u.out().is_empty(), "nothing was queued"); + u.udp.send(&mut u.ip, id, b"x").unwrap(); + assert_eq!(u.out().len(), 1); +} + +#[test] +fn s_udp_us_052_the_whole_4_tuple_must_match() { + let (mut u, id) = connected_to_dns(); + u.datagram(&edit(hex(V_ICMP_PU_A_OUT), |ip| ip[51] = 54)); + assert_eq!(u.count(Counter::IcmpErrorNoSocket), 1); + assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); +} + +#[test] +fn s_udp_us_053_accepted_datagrams_outlive_close() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, b"1").unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, b"2").unwrap(); + u.udp.close(id).unwrap(); + u.bind(ANY, 50_001).unwrap(); + let out = u.out(); + assert_eq!(out.len(), 2); + for (frame, data) in out.iter().zip([b"1", b"2"]) { + let ip = ip_of(frame).unwrap(); + assert_eq!((ip.source(), &ip.payload()[..2], &ip.payload()[8..]), (A, &[0xc3, 0x51][..], &data[..])); + } +} + +#[test] +fn s_udp_us_054_close_discards_what_was_received() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + for _ in 0..3 { + u.datagram(&hi()); + } + u.udp.close(id).unwrap(); + assert_eq!(u.count(Counter::RxDiscardedOnClose), 3); + assert_eq!(u.datagram(&hi()), Some(Verdict::NoSocket)); + assert_eq!(u.out().len(), 1); +} + +/// US-55's model: which sockets exist, how each is bound. +#[test] +fn s_udp_us_055_prop_the_demultiplexer() { + for seed in 1..=40u64 { + let mut u = U::uf(); + let mut rng = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1; + let mut next = move || { + rng ^= rng << 13; + rng ^= rng >> 7; + rng ^= rng << 17; + rng + }; + let mut model: BTreeMap = BTreeMap::new(); + let ports = [5_001u16, 5_002, 5_003]; + let peers = [(B, 5_000u16), (DNS, 53)]; + for _ in 0..300 { + match next() % 5 { + 0 => { + let p = ports[(next() % 3) as usize]; + let addr = if next() % 2 == 0 { ANY } else { A }; + let result = u.bind(addr, p); + match model.get(&p) { + Some(_) => assert_eq!(result.err(), Some(Error::Refused(Counter::PortInUse))), + None => { + let id = result.unwrap(); + model.insert(p, (id, u.udp.binding(id).unwrap().0)); + } + } + } + 1 => { + u.draws.push_back(next() as u32); + let id = u.bind(ANY, 0).unwrap(); + let p = u.udp.binding(id).unwrap().1.get(); + assert!(p >= limits::EPHEMERAL_FIRST); + assert!(!model.contains_key(&p), "the port was free"); + model.insert(p, (id, Binding::Any)); + } + 2 => { + if let Some((&p, &(id, _))) = model.iter().nth((next() % 4) as usize) { + let (peer, pp) = peers[(next() % 2) as usize]; + u.udp.connect(&mut u.ip, id, peer, pp).unwrap(); + model.insert(p, (id, u.udp.binding(id).unwrap().0)); + } + } + 3 => { + if let Some((&p, &(id, _))) = model.iter().nth((next() % 4) as usize) { + u.udp.close(id).unwrap(); + model.remove(&p); + } + } + _ => { + let dport = ports[(next() % 3) as usize]; + let (source, sport) = if next() % 4 == 0 { (B, 0) } else { peers[(next() % 2) as usize] }; + let (frame_to, destination) = match next() % 3 { + 0 => (MacAddr::BROADCAST, LIMITED), + 1 => (MacAddr::multicast(toyos_net_wire::ipv4::MulticastAddr::new(MDNS).unwrap()), MDNS), + _ => (MAC_A, A), + }; + let expected = model.get(&dport).map(|&(id, b)| { + let accepts = match (destination == A, b) { + (true, Binding::Any) => true, + (true, Binding::Specific(l)) => l == A, + (true, Binding::Connected { local, peer, peer_port }) => local == A && peer == source && Some(peer_port) == toyos_net_wire::Port::new(sport), + (false, binding) => binding == Binding::Any, + }; + (id, accepts) + }); + let verdict = u.frame(ð(frame_to, MAC_B, 0x0800, &udp(source, sport, destination, dport, b"x"))); + let delivered = matches!(verdict, Some(Verdict::Delivered)); + assert_eq!(delivered, expected.is_some_and(|(_, a)| a), "seed {seed}: {destination}:{dport} from {source}:{sport}"); + if let Some((id, true)) = expected { + let r = received(&mut u, id).1; + if let Some((_, Binding::Connected { peer, peer_port, .. })) = model.get(&dport) { + assert_eq!((r.source, r.source_port), (*peer, Some(*peer_port)), "a connected socket hears its peer alone"); + } + } + u.out(); + } + } + let ports_held: BTreeSet = model.keys().copied().collect(); + assert_eq!(u.udp.socket_count(), ports_held.len(), "one socket per port"); + } + } +} + +#[test] +fn s_udp_us_056_prop_ip_options_never_surface() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_001).unwrap(); + u.datagram(&hi()); + let plain = received(&mut u, id); + for with_options in [hex(V_IP_RR), hex(V_IP_NOPS)] { + u.datagram(&with_options); + assert_eq!(received(&mut u, id), plain); + } + let mut sent = Vec::new(); + for n in 0..64u16 { + let destination = [B, DNS, MDNS, ip4(198, 51, 100, 7)][usize::from(n % 4)]; + u.udp.send_to(&mut u.ip, id, destination, 1 + n, &vec![7; usize::from(n) * 23]).unwrap(); + sent.extend(u.out()); + } + assert_eq!(sent.len(), 64); + assert!(sent.iter().all(|f| ip_of(f).unwrap().header_len() == 20)); +} + +#[test] +fn s_udp_017_a_closed_port_is_answered() { + let mut u = U::uf(); + assert_eq!(u.datagram(&hi()), Some(Verdict::NoSocket)); + assert_eq!(ip_bytes(&u.out()[0]), hex(V_ICMP_PORT_UNREACH_GEN)); +} + +#[test] +fn s_udp_018_the_limited_broadcast_reaches_port_68() { + let mut u = U::bare(); + let id = u.bind(ANY, 68).unwrap(); + u.frame(ð(MacAddr::BROADCAST, MAC_R, 0x0800, &udp(R, 67, LIMITED, 68, b"offer"))); + assert_eq!(received(&mut u, id).0, b"offer"); +} diff --git a/toyos-net-udp/tests/send.rs b/toyos-net-udp/tests/send.rs new file mode 100644 index 00000000000..d853fc9f272 --- /dev/null +++ b/toyos-net-udp/tests/send.rs @@ -0,0 +1,289 @@ +//! §U3 and §U4: connecting and sending. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_udp::{limits, Binding, Counter, Error, Event, Refusal, SocketError, Verdict}; +use toyos_net_wire::ethernet::MacAddr; +use toyos_net_wire::ipv4::Ttl; + +fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { + Ipv4Addr::new(a, b, c, d) +} + +fn refused(counter: Counter) -> Result<(), Error> { + Err(Error::Refused(counter)) +} + +fn udp_frames(frames: &[Vec]) -> Vec> { + frames.iter().filter(|f| ip_of(f).is_some_and(|ip| ip.protocol() == toyos_net_wire::ipv4::Protocol::Udp)).cloned().collect() +} + +#[test] +fn s_udp_us_012_connect_fixes_the_local_address() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.connect(&mut u.ip, id, DNS, 53).unwrap(); + assert_eq!(u.udp.binding(id), Ok((Binding::Connected { local: A, peer: DNS, peer_port: port(53) }, port(50_001)))); + u.udp.send(&mut u.ip, id, b"hi").unwrap(); + assert_eq!(u.out(), [hex(V_UDP_A_OUT)]); +} + +#[test] +fn s_udp_us_013_what_connect_refuses() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + for (peer, p, rule) in [ + (ANY, 53, Counter::ConnectUnspecified), + (LIMITED, 53, Counter::ConnectGroup), + (ip4(192, 0, 2, 255), 53, Counter::ConnectGroup), + (MDNS, 5353, Counter::ConnectGroup), + (ip4(127, 0, 0, 1), 53, Counter::SendLoopback), + (A, 53, Counter::SendToSelf), + (ip4(240, 0, 0, 1), 53, Counter::SendInvalidDestination), + (DNS, 0, Counter::ConnectPortZero), + ] { + assert_eq!(u.udp.connect(&mut u.ip, id, peer, p), refused(rule), "{peer}:{p}"); + assert_eq!(u.udp.binding(id).unwrap().0, Binding::Any); + } +} + +#[test] +fn s_udp_us_014_connect_needs_a_source() { + let mut u = U::bare(); + let id = u.bind(ANY, 50_001).unwrap(); + assert_eq!(u.udp.connect(&mut u.ip, id, DNS, 53), refused(Counter::NoSourceAddress)); +} + +fn connected_to_dns() -> (U, toyos_net_udp::SocketId) { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.connect(&mut u.ip, id, DNS, 53).unwrap(); + (u, id) +} + +#[test] +fn s_udp_us_015_a_connected_socket_hears_its_peer_alone() { + let (mut u, id) = connected_to_dns(); + assert_eq!(u.datagram(&udp(DNS, 53, A, 50_001, b"hi")), Some(Verdict::Delivered)); + assert_eq!(u.recv(id).unwrap().unwrap().0, b"hi"); + for (source, sport) in [(B, 53), (DNS, 54)] { + let stranger = udp(source, sport, A, 50_001, b"hi"); + assert_eq!(u.datagram(&stranger), Some(Verdict::NoSocket)); + let out = u.out(); + assert_eq!(out.len(), 1); + let error = ip_of(&out[0]).unwrap(); + assert_eq!((error.protocol(), error.payload()[0], error.payload()[1]), (toyos_net_wire::ipv4::Protocol::Icmp, 3, 3)); + assert_eq!(error.payload()[8..], stranger[..]); + } + assert_eq!(u.udp.recv(id, &mut [0; 64]), Ok(None)); +} + +#[test] +fn s_udp_us_016_connect_purges_strangers() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.datagram(&udp(DNS, 53, A, 50_001, b"1")); + u.datagram(&udp(DNS, 53, A, 50_001, b"2")); + u.datagram(&udp(B, 5_000, A, 50_001, b"3")); + u.udp.connect(&mut u.ip, id, B, 5_000).unwrap(); + assert_eq!(u.count(Counter::RxDroppedOnConnect), 2); + assert_eq!(u.recv(id).unwrap().unwrap().0, b"3"); + assert_eq!(u.udp.recv(id, &mut [0; 64]), Ok(None)); +} + +#[test] +fn s_udp_us_017_a_connected_socket_sends_to_its_peer() { + let (mut u, id) = connected_to_dns(); + assert_eq!(u.udp.send_to(&mut u.ip, id, B, 5_000, b"x"), refused(Counter::ConnectedDestinationMismatch)); + assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, b"x"), Ok(())); + let bound = u.bind(ANY, 5_002).unwrap(); + assert_eq!(u.udp.send(&mut u.ip, bound, b"x"), refused(Counter::NotConnected)); +} + +#[test] +fn s_udp_us_018_reconnect() { + let (mut u, id) = connected_to_dns(); + u.udp.connect(&mut u.ip, id, B, 5_000).unwrap(); + assert_eq!(u.udp.binding(id).unwrap().0, Binding::Connected { local: A, peer: B, peer_port: port(5_000) }); + assert_eq!(u.datagram(&udp(DNS, 53, A, 50_001, b"hi")), Some(Verdict::NoSocket)); + assert_eq!(u.udp.recv(id, &mut [0; 64]), Ok(None)); +} + +#[test] +fn s_udp_us_019_a_bound_socket_sends() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, b"hi").unwrap(); + let out = u.out(); + assert_eq!(out, [hex(V_UDP_A_OUT)]); + let ip = ip_of(&out[0]).unwrap(); + assert_eq!((ip.payload()[6], ip.payload()[7]), (0x4f, 0xb3)); + assert!(ip.dont_fragment() && ip.identification() == 0 && ip.ttl() == 64); +} + +#[test] +fn s_udp_us_020_what_send_refuses() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + for (destination, p, rule) in [ + (DNS, 0, Counter::SendPortZero), + (ANY, 53, Counter::SendUnspecifiedDestination), + (ip4(127, 0, 0, 1), 53, Counter::SendLoopback), + (A, 53, Counter::SendToSelf), + (ip4(240, 0, 0, 1), 53, Counter::SendInvalidDestination), + ] { + assert_eq!(u.udp.send_to(&mut u.ip, id, destination, p, b"x"), refused(rule), "{destination}:{p}"); + } + let logged: Vec = u.udp.drain_events().collect(); + assert_eq!(logged, [Event::Refused(Refusal { rule: Counter::SendUnspecifiedDestination, local: (ANY, port(50_001)), peer: (ANY, 53) })]); + assert!(u.out().is_empty()); +} + +#[test] +fn s_udp_us_021_broadcast_needs_permission() { + let mut u = U::uf(); + let id = u.bind(ANY, 5_000).unwrap(); + for destination in [LIMITED, ip4(192, 0, 2, 255)] { + assert_eq!(u.udp.send_to(&mut u.ip, id, destination, 5_001, b"hi"), refused(Counter::BroadcastNotPermitted)); + } + assert_eq!(u.udp.drain_events().count(), 2, "each refusal is logged"); + u.udp.set_broadcast(id, true).unwrap(); + for destination in [LIMITED, ip4(192, 0, 2, 255)] { + u.udp.send_to(&mut u.ip, id, destination, 5_001, b"hi").unwrap(); + } + let out = u.out(); + assert_eq!(out.len(), 2); + for (frame, destination) in out.iter().zip([LIMITED, ip4(192, 0, 2, 255)]) { + assert_eq!(destination_of(frame), MacAddr::BROADCAST); + let ip = ip_of(frame).unwrap(); + assert_eq!((ip.destination(), ip.ttl()), (destination, 64)); + } +} + +#[test] +fn s_udp_us_022_multicast_ttl() { + let mut u = U::uf(); + let query = hex(V_UDP_MDNS)[42..].to_vec(); + let mdns = u.bind(ANY, 5353).unwrap(); + u.udp.set_ttl(mdns, Ttl::new(255).unwrap(), Ttl::new(255).unwrap()).unwrap(); + u.udp.send_to(&mut u.ip, mdns, MDNS, 5353, &query).unwrap(); + let expected = edit(hex(V_UDP_MDNS)[14..].to_vec(), |ip| set_source(ip, A)); + let out = u.out(); + assert_eq!(destination_of(&out[0]), MacAddr([1, 0, 0x5e, 0, 0, 0xfb])); + assert_eq!(ip_bytes(&out[0]), expected); + let plain = u.bind(ANY, 5354).unwrap(); + u.udp.send_to(&mut u.ip, plain, MDNS, 5353, &query).unwrap(); + assert_eq!(ip_of(&u.out()[0]).unwrap().ttl(), 1); +} + +#[test] +fn s_udp_us_023_payload_sizes() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, &[0; 1_472]).unwrap(); + assert_eq!(ip_of(&u.out()[0]).unwrap().total_length(), 1_500); + assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, &[0; 1_473]), refused(Counter::ExceedsMtu)); + u.udp.send_to(&mut u.ip, id, DNS, 53, &[]).unwrap(); + let empty = u.out(); + assert_eq!(ip_of(&empty[0]).unwrap().payload()[4..6], [0, 8]); + assert_eq!(limits::MAX_PAYLOAD, 1_472); +} + +#[test] +fn s_udp_us_024_the_source_address_must_be_ours() { + let mut u = U::bare(); + let id = u.bind(ANY, 50_001).unwrap(); + assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, b"x"), refused(Counter::NoSourceAddress)); + + let mut u = U::uf(); + let id = u.bind(A, 5_001).unwrap(); + u.ip.remove_address(u.clock(), u.if0, A).unwrap(); + assert_eq!(u.udp.send_to(&mut u.ip, id, B, 5_000, b"x"), refused(Counter::SourceAddressNotAssigned)); + u.ip.add_address(u.clock(), u.if0, A, 24).unwrap(); + u.run(300); + u.udp.send_to(&mut u.ip, id, B, 5_000, b"x").unwrap(); + let out = udp_frames(&u.run(2_000)); + assert_eq!(out.len(), 1); + assert_eq!(ip_of(&out[0]).unwrap().source(), A); +} + +#[test] +fn s_udp_us_025_a_full_queue_refuses() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + for _ in 0..16 { + u.udp.send_to(&mut u.ip, id, DNS, 53, &[1; 100]).unwrap(); + } + assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, &[1; 100]), refused(Counter::TxQueueFull)); + assert_eq!(u.out_with(1).len(), 1); + assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, &[1; 100]), Ok(())); +} + +#[test] +fn s_udp_us_026_no_head_of_line_blocking() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + let far = ip4(192, 0, 2, 77); + u.udp.send_to(&mut u.ip, id, far, 53, b"1").unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, b"2").unwrap(); + let out = u.out(); + assert_eq!(out.len(), 2); + assert!(out.iter().any(|f| is_arp_request_for(f, far))); + assert!(out.iter().any(|f| ip_of(f).is_some_and(|ip| ip.destination() == DNS))); + let reply = { + let mut a = vec![0, 1, 8, 0, 6, 4, 0, 2]; + a.extend_from_slice(&[2, 0, 0, 0, 0, 0x77]); + a.extend_from_slice(&far.octets()); + a.extend_from_slice(&MAC_A.0); + a.extend_from_slice(&A.octets()); + eth(MAC_A, MacAddr([2, 0, 0, 0, 0, 0x77]), 0x0806, &a) + }; + u.frame(&reply); + let out = u.out(); + assert_eq!(out.len(), 1); + assert_eq!(ip_of(&out[0]).unwrap().destination(), far); +} + +#[test] +fn s_udp_us_027_a_failed_next_hop_is_reported_once() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + let far = ip4(192, 0, 2, 77); + u.udp.connect(&mut u.ip, id, far, 53).unwrap(); + u.udp.send(&mut u.ip, id, b"q").unwrap(); + u.run(3_000); + assert_eq!(u.ip_count(toyos_net_ip::Counter::NbPendingDropped), 1); + assert_eq!(u.udp.recv(id, &mut [0; 64]), Err(Error::Failed(SocketError::Unreachable))); + assert_eq!(u.udp.recv(id, &mut [0; 64]), Ok(None)); +} + +fn acquisition() -> (U, toyos_net_udp::SocketId) { + let mut u = U::bare(); + let id = u.bind(ANY, 68).unwrap(); + u.udp.set_acquisition(id).unwrap(); + u.udp.set_broadcast(id, true).unwrap(); + (u, id) +} + +#[test] +fn s_udp_us_028_only_the_acquisition_socket_sends_from_nowhere() { + let (mut u, id) = acquisition(); + u.udp.send_from(&mut u.ip, id, ANY, LIMITED, 67, b"discover").unwrap(); + let out = u.out(); + assert_eq!(ip_of(&out[0]).unwrap().source(), ANY); + assert_eq!(u.udp.send_from(&mut u.ip, id, ANY, R, 67, b"x"), refused(Counter::SourceNotPermitted)); + let other = u.bind(ANY, 5_000).unwrap(); + u.udp.set_broadcast(other, true).unwrap(); + assert_eq!(u.udp.send_from(&mut u.ip, other, ANY, LIMITED, 67, b"x"), refused(Counter::SourceNotPermitted)); +} + +#[test] +fn s_udp_us_029_the_discover_byte_for_byte() { + let (mut u, id) = acquisition(); + let discover = hex(V_DHCP_DISCOVER); + u.udp.send_from(&mut u.ip, id, ANY, LIMITED, 67, &discover[42..]).unwrap(); + assert_eq!(u.out(), [discover]); +} From 6df4a4100596e170c261fd804a9b58f532475417 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 13:29:26 +0200 Subject: [PATCH 05/20] toyos-dhcp: the DHCPv4 client of ToyOS's own stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 3's third crate, written clean-room from Part D of the stage-3 UDP/DHCP specification and the RFCs: RFC 2131's client with RFC 3396 long options, RFC 4039 rapid commit, an RFC 4361 client identifier (H-2) a server may echo (RFC 6842), and RFC 5227 conflict detection through toyos-net-ip before an acknowledged address is used (H-3 = IP-D1). Pure: each call takes the time and a draw source and returns at most one transmission, a configuration change and an address request. Messages are built to one layout, options in §D2.2's order, padded to 300 bytes; a server's is read by §D3's checks in order, each refusal one named counter, BOOTP replies, unauthenticated FORCERENEW and a lease without a mask refused and logged. Renewals follow RFC 2131 §4.4.5's halving with the 60 s floor; a late timer runs only the latest due event; NAK loops back off (H-7). The tests compare every client message the stack frames, through toyos-net-ip and toyos-net-udp, against the specification's frame vectors byte for byte (DISCOVER, REQUEST, RENEW, REBIND, REBOOT, DECLINE). DH-73's property found that link_up on a lease already expired entered INIT-REBOOT with a deadline in the past; such a lease now expires. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- Cargo.lock | 9 + Cargo.toml | 1 + toyos-dhcp/Cargo.toml | 14 + toyos-dhcp/src/lib.rs | 903 +++++++++++++++++++++++++++++++++ toyos-dhcp/src/message.rs | 224 ++++++++ toyos-dhcp/tests/ack.rs | 131 +++++ toyos-dhcp/tests/common/mod.rs | 573 +++++++++++++++++++++ toyos-dhcp/tests/lease.rs | 294 +++++++++++ toyos-dhcp/tests/link.rs | 126 +++++ toyos-dhcp/tests/parse.rs | 309 +++++++++++ toyos-dhcp/tests/props.rs | 199 ++++++++ toyos-dhcp/tests/stack.rs | 109 ++++ toyos-dhcp/tests/tx.rs | 112 ++++ 13 files changed, 3004 insertions(+) create mode 100644 toyos-dhcp/Cargo.toml create mode 100644 toyos-dhcp/src/lib.rs create mode 100644 toyos-dhcp/src/message.rs create mode 100644 toyos-dhcp/tests/ack.rs create mode 100644 toyos-dhcp/tests/common/mod.rs create mode 100644 toyos-dhcp/tests/lease.rs create mode 100644 toyos-dhcp/tests/link.rs create mode 100644 toyos-dhcp/tests/parse.rs create mode 100644 toyos-dhcp/tests/props.rs create mode 100644 toyos-dhcp/tests/stack.rs create mode 100644 toyos-dhcp/tests/tx.rs diff --git a/Cargo.lock b/Cargo.lock index b261e4e4585..f3c87c3c550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,6 +1030,15 @@ dependencies = [ "toyos-abi", ] +[[package]] +name = "toyos-dhcp" +version = "0.1.0" +dependencies = [ + "toyos-net-ip", + "toyos-net-udp", + "toyos-net-wire", +] + [[package]] name = "toyos-dma" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3f57fa1df60..74ec4ae09b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "toyos-bootmap", "toyos-cpuvuln", "toyos-desktop", + "toyos-dhcp", "toyos-dns", "toyos-dma", "toyos-elf", diff --git a/toyos-dhcp/Cargo.toml b/toyos-dhcp/Cargo.toml new file mode 100644 index 00000000000..fb38dc51969 --- /dev/null +++ b/toyos-dhcp/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "toyos-dhcp" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "ToyOS's DHCPv4 client, pure: the caller hands in the time, the draws and the server's messages, and carries out what each call returns." +publish = false + +[dependencies] +toyos-net-wire = { path = "../toyos-net-wire" } + +[dev-dependencies] +toyos-net-ip = { path = "../toyos-net-ip" } +toyos-net-udp = { path = "../toyos-net-udp" } diff --git a/toyos-dhcp/src/lib.rs b/toyos-dhcp/src/lib.rs new file mode 100644 index 00000000000..1e5f734d426 --- /dev/null +++ b/toyos-dhcp/src/lib.rs @@ -0,0 +1,903 @@ +//! ToyOS's DHCPv4 client (RFC 2131, RFC 2132) with long options (RFC 3396), rapid commit +//! (RFC 4039), an RFC 4361 client identifier a server may echo (RFC 6842), and conflict +//! detection before first use (RFC 5227), which `toyos-net-ip` runs and reports back. Pure: the +//! caller hands in the time, the draws and a server's UDP payloads, and carries out what each +//! call returns — at most one transmission, a configuration change, a request about the address. +//! Nothing here reads a clock, draws randomness or does I/O. +//! +//! **Draws in a fixed order.** A call that starts an exchange draws its xid first; one that arms +//! a jittered wait then draws the jitter; nothing else draws. The xid is the draw itself: the +//! only thing keeping an off-link host from forging replies to this client. +//! +//! **Modern only.** A BOOTP reply, an unauthenticated FORCERENEW and a lease without a subnet +//! mask are refused, counted and named in an [`Event`]; DHCPINFORM and DHCPRELEASE are never sent. + +#![cfg_attr(not(test), no_std)] +#![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] + +extern crate alloc; + +mod message; + +use alloc::vec::Vec; +use core::net::Ipv4Addr; +use core::time::Duration; + +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +use toyos_net_wire::Instant; + +use message::{Build, Kind, Reply, ReplyKind}; + +toyos_net_wire::counters! { + Truncated = "dhcp.truncated", logged; + NotReply = "dhcp.not-reply", logged; + HardwareType = "dhcp.hardware-type", logged; + ChaddrMismatch = "dhcp.chaddr-mismatch"; + BootpReply = "dhcp.bootp-reply", logged; + OptionTruncated = "dhcp.option-truncated", logged; + NoEnd = "dhcp.no-end"; + OverloadInvalid = "dhcp.overload-invalid", logged; + OptionLength = "dhcp.option-length", logged; + MessageTypeMissing = "dhcp.message-type-missing", logged; + WrongDirection = "dhcp.wrong-direction", logged; + Forcerenew = "dhcp.forcerenew", logged; + MessageTypeUnsupported = "dhcp.message-type-unsupported", logged; + XidMismatch = "dhcp.xid-mismatch"; + ClientIdMismatch = "dhcp.client-id-mismatch", logged; + NoServerId = "dhcp.no-server-id", logged; + ServerIdInvalid = "dhcp.server-id-invalid", logged; + UnexpectedOffer = "dhcp.unexpected-offer"; + UnexpectedAck = "dhcp.unexpected-ack"; + UnexpectedNak = "dhcp.unexpected-nak"; + YiaddrInvalid = "dhcp.yiaddr-invalid", logged; + NoLeaseTime = "dhcp.no-lease-time", logged; + LeaseZero = "dhcp.lease-zero", logged; + NoSubnetMask = "dhcp.no-subnet-mask", logged; + MaskInvalid = "dhcp.mask-invalid", logged; + RouterInvalid = "dhcp.router-invalid", logged; + DnsInvalid = "dhcp.dns-invalid", logged; + DnsTruncated = "dhcp.dns-truncated"; + TimerOptionInvalid = "dhcp.timer-option-invalid", logged; + AckWrongServer = "dhcp.ack-wrong-server", logged; + AckAddressChanged = "dhcp.ack-address-changed", logged; + NakWrongServer = "dhcp.nak-wrong-server", logged; + RequestTimeout = "dhcp.request-timeout", logged; + LeaseExpired = "dhcp.lease-expired", logged; + Nak = "dhcp.nak", logged; + RebootUnanswered = "dhcp.reboot-unanswered"; + Declined = "dhcp.declined", logged; + NakBackoff = "dhcp.nak-backoff"; + TxDiscover = "dhcp.tx.discover"; + TxRequest = "dhcp.tx.request"; + TxDecline = "dhcp.tx.decline"; + RxOffer = "dhcp.rx.offer"; + RxAck = "dhcp.rx.ack"; + RxNak = "dhcp.rx.nak"; + EventOverflow = "dhcp.event-overflow"; +} + +pub mod limits { + use core::time::Duration; + + pub const FIRST_WAIT: Duration = Duration::from_secs(4); + pub const MAX_WAIT: Duration = Duration::from_secs(64); + /// A wait is moved by up to this either way (RFC 2131 §4.1). + pub const JITTER: Duration = Duration::from_secs(1); + pub const REQUEST_TRANSMISSIONS: u32 = 4; + pub const RENEW_FLOOR: Duration = Duration::from_secs(60); + pub const DECLINE_BACKOFF: Duration = Duration::from_secs(10); + pub const NAK_BACKOFF_MAX: Duration = Duration::from_secs(64); + pub const MAX_DNS: usize = 3; + /// The largest message the client accepts: a 1,500-byte datagram under either reading of + /// RFC 2132 §9.10. + pub const MAX_MESSAGE: u16 = 1_472; + /// RFC 1542 §2.1's minimum; every message the client sends is padded to it. + pub const MIN_SENT: usize = 300; + /// Refusal events held for the shell until it drains them. + pub const EVENTS: usize = 1_024; +} + +/// A host name the client may send: one DNS label (RFC 1035 §2.3.1, RFC 1123 §2.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HostName(Vec); + +impl HostName { + pub fn new(name: &str) -> Option { + let bytes = name.as_bytes(); + let label = (1..=63).contains(&bytes.len()) + && bytes.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'-') + && bytes.first() != Some(&b'-') + && bytes.last() != Some(&b'-'); + label.then(|| Self(bytes.to_vec())) + } + + pub fn bytes(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Timers { + pub t1: Instant, + pub t2: Instant, + pub expiry: Instant, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Lease { + pub address: Ipv4Addr, + pub prefix_len: u8, + pub router: Option, + pub dns: Vec, + pub server: Ipv4Addr, + /// The first transmission of the request the ACK answered (RFC 2131 §4.4.1). + pub base: Instant, + /// `None` for an infinite lease. + pub timers: Option, +} + +impl Lease { + fn expired(&self, now: Instant) -> bool { + self.timers.is_some_and(|t| now >= t.expiry) + } +} + +/// A message to send: UDP 68 → 67, built by the stack around this payload. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Transmission { + /// 0.0.0.0, or the lease's address when renewing or rebinding. + pub source: Ipv4Addr, + /// The limited broadcast, or the lease's server identifier when renewing. + pub destination: Ipv4Addr, + pub payload: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Reason { + Expired, + Refused, + Conflict, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Config { + /// A new lease: address, prefix, route and resolvers together. + Configured(Lease), + /// The same parameters, new times. + Extended(Lease), + /// The same address with a changed prefix, router or resolvers. + Reconfigured(Lease), + /// Address, route and resolvers go. + Deconfigured(Reason), +} + +/// What the shell asks `toyos-net-ip` for. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AddressRequest { + /// Add as tentative: conflict detection runs, then reports back (RFC 5227 §2.1). + Probe { address: Ipv4Addr, prefix_len: u8 }, + /// Remove the tentative address. + Cancel(Ipv4Addr), +} + +/// What one call asks the shell to do: at most one of each. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Output { + pub transmit: Option, + pub config: Option, + pub address: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Phase { + Selecting, + Requesting, + Probing, + Bound, + Renewing, + Rebinding, + Rebooting, + BackingOff, +} + +/// Who a logged refusal names. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Peer { + /// A message's IPv4 source. + From(Ipv4Addr), + /// The server a request went unanswered by. + Server(Ipv4Addr), + /// The address a lease held. + Lease(Ipv4Addr), + /// The host an address was found in use by. + Conflict { address: Ipv4Addr, mac: MacAddr }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Event { + Refused { rule: Counter, peer: Peer }, +} + +#[derive(Debug)] +struct Selecting { + xid: u32, + /// The exchange's first DISCOVER: a rapid-commit lease runs from it. + start: Instant, + /// The last DISCOVER's, which the REQUEST repeats (RFC 2131 §3.1 (3)). + secs: u16, + step: u32, + deadline: Instant, +} + +#[derive(Debug)] +struct Requesting { + xid: u32, + server: Ipv4Addr, + address: Ipv4Addr, + secs: u16, + first: Instant, + step: u32, + deadline: Instant, +} + +/// RENEWING or REBINDING: each transmission is an exchange of its own. +#[derive(Debug)] +struct Renewal { + lease: Lease, + xid: u32, + /// The first transmission of this lease period's renewal, which `secs` counts from. + started: Instant, + /// This transmission, which an ACK to it makes the lease's base. + sent: Instant, + deadline: Instant, +} + +#[derive(Debug)] +struct Rebooting { + lease: Lease, + xid: u32, + start: Instant, + step: u32, + deadline: Instant, +} + +#[derive(Debug)] +enum State { + Selecting(Selecting), + Requesting(Requesting), + /// The acknowledged lease, its address under conflict detection. + Probing(Lease), + Bound(Lease), + Renewing(Renewal), + Rebinding(Renewal), + Rebooting(Rebooting), + BackingOff(Instant), +} + +pub struct Client { + mac: IndividualMac, + client_id: [u8; 15], + host_name: Option, + state: State, + /// Consecutive NAKs since the last BOUND (§D11). + naks: u32, + counters: Counters, + events: Vec, +} + +/// Not 0.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 224.0.0.0/4 or 240.0.0.0/4: one host (§D4.1). +fn host(addr: Ipv4Addr) -> bool { + let [a, b, ..] = addr.octets(); + !(a == 0 || a == 127 || (a, b) == (169, 254) || a >= 224) +} + +fn mask(len: u8) -> u32 { + u32::MAX.checked_shl(u32::from(32u8.saturating_sub(len))).unwrap_or(0) +} + +/// A contiguous mask of 1 to 31 bits (H-14). +fn prefix_len(mask_addr: Ipv4Addr) -> Option { + let bits = u32::from(mask_addr); + let len = u8::try_from(bits.leading_ones()).ok()?; + ((1..=31).contains(&len) && mask(len) == bits).then_some(len) +} + +fn same_subnet(a: Ipv4Addr, b: Ipv4Addr, len: u8) -> bool { + (u32::from(a) ^ u32::from(b)) & mask(len) == 0 +} + +/// The directed broadcast of a prefix of /30 or shorter. +fn broadcast(addr: Ipv4Addr, len: u8) -> Option { + (len <= 30).then(|| Ipv4Addr::from(u32::from(addr) | !mask(len))) +} + +fn secs(since: Duration) -> u16 { + u16::try_from(since.as_secs()).unwrap_or(u16::MAX) +} + +/// 4, 8, 16, 32, then 64 s (RFC 2131 §4.1). +fn backoff(step: u32) -> Duration { + 2u32.checked_pow(step).and_then(|m| limits::FIRST_WAIT.checked_mul(m)).map_or(limits::MAX_WAIT, |d| d.min(limits::MAX_WAIT)) +} + +/// A wait moved by (draw mod 2,001) − 1,000 ms (§D6.3 (1)). +fn jittered(wait: Duration, draw: u32) -> Duration { + let jitter = u64::from(draw.checked_rem(2_001).unwrap_or(0)); + let ms = u64::try_from(wait.as_millis()).unwrap_or(u64::MAX).saturating_add(jitter).saturating_sub(1_000); + Duration::from_millis(ms) +} + +/// Half the time left until `until`, and never less than the floor (RFC 2131 §4.4.5). +fn halfway(now: Instant, until: Instant) -> Instant { + let half = Duration::from_nanos(until.since(now).as_nanos().checked_div(2).and_then(|n| u64::try_from(n).ok()).unwrap_or(0)); + now.after(half.max(limits::RENEW_FLOOR)).min(until) +} + +fn ms(seconds: u32) -> Duration { + Duration::from_millis(u64::from(seconds).saturating_mul(1_000)) +} + +impl Client { + /// Begins the first exchange: a DISCOVER now (DIV-D1: no initial delay). + pub fn start(now: Instant, mac: IndividualMac, host_name: Option, mut draw: impl FnMut() -> u32) -> (Self, Output) { + let [m0, m1, m2, m3, m4, m5] = mac.get().0; + let client_id = [255, m2, m3, m4, m5, 0, 3, 0, 1, m0, m1, m2, m3, m4, m5]; + let mut client = Self { + mac, + client_id, + host_name, + state: State::BackingOff(now), + naks: 0, + counters: Counters::default(), + events: Vec::new(), + }; + let out = client.begin(now, &mut draw); + (client, out) + } + + pub fn counters(&self) -> &Counters { + &self.counters + } + + /// Refusals to log since the last call. + pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> { + self.events.drain(..) + } + + pub fn phase(&self) -> Phase { + match self.state { + State::Selecting(_) => Phase::Selecting, + State::Requesting(_) => Phase::Requesting, + State::Probing(_) => Phase::Probing, + State::Bound(_) => Phase::Bound, + State::Renewing(_) => Phase::Renewing, + State::Rebinding(_) => Phase::Rebinding, + State::Rebooting(_) => Phase::Rebooting, + State::BackingOff(_) => Phase::BackingOff, + } + } + + /// The transaction a reply must carry; none while bound, probing or backing off. + pub fn xid(&self) -> Option { + match &self.state { + State::Selecting(s) => Some(s.xid), + State::Requesting(r) => Some(r.xid), + State::Renewing(r) | State::Rebinding(r) => Some(r.xid), + State::Rebooting(r) => Some(r.xid), + State::Probing(_) | State::Bound(_) | State::BackingOff(_) => None, + } + } + + /// The lease in use: configured, or being renewed or verified. + pub fn lease(&self) -> Option<&Lease> { + match &self.state { + State::Bound(lease) => Some(lease), + State::Renewing(r) | State::Rebinding(r) => Some(&r.lease), + State::Rebooting(r) => Some(&r.lease), + State::Selecting(_) | State::Requesting(_) | State::Probing(_) | State::BackingOff(_) => None, + } + } + + pub fn next_deadline(&self) -> Option { + match &self.state { + State::Selecting(s) => Some(s.deadline), + State::Requesting(r) => Some(r.deadline), + State::Probing(_) => None, + State::Bound(lease) => lease.timers.map(|t| t.t1), + State::Renewing(r) | State::Rebinding(r) => Some(r.deadline), + State::Rebooting(r) => Some(r.lease.timers.map_or(r.deadline, |t| r.deadline.min(t.expiry))), + State::BackingOff(until) => Some(*until), + } + } + + fn refuse(&mut self, rule: Counter, peer: Peer) { + self.counters.add(rule, 1); + if !rule.logged() { + return; + } + if self.events.len() >= limits::EVENTS { + self.counters.add(Counter::EventOverflow, 1); + } else { + self.events.push(Event::Refused { rule, peer }); + } + } + + fn message(&self, kind: Kind, xid: u32, secs: u16, ciaddr: Ipv4Addr, server: Option, requested: Option) -> Vec { + Build { + kind, + xid, + secs, + ciaddr, + server, + requested, + mac: self.mac.get(), + client_id: &self.client_id, + host_name: self.host_name.as_ref(), + } + .bytes() + } + + fn broadcast(&mut self, payload: Vec, counter: Counter) -> Transmission { + self.counters.add(counter, 1); + Transmission { source: Ipv4Addr::UNSPECIFIED, destination: Ipv4Addr::BROADCAST, payload } + } + + /// A new exchange: an xid, a DISCOVER now, and its first jittered wait (INIT → SELECTING). + fn begin(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) -> Output { + let xid = draw(); + let payload = self.message(Kind::Discover, xid, 0, Ipv4Addr::UNSPECIFIED, None, None); + let transmit = self.broadcast(payload, Counter::TxDiscover); + let deadline = now.after(jittered(backoff(0), draw())); + self.state = State::Selecting(Selecting { xid, start: now, secs: 0, step: 0, deadline }); + Output { transmit: Some(transmit), ..Output::default() } + } + + /// A DECLINE of `address` to `server`, then 10 s of BACKING-OFF (RFC 2131 §3.1 (5)). + fn decline(&mut self, now: Instant, address: Ipv4Addr, server: Ipv4Addr, mac: MacAddr, draw: &mut impl FnMut() -> u32) -> Transmission { + let xid = draw(); + let payload = self.message(Kind::Decline, xid, 0, Ipv4Addr::UNSPECIFIED, Some(server), Some(address)); + self.refuse(Counter::Declined, Peer::Conflict { address, mac }); + self.state = State::BackingOff(now.after(limits::DECLINE_BACKOFF)); + self.broadcast(payload, Counter::TxDecline) + } + + /// A RENEWING (unicast to the server) or REBINDING (broadcast) request with a fresh xid. + fn renew(&mut self, now: Instant, lease: Lease, started: Instant, rebind: bool, draw: &mut impl FnMut() -> u32) -> Output { + let xid = draw(); + let payload = self.message(Kind::Request, xid, secs(now.since(started)), lease.address, None, None); + self.counters.add(Counter::TxRequest, 1); + let Some(timers) = lease.timers else { return Output::default() }; + let (destination, deadline) = if rebind { + (Ipv4Addr::BROADCAST, halfway(now, timers.expiry)) + } else { + (lease.server, halfway(now, timers.t2)) + }; + let transmit = Transmission { source: lease.address, destination, payload }; + let renewal = Renewal { lease, xid, started, sent: now, deadline }; + self.state = if rebind { State::Rebinding(renewal) } else { State::Renewing(renewal) }; + Output { transmit: Some(transmit), ..Output::default() } + } + + /// The lease ended: deconfigured, and a new exchange in the same call (RFC 2131 §4.4.5). + fn expire(&mut self, now: Instant, address: Ipv4Addr, draw: &mut impl FnMut() -> u32) -> Output { + self.refuse(Counter::LeaseExpired, Peer::Lease(address)); + Output { config: Some(Config::Deconfigured(Reason::Expired)), ..self.begin(now, draw) } + } + + /// A lease in use whose deadlines may be due: only the latest due event runs (§D5.2). + fn run_lease(&mut self, now: Instant, lease: Lease, started: Option, draw: &mut impl FnMut() -> u32) -> Output { + let Some(timers) = lease.timers else { + self.state = State::Bound(lease); + return Output::default(); + }; + if now >= timers.expiry { + return self.expire(now, lease.address, draw); + } + if now >= timers.t2 { + return self.renew(now, lease, started.unwrap_or(now), true, draw); + } + if now >= timers.t1 { + return self.renew(now, lease, started.unwrap_or(now), false, draw); + } + self.state = State::Bound(lease); + Output::default() + } + + /// Enters BOUND with `lease`, or at once RENEWING or REBINDING when those are due. + fn bind(&mut self, now: Instant, lease: Lease, draw: &mut impl FnMut() -> u32) -> Output { + self.naks = 0; + self.run_lease(now, lease, None, draw) + } + + /// A NAK the state accepted (§D11): the n-th in a row delays the new exchange by 0, 4, 8, 16, + /// 32, then 64 s, each delayed one jittered. + fn nak(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) -> Output { + self.naks = self.naks.saturating_add(1); + match self.naks.checked_sub(2) { + None => self.begin(now, draw), + Some(step) => { + let wait = jittered(backoff(step), draw()); + self.counters.add(Counter::NakBackoff, 1); + self.state = State::BackingOff(now.after(wait)); + Output::default() + } + } + } + + pub fn timer(&mut self, now: Instant, mut draw: impl FnMut() -> u32) -> Output { + if self.next_deadline().is_none_or(|d| now < d) { + return Output::default(); + } + let state = core::mem::replace(&mut self.state, State::BackingOff(now)); + match state { + State::Selecting(mut s) => { + s.secs = secs(now.since(s.start)); + s.step = s.step.saturating_add(1); + let payload = self.message(Kind::Discover, s.xid, s.secs, Ipv4Addr::UNSPECIFIED, None, None); + let transmit = self.broadcast(payload, Counter::TxDiscover); + s.deadline = now.after(jittered(backoff(s.step), draw())); + self.state = State::Selecting(s); + Output { transmit: Some(transmit), ..Output::default() } + } + State::Requesting(r) if r.step.saturating_add(1) >= limits::REQUEST_TRANSMISSIONS => { + self.refuse(Counter::RequestTimeout, Peer::Server(r.server)); + self.begin(now, &mut draw) + } + State::Requesting(mut r) => { + r.step = r.step.saturating_add(1); + let payload = self.message(Kind::Request, r.xid, r.secs, Ipv4Addr::UNSPECIFIED, Some(r.server), Some(r.address)); + let transmit = self.broadcast(payload, Counter::TxRequest); + r.deadline = now.after(jittered(backoff(r.step), draw())); + self.state = State::Requesting(r); + Output { transmit: Some(transmit), ..Output::default() } + } + State::Bound(lease) => self.run_lease(now, lease, None, &mut draw), + State::Renewing(r) | State::Rebinding(r) => self.run_lease(now, r.lease, Some(r.started), &mut draw), + State::Rebooting(r) if r.lease.expired(now) => self.expire(now, r.lease.address, &mut draw), + State::Rebooting(r) if r.step.saturating_add(1) >= limits::REQUEST_TRANSMISSIONS => { + self.counters.add(Counter::RebootUnanswered, 1); + self.run_lease(now, r.lease, None, &mut draw) + } + State::Rebooting(mut r) => { + r.step = r.step.saturating_add(1); + let transmit = self.reboot_request(now, &r); + r.deadline = now.after(jittered(backoff(r.step), draw())); + self.state = State::Rebooting(r); + Output { transmit: Some(transmit), ..Output::default() } + } + State::BackingOff(_) => self.begin(now, &mut draw), + State::Probing(lease) => { + self.state = State::Probing(lease); + Output::default() + } + } + } + + /// INIT-REBOOT's REQUEST: the held address in option 50, no server identifier (RFC 2131 §4.4.2). + fn reboot_request(&mut self, now: Instant, r: &Rebooting) -> Transmission { + let payload = self.message(Kind::Request, r.xid, secs(now.since(r.start)), Ipv4Addr::UNSPECIFIED, None, Some(r.lease.address)); + self.broadcast(payload, Counter::TxRequest) + } + + /// The link came up (§D9): without a lease the exchange starts over; with one, INIT-REBOOT + /// verifies it while it stays in use, and [ip] announces it rather than probing again. + pub fn link_up(&mut self, now: Instant, mut draw: impl FnMut() -> u32) -> Output { + let state = core::mem::replace(&mut self.state, State::BackingOff(now)); + let lease = match state { + State::Bound(lease) => lease, + State::Renewing(r) | State::Rebinding(r) => r.lease, + State::Rebooting(r) => r.lease, + State::Probing(lease) => { + return Output { address: Some(AddressRequest::Cancel(lease.address)), ..self.begin(now, &mut draw) }; + } + State::Selecting(_) | State::Requesting(_) | State::BackingOff(_) => return self.begin(now, &mut draw), + }; + if lease.expired(now) { + return self.expire(now, lease.address, &mut draw); + } + let xid = draw(); + let r = Rebooting { lease, xid, start: now, step: 0, deadline: now }; + let transmit = self.reboot_request(now, &r); + let deadline = now.after(jittered(backoff(0), draw())); + self.state = State::Rebooting(Rebooting { deadline, ..r }); + Output { transmit: Some(transmit), ..Output::default() } + } + + /// [ip] finished conflict detection: the address is in use from here (§D8 (2)). + pub fn verified(&mut self, now: Instant, mut draw: impl FnMut() -> u32) -> Output { + let lease = match core::mem::replace(&mut self.state, State::BackingOff(now)) { + State::Probing(lease) => lease, + other => { + self.state = other; + return Output::default(); + } + }; + if lease.expired(now) { + self.refuse(Counter::LeaseExpired, Peer::Lease(lease.address)); + return self.begin(now, &mut draw); + } + let configured = Config::Configured(lease.clone()); + Output { config: Some(configured), ..self.bind(now, lease, &mut draw) } + } + + /// Another host holds the address: while probing, a DECLINE; while in use, it was lost after + /// [ip]'s one defence, so it is deconfigured and declined (§D8 (3), (5)). + pub fn conflict(&mut self, now: Instant, mac: MacAddr, mut draw: impl FnMut() -> u32) -> Output { + let state = core::mem::replace(&mut self.state, State::BackingOff(now)); + let (lease, in_use) = match state { + State::Probing(lease) => (lease, false), + State::Bound(lease) => (lease, true), + State::Renewing(r) | State::Rebinding(r) => (r.lease, true), + State::Rebooting(r) => (r.lease, true), + other => { + self.state = other; + return Output::default(); + } + }; + let transmit = self.decline(now, lease.address, lease.server, mac, &mut draw); + let config = in_use.then_some(Config::Deconfigured(Reason::Conflict)); + Output { transmit: Some(transmit), config, address: None } + } + + /// The link went down while [ip] probed: the exchange is abandoned (§D8 (6)). + pub fn not_verified(&mut self, now: Instant, mut draw: impl FnMut() -> u32) -> Output { + match self.state { + State::Probing(_) => self.begin(now, &mut draw), + _ => Output::default(), + } + } + + /// A server's UDP payload and the IPv4 source it came from (§D3). + pub fn receive(&mut self, now: Instant, payload: &[u8], from: Ipv4Addr, mut draw: impl FnMut() -> u32) -> Output { + let peer = Peer::From(from); + let reply = match Reply::parse(payload, self.mac.get()) { + Ok(reply) => reply, + Err(rule) => { + self.refuse(rule, peer); + return Output::default(); + } + }; + self.counters.add(Counter::NoEnd, reply.no_end); + self.counters.add( + match reply.kind { + ReplyKind::Offer => Counter::RxOffer, + ReplyKind::Ack => Counter::RxAck, + ReplyKind::Nak => Counter::RxNak, + }, + 1, + ); + if self.xid() != Some(reply.xid) { + self.refuse(Counter::XidMismatch, peer); + return Output::default(); + } + if reply.client_id.as_deref().is_some_and(|id| id != self.client_id) { + self.refuse(Counter::ClientIdMismatch, peer); + return Output::default(); + } + let server = match reply.server { + None => return self.refused(Counter::NoServerId, peer), + Some(server) if !host(server) => return self.refused(Counter::ServerIdInvalid, peer), + Some(server) => server, + }; + let unexpected = match (&self.state, reply.kind) { + (State::Selecting(_), ReplyKind::Ack) if !reply.rapid => Some(Counter::UnexpectedAck), + (State::Selecting(_), ReplyKind::Nak) => Some(Counter::UnexpectedNak), + (State::Selecting(_), _) => None, + (_, ReplyKind::Offer) => Some(Counter::UnexpectedOffer), + _ => None, + }; + if let Some(rule) = unexpected { + return self.refused(rule, peer); + } + match reply.kind { + ReplyKind::Nak => self.receive_nak(now, server, peer, &mut draw), + ReplyKind::Offer | ReplyKind::Ack => { + let Some(offered) = self.acceptable(&reply, server, peer) else { return Output::default() }; + self.accept(now, reply.kind, offered, peer, &mut draw) + } + } + } + + fn refused(&mut self, rule: Counter, peer: Peer) -> Output { + self.refuse(rule, peer); + Output::default() + } + + /// §D4.1: the address, the lease time and the mask an OFFER or ACK must carry. + fn acceptable(&mut self, reply: &Reply, server: Ipv4Addr, peer: Peer) -> Option { + let yiaddr = reply.yiaddr; + let refusal = if !host(yiaddr) { + Some(Counter::YiaddrInvalid) + } else { + match (reply.lease, reply.mask) { + (None, _) => Some(Counter::NoLeaseTime), + (Some(0), _) => Some(Counter::LeaseZero), + (_, None) => Some(Counter::NoSubnetMask), + (Some(_), Some(m)) => match prefix_len(m) { + None => Some(Counter::MaskInvalid), + Some(len) if len <= 30 && (yiaddr == Ipv4Addr::from(u32::from(yiaddr) & mask(len)) || Some(yiaddr) == broadcast(yiaddr, len)) => { + Some(Counter::YiaddrInvalid) + } + Some(_) => None, + }, + } + }; + if let Some(rule) = refusal { + self.refuse(rule, peer); + return None; + } + Some(Offered { + address: yiaddr, + prefix_len: reply.mask.and_then(prefix_len).unwrap_or(32), + server, + lease: reply.lease.unwrap_or(0), + routers: reply.routers.clone(), + dns: reply.dns.clone(), + t1: reply.t1, + t2: reply.t2, + }) + } + + fn receive_nak(&mut self, now: Instant, server: Ipv4Addr, peer: Peer, draw: &mut impl FnMut() -> u32) -> Output { + let wrong = match &self.state { + State::Requesting(r) => r.server != server, + State::Renewing(r) => r.lease.server != server, + State::Rebinding(_) | State::Rebooting(_) => false, + State::Selecting(_) | State::Probing(_) | State::Bound(_) | State::BackingOff(_) => return Output::default(), + }; + if wrong { + return self.refused(Counter::NakWrongServer, peer); + } + if matches!(self.state, State::Requesting(_)) { + return self.nak(now, draw); + } + self.refuse(Counter::Nak, peer); + Output { config: Some(Config::Deconfigured(Reason::Refused)), ..self.nak(now, draw) } + } + + /// An acceptable OFFER or ACK for the state the client is in (§D5.2): an ACK must name the + /// server asked while REQUESTING or RENEWING, and the address asked for or held. + fn accept(&mut self, now: Instant, kind: ReplyKind, offered: Offered, peer: Peer, draw: &mut impl FnMut() -> u32) -> Output { + let state = core::mem::replace(&mut self.state, State::BackingOff(now)); + match (state, kind) { + (State::Selecting(s), ReplyKind::Offer) => { + let payload = self.message(Kind::Request, s.xid, s.secs, Ipv4Addr::UNSPECIFIED, Some(offered.server), Some(offered.address)); + let transmit = self.broadcast(payload, Counter::TxRequest); + let deadline = now.after(jittered(backoff(0), draw())); + self.state = State::Requesting(Requesting { + xid: s.xid, + server: offered.server, + address: offered.address, + secs: s.secs, + first: now, + step: 0, + deadline, + }); + Output { transmit: Some(transmit), ..Output::default() } + } + (State::Selecting(s), _) => { + let lease = offered.lease_from(s.start, peer, self); + self.probe(lease) + } + (State::Requesting(r), _) if offered.server != r.server => self.keep(State::Requesting(r), Counter::AckWrongServer, peer), + (State::Requesting(r), _) if offered.address != r.address => self.keep(State::Requesting(r), Counter::AckAddressChanged, peer), + (State::Requesting(r), _) => { + let lease = offered.lease_from(r.first, peer, self); + self.probe(lease) + } + (State::Renewing(r), _) if offered.server != r.lease.server => self.keep(State::Renewing(r), Counter::AckWrongServer, peer), + (State::Renewing(r), _) if offered.address != r.lease.address => self.keep(State::Renewing(r), Counter::AckAddressChanged, peer), + (State::Rebinding(r), _) if offered.address != r.lease.address => self.keep(State::Rebinding(r), Counter::AckAddressChanged, peer), + (State::Renewing(r) | State::Rebinding(r), _) => { + let lease = offered.lease_from(r.sent, peer, self); + self.renewed(now, &r.lease, lease, draw) + } + (State::Rebooting(r), _) if offered.address != r.lease.address => self.keep(State::Rebooting(r), Counter::AckAddressChanged, peer), + (State::Rebooting(r), _) => { + let lease = offered.lease_from(r.start, peer, self); + self.renewed(now, &r.lease, lease, draw) + } + (state @ (State::Probing(_) | State::Bound(_) | State::BackingOff(_)), _) => { + self.state = state; + Output::default() + } + } + } + + fn keep(&mut self, state: State, rule: Counter, peer: Peer) -> Output { + self.state = state; + self.refused(rule, peer) + } + + /// An acknowledged new address: [ip] probes it before it is used (RFC 5227 §2.1). + fn probe(&mut self, lease: Lease) -> Output { + let address = Some(AddressRequest::Probe { address: lease.address, prefix_len: lease.prefix_len }); + self.state = State::Probing(lease); + Output { address, ..Output::default() } + } + + /// The held address acknowledged again: extended, or reconfigured when its prefix, router or + /// resolvers changed. + fn renewed(&mut self, now: Instant, old: &Lease, lease: Lease, draw: &mut impl FnMut() -> u32) -> Output { + let same = old.prefix_len == lease.prefix_len && old.router == lease.router && old.dns == lease.dns; + let config = if same { Config::Extended(lease.clone()) } else { Config::Reconfigured(lease.clone()) }; + Output { config: Some(config), ..self.bind(now, lease, draw) } + } +} + +/// What an acceptable OFFER or ACK holds, before §D4.2 degrades it into a lease. +struct Offered { + address: Ipv4Addr, + prefix_len: u8, + server: Ipv4Addr, + lease: u32, + routers: Vec, + dns: Vec, + t1: Option, + t2: Option, +} + +impl Offered { + /// §D4.2 and §D7: the first usable router, at most three resolvers, and T1 and T2, the + /// lease running from `base`. + fn lease_from(self, base: Instant, peer: Peer, client: &mut Client) -> Lease { + let (address, len) = (self.address, self.prefix_len); + let mut router = None; + for candidate in self.routers { + if host(candidate) && same_subnet(candidate, address, len) && candidate != address { + router = Some(candidate); + break; + } + client.refuse(Counter::RouterInvalid, peer); + } + let mut dns = Vec::new(); + for server in self.dns { + if !host(server) || Some(server) == broadcast(address, len) { + client.refuse(Counter::DnsInvalid, peer); + } else if dns.len() < limits::MAX_DNS { + dns.push(server); + } else { + client.counters.add(Counter::DnsTruncated, 1); + } + } + let timers = if self.lease == u32::MAX { + for _ in self.t1.iter().chain(self.t2.iter()) { + client.refuse(Counter::TimerOptionInvalid, peer); + } + None + } else { + let lease = ms(self.lease); + let t2 = match self.t2 { + Some(t2) if t2 > 0 && t2 < self.lease => ms(t2), + given => { + if given.is_some() { + client.refuse(Counter::TimerOptionInvalid, peer); + } + lease.saturating_mul(7).checked_div(8).unwrap_or(lease) + } + }; + let t1 = match self.t1 { + Some(t1) if t1 > 0 && ms(t1) < t2 => ms(t1), + given => { + if given.is_some() { + client.refuse(Counter::TimerOptionInvalid, peer); + } + lease.checked_div(2).unwrap_or(lease).min(t2) + } + }; + Some(Timers { t1: base.after(t1), t2: base.after(t2), expiry: base.after(lease) }) + }; + Lease { address, prefix_len: len, router, dns, server: self.server, base, timers } + } +} diff --git a/toyos-dhcp/src/message.rs b/toyos-dhcp/src/message.rs new file mode 100644 index 00000000000..41d48d4356a --- /dev/null +++ b/toyos-dhcp/src/message.rs @@ -0,0 +1,224 @@ +//! DHCP messages (RFC 2131 §2, RFC 2132, RFC 3396): the client's, built to one layout with its +//! options in one order (§D2), and a server's, read by §D3.1's checks 1 to 7 in order. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::net::Ipv4Addr; + +use toyos_net_wire::ethernet::MacAddr; + +use crate::{limits, Counter, HostName}; + +const COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; +const OPTIONS_AT: usize = 240; +const SNAME: core::ops::Range = 44..108; +const FILE: core::ops::Range = 108..236; +/// The codes the client reads from a server; every other code is skipped whatever it holds. +const READ: [u8; 12] = [1, 3, 6, 50, 51, 52, 53, 54, 58, 59, 61, 80]; +/// What the client asks for, and nothing it does not use (RFC 7844 §3.6). +const PARAMETERS: [u8; 6] = [1, 3, 6, 51, 58, 59]; +const OVERLOAD: u8 = 52; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Kind { + Discover, + Request, + Decline, +} + +/// One message the client sends; which options it carries is §D2.2's table. +pub(crate) struct Build<'a> { + pub kind: Kind, + pub xid: u32, + pub secs: u16, + pub ciaddr: Ipv4Addr, + pub server: Option, + pub requested: Option, + pub mac: MacAddr, + pub client_id: &'a [u8], + pub host_name: Option<&'a HostName>, +} + +fn option(out: &mut Vec, code: u8, data: &[u8]) { + out.push(code); + out.push(u8::try_from(data.len()).unwrap_or(u8::MAX)); + out.extend_from_slice(data); +} + +impl Build<'_> { + /// The payload: fixed fields, the cookie, the options in §D2.2's order, END, and zero bytes to + /// at least 300 in all (RFC 1542 §2.1). + pub fn bytes(&self) -> Vec { + let mut out = Vec::with_capacity(limits::MIN_SENT); + out.extend_from_slice(&[1, 1, 6, 0]); + out.extend_from_slice(&self.xid.to_be_bytes()); + out.extend_from_slice(&self.secs.to_be_bytes()); + out.extend_from_slice(&[0, 0]); + out.extend_from_slice(&self.ciaddr.octets()); + out.extend_from_slice(&[0; 12]); + out.extend_from_slice(&self.mac.0); + out.resize(OPTIONS_AT.saturating_sub(COOKIE.len()), 0); + out.extend_from_slice(&COOKIE); + let kind = match self.kind { + Kind::Discover => 1, + Kind::Request => 3, + Kind::Decline => 4, + }; + option(&mut out, 53, &[kind]); + if let Some(server) = self.server { + option(&mut out, 54, &server.octets()); + } + if let Some(requested) = self.requested { + option(&mut out, 50, &requested.octets()); + } + option(&mut out, 61, self.client_id); + if self.kind != Kind::Decline { + option(&mut out, 57, &limits::MAX_MESSAGE.to_be_bytes()); + option(&mut out, 55, &PARAMETERS); + if let Some(name) = self.host_name { + option(&mut out, 12, name.bytes()); + } + } + if self.kind == Kind::Discover { + option(&mut out, 80, &[]); + } + out.push(255); + if out.len() < limits::MIN_SENT { + out.resize(limits::MIN_SENT, 0); + } + out + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ReplyKind { + Offer, + Ack, + Nak, +} + +/// A server message that passed checks 1 to 7, its options joined per RFC 3396 §7 and each read +/// one checked for its length. +#[derive(Debug)] +pub(crate) struct Reply { + pub kind: ReplyKind, + pub xid: u32, + pub yiaddr: Ipv4Addr, + /// Fields that ended without END (L-D2). + pub no_end: u64, + pub server: Option, + pub lease: Option, + pub mask: Option, + pub routers: Vec, + pub dns: Vec, + pub t1: Option, + pub t2: Option, + pub client_id: Option>, + pub rapid: bool, +} + +/// Walks one field, joining every instance of a read code in order; `true` when END closed it. +fn walk(field: &[u8], found: &mut BTreeMap>, overload_here: bool) -> Result { + let mut rest = field; + loop { + match rest { + [] => return Ok(false), + [0, after @ ..] => rest = after, + [255, ..] => return Ok(true), + [code, after @ ..] => { + let (&len, data) = after.split_first().ok_or(Counter::OptionTruncated)?; + let (value, next) = data.split_at_checked(usize::from(len)).ok_or(Counter::OptionTruncated)?; + if *code == OVERLOAD && !overload_here { + return Err(Counter::OverloadInvalid); + } + if READ.contains(code) { + found.entry(*code).or_default().extend_from_slice(value); + } + rest = next; + } + } + } +} + +fn address(bytes: &[u8]) -> Option { + bytes.first_chunk::<4>().map(|&a| Ipv4Addr::from(a)) +} + +fn number(bytes: &[u8]) -> Option { + bytes.first_chunk::<4>().map(|&n| u32::from_be_bytes(n)) +} + +fn addresses(bytes: &[u8]) -> Vec { + bytes.as_chunks::<4>().0.iter().map(|&a| Ipv4Addr::from(a)).collect() +} + +impl Reply { + pub fn parse(bytes: &[u8], mac: MacAddr) -> Result { + let (fixed, options) = bytes.split_at_checked(OPTIONS_AT).ok_or(Counter::Truncated)?; + let (&[op, htype, hlen, _, x0, x1, x2, x3], _) = fixed.split_first_chunk::<8>().ok_or(Counter::Truncated)?; + if op != 2 { + return Err(Counter::NotReply); + } + if (htype, hlen) != (1, 6) { + return Err(Counter::HardwareType); + } + if fixed.get(28..34) != Some(&mac.0[..]) { + return Err(Counter::ChaddrMismatch); + } + if fixed.get(236..240) != Some(&COOKIE[..]) { + return Err(Counter::BootpReply); + } + let mut found = BTreeMap::new(); + let mut no_end = u64::from(!walk(options, &mut found, true)?); + let overload = match found.get(&OVERLOAD).map(Vec::as_slice) { + None => 0, + Some(&[value @ 1..=3]) => value, + Some(_) => return Err(Counter::OverloadInvalid), + }; + for (bit, range) in [(1, FILE), (2, SNAME)] { + if overload & bit != 0 { + let field = fixed.get(range).ok_or(Counter::Truncated)?; + no_end = no_end.saturating_add(u64::from(!walk(field, &mut found, false)?)); + } + } + for (code, value) in &found { + let len = value.len(); + let fits = match code { + 53 => len == 1, + 1 | 50 | 51 | 54 | 58 | 59 => len == 4, + 3 | 6 => len > 0 && len.is_multiple_of(4), + 80 => len == 0, + 61 => len >= 2, + _ => true, + }; + if !fits { + return Err(Counter::OptionLength); + } + } + let kind = match found.get(&53).map(Vec::as_slice) { + None => return Err(Counter::MessageTypeMissing), + Some(&[2]) => ReplyKind::Offer, + Some(&[5]) => ReplyKind::Ack, + Some(&[6]) => ReplyKind::Nak, + Some(&[1 | 3 | 4 | 7 | 8]) => return Err(Counter::WrongDirection), + Some(&[9]) => return Err(Counter::Forcerenew), + Some(_) => return Err(Counter::MessageTypeUnsupported), + }; + let get = |code: u8| found.get(&code).map(Vec::as_slice); + Ok(Self { + kind, + xid: u32::from_be_bytes([x0, x1, x2, x3]), + yiaddr: fixed.get(16..20).and_then(address).unwrap_or(Ipv4Addr::UNSPECIFIED), + no_end, + server: get(54).and_then(address), + lease: get(51).and_then(number), + mask: get(1).and_then(address), + routers: get(3).map(addresses).unwrap_or_default(), + dns: get(6).map(addresses).unwrap_or_default(), + t1: get(58).and_then(number), + t2: get(59).and_then(number), + client_id: get(61).map(<[u8]>::to_vec), + rapid: get(80).is_some(), + }) + } +} diff --git a/toyos-dhcp/tests/ack.rs b/toyos-dhcp/tests/ack.rs new file mode 100644 index 00000000000..066cce4a096 --- /dev/null +++ b/toyos-dhcp/tests/ack.rs @@ -0,0 +1,131 @@ +//! §D8, §D10: acknowledgement, conflict detection, decline and rapid commit. + +mod common; + + + +use common::*; +use toyos_dhcp::{AddressRequest, Config, Counter, Event, Lease, Peer, Phase, Timers}; + +fn requesting() -> D { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d +} + +fn probing() -> D { + let mut d = requesting(); + d.receive(20, &ack()); + d +} + +fn fixture_db_lease(base: u64) -> Lease { + Lease { + address: A, + prefix_len: 24, + router: Some(R), + dns: vec![DNS, DNS2], + server: R, + base: at(base), + timers: Some(Timers { t1: at(base + 1_800_000), t2: at(base + 3_150_000), expiry: at(base + 3_600_000) }), + } +} + +#[test] +fn s_dhcp_dh_011_an_ack_is_probed_first() { + let mut d = requesting(); + let out = d.receive(20, &ack()); + assert_eq!(out.address, Some(AddressRequest::Probe { address: A, prefix_len: 24 })); + assert_eq!((out.transmit, out.config), (None, None)); + assert_eq!(d.client.phase(), Phase::Probing); + assert_eq!(d.deadline(), None); +} + +#[test] +fn s_dhcp_dh_012_verified_configures() { + let mut d = probing(); + let out = d.verified(5_020); + assert_eq!(out.config, Some(Config::Configured(fixture_db_lease(10)))); + assert_eq!(out.transmit, None); + assert_eq!(d.client.phase(), Phase::Bound); + assert_eq!(d.deadline(), Some(1_800_010)); +} + +#[test] +fn s_dhcp_dh_013_a_conflict_declines() { + let mut d = probing(); + d.draws.push_back(0x0dec_11e0); + let out = d.conflict(3_000, MAC_B); + let t = sent(&out); + assert_eq!(t.payload, payload_of(V_DHCP_DECLINE)); + assert_eq!(framed(t), hex(V_DHCP_DECLINE)); + assert_eq!(out.config, None); + assert_eq!(d.count(Counter::Declined), 1); + assert_eq!(d.events, [Event::Refused { rule: Counter::Declined, peer: Peer::Conflict { address: A, mac: MAC_B } }]); + assert_eq!(d.client.phase(), Phase::BackingOff); + d.draws.extend([0x2222_2222, 1_000]); + assert!(d.timer(12_999).transmit.is_none()); + let again = d.timer(13_000); + assert_eq!((message_type(sent(&again)), xid_secs(sent(&again)).0), (1, 0x2222_2222)); + assert_eq!(d.deadline(), Some(17_000)); +} + +#[test] +fn s_dhcp_dh_014_every_decline_waits_ten_seconds() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + let mut t = 0; + for round in 0..11u32 { + let xid = d.client.xid().unwrap(); + d.receive(t + 10, &server(xid, A, &offer_options())); + d.receive(t + 20, &server(xid, A, &ack_options())); + assert_eq!(d.client.phase(), Phase::Probing, "round {round}"); + let declined = d.conflict(t + 30, MAC_B); + assert_eq!(message_type(sent(&declined)), 4); + assert!(d.timer(t + 30 + 9_999).transmit.is_none()); + let discover = d.timer(t + 30 + 10_000); + assert_eq!(message_type(sent(&discover)), 1, "round {round}: DISCOVER 10 s after the DECLINE"); + t += 10_030; + } + assert_eq!(d.count(Counter::Declined), 11); +} + +#[test] +fn s_dhcp_dh_015_rapid_commit() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let out = d.receive(10, &hex(V_DHCP_ACK_RAPID)); + assert_eq!(probe_of(&out), Some(A)); + assert_eq!(out.transmit, None); + let configured = d.verified(5_010); + let Some(Config::Configured(lease)) = configured.config else { panic!("{configured:?}") }; + assert_eq!((lease.server, lease.base), (R, at(0))); + assert_eq!(lease.timers, Some(Timers { t1: at(1_800_000), t2: at(3_150_000), expiry: at(3_600_000) })); +} + +#[test] +fn s_dhcp_dh_016_an_ack_without_rapid_commit_while_selecting() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let out = d.receive(10, &ack()); + assert_eq!(out, toyos_dhcp::Output::default()); + assert_eq!(d.count(Counter::UnexpectedAck), 1); + assert_eq!(d.client.phase(), Phase::Selecting); +} + +#[test] +fn s_dhcp_dh_017_a_rapid_ack_answers_a_request_too() { + let mut d = requesting(); + let out = d.receive(20, &hex(V_DHCP_ACK_RAPID)); + assert_eq!(probe_of(&out), Some(A)); + assert_eq!(d.client.phase(), Phase::Probing); +} + +#[test] +fn s_dhcp_dh_018_the_lease_runs_from_the_first_request() { + let mut d = requesting(); + let again = d.timer(4_010); + assert_eq!(message_type(sent(&again)), 3); + d.receive(4_020, &ack()); + let out = d.verified(9_020); + let Some(Config::Configured(lease)) = out.config else { panic!() }; + assert_eq!(lease.base, at(10)); + assert_eq!(lease.timers.unwrap().t1, at(1_800_010)); +} diff --git a/toyos-dhcp/tests/common/mod.rs b/toyos-dhcp/tests/common/mod.rs new file mode 100644 index 00000000000..b968140abc5 --- /dev/null +++ b/toyos-dhcp/tests/common/mod.rs @@ -0,0 +1,573 @@ +//! Fixtures DS and DB, a scripted random source, the §V vectors, server messages built here and +//! checked against those vectors, and the stack a transmission is framed by. + +#![allow(dead_code)] + +use std::collections::VecDeque; +use std::net::Ipv4Addr; + +use toyos_dhcp::{AddressRequest, Client, Config, Counter, Event, HostName, Lease, Output, Phase, Transmission}; +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +use toyos_net_wire::Instant; + +pub const MAC_A: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0a]); +pub const MAC_B: MacAddr = MacAddr([2, 0, 0, 0, 0, 0x0b]); +pub const MAC_R: MacAddr = MacAddr([2, 0, 0, 0, 0, 0xfe]); +pub const A: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1); +pub const R: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 254); +pub const OTHER_SERVER: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 253); +pub const DNS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 53); +pub const DNS2: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 53); +pub const XID: u32 = 0x9a3c_21f7; +pub const CLIENT_ID: [u8; 15] = [0xff, 0, 0, 0, 0x0a, 0, 3, 0, 1, 2, 0, 0, 0, 0, 0x0a]; + +pub fn hex(text: &str) -> Vec { + text.split_whitespace().map(|b| u8::from_str_radix(b, 16).unwrap()).collect() +} + +pub fn at(ms: u64) -> Instant { + Instant::from_millis(ms) +} + +/// The DHCP payload of a frame vector. +pub fn payload_of(frame: &str) -> Vec { + hex(frame)[42..].to_vec() +} + +/// A server message: op 2, the given xid and yiaddr, chaddr A, the cookie, then `options` and END. +pub fn server(xid: u32, yiaddr: Ipv4Addr, options: &[(u8, Vec)]) -> Vec { + let mut m = vec![2, 1, 6, 0]; + m.extend_from_slice(&xid.to_be_bytes()); + m.extend_from_slice(&[0; 8]); + m.extend_from_slice(&yiaddr.octets()); + m.extend_from_slice(&[0; 8]); + m.extend_from_slice(&MAC_A.0); + m.resize(236, 0); + m.extend_from_slice(&[0x63, 0x82, 0x53, 0x63]); + for (code, data) in options { + m.push(*code); + m.push(data.len() as u8); + m.extend_from_slice(data); + } + m.push(255); + m +} + +pub fn ip(a: Ipv4Addr) -> Vec { + a.octets().to_vec() +} + +pub fn seconds(s: u32) -> Vec { + s.to_be_bytes().to_vec() +} + +/// V-DHCP-OFFER's options, which the vectors below are checked against. +pub fn offer_options() -> Vec<(u8, Vec)> { + vec![(53, vec![2]), (54, ip(R)), (51, seconds(3_600)), (1, vec![255, 255, 255, 0]), (3, ip(R)), (6, ip(DNS))] +} + +pub fn ack_options() -> Vec<(u8, Vec)> { + let mut o = offer_options(); + o[0] = (53, vec![5]); + o[5] = (6, [ip(DNS), ip(DNS2)].concat()); + o.push((61, CLIENT_ID.to_vec())); + o +} + +/// An OFFER from `options` with one option replaced, added or removed. +pub fn with(options: Vec<(u8, Vec)>, code: u8, data: Option>) -> Vec<(u8, Vec)> { + let mut o: Vec<(u8, Vec)> = options.into_iter().filter(|(c, _)| *c != code).collect(); + if let Some(data) = data { + o.push((code, data)); + } + o +} + +pub fn offer() -> Vec { + server(XID, A, &offer_options()) +} + +pub fn ack() -> Vec { + server(XID, A, &ack_options()) +} + +pub fn ack_with(xid: u32, options: Vec<(u8, Vec)>) -> Vec { + server(xid, A, &options) +} + +pub fn nak(xid: u32, server_id: Ipv4Addr) -> Vec { + server(xid, Ipv4Addr::UNSPECIFIED, &[(53, vec![6]), (54, ip(server_id))]) +} + +pub struct D { + pub client: Client, + pub draws: VecDeque, + pub now: u64, + pub events: Vec, +} + +impl D { + /// Fixture DS: MAC A, host name "toyos", `start` at t = 0 with the scripted draws; every + /// draw past the script is 1,000, a zero jitter. + pub fn ds(draws: &[u32]) -> (Self, Output) { + Self::named(draws, HostName::new("toyos")) + } + + pub fn named(draws: &[u32], host: Option) -> (Self, Output) { + let mut draws: VecDeque = draws.iter().copied().collect(); + let (client, out) = Client::start(at(0), IndividualMac::new(MAC_A).unwrap(), host, || draws.pop_front().unwrap_or(1_000)); + (Self { client, draws, now: 0, events: Vec::new() }, out) + } + + /// Fixture DB: bound to 192.0.2.1/24 through 192.0.2.254, base 10, verified at 5,020. + pub fn db() -> Self { + let (mut d, _) = Self::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d.receive(20, &ack()); + let out = d.verified(5_020); + assert!(matches!(out.config, Some(Config::Configured(_)))); + assert_eq!(d.client.phase(), Phase::Bound); + d.client.drain_events().for_each(drop); + d + } + + pub fn receive(&mut self, t: u64, payload: &[u8]) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.receive(at(t), payload, R, || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + pub fn timer(&mut self, t: u64) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.timer(at(t), || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + pub fn verified(&mut self, t: u64) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.verified(at(t), || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + pub fn conflict(&mut self, t: u64, mac: MacAddr) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.conflict(at(t), mac, || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + pub fn not_verified(&mut self, t: u64) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.not_verified(at(t), || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + pub fn link_up(&mut self, t: u64) -> Output { + self.now = t; + let mut draws = std::mem::take(&mut self.draws); + let out = self.client.link_up(at(t), || draws.pop_front().unwrap_or(1_000)); + self.draws = draws; + self.collect(); + out + } + + /// Fires every deadline up to `t` at its own time; every transmission, with its time. + pub fn run(&mut self, t: u64) -> Vec<(u64, Output)> { + let mut out = Vec::new(); + while let Some(d) = self.client.next_deadline().filter(|d| *d <= at(t)) { + let ms = d.nanos() / 1_000_000; + out.push((ms, self.timer(ms))); + } + self.now = t; + out + } + + fn collect(&mut self) { + self.events.extend(self.client.drain_events()); + } + + pub fn count(&self, counter: Counter) -> u64 { + self.client.counters().get(counter) + } + + pub fn logged(&self, rule: Counter) -> usize { + self.events.iter().filter(|Event::Refused { rule: r, .. }| *r == rule).count() + } + + pub fn deadline(&self) -> Option { + self.client.next_deadline().map(|d| d.nanos() / 1_000_000) + } + + pub fn lease(&self) -> &Lease { + self.client.lease().expect("a lease is in use") + } +} + +/// The xid and secs a transmission carries. +pub fn xid_secs(t: &Transmission) -> (u32, u16) { + let p = &t.payload; + (u32::from_be_bytes([p[4], p[5], p[6], p[7]]), u16::from_be_bytes([p[8], p[9]])) +} + +pub fn sent(out: &Output) -> &Transmission { + out.transmit.as_ref().expect("a transmission") +} + +pub fn message_type(t: &Transmission) -> u8 { + assert_eq!(t.payload[240..243], [53, 1, t.payload[242]]); + t.payload[242] +} + +pub fn probe_of(out: &Output) -> Option { + match out.address { + Some(AddressRequest::Probe { address, .. }) => Some(address), + _ => None, + } +} + +pub fn ms(i: Instant) -> u64 { + i.nanos() / 1_000_000 +} + +/// Frames `t` as the stack sends it: `toyos-net-ip` with 192.0.2.1/24 assigned and the router +/// resolved, the acquisition socket of `toyos-net-udp` on port 68. +pub fn framed(t: &Transmission) -> Vec { + use toyos_net_ip::{Ip, Sent, FRAME}; + let mut ip = Ip::new(Instant::from_nanos(0), [0x5a; 16]); + let if0 = ip.add_interface(Instant::from_nanos(0), IndividualMac::new(MAC_A).unwrap()); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + ip.add_address(Instant::from_nanos(0), if0, A, 24).unwrap(); + let setup = |ip: &mut Ip, until: Instant| { + while let Some(d) = ip.next_deadline().filter(|d| *d <= until) { + ip.fire(d); + ip.transmit(d, usize::MAX, |_, _| {}); + } + }; + setup(&mut ip, at(3_000)); + let now = at(3_000); + let _ = ip.resolve(now, if0, R); + ip.transmit(now, usize::MAX, |_, _| {}); + let mut reply = MAC_A.0.to_vec(); + reply.extend_from_slice(&MAC_R.0); + reply.extend_from_slice(&[8, 6, 0, 1, 8, 0, 6, 4, 0, 2]); + reply.extend_from_slice(&MAC_R.0); + reply.extend_from_slice(&R.octets()); + reply.extend_from_slice(&MAC_A.0); + reply.extend_from_slice(&A.octets()); + let _ = ip.receive(now, if0, &reply); + ip.transmit(now, usize::MAX, |_, _| {}); + let mut udp = toyos_net_udp::Udp::new(); + let socket = udp.bind(&ip, Ipv4Addr::UNSPECIFIED, toyos_net_wire::Port::new(68), || 0).unwrap(); + udp.set_acquisition(socket).unwrap(); + udp.set_broadcast(socket, true).unwrap(); + udp.send_from(&mut ip, socket, t.source, t.destination, 67, &t.payload).unwrap(); + let mut frame = Vec::new(); + udp.transmit(1, |out| { + let mut buf = [0u8; FRAME]; + match ip.send_udp(now, out, &mut buf) { + Ok(Sent::Frame(n)) => { + frame = buf[..n].to_vec(); + true + } + other => panic!("{other:?}"), + } + }); + frame +} + +pub const V_DHCP_DISCOVER: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 39 a6 00 00 00 00 ff ff +ff ff 00 44 00 43 01 34 13 19 01 01 06 00 9a 3c +21 f7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 01 3d 0f ff +00 00 00 0a 00 03 00 01 02 00 00 00 00 0a 39 02 +05 c0 37 06 01 03 06 33 3a 3b 0c 05 74 6f 79 6f +73 50 00 ff 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_REQUEST: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 39 a6 00 00 00 00 ff ff +ff ff 00 44 00 43 01 34 08 7c 01 01 06 00 9a 3c +21 f7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 03 36 04 c0 +00 02 fe 32 04 c0 00 02 01 3d 0f ff 00 00 00 0a +00 03 00 01 02 00 00 00 00 0a 39 02 05 c0 37 06 +01 03 06 33 3a 3b 0c 05 74 6f 79 6f 73 ff 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_RENEW: &str = " +02 00 00 00 00 fe 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 b4 a5 c0 00 02 01 c0 00 +02 fe 00 44 00 43 01 34 af ee 01 01 06 00 0b ad +ca fe 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 03 3d 0f ff +00 00 00 0a 00 03 00 01 02 00 00 00 00 0a 39 02 +05 c0 37 06 01 03 06 33 3a 3b 0c 05 74 6f 79 6f +73 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_REBIND: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 77 a4 c0 00 02 01 ff ff +ff ff 00 44 00 43 01 34 2e a4 01 01 06 00 1c 0f +fe e5 00 00 00 00 c0 00 02 01 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 03 3d 0f ff +00 00 00 0a 00 03 00 01 02 00 00 00 00 0a 39 02 +05 c0 37 06 01 03 06 33 3a 3b 0c 05 74 6f 79 6f +73 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_REBOOT: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 39 a6 00 00 00 00 ff ff +ff ff 00 44 00 43 01 34 4a 66 01 01 06 00 5e ed +1e 55 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 03 32 04 c0 +00 02 01 3d 0f ff 00 00 00 0a 00 03 00 01 02 00 +00 00 00 0a 39 02 05 c0 37 06 01 03 06 33 3a 3b +0c 05 74 6f 79 6f 73 ff 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_DECLINE: &str = " +ff ff ff ff ff ff 02 00 00 00 00 0a 08 00 45 00 +01 48 00 00 40 00 40 11 39 a6 00 00 00 00 ff ff +ff ff 00 44 00 43 01 34 c9 ff 01 01 06 00 0d ec +11 e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 04 36 04 c0 +00 02 fe 32 04 c0 00 02 01 3d 0f ff 00 00 00 0a +00 03 00 01 02 00 00 00 00 0a ff 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00"; + +pub const V_DHCP_OFFER: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +c0 00 02 01 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 02 36 04 c0 00 02 fe 33 04 00 00 0e 10 01 +04 ff ff ff 00 03 04 c0 00 02 fe 06 04 c0 00 02 +35 ff"; + +pub const V_DHCP_OFFER_FRAME: &str = " +02 00 00 00 00 0a 02 00 00 00 00 fe 08 00 45 00 +01 2e 00 00 40 00 40 11 b4 bf c0 00 02 fe c0 00 +02 01 00 43 00 44 01 1a a8 44 02 01 06 00 9a 3c +21 f7 00 00 00 00 00 00 00 00 c0 00 02 01 00 00 +00 00 00 00 00 00 02 00 00 00 00 0a 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 63 82 53 63 35 01 02 36 04 c0 +00 02 fe 33 04 00 00 0e 10 01 04 ff ff ff 00 03 +04 c0 00 02 fe 06 04 c0 00 02 35 ff"; + +pub const V_DHCP_ACK: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +c0 00 02 01 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 05 36 04 c0 00 02 fe 33 04 00 00 0e 10 01 +04 ff ff ff 00 03 04 c0 00 02 fe 06 08 c0 00 02 +35 c6 33 64 35 3d 0f ff 00 00 00 0a 00 03 00 01 +02 00 00 00 00 0a ff"; + +pub const V_DHCP_ACK_RAPID: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +c0 00 02 01 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 05 36 04 c0 00 02 fe 33 04 00 00 0e 10 01 +04 ff ff ff 00 03 04 c0 00 02 fe 06 04 c0 00 02 +35 50 00 ff"; + +pub const V_DHCP_NAK: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 06 36 04 c0 00 02 fe ff"; + +pub const V_DHCP_OFFER_OVERLOAD: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +c0 00 02 01 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 36 04 c0 00 +02 fe 33 04 00 00 0e 10 01 04 ff ff ff 00 ff 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 02 34 01 01 ff"; + +pub const V_DHCP_OFFER_SPLIT: &str = " +02 01 06 00 9a 3c 21 f7 00 00 00 00 00 00 00 00 +c0 00 02 01 00 00 00 00 00 00 00 00 02 00 00 00 +00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 +35 01 02 36 04 c0 00 02 fe 33 04 00 00 0e 10 01 +04 ff ff ff 00 06 03 c0 00 02 06 05 35 c6 33 64 +35 ff"; diff --git a/toyos-dhcp/tests/lease.rs b/toyos-dhcp/tests/lease.rs new file mode 100644 index 00000000000..6078db6777d --- /dev/null +++ b/toyos-dhcp/tests/lease.rs @@ -0,0 +1,294 @@ +//! §D5–§D7 and §D11: renewing, rebinding, expiry, NAKs and the lease's times. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_dhcp::{Config, Counter, Phase, Reason, Timers}; + +/// Fixture DB's ACK re-sent as `xid`, with options changed. +fn renewal_ack(xid: u32, change: impl FnOnce(&mut Vec<(u8, Vec)>)) -> Vec { + let mut options = ack_options(); + change(&mut options); + let mut m = server(xid, A, &options); + m[12..16].copy_from_slice(&A.octets()); + m +} + +fn renewing() -> D { + let mut d = D::db(); + d.draws.push_back(0x0bad_cafe); + d.timer(1_800_010); + d +} + +fn set(options: &mut [(u8, Vec)], code: u8, data: Vec) { + options.iter_mut().find(|(c, _)| *c == code).unwrap().1 = data; +} + +#[test] +fn s_dhcp_dh_019_the_first_renew() { + let mut d = D::db(); + d.draws.push_back(0x0bad_cafe); + let out = d.timer(1_800_010); + let t = sent(&out); + assert_eq!(t.payload, payload_of(V_DHCP_RENEW)); + assert_eq!((t.source, t.destination), (A, R)); + assert_eq!(framed(t), hex(V_DHCP_RENEW), "unicast to the server identifier, framed to the router"); + assert_eq!(d.client.phase(), Phase::Renewing); + assert_eq!(d.deadline(), Some(2_475_010)); +} + +#[test] +fn s_dhcp_dh_020_renew_rebind_expire() { + let mut d = D::db(); + d.draws.extend((1..=20u32).flat_map(|n| [0x1000 + n])); + let out = d.run(3_600_010); + let renews: Vec<(u64, u16)> = out.iter().filter(|(_, o)| o.transmit.as_ref().is_some_and(|t| t.destination == R)).map(|(t, o)| (*t, xid_secs(sent(o)).1)).collect(); + assert_eq!(renews, [(1_800_010, 0), (2_475_010, 675), (2_812_510, 1_012), (2_981_260, 1_181), (3_065_635, 1_265), (3_125_635, 1_325)]); + let rebinds: Vec<(u64, u16)> = out + .iter() + .filter(|(_, o)| o.transmit.as_ref().is_some_and(|t| t.destination == Ipv4Addr::BROADCAST && t.source == A)) + .map(|(t, o)| (*t, xid_secs(sent(o)).1)) + .collect(); + assert_eq!(rebinds, [(3_150_010, 1_350), (3_375_010, 1_575), (3_487_510, 1_687), (3_547_510, 1_747)]); + let xids: std::collections::BTreeSet = out.iter().filter_map(|(_, o)| o.transmit.as_ref()).map(|t| xid_secs(t).0).collect(); + assert_eq!(xids.len(), out.len(), "a fresh xid per transmission"); + let (last_at, last) = out.last().unwrap(); + assert_eq!(*last_at, 3_600_010); + assert_eq!(last.config, Some(Config::Deconfigured(Reason::Expired))); + assert_eq!(message_type(sent(last)), 1); + assert_eq!(d.logged(Counter::LeaseExpired), 1); +} + +#[test] +fn s_dhcp_dh_021_t2_alone_means_rebinding_at_t1() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + let mut options = ack_options(); + options.push((59, seconds(1_000))); + d.receive(20, &server(XID, A, &options)); + let out = d.verified(5_020); + let Some(Config::Configured(lease)) = out.config else { panic!() }; + assert_eq!(lease.timers.map(|t| (t.t1, t.t2)), Some((at(1_000_010), at(1_000_010)))); + d.draws.push_back(0x1c0f_fee5); + let rebind = d.timer(1_000_010); + let t = sent(&rebind); + assert_eq!(t.payload, payload_of(V_DHCP_REBIND)); + assert_eq!(framed(t), hex(V_DHCP_REBIND)); + assert_eq!(d.client.phase(), Phase::Rebinding); +} + +#[test] +fn s_dhcp_dh_022_extended() { + let mut d = renewing(); + let out = d.receive(1_800_500, &renewal_ack(0x0bad_cafe, |_| {})); + let Some(Config::Extended(lease)) = out.config else { panic!("{out:?}") }; + assert_eq!(lease.base, at(1_800_010)); + assert_eq!(lease.timers, Some(Timers { t1: at(3_600_010), t2: at(4_950_010), expiry: at(5_400_010) })); + assert_eq!(d.client.phase(), Phase::Bound); +} + +#[test] +fn s_dhcp_dh_023_reconfigured() { + let mut d = renewing(); + let out = d.receive(1_800_500, &renewal_ack(0x0bad_cafe, |o| set(o, 6, ip(Ipv4Addr::new(192, 0, 2, 54))))); + let Some(Config::Reconfigured(lease)) = out.config else { panic!("{out:?}") }; + assert_eq!((lease.dns.as_slice(), lease.address, lease.router), (&[Ipv4Addr::new(192, 0, 2, 54)][..], A, Some(R))); +} + +#[test] +fn s_dhcp_dh_024_an_ack_for_another_address() { + let mut d = renewing(); + let mut m = renewal_ack(0x0bad_cafe, |_| {}); + m[16..20].copy_from_slice(&[192, 0, 2, 7]); + assert_eq!(d.receive(1_800_500, &m), toyos_dhcp::Output::default()); + assert_eq!(d.logged(Counter::AckAddressChanged), 1); + assert_eq!((d.client.phase(), d.deadline()), (Phase::Renewing, Some(2_475_010))); +} + +#[test] +fn s_dhcp_dh_025_an_ack_from_another_server() { + let mut d = renewing(); + d.receive(1_800_500, &renewal_ack(0x0bad_cafe, |o| set(o, 54, ip(OTHER_SERVER)))); + assert_eq!(d.logged(Counter::AckWrongServer), 1); + assert_eq!(d.client.phase(), Phase::Renewing); +} + +#[test] +fn s_dhcp_dh_026_rebinding_takes_any_server() { + let mut d = D::db(); + d.run(3_150_009); + let rebind = d.timer(3_150_010); + let xid = xid_secs(sent(&rebind)).0; + let out = d.receive(3_200_000, &renewal_ack(xid, |o| set(o, 54, ip(OTHER_SERVER)))); + let Some(Config::Extended(lease)) = out.config else { panic!("{out:?}") }; + assert_eq!(lease.server, OTHER_SERVER); + assert_eq!(lease.timers.unwrap().t1, at(3_150_010 + 1_800_000)); + let renew = d.timer(3_150_010 + 1_800_000); + assert_eq!(sent(&renew).destination, OTHER_SERVER); +} + +#[test] +fn s_dhcp_dh_027_a_nak_while_renewing() { + let mut d = renewing(); + d.draws.extend([0x3333_3333, 1_000]); + let out = d.receive(1_800_500, &nak(0x0bad_cafe, R)); + assert_eq!(out.config, Some(Config::Deconfigured(Reason::Refused))); + assert_eq!(d.logged(Counter::Nak), 1); + assert_eq!((message_type(sent(&out)), xid_secs(sent(&out)).0), (1, 0x3333_3333)); +} + +#[test] +fn s_dhcp_dh_028_a_nak_from_another_server() { + let mut d = renewing(); + d.receive(1_800_500, &nak(0x0bad_cafe, OTHER_SERVER)); + assert_eq!(d.logged(Counter::NakWrongServer), 1); + assert_eq!(d.client.phase(), Phase::Renewing); +} + +#[test] +fn s_dhcp_dh_029_a_nak_while_requesting() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d.draws.extend([0x4444_4444, 1_000]); + let out = d.receive(20, &hex(V_DHCP_NAK)); + assert_eq!(out.config, None); + assert_eq!((message_type(sent(&out)), xid_secs(sent(&out)).0), (1, 0x4444_4444)); +} + +#[test] +fn s_dhcp_dh_030_a_nak_loop_is_slowed() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let mut t = 0; + let mut delays = Vec::new(); + for _ in 0..7 { + let xid = d.client.xid().unwrap(); + d.receive(t + 10, &server(xid, A, &offer_options())); + let naked = t + 20; + let immediate = d.receive(naked, &nak(xid, R)); + let restarted = match immediate.transmit { + Some(_) => naked, + None => { + let at = d.deadline().unwrap(); + assert_eq!(message_type(sent(&d.timer(at))), 1); + at + } + }; + delays.push(restarted - naked); + t = restarted; + } + assert_eq!(delays, [0, 4_000, 8_000, 16_000, 32_000, 64_000, 64_000]); + assert_eq!(d.count(Counter::NakBackoff), 6); + let xid = d.client.xid().unwrap(); + d.receive(t + 10, &server(xid, A, &offer_options())); + d.receive(t + 20, &server(xid, A, &ack_options())); + d.verified(t + 30); + let renew = d.timer(t + 30 + 1_800_000); + let xid = xid_secs(sent(&renew)).0; + assert!(d.receive(t + 30 + 1_800_100, &nak(xid, R)).transmit.is_some(), "after a BOUND the next NAK restarts at once"); +} + +#[test] +fn s_dhcp_dh_031_a_late_timer_rebinds_once() { + let mut d = D::db(); + let out = d.timer(3_200_000); + let t = sent(&out); + assert_eq!((t.source, t.destination), (A, Ipv4Addr::BROADCAST)); + assert_eq!(d.client.phase(), Phase::Rebinding); + assert_eq!(d.deadline(), Some(3_400_005)); + assert_eq!(d.count(Counter::TxRequest), 2, "the REQUEST that bound, and one REBIND"); +} + +#[test] +fn s_dhcp_dh_032_a_later_timer_expires() { + let mut d = D::db(); + let out = d.timer(3_700_000); + assert_eq!(out.config, Some(Config::Deconfigured(Reason::Expired))); + assert_eq!(message_type(sent(&out)), 1); + assert_eq!(d.count(Counter::TxRequest), 1); +} + +fn times(t1: Option, t2: Option) -> (Timers, u64) { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + let mut options = ack_options(); + options.extend(t1.map(|t| (58, seconds(t)))); + options.extend(t2.map(|t| (59, seconds(t)))); + d.receive(20, &server(XID, A, &options)); + let Some(Config::Configured(lease)) = d.verified(5_020).config else { panic!() }; + (lease.timers.unwrap(), d.count(Counter::TimerOptionInvalid)) +} + +#[test] +fn s_dhcp_dh_033_timer_options() { + let s = |secs: u64| at(10 + secs * 1_000); + let check = |(t, invalid): (Timers, u64), t1: u64, t2: u64, n: u64| { + assert_eq!((t.t1, t.t2, invalid), (s(t1), s(t2), n)); + }; + check(times(Some(600), Some(900)), 600, 900, 0); + check(times(Some(900), Some(600)), 600, 600, 1); + check(times(None, Some(3_600)), 1_800, 3_150, 1); + check(times(Some(0), None), 1_800, 3_150, 1); + check(times(Some(1_800), None), 1_800, 3_150, 0); +} + +#[test] +fn s_dhcp_dh_034_an_infinite_lease() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + let mut options = ack_options(); + set(&mut options, 51, seconds(u32::MAX)); + options.push((58, seconds(600))); + d.receive(20, &server(XID, A, &options)); + let Some(Config::Configured(lease)) = d.verified(5_020).config else { panic!() }; + assert_eq!(lease.timers, None); + assert_eq!(d.deadline(), None); + assert_eq!(d.count(Counter::TimerOptionInvalid), 1); + assert_eq!(d.timer(u64::MAX / 2_000_000), toyos_dhcp::Output::default()); + assert_eq!(d.client.phase(), Phase::Bound); +} + +#[test] +fn s_dhcp_dh_035_a_ten_second_lease() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + let mut options = ack_options(); + set(&mut options, 51, seconds(10)); + d.receive(20, &server(XID, A, &options)); + assert!(matches!(d.verified(4_020).config, Some(Config::Configured(_)))); + let out = d.run(10_010); + let at: Vec = out.iter().map(|(t, _)| *t).collect(); + assert_eq!(at, [5_010, 8_760, 10_010]); + assert_eq!(sent(&out[0].1).destination, R); + assert_eq!(sent(&out[1].1).destination, Ipv4Addr::BROADCAST); + assert_eq!(out[2].1.config, Some(Config::Deconfigured(Reason::Expired))); + assert_eq!(message_type(sent(&out[2].1)), 1); +} + +#[test] +fn s_dhcp_dh_036_a_lease_that_expired_while_probed() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + let mut options = ack_options(); + set(&mut options, 51, seconds(3)); + d.receive(20, &server(XID, A, &options)); + let out = d.verified(5_020); + assert_eq!(out.config, None, "never configured"); + assert_eq!(d.logged(Counter::LeaseExpired), 1); + assert_eq!(message_type(sent(&out)), 1); +} + +#[test] +fn s_dhcp_dh_037_each_renewal_has_its_own_xid() { + let mut d = renewing(); + d.draws.push_back(0x2222_2222); + let second = d.timer(2_475_010); + assert_eq!(xid_secs(sent(&second)).0, 0x2222_2222); + d.receive(2_475_100, &renewal_ack(0x0bad_cafe, |_| {})); + assert_eq!(d.count(Counter::XidMismatch), 1); + let out = d.receive(2_475_200, &renewal_ack(0x2222_2222, |_| {})); + let Some(Config::Extended(lease)) = out.config else { panic!("{out:?}") }; + assert_eq!(lease.base, at(2_475_010)); +} diff --git a/toyos-dhcp/tests/link.rs b/toyos-dhcp/tests/link.rs new file mode 100644 index 00000000000..1b2c6cc1803 --- /dev/null +++ b/toyos-dhcp/tests/link.rs @@ -0,0 +1,126 @@ +//! §D8 and §D9: link changes, INIT-REBOOT and lost addresses. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_dhcp::{AddressRequest, Config, Counter, Phase, Reason}; + +fn rebooting() -> D { + let mut d = D::db(); + d.draws.extend([0x5eed_1e55, 1_000]); + d.link_up(100_000); + d +} + +fn reboot_ack(xid: u32, yiaddr: Ipv4Addr) -> Vec { + server(xid, yiaddr, &with(ack_options(), 54, Some(ip(OTHER_SERVER)))) +} + +#[test] +fn s_dhcp_dh_064_link_up_with_a_lease() { + let mut d = D::db(); + d.draws.extend([0x5eed_1e55, 1_000]); + let out = d.link_up(100_000); + let t = sent(&out); + assert_eq!(t.payload, payload_of(V_DHCP_REBOOT)); + assert_eq!(framed(t), hex(V_DHCP_REBOOT)); + assert_eq!((out.config, out.address), (None, None), "the address stays configured; [ip] announces it"); + assert_eq!(d.client.phase(), Phase::Rebooting); + assert_eq!(d.client.lease().map(|l| l.address), Some(A)); + assert_eq!(d.deadline(), Some(104_000)); +} + +#[test] +fn s_dhcp_dh_065_the_reboot_is_acknowledged() { + let mut d = rebooting(); + let out = d.receive(100_050, &reboot_ack(0x5eed_1e55, A)); + let Some(Config::Extended(lease)) = out.config else { panic!("{out:?}") }; + assert_eq!((lease.base, lease.timers.unwrap().t1), (at(100_000), at(1_900_000))); + assert_eq!(d.client.phase(), Phase::Bound); +} + +#[test] +fn s_dhcp_dh_066_the_reboot_is_refused() { + let mut d = rebooting(); + let out = d.receive(100_050, &nak(0x5eed_1e55, R)); + assert_eq!(out.config, Some(Config::Deconfigured(Reason::Refused))); + assert_eq!(message_type(sent(&out)), 1); +} + +#[test] +fn s_dhcp_dh_067_an_unanswered_reboot_keeps_the_lease() { + let mut d = rebooting(); + let out = d.run(128_000); + assert_eq!(out.iter().map(|(t, _)| *t).collect::>(), [104_000, 112_000, 128_000]); + assert!(out.iter().all(|(_, o)| message_type(sent(o)) == 3)); + let give_up = d.timer(160_000); + assert_eq!(give_up, toyos_dhcp::Output::default()); + assert_eq!(d.count(Counter::RebootUnanswered), 1); + assert_eq!(d.client.phase(), Phase::Bound); + let timers = d.lease().timers.unwrap(); + assert_eq!((timers.t1, timers.expiry), (at(1_800_010), at(3_600_010))); + assert_eq!(d.deadline(), Some(1_800_010)); +} + +#[test] +fn s_dhcp_dh_068_the_lease_expires_while_rebooting() { + let mut d = D::db(); + d.link_up(3_590_000); + let out = d.run(3_600_010); + let (t, last) = out.last().unwrap(); + assert_eq!(*t, 3_600_010); + assert_eq!(last.config, Some(Config::Deconfigured(Reason::Expired))); + assert_eq!(message_type(sent(last)), 1); +} + +#[test] +fn s_dhcp_dh_069_a_reboot_ack_for_another_address() { + let mut d = rebooting(); + d.receive(100_050, &reboot_ack(0x5eed_1e55, Ipv4Addr::new(192, 0, 2, 7))); + assert_eq!(d.logged(Counter::AckAddressChanged), 1); + assert_eq!(d.client.phase(), Phase::Rebooting); +} + +#[test] +fn s_dhcp_dh_070_link_up_while_selecting() { + let (mut d, _) = D::ds(&[XID, 1_000, 0x5555_5555, 1_000]); + let out = d.link_up(2_000); + assert_eq!(xid_secs(sent(&out)), (0x5555_5555, 0)); + assert_eq!(d.deadline(), Some(6_000)); +} + +#[test] +fn s_dhcp_dh_071_link_up_while_probing() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d.receive(20, &ack()); + let out = d.link_up(30); + assert_eq!(out.address, Some(AddressRequest::Cancel(A))); + assert_eq!(message_type(sent(&out)), 1); + assert_eq!(d.client.phase(), Phase::Selecting); +} + +#[test] +fn s_dhcp_dh_072_an_address_lost_in_use() { + let mut d = D::db(); + d.draws.push_back(0x0dec_11e0); + let out = d.conflict(500_000, MAC_B); + assert_eq!(out.config, Some(Config::Deconfigured(Reason::Conflict))); + assert_eq!(sent(&out).payload, payload_of(V_DHCP_DECLINE)); + assert_eq!(d.client.phase(), Phase::BackingOff); + assert_eq!(d.deadline(), Some(510_000)); + assert_eq!(message_type(sent(&d.timer(510_000))), 1); +} + +#[test] +fn s_dhcp_dh_076_the_link_went_down_while_probing() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000, 0x6666_6666, 1_000]); + d.receive(10, &offer()); + d.receive(20, &ack()); + let out = d.not_verified(40); + assert_eq!(out.config, None); + assert_eq!(xid_secs(sent(&out)), (0x6666_6666, 0)); + assert_eq!(message_type(sent(&out)), 1); +} diff --git a/toyos-dhcp/tests/parse.rs b/toyos-dhcp/tests/parse.rs new file mode 100644 index 00000000000..d00c0089773 --- /dev/null +++ b/toyos-dhcp/tests/parse.rs @@ -0,0 +1,309 @@ +//! §D3 and §D4: reading and validating server messages. Each input reaches a client in +//! SELECTING after DH-01 unless it says otherwise. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_dhcp::{Config, Counter, Output, Phase}; + +fn selecting() -> D { + D::ds(&[XID, 1_000]).0 +} + +/// Delivers `message` to a selecting client: the refusal it counted and whether it logged one. +fn refused(message: &[u8], rule: Counter) { + let mut d = selecting(); + let out = d.receive(10, message); + assert_eq!(out, Output::default(), "{rule:?}"); + assert_eq!(d.count(rule), 1, "{rule:?}"); + assert_eq!(d.logged(rule), usize::from(rule.logged()), "{rule:?}"); + assert_eq!(d.client.phase(), Phase::Selecting); +} + +/// Delivers `message`: a REQUEST must follow. +fn accepted(message: &[u8]) -> D { + let mut d = selecting(); + let out = d.receive(10, message); + assert_eq!(message_type(sent(&out)), 3, "a REQUEST follows"); + d +} + +fn offer_with(code: u8, data: Option>) -> Vec { + server(XID, A, &with(offer_options(), code, data)) +} + +fn edited(change: impl FnOnce(&mut Vec)) -> Vec { + let mut m = offer(); + change(&mut m); + m +} + +#[test] +fn s_dhcp_dh_038_truncated() { + refused(&offer()[..239], Counter::Truncated); +} + +#[test] +fn s_dhcp_dh_039_not_a_reply() { + refused(&edited(|m| m[0] = 1), Counter::NotReply); + refused( + &edited(|m| { + m[0] = 1; + m[28..34].copy_from_slice(&MAC_B.0); + }), + Counter::NotReply, + ); +} + +#[test] +fn s_dhcp_dh_040_the_hardware_type() { + refused(&edited(|m| m[1] = 6), Counter::HardwareType); + refused(&edited(|m| m[2] = 16), Counter::HardwareType); +} + +#[test] +fn s_dhcp_dh_041_another_clients_reply() { + refused(&edited(|m| m[28..34].copy_from_slice(&MAC_B.0)), Counter::ChaddrMismatch); + accepted(&edited(|m| m[34..44].fill(0xff))); +} + +#[test] +fn s_dhcp_dh_042_another_transaction() { + refused(&edited(|m| m[7] = 0xf8), Counter::XidMismatch); +} + +#[test] +fn s_dhcp_dh_043_bootp_is_refused() { + refused(&edited(|m| m[236..240].fill(0)), Counter::BootpReply); + refused( + &edited(|m| { + m[236..240].fill(0); + m[7] = 0xf8; + }), + Counter::BootpReply, + ); +} + +#[test] +fn s_dhcp_dh_044_past_end_and_padding() { + assert_eq!(offer(), hex(V_DHCP_OFFER), "the harness builds the vector"); + accepted(&[offer(), vec![0xee; 8]].concat()); + let mut padded = offer(); + padded.pop(); + padded.resize(1_471, 0); + padded.push(255); + assert_eq!(padded.len(), 1_472); + accepted(&padded); +} + +#[test] +fn s_dhcp_dh_045_an_option_past_its_field() { + let mut m = offer(); + let n = m.len(); + m.truncate(n - 7); + m.extend_from_slice(&[6, 5, 192, 0, 2, 0x35]); + refused(&m, Counter::OptionTruncated); +} + +#[test] +fn s_dhcp_dh_046_no_end() { + let mut m = offer(); + m.pop(); + assert_eq!(m.len(), 273); + let d = accepted(&m); + assert_eq!(d.count(Counter::NoEnd), 1); +} + +#[test] +fn s_dhcp_dh_047_a_repeated_message_type() { + let mut m = offer(); + m.pop(); + m.extend_from_slice(&[53, 1, 2, 255]); + refused(&m, Counter::OptionLength); +} + +#[test] +fn s_dhcp_dh_048_option_lengths() { + for (code, data) in [(54, vec![192, 0, 2, 254, 0]), (1, vec![255, 255, 255]), (3, vec![0; 6]), (6, vec![]), (51, vec![0, 1])] { + refused(&offer_with(code, Some(data)), Counter::OptionLength); + } +} + +#[test] +fn s_dhcp_dh_049_split_options_are_joined() { + accepted(&hex(V_DHCP_OFFER_SPLIT)); + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &hex(V_DHCP_OFFER_SPLIT)); + let mut split = hex(V_DHCP_OFFER_SPLIT); + split[242] = 5; + d.receive(20, &split); + let Some(Config::Configured(lease)) = d.verified(5_020).config else { panic!() }; + assert_eq!(lease.dns, [DNS, DNS2]); +} + +#[test] +fn s_dhcp_dh_050_an_overloaded_file_field() { + let mut d = selecting(); + let out = d.receive(10, &hex(V_DHCP_OFFER_OVERLOAD)); + assert_eq!(sent(&out).payload, payload_of(V_DHCP_REQUEST)); +} + +#[test] +fn s_dhcp_dh_051_overload_misused() { + let overload = hex(V_DHCP_OFFER_OVERLOAD); + let mut value_4 = overload.clone(); + value_4[245] = 4; + refused(&value_4, Counter::OverloadInvalid); + let n = overload.len(); + let mut length_2 = overload[..n - 1].to_vec(); + length_2[244] = 2; + length_2.extend_from_slice(&[0, 255]); + refused(&length_2, Counter::OverloadInvalid); + let mut nested = overload; + nested[126..130].copy_from_slice(&[52, 1, 1, 255]); + refused(&nested, Counter::OverloadInvalid); +} + +#[test] +fn s_dhcp_dh_052_message_types() { + refused(&offer_with(53, None), Counter::MessageTypeMissing); + refused(&offer_with(53, Some(vec![1])), Counter::WrongDirection); + refused(&offer_with(53, Some(vec![3])), Counter::WrongDirection); + refused(&offer_with(53, Some(vec![10])), Counter::MessageTypeUnsupported); + let mut d = D::db(); + d.receive(100_000, &server(0x1234_5678, A, &with(offer_options(), 53, Some(vec![9])))); + assert_eq!((d.count(Counter::Forcerenew), d.count(Counter::XidMismatch)), (1, 0)); + assert_eq!(d.logged(Counter::Forcerenew), 1); +} + +#[test] +fn s_dhcp_dh_053_a_foreign_client_identifier() { + let mut id = CLIENT_ID.to_vec(); + id[14] = 0x0b; + refused(&offer_with(61, Some(id)), Counter::ClientIdMismatch); +} + +#[test] +fn s_dhcp_dh_054_the_server_identifier() { + refused(&offer_with(54, None), Counter::NoServerId); + for bad in [Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST, Ipv4Addr::new(224, 0, 0, 1), Ipv4Addr::new(127, 0, 0, 1)] { + refused(&offer_with(54, Some(ip(bad))), Counter::ServerIdInvalid); + } +} + +#[test] +fn s_dhcp_dh_055_yiaddr_must_be_a_host() { + for bad in [ + Ipv4Addr::UNSPECIFIED, + Ipv4Addr::new(0, 1, 2, 3), + Ipv4Addr::new(127, 0, 0, 1), + Ipv4Addr::new(169, 254, 1, 1), + Ipv4Addr::new(224, 0, 0, 251), + Ipv4Addr::new(240, 0, 0, 1), + Ipv4Addr::BROADCAST, + ] { + refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid); + } +} + +#[test] +fn s_dhcp_dh_056_the_lease_time() { + refused(&offer_with(51, None), Counter::NoLeaseTime); + refused(&offer_with(51, Some(seconds(0))), Counter::LeaseZero); +} + +#[test] +fn s_dhcp_dh_057_the_mask() { + refused(&offer_with(1, None), Counter::NoSubnetMask); + for bad in [[255, 0, 255, 0], [0, 0, 0, 0], [255, 255, 255, 255]] { + refused(&offer_with(1, Some(bad.to_vec())), Counter::MaskInvalid); + } + let slash_31 = with(with(offer_options(), 1, Some(vec![255, 255, 255, 254])), 3, None); + accepted(&server(XID, Ipv4Addr::new(192, 0, 2, 0), &slash_31)); + accepted(&offer_with(1, Some(vec![128, 0, 0, 0]))); +} + +#[test] +fn s_dhcp_dh_058_network_and_broadcast_yiaddr() { + for bad in [Ipv4Addr::new(192, 0, 2, 0), Ipv4Addr::new(192, 0, 2, 255)] { + refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid); + } + let slash_30 = with(offer_options(), 1, Some(vec![255, 255, 255, 252])); + refused(&server(XID, Ipv4Addr::new(192, 0, 2, 3), &slash_30), Counter::YiaddrInvalid); + accepted(&server(XID, A, &slash_30)); +} + +fn configured(ack_options: Vec<(u8, Vec)>) -> (D, toyos_dhcp::Lease) { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d.receive(20, &server(XID, A, &ack_options)); + let Some(Config::Configured(lease)) = d.verified(5_020).config else { panic!() }; + (d, lease) +} + +#[test] +fn s_dhcp_dh_059_the_first_usable_router() { + let routers = [ip(Ipv4Addr::UNSPECIFIED), ip(Ipv4Addr::new(198, 51, 100, 1)), ip(A), ip(R)].concat(); + let (d, lease) = configured(with(ack_options(), 3, Some(routers))); + assert_eq!(lease.router, Some(R)); + assert_eq!((d.count(Counter::RouterInvalid), d.logged(Counter::RouterInvalid)), (3, 3)); + let (_, lease) = configured(with(ack_options(), 3, Some(ip(Ipv4Addr::new(198, 51, 100, 1))))); + assert_eq!(lease.router, None); + assert_eq!(lease.address, A); +} + +#[test] +fn s_dhcp_dh_060_at_most_three_resolvers() { + let dns = [ + Ipv4Addr::UNSPECIFIED, + DNS, + Ipv4Addr::new(224, 0, 0, 1), + Ipv4Addr::new(192, 0, 2, 255), + DNS2, + Ipv4Addr::new(192, 0, 2, 54), + Ipv4Addr::new(192, 0, 2, 55), + ]; + let (d, lease) = configured(with(ack_options(), 6, Some(dns.iter().flat_map(|a| a.octets()).collect()))); + assert_eq!(lease.dns, [DNS, DNS2, Ipv4Addr::new(192, 0, 2, 54)]); + assert_eq!((d.count(Counter::DnsInvalid), d.count(Counter::DnsTruncated)), (3, 1)); +} + +#[test] +fn s_dhcp_dh_061_identity_while_requesting() { + let requesting = || { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d + }; + let mut d = requesting(); + d.receive(20, &server(XID, A, &with(ack_options(), 54, Some(ip(OTHER_SERVER))))); + assert_eq!((d.logged(Counter::AckWrongServer), d.client.phase()), (1, Phase::Requesting)); + let mut d = requesting(); + d.receive(20, &server(XID, Ipv4Addr::new(192, 0, 2, 7), &ack_options())); + assert_eq!((d.logged(Counter::AckAddressChanged), d.client.phase()), (1, Phase::Requesting)); + let mut d = requesting(); + d.receive(20, &nak(XID, OTHER_SERVER)); + assert_eq!((d.logged(Counter::NakWrongServer), d.client.phase()), (1, Phase::Requesting)); +} + +#[test] +fn s_dhcp_dh_062_offers_nobody_asked_for() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + d.receive(10, &offer()); + d.receive(15, &server(XID, A, &with(offer_options(), 54, Some(ip(OTHER_SERVER))))); + assert_eq!((d.count(Counter::UnexpectedOffer), d.client.phase()), (1, Phase::Requesting)); + let mut d = D::db(); + d.receive(100_000, &hex(V_DHCP_OFFER)); + assert_eq!(d.count(Counter::XidMismatch), 1); +} + +#[test] +fn s_dhcp_dh_063_a_refused_offer_then_another() { + let mut d = selecting(); + d.receive(10, &offer_with(51, Some(seconds(0)))); + let out = d.receive(20, &server(XID, A, &with(offer_options(), 54, Some(ip(OTHER_SERVER))))); + let payload = &sent(&out).payload; + assert_eq!(payload[243..249], [54, 4, 192, 0, 2, 253]); +} diff --git a/toyos-dhcp/tests/props.rs b/toyos-dhcp/tests/props.rs new file mode 100644 index 00000000000..4f510e716ca --- /dev/null +++ b/toyos-dhcp/tests/props.rs @@ -0,0 +1,199 @@ +//! DH-73 and DH-74: invariants over seeded random runs. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_dhcp::{Config, Counter, HostName, Output, Transmission}; + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn below(&mut self, n: u64) -> u64 { + self.next() % n + } +} + +/// Refusals of a whole message: a message names at most one. +const REFUSALS: [Counter; 27] = [ + Counter::Truncated, + Counter::NotReply, + Counter::HardwareType, + Counter::ChaddrMismatch, + Counter::BootpReply, + Counter::OptionTruncated, + Counter::OverloadInvalid, + Counter::OptionLength, + Counter::MessageTypeMissing, + Counter::WrongDirection, + Counter::Forcerenew, + Counter::MessageTypeUnsupported, + Counter::XidMismatch, + Counter::ClientIdMismatch, + Counter::NoServerId, + Counter::ServerIdInvalid, + Counter::UnexpectedOffer, + Counter::UnexpectedAck, + Counter::UnexpectedNak, + Counter::YiaddrInvalid, + Counter::NoLeaseTime, + Counter::LeaseZero, + Counter::NoSubnetMask, + Counter::MaskInvalid, + Counter::AckWrongServer, + Counter::AckAddressChanged, + Counter::NakWrongServer, +]; + +fn refusals(d: &D) -> u64 { + REFUSALS.iter().map(|&c| d.count(c)).sum() +} + +/// A server message for the client's transaction or another, sometimes mutated. +fn message(rng: &mut Rng, xid: u32) -> Vec { + let xid = if rng.below(4) == 0 { rng.next() as u32 } else { xid }; + let mut m = match rng.below(4) { + 0 => server(xid, A, &offer_options()), + 1 => server(xid, A, &ack_options()), + 2 => nak(xid, R), + _ => server(xid, A, &with(ack_options(), 80, Some(vec![]))), + }; + match rng.below(5) { + 0 => { + let at = rng.below(m.len() as u64) as usize; + m[at] ^= 1 << rng.below(8); + } + 1 => m.truncate(rng.below(m.len() as u64) as usize), + _ => {} + } + m +} + +struct Seen { + transmissions: Vec, + configured: usize, +} + +/// One seeded run; checks DH-73's invariants as it goes and keeps every transmission. +fn run(seed: u64, host: Option) -> Seen { + let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1); + let (mut d, first) = D::named(&[], host); + d.draws = (0..10_000).map(|_| rng.next() as u32).collect(); + let mut seen = Seen { transmissions: first.transmit.into_iter().collect(), configured: 0 }; + let mut configured: Option = None; + let mut now = 0u64; + for _ in 0..600 { + let span = if rng.below(3) == 0 { 2_000_000 } else { 30_000 }; + now += rng.below(span); + let due = d.client.next_deadline().is_some_and(|t| t <= at(now)); + let before = refusals(&d); + let (out, verified_call): (Output, bool) = match rng.below(8) { + _ if due => (d.timer(if rng.below(2) == 0 { now } else { d.client.next_deadline().unwrap().nanos().div_ceil(1_000_000) }), false), + 0 => (d.verified(now), true), + 1 => (d.conflict(now, MAC_B), false), + 2 => (d.not_verified(now), false), + 3 => (d.link_up(now), false), + _ => { + let m = message(&mut rng, d.client.xid().unwrap_or(0)); + let out = d.receive(now, &m); + assert!(refusals(&d) - before <= 1, "seed {seed}: a message names at most one refusal"); + (out, false) + } + }; + now = d.now; + if let Some(Config::Configured(_)) = &out.config { + seen.configured += 1; + assert!(verified_call, "seed {seed}: configured without verified"); + } + match &out.config { + Some(Config::Configured(l) | Config::Extended(l) | Config::Reconfigured(l)) => configured = Some(l.address), + Some(Config::Deconfigured(_)) => configured = None, + None => {} + } + if let Some(t) = &out.transmit { + let kind = message_type(t); + if t.source.is_unspecified() { + let ciaddr = Ipv4Addr::new(t.payload[12], t.payload[13], t.payload[14], t.payload[15]); + assert!(matches!(kind, 1 | 3 | 4), "seed {seed}"); + assert!(ciaddr.is_unspecified(), "seed {seed}: only RENEWING and REBINDING send from the lease"); + } else { + assert_eq!(Some(t.source), configured, "seed {seed}: sent from an address not configured"); + } + seen.transmissions.push(t.clone()); + } + if let Some(deadline) = d.client.next_deadline() { + assert!(deadline > at(d.now), "seed {seed}: a deadline not after now"); + } + } + seen +} + +#[test] +fn s_dhcp_dh_073_prop_invariants() { + let (mut configured, mut from_lease) = (0, 0); + for seed in 1..=64 { + let seen = run(seed, HostName::new("toyos")); + configured += seen.configured; + from_lease += seen.transmissions.iter().filter(|t| !t.source.is_unspecified()).count(); + } + assert!(configured > 50 && from_lease > 50, "the runs reach leases: {configured} configured, {from_lease} sent from one"); +} + +/// Walks a built message's options: each code and its data, until END. +fn options(payload: &[u8]) -> (Vec<(u8, Vec)>, usize) { + let mut at = 240; + let mut found = Vec::new(); + while payload[at] != 255 { + let (code, len) = (payload[at], usize::from(payload[at + 1])); + found.push((code, payload[at + 2..at + 2 + len].to_vec())); + at += 2 + len; + } + (found, at) +} + +#[test] +fn s_dhcp_dh_074_prop_every_message_is_built_one_way() { + const ORDER: [u8; 8] = [53, 54, 50, 61, 57, 55, 12, 80]; + for (seed, host) in (1..=32).map(|s| (s, if s % 2 == 0 { HostName::new("toyos") } else { None })) { + let named = host.is_some(); + for t in run(seed, host).transmissions { + let p = &t.payload; + assert_eq!(p.len(), 300, "seed {seed}"); + let (found, end) = options(p); + assert!(p[end + 1..].iter().all(|b| *b == 0), "zero padding"); + let codes: Vec = found.iter().map(|(c, _)| *c).collect(); + let order: Vec = ORDER.iter().copied().filter(|c| codes.contains(c)).collect(); + assert_eq!(codes, order, "seed {seed}: §D2.2's order"); + let kind = found[0].1[0]; + let ciaddr = Ipv4Addr::new(p[12], p[13], p[14], p[15]); + let expected: Vec = match (kind, codes.contains(&54), codes.contains(&50), ciaddr.is_unspecified()) { + (1, ..) => vec![53, 61, 57, 55, 12, 80], + (3, true, true, true) => vec![53, 54, 50, 61, 57, 55, 12], + (3, false, true, true) => vec![53, 50, 61, 57, 55, 12], + (3, false, false, false) => vec![53, 61, 57, 55, 12], + (4, true, true, true) => vec![53, 54, 50, 61], + other => panic!("seed {seed}: {other:?}"), + }; + let expected: Vec = expected.into_iter().filter(|c| *c != 12 || named).collect(); + assert_eq!(codes, expected, "seed {seed}: the options of type {kind}"); + for (code, data) in &found { + match code { + 61 => assert_eq!(data, &CLIENT_ID), + 57 => assert_eq!(data, &[5, 0xc0]), + 55 => assert_eq!(data, &[1, 3, 6, 51, 58, 59]), + 12 => assert_eq!(data, b"toyos"), + 80 => assert!(data.is_empty()), + _ => {} + } + } + } + } +} diff --git a/toyos-dhcp/tests/stack.rs b/toyos-dhcp/tests/stack.rs new file mode 100644 index 00000000000..d17bd6fa6ed --- /dev/null +++ b/toyos-dhcp/tests/stack.rs @@ -0,0 +1,109 @@ +//! DH-07 and DH-75: server messages reaching the client through `toyos-net-ip` and +//! `toyos-net-udp`, as netd will compose them. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{Cast, Delivery, Ip}; +use toyos_net_udp::{SocketId, Udp, Verdict}; +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +use toyos_net_wire::{Instant, Port}; + +fn sum(data: &[u8]) -> u16 { + let mut s: u32 = 0; + for chunk in data.chunks(2) { + s += if chunk.len() == 2 { u32::from(chunk[0]) << 8 | u32::from(chunk[1]) } else { u32::from(chunk[0]) << 8 }; + while s > 0xffff { + s = (s & 0xffff) + (s >> 16); + } + } + s as u16 +} + +/// A UDP datagram 67 → 68 around `payload`, checksums by the sum above, framed from the router. +fn from_server(to_mac: MacAddr, destination: Ipv4Addr, dport: u16, payload: &[u8]) -> Vec { + let length = 8 + payload.len(); + let mut udp = vec![0, 67, (dport >> 8) as u8, dport as u8, (length >> 8) as u8, length as u8, 0, 0]; + udp.extend_from_slice(payload); + let mut pseudo = R.octets().to_vec(); + pseudo.extend_from_slice(&destination.octets()); + pseudo.extend_from_slice(&[0, 17, (length >> 8) as u8, length as u8]); + pseudo.extend_from_slice(&udp); + udp[6..8].copy_from_slice(&(!sum(&pseudo)).to_be_bytes()); + let total = 20 + length; + let mut ip = vec![0x45, 0, (total >> 8) as u8, total as u8, 0, 0, 0x40, 0, 64, 17, 0, 0]; + ip.extend_from_slice(&R.octets()); + ip.extend_from_slice(&destination.octets()); + let c = !sum(&ip); + ip[10..12].copy_from_slice(&c.to_be_bytes()); + ip.extend_from_slice(&udp); + let mut frame = to_mac.0.to_vec(); + frame.extend_from_slice(&MAC_R.0); + frame.extend_from_slice(&[8, 0]); + frame.extend_from_slice(&ip); + frame +} + +struct Stack { + ip: Ip, + udp: Udp, + socket: SocketId, + if0: toyos_net_ip::IfIndex, +} + +impl Stack { + /// A's interface up with no address, netd's acquisition socket on port 68. + fn new() -> Self { + let mut ip = Ip::new(Instant::from_nanos(0), [0x5a; 16]); + let if0 = ip.add_interface(Instant::from_nanos(0), IndividualMac::new(MAC_A).unwrap()); + ip.link_up(Instant::from_nanos(0), if0).unwrap(); + let mut udp = Udp::new(); + let socket = udp.bind(&ip, Ipv4Addr::UNSPECIFIED, Port::new(68), || 0).unwrap(); + udp.set_acquisition(socket).unwrap(); + Self { ip, udp, socket, if0 } + } + + /// A frame arrives: what reached the socket, and how [ip] admitted it. + fn deliver(&mut self, now: u64, frame: &[u8]) -> Option<(Vec, Cast)> { + let Some(Delivery::Udp(arrival, datagram)) = self.ip.receive(at(now), self.if0, frame) else { return None }; + assert_eq!(self.udp.receive(at(now), &arrival, &datagram), Verdict::Delivered); + let mut buf = [0u8; 1_500]; + let r = self.udp.recv(self.socket, &mut buf).unwrap().unwrap(); + Some((buf[..r.len].to_vec(), arrival.cast)) + } +} + +#[test] +fn s_dhcp_dh_007_either_framing_of_the_offer() { + let broadcast = from_server(MacAddr::BROADCAST, Ipv4Addr::BROADCAST, 68, &offer()); + for (frame, cast) in [(broadcast, Cast::LimitedBroadcast), (hex(V_DHCP_OFFER_FRAME), Cast::Acquisition)] { + let mut stack = Stack::new(); + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + let (payload, admitted) = stack.deliver(10, &frame).expect("reaches the socket"); + assert_eq!(admitted, cast); + let out = d.receive(10, &payload); + assert_eq!(sent(&out).payload, payload_of(V_DHCP_REQUEST)); + } +} + +#[test] +fn s_dhcp_dh_075_the_acquisition_exception_ends_with_an_address() { + let mut stack = Stack::new(); + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + let (payload, cast) = stack.deliver(10, &hex(V_DHCP_OFFER_FRAME)).expect("admitted"); + assert_eq!(cast, Cast::Acquisition); + assert_eq!(message_type(sent(&d.receive(10, &payload))), 3); + + stack.ip.add_address(at(20), stack.if0, A, 24).unwrap(); + while let Some(deadline) = stack.ip.next_deadline().filter(|t| *t <= at(3_000)) { + stack.ip.fire(deadline); + stack.ip.transmit(deadline, usize::MAX, |_, _| {}); + } + assert!(stack.ip.is_assigned(A)); + let before = stack.ip.counters().get(toyos_net_ip::Counter::IpNotForUs); + let elsewhere = from_server(MAC_A, Ipv4Addr::new(192, 0, 2, 7), 68, &offer()); + assert!(stack.deliver(3_000, &elsewhere).is_none()); + assert_eq!(stack.ip.counters().get(toyos_net_ip::Counter::IpNotForUs), before + 1); +} diff --git a/toyos-dhcp/tests/tx.rs b/toyos-dhcp/tests/tx.rs new file mode 100644 index 00000000000..033e735369c --- /dev/null +++ b/toyos-dhcp/tests/tx.rs @@ -0,0 +1,112 @@ +//! §D2 and §D6: building and retransmitting. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_dhcp::{HostName, Phase}; + +#[test] +fn s_dhcp_dh_001_the_first_discover() { + let (d, out) = D::ds(&[XID, 1_000]); + let t = sent(&out); + assert_eq!(t.payload, payload_of(V_DHCP_DISCOVER)); + assert_eq!((t.source, t.destination), (Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST)); + assert_eq!(framed(t), hex(V_DHCP_DISCOVER)); + assert_eq!(d.client.phase(), Phase::Selecting); + assert_eq!(d.deadline(), Some(4_000)); +} + +#[test] +fn s_dhcp_dh_002_the_host_name() { + let (_, out) = D::named(&[XID, 1_000], None); + let with_name = payload_of(V_DHCP_DISCOVER); + let name = [0x0c, 5, b't', b'o', b'y', b'o', b's']; + let at = with_name.windows(7).position(|w| w == name).unwrap(); + let mut expected = [&with_name[..at], &with_name[at + 7..]].concat(); + expected.extend_from_slice(&[0; 7]); + assert_eq!(sent(&out).payload, expected); + for bad in ["", &"a".repeat(64), "a_b", "-ab", "ab-"] { + assert!(HostName::new(bad).is_none(), "{bad:?}"); + } + for good in ["toyos-t14", "9lives"] { + assert!(HostName::new(good).is_some(), "{good}"); + } +} + +#[test] +fn s_dhcp_dh_003_the_discover_schedule() { + let (mut d, out) = D::ds(&[XID, 1_000, 0, 2_000, 1_000, 1_000, 1_000]); + let mut sent_at = vec![(0, xid_secs(sent(&out)))]; + sent_at.extend(d.run(188_000).iter().map(|(t, o)| (*t, xid_secs(sent(o))))); + let times: Vec = sent_at.iter().map(|(t, _)| *t).collect(); + assert_eq!(times, [0, 4_000, 11_000, 28_000, 60_000, 124_000, 188_000]); + assert!(sent_at.iter().all(|(_, (xid, _))| *xid == XID)); + let secs: Vec = sent_at.iter().map(|(_, (_, s))| *s).collect(); + assert_eq!(secs, [0, 4, 11, 28, 60, 124, 188]); +} + +#[test] +fn s_dhcp_dh_004_jitter_offsets() { + for (draw, deadline) in [(0, 3_000), (1_000, 4_000), (2_000, 5_000), (2_001, 3_000), (0xffff_ffff, 3_885)] { + let (d, _) = D::ds(&[XID, draw]); + assert_eq!(d.deadline(), Some(deadline), "draw {draw}"); + } +} + +#[test] +fn s_dhcp_dh_005_secs_saturates() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let out = d.timer(70_000_000); + assert_eq!(xid_secs(sent(&out)).1, 65_535); +} + +#[test] +fn s_dhcp_dh_006_the_request() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + let out = d.receive(10, &offer()); + let t = sent(&out); + assert_eq!(t.payload, payload_of(V_DHCP_REQUEST)); + assert_eq!(framed(t), hex(V_DHCP_REQUEST)); + assert_eq!(d.client.phase(), Phase::Requesting); + assert_eq!(d.deadline(), Some(4_010)); +} + +#[test] +fn s_dhcp_dh_008_the_request_repeats_the_discovers_secs() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let retransmitted = d.timer(4_000); + assert_eq!(xid_secs(sent(&retransmitted)).1, 4); + let request = d.receive(4_500, &offer()); + assert_eq!(xid_secs(sent(&request)).1, 4); + let again: Vec<(u64, u16)> = d.run(32_500).iter().map(|(t, o)| (*t, xid_secs(sent(o)).1)).collect(); + assert_eq!(again, [(8_500, 4), (16_500, 4), (32_500, 4)]); +} + +#[test] +fn s_dhcp_dh_009_four_requests_then_start_over() { + let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); + let first = d.receive(10, &offer()); + assert_eq!(xid_secs(sent(&first)), (XID, 0)); + let requests = d.run(28_010); + assert_eq!(requests.iter().map(|(t, _)| *t).collect::>(), [4_010, 12_010, 28_010]); + assert!(requests.iter().all(|(_, o)| xid_secs(sent(o)) == (XID, 0) && message_type(sent(o)) == 3)); + d.draws.extend([0x1111_1111, 1_000]); + let out = d.timer(60_010); + assert_eq!(d.count(toyos_dhcp::Counter::RequestTimeout), 1); + assert_eq!(d.logged(toyos_dhcp::Counter::RequestTimeout), 1); + let t = sent(&out); + assert_eq!((message_type(t), xid_secs(t)), (1, (0x1111_1111, 0))); + assert_eq!(d.client.phase(), Phase::Selecting); + assert_eq!(d.deadline(), Some(64_010)); +} + +#[test] +fn s_dhcp_dh_010_a_slow_offer_still_counts() { + let (mut d, _) = D::ds(&[XID, 1_000]); + let again = d.timer(4_000); + assert_eq!(xid_secs(sent(&again)).0, XID); + let out = d.receive(4_100, &offer()); + assert_eq!(message_type(sent(&out)), 3); +} From c341a44c52384787b95f750d75a45b7ca7888a52 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 13:39:29 +0200 Subject: [PATCH 06/20] net stage 3: cut what no caller needs, and make the unreachable unrepresentable toyos-net-ip: FAILED carries nothing (its hold-down is the entry's deadline), so Nud::Failed is a unit variant; the accessors no test or caller read go (Ip::mac, Ip::joined, Linked/Probing/Unreachable::mac, Unreachable::requests, Failed::since), and Limiter::global_burst is private. NUD's fail takes the INCOMPLETE entry's queue from the arm that matched it rather than re-matching a state it cannot be in. toyos-net-udp and toyos-dhcp: a refusal is a Refusal drained by drain_refusals, as it was the only event either crate had. UDP's port-0 checks yield the typed port they refuse on, so no unreachable second check remains; DHCP's renew takes the lease's timers from the one caller that has them, the lease degradation is a Client method, and the two limits nothing read (JITTER, NAK_BACKOFF_MAX) go. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-dhcp/src/lib.rs | 99 +++++++++++++++---------------- toyos-dhcp/tests/ack.rs | 4 +- toyos-dhcp/tests/common/mod.rs | 12 ++-- toyos-net-ip/src/config.rs | 4 -- toyos-net-ip/src/lib.rs | 11 +--- toyos-net-ip/src/limiter.rs | 2 +- toyos-net-ip/src/nud.rs | 54 +++++------------ toyos-net-ip/src/route.rs | 2 +- toyos-net-ip/tests/addr.rs | 4 +- toyos-net-ip/tests/clk.rs | 2 +- toyos-net-ip/tests/icmp.rs | 2 +- toyos-net-ip/tests/nbr.rs | 2 +- toyos-net-ip/tests/nud.rs | 10 ++-- toyos-net-udp/src/lib.rs | 35 +++++------ toyos-net-udp/tests/common/mod.rs | 2 +- toyos-net-udp/tests/send.rs | 8 +-- 16 files changed, 103 insertions(+), 150 deletions(-) diff --git a/toyos-dhcp/src/lib.rs b/toyos-dhcp/src/lib.rs index 1e5f734d426..e9fd1f36df1 100644 --- a/toyos-dhcp/src/lib.rs +++ b/toyos-dhcp/src/lib.rs @@ -10,7 +10,7 @@ //! only thing keeping an off-link host from forging replies to this client. //! //! **Modern only.** A BOOTP reply, an unauthenticated FORCERENEW and a lease without a subnet -//! mask are refused, counted and named in an [`Event`]; DHCPINFORM and DHCPRELEASE are never sent. +//! mask are refused, counted and named in a [`Refusal`]; DHCPINFORM and DHCPRELEASE are never sent. #![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] @@ -92,19 +92,16 @@ pub mod limits { pub const FIRST_WAIT: Duration = Duration::from_secs(4); pub const MAX_WAIT: Duration = Duration::from_secs(64); - /// A wait is moved by up to this either way (RFC 2131 §4.1). - pub const JITTER: Duration = Duration::from_secs(1); pub const REQUEST_TRANSMISSIONS: u32 = 4; pub const RENEW_FLOOR: Duration = Duration::from_secs(60); pub const DECLINE_BACKOFF: Duration = Duration::from_secs(10); - pub const NAK_BACKOFF_MAX: Duration = Duration::from_secs(64); pub const MAX_DNS: usize = 3; /// The largest message the client accepts: a 1,500-byte datagram under either reading of /// RFC 2132 §9.10. pub const MAX_MESSAGE: u16 = 1_472; /// RFC 1542 §2.1's minimum; every message the client sends is padded to it. pub const MIN_SENT: usize = 300; - /// Refusal events held for the shell until it drains them. + /// Refusals held for the shell until it drains them. pub const EVENTS: usize = 1_024; } @@ -225,8 +222,9 @@ pub enum Peer { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Event { - Refused { rule: Counter, peer: Peer }, +pub struct Refusal { + pub rule: Counter, + pub peer: Peer, } #[derive(Debug)] @@ -293,7 +291,7 @@ pub struct Client { /// Consecutive NAKs since the last BOUND (§D11). naks: u32, counters: Counters, - events: Vec, + refusals: Vec, } /// Not 0.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 224.0.0.0/4 or 240.0.0.0/4: one host (§D4.1). @@ -360,7 +358,7 @@ impl Client { state: State::BackingOff(now), naks: 0, counters: Counters::default(), - events: Vec::new(), + refusals: Vec::new(), }; let out = client.begin(now, &mut draw); (client, out) @@ -371,8 +369,8 @@ impl Client { } /// Refusals to log since the last call. - pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> { - self.events.drain(..) + pub fn drain_refusals(&mut self) -> alloc::vec::Drain<'_, Refusal> { + self.refusals.drain(..) } pub fn phase(&self) -> Phase { @@ -426,10 +424,10 @@ impl Client { if !rule.logged() { return; } - if self.events.len() >= limits::EVENTS { + if self.refusals.len() >= limits::EVENTS { self.counters.add(Counter::EventOverflow, 1); } else { - self.events.push(Event::Refused { rule, peer }); + self.refusals.push(Refusal { rule, peer }); } } @@ -473,11 +471,10 @@ impl Client { } /// A RENEWING (unicast to the server) or REBINDING (broadcast) request with a fresh xid. - fn renew(&mut self, now: Instant, lease: Lease, started: Instant, rebind: bool, draw: &mut impl FnMut() -> u32) -> Output { + fn renew(&mut self, now: Instant, lease: Lease, timers: Timers, started: Instant, rebind: bool, draw: &mut impl FnMut() -> u32) -> Output { let xid = draw(); let payload = self.message(Kind::Request, xid, secs(now.since(started)), lease.address, None, None); self.counters.add(Counter::TxRequest, 1); - let Some(timers) = lease.timers else { return Output::default() }; let (destination, deadline) = if rebind { (Ipv4Addr::BROADCAST, halfway(now, timers.expiry)) } else { @@ -505,10 +502,10 @@ impl Client { return self.expire(now, lease.address, draw); } if now >= timers.t2 { - return self.renew(now, lease, started.unwrap_or(now), true, draw); + return self.renew(now, lease, timers, started.unwrap_or(now), true, draw); } if now >= timers.t1 { - return self.renew(now, lease, started.unwrap_or(now), false, draw); + return self.renew(now, lease, timers, started.unwrap_or(now), false, draw); } self.state = State::Bound(lease); Output::default() @@ -787,25 +784,25 @@ impl Client { Output { transmit: Some(transmit), ..Output::default() } } (State::Selecting(s), _) => { - let lease = offered.lease_from(s.start, peer, self); + let lease = self.lease_of(offered, s.start, peer); self.probe(lease) } (State::Requesting(r), _) if offered.server != r.server => self.keep(State::Requesting(r), Counter::AckWrongServer, peer), (State::Requesting(r), _) if offered.address != r.address => self.keep(State::Requesting(r), Counter::AckAddressChanged, peer), (State::Requesting(r), _) => { - let lease = offered.lease_from(r.first, peer, self); + let lease = self.lease_of(offered, r.first, peer); self.probe(lease) } (State::Renewing(r), _) if offered.server != r.lease.server => self.keep(State::Renewing(r), Counter::AckWrongServer, peer), (State::Renewing(r), _) if offered.address != r.lease.address => self.keep(State::Renewing(r), Counter::AckAddressChanged, peer), (State::Rebinding(r), _) if offered.address != r.lease.address => self.keep(State::Rebinding(r), Counter::AckAddressChanged, peer), (State::Renewing(r) | State::Rebinding(r), _) => { - let lease = offered.lease_from(r.sent, peer, self); + let lease = self.lease_of(offered, r.sent, peer); self.renewed(now, &r.lease, lease, draw) } (State::Rebooting(r), _) if offered.address != r.lease.address => self.keep(State::Rebooting(r), Counter::AckAddressChanged, peer), (State::Rebooting(r), _) => { - let lease = offered.lease_from(r.start, peer, self); + let lease = self.lease_of(offered, r.start, peer); self.renewed(now, &r.lease, lease, draw) } (state @ (State::Probing(_) | State::Bound(_) | State::BackingOff(_)), _) => { @@ -834,70 +831,68 @@ impl Client { let config = if same { Config::Extended(lease.clone()) } else { Config::Reconfigured(lease.clone()) }; Output { config: Some(config), ..self.bind(now, lease, draw) } } -} - -/// What an acceptable OFFER or ACK holds, before §D4.2 degrades it into a lease. -struct Offered { - address: Ipv4Addr, - prefix_len: u8, - server: Ipv4Addr, - lease: u32, - routers: Vec, - dns: Vec, - t1: Option, - t2: Option, -} -impl Offered { /// §D4.2 and §D7: the first usable router, at most three resolvers, and T1 and T2, the /// lease running from `base`. - fn lease_from(self, base: Instant, peer: Peer, client: &mut Client) -> Lease { - let (address, len) = (self.address, self.prefix_len); + fn lease_of(&mut self, offered: Offered, base: Instant, peer: Peer) -> Lease { + let (address, len) = (offered.address, offered.prefix_len); let mut router = None; - for candidate in self.routers { + for candidate in offered.routers { if host(candidate) && same_subnet(candidate, address, len) && candidate != address { router = Some(candidate); break; } - client.refuse(Counter::RouterInvalid, peer); + self.refuse(Counter::RouterInvalid, peer); } let mut dns = Vec::new(); - for server in self.dns { + for server in offered.dns { if !host(server) || Some(server) == broadcast(address, len) { - client.refuse(Counter::DnsInvalid, peer); + self.refuse(Counter::DnsInvalid, peer); } else if dns.len() < limits::MAX_DNS { dns.push(server); } else { - client.counters.add(Counter::DnsTruncated, 1); + self.counters.add(Counter::DnsTruncated, 1); } } - let timers = if self.lease == u32::MAX { - for _ in self.t1.iter().chain(self.t2.iter()) { - client.refuse(Counter::TimerOptionInvalid, peer); + let timers = if offered.lease == u32::MAX { + for _ in offered.t1.iter().chain(offered.t2.iter()) { + self.refuse(Counter::TimerOptionInvalid, peer); } None } else { - let lease = ms(self.lease); - let t2 = match self.t2 { - Some(t2) if t2 > 0 && t2 < self.lease => ms(t2), + let lease = ms(offered.lease); + let t2 = match offered.t2 { + Some(t2) if t2 > 0 && t2 < offered.lease => ms(t2), given => { if given.is_some() { - client.refuse(Counter::TimerOptionInvalid, peer); + self.refuse(Counter::TimerOptionInvalid, peer); } lease.saturating_mul(7).checked_div(8).unwrap_or(lease) } }; - let t1 = match self.t1 { + let t1 = match offered.t1 { Some(t1) if t1 > 0 && ms(t1) < t2 => ms(t1), given => { if given.is_some() { - client.refuse(Counter::TimerOptionInvalid, peer); + self.refuse(Counter::TimerOptionInvalid, peer); } lease.checked_div(2).unwrap_or(lease).min(t2) } }; Some(Timers { t1: base.after(t1), t2: base.after(t2), expiry: base.after(lease) }) }; - Lease { address, prefix_len: len, router, dns, server: self.server, base, timers } + Lease { address, prefix_len: len, router, dns, server: offered.server, base, timers } } } + +/// What an acceptable OFFER or ACK holds, before §D4.2 degrades it into a lease. +struct Offered { + address: Ipv4Addr, + prefix_len: u8, + server: Ipv4Addr, + lease: u32, + routers: Vec, + dns: Vec, + t1: Option, + t2: Option, +} diff --git a/toyos-dhcp/tests/ack.rs b/toyos-dhcp/tests/ack.rs index 066cce4a096..97e21a63ab4 100644 --- a/toyos-dhcp/tests/ack.rs +++ b/toyos-dhcp/tests/ack.rs @@ -5,7 +5,7 @@ mod common; use common::*; -use toyos_dhcp::{AddressRequest, Config, Counter, Event, Lease, Peer, Phase, Timers}; +use toyos_dhcp::{AddressRequest, Config, Counter, Lease, Peer, Phase, Refusal, Timers}; fn requesting() -> D { let (mut d, _) = D::ds(&[XID, 1_000, 1_000]); @@ -61,7 +61,7 @@ fn s_dhcp_dh_013_a_conflict_declines() { assert_eq!(framed(t), hex(V_DHCP_DECLINE)); assert_eq!(out.config, None); assert_eq!(d.count(Counter::Declined), 1); - assert_eq!(d.events, [Event::Refused { rule: Counter::Declined, peer: Peer::Conflict { address: A, mac: MAC_B } }]); + assert_eq!(d.refusals, [Refusal { rule: Counter::Declined, peer: Peer::Conflict { address: A, mac: MAC_B } }]); assert_eq!(d.client.phase(), Phase::BackingOff); d.draws.extend([0x2222_2222, 1_000]); assert!(d.timer(12_999).transmit.is_none()); diff --git a/toyos-dhcp/tests/common/mod.rs b/toyos-dhcp/tests/common/mod.rs index b968140abc5..35e472953d2 100644 --- a/toyos-dhcp/tests/common/mod.rs +++ b/toyos-dhcp/tests/common/mod.rs @@ -6,7 +6,7 @@ use std::collections::VecDeque; use std::net::Ipv4Addr; -use toyos_dhcp::{AddressRequest, Client, Config, Counter, Event, HostName, Lease, Output, Phase, Transmission}; +use toyos_dhcp::{AddressRequest, Client, Config, Counter, HostName, Lease, Output, Phase, Refusal, Transmission}; use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; use toyos_net_wire::Instant; @@ -103,7 +103,7 @@ pub struct D { pub client: Client, pub draws: VecDeque, pub now: u64, - pub events: Vec, + pub refusals: Vec, } impl D { @@ -116,7 +116,7 @@ impl D { pub fn named(draws: &[u32], host: Option) -> (Self, Output) { let mut draws: VecDeque = draws.iter().copied().collect(); let (client, out) = Client::start(at(0), IndividualMac::new(MAC_A).unwrap(), host, || draws.pop_front().unwrap_or(1_000)); - (Self { client, draws, now: 0, events: Vec::new() }, out) + (Self { client, draws, now: 0, refusals: Vec::new() }, out) } /// Fixture DB: bound to 192.0.2.1/24 through 192.0.2.254, base 10, verified at 5,020. @@ -127,7 +127,7 @@ impl D { let out = d.verified(5_020); assert!(matches!(out.config, Some(Config::Configured(_)))); assert_eq!(d.client.phase(), Phase::Bound); - d.client.drain_events().for_each(drop); + d.client.drain_refusals().for_each(drop); d } @@ -197,7 +197,7 @@ impl D { } fn collect(&mut self) { - self.events.extend(self.client.drain_events()); + self.refusals.extend(self.client.drain_refusals()); } pub fn count(&self, counter: Counter) -> u64 { @@ -205,7 +205,7 @@ impl D { } pub fn logged(&self, rule: Counter) -> usize { - self.events.iter().filter(|Event::Refused { rule: r, .. }| *r == rule).count() + self.refusals.iter().filter(|r| r.rule == rule).count() } pub fn deadline(&self) -> Option { diff --git a/toyos-net-ip/src/config.rs b/toyos-net-ip/src/config.rs index 8319a29c78c..3a285d26203 100644 --- a/toyos-net-ip/src/config.rs +++ b/toyos-net-ip/src/config.rs @@ -119,10 +119,6 @@ impl Ip { Ok(()) } - pub fn joined(&self, iface: IfIndex, group: MulticastAddr) -> bool { - self.ifaces.get(iface.0).is_some_and(|i| i.igmp.joined(group)) - } - pub fn igmp_mode(&self, iface: IfIndex) -> Option { self.ifaces.get(iface.0).map(|i| i.igmp.mode()) } diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs index 78cf7d0d9c8..1177989a18d 100644 --- a/toyos-net-ip/src/lib.rs +++ b/toyos-net-ip/src/lib.rs @@ -37,8 +37,8 @@ extern crate alloc; mod acd; mod addr; -mod config; mod arp; +mod config; mod counters; mod draw; mod egress; @@ -70,7 +70,7 @@ pub use counters::{Counter, Counters, RefusalLog}; pub use egress::{Sent, UdpOut, FRAME}; pub use igmp::IgmpMode; pub use limiter::Limiter; -pub use nud::{Failed, Incomplete, Linked, Nud, Probing, Reachable, Unreachable}; +pub use nud::{Incomplete, Linked, Nud, Probing, Reachable, Unreachable}; pub use route::{NextHop, Route, Source}; pub use toyos_net_wire::Instant; @@ -316,7 +316,6 @@ pub struct Ip { generation: u64, } - impl Ip { pub fn new(now: Instant, secret: Key) -> Self { let draws = draw::Draws::new(secret); @@ -376,10 +375,6 @@ impl Ip { self.generation } - pub fn mac(&self, iface: IfIndex) -> Option { - self.ifaces.get(iface.0).map(|i| i.mac) - } - pub fn neighbour(&self, iface: IfIndex, addr: Ipv4Addr) -> Option<&Nud> { self.ifaces.get(iface.0)?.neighbours.get(&addr).map(|n| &n.state) } @@ -440,7 +435,7 @@ mod compile_fail { /// match n { /// Nud::Incomplete(i) => i.queued(), /// Nud::Reachable(r) => usize::from(r.mac().0[0]), - /// Nud::Failed(_) => 0, + /// Nud::Failed => 0, /// _ => 1, /// } /// } diff --git a/toyos-net-ip/src/limiter.rs b/toyos-net-ip/src/limiter.rs index 15d3dd95349..212a64c4d02 100644 --- a/toyos-net-ip/src/limiter.rs +++ b/toyos-net-ip/src/limiter.rs @@ -61,7 +61,7 @@ impl Limiter { } /// The global bucket's burst during the second `now` falls in. - pub fn global_burst(&self, now: Instant) -> u64 { + fn global_burst(&self, now: Instant) -> u64 { let [a, b, c, d, e, f, g, h] = (now.nanos() / SECOND).to_le_bytes(); let span = GLOBAL_BURST_MAX.saturating_sub(GLOBAL_BURST_MIN).saturating_add(1); let draw = siphash24(&self.key, &[2, a, b, c, d, e, f, g, h]).checked_rem(span).unwrap_or(0); diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index c695ae2a3d4..5300b9f5730 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -34,7 +34,8 @@ pub enum Nud { Delay(Linked), Probe(Probing), Unreachable(Unreachable), - Failed(Failed), + /// Resolution failed; its hold-down is the entry's deadline. + Failed, } #[derive(Debug)] @@ -76,12 +77,6 @@ pub struct Linked { mac: MacAddr, } -impl Linked { - pub fn mac(&self) -> MacAddr { - self.mac - } -} - #[derive(Debug)] pub struct Probing { mac: MacAddr, @@ -89,10 +84,6 @@ pub struct Probing { } impl Probing { - pub fn mac(&self) -> MacAddr { - self.mac - } - pub fn requests(&self) -> u8 { self.requests } @@ -110,31 +101,12 @@ pub struct Unreachable { } impl Unreachable { - pub fn mac(&self) -> MacAddr { - self.mac - } - - pub fn requests(&self) -> u32 { - self.requests - } - /// No request pending: nothing is sent until the next datagram. pub fn quiescent(&self) -> bool { !self.backoff } } -#[derive(Debug)] -pub struct Failed { - since: Instant, -} - -impl Failed { - pub fn since(&self) -> Instant { - self.since - } -} - impl Nud { pub fn mac(&self) -> Option { match self { @@ -142,7 +114,7 @@ impl Nud { Self::Stale(l) | Self::Delay(l) => Some(l.mac), Self::Probe(p) => Some(p.mac), Self::Unreachable(u) => Some(u.mac), - Self::Incomplete(_) | Self::Failed(_) => None, + Self::Incomplete(_) | Self::Failed => None, } } @@ -221,7 +193,7 @@ pub(crate) fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { s.sent = false; backoff(s.requests) } - Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed(_) => return, + Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed => return, }; cx.timers.arm(timer(cx, addr), now.after(wait)); } @@ -233,7 +205,7 @@ fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { return true; } let class = |n: &Neighbour| match &n.state { - Nud::Failed(_) => Some(0), + Nud::Failed => Some(0), Nud::Unreachable(u) if u.quiescent() && !n.queued => Some(1), Nud::Stale(_) => Some(2), _ => None, @@ -282,7 +254,7 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt } Link::Resolved(mac) } - Nud::Failed(_) => { + Nud::Failed => { cx.log.count(Counter::NbFailedRefused); Link::Failed(Counter::NbFailedRefused) } @@ -309,7 +281,11 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let spaced = n.last_request.map_or(now, |at| at.after(RETRANS)).max(now); match &mut n.state { Nud::Incomplete(s) if s.requests < BROADCAST_SOLICIT => request(i, cx, addr, None), - Nud::Incomplete(_) => fail(i, cx, addr), + Nud::Incomplete(s) => { + let pending = core::mem::take(&mut s.pending); + n.state = Nud::Failed; + fail(i, cx, addr, pending); + } Nud::Reachable(s) => { let end = s.confirmed.after(reachable); if now < end { @@ -338,7 +314,7 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { s.backoff = false; cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } - Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed(_) => { + Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => { i.neighbours.remove(&addr); route::refresh_active(i, cx); } @@ -364,11 +340,9 @@ pub(crate) fn drop_held(cx: &mut Cx<'_>, held: Held) { } /// INCOMPLETE gave up: every held datagram is dropped. -fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { +fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: VecDeque) { let now = cx.now; - let Some(n) = i.neighbours.get_mut(&addr) else { return }; - let Nud::Incomplete(s) = core::mem::replace(&mut n.state, Nud::Failed(Failed { since: now })) else { return }; - for held in s.pending { + for held in pending { i.held = i.held.saturating_sub(1); drop_held(cx, held); } diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs index 02e5aff1baf..b6ec1b09585 100644 --- a/toyos-net-ip/src/route.rs +++ b/toyos-net-ip/src/route.rs @@ -103,7 +103,7 @@ fn active(i: &Interface) -> Option { let state = |g: &Ipv4Addr| i.neighbours.get(g).map(|n| &n.state); i.gateways .iter() - .find(|g| !matches!(state(g), Some(Nud::Failed(_) | Nud::Unreachable(_)))) + .find(|g| !matches!(state(g), Some(Nud::Failed | Nud::Unreachable(_)))) .or_else(|| i.gateways.iter().find(|g| matches!(state(g), Some(Nud::Unreachable(_))))) .or_else(|| i.gateways.first()) .copied() diff --git a/toyos-net-ip/tests/addr.rs b/toyos-net-ip/tests/addr.rs index c78a8f30196..b36dc0fecd8 100644 --- a/toyos-net-ip/tests/addr.rs +++ b/toyos-net-ip/tests/addr.rs @@ -326,7 +326,7 @@ fn fail(h: &mut H, gateway: Ipv4Addr) { let _ = h.ip.resolve(h.clock(), h.if0, gateway); h.out(); h.run(start + 3_000); - assert!(matches!(h.state(gateway), Some(Nud::Failed(_)))); + assert!(matches!(h.state(gateway), Some(Nud::Failed))); } #[test] @@ -357,7 +357,7 @@ fn s_ip_rte_014_with_no_good_gateway() { let t = h.now; h.run(t + 8_000); assert!(matches!(h.state(other), Some(Nud::Unreachable(_)))); - assert!(matches!(h.state(R), Some(Nud::Failed(_)))); + assert!(matches!(h.state(R), Some(Nud::Failed))); assert_eq!(route(&mut h, REMOTE).map(|r| r.next_hop), Ok(NextHop::Neighbour(other))); } diff --git a/toyos-net-ip/tests/clk.rs b/toyos-net-ip/tests/clk.rs index 96fd6ab048d..a464762d381 100644 --- a/toyos-net-ip/tests/clk.rs +++ b/toyos-net-ip/tests/clk.rs @@ -42,7 +42,7 @@ fn s_ip_clk_003_firing_more_often_changes_nothing() { assert_eq!(sent_every_ms, [0, 1_000, 2_000]); assert_eq!(sent_at_deadlines, sent_every_ms); for h in [&every_ms, &at_deadlines] { - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); assert_eq!(h.count(Counter::NbFailed), 1); } } diff --git a/toyos-net-ip/tests/icmp.rs b/toyos-net-ip/tests/icmp.rs index 01b32f3171d..a0ef0834caf 100644 --- a/toyos-net-ip/tests/icmp.rs +++ b/toyos-net-ip/tests/icmp.rs @@ -456,7 +456,7 @@ fn s_ip_icd_012_a_resolution_failure_is_local() { let out = h.run(3_000); assert_eq!(out.len(), 2, "only the two retransmitted requests"); assert!(h.events.iter().any(|e| matches!(e, Event::Unreachable(f) if f.destination == B))); - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); } #[test] diff --git a/toyos-net-ip/tests/nbr.rs b/toyos-net-ip/tests/nbr.rs index 6f3f35ba010..73c69963a45 100644 --- a/toyos-net-ip/tests/nbr.rs +++ b/toyos-net-ip/tests/nbr.rs @@ -242,5 +242,5 @@ fn s_arp_018_invalid_sender_addresses() { h.frame(&request(MAC_B, ip4(0, 0, 0, 0), ip4(192, 0, 2, 7))); assert_eq!(h.count(Counter::ArpInvalidSenderAddress), 5, "a probe is a request from 0.0.0.0"); assert!(h.state(ip4(0, 0, 0, 0)).is_none()); - assert!(matches!(h.state(B), None | Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), None | Some(Nud::Failed))); } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index bfe995dc498..72867f7d06f 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -43,7 +43,7 @@ fn s_ip_nud_002_three_requests_then_failed() { let times: Vec = out.iter().filter(|o| o.requests(B)).map(|o| o.at).collect(); assert_eq!(times, [0, 1_000, 2_000]); assert_eq!(out.len(), 3); - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); assert_eq!(h.count(Counter::NbFailed), 1); assert_eq!(h.count(Counter::NbPendingDropped), 1); assert!(h.events.contains(&Event::Unreachable(flow_to(B)))); @@ -220,7 +220,7 @@ fn s_ip_nud_014_failed_is_held_down() { assert_eq!(h.count(Counter::NbFailedRefused), 1); assert!(h.out().is_empty()); assert!(h.run(22_999).is_empty()); - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); h.run(23_000); assert!(h.state(B).is_none()); h.at(23_001); @@ -286,7 +286,7 @@ fn s_ip_nud_018_negative_advice_leaves_probe_incomplete_and_failed() { h.run(3_000); h.at(3_500); h.ip.advise(h.clock(), B, Advice::Reverify); - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); assert!(h.out().is_empty()); } @@ -299,7 +299,7 @@ fn s_ip_nud_019_positive_advice_needs_a_mac() { assert!(matches!(h.state(B), Some(Nud::Incomplete(_)))); h.run(3_000); h.ip.advise(h.clock(), B, Advice::Confirmed); - assert!(matches!(h.state(B), Some(Nud::Failed(_)))); + assert!(matches!(h.state(B), Some(Nud::Failed))); let (mut h, t0) = unreachable_r(); h.at(t0 + 10); h.ip.advise(h.clock(), REMOTE, Advice::Confirmed); @@ -360,7 +360,7 @@ fn s_ip_nud_022_a_full_table_evicts_failed_first() { let _ = h.ip.resolve(h.clock(), h.if0, failed); h.out(); h.run(3_000); - assert!(matches!(h.state(failed), Some(Nud::Failed(_)))); + assert!(matches!(h.state(failed), Some(Nud::Failed))); for n in 1..limits::nud::TABLE_MAX as u32 { reach_wide(&mut h, neighbour(n)); } diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs index d646940f04e..5e56230d8d3 100644 --- a/toyos-net-udp/src/lib.rs +++ b/toyos-net-udp/src/lib.rs @@ -10,7 +10,7 @@ //! hop has left this crate and spends no credit, so a socket's next datagram is never behind it. //! //! **Refusals are values.** Every refusal is a named [`Counter`] and the [`Error`] the call -//! returns; one of legacy or insecure input is also an [`Event`] naming the socket and the peer. +//! returns; one of legacy or insecure input is also a [`Refusal`] naming the socket and the peer. #![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] @@ -87,7 +87,7 @@ pub mod limits { pub const EPHEMERAL_COUNT: u16 = 16_384; /// The interface MTU less the IPv4 and UDP headers: nothing is fragmented (DF on every datagram). pub const MAX_PAYLOAD: usize = toyos_net_ip::MTU.saturating_sub(28); - /// Refusal events held for the shell until it drains them. + /// Refusals held for the shell until it drains them. pub const EVENTS: usize = 1_024; } @@ -156,11 +156,6 @@ pub struct Refusal { pub peer: (Ipv4Addr, u16), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Event { - Refused(Refusal), -} - #[derive(Debug)] struct Stored { payload: Vec, @@ -216,7 +211,7 @@ pub struct Udp { /// Datagrams a closed socket had accepted: they still leave (§U9 (3)). orphans: VecDeque<(Port, Queued)>, counters: Counters, - events: Vec, + refusals: Vec, } /// Not a destination any datagram may name: 0.0.0.0/8 but 0.0.0.0 itself, and 240.0.0.0/4 but the @@ -243,8 +238,8 @@ impl Udp { } /// Refusals to log since the last call. - pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> { - self.events.drain(..) + pub fn drain_refusals(&mut self) -> alloc::vec::Drain<'_, Refusal> { + self.refusals.drain(..) } pub fn socket_count(&self) -> usize { @@ -258,10 +253,10 @@ impl Udp { fn refuse(&mut self, rule: Counter, local: (Ipv4Addr, Port), peer: (Ipv4Addr, u16)) -> Result { self.count(rule); if rule.logged() { - if self.events.len() >= limits::EVENTS { + if self.refusals.len() >= limits::EVENTS { self.count(Counter::EventOverflow); } else { - self.events.push(Event::Refused(Refusal { rule, local, peer })); + self.refusals.push(Refusal { rule, local, peer }); } } Err(Error::Refused(rule)) @@ -397,11 +392,11 @@ impl Udp { } else { Self::unusable(ip, peer) }; - let refusal = refusal.or((peer_port == 0).then_some(Counter::ConnectPortZero)); - if let Some(rule) = refusal { - return self.refuse(rule, (local, local_port), (peer, peer_port)); - } - let Some(peer_port) = Port::new(peer_port) else { return Err(Error::Refused(Counter::ConnectPortZero)) }; + let peer_port = match (refusal, Port::new(peer_port)) { + (None, Some(port)) => port, + (Some(rule), _) => return self.refuse(rule, (local, local_port), (peer, peer_port)), + (None, None) => return self.refuse(Counter::ConnectPortZero, (local, local_port), (peer, peer_port)), + }; let source = if from_any { Source::Any } else { Source::Bound(local) }; let local = match ip.route(peer, source, None) { Ok(route) => route.source, @@ -458,10 +453,9 @@ impl Udp { return self.refuse(Counter::ConnectedDestinationMismatch, me, peer); } } + let Some(port) = Port::new(port) else { return self.refuse(Counter::SendPortZero, me, peer) }; let broadcast = destination.is_broadcast() || ip.is_directed_broadcast(destination); - let refusal = if port == 0 { - Some(Counter::SendPortZero) - } else if destination.is_unspecified() { + let refusal = if destination.is_unspecified() { Some(Counter::SendUnspecifiedDestination) } else if let Some(rule) = Self::unusable(ip, destination) { Some(rule) @@ -499,7 +493,6 @@ impl Udp { if socket.tx.len() >= limits::TX_DATAGRAMS || bytes > limits::TX_BYTES { return self.refuse(Counter::TxQueueFull, me, peer); } - let Some(port) = Port::new(port) else { return Err(Error::Refused(Counter::SendPortZero)) }; let ttl = if destination.is_multicast() { multicast_ttl } else { ttl }; socket.tx.push_back(Queued { source, destination, port, ttl, payload: payload.to_vec() }); socket.tx_bytes = bytes; diff --git a/toyos-net-udp/tests/common/mod.rs b/toyos-net-udp/tests/common/mod.rs index 2cafb5122c3..f9800081134 100644 --- a/toyos-net-udp/tests/common/mod.rs +++ b/toyos-net-udp/tests/common/mod.rs @@ -222,7 +222,7 @@ impl U { self.base = Counter::ALL.iter().map(|&c| (c, self.udp.counters().get(c))).collect(); self.ip_base = toyos_net_ip::Counter::ALL.iter().map(|&c| (c, self.ip.counters().get(c))).collect(); self.ip.drain_events().for_each(drop); - self.udp.drain_events().for_each(drop); + self.udp.drain_refusals().for_each(drop); self.events.clear(); } diff --git a/toyos-net-udp/tests/send.rs b/toyos-net-udp/tests/send.rs index d853fc9f272..7d478718a62 100644 --- a/toyos-net-udp/tests/send.rs +++ b/toyos-net-udp/tests/send.rs @@ -5,7 +5,7 @@ mod common; use std::net::Ipv4Addr; use common::*; -use toyos_net_udp::{limits, Binding, Counter, Error, Event, Refusal, SocketError, Verdict}; +use toyos_net_udp::{limits, Binding, Counter, Error, Refusal, SocketError, Verdict}; use toyos_net_wire::ethernet::MacAddr; use toyos_net_wire::ipv4::Ttl; @@ -137,8 +137,8 @@ fn s_udp_us_020_what_send_refuses() { ] { assert_eq!(u.udp.send_to(&mut u.ip, id, destination, p, b"x"), refused(rule), "{destination}:{p}"); } - let logged: Vec = u.udp.drain_events().collect(); - assert_eq!(logged, [Event::Refused(Refusal { rule: Counter::SendUnspecifiedDestination, local: (ANY, port(50_001)), peer: (ANY, 53) })]); + let logged: Vec = u.udp.drain_refusals().collect(); + assert_eq!(logged, [Refusal { rule: Counter::SendUnspecifiedDestination, local: (ANY, port(50_001)), peer: (ANY, 53) }]); assert!(u.out().is_empty()); } @@ -149,7 +149,7 @@ fn s_udp_us_021_broadcast_needs_permission() { for destination in [LIMITED, ip4(192, 0, 2, 255)] { assert_eq!(u.udp.send_to(&mut u.ip, id, destination, 5_001, b"hi"), refused(Counter::BroadcastNotPermitted)); } - assert_eq!(u.udp.drain_events().count(), 2, "each refusal is logged"); + assert_eq!(u.udp.drain_refusals().count(), 2, "each refusal is logged"); u.udp.set_broadcast(id, true).unwrap(); for destination in [LIMITED, ip4(192, 0, 2, 255)] { u.udp.send_to(&mut u.ip, id, destination, 5_001, b"hi").unwrap(); From d9f1771d605bf88a4dbd8395bf461cb2ffb6728d Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 13:44:47 +0200 Subject: [PATCH 07/20] toyos-net-ip: pin that released datagrams are outside the control queue's bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ip.md §5.4 lists datagrams released from a pending queue among the frames its 64-frame FIFO drops when full. A released datagram's sender was already told it is held, so dropping it there is the silent loss the owner's premise rules out ("a full ring backpressures the sender and never drops"); it stays bounded by PENDING_TOTAL, counted where it was held until it leaves. The test fills the queue with 64 of [ip]'s own frames and resolves eight neighbours holding 64 datagrams: all 128 frames leave and nothing is counted ip.control-queue-full. A mutation making release subject to the bound turns it red, and no other test did. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/tests/io.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index dae66fd275a..238f59ee71c 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -516,3 +516,27 @@ fn s_ip_out_006_the_control_queue_is_bounded() { assert_eq!(h.count(Counter::IpControlQueueFull), 1); assert_eq!(h.out().len(), limits::CONTROL_QUEUE); } + +/// The bound is on [ip]'s own frames: a datagram its sender was told is held stays bounded by +/// the pending queues it was held in, and is never dropped on release. +#[test] +fn s_ip_out_006_released_datagrams_are_bounded_where_held() { + let mut h = H::fixture_i(); + let held = |n: u8| ip4(192, 0, 2, 10 + n); + for n in 0..8 { + for _ in 0..8 { + assert_eq!(h.udp_to(held(n)), Ok(None)); + } + } + h.out(); + for n in 0..limits::CONTROL_QUEUE as u8 { + let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); + } + for n in 0..8 { + h.reply_from(held(n), MacAddr([2, 0, 0, 0, 1, n])); + } + let out = h.out(); + assert_eq!(out.iter().filter(|o| o.ip().is_some()).count(), limits::nud::PENDING_TOTAL); + assert_eq!(out.len(), limits::CONTROL_QUEUE + limits::nud::PENDING_TOTAL); + assert_eq!(h.count(Counter::IpControlQueueFull), 0); +} From fdd546c122c2d896795f0e908988e80ec927751a Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 13:51:41 +0200 Subject: [PATCH 08/20] net stage 3: the tests four surviving mutations asked for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A checked mutation per rule found four that no test turned red: - NUD request spacing removed: NUD-16's advice at 600 lands on PROBE, which ignores advice anyway, so the scenario never reached the rule. The new NUD-16 test sends the request at 0, confirms at 5 and advises at 100: the poll waits for 1,000 (ip.md §6.3, RFC 1122 §2.3.2.1). - Transmit credit ignored by [ip]: OUT-06 now asserts that no credit moves nothing and one credit moves the oldest frame. - The acquisition exception delivered to any socket on port 68: US-44 now also has a socket on 68 without the mark, which must not hear it (udp-dhcp.md §U5.3). - A datagram [ip] holds spending UDP's credit: with one credit, the next socket datagram leaves while the first waits for resolution (§U4.6 (4)). NUD-28's FAILED block had become vacuous when FAILED lost its fields; it now states that FAILED carries nothing, and the compiling twin binds STALE's Linked so the STALE block can fail only on the missing queue. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/lib.rs | 8 ++++++-- toyos-net-ip/tests/io.rs | 5 ++++- toyos-net-ip/tests/nud.rs | 16 ++++++++++++++++ toyos-net-udp/tests/recv.rs | 9 +++++++++ toyos-net-udp/tests/send.rs | 13 +++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs index 1177989a18d..58bcb422076 100644 --- a/toyos-net-ip/src/lib.rs +++ b/toyos-net-ip/src/lib.rs @@ -430,11 +430,15 @@ mod compile_fail { /// NUD-28: a MAC exists only in a state that has one, and a pending queue only in INCOMPLETE. /// /// ``` - /// use toyos_net_ip::Nud; + /// use toyos_net_ip::{Linked, Nud}; /// fn f(n: &Nud) -> usize { /// match n { /// Nud::Incomplete(i) => i.queued(), /// Nud::Reachable(r) => usize::from(r.mac().0[0]), + /// Nud::Stale(s) => { + /// let _: &Linked = s; + /// 2 + /// } /// Nud::Failed => 0, /// _ => 1, /// } @@ -448,7 +452,7 @@ mod compile_fail { /// /// ```compile_fail /// use toyos_net_ip::Nud; - /// fn f(n: &Nud) { if let Nud::Failed(i) = n { let _ = i.mac(); } } + /// fn f(n: &Nud) { if let Nud::Failed(_) = n {} } /// ``` /// /// ```compile_fail diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index 238f59ee71c..78bad38c7dd 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -514,7 +514,10 @@ fn s_ip_out_006_the_control_queue_is_bounded() { let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); } assert_eq!(h.count(Counter::IpControlQueueFull), 1); - assert_eq!(h.out().len(), limits::CONTROL_QUEUE); + assert!(h.out_with(0).is_empty(), "no credit, nothing leaves"); + let first = h.out_with(1); + assert!(first.len() == 1 && first[0].requests(ip4(192, 0, 2, 100)), "one credit, the oldest frame"); + assert_eq!(h.out().len(), limits::CONTROL_QUEUE - 1); } /// The bound is on [ip]'s own frames: a datagram its sender was told is held stays bounded by diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 72867f7d06f..5d1a1abd876 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -256,6 +256,22 @@ fn s_ip_nud_016_requests_are_spaced() { assert_eq!(later.iter().map(|o| (o.at, o.frame.clone())).collect::>(), [(1_100, hex(V_ARP_POLL_R))]); } +#[test] +fn s_ip_nud_016_advice_inside_the_spacing_waits_for_its_boundary() { + let mut h = H::fixture_i(); + let _ = h.ip.resolve(h.clock(), h.if0, R); + assert_eq!(h.out().len(), 1, "the request at 0"); + h.at(5); + h.reply_from(R, MAC_R); + assert!(h.is_reachable(R)); + h.at(100); + h.ip.advise(h.clock(), R, Advice::Reverify); + assert!(matches!(h.state(R), Some(Nud::Probe(_)))); + assert!(h.out().is_empty(), "100 ms after the last request"); + let later = h.run(1_000); + assert_eq!(later.iter().map(|o| (o.at, o.frame.clone())).collect::>(), [(1_000, hex(V_ARP_POLL_R))]); +} + #[test] fn s_ip_nud_017_negative_advice_probes() { let mut h = H::fixture_i_with(R, MAC_R); diff --git a/toyos-net-udp/tests/recv.rs b/toyos-net-udp/tests/recv.rs index 95b41865df7..21540c83589 100644 --- a/toyos-net-udp/tests/recv.rs +++ b/toyos-net-udp/tests/recv.rs @@ -214,6 +214,15 @@ fn s_udp_us_044_the_acquisition_exception() { assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); } +#[test] +fn s_udp_us_044_the_exception_reaches_the_acquisition_socket_alone() { + let mut u = U::bare(); + let id = u.bind(ANY, 68).unwrap(); + assert_eq!(u.frame(&hex(V_DHCP_OFFER_FRAME)), Some(Verdict::Ignored)); + assert_eq!(u.count(Counter::RxNoSocket), 1); + assert_eq!(u.udp.recv(id, &mut [0; 1_500]), Ok(None)); +} + #[test] fn s_udp_us_045_what_a_datagram_carries() { let mut u = U::uf(); diff --git a/toyos-net-udp/tests/send.rs b/toyos-net-udp/tests/send.rs index 7d478718a62..5c3e1ce2123 100644 --- a/toyos-net-udp/tests/send.rs +++ b/toyos-net-udp/tests/send.rs @@ -247,6 +247,19 @@ fn s_udp_us_026_no_head_of_line_blocking() { assert_eq!(ip_of(&out[0]).unwrap().destination(), far); } +#[test] +fn s_udp_us_026_a_held_datagram_spends_no_credit() { + let mut u = U::uf(); + let id = u.bind(ANY, 50_001).unwrap(); + let far = ip4(192, 0, 2, 77); + u.udp.send_to(&mut u.ip, id, far, 53, b"1").unwrap(); + u.udp.send_to(&mut u.ip, id, DNS, 53, b"2").unwrap(); + let out = u.out_with(1); + assert_eq!(out.len(), 1); + assert!(ip_of(&out[0]).is_some_and(|ip| ip.destination() == DNS)); + assert!(is_arp_request_for(&u.out_with(1)[0], far)); +} + #[test] fn s_udp_us_027_a_failed_next_hop_is_reported_once() { let mut u = U::uf(); From ce7fcb930e204b874d02e1e20728b89eb3614435 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 14:09:02 +0200 Subject: [PATCH 09/20] net stage 3: one test per scenario, and the track records what stage 3 owes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ip.md §17 gives every scenario exactly one test, and a scenario with several inputs is one test that checks each; the four checks the surviving mutations asked for become parts of NUD-16, OUT-06, US-26 and US-44 instead of second tests under the same ids. The track's list of wire scenarios owed by stages 3 to 5 is gone: every one is tested here. In its place, what the stage 3 specifications still owe (OUT-07 to the shard, US-57 to netd, the fragment and path-MTU scenarios to IP hardening) and the three places stage 3 departs from them, each with its exit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- issues/design-debt/toyos-has-its-own-network-stack.md | 8 +++++++- toyos-net-ip/tests/io.rs | 7 ++----- toyos-net-ip/tests/nud.rs | 4 +--- toyos-net-udp/tests/recv.rs | 6 ++---- toyos-net-udp/tests/send.rs | 5 +---- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/issues/design-debt/toyos-has-its-own-network-stack.md b/issues/design-debt/toyos-has-its-own-network-stack.md index a512ac65046..539ec22fefb 100644 --- a/issues/design-debt/toyos-has-its-own-network-stack.md +++ b/issues/design-debt/toyos-has-its-own-network-stack.md @@ -18,7 +18,13 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade The listener defects are this track's: `issues/hardware/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/hardware/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/hardware/an-accept-that-never-reaches-netd-strands-its-listener.md`, in std's accept. -Owed from the wire specification by stages 3–5: ETH-32, whose subnet broadcast needs the subnet `toyos-net-ip` holds; and those whose layer tags are `[ip]`, `[shell]` or `[udp]`: ETH-11, 12, 22–25, 33; ARP-16–18; IP-25, 26, 35; IPP-01–13; ICMP-32–46; IGMP-23–25, 29; UDP-17, 18; and the policy halves of ETH-10, 14, 19, IP-02, 20–23, 29, IPO-15, 16, ICMP-23, 24, 26 and UDP-22. +Owed from the stage 3 specifications: OUT-07 by stage 4, whose scheduler chooses between a flow's segment and [ip]'s own frames; US-57 by stage 5, whose netd maps UDP's refusals onto the pipe ABI; FRA-01–20 and PMTU-01–06 by IP hardening. + +What stage 3 departs from its specifications: + +- `ip.md` §5.4 drops a datagram released from a pending queue when the 64-frame control queue is full; `toyos-net-ip` never does, because its sender was told it is held, and it stays bounded by the pending queues it was held in. Exit: the specification says so, or the owner rules the drop. +- `udp-dhcp.md` §D2 has the client count `dhcp.renew-unroutable`, but the client never learns of the refusal, which `toyos-net-udp` counts as `udp.no-route`. Exit: netd counts it where the refusal lands at stage 5, or the specification drops it. +- 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` (`wire.md` §3.3, ETH-23). Exit: the scenario names the frame filter. What `toyos-net-wire` does not yet meet: diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index 78bad38c7dd..2fc0974d127 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -518,12 +518,9 @@ fn s_ip_out_006_the_control_queue_is_bounded() { let first = h.out_with(1); assert!(first.len() == 1 && first[0].requests(ip4(192, 0, 2, 100)), "one credit, the oldest frame"); assert_eq!(h.out().len(), limits::CONTROL_QUEUE - 1); -} -/// The bound is on [ip]'s own frames: a datagram its sender was told is held stays bounded by -/// the pending queues it was held in, and is never dropped on release. -#[test] -fn s_ip_out_006_released_datagrams_are_bounded_where_held() { + // A datagram whose sender was told it is held stays bounded where it was held: a full queue + // of [ip]'s own frames never drops it on release. let mut h = H::fixture_i(); let held = |n: u8| ip4(192, 0, 2, 10 + n); for n in 0..8 { diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 5d1a1abd876..376c3dcdbab 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -254,10 +254,8 @@ fn s_ip_nud_016_requests_are_spaced() { assert_eq!(first.iter().map(|o| o.frame.clone()).collect::>(), [hex(V_ARP_POLL_R)]); assert!(second.is_empty()); assert_eq!(later.iter().map(|o| (o.at, o.frame.clone())).collect::>(), [(1_100, hex(V_ARP_POLL_R))]); -} -#[test] -fn s_ip_nud_016_advice_inside_the_spacing_waits_for_its_boundary() { + // Advice that reaches a confirmed entry within RETRANS of its last request. let mut h = H::fixture_i(); let _ = h.ip.resolve(h.clock(), h.if0, R); assert_eq!(h.out().len(), 1, "the request at 0"); diff --git a/toyos-net-udp/tests/recv.rs b/toyos-net-udp/tests/recv.rs index 21540c83589..4ec61b4425c 100644 --- a/toyos-net-udp/tests/recv.rs +++ b/toyos-net-udp/tests/recv.rs @@ -212,13 +212,11 @@ fn s_udp_us_044_the_acquisition_exception() { fix(&mut ip); assert_eq!(u.frame(ð(MAC_A, MAC_R, 0x0800, &ip)), None); assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); -} -#[test] -fn s_udp_us_044_the_exception_reaches_the_acquisition_socket_alone() { + // A socket on 68 without the acquisition mark never hears it (§U5.3). let mut u = U::bare(); let id = u.bind(ANY, 68).unwrap(); - assert_eq!(u.frame(&hex(V_DHCP_OFFER_FRAME)), Some(Verdict::Ignored)); + assert_eq!(u.frame(&offer), Some(Verdict::Ignored)); assert_eq!(u.count(Counter::RxNoSocket), 1); assert_eq!(u.udp.recv(id, &mut [0; 1_500]), Ok(None)); } diff --git a/toyos-net-udp/tests/send.rs b/toyos-net-udp/tests/send.rs index 5c3e1ce2123..55cc073e7b6 100644 --- a/toyos-net-udp/tests/send.rs +++ b/toyos-net-udp/tests/send.rs @@ -245,13 +245,10 @@ fn s_udp_us_026_no_head_of_line_blocking() { let out = u.out(); assert_eq!(out.len(), 1); assert_eq!(ip_of(&out[0]).unwrap().destination(), far); -} -#[test] -fn s_udp_us_026_a_held_datagram_spends_no_credit() { + // With one frame of credit: the held datagram spends none of it. let mut u = U::uf(); let id = u.bind(ANY, 50_001).unwrap(); - let far = ip4(192, 0, 2, 77); u.udp.send_to(&mut u.ip, id, far, 53, b"1").unwrap(); u.udp.send_to(&mut u.ip, id, DNS, 53, b"2").unwrap(); let out = u.out_with(1); From 70dc74d028b4d9916ca8380e6ea1075836d15484 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 17:06:11 +0200 Subject: [PATCH 10/20] =?UTF-8?q?net=20stage=203:=20answer=20the=20third?= =?UTF-8?q?=20review=20=E2=80=94=20bounds,=20one=20home=20for=20address=20?= =?UTF-8?q?arithmetic,=20and=20pinned=20refusals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review of #635 at ce7fcb930 upheld nine blockers; the spec owner's rulings for this round bind beside them. Per decision: - toyos-net-wire holds the IPv4 address classes and `Cidr` (addr.rs): `is_host`, `is_martian`, and a prefix built from an address and a length or a contiguous mask. toyos-net-ip, toyos-net-udp and toyos-dhcp read them from there; their copies are deleted. toyos-dhcp does not depend on toyos-net-ip, so the arithmetic moved down rather than across. - The acquisition exception (C-3, ip.md §2.4 (5)) admits a destination that names one host only. A host destination can only arrive in a frame to our MAC (§4.1 step 3 refuses it in a group frame first), so the separate link check was dead and is gone. - toyos-net-udp's close holds at most CLOSED_DATAGRAMS (one socket's queue) of accepted datagrams together, and counts the rest `udp.tx-discarded-on-close`: architecture §3.3 holds nothing without a bound, and nobody is left to refuse them to. A closed socket's turn leaves with it, and the closed sockets' datagrams take one turn among the sockets instead of going first. - ip.md §5.4 as amended: the control queue holds at most 64 frames, released datagrams included. A released datagram that finds it full stays at the head of its neighbour's pending queue and enters as room appears; it is its neighbour's until it has left. An entry holding one is evicted after every other candidate (§6.8), dropping them as `nb.pending-evicted`, and is not idle: an idle lifetime that passed while it held them ends when the last leaves (NUD-29, NUD-30). - IGMP caps a query's sources at one past the limit before merging, so a merge is bounded. - tcp's unused re-exports of REFUSAL_LOG_INTERVAL and siphash24 go; its tests read siphash24 from toyos-net-wire. `Ip::wire_refusals` goes; `wire_counters` answers the same. Tests the review asked for: MUST-57 in IN-09; the undrained-refusal bound in MOD-01, US-20 and DH-38; DHCP refusals pinned with §D3.1's log flag as a literal and the refusal's peer; §13.2's logged list as literals in MOD-02; ACD-08's 60 s as a literal; §6.8's full eviction order in NUD-22; the acquisition exception's negative case, its probing case and `ip.acquisition-admitted` in US-44; close's bound, slot reuse and turn in US-53; `udp.no-route` against `udp.no-source-address` in US-24; DH-73's exactly one reason; NUD-28's Probe and Unreachable. New scenarios OUT-08, NUD-29 and NUD-30 each have their test. The track drops the §5.4 departure and the renew-unroutable departure (netd's at stage 5 now), and records DH-64's announce request, close's bound, and stage 3's missing independent oracle. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .../toyos-has-its-own-network-stack.md | 10 +- toyos-dhcp/src/lib.rs | 81 +++++------- toyos-dhcp/tests/parse.rs | 73 ++++++----- toyos-dhcp/tests/props.rs | 6 +- toyos-net-ip/src/acd.rs | 12 +- toyos-net-ip/src/addr.rs | 51 +------- toyos-net-ip/src/arp.rs | 2 +- toyos-net-ip/src/config.rs | 30 ++--- toyos-net-ip/src/counters.rs | 1 + toyos-net-ip/src/egress.rs | 109 ++++++++++++---- toyos-net-ip/src/icmp.rs | 2 +- toyos-net-ip/src/iface.rs | 6 +- toyos-net-ip/src/igmp.rs | 14 ++- toyos-net-ip/src/input.rs | 16 +-- toyos-net-ip/src/lib.rs | 31 +++-- toyos-net-ip/src/nud.rs | 64 +++++++--- toyos-net-ip/src/route.rs | 11 +- toyos-net-ip/tests/acd.rs | 4 +- toyos-net-ip/tests/common/mod.rs | 3 +- toyos-net-ip/tests/igmp.rs | 29 ++++- toyos-net-ip/tests/io.rs | 50 +++++--- toyos-net-ip/tests/nud.rs | 119 +++++++++++++++--- toyos-net-tcp/src/lib.rs | 3 +- toyos-net-tcp/tests/isn.rs | 3 +- toyos-net-tcp/tests/port.rs | 3 +- toyos-net-udp/src/lib.rs | 97 +++++++++----- toyos-net-udp/tests/recv.rs | 68 +++++++++- toyos-net-udp/tests/send.rs | 11 ++ toyos-net-wire/Cargo.toml | 2 +- toyos-net-wire/src/addr.rs | 75 +++++++++++ toyos-net-wire/src/lib.rs | 5 +- 31 files changed, 673 insertions(+), 318 deletions(-) create mode 100644 toyos-net-wire/src/addr.rs diff --git a/issues/design-debt/toyos-has-its-own-network-stack.md b/issues/design-debt/toyos-has-its-own-network-stack.md index 539ec22fefb..a1e580aaebc 100644 --- a/issues/design-debt/toyos-has-its-own-network-stack.md +++ b/issues/design-debt/toyos-has-its-own-network-stack.md @@ -18,13 +18,17 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade The listener defects are this track's: `issues/hardware/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/hardware/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/hardware/an-accept-that-never-reaches-netd-strands-its-listener.md`, in std's accept. -Owed from the stage 3 specifications: OUT-07 by stage 4, whose scheduler chooses between a flow's segment and [ip]'s own frames; US-57 by stage 5, whose netd maps UDP's refusals onto the pipe ABI; FRA-01–20 and PMTU-01–06 by IP hardening. +Owed from the stage 3 specifications: OUT-07 by stage 4, whose scheduler chooses between a flow's segment and [ip]'s own frames; US-57 by stage 5, whose netd maps UDP's refusals onto the pipe ABI; `dhcp.renew-unroutable` by stage 5, whose netd counts it where `toyos-net-udp` refuses the renewal `udp.no-route`; FRA-01–20 and PMTU-01–06 by IP hardening. What stage 3 departs from its specifications: -- `ip.md` §5.4 drops a datagram released from a pending queue when the 64-frame control queue is full; `toyos-net-ip` never does, because its sender was told it is held, and it stays bounded by the pending queues it was held in. Exit: the specification says so, or the owner rules the drop. -- `udp-dhcp.md` §D2 has the client count `dhcp.renew-unroutable`, but the client never learns of the refusal, which `toyos-net-udp` counts as `udp.no-route`. Exit: netd counts it where the refusal lands at stage 5, or the specification drops it. - 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` (`wire.md` §3.3, 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 (IP-D6, C-4), so the client asks nothing. Exit: the scenario drops the request, or [ip] stops announcing on its own. +- `udp-dhcp.md` §U9 (3) 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 architecture §3.3 holds nothing without a bound. Exit: the specification bounds them. + +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. What `toyos-net-wire` does not yet meet: diff --git a/toyos-dhcp/src/lib.rs b/toyos-dhcp/src/lib.rs index e9fd1f36df1..e9734d6aa75 100644 --- a/toyos-dhcp/src/lib.rs +++ b/toyos-dhcp/src/lib.rs @@ -34,6 +34,7 @@ use alloc::vec::Vec; use core::net::Ipv4Addr; use core::time::Duration; +use toyos_net_wire::addr::{is_host, Cidr}; use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; use toyos_net_wire::Instant; @@ -294,30 +295,10 @@ pub struct Client { refusals: Vec, } -/// Not 0.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 224.0.0.0/4 or 240.0.0.0/4: one host (§D4.1). +/// §D4.1's unicast host address: a host, and not in 169.254.0.0/16, which RFC 3927 §1.6 says a +/// DHCP server should not hand out. fn host(addr: Ipv4Addr) -> bool { - let [a, b, ..] = addr.octets(); - !(a == 0 || a == 127 || (a, b) == (169, 254) || a >= 224) -} - -fn mask(len: u8) -> u32 { - u32::MAX.checked_shl(u32::from(32u8.saturating_sub(len))).unwrap_or(0) -} - -/// A contiguous mask of 1 to 31 bits (H-14). -fn prefix_len(mask_addr: Ipv4Addr) -> Option { - let bits = u32::from(mask_addr); - let len = u8::try_from(bits.leading_ones()).ok()?; - ((1..=31).contains(&len) && mask(len) == bits).then_some(len) -} - -fn same_subnet(a: Ipv4Addr, b: Ipv4Addr, len: u8) -> bool { - (u32::from(a) ^ u32::from(b)) & mask(len) == 0 -} - -/// The directed broadcast of a prefix of /30 or shorter. -fn broadcast(addr: Ipv4Addr, len: u8) -> Option { - (len <= 30).then(|| Ipv4Addr::from(u32::from(addr) | !mask(len))) + is_host(addr) && !addr.is_link_local() } fn secs(since: Duration) -> u16 { @@ -714,31 +695,26 @@ impl Client { /// §D4.1: the address, the lease time and the mask an OFFER or ACK must carry. fn acceptable(&mut self, reply: &Reply, server: Ipv4Addr, peer: Peer) -> Option { let yiaddr = reply.yiaddr; - let refusal = if !host(yiaddr) { - Some(Counter::YiaddrInvalid) + let accepted = if !host(yiaddr) { + Err(Counter::YiaddrInvalid) } else { match (reply.lease, reply.mask) { - (None, _) => Some(Counter::NoLeaseTime), - (Some(0), _) => Some(Counter::LeaseZero), - (_, None) => Some(Counter::NoSubnetMask), - (Some(_), Some(m)) => match prefix_len(m) { - None => Some(Counter::MaskInvalid), - Some(len) if len <= 30 && (yiaddr == Ipv4Addr::from(u32::from(yiaddr) & mask(len)) || Some(yiaddr) == broadcast(yiaddr, len)) => { - Some(Counter::YiaddrInvalid) - } - Some(_) => None, + (None, _) => Err(Counter::NoLeaseTime), + (Some(0), _) => Err(Counter::LeaseZero), + (_, None) => Err(Counter::NoSubnetMask), + // A contiguous mask of 1 to 31 bits (H-14). + (Some(lease), Some(mask)) => match Cidr::from_mask(yiaddr, mask).filter(|c| c.prefix_len() <= 31) { + None => Err(Counter::MaskInvalid), + Some(cidr) if cidr.is_edge(yiaddr) => Err(Counter::YiaddrInvalid), + Some(cidr) => Ok((cidr, lease)), }, } }; - if let Some(rule) = refusal { - self.refuse(rule, peer); - return None; - } + let (cidr, lease) = accepted.inspect_err(|&rule| self.refuse(rule, peer)).ok()?; Some(Offered { - address: yiaddr, - prefix_len: reply.mask.and_then(prefix_len).unwrap_or(32), + cidr, server, - lease: reply.lease.unwrap_or(0), + lease, routers: reply.routers.clone(), dns: reply.dns.clone(), t1: reply.t1, @@ -769,13 +745,13 @@ impl Client { let state = core::mem::replace(&mut self.state, State::BackingOff(now)); match (state, kind) { (State::Selecting(s), ReplyKind::Offer) => { - let payload = self.message(Kind::Request, s.xid, s.secs, Ipv4Addr::UNSPECIFIED, Some(offered.server), Some(offered.address)); + let payload = self.message(Kind::Request, s.xid, s.secs, Ipv4Addr::UNSPECIFIED, Some(offered.server), Some(offered.cidr.addr())); let transmit = self.broadcast(payload, Counter::TxRequest); let deadline = now.after(jittered(backoff(0), draw())); self.state = State::Requesting(Requesting { xid: s.xid, server: offered.server, - address: offered.address, + address: offered.cidr.addr(), secs: s.secs, first: now, step: 0, @@ -788,19 +764,19 @@ impl Client { self.probe(lease) } (State::Requesting(r), _) if offered.server != r.server => self.keep(State::Requesting(r), Counter::AckWrongServer, peer), - (State::Requesting(r), _) if offered.address != r.address => self.keep(State::Requesting(r), Counter::AckAddressChanged, peer), + (State::Requesting(r), _) if offered.cidr.addr() != r.address => self.keep(State::Requesting(r), Counter::AckAddressChanged, peer), (State::Requesting(r), _) => { let lease = self.lease_of(offered, r.first, peer); self.probe(lease) } (State::Renewing(r), _) if offered.server != r.lease.server => self.keep(State::Renewing(r), Counter::AckWrongServer, peer), - (State::Renewing(r), _) if offered.address != r.lease.address => self.keep(State::Renewing(r), Counter::AckAddressChanged, peer), - (State::Rebinding(r), _) if offered.address != r.lease.address => self.keep(State::Rebinding(r), Counter::AckAddressChanged, peer), + (State::Renewing(r), _) if offered.cidr.addr() != r.lease.address => self.keep(State::Renewing(r), Counter::AckAddressChanged, peer), + (State::Rebinding(r), _) if offered.cidr.addr() != r.lease.address => self.keep(State::Rebinding(r), Counter::AckAddressChanged, peer), (State::Renewing(r) | State::Rebinding(r), _) => { let lease = self.lease_of(offered, r.sent, peer); self.renewed(now, &r.lease, lease, draw) } - (State::Rebooting(r), _) if offered.address != r.lease.address => self.keep(State::Rebooting(r), Counter::AckAddressChanged, peer), + (State::Rebooting(r), _) if offered.cidr.addr() != r.lease.address => self.keep(State::Rebooting(r), Counter::AckAddressChanged, peer), (State::Rebooting(r), _) => { let lease = self.lease_of(offered, r.start, peer); self.renewed(now, &r.lease, lease, draw) @@ -835,10 +811,10 @@ impl Client { /// §D4.2 and §D7: the first usable router, at most three resolvers, and T1 and T2, the /// lease running from `base`. fn lease_of(&mut self, offered: Offered, base: Instant, peer: Peer) -> Lease { - let (address, len) = (offered.address, offered.prefix_len); + let (cidr, address) = (offered.cidr, offered.cidr.addr()); let mut router = None; for candidate in offered.routers { - if host(candidate) && same_subnet(candidate, address, len) && candidate != address { + if host(candidate) && cidr.contains(candidate) && candidate != address { router = Some(candidate); break; } @@ -846,7 +822,7 @@ impl Client { } let mut dns = Vec::new(); for server in offered.dns { - if !host(server) || Some(server) == broadcast(address, len) { + if !host(server) || Some(server) == cidr.broadcast() { self.refuse(Counter::DnsInvalid, peer); } else if dns.len() < limits::MAX_DNS { dns.push(server); @@ -881,14 +857,13 @@ impl Client { }; Some(Timers { t1: base.after(t1), t2: base.after(t2), expiry: base.after(lease) }) }; - Lease { address, prefix_len: len, router, dns, server: offered.server, base, timers } + Lease { address, prefix_len: cidr.prefix_len(), router, dns, server: offered.server, base, timers } } } /// What an acceptable OFFER or ACK holds, before §D4.2 degrades it into a lease. struct Offered { - address: Ipv4Addr, - prefix_len: u8, + cidr: Cidr, server: Ipv4Addr, lease: u32, routers: Vec, diff --git a/toyos-dhcp/tests/parse.rs b/toyos-dhcp/tests/parse.rs index d00c0089773..125e47d2802 100644 --- a/toyos-dhcp/tests/parse.rs +++ b/toyos-dhcp/tests/parse.rs @@ -6,19 +6,21 @@ mod common; use std::net::Ipv4Addr; use common::*; -use toyos_dhcp::{Config, Counter, Output, Phase}; +use toyos_dhcp::{limits, Config, Counter, Output, Peer, Phase, Refusal}; fn selecting() -> D { D::ds(&[XID, 1_000]).0 } -/// Delivers `message` to a selecting client: the refusal it counted and whether it logged one. -fn refused(message: &[u8], rule: Counter) { +/// Delivers `message` to a selecting client: refused by `rule`, and named with the server that +/// sent it exactly when §D3.1 and §D14 log it. +fn refused(message: &[u8], rule: Counter, logged: bool) { let mut d = selecting(); let out = d.receive(10, message); assert_eq!(out, Output::default(), "{rule:?}"); assert_eq!(d.count(rule), 1, "{rule:?}"); - assert_eq!(d.logged(rule), usize::from(rule.logged()), "{rule:?}"); + let named: &[Refusal] = if logged { &[Refusal { rule, peer: Peer::From(R) }] } else { &[] }; + assert_eq!(d.refusals, named, "{rule:?}"); assert_eq!(d.client.phase(), Phase::Selecting); } @@ -42,47 +44,56 @@ fn edited(change: impl FnOnce(&mut Vec)) -> Vec { #[test] fn s_dhcp_dh_038_truncated() { - refused(&offer()[..239], Counter::Truncated); + refused(&offer()[..239], Counter::Truncated, true); + + let mut d = selecting(); + for _ in 0..=limits::EVENTS { + assert_eq!(d.client.receive(at(10), &offer()[..239], R, || 1_000), Output::default()); + } + assert_eq!(d.client.drain_refusals().count(), limits::EVENTS, "an undrained shell holds a bounded list"); + assert_eq!(d.count(Counter::EventOverflow), 1); } #[test] fn s_dhcp_dh_039_not_a_reply() { - refused(&edited(|m| m[0] = 1), Counter::NotReply); + refused(&edited(|m| m[0] = 1), Counter::NotReply, true); refused( &edited(|m| { m[0] = 1; m[28..34].copy_from_slice(&MAC_B.0); }), Counter::NotReply, + true, ); } #[test] fn s_dhcp_dh_040_the_hardware_type() { - refused(&edited(|m| m[1] = 6), Counter::HardwareType); - refused(&edited(|m| m[2] = 16), Counter::HardwareType); + refused(&edited(|m| m[1] = 6), Counter::HardwareType, true); + refused(&edited(|m| m[2] = 16), Counter::HardwareType, true); } #[test] fn s_dhcp_dh_041_another_clients_reply() { - refused(&edited(|m| m[28..34].copy_from_slice(&MAC_B.0)), Counter::ChaddrMismatch); + refused(&edited(|m| m[28..34].copy_from_slice(&MAC_B.0)), Counter::ChaddrMismatch, false); accepted(&edited(|m| m[34..44].fill(0xff))); } #[test] fn s_dhcp_dh_042_another_transaction() { - refused(&edited(|m| m[7] = 0xf8), Counter::XidMismatch); + refused(&edited(|m| m[7] = 0xf8), Counter::XidMismatch, false); } #[test] fn s_dhcp_dh_043_bootp_is_refused() { - refused(&edited(|m| m[236..240].fill(0)), Counter::BootpReply); + refused(&edited(|m| m[236..240].fill(0)), Counter::BootpReply, true); refused( &edited(|m| { m[236..240].fill(0); m[7] = 0xf8; }), Counter::BootpReply, + true, ); } @@ -104,7 +115,7 @@ fn s_dhcp_dh_045_an_option_past_its_field() { let n = m.len(); m.truncate(n - 7); m.extend_from_slice(&[6, 5, 192, 0, 2, 0x35]); - refused(&m, Counter::OptionTruncated); + refused(&m, Counter::OptionTruncated, true); } #[test] @@ -121,13 +132,13 @@ fn s_dhcp_dh_047_a_repeated_message_type() { let mut m = offer(); m.pop(); m.extend_from_slice(&[53, 1, 2, 255]); - refused(&m, Counter::OptionLength); + refused(&m, Counter::OptionLength, true); } #[test] fn s_dhcp_dh_048_option_lengths() { for (code, data) in [(54, vec![192, 0, 2, 254, 0]), (1, vec![255, 255, 255]), (3, vec![0; 6]), (6, vec![]), (51, vec![0, 1])] { - refused(&offer_with(code, Some(data)), Counter::OptionLength); + refused(&offer_with(code, Some(data)), Counter::OptionLength, true); } } @@ -155,23 +166,23 @@ fn s_dhcp_dh_051_overload_misused() { let overload = hex(V_DHCP_OFFER_OVERLOAD); let mut value_4 = overload.clone(); value_4[245] = 4; - refused(&value_4, Counter::OverloadInvalid); + refused(&value_4, Counter::OverloadInvalid, true); let n = overload.len(); let mut length_2 = overload[..n - 1].to_vec(); length_2[244] = 2; length_2.extend_from_slice(&[0, 255]); - refused(&length_2, Counter::OverloadInvalid); + refused(&length_2, Counter::OverloadInvalid, true); let mut nested = overload; nested[126..130].copy_from_slice(&[52, 1, 1, 255]); - refused(&nested, Counter::OverloadInvalid); + refused(&nested, Counter::OverloadInvalid, true); } #[test] fn s_dhcp_dh_052_message_types() { - refused(&offer_with(53, None), Counter::MessageTypeMissing); - refused(&offer_with(53, Some(vec![1])), Counter::WrongDirection); - refused(&offer_with(53, Some(vec![3])), Counter::WrongDirection); - refused(&offer_with(53, Some(vec![10])), Counter::MessageTypeUnsupported); + refused(&offer_with(53, None), Counter::MessageTypeMissing, true); + refused(&offer_with(53, Some(vec![1])), Counter::WrongDirection, true); + refused(&offer_with(53, Some(vec![3])), Counter::WrongDirection, true); + refused(&offer_with(53, Some(vec![10])), Counter::MessageTypeUnsupported, true); let mut d = D::db(); d.receive(100_000, &server(0x1234_5678, A, &with(offer_options(), 53, Some(vec![9])))); assert_eq!((d.count(Counter::Forcerenew), d.count(Counter::XidMismatch)), (1, 0)); @@ -182,14 +193,14 @@ fn s_dhcp_dh_052_message_types() { fn s_dhcp_dh_053_a_foreign_client_identifier() { let mut id = CLIENT_ID.to_vec(); id[14] = 0x0b; - refused(&offer_with(61, Some(id)), Counter::ClientIdMismatch); + refused(&offer_with(61, Some(id)), Counter::ClientIdMismatch, true); } #[test] fn s_dhcp_dh_054_the_server_identifier() { - refused(&offer_with(54, None), Counter::NoServerId); + refused(&offer_with(54, None), Counter::NoServerId, true); for bad in [Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST, Ipv4Addr::new(224, 0, 0, 1), Ipv4Addr::new(127, 0, 0, 1)] { - refused(&offer_with(54, Some(ip(bad))), Counter::ServerIdInvalid); + refused(&offer_with(54, Some(ip(bad))), Counter::ServerIdInvalid, true); } } @@ -204,21 +215,21 @@ fn s_dhcp_dh_055_yiaddr_must_be_a_host() { Ipv4Addr::new(240, 0, 0, 1), Ipv4Addr::BROADCAST, ] { - refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid); + refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid, true); } } #[test] fn s_dhcp_dh_056_the_lease_time() { - refused(&offer_with(51, None), Counter::NoLeaseTime); - refused(&offer_with(51, Some(seconds(0))), Counter::LeaseZero); + refused(&offer_with(51, None), Counter::NoLeaseTime, true); + refused(&offer_with(51, Some(seconds(0))), Counter::LeaseZero, true); } #[test] fn s_dhcp_dh_057_the_mask() { - refused(&offer_with(1, None), Counter::NoSubnetMask); + refused(&offer_with(1, None), Counter::NoSubnetMask, true); for bad in [[255, 0, 255, 0], [0, 0, 0, 0], [255, 255, 255, 255]] { - refused(&offer_with(1, Some(bad.to_vec())), Counter::MaskInvalid); + refused(&offer_with(1, Some(bad.to_vec())), Counter::MaskInvalid, true); } let slash_31 = with(with(offer_options(), 1, Some(vec![255, 255, 255, 254])), 3, None); accepted(&server(XID, Ipv4Addr::new(192, 0, 2, 0), &slash_31)); @@ -228,10 +239,10 @@ fn s_dhcp_dh_057_the_mask() { #[test] fn s_dhcp_dh_058_network_and_broadcast_yiaddr() { for bad in [Ipv4Addr::new(192, 0, 2, 0), Ipv4Addr::new(192, 0, 2, 255)] { - refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid); + refused(&server(XID, bad, &offer_options()), Counter::YiaddrInvalid, true); } let slash_30 = with(offer_options(), 1, Some(vec![255, 255, 255, 252])); - refused(&server(XID, Ipv4Addr::new(192, 0, 2, 3), &slash_30), Counter::YiaddrInvalid); + refused(&server(XID, Ipv4Addr::new(192, 0, 2, 3), &slash_30), Counter::YiaddrInvalid, true); accepted(&server(XID, A, &slash_30)); } diff --git a/toyos-dhcp/tests/props.rs b/toyos-dhcp/tests/props.rs index 4f510e716ca..e50823dfb58 100644 --- a/toyos-dhcp/tests/props.rs +++ b/toyos-dhcp/tests/props.rs @@ -22,7 +22,7 @@ impl Rng { } } -/// Refusals of a whole message: a message names at most one. +/// Refusals of a whole message: a refused message names exactly one, an accepted one none. const REFUSALS: [Counter; 27] = [ Counter::Truncated, Counter::NotReply, @@ -103,8 +103,10 @@ fn run(seed: u64, host: Option) -> Seen { 3 => (d.link_up(now), false), _ => { let m = message(&mut rng, d.client.xid().unwrap_or(0)); + let phase = d.client.phase(); let out = d.receive(now, &m); - assert!(refusals(&d) - before <= 1, "seed {seed}: a message names at most one refusal"); + let refused = out == Output::default() && d.client.phase() == phase; + assert_eq!(refusals(&d) - before, u64::from(refused), "seed {seed}: a refused message names exactly one reason"); (out, false) } }; diff --git a/toyos-net-ip/src/acd.rs b/toyos-net-ip/src/acd.rs index 4a87fe676ea..ab918c64066 100644 --- a/toyos-net-ip/src/acd.rs +++ b/toyos-net-ip/src/acd.rs @@ -1,7 +1,7 @@ //! Address conflict detection (RFC 5227, §8). A new address probes on RFC 5227's schedule scaled //! by the owner's ruling (`limits::acd`), is usable from its first announcement, and is then //! defended once per DEFEND_INTERVAL and given up on a second conflict inside it (§2.4 (b)): one -//! forged packet cannot take an address away. Every interval starts when its frame leaves. +//! forged packet cannot take an address away. use core::net::Ipv4Addr; @@ -34,7 +34,7 @@ fn timer(cx: &Cx<'_>, addr: Ipv4Addr) -> Timer { } fn record(i: &mut Interface, addr: Ipv4Addr) -> Option<&mut Address> { - i.addresses.iter_mut().find(|a| a.cidr.addr == addr) + i.addresses.iter_mut().find(|a| a.cidr.addr() == addr) } /// Schedules a tentative address's first probe: a draw in [0, PROBE_WAIT], deferred past @@ -122,7 +122,7 @@ pub(crate) fn announced(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, owed /// Removes `addr` after a conflict, and tells the shell which kind. fn lose(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { let now = cx.now; - let Some(index) = i.addresses.iter().position(|a| a.cidr.addr == addr) else { return }; + let Some(index) = i.addresses.iter().position(|a| a.cidr.addr() == addr) else { return }; let removed = i.addresses.remove(index); cx.timers.cancel(timer(cx, addr)); cx.log.refuse(Counter::AcdConflict, cx.iface, Peer::Arp { ip: addr, mac }); @@ -156,7 +156,7 @@ pub(crate) fn conflict(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: /// The link came up: held addresses stay usable and are announced twice, and probing starts for /// any address added while it was down (§8.6). pub(crate) fn link_up(i: &mut Interface, cx: &mut Cx<'_>) { - let addrs: alloc::vec::Vec<(Ipv4Addr, bool)> = i.addresses.iter().map(|a| (a.cidr.addr, a.usable())).collect(); + let addrs: alloc::vec::Vec<(Ipv4Addr, bool)> = i.addresses.iter().map(|a| (a.cidr.addr(), a.usable())).collect(); for (addr, usable) in addrs { if usable { if let Some(Address { phase: Phase::Usable { owed, .. }, .. }) = record(i, addr) { @@ -173,13 +173,13 @@ pub(crate) fn link_up(i: &mut Interface, cx: &mut Cx<'_>) { pub(crate) fn link_down(i: &mut Interface, cx: &mut Cx<'_>) { let iface = cx.iface; for a in &mut i.addresses { - cx.timers.cancel(Timer::Acd(iface, a.cidr.addr)); + cx.timers.cancel(Timer::Acd(iface, a.cidr.addr())); if let Phase::Usable { owed, queued, .. } = &mut a.phase { *owed = 0; *queued = false; } } - let tentative = i.addresses.iter().filter(|a| !a.usable()).map(|a| a.cidr.addr); + let tentative = i.addresses.iter().filter(|a| !a.usable()).map(|a| a.cidr.addr()); for addr in tentative.collect::>() { cx.log.event(Event::NotVerified { iface, addr }); } diff --git a/toyos-net-ip/src/addr.rs b/toyos-net-ip/src/addr.rs index b25fac3fcd6..d4518eb79b3 100644 --- a/toyos-net-ip/src/addr.rs +++ b/toyos-net-ip/src/addr.rs @@ -1,55 +1,8 @@ -//! An interface's address records, and the address classes every rule reads (RFC 1122 -//! §3.2.1.3, RFC 6890, RFC 3021). - -use core::net::Ipv4Addr; +//! An interface's address records. +use toyos_net_wire::addr::Cidr; use toyos_net_wire::Instant; -/// Not 0.0.0.0/8, 127.0.0.0/8, 224.0.0.0/4 or 240.0.0.0/4 (which holds 255.255.255.255): an -/// address that can name one host. -pub(crate) const fn is_host(addr: Ipv4Addr) -> bool { - let [first, ..] = addr.octets(); - !(first == 0 || first == 127 || first >= 224) -} - -/// 240.0.0.0/4 but the limited broadcast. -pub(crate) const fn is_class_e(addr: Ipv4Addr) -> bool { - let [first, ..] = addr.octets(); - first >= 240 && !addr.is_broadcast() -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct Cidr { - pub addr: Ipv4Addr, - /// 1 to 32. - pub len: u8, -} - -impl Cidr { - fn mask(self) -> u32 { - u32::MAX.checked_shl(u32::from(32u8.saturating_sub(self.len))).unwrap_or(0) - } - - pub fn contains(self, other: Ipv4Addr) -> bool { - (u32::from(self.addr) ^ u32::from(other)) & self.mask() == 0 - } - - pub fn network(self) -> Ipv4Addr { - Ipv4Addr::from(u32::from(self.addr) & self.mask()) - } - - /// Host bits all one; a /31 or /32 has none (RFC 3021 §2.2.1). - pub fn broadcast(self) -> Option { - (self.len <= 30).then(|| Ipv4Addr::from(u32::from(self.addr) | !self.mask())) - } - - /// The network or directed-broadcast address of a prefix of /30 or shorter, which no host - /// may use. - pub fn is_edge(self, addr: Ipv4Addr) -> bool { - self.len <= 30 && self.contains(addr) && (addr == self.network() || Some(addr) == self.broadcast()) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AddrState { /// Probed for conflicts: not a source, not for us, answers no ARP. diff --git a/toyos-net-ip/src/arp.rs b/toyos-net-ip/src/arp.rs index 35c0958136f..a04fb60cdd1 100644 --- a/toyos-net-ip/src/arp.rs +++ b/toyos-net-ip/src/arp.rs @@ -3,10 +3,10 @@ //! any other packet can confirm, assert a MAC into STALE, or be ignored, and a MAC change is //! always logged (§7.3). +use toyos_net_wire::addr::is_host; use toyos_net_wire::arp::{Arp, Operation}; use toyos_net_wire::ethernet::MacClass; -use crate::addr::is_host; use crate::counters::Counter; use crate::iface::{Cx, Interface}; use crate::{acd, egress, nud}; diff --git a/toyos-net-ip/src/config.rs b/toyos-net-ip/src/config.rs index 3a285d26203..c10f73f2ed5 100644 --- a/toyos-net-ip/src/config.rs +++ b/toyos-net-ip/src/config.rs @@ -5,10 +5,11 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; use core::net::Ipv4Addr; +use toyos_net_wire::addr::{is_host, Cidr}; use toyos_net_wire::ipv4::MulticastAddr; use toyos_net_wire::Instant; -use crate::addr::{is_host, Address, Cidr, Phase}; +use crate::addr::{Address, Phase}; use crate::counters::Counter; use crate::nud::Link; use crate::route::{self, NextHop, Source}; @@ -26,24 +27,17 @@ impl Ip { /// and keeps its state, which is how a renewal arrives (§2.2 (4)). pub fn add_address(&mut self, now: Instant, iface: IfIndex, addr: Ipv4Addr, prefix_len: u8) -> Result<(), Counter> { let now = self.clock(now); - let cidr = Cidr { addr, len: prefix_len }; - let refusal = if !(1..=32).contains(&prefix_len) { - Some(Counter::AddrPrefixInvalid) - } else if !is_host(addr) || cidr.is_edge(addr) { - Some(Counter::AddrNotUnicast) - } else if self.ifaces.iter().enumerate().any(|(n, i)| n != iface.0 && i.owns(addr)) { - Some(Counter::AddrDuplicate) - } else { - None + let cidr = match Cidr::new(addr, prefix_len) { + None => Err(Counter::AddrPrefixInvalid), + Some(cidr) if !is_host(addr) || cidr.is_edge(addr) => Err(Counter::AddrNotUnicast), + Some(_) if self.ifaces.iter().enumerate().any(|(n, i)| n != iface.0 && i.owns(addr)) => Err(Counter::AddrDuplicate), + Some(cidr) => Ok(cidr), }; - if let Some(refusal) = refusal { - self.log.count(refusal); - return Err(refusal); - } + let cidr = cidr.inspect_err(|&refusal| self.log.count(refusal))?; let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; - if let Some(held) = i.addresses.iter_mut().find(|a| a.cidr.addr == addr) { - if held.cidr.len != prefix_len { - held.cidr.len = prefix_len; + if let Some(held) = i.addresses.iter_mut().find(|a| a.cidr.addr() == addr) { + if held.cidr != cidr { + held.cidr = cidr; if held.usable() { route::withdraw_off_link(i, &mut cx); cx.bump(); @@ -65,7 +59,7 @@ impl Ip { pub fn remove_address(&mut self, now: Instant, iface: IfIndex, addr: Ipv4Addr) -> Result<(), Counter> { let now = self.clock(now); let Some((i, mut cx)) = self.split(now, iface) else { return Err(self.unknown()) }; - let Some(index) = i.addresses.iter().position(|a| a.cidr.addr == addr) else { + let Some(index) = i.addresses.iter().position(|a| a.cidr.addr() == addr) else { cx.log.count(Counter::AddrUnknown); return Err(Counter::AddrUnknown); }; diff --git a/toyos-net-ip/src/counters.rs b/toyos-net-ip/src/counters.rs index c24b6a2ea27..8029f1d12b8 100644 --- a/toyos-net-ip/src/counters.rs +++ b/toyos-net-ip/src/counters.rs @@ -61,6 +61,7 @@ toyos_net_wire::counters! { NbPendingOverflow = "nb.pending-overflow"; NbPendingFull = "nb.pending-full"; NbPendingDropped = "nb.pending-dropped"; + NbPendingEvicted = "nb.pending-evicted"; NbFailedRefused = "nb.failed-refused"; NbTableFull = "nb.table-full"; NbResolved = "nb.resolved"; diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs index 381f51859de..07089c5873e 100644 --- a/toyos-net-ip/src/egress.rs +++ b/toyos-net-ip/src/egress.rs @@ -1,9 +1,11 @@ //! Egress (§5). [ip]'s own frames wait in one FIFO the transmit opportunity drains ahead of the //! data flows: an ARP or IGMP frame is built when it leaves, from the state of that moment, and -//! the timer it feeds starts then. The FIFO holds at most CONTROL_QUEUE of [ip]'s own frames, at -//! most ECHO_REPLIES of them echo replies; a frame past either is dropped and counted, and its -//! producer moves on as if it had left. A datagram released by resolution is bounded where it -//! was held and is never dropped here. +//! the timer it feeds starts then. The FIFO holds at most CONTROL_QUEUE frames, at most +//! ECHO_REPLIES of them echo replies; one of [ip]'s own frames past either is dropped and +//! counted, and its producer moves on as if it had left. A datagram resolution releases into a +//! full FIFO is not dropped: it stays at the head of its neighbour's pending queue, within +//! PENDING_TOTAL, and enters as room appears, before any frame queued after it (§5.4). It is its +//! neighbour's until it has left. //! //! Every datagram is atomic: DF set, identification 0 (`toyos-net-wire`'s one IPv4 form). @@ -43,7 +45,7 @@ pub(crate) enum Item { Announce { iface: IfIndex, addr: Ipv4Addr, owed: bool }, Igmp { iface: IfIndex, report: igmp::Report }, Frame { iface: IfIndex, frame: Vec, kind: FrameKind }, - Released { iface: IfIndex, held: Held }, + Released(Released), } impl Item { @@ -55,19 +57,39 @@ impl Item { | Self::Announce { iface, .. } | Self::Igmp { iface, .. } | Self::Frame { iface, .. } - | Self::Released { iface, .. } => *iface, + | Self::Released(Released { iface, .. }) => *iface, } } } +/// A datagram resolution released: still its neighbour's, whose pending queue holds it until it +/// has left (§6.5). +#[derive(Debug)] +pub(crate) struct Released { + iface: IfIndex, + next_hop: Ipv4Addr, + held: Held, +} + +impl Released { + fn toward(&self, iface: IfIndex, next_hop: Ipv4Addr) -> bool { + self.iface == iface && self.next_hop == next_hop + } +} + #[derive(Debug, Default)] pub(crate) struct Control { items: VecDeque, - own: usize, echoes: usize, + /// Released datagrams that found `items` full, in release order: empty whenever it has room. + waiting: VecDeque, } impl Control { + fn full(&self) -> bool { + self.items.len() >= CONTROL_QUEUE + } + /// Queues one of [ip]'s own frames; `false` when it was dropped and counted. pub fn push(&mut self, item: Item, log: &mut Log) -> bool { let echo = matches!(item, Item::Frame { kind: FrameKind::Echo, .. }); @@ -75,11 +97,10 @@ impl Control { log.count(Counter::IcmpEchoReplyDropped); return false; } - if self.own >= CONTROL_QUEUE { + if self.full() { log.count(Counter::IpControlQueueFull); return false; } - self.own = self.own.saturating_add(1); if echo { self.echoes = self.echoes.saturating_add(1); } @@ -87,28 +108,61 @@ impl Control { true } - /// A datagram resolution released, in the order it was held. - pub fn release(&mut self, iface: IfIndex, held: Held) { - self.items.push_back(Item::Released { iface, held }); + /// A datagram resolution released toward `next_hop`, in the order it was held. + pub fn release(&mut self, iface: IfIndex, next_hop: Ipv4Addr, held: Held) { + let released = Released { iface, next_hop, held }; + if self.full() { + self.waiting.push_back(released); + } else { + self.items.push_back(Item::Released(released)); + } + } + + /// Whether datagrams released toward `next_hop` have yet to leave. + pub fn holds(&self, iface: IfIndex, next_hop: Ipv4Addr) -> bool { + self.waiting.iter().any(|r| r.toward(iface, next_hop)) + || self.items.iter().any(|item| matches!(item, Item::Released(r) if r.toward(iface, next_hop))) + } + + /// Hands back the datagrams released toward `next_hop` that have yet to leave: its entry is + /// gone. + pub fn take(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> Vec { + let (items, kept): (Vec, Vec) = self.items.drain(..).partition(|item| matches!(item, Item::Released(r) if r.toward(iface, next_hop))); + self.items = kept.into(); + let (waiting, kept): (Vec, Vec) = self.waiting.drain(..).partition(|r| r.toward(iface, next_hop)); + self.waiting = kept.into(); + self.refill(); + Self::datagrams(items).chain(waiting.into_iter().map(|r| r.held)).collect() } fn pop(&mut self) -> Option { let item = self.items.pop_front()?; self.forget(&item); + self.refill(); Some(item) } fn forget(&mut self, item: &Item) { - match item { - Item::Released { .. } => {} - Item::Frame { kind: FrameKind::Echo, .. } => { - self.own = self.own.saturating_sub(1); - self.echoes = self.echoes.saturating_sub(1); - } - _ => self.own = self.own.saturating_sub(1), + if matches!(item, Item::Frame { kind: FrameKind::Echo, .. }) { + self.echoes = self.echoes.saturating_sub(1); } } + /// Room goes to the oldest released datagram waiting for it. + fn refill(&mut self) { + while !self.full() { + let Some(released) = self.waiting.pop_front() else { return }; + self.items.push_back(Item::Released(released)); + } + } + + fn datagrams(items: Vec) -> impl Iterator { + items.into_iter().filter_map(|item| match item { + Item::Released(r) => Some(r.held), + _ => None, + }) + } + /// Drops everything waiting for `iface`, whose link went down, and hands back the datagrams /// resolution had released. pub fn purge(&mut self, iface: IfIndex) -> Vec { @@ -117,12 +171,10 @@ impl Control { for item in &gone { self.forget(item); } - gone.into_iter() - .filter_map(|item| match item { - Item::Released { held, .. } => Some(held), - _ => None, - }) - .collect() + let (waiting, kept): (Vec, Vec) = self.waiting.drain(..).partition(|r| r.iface == iface); + self.waiting = kept.into(); + self.refill(); + Self::datagrams(gone).chain(waiting.into_iter().map(|r| r.held)).collect() } } @@ -210,7 +262,7 @@ impl Ip { left } Item::Probe { addr, .. } => { - let queued = i.addresses.iter().any(|a| a.cidr.addr == addr && matches!(a.phase, crate::addr::Phase::Tentative { queued: true, .. })); + let queued = i.addresses.iter().any(|a| a.cidr.addr() == addr && matches!(a.phase, crate::addr::Phase::Tentative { queued: true, .. })); if !queued { return false; } @@ -239,10 +291,13 @@ impl Ip { sent(&mut cx, kind); true } - Item::Released { held, .. } => { + Item::Released(Released { next_hop, held, .. }) => { i.held = i.held.saturating_sub(1); sink(iface, &held.frame); sent(&mut cx, held.kind); + if !cx.control.holds(iface, next_hop) { + nud::drained(i, &mut cx, next_hop); + } true } } diff --git a/toyos-net-ip/src/icmp.rs b/toyos-net-ip/src/icmp.rs index 2379c19ed1b..ef9fcefd1bb 100644 --- a/toyos-net-ip/src/icmp.rs +++ b/toyos-net-ip/src/icmp.rs @@ -4,12 +4,12 @@ //! not one host, and only when the destination's bucket and the global one both have a token. //! An inbound error is handed on only once its quote names one of our datagrams. +use toyos_net_wire::addr::is_host; use toyos_net_wire::ethernet::MacClass; use toyos_net_wire::icmp::{Echo, EchoBuilder, HostUnreachable, IcmpError, IcmpMessage, IcmpPacket, Quote, UnreachableBuilder, UnreachableCode}; use toyos_net_wire::ipv4::{Ecn, Ipv4Builder, Ipv4Packet, Ipv4Source, Protocol, TrafficClass, Ttl}; use toyos_net_wire::{Instant, Port}; -use crate::addr::is_host; use crate::counters::Counter; use crate::egress::FrameKind; use crate::route::Source; diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs index aadd24b77fc..357c39ce461 100644 --- a/toyos-net-ip/src/iface.rs +++ b/toyos-net-ip/src/iface.rs @@ -54,11 +54,11 @@ impl Interface { } pub fn is_usable(&self, addr: Ipv4Addr) -> bool { - self.usable().any(|a| a.cidr.addr == addr) + self.usable().any(|a| a.cidr.addr() == addr) } pub fn owns(&self, addr: Ipv4Addr) -> bool { - self.addresses.iter().any(|a| a.cidr.addr == addr) + self.addresses.iter().any(|a| a.cidr.addr() == addr) } /// Inside the prefix of a usable address: reachable without a gateway. @@ -68,7 +68,7 @@ impl Interface { /// §3.5 rules 1 and 2: 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) + self.usable().find(|a| a.cidr.contains(toward)).or_else(|| self.usable().next()).map(|a| a.cidr.addr()) } /// The directed broadcast of one of its usable prefixes. diff --git a/toyos-net-ip/src/igmp.rs b/toyos-net-ip/src/igmp.rs index 423e4c3c9b2..5f9e9296e65 100644 --- a/toyos-net-ip/src/igmp.rs +++ b/toyos-net-ip/src/igmp.rs @@ -371,8 +371,9 @@ fn query(i: &mut Interface, cx: &mut Cx<'_>, packet: &Ipv4Packet<'_>, query: &Qu QueryGroup::General => return cx.timers.arm(timer(cx, Timer::General), at), QueryGroup::Specific(group) => group, }; + // One source past the cap is enough to know the list overflows (§10.5 (5)). let sources: Vec = match &query.version { - QueryVersion::V3(v3) => v3.sources().collect(), + QueryVersion::V3(v3) => v3.sources().take(IGMP_QUERY_SOURCES.saturating_add(1)).collect(), QueryVersion::V1 | QueryVersion::V2 => Vec::new(), }; let key = timer(cx, Timer::Response(group)); @@ -381,7 +382,14 @@ fn query(i: &mut Interface, cx: &mut Cx<'_>, packet: &Ipv4Packet<'_>, query: &Qu None => sources, Some(list) if sources.is_empty() || list.is_empty() => Vec::new(), Some(mut list) => { - list.extend(sources.into_iter().filter(|s| !list.contains(s)).collect::>()); + for source in sources { + if list.len() > IGMP_QUERY_SOURCES { + break; + } + if !list.contains(&source) { + list.push(source); + } + } list } }; @@ -450,7 +458,7 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, t: Timer, batch: &mut Vec /// Builds `report` as it leaves, from the interface's source of this moment (0.0.0.0 while it /// has none, RFC 9776 §4.2.14); a change record overtaken by a newer change is left out. pub(crate) fn emit(i: &mut Interface, cx: &mut Cx<'_>, report: &Report, out: &mut [u8]) -> Option { - let source = Ipv4Source::new(i.usable().next().map_or(Ipv4Addr::UNSPECIFIED, |a| a.cidr.addr)).ok()?; + let source = Ipv4Source::new(i.usable().next().map_or(Ipv4Addr::UNSPECIFIED, |a| a.cidr.addr())).ok()?; let mac = i.mac; let current = |i: &Interface, group: MulticastAddr, seq: u32| { i.igmp.groups.get(&group).and_then(|g| g.change).is_some_and(|c| c.seq == seq && c.queued) diff --git a/toyos-net-ip/src/input.rs b/toyos-net-ip/src/input.rs index e57faf1ae5a..477b4dbdffc 100644 --- a/toyos-net-ip/src/input.rs +++ b/toyos-net-ip/src/input.rs @@ -3,6 +3,7 @@ use core::net::Ipv4Addr; +use toyos_net_wire::addr::{is_host, is_martian}; use toyos_net_wire::arp::Arp; use toyos_net_wire::ethernet::{EtherType, Frame, MacAddr, MacClass, TagProtocol, Tags}; use toyos_net_wire::icmp::HostUnreachable; @@ -12,7 +13,6 @@ use toyos_net_wire::tcp::TcpSegment; use toyos_net_wire::udp::UdpDatagram; use toyos_net_wire::Instant; -use crate::addr::{is_class_e, is_host}; use crate::counters::Counter; use crate::iface::Interface; use crate::{arp, igmp, Arrival, Cast, Delivery, IfIndex, Ip, Peer}; @@ -45,10 +45,11 @@ fn dispatch(i: &Interface, frame: &Frame<'_>) -> Option { } } -/// The acquisition exception (ruling H-1): before the interface holds a usable address, a -/// unicast datagram to UDP port 68 in a frame to our MAC is admitted for the DHCP client alone. -fn acquisition(i: &Interface, link: MacClass, packet: &Ipv4Packet<'_>) -> bool { - link == MacClass::Individual +/// The acquisition exception (C-3): before the interface holds a usable address, a datagram to +/// UDP port 68 whose destination names one host, and so (step 3) came in a frame to our MAC, is +/// admitted for the DHCP client alone. +fn acquisition(i: &Interface, packet: &Ipv4Packet<'_>) -> bool { + is_host(packet.destination()) && i.usable().next().is_none() && packet.protocol() == Protocol::Udp && !packet.is_fragment() @@ -64,8 +65,7 @@ fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packe MacClass::Group if !group => return Err(Counter::IpUnicastInLinkMulticast), MacClass::Individual | MacClass::Broadcast | MacClass::Group => {} } - let [first, ..] = destination.octets(); - if first == 0 || first == 127 || is_class_e(destination) { + if is_martian(destination) { return Err(Counter::IpMartianDestination); } let cast = if i.is_usable(destination) { @@ -78,7 +78,7 @@ fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packe Cast::Multicast(g) } else if i.owns(destination) { return Err(Counter::IpTentativeDestination); - } else if acquisition(i, link, packet) { + } else if acquisition(i, packet) { Cast::Acquisition } else { return Err(Counter::IpNotForUs); diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs index 58bcb422076..68a28032b18 100644 --- a/toyos-net-ip/src/lib.rs +++ b/toyos-net-ip/src/lib.rs @@ -355,11 +355,6 @@ impl Ip { &self.log.counters } - /// How many refusals of the wire rule `name` (a `toyos_net_wire` reason's `name()`) [ip] met. - pub fn wire_refusals(&self, name: &str) -> u64 { - self.log.wire.get(name).copied().unwrap_or(0) - } - /// Every wire refusal [ip] met, by name, for inspect. pub fn wire_counters(&self) -> impl Iterator + '_ { self.log.wire.iter().map(|(&name, &n)| (name, n)) @@ -380,12 +375,12 @@ impl Ip { } pub fn address(&self, iface: IfIndex, addr: Ipv4Addr) -> Option { - self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr == addr).map(|a| a.state()) + self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr() == addr).map(|a| a.state()) } /// The prefix length of one of the interface's addresses. pub fn prefix_len(&self, iface: IfIndex, addr: Ipv4Addr) -> Option { - self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr == addr).map(|a| a.cidr.len) + self.ifaces.get(iface.0)?.addresses.iter().find(|a| a.cidr.addr() == addr).map(|a| a.cidr.prefix_len()) } pub fn reachable_time(&self, iface: IfIndex) -> Option { @@ -398,12 +393,12 @@ impl Ip { /// An announcing or assigned address of any interface: one a socket may bind and send from. pub fn is_assigned(&self, addr: Ipv4Addr) -> bool { - self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr == addr && a.usable())) + self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr() == addr && a.usable())) } /// One of our addresses in any state. pub fn is_local(&self, addr: Ipv4Addr) -> bool { - self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr == addr)) + self.ifaces.iter().any(|i| i.addresses.iter().any(|a| a.cidr.addr() == addr)) } /// The directed broadcast of a usable prefix of /30 or shorter. @@ -427,7 +422,8 @@ impl Ip { // Each compile_fail block sits beside one that compiles, so a typo cannot pass it. #[cfg(doctest)] mod compile_fail { - /// NUD-28: a MAC exists only in a state that has one, and a pending queue only in INCOMPLETE. + /// NUD-28: a MAC exists only in a state that has one, and only INCOMPLETE takes datagrams into a + /// pending queue. /// /// ``` /// use toyos_net_ip::{Linked, Nud}; @@ -435,12 +431,13 @@ mod compile_fail { /// match n { /// Nud::Incomplete(i) => i.queued(), /// Nud::Reachable(r) => usize::from(r.mac().0[0]), - /// Nud::Stale(s) => { + /// Nud::Stale(s) | Nud::Delay(s) => { /// let _: &Linked = s; /// 2 /// } + /// Nud::Probe(p) => usize::from(p.requests()), + /// Nud::Unreachable(u) => usize::from(u.quiescent()), /// Nud::Failed => 0, - /// _ => 1, /// } /// } /// ``` @@ -464,6 +461,16 @@ mod compile_fail { /// use toyos_net_ip::Nud; /// fn f(n: &Nud) { if let Nud::Stale(s) = n { let _ = s.queued(); } } /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Probe(p) = n { let _ = p.queued(); } } + /// ``` + /// + /// ```compile_fail + /// use toyos_net_ip::Nud; + /// fn f(n: &Nud) { if let Nud::Unreachable(u) = n { let _ = u.queued(); } } + /// ``` #[allow(non_camel_case_types)] pub struct s_ip_nud_028_a_state_holds_only_its_own_fields; } diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 5300b9f5730..586438078f2 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -5,8 +5,10 @@ //! measured from each request's hand-off; a timer fires once per `fire` and its next deadline is //! taken from that moment, so a jumped clock never replays missed periods. //! -//! Each state carries exactly its own fields: a MAC only where one is known, a pending queue -//! only while INCOMPLETE. +//! Each state carries exactly its own fields: a MAC only where one is known, and a pending queue +//! that takes datagrams only while INCOMPLETE. What resolution released into a full control +//! queue waits at egress, which only drains it (§6.5); until it has, its entry does not idle out +//! and is evicted only after every other candidate (§6.8). use alloc::collections::VecDeque; use alloc::vec::Vec; @@ -199,24 +201,37 @@ pub(crate) fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } /// Makes room for one more entry, evicting FAILED, then quiescent UNREACHABLE, then STALE, each -/// the one unused longest (§6.8). +/// the one unused longest, and after all of them one whose pending queue still holds released +/// datagrams (§6.8). fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { if i.neighbours.len() < TABLE_MAX { return true; } - let class = |n: &Neighbour| match &n.state { - Nud::Failed => Some(0), - Nud::Unreachable(u) if u.quiescent() && !n.queued => Some(1), - Nud::Stale(_) => Some(2), - _ => None, + let class = |addr: Ipv4Addr, n: &Neighbour| { + let class = match &n.state { + Nud::Failed => 0, + Nud::Unreachable(u) if u.quiescent() && !n.queued => 1, + Nud::Stale(_) => 2, + _ => return None, + }; + Some(if cx.control.holds(cx.iface, addr) { 3 } else { class }) }; - let victim = i.neighbours.iter().filter_map(|(a, n)| class(n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a); + let victim = i.neighbours.iter().filter_map(|(a, n)| class(*a, n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a); let Some(victim) = victim else { return false }; - i.neighbours.remove(&victim); - cx.timers.cancel(timer(cx, victim)); + remove(i, cx, victim); true } +/// Deletes `addr`'s entry, and with it the released datagrams its pending queue still holds. +fn remove(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + i.neighbours.remove(&addr); + cx.timers.cancel(timer(cx, addr)); + for held in cx.control.take(cx.iface, addr) { + i.held = i.held.saturating_sub(1); + drop_held(cx, held, Counter::NbPendingEvicted); + } +} + /// A datagram or a flow wants `addr` now (§6.3 "send"): creates INCOMPLETE, moves STALE to /// DELAY, or asks a quiescent UNREACHABLE for a request. pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Option) -> Link { @@ -314,13 +329,26 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { s.backoff = false; cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } + // Not idle while released datagrams are its: `drained` deletes it once they have left. + Nud::Stale(_) | Nud::Unreachable(_) if cx.control.holds(cx.iface, addr) => {} Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => { - i.neighbours.remove(&addr); + remove(i, cx, addr); route::refresh_active(i, cx); } } } +/// The last datagram released toward `addr` left. Every STALE or quiescent UNREACHABLE entry has +/// a deadline armed but one whose idle lifetime passed while it held them, which goes now. +pub(crate) fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { + let Some(n) = i.neighbours.get(&addr) else { return }; + let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.queued && cx.timers.get(timer(cx, addr)).is_none(); + if idle { + remove(i, cx, addr); + route::refresh_active(i, cx); + } +} + /// PROBE's first unicast request, now or at the spacing boundary. fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { if at <= cx.now { @@ -332,8 +360,8 @@ fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { } /// A held datagram that will never leave: counted, and its sender told (§6.5, §9.6). -pub(crate) fn drop_held(cx: &mut Cx<'_>, held: Held) { - cx.log.count(Counter::NbPendingDropped); +fn drop_held(cx: &mut Cx<'_>, held: Held, counter: Counter) { + cx.log.count(counter); if let Some(flow) = held.flow { cx.log.event(Event::Unreachable(flow)); } @@ -344,7 +372,7 @@ fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: VecDeque, addr: Ipv4Addr, mac: MacAddr, pending: VecDeque() { *destination = mac.0; } - cx.control.release(cx.iface, held); + cx.control.release(cx.iface, addr, held); } cx.log.count(Counter::NbResolved); cx.log.event(Event::Resolved { iface: cx.iface, next_hop: addr }); @@ -467,12 +495,12 @@ pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>) { cx.timers.cancel(timer(cx, addr)); if let Nud::Incomplete(s) = n.state { for held in s.pending { - drop_held(cx, held); + drop_held(cx, held, Counter::NbPendingDropped); } } } for held in cx.control.purge(cx.iface) { - drop_held(cx, held); + drop_held(cx, held, Counter::NbPendingDropped); } i.held = 0; } diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs index b6ec1b09585..f260510c661 100644 --- a/toyos-net-ip/src/route.rs +++ b/toyos-net-ip/src/route.rs @@ -5,10 +5,10 @@ use core::net::Ipv4Addr; +use toyos_net_wire::addr::{is_host, is_martian, Cidr}; use toyos_net_wire::ipv4::MulticastAddr; use toyos_net_wire::Instant; -use crate::addr::{is_class_e, is_host}; use crate::counters::Counter; use crate::iface::{Cx, Interface}; use crate::nud::Nud; @@ -66,8 +66,7 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source }; return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Broadcast, source }); } - let [first, ..] = destination.octets(); - if first == 0 || first == 127 || is_class_e(destination) { + if is_martian(destination) { return Err(Counter::RouteInvalidDestination); } if ifaces.iter().any(|i| i.owns(destination)) { @@ -85,8 +84,8 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source } let connected = candidates() .flat_map(|(index, i)| i.usable().filter(|a| a.cidr.contains(destination)).map(move |a| (index, i, a.cidr))) - .fold(None, |best: Option<(usize, &Interface, crate::addr::Cidr)>, (index, i, cidr)| match best { - Some((_, _, b)) if b.len >= cidr.len => best, + .fold(None, |best: Option<(usize, &Interface, Cidr)>, (index, i, cidr)| match best { + Some((_, _, b)) if b.prefix_len() >= cidr.prefix_len() => best, _ => Some((index, i, cidr)), }); if let Some((index, i, cidr)) = connected { @@ -142,7 +141,7 @@ impl Ip { /// refused entry is refused whole and the old one kept (§3.2). pub fn set_gateways(&mut self, now: Instant, iface: IfIndex, gateways: &[Ipv4Addr]) -> Result<(), Counter> { let now = self.clock(now); - let local: alloc::vec::Vec = self.ifaces.iter().flat_map(|i| i.addresses.iter().map(|a| a.cidr.addr)).collect(); + let local: alloc::vec::Vec = self.ifaces.iter().flat_map(|i| i.addresses.iter().map(|a| a.cidr.addr())).collect(); let Some((i, mut cx)) = self.split(now, iface) else { self.log.count(Counter::UnknownInterface); return Err(Counter::UnknownInterface); diff --git a/toyos-net-ip/tests/acd.rs b/toyos-net-ip/tests/acd.rs index 676740dd1a5..f58a8d6f723 100644 --- a/toyos-net-ip/tests/acd.rs +++ b/toyos-net-ip/tests/acd.rs @@ -6,7 +6,7 @@ use std::net::Ipv4Addr; use std::time::Duration; use common::*; -use toyos_net_ip::limits::acd::{ANNOUNCE_INTERVAL, ANNOUNCE_WAIT, PROBE_MAX, PROBE_MIN, PROBE_WAIT, RATE_LIMIT_INTERVAL}; +use toyos_net_ip::limits::acd::{ANNOUNCE_INTERVAL, ANNOUNCE_WAIT, PROBE_MAX, PROBE_MIN, PROBE_WAIT}; use toyos_net_ip::{AddrState, Counter, Event, Instant, Peer, Source}; use toyos_net_wire::ethernet::MacAddr; @@ -164,7 +164,7 @@ fn s_ip_acd_008_ten_conflicts_rate_limit_new_candidates() { let added = tenth.after(Duration::from_secs(5)); h.ip.add_address(added, h.if0, ip4(192, 0, 2, 40), 24).unwrap(); assert_eq!(h.count(Counter::AcdRateLimited), 1); - assert_eq!(h.ip.next_deadline(), Some(tenth.after(RATE_LIMIT_INTERVAL))); + assert_eq!(h.ip.next_deadline(), Some(tenth.after(Duration::from_millis(60_000)))); } #[test] diff --git a/toyos-net-ip/tests/common/mod.rs b/toyos-net-ip/tests/common/mod.rs index 30cfea5380e..6ac7f82f7ba 100644 --- a/toyos-net-ip/tests/common/mod.rs +++ b/toyos-net-ip/tests/common/mod.rs @@ -327,7 +327,8 @@ impl H { } pub fn wire(&self, name: &str) -> u64 { - self.ip.wire_refusals(name) - self.wire.get(name).copied().unwrap_or(0) + let now = self.ip.wire_counters().find(|(n, _)| *n == name).map_or(0, |(_, v)| v); + now - self.wire.get(name).copied().unwrap_or(0) } /// A frame received on if0 at the current time. diff --git a/toyos-net-ip/tests/igmp.rs b/toyos-net-ip/tests/igmp.rs index 7e7b49263f2..869c64a2d9a 100644 --- a/toyos-net-ip/tests/igmp.rs +++ b/toyos-net-ip/tests/igmp.rs @@ -5,7 +5,7 @@ mod common; use std::net::Ipv4Addr; use common::*; -use toyos_net_ip::{Counter, IgmpMode, Peer, RefusalLog}; +use toyos_net_ip::{limits, Counter, Event, IgmpMode, Peer, RefusalLog}; use toyos_net_wire::ethernet::MacAddr; use toyos_net_wire::ipv4::MulticastAddr; @@ -523,6 +523,14 @@ fn s_ip_mod_001_one_line_per_rule_per_10_s() { let times = (0..100u64).map(|n| n * 10).chain([10_001]); let lines: Vec = refusals.iter().zip(times).filter_map(|(r, t)| log.admit(H::instant(t), r.rule)).collect(); assert_eq!(lines, [0, 99]); + + let frame = to_a(&hex(V_IP_LSRR)); + for _ in 0..=limits::EVENTS { + let _ = h.ip.receive(h.clock(), h.if0, &frame); + } + let named = h.ip.drain_events().filter(|e| matches!(e, Event::Refused(_))).count(); + assert_eq!(named, limits::EVENTS, "an undrained shell holds a bounded list"); + assert_eq!(h.count(Counter::IpEventOverflow), 1); } #[test] @@ -533,4 +541,23 @@ fn s_ip_mod_002_ordinary_refusals_are_not_logged() { } assert_eq!(h.count(Counter::IpNotForUs), 1_000); assert!(h.events.iter().all(|e| !matches!(e, toyos_net_ip::Event::Refused(_)))); + + let mut logged: Vec<&str> = Counter::ALL.iter().filter(|c| c.logged()).map(|c| c.name()).collect(); + logged.sort_unstable(); + let mut section_13_2 = [ + "ip.source-route", + "ip.fragment", + "icmp.redirect", + "icmp.timestamp-request", + "icmp.source-quench", + "igmp.v1-query", + "igmp.query-no-router-alert", + "igmp.general-query-destination", + "arp.mac-changed", + "arp.override-locked", + "acd.conflict", + "acd.defended", + ]; + section_13_2.sort_unstable(); + assert_eq!(logged, section_13_2, "§13.2's logged refusals, and no others"); } diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index 2fc0974d127..d7007dd5778 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -103,8 +103,14 @@ fn s_ip_in_008_fragments_before_options() { assert_eq!(h.count(Counter::IpSourceRoute), 0); } +/// A SYN from B:40000 to `destination`:80. +fn syn_to(destination: Ipv4Addr) -> Vec { + let tcp = [0x9c, 0x40, 0, 80, 0, 0, 0, 1, 0, 0, 0, 0, 0x50, 0x02, 0xff, 0xff, 0, 0, 0, 0]; + ipv4(B, destination, 6, &tcp) +} + #[test] -fn s_ip_in_009_the_delivery_carries_the_tos() { +fn s_ip_in_009_what_a_transport_is_handed() { let mut h = H::fixture_i(); let Some(Delivery::Udp(arrival, datagram)) = h.datagram(&hex(V_IP_DSCP)) else { panic!("not delivered") }; assert_eq!(arrival.packet.traffic_class().dscp(), 46); @@ -112,6 +118,12 @@ fn s_ip_in_009_the_delivery_carries_the_tos() { assert_eq!(arrival.cast, Cast::Unicast); assert_eq!(arrival.iface, h.if0); assert_eq!(datagram.destination_port().get(), 5001); + + assert!(matches!(h.datagram(&syn_to(A)), Some(Delivery::Tcp(..)))); + for (mac, destination) in [(MacAddr::BROADCAST, ip4(255, 255, 255, 255)), (MacAddr::BROADCAST, ip4(192, 0, 2, 255)), (MDNS_MAC, MDNS)] { + assert!(h.datagram_to(mac, &syn_to(destination)).is_none(), "tcp.md MUST-57: {destination} reached TCP"); + } + assert_eq!(h.count(Counter::IpTcpNotUnicast), 3); } #[test] @@ -518,25 +530,29 @@ fn s_ip_out_006_the_control_queue_is_bounded() { let first = h.out_with(1); assert!(first.len() == 1 && first[0].requests(ip4(192, 0, 2, 100)), "one credit, the oldest frame"); assert_eq!(h.out().len(), limits::CONTROL_QUEUE - 1); +} - // A datagram whose sender was told it is held stays bounded where it was held: a full queue - // of [ip]'s own frames never drops it on release. +#[test] +fn s_ip_out_008_released_datagrams_wait_at_the_head_of_their_queue() { let mut h = H::fixture_i(); - let held = |n: u8| ip4(192, 0, 2, 10 + n); - for n in 0..8 { - for _ in 0..8 { - assert_eq!(h.udp_to(held(n)), Ok(None)); - } + for data in [b"1", b"2"] { + assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); } - h.out(); - for n in 0..limits::CONTROL_QUEUE as u8 { + for n in 1..limits::CONTROL_QUEUE as u8 { let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); } - for n in 0..8 { - h.reply_from(held(n), MacAddr([2, 0, 0, 0, 1, n])); - } - let out = h.out(); - assert_eq!(out.iter().filter(|o| o.ip().is_some()).count(), limits::nud::PENDING_TOTAL); - assert_eq!(out.len(), limits::CONTROL_QUEUE + limits::nud::PENDING_TOTAL); - assert_eq!(h.count(Counter::IpControlQueueFull), 0); + h.frame(&hex(V_ARP_REPLY)); + assert!(h.is_reachable(B)); + let dropped = [Counter::IpControlQueueFull, Counter::NbPendingOverflow, Counter::NbPendingDropped, Counter::NbPendingEvicted]; + assert!(dropped.iter().all(|&c| h.count(c) == 0), "nothing dropped"); + + let first = h.out_with(limits::CONTROL_QUEUE + 1); + assert!(first[..limits::CONTROL_QUEUE].iter().all(|o| o.arp().is_some()), "the 64 queued frames first"); + let mut data: Vec> = first[limits::CONTROL_QUEUE..].iter().map(|o| o.ip().unwrap().payload()[8..].to_vec()).collect(); + assert_eq!(data, [b"1"]); + // The next opportunity: [ip]'s frames, then datagram 3 from the transport. + data.extend(h.out_with(1).iter().map(|o| o.ip().unwrap().payload()[8..].to_vec())); + data.push(ip_of(&h.send(A, B, 5001, 5001, b"3").unwrap().unwrap()).unwrap().payload()[8..].to_vec()); + assert_eq!(data, [b"1", b"2", b"3"]); + assert!(dropped.iter().all(|&c| h.count(c) == 0)); } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 376c3dcdbab..cc771d4c911 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -345,11 +345,13 @@ fn s_ip_nud_021_stale_is_deleted_when_idle() { assert!(matches!(h.state(B), Some(Nud::Delay(_)))); } +const WIDE_A: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); + /// if0 at 10.0.0.1/16, with room for a full table of neighbours. fn wide() -> H { let mut h = H::raw(); let if0 = h.if0; - h.assign(if0, Ipv4Addr::new(10, 0, 0, 1), 16, 0); + h.assign(if0, WIDE_A, 16, 0); h.settle(); h } @@ -358,40 +360,63 @@ fn neighbour(n: u32) -> Ipv4Addr { Ipv4Addr::from(u32::from(Ipv4Addr::new(10, 0, 1, 0)) + n) } +fn wide_mac(addr: Ipv4Addr) -> MacAddr { + MacAddr([2, 1, 0, 0, (u32::from(addr) >> 8) as u8, u32::from(addr) as u8]) +} + fn reach_wide(h: &mut H, addr: Ipv4Addr) { let now = h.clock(); let _ = h.ip.resolve(now, h.if0, addr); h.out(); - let m = MacAddr([2, 1, 0, 0, (u32::from(addr) >> 8) as u8, u32::from(addr) as u8]); - h.frame(ð(MAC_A, m, 0x0806, &arp_packet(2, m, addr, MAC_A, Ipv4Addr::new(10, 0, 0, 1)))); + h.frame(ð(MAC_A, wide_mac(addr), 0x0806, &arp_packet(2, wide_mac(addr), addr, MAC_A, WIDE_A))); assert!(h.is_reachable(addr)); } +/// `addr` asks who has 10.0.0.1: learned into STALE if new, and answered once the queue drains. +fn ask_wide(h: &mut H, addr: Ipv4Addr) { + h.frame(ð(MacAddr::BROADCAST, wide_mac(addr), 0x0806, &arp_packet(1, wide_mac(addr), addr, MacAddr::ZERO, WIDE_A))); +} + +/// `addr` announces itself: an assertion, which resolves it into STALE. +fn announce_wide(h: &mut H, addr: Ipv4Addr) { + h.frame(ð(MacAddr::BROADCAST, wide_mac(addr), 0x0806, &arp_packet(1, wide_mac(addr), addr, MacAddr::ZERO, addr))); +} + #[test] -fn s_ip_nud_022_a_full_table_evicts_failed_first() { +fn s_ip_nud_022_a_full_table_evicts_in_order() { let mut h = wide(); - let failed = neighbour(0); + let (failed, unreachable, stale) = (neighbour(0), neighbour(1), neighbour(2)); let _ = h.ip.resolve(h.clock(), h.if0, failed); - h.out(); - h.run(3_000); + reach_wide(&mut h, unreachable); + h.ip.advise(h.clock(), unreachable, Advice::Reverify); + ask_wide(&mut h, stale); + h.run(5_000); assert!(matches!(h.state(failed), Some(Nud::Failed))); - for n in 1..limits::nud::TABLE_MAX as u32 { + assert!(matches!(h.state(unreachable), Some(Nud::Unreachable(u)) if u.quiescent())); + assert!(h.is_stale(stale)); + for n in 3..limits::nud::TABLE_MAX as u32 { reach_wide(&mut h, neighbour(n)); } - let new = neighbour(9_000); - assert_eq!(h.send(Ipv4Addr::new(10, 0, 0, 1), new, 5001, 5001, b"x"), Ok(None)); - assert!(h.state(failed).is_none()); - assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); + let mut left = vec![failed, unreachable, stale]; + for n in 0..3 { + let new = neighbour(9_000 + n); + assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Ok(None)); + assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); + let gone = left.remove(0); + assert!(h.state(gone).is_none(), "§6.8's order: {gone} next"); + assert!(left.iter().all(|a| h.state(*a).is_some())); + } + assert_eq!(h.send(WIDE_A, neighbour(9_003), 5001, 5001, b"x"), Err(Counter::NbTableFull)); let mut h = wide(); for n in 0..limits::nud::TABLE_MAX as u32 { reach_wide(&mut h, neighbour(n)); } let new = neighbour(9_000); - assert_eq!(h.send(Ipv4Addr::new(10, 0, 0, 1), new, 5001, 5001, b"x"), Err(Counter::NbTableFull)); + assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Err(Counter::NbTableFull)); assert_eq!(h.count(Counter::NbTableFull), 1); let asker = neighbour(9_001); - h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, asker, MacAddr::ZERO, Ipv4Addr::new(10, 0, 0, 1)))); + h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, asker, MacAddr::ZERO, WIDE_A))); let out = h.out(); assert_eq!(out.len(), 1); assert!(out[0].arp().is_some_and(|a| a.target_ip == asker)); @@ -501,3 +526,69 @@ fn s_ip_nud_027_prop_reachable_time_is_drawn_every_two_hours() { assert!((3..=5).contains(&changes), "seed {seed}: {changes} redraws in ten hours"); } } + +#[test] +fn s_ip_nud_029_released_datagrams_are_evicted_last() { + let b = neighbour(0); + let flow = Flow { source: WIDE_A, source_port: Port::new(5001).unwrap(), destination: b, destination_port: Port::new(5001).unwrap() }; + for others_reachable in [false, true] { + let mut h = wide(); + for data in [b"1", b"2"] { + assert_eq!(h.send(WIDE_A, b, 5001, 5001, data), Ok(None)); + } + h.out(); + for n in 1..limits::nud::TABLE_MAX as u32 { + h.at(u64::from(n)); + if others_reachable { + reach_wide(&mut h, neighbour(n)); + } else { + ask_wide(&mut h, neighbour(n)); + h.out(); + } + } + for n in 1..=limits::CONTROL_QUEUE as u32 { + ask_wide(&mut h, neighbour(n)); + } + announce_wide(&mut h, b); + assert!(h.is_stale(b)); + let new = neighbour(9_000); + assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Ok(None)); + assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); + if others_reachable { + assert!(h.state(b).is_none()); + assert_eq!(h.count(Counter::NbPendingEvicted), 2); + assert_eq!(h.events.iter().filter(|e| **e == Event::Unreachable(flow)).count(), 2); + assert!(h.out().iter().all(|o| o.to() != wide_mac(b))); + } else { + assert!(h.state(neighbour(1)).is_none(), "the longest unused of the others"); + assert!(h.is_stale(b)); + let to_b: Vec> = h.out().iter().filter(|o| o.to() == wide_mac(b)).map(payload).collect(); + assert_eq!(to_b, [b"1", b"2"]); + assert_eq!(h.count(Counter::NbPendingEvicted), 0); + } + } +} + +#[test] +fn s_ip_nud_030_released_datagrams_keep_their_entry() { + let mut h = H::fixture_i(); + for data in [b"1", b"2"] { + assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); + } + h.out(); + for n in 0..limits::CONTROL_QUEUE as u8 { + let m = MacAddr([2, 1, 0, 0, 0, n]); + h.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, Ipv4Addr::new(192, 0, 2, 100 + n), MacAddr::ZERO, A))); + } + h.frame(ð(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, MAC_B, B, MacAddr::ZERO, B))); + assert!(h.is_stale(B)); + h.ip.fire(H::instant(600_000)); + h.at(600_000); + assert!(h.is_stale(B), "not idle while it holds 1 and 2"); + assert!(h.out_with(limits::CONTROL_QUEUE).iter().all(|o| o.to() != MAC_B), "the queued frames first"); + assert!(h.is_stale(B), "1 and 2 are not out yet"); + assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"1"]); + assert!(h.is_stale(B)); + assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"2"]); + assert!(h.state(B).is_none(), "deleted as its queue drains"); +} diff --git a/toyos-net-tcp/src/lib.rs b/toyos-net-tcp/src/lib.rs index 51d2fe45e26..2a20993ff21 100644 --- a/toyos-net-tcp/src/lib.rs +++ b/toyos-net-tcp/src/lib.rs @@ -50,10 +50,9 @@ use toyos_net_wire::siphash; use toyos_net_wire::Port; pub use counters::{Counter, Counters, Refusal, RefusalLog}; -pub use toyos_net_wire::REFUSAL_LOG_INTERVAL; pub use seq::Seq; pub use stack::{ConnId, Info, ListenerId, Outgoing, Tcp}; -pub use toyos_net_wire::siphash::{siphash24, Key}; +pub use toyos_net_wire::siphash::Key; pub use toyos_net_wire::Instant; /// The TSval a 4-tuple with `offset` sends at `now`: one tick per millisecond (RFC 7323 §5.4). diff --git a/toyos-net-tcp/tests/isn.rs b/toyos-net-tcp/tests/isn.rs index 042191e03ec..75d11c2ae12 100644 --- a/toyos-net-tcp/tests/isn.rs +++ b/toyos-net-tcp/tests/isn.rs @@ -4,7 +4,8 @@ mod common; use common::*; -use toyos_net_tcp::{isn, siphash24, ts_offset, Instant, Tcp, Tuple}; +use toyos_net_tcp::{isn, ts_offset, Instant, Tcp, Tuple}; +use toyos_net_wire::siphash::siphash24; fn tuple(local_port: u16, remote_port: u16) -> Tuple { Tuple { local: ep(A, local_port), remote: ep(B, remote_port) } diff --git a/toyos-net-tcp/tests/port.rs b/toyos-net-tcp/tests/port.rs index 606a33e4e35..8c96e577890 100644 --- a/toyos-net-tcp/tests/port.rs +++ b/toyos-net-tcp/tests/port.rs @@ -6,7 +6,8 @@ use std::collections::HashSet; use std::net::Ipv4Addr; use common::*; -use toyos_net_tcp::{siphash24, Counter, Error, Instant, Tcp}; +use toyos_net_tcp::{Counter, Error, Instant, Tcp}; +use toyos_net_wire::siphash::siphash24; fn stack() -> Tcp { Tcp::new(config(65_535)).unwrap() diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs index 5e56230d8d3..17287645442 100644 --- a/toyos-net-udp/src/lib.rs +++ b/toyos-net-udp/src/lib.rs @@ -5,7 +5,9 @@ //! nothing here reads a clock, draws randomness or does I/O. //! //! **Back-pressure is a refusal.** Each socket holds at most `limits::TX_DATAGRAMS` accepted -//! datagrams: a send past it is refused and the caller keeps its datagram. A datagram leaves only +//! datagrams: a send past it is refused and the caller keeps its datagram. What closed sockets +//! had accepted is held to `limits::CLOSED_DATAGRAMS` together and takes one turn among the +//! sockets, so closing never grows the stack or starves another socket. A datagram leaves only //! when [`Udp::transmit`] offers it to the device and is built then; one [ip] holds for its next //! hop has left this crate and spends no credit, so a socket's next datagram is never behind it. //! @@ -33,6 +35,7 @@ use alloc::vec::Vec; use core::net::Ipv4Addr; use toyos_net_ip::{Arrival, Cast, ErrorClass, Flow, Ip, Source, Transport, TransportError, UdpOut}; +use toyos_net_wire::addr::is_martian; use toyos_net_wire::ethernet::MacClass; use toyos_net_wire::ipv4::Ttl; use toyos_net_wire::udp::{UdpBuilder, UdpChecksum, UdpDatagram}; @@ -69,6 +72,7 @@ toyos_net_wire::counters! { RxTruncated = "udp.rx-truncated"; RxDroppedOnConnect = "udp.rx-dropped-on-connect"; RxDiscardedOnClose = "udp.rx-discarded-on-close"; + TxDiscardedOnClose = "udp.tx-discarded-on-close"; Tx = "udp.tx"; IcmpErrorDelivered = "udp.icmp-error-delivered"; IcmpErrorSoft = "udp.icmp-error-soft"; @@ -82,6 +86,9 @@ pub mod limits { pub const RX_BYTES: usize = 65_536; pub const TX_DATAGRAMS: usize = 16; pub const TX_BYTES: usize = 65_536; + /// Datagrams closed sockets had accepted and that have not left: one socket's queue, whose + /// bytes MAX_PAYLOAD bounds. + pub const CLOSED_DATAGRAMS: usize = TX_DATAGRAMS; /// IANA's dynamic range, 49152 to 65535 (RFC 6335 §6). pub const EPHEMERAL_FIRST: u16 = 49_152; pub const EPHEMERAL_COUNT: u16 = 16_384; @@ -202,25 +209,27 @@ struct Slot { socket: Option, } +/// Whose datagram leaves next: a socket's, or one a closed socket had accepted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Turn { + Socket(u32), + Closed, +} + #[derive(Debug, Default)] pub struct Udp { slots: Vec, free: Vec, ports: BTreeMap, - active: VecDeque, - /// Datagrams a closed socket had accepted: they still leave (§U9 (3)). - orphans: VecDeque<(Port, Queued)>, + /// One turn per socket with datagrams queued, and one for `closed` while it holds any. + turns: VecDeque, + /// Datagrams closed sockets had accepted: they still leave (§U9 (3)), at most + /// `limits::CLOSED_DATAGRAMS` of them. + closed: VecDeque<(Port, Queued)>, counters: Counters, refusals: Vec, } -/// Not a destination any datagram may name: 0.0.0.0/8 but 0.0.0.0 itself, and 240.0.0.0/4 but the -/// limited broadcast. -fn invalid(addr: Ipv4Addr) -> bool { - let [first, ..] = addr.octets(); - (first == 0 && !addr.is_unspecified()) || (first >= 240 && !addr.is_broadcast()) -} - fn route_refusal(refusal: toyos_net_ip::Counter) -> Counter { match refusal { toyos_net_ip::Counter::RouteNoSourceAddress => Counter::NoSourceAddress, @@ -369,7 +378,7 @@ impl Udp { Some(Counter::SendLoopback) } else if ip.is_local(addr) { Some(Counter::SendToSelf) - } else if invalid(addr) { + } else if is_martian(addr) { Some(Counter::SendInvalidDestination) } else { None @@ -497,7 +506,7 @@ impl Udp { socket.tx.push_back(Queued { source, destination, port, ttl, payload: payload.to_vec() }); socket.tx_bytes = bytes; if !core::mem::replace(&mut socket.active, true) { - self.active.push_back(id.index); + self.turns.push_back(Turn::Socket(id.index)); } Ok(()) } @@ -528,7 +537,9 @@ impl Udp { })) } - /// Releases the port at once: received datagrams are discarded, accepted ones still leave. + /// Releases the port at once: received datagrams are discarded, and accepted ones still leave + /// while fewer than `limits::CLOSED_DATAGRAMS` wait; the rest are refused + /// `udp.tx-discarded-on-close`, since nobody is left to refuse them to. pub fn close(&mut self, id: SocketId) -> Result<(), Error> { self.socket(id)?; let Some(slot) = usize::try_from(id.index).ok().and_then(|i| self.slots.get_mut(i)) else { return Err(Error::NoSuchSocket) }; @@ -536,8 +547,18 @@ impl Udp { slot.generation = slot.generation.wrapping_add(1); self.free.push(id.index); self.ports.remove(&socket.port); + if socket.active { + self.turns.retain(|t| *t != Turn::Socket(id.index)); + } self.counters.add(Counter::RxDiscardedOnClose, u64::try_from(socket.rx.len()).unwrap_or(u64::MAX)); - self.orphans.extend(socket.tx.into_iter().map(|q| (socket.port, q))); + let room = limits::CLOSED_DATAGRAMS.saturating_sub(self.closed.len()); + let discarded = socket.tx.len().saturating_sub(room); + self.counters.add(Counter::TxDiscardedOnClose, u64::try_from(discarded).unwrap_or(u64::MAX)); + let turn = self.closed.is_empty(); + self.closed.extend(socket.tx.into_iter().take(room).map(|q| (socket.port, q))); + if turn && !self.closed.is_empty() { + self.turns.push_back(Turn::Closed); + } Ok(()) } @@ -636,29 +657,37 @@ impl Udp { } } - /// A transmit opportunity with room for `credit` frames: closed sockets' datagrams first, - /// then one datagram per socket in turn, each built as `sink` takes it. `sink` answers whether - /// it spent a frame; one [ip] holds for its next hop spends none. Returns the frames spent. + /// A transmit opportunity with room for `credit` frames: one datagram per turn, each socket + /// with datagrams queued taking one and closed sockets' datagrams together one more, each built + /// as `sink` takes it. `sink` answers whether it spent a frame; one [ip] holds for its next hop + /// spends none. Returns the frames spent. pub fn transmit(&mut self, credit: usize, mut sink: impl FnMut(&UdpOut<'_>) -> bool) -> usize { let mut spent = 0usize; while spent < credit { - let (port, queued) = if let Some(orphan) = self.orphans.pop_front() { - orphan - } else { - let Some(index) = self.active.pop_front() else { break }; - let Some(socket) = self.at(index) else { continue }; - let Some(queued) = socket.tx.pop_front() else { - socket.active = false; - continue; - }; - socket.tx_bytes = socket.tx_bytes.saturating_sub(queued.payload.len()); - let port = socket.port; - if socket.tx.is_empty() { - socket.active = false; - } else { - self.active.push_back(index); + let Some(turn) = self.turns.pop_front() else { break }; + let (port, queued) = match turn { + Turn::Closed => { + let Some(closed) = self.closed.pop_front() else { continue }; + if !self.closed.is_empty() { + self.turns.push_back(Turn::Closed); + } + closed + } + Turn::Socket(index) => { + let Some(socket) = self.at(index) else { continue }; + let Some(queued) = socket.tx.pop_front() else { + socket.active = false; + continue; + }; + socket.tx_bytes = socket.tx_bytes.saturating_sub(queued.payload.len()); + let port = socket.port; + if socket.tx.is_empty() { + socket.active = false; + } else { + self.turns.push_back(turn); + } + (port, queued) } - (port, queued) }; self.count(Counter::Tx); let out = UdpOut { diff --git a/toyos-net-udp/tests/recv.rs b/toyos-net-udp/tests/recv.rs index 4ec61b4425c..555691115a9 100644 --- a/toyos-net-udp/tests/recv.rs +++ b/toyos-net-udp/tests/recv.rs @@ -131,7 +131,7 @@ fn s_udp_us_039_a_bad_checksum_is_malformed() { let mut bad = hi(); bad[26..28].copy_from_slice(&[0xec, 0x5c]); assert_eq!(u.datagram(&bad), None); - assert_eq!(u.ip.wire_refusals("udp.checksum"), 1); + assert_eq!(u.ip.wire_counters().find(|(name, _)| *name == "udp.checksum"), Some(("udp.checksum", 1))); assert_eq!(u.udp.recv(id, &mut [0; 8]), Ok(None)); assert!(u.out().is_empty()); } @@ -204,6 +204,11 @@ fn s_udp_us_044_the_acquisition_exception() { assert_eq!(u.udp.receive(u.clock(), &arrival, &datagram), Verdict::Delivered); let (payload, r) = received(&mut u, id); assert_eq!((payload.as_slice(), r.destination), (&offer[42..], A)); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpAcquisitionAdmitted), 1); + u.ip.add_address(u.clock(), u.if0, A, 24).unwrap(); + let elsewhere = edit(offer[14..].to_vec(), |ip| set_destination(ip, ip4(192, 0, 2, 7))); + assert_eq!(u.frame(ð(MAC_A, MAC_R, 0x0800, &elsewhere)), Some(Verdict::Delivered), "a probing address is not usable"); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpAcquisitionAdmitted), 2); let mut u = U::bare(); u.bind(ANY, 5_000).unwrap(); @@ -213,6 +218,15 @@ fn s_udp_us_044_the_acquisition_exception() { assert_eq!(u.frame(ð(MAC_A, MAC_R, 0x0800, &ip)), None); assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); + let mut u = U::bare(); + let id = u.bind(ANY, 68).unwrap(); + u.udp.set_acquisition(id).unwrap(); + let group = edit(offer[14..].to_vec(), |ip| set_destination(ip, ip4(239, 9, 9, 9))); + assert_eq!(u.frame(ð(MAC_A, MAC_R, 0x0800, &group)), None, "C-3 admits a unicast destination only"); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpNotForUs), 1); + assert_eq!(u.ip_count(toyos_net_ip::Counter::IpAcquisitionAdmitted), 0); + assert_eq!(u.udp.recv(id, &mut [0; 1_500]), Ok(None)); + // A socket on 68 without the acquisition mark never hears it (§U5.3). let mut u = U::bare(); let id = u.bind(ANY, 68).unwrap(); @@ -324,6 +338,58 @@ fn s_udp_us_053_accepted_datagrams_outlive_close() { let ip = ip_of(frame).unwrap(); assert_eq!((ip.source(), &ip.payload()[..2], &ip.payload()[8..]), (A, &[0xc3, 0x51][..], &data[..])); } + + // Closed sockets hold at most CLOSED_DATAGRAMS together; past it a close refuses the rest. + let mut u = U::uf(); + let mut accepted = 0; + for n in 0..100u16 { + let id = u.bind(ANY, 50_001 + n).unwrap(); + for _ in 0..limits::TX_DATAGRAMS { + u.udp.send_to(&mut u.ip, id, DNS, 53, &[7; 1_000]).unwrap(); + accepted += 1; + } + u.udp.close(id).unwrap(); + } + assert_eq!(accepted, 1_600); + assert_eq!(u.count(Counter::TxDiscardedOnClose), 1_600 - 16); + let out = u.out(); + assert_eq!(out.len(), 16); + assert!(out.iter().all(|f| ip_of(f).unwrap().payload()[..2] == 50_001u16.to_be_bytes()), "the first close's, in order"); + + let mut u = U::uf(); + let first = u.bind(ANY, 50_001).unwrap(); + for _ in 0..10 { + u.udp.send_to(&mut u.ip, first, DNS, 53, b"a").unwrap(); + } + u.udp.close(first).unwrap(); + let second = u.bind(ANY, 50_002).unwrap(); + for _ in 0..limits::TX_DATAGRAMS { + u.udp.send_to(&mut u.ip, second, DNS, 53, b"b").unwrap(); + } + u.udp.close(second).unwrap(); + assert_eq!(u.count(Counter::TxDiscardedOnClose), 10); + let order: String = u.out().iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect(); + assert_eq!(order, "aaaaaaaaaabbbbbb"); + + // A slot a closed socket leaves takes no turn with it, and closed sockets take one turn. + let mut u = U::uf(); + let x = u.bind(ANY, 50_001).unwrap(); + u.udp.send_to(&mut u.ip, x, DNS, 53, b"x").unwrap(); + u.udp.close(x).unwrap(); + assert_eq!(u.out_with(1).len(), 1); + let y = u.bind(ANY, 50_002).unwrap(); + let z = u.bind(ANY, 50_003).unwrap(); + for _ in 0..2 { + u.udp.send_to(&mut u.ip, y, DNS, 53, b"y").unwrap(); + u.udp.send_to(&mut u.ip, z, DNS, 53, b"z").unwrap(); + } + let w = u.bind(ANY, 50_004).unwrap(); + for _ in 0..2 { + u.udp.send_to(&mut u.ip, w, DNS, 53, b"w").unwrap(); + } + u.udp.close(w).unwrap(); + let order: String = u.out().iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect(); + assert_eq!(order, "yzwyzw"); } #[test] diff --git a/toyos-net-udp/tests/send.rs b/toyos-net-udp/tests/send.rs index 55cc073e7b6..96dacd48248 100644 --- a/toyos-net-udp/tests/send.rs +++ b/toyos-net-udp/tests/send.rs @@ -140,6 +140,12 @@ fn s_udp_us_020_what_send_refuses() { let logged: Vec = u.udp.drain_refusals().collect(); assert_eq!(logged, [Refusal { rule: Counter::SendUnspecifiedDestination, local: (ANY, port(50_001)), peer: (ANY, 53) }]); assert!(u.out().is_empty()); + + for _ in 0..=limits::EVENTS { + assert_eq!(u.udp.send_to(&mut u.ip, id, ANY, 53, b"x"), refused(Counter::SendUnspecifiedDestination)); + } + assert_eq!(u.udp.drain_refusals().count(), limits::EVENTS, "an undrained shell holds a bounded list"); + assert_eq!(u.count(Counter::EventOverflow), 1); } #[test] @@ -198,6 +204,11 @@ fn s_udp_us_024_the_source_address_must_be_ours() { let id = u.bind(ANY, 50_001).unwrap(); assert_eq!(u.udp.send_to(&mut u.ip, id, DNS, 53, b"x"), refused(Counter::NoSourceAddress)); + let mut u = U::uf(); + u.ip.set_gateways(u.clock(), u.if0, &[]).unwrap(); + let id = u.bind(ANY, 50_001).unwrap(); + assert_eq!(u.udp.send_to(&mut u.ip, id, ip4(198, 51, 100, 7), 53, b"x"), refused(Counter::NoRoute), "an address, and no gateway"); + let mut u = U::uf(); let id = u.bind(A, 5_001).unwrap(); u.ip.remove_address(u.clock(), u.if0, A).unwrap(); diff --git a/toyos-net-wire/Cargo.toml b/toyos-net-wire/Cargo.toml index 84159312a08..87075fd18fc 100644 --- a/toyos-net-wire/Cargo.toml +++ b/toyos-net-wire/Cargo.toml @@ -2,6 +2,6 @@ name = "toyos-net-wire" version = "0.1.0" edition = "2021" -description = "Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP wire formats, parsed in place and built (no_std, forbid(unsafe_code))." +description = "Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP wire formats, parsed in place and built, and what every net crate shares: the caller's clock, SipHash, IPv4 address classes and the counters macro (no_std, forbid(unsafe_code))." license = "MIT OR Apache-2.0" publish = false diff --git a/toyos-net-wire/src/addr.rs b/toyos-net-wire/src/addr.rs new file mode 100644 index 00000000000..c945fc7551a --- /dev/null +++ b/toyos-net-wire/src/addr.rs @@ -0,0 +1,75 @@ +//! IPv4 address classes and prefixes (RFC 1122 §3.2.1.3, RFC 6890, RFC 4632, RFC 3021): the one +//! place a net crate reads them from. + +use core::net::Ipv4Addr; + +/// Not 0.0.0.0/8, 127.0.0.0/8, 224.0.0.0/4 or 240.0.0.0/4 (which holds 255.255.255.255): an +/// address that can name one host. +pub const fn is_host(addr: Ipv4Addr) -> bool { + let [first, ..] = addr.octets(); + !(first == 0 || first == 127 || first >= 224) +} + +/// Never a destination: 0.0.0.0/8, 127.0.0.0/8, and 240.0.0.0/4 but the limited broadcast. +pub const fn is_martian(addr: Ipv4Addr) -> bool { + let [first, ..] = addr.octets(); + first == 0 || first == 127 || (first >= 240 && !addr.is_broadcast()) +} + +/// An address and the length of its prefix, 1 to 32. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Cidr { + addr: Ipv4Addr, + len: u8, +} + +impl Cidr { + pub const fn new(addr: Ipv4Addr, len: u8) -> Option { + if matches!(len, 1..=32) { + Some(Self { addr, len }) + } else { + None + } + } + + /// The prefix a subnet mask names: contiguous ones, then zeros (RFC 4632 §3.1). + pub fn from_mask(addr: Ipv4Addr, mask: Ipv4Addr) -> Option { + let bits = u32::from(mask); + let len = bits.leading_ones(); + if len.checked_add(bits.trailing_zeros()) != Some(32) { + return None; + } + Self::new(addr, u8::try_from(len).ok()?) + } + + pub const fn addr(self) -> Ipv4Addr { + self.addr + } + + pub const fn prefix_len(self) -> u8 { + self.len + } + + fn mask(self) -> u32 { + u32::MAX.checked_shl(u32::from(32u8.saturating_sub(self.len))).unwrap_or(0) + } + + pub fn contains(self, other: Ipv4Addr) -> bool { + (u32::from(self.addr) ^ u32::from(other)) & self.mask() == 0 + } + + pub fn network(self) -> Ipv4Addr { + Ipv4Addr::from(u32::from(self.addr) & self.mask()) + } + + /// Host bits all one; a /31 or /32 has none (RFC 3021 §2.2.1). + pub fn broadcast(self) -> Option { + (self.len <= 30).then(|| Ipv4Addr::from(u32::from(self.addr) | !self.mask())) + } + + /// The network or directed-broadcast address of a prefix of /30 or shorter, which no host + /// may use. + pub fn is_edge(self, addr: Ipv4Addr) -> bool { + self.len <= 30 && self.contains(addr) && (addr == self.network() || Some(addr) == self.broadcast()) + } +} diff --git a/toyos-net-wire/src/lib.rs b/toyos-net-wire/src/lib.rs index 33c1c6434b7..3f38ca174e3 100644 --- a/toyos-net-wire/src/lib.rs +++ b/toyos-net-wire/src/lib.rs @@ -1,7 +1,7 @@ //! Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP, parsed in place and built: a parse refuses by the first rule its input breaks, a build refuses what it cannot represent. //! -//! It is also what every net crate above it shares: the caller's clock, the keyed function, and -//! the declaration of a crate's counters. +//! It is also what every net crate above it shares: the caller's clock, the keyed function, IPv4 +//! address classes and prefixes, and the declaration of a crate's counters. #![no_std] #![forbid(unsafe_code)] @@ -142,6 +142,7 @@ macro_rules! counters { }; } +pub mod addr; pub mod arp; pub mod checksum; mod emit; From 6a728693988978554725ad08950a4b2af4989557 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 17:19:33 +0200 Subject: [PATCH 11/20] toyos-net-udp: US-53 reuses a closed socket's slot before any transmit A closed socket's stale turn is consumed by the next transmit opportunity, so a slot reused only after one never meets it: the check that close takes its turn away (the review's P2) passed whether or not close did. The slot is now reused at once, and the mutation that leaves the turn behind goes red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-udp/tests/recv.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/toyos-net-udp/tests/recv.rs b/toyos-net-udp/tests/recv.rs index 555691115a9..a92d917a15e 100644 --- a/toyos-net-udp/tests/recv.rs +++ b/toyos-net-udp/tests/recv.rs @@ -371,25 +371,32 @@ fn s_udp_us_053_accepted_datagrams_outlive_close() { let order: String = u.out().iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect(); assert_eq!(order, "aaaaaaaaaabbbbbb"); - // A slot a closed socket leaves takes no turn with it, and closed sockets take one turn. + // The slot a closed socket leaves takes no turn with it to the next socket in it, and closed + // sockets' datagrams take one turn among the sockets. let mut u = U::uf(); let x = u.bind(ANY, 50_001).unwrap(); - u.udp.send_to(&mut u.ip, x, DNS, 53, b"x").unwrap(); + for _ in 0..2 { + u.udp.send_to(&mut u.ip, x, DNS, 53, b"x").unwrap(); + } u.udp.close(x).unwrap(); - assert_eq!(u.out_with(1).len(), 1); let y = u.bind(ANY, 50_002).unwrap(); let z = u.bind(ANY, 50_003).unwrap(); - for _ in 0..2 { - u.udp.send_to(&mut u.ip, y, DNS, 53, b"y").unwrap(); - u.udp.send_to(&mut u.ip, z, DNS, 53, b"z").unwrap(); - } + let send_yz = |u: &mut U| { + for _ in 0..2 { + u.udp.send_to(&mut u.ip, y, DNS, 53, b"y").unwrap(); + u.udp.send_to(&mut u.ip, z, DNS, 53, b"z").unwrap(); + } + }; + let order = |u: &mut U| -> String { u.out().iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect() }; + send_yz(&mut u); + assert_eq!(order(&mut u), "xyzxyz"); + send_yz(&mut u); let w = u.bind(ANY, 50_004).unwrap(); for _ in 0..2 { u.udp.send_to(&mut u.ip, w, DNS, 53, b"w").unwrap(); } u.udp.close(w).unwrap(); - let order: String = u.out().iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect(); - assert_eq!(order, "yzwyzw"); + assert_eq!(order(&mut u), "yzwyzw"); } #[test] From 0cd700ce5a21936581cf4cee13f22d0257593f40 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 19:11:55 +0200 Subject: [PATCH 12/20] =?UTF-8?q?net=20stage=203:=20answer=20the=20fourth?= =?UTF-8?q?=20review=20=E2=80=94=20the=20resolved=20states=20carry=20their?= =?UTF-8?q?=20queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolved states carry the pending queue §6.2 gives them. REACHABLE, STALE, DELAY, PROBE and UNREACHABLE hold a `Released` queue that only drains; INCOMPLETE's `Pending` is the only one with `push`, and resolution moves it into the resolved state. NUD-28's doctests now push into each resolved queue (compile_fail) beside a twin that pushes into INCOMPLETE's and drains every resolved one. The control queue keeps only the order: a released datagram's turn names its entry by address and id, and the datagram leaves from the entry's queue when the turn comes, to the MAC of that moment. `Control::holds`, `Control::take` and `Released::toward` are gone, and with them the scans `make_room` ran per candidate. An evicted entry's turns name an id no entry holds, so they leave nothing and spend no credit; the id keeps them from giving a later entry for the same address an earlier place, which NUD-29's second arm now pins. The idle rule is pinned on both sides. NUD-29's first arm asserts that B stays STALE once 1 and 2 have left, since its lifetime has not passed. NUD-30 gains a quiescent UNREACHABLE arm beside the STALE one. ACD-01 asserts IP-D1's scaled constants (28,571,428 and 57,142,857 ns) and the 114,285,713 ns floor as literals. `toyos-net-tcp`'s connect refuses a remote that `addr::is_host` refuses, and `unicast()` is gone; HS-35's three addresses stay refused, and 0/8 and 127/8 are refused too (MUST-46). The spec owner's ruling on the ACD NOTE: a probe or an announcement that finds the control queue full is not counted as sent. It waits for room, as a released datagram's turn does, so an address is never verified without its probes on the wire. A defence waits one at a time. ACD-18, a proposed id, fills the control queue from a second interface during probing, and again when a defence is due. A request is built when it leaves, from the state of that moment: broadcast in INCOMPLETE and UNREACHABLE, unicast in PROBE, and in REACHABLE, STALE and DELAY not at all (§6.3). OUT-08's first frame, B's request queued before the reply, no longer leaves. The review's REMOVEs are deleted: egress's header clause and `toyos-net-wire::addr`'s "the one place" clause. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/acd.rs | 33 +++-- toyos-net-ip/src/addr.rs | 5 +- toyos-net-ip/src/egress.rs | 102 +++++--------- toyos-net-ip/src/iface.rs | 2 + toyos-net-ip/src/lib.rs | 53 ++++--- toyos-net-ip/src/nud.rs | 278 +++++++++++++++++++++++++------------ toyos-net-ip/tests/acd.rs | 70 +++++++++- toyos-net-ip/tests/io.rs | 13 +- toyos-net-ip/tests/nud.rs | 81 ++++++++--- toyos-net-tcp/src/lib.rs | 1 - toyos-net-tcp/src/stack.rs | 7 +- toyos-net-wire/src/addr.rs | 3 +- 12 files changed, 410 insertions(+), 238 deletions(-) diff --git a/toyos-net-ip/src/acd.rs b/toyos-net-ip/src/acd.rs index ab918c64066..c3a093905e4 100644 --- a/toyos-net-ip/src/acd.rs +++ b/toyos-net-ip/src/acd.rs @@ -57,12 +57,12 @@ pub(crate) fn start(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { cx.timers.arm(timer(cx, addr), at); } +/// Queues an announcement of `addr`: one it owes, or a defence, of which one waits at a time. fn announce(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, owed: bool) { - if let Some(Address { phase: Phase::Usable { queued, .. }, .. }) = record(i, addr).filter(|_| owed) { - *queued = true; - } - if !cx.control.push(Item::Announce { iface: cx.iface, addr, owed }, cx.log) { - announced(i, cx, addr, owed); + let Some(Address { phase: Phase::Usable { queued, defending, .. }, .. }) = record(i, addr) else { return }; + let waits = if owed { queued } else { defending }; + if !core::mem::replace(waits, true) { + cx.control.hold(Item::Announce { iface: cx.iface, addr, owed }); } } @@ -73,12 +73,10 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { match a.phase { Phase::Tentative { probes, queued: false } if probes < PROBE_NUM => { a.phase = Phase::Tentative { probes, queued: true }; - if !cx.control.push(Item::Probe { iface: cx.iface, addr }, cx.log) { - probed(i, cx, addr); - } + cx.control.hold(Item::Probe { iface: cx.iface, addr }); } Phase::Tentative { probes, .. } if probes >= PROBE_NUM => { - a.phase = Phase::Usable { assigned: false, owed: ANNOUNCE_NUM, queued: false }; + a.phase = Phase::Usable { assigned: false, owed: ANNOUNCE_NUM, queued: false, defending: false }; announce(i, cx, addr, true); cx.log.count(Counter::AcdVerified); cx.log.event(Event::Verified { iface: cx.iface, addr }); @@ -93,7 +91,7 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } } -/// A probe for `addr` left, or was lost. +/// A probe for `addr` left. pub(crate) fn probed(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let now = cx.now; let Some(Address { phase: Phase::Tentative { probes, queued: queued @ true }, .. }) = record(i, addr) else { return }; @@ -106,11 +104,17 @@ pub(crate) fn probed(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { cx.timers.arm(timer(cx, addr), now.after(wait)); } -/// An announcement of `addr` left, or was lost; a defence owes nothing. +/// An announcement of `addr` left; a defence owes nothing. pub(crate) fn announced(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, owed: bool) { let now = cx.now; - let Some(Address { phase: Phase::Usable { assigned, owed: left, queued: queued @ true }, .. }) = record(i, addr).filter(|_| owed) else { return }; - *queued = false; + let Some(Address { phase: Phase::Usable { assigned, owed: left, queued, defending }, .. }) = record(i, addr) else { return }; + if !owed { + *defending = false; + return; + } + if !core::mem::replace(queued, false) { + return; + } *left = left.saturating_sub(1); if *left > 0 { cx.timers.arm(timer(cx, addr), now.after(ANNOUNCE_INTERVAL)); @@ -174,9 +178,10 @@ pub(crate) fn link_down(i: &mut Interface, cx: &mut Cx<'_>) { let iface = cx.iface; for a in &mut i.addresses { cx.timers.cancel(Timer::Acd(iface, a.cidr.addr())); - if let Phase::Usable { owed, queued, .. } = &mut a.phase { + if let Phase::Usable { owed, queued, defending, .. } = &mut a.phase { *owed = 0; *queued = false; + *defending = false; } } let tentative = i.addresses.iter().filter(|a| !a.usable()).map(|a| a.cidr.addr()); diff --git a/toyos-net-ip/src/addr.rs b/toyos-net-ip/src/addr.rs index d4518eb79b3..9fc622f06aa 100644 --- a/toyos-net-ip/src/addr.rs +++ b/toyos-net-ip/src/addr.rs @@ -16,8 +16,9 @@ pub enum AddrState { pub(crate) enum Phase { /// Probing: probes gone so far, and whether one waits to leave. Tentative { probes: u8, queued: bool }, - /// Usable, with the announcements still owed and whether one waits to leave. - Usable { assigned: bool, owed: u8, queued: bool }, + /// Usable, with the announcements still owed, whether one waits to leave, and whether a + /// defence waits to leave. + Usable { assigned: bool, owed: u8, queued: bool, defending: bool }, } #[derive(Clone, Copy, Debug)] diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs index 07089c5873e..692ced2fd4c 100644 --- a/toyos-net-ip/src/egress.rs +++ b/toyos-net-ip/src/egress.rs @@ -2,10 +2,11 @@ //! data flows: an ARP or IGMP frame is built when it leaves, from the state of that moment, and //! the timer it feeds starts then. The FIFO holds at most CONTROL_QUEUE frames, at most //! ECHO_REPLIES of them echo replies; one of [ip]'s own frames past either is dropped and -//! counted, and its producer moves on as if it had left. A datagram resolution releases into a -//! full FIFO is not dropped: it stays at the head of its neighbour's pending queue, within -//! PENDING_TOTAL, and enters as room appears, before any frame queued after it (§5.4). It is its -//! neighbour's until it has left. +//! counted, and its producer moves on as if it had left. Three kinds are never dropped: a +//! released datagram's turn, an ACD probe and an ACD announcement. One that finds the FIFO full +//! waits behind it and enters as room appears, ahead of any frame queued later (§5.4, RFC 5227 +//! §2.1). A turn holds no datagram: it names the entry whose queue does, and a turn whose entry +//! was evicted leaves nothing and spends no credit. //! //! Every datagram is atomic: DF set, identification 0 (`toyos-net-wire`'s one IPv4 form). @@ -39,13 +40,13 @@ pub(crate) enum FrameKind { #[derive(Debug)] pub(crate) enum Item { - Request { iface: IfIndex, target: Ipv4Addr, to: Option }, + Request { iface: IfIndex, target: Ipv4Addr }, Reply { iface: IfIndex, to: MacAddr, target: Ipv4Addr, ours: Ipv4Addr }, Probe { iface: IfIndex, addr: Ipv4Addr }, Announce { iface: IfIndex, addr: Ipv4Addr, owed: bool }, Igmp { iface: IfIndex, report: igmp::Report }, Frame { iface: IfIndex, frame: Vec, kind: FrameKind }, - Released(Released), + Turn(Turn), } impl Item { @@ -57,32 +58,26 @@ impl Item { | Self::Announce { iface, .. } | Self::Igmp { iface, .. } | Self::Frame { iface, .. } - | Self::Released(Released { iface, .. }) => *iface, + | Self::Turn(Turn { iface, .. }) => *iface, } } } -/// A datagram resolution released: still its neighbour's, whose pending queue holds it until it -/// has left (§6.5). +/// A released datagram's place in the FIFO: the oldest of entry `id`'s released datagrams leaves +/// when it comes (§6.5). #[derive(Debug)] -pub(crate) struct Released { - iface: IfIndex, - next_hop: Ipv4Addr, - held: Held, -} - -impl Released { - fn toward(&self, iface: IfIndex, next_hop: Ipv4Addr) -> bool { - self.iface == iface && self.next_hop == next_hop - } +pub(crate) struct Turn { + pub iface: IfIndex, + pub next_hop: Ipv4Addr, + pub id: u64, } #[derive(Debug, Default)] pub(crate) struct Control { items: VecDeque, echoes: usize, - /// Released datagrams that found `items` full, in release order: empty whenever it has room. - waiting: VecDeque, + /// What may not be dropped and found `items` full, in order: empty whenever it has room. + waiting: VecDeque, } impl Control { @@ -108,33 +103,15 @@ impl Control { true } - /// A datagram resolution released toward `next_hop`, in the order it was held. - pub fn release(&mut self, iface: IfIndex, next_hop: Ipv4Addr, held: Held) { - let released = Released { iface, next_hop, held }; + /// Queues a turn, a probe or an announcement, which waits for room rather than being dropped. + pub fn hold(&mut self, item: Item) { if self.full() { - self.waiting.push_back(released); + self.waiting.push_back(item); } else { - self.items.push_back(Item::Released(released)); + self.items.push_back(item); } } - /// Whether datagrams released toward `next_hop` have yet to leave. - pub fn holds(&self, iface: IfIndex, next_hop: Ipv4Addr) -> bool { - self.waiting.iter().any(|r| r.toward(iface, next_hop)) - || self.items.iter().any(|item| matches!(item, Item::Released(r) if r.toward(iface, next_hop))) - } - - /// Hands back the datagrams released toward `next_hop` that have yet to leave: its entry is - /// gone. - pub fn take(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> Vec { - let (items, kept): (Vec, Vec) = self.items.drain(..).partition(|item| matches!(item, Item::Released(r) if r.toward(iface, next_hop))); - self.items = kept.into(); - let (waiting, kept): (Vec, Vec) = self.waiting.drain(..).partition(|r| r.toward(iface, next_hop)); - self.waiting = kept.into(); - self.refill(); - Self::datagrams(items).chain(waiting.into_iter().map(|r| r.held)).collect() - } - fn pop(&mut self) -> Option { let item = self.items.pop_front()?; self.forget(&item); @@ -148,33 +125,23 @@ impl Control { } } - /// Room goes to the oldest released datagram waiting for it. + /// Room goes to the oldest item waiting for it. fn refill(&mut self) { while !self.full() { - let Some(released) = self.waiting.pop_front() else { return }; - self.items.push_back(Item::Released(released)); + let Some(item) = self.waiting.pop_front() else { return }; + self.items.push_back(item); } } - fn datagrams(items: Vec) -> impl Iterator { - items.into_iter().filter_map(|item| match item { - Item::Released(r) => Some(r.held), - _ => None, - }) - } - - /// Drops everything waiting for `iface`, whose link went down, and hands back the datagrams - /// resolution had released. - pub fn purge(&mut self, iface: IfIndex) -> Vec { + /// Drops everything waiting for `iface`, whose link went down. + pub fn purge(&mut self, iface: IfIndex) { let (gone, kept): (Vec, Vec) = self.items.drain(..).partition(|item| item.iface() == iface); self.items = kept.into(); for item in &gone { self.forget(item); } - let (waiting, kept): (Vec, Vec) = self.waiting.drain(..).partition(|r| r.iface == iface); - self.waiting = kept.into(); + self.waiting.retain(|item| item.iface() != iface); self.refill(); - Self::datagrams(gone).chain(waiting.into_iter().map(|r| r.held)).collect() } } @@ -240,11 +207,11 @@ impl Ip { } }; match item { - Item::Request { target, to, .. } => { - let Some(n) = i.neighbours.get(&target).filter(|n| n.queued) else { return false }; - let sender = n.hint.filter(|h| i.is_usable(*h)).or_else(|| i.source_for(target)); - nud::request_left(i, &mut cx, target); - let left = sender.is_some_and(|s| arp(&Arp::request(mac, s, target), to.unwrap_or(MacAddr::BROADCAST))); + Item::Request { target, .. } => { + let hint = i.neighbours.get(&target).and_then(|n| n.hint); + let Some(to) = nud::request_leaves(i, &mut cx, target) else { return false }; + let sender = hint.filter(|h| i.is_usable(*h)).or_else(|| i.source_for(target)); + let left = sender.is_some_and(|s| arp(&Arp::request(mac, s, target), to)); if left { cx.log.count(Counter::ArpRequestsSent); } @@ -291,13 +258,10 @@ impl Ip { sent(&mut cx, kind); true } - Item::Released(Released { next_hop, held, .. }) => { - i.held = i.held.saturating_sub(1); + Item::Turn(Turn { next_hop, id, .. }) => { + let Some(held) = nud::leave(i, &mut cx, next_hop, id) else { return false }; sink(iface, &held.frame); sent(&mut cx, held.kind); - if !cx.control.holds(iface, next_hop) { - nud::drained(i, &mut cx, next_hop); - } true } } diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs index 357c39ce461..1b44ec0ddda 100644 --- a/toyos-net-ip/src/iface.rs +++ b/toyos-net-ip/src/iface.rs @@ -23,6 +23,8 @@ pub(crate) struct Interface { pub gateways: Vec, pub active: Option, pub neighbours: BTreeMap, + /// Neighbour entries created so far: the next one's id. + pub entries: u64, /// Datagrams waiting in [ip] for this interface: held for resolution, or released and not /// yet left. pub held: usize, diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs index 68a28032b18..9829c456d9f 100644 --- a/toyos-net-ip/src/lib.rs +++ b/toyos-net-ip/src/lib.rs @@ -70,7 +70,7 @@ pub use counters::{Counter, Counters, RefusalLog}; pub use egress::{Sent, UdpOut, FRAME}; pub use igmp::IgmpMode; pub use limiter::Limiter; -pub use nud::{Incomplete, Linked, Nud, Probing, Reachable, Unreachable}; +pub use nud::{Held, Incomplete, Linked, Nud, Pending, Probing, Reachable, Released, Unreachable}; pub use route::{NextHop, Route, Source}; pub use toyos_net_wire::Instant; @@ -342,6 +342,7 @@ impl Ip { gateways: Vec::new(), active: None, neighbours: BTreeMap::new(), + entries: 0, held: 0, reachable, reachable_drawn: now, @@ -422,22 +423,34 @@ impl Ip { // Each compile_fail block sits beside one that compiles, so a typo cannot pass it. #[cfg(doctest)] mod compile_fail { - /// NUD-28: a MAC exists only in a state that has one, and only INCOMPLETE takes datagrams into a - /// pending queue. + /// NUD-28: a MAC exists only in a state that has one; only INCOMPLETE takes datagrams into its + /// pending queue, and a resolved state's queue only drains. /// /// ``` - /// use toyos_net_ip::{Linked, Nud}; - /// fn f(n: &Nud) -> usize { + /// use toyos_net_ip::{Held, Linked, Nud}; + /// fn f(n: &mut Nud, d: Held) -> Option { /// match n { - /// Nud::Incomplete(i) => i.queued(), - /// Nud::Reachable(r) => usize::from(r.mac().0[0]), + /// Nud::Incomplete(i) => { + /// let _ = i.requests(); + /// i.pending.push(d) + /// } + /// Nud::Reachable(r) => { + /// let _ = r.mac(); + /// r.released.pop() + /// } /// Nud::Stale(s) | Nud::Delay(s) => { - /// let _: &Linked = s; - /// 2 + /// let s: &mut Linked = s; + /// s.released.pop() + /// } + /// Nud::Probe(p) => { + /// let _ = p.requests(); + /// p.released.pop() + /// } + /// Nud::Unreachable(u) => { + /// let _ = u.quiescent(); + /// u.released.pop() /// } - /// Nud::Probe(p) => usize::from(p.requests()), - /// Nud::Unreachable(u) => usize::from(u.quiescent()), - /// Nud::Failed => 0, + /// Nud::Failed => None, /// } /// } /// ``` @@ -453,23 +466,23 @@ mod compile_fail { /// ``` /// /// ```compile_fail - /// use toyos_net_ip::Nud; - /// fn f(n: &Nud) { if let Nud::Reachable(r) = n { let _ = r.queued(); } } + /// use toyos_net_ip::{Held, Nud}; + /// fn f(n: &mut Nud, d: Held) { if let Nud::Reachable(r) = n { r.released.push(d); } } /// ``` /// /// ```compile_fail - /// use toyos_net_ip::Nud; - /// fn f(n: &Nud) { if let Nud::Stale(s) = n { let _ = s.queued(); } } + /// use toyos_net_ip::{Held, Nud}; + /// fn f(n: &mut Nud, d: Held) { if let Nud::Stale(s) = n { s.released.push(d); } } /// ``` /// /// ```compile_fail - /// use toyos_net_ip::Nud; - /// fn f(n: &Nud) { if let Nud::Probe(p) = n { let _ = p.queued(); } } + /// use toyos_net_ip::{Held, Nud}; + /// fn f(n: &mut Nud, d: Held) { if let Nud::Probe(p) = n { p.released.push(d); } } /// ``` /// /// ```compile_fail - /// use toyos_net_ip::Nud; - /// fn f(n: &Nud) { if let Nud::Unreachable(u) = n { let _ = u.queued(); } } + /// use toyos_net_ip::{Held, Nud}; + /// fn f(n: &mut Nud, d: Held) { if let Nud::Unreachable(u) = n { u.released.push(d); } } /// ``` #[allow(non_camel_case_types)] pub struct s_ip_nud_028_a_state_holds_only_its_own_fields; diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 586438078f2..207c048ccf1 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -6,9 +6,10 @@ //! taken from that moment, so a jumped clock never replays missed periods. //! //! Each state carries exactly its own fields: a MAC only where one is known, and a pending queue -//! that takes datagrams only while INCOMPLETE. What resolution released into a full control -//! queue waits at egress, which only drains it (§6.5); until it has, its entry does not idle out -//! and is evicted only after every other candidate (§6.8). +//! that takes datagrams in INCOMPLETE and only drains in every state resolution leads to (§6.2). +//! Resolution moves INCOMPLETE's queue into the resolved state and gives each datagram a turn in +//! the control queue, where it leaves to the MAC of that moment. While its queue holds any, an +//! entry does not idle out and is evicted only after every other candidate (§6.8). use alloc::collections::VecDeque; use alloc::vec::Vec; @@ -19,11 +20,11 @@ use toyos_net_wire::ethernet::MacAddr; use toyos_net_wire::Instant; use crate::counters::Counter; -use crate::egress::{FrameKind, Item}; +use crate::egress::{FrameKind, Item, Turn}; use crate::iface::{Cx, Interface}; use crate::limits::nud::{ BACKOFF_MULTIPLE, BROADCAST_SOLICIT, DELAY_FIRST_PROBE, FAILED_HOLD, IDLE_LIFETIME, LOCKTIME, MAX_RETRANS, - RETRANS, TABLE_MAX, UNICAST_SOLICIT, + PENDING_PER_NEIGHBOUR, RETRANS, TABLE_MAX, UNICAST_SOLICIT, }; use crate::timers::Timer; use crate::{route, Event, Flow, Peer}; @@ -40,27 +41,64 @@ pub enum Nud { Failed, } +/// A frame [ip] built and holds for its next hop, and who is told if it never leaves. +#[derive(Debug)] +pub struct Held { + pub(crate) frame: Vec, + pub(crate) kind: FrameKind, + pub(crate) flow: Option, +} + +/// INCOMPLETE's pending queue: the only one that takes datagrams (§6.5). +#[derive(Debug, Default)] +pub struct Pending(VecDeque); + +impl Pending { + pub fn queued(&self) -> usize { + self.0.len() + } + + /// Takes `held`, handing back the oldest when PENDING_PER_NEIGHBOUR wait already (RFC 4861 + /// §7.2.2). + pub fn push(&mut self, held: Held) -> Option { + let oldest = if self.0.len() >= PENDING_PER_NEIGHBOUR { self.0.pop_front() } else { None }; + self.0.push_back(held); + oldest + } +} + +/// A resolved entry's pending queue: what resolution released that has yet to leave, oldest +/// first. It only drains (§6.2). +#[derive(Debug, Default)] +pub struct Released(VecDeque); + +impl Released { + pub fn queued(&self) -> usize { + self.0.len() + } + + pub fn pop(&mut self) -> Option { + self.0.pop_front() + } +} + #[derive(Debug)] pub struct Incomplete { requests: u8, - pending: VecDeque, + pub pending: Pending, } impl Incomplete { pub fn requests(&self) -> u8 { self.requests } - - /// Datagrams waiting for the answer. - pub fn queued(&self) -> usize { - self.pending.len() - } } #[derive(Debug)] pub struct Reachable { mac: MacAddr, confirmed: Instant, + pub released: Released, } impl Reachable { @@ -73,16 +111,18 @@ impl Reachable { } } -/// STALE or DELAY: a MAC and nothing else. +/// STALE or DELAY: a MAC and its released datagrams. #[derive(Debug)] pub struct Linked { mac: MacAddr, + pub released: Released, } #[derive(Debug)] pub struct Probing { mac: MacAddr, requests: u8, + pub released: Released, } impl Probing { @@ -100,6 +140,7 @@ pub struct Unreachable { backoff: bool, /// A datagram went to the MAC since that request. sent: bool, + pub released: Released, } impl Unreachable { @@ -124,19 +165,46 @@ impl Nud { fn solicits(&self) -> bool { matches!(self, Self::Incomplete(_) | Self::Probe(_) | Self::Unreachable(_)) } -} -/// A frame [ip] built and holds for its next hop, and who is told if it never leaves. -#[derive(Debug)] -pub(crate) struct Held { - pub frame: Vec, - pub kind: FrameKind, - pub flow: Option, + fn released(&self) -> Option<&Released> { + match self { + Self::Reachable(Reachable { released, .. }) + | Self::Stale(Linked { released, .. }) + | Self::Delay(Linked { released, .. }) + | Self::Probe(Probing { released, .. }) + | Self::Unreachable(Unreachable { released, .. }) => Some(released), + Self::Incomplete(_) | Self::Failed => None, + } + } + + fn released_mut(&mut self) -> Option<&mut Released> { + match self { + Self::Reachable(Reachable { released, .. }) + | Self::Stale(Linked { released, .. }) + | Self::Delay(Linked { released, .. }) + | Self::Probe(Probing { released, .. }) + | Self::Unreachable(Unreachable { released, .. }) => Some(released), + Self::Incomplete(_) | Self::Failed => None, + } + } + + /// Released datagrams have yet to leave. + fn releasing(&self) -> bool { + self.released().is_some_and(|r| !r.0.is_empty()) + } + + /// A resolved state's released datagrams, taken for the state that follows it. + fn take_released(&mut self) -> Released { + self.released_mut().map(core::mem::take).unwrap_or_default() + } } #[derive(Debug)] pub(crate) struct Neighbour { pub state: Nud, + /// Names this entry apart from any earlier one for the same address: a turn of an evicted + /// entry must not hand a later one's datagram its place. + pub id: u64, pub last_request: Option, /// A request waits in the control queue. pub queued: bool, @@ -162,18 +230,35 @@ fn timer(cx: &Cx<'_>, addr: Ipv4Addr) -> Timer { Timer::Neighbour(cx.iface, addr) } -/// Queues a request to `addr`, broadcast or to `to`; one the full queue refuses counts as sent, -/// so the machine moves on and never stalls on a loss. -fn request(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, to: Option) { +/// Queues a request to `addr`; one the full queue refuses counts as sent, so the machine moves on +/// and never stalls on a loss. +fn request(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let Some(n) = i.neighbours.get_mut(&addr) else { return }; n.queued = true; - if !cx.control.push(Item::Request { iface: cx.iface, target: addr, to }, cx.log) { + if !cx.control.push(Item::Request { iface: cx.iface, target: addr }, cx.log) { request_left(i, cx, addr); } } +/// A request to `addr` reached the head of the control queue and takes its form from the state of +/// this moment (§6.4): broadcast in INCOMPLETE and UNREACHABLE, to the cached MAC in PROBE, and +/// in every other state none at all, since they send no request (§6.3). It waits no longer. +pub(crate) fn request_leaves(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) -> Option { + let n = i.neighbours.get_mut(&addr).filter(|n| n.queued)?; + let to = match &n.state { + Nud::Incomplete(_) | Nud::Unreachable(_) => MacAddr::BROADCAST, + Nud::Probe(p) => p.mac, + Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed => { + n.queued = false; + return None; + } + }; + request_left(i, cx, addr); + Some(to) +} + /// A request to `addr` left, or was lost: its spacing and its state's deadline start now. -pub(crate) fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { +fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let now = cx.now; let Some(n) = i.neighbours.get_mut(&addr) else { return }; if !core::mem::replace(&mut n.queued, false) { @@ -201,37 +286,44 @@ pub(crate) fn request_left(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } /// Makes room for one more entry, evicting FAILED, then quiescent UNREACHABLE, then STALE, each -/// the one unused longest, and after all of them one whose pending queue still holds released -/// datagrams (§6.8). +/// the one unused longest, and after all of them one whose queue still holds released datagrams +/// (§6.8). fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { if i.neighbours.len() < TABLE_MAX { return true; } - let class = |addr: Ipv4Addr, n: &Neighbour| { + let class = |n: &Neighbour| { let class = match &n.state { Nud::Failed => 0, Nud::Unreachable(u) if u.quiescent() && !n.queued => 1, Nud::Stale(_) => 2, _ => return None, }; - Some(if cx.control.holds(cx.iface, addr) { 3 } else { class }) + Some(if n.state.releasing() { 3 } else { class }) }; - let victim = i.neighbours.iter().filter_map(|(a, n)| class(*a, n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a); + let victim = i.neighbours.iter().filter_map(|(a, n)| class(n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a); let Some(victim) = victim else { return false }; remove(i, cx, victim); true } -/// Deletes `addr`'s entry, and with it the released datagrams its pending queue still holds. +/// Deletes `addr`'s entry, and with it the released datagrams its queue still holds: their turns +/// name an entry that is gone. fn remove(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { - i.neighbours.remove(&addr); cx.timers.cancel(timer(cx, addr)); - for held in cx.control.take(cx.iface, addr) { + let Some(mut n) = i.neighbours.remove(&addr) else { return }; + for held in n.state.take_released().0 { i.held = i.held.saturating_sub(1); drop_held(cx, held, Counter::NbPendingEvicted); } } +fn insert(i: &mut Interface, addr: Ipv4Addr, state: Nud, now: Instant, hint: Option) { + let id = i.entries; + i.entries = i.entries.wrapping_add(1); + i.neighbours.insert(addr, Neighbour { state, id, last_request: None, queued: false, used: now, hint }); +} + /// A datagram or a flow wants `addr` now (§6.3 "send"): creates INCOMPLETE, moves STALE to /// DELAY, or asks a quiescent UNREACHABLE for a request. pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Option) -> Link { @@ -241,9 +333,8 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt cx.log.count(Counter::NbTableFull); return Link::Failed(Counter::NbTableFull); } - let state = Nud::Incomplete(Incomplete { requests: 0, pending: VecDeque::new() }); - i.neighbours.insert(addr, Neighbour { state, last_request: None, queued: false, used: now, hint }); - request(i, cx, addr, None); + insert(i, addr, Nud::Incomplete(Incomplete { requests: 0, pending: Pending::default() }), now, hint); + request(i, cx, addr); return Link::Pending; }; n.used = now; @@ -253,10 +344,10 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt let queued = n.queued; match &mut n.state { Nud::Incomplete(_) => Link::Pending, - Nud::Reachable(Reachable { mac, .. }) | Nud::Delay(Linked { mac }) | Nud::Probe(Probing { mac, .. }) => Link::Resolved(*mac), - Nud::Stale(Linked { mac }) => { + Nud::Reachable(Reachable { mac, .. }) | Nud::Delay(Linked { mac, .. }) | Nud::Probe(Probing { mac, .. }) => Link::Resolved(*mac), + Nud::Stale(Linked { mac, released }) => { let mac = *mac; - n.state = Nud::Delay(Linked { mac }); + n.state = Nud::Delay(Linked { mac, released: core::mem::take(released) }); cx.timers.arm(timer(cx, addr), now.after(DELAY_FIRST_PROBE)); Link::Resolved(mac) } @@ -265,7 +356,7 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt if u.backoff { u.sent = true; } else if !queued { - request(i, cx, addr, None); + request(i, cx, addr); } Link::Resolved(mac) } @@ -279,12 +370,10 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt /// Holds a frame for an INCOMPLETE neighbour; the queue keeps the newest (RFC 4861 §7.2.2). pub(crate) fn hold(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, held: Held) { let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return }; - if s.pending.len() >= crate::limits::nud::PENDING_PER_NEIGHBOUR { - s.pending.pop_front(); + if s.pending.push(held).is_some() { i.held = i.held.saturating_sub(1); cx.log.count(Counter::NbPendingOverflow); } - s.pending.push_back(held); i.held = i.held.saturating_add(1); } @@ -294,8 +383,9 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let reachable = i.reachable_time(cx.draws, now); let Some(n) = i.neighbours.get_mut(&addr) else { return }; let spaced = n.last_request.map_or(now, |at| at.after(RETRANS)).max(now); + let releasing = n.state.releasing(); match &mut n.state { - Nud::Incomplete(s) if s.requests < BROADCAST_SOLICIT => request(i, cx, addr, None), + Nud::Incomplete(s) if s.requests < BROADCAST_SOLICIT => request(i, cx, addr), Nud::Incomplete(s) => { let pending = core::mem::take(&mut s.pending); n.state = Nud::Failed; @@ -306,31 +396,29 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { if now < end { cx.timers.arm(timer(cx, addr), end); } else { - n.state = Nud::Stale(Linked { mac: s.mac }); + n.state = Nud::Stale(Linked { mac: s.mac, released: core::mem::take(&mut s.released) }); cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } } Nud::Delay(s) => { - n.state = Nud::Probe(Probing { mac: s.mac, requests: 0 }); + n.state = Nud::Probe(Probing { mac: s.mac, requests: 0, released: core::mem::take(&mut s.released) }); probe(i, cx, addr, spaced); } - Nud::Probe(s) if s.requests < UNICAST_SOLICIT => { - let mac = s.mac; - request(i, cx, addr, Some(mac)); - } + Nud::Probe(s) if s.requests < UNICAST_SOLICIT => request(i, cx, addr), Nud::Probe(s) => { - n.state = Nud::Unreachable(Unreachable { mac: s.mac, requests: 0, backoff: false, sent: false }); + let released = core::mem::take(&mut s.released); + n.state = Nud::Unreachable(Unreachable { mac: s.mac, requests: 0, backoff: false, sent: false, released }); cx.log.count(Counter::NbUnreachable); cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); route::refresh_active(i, cx); } - Nud::Unreachable(s) if s.backoff && s.sent => request(i, cx, addr, None), + Nud::Unreachable(s) if s.backoff && s.sent => request(i, cx, addr), Nud::Unreachable(s) if s.backoff => { s.backoff = false; cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } // Not idle while released datagrams are its: `drained` deletes it once they have left. - Nud::Stale(_) | Nud::Unreachable(_) if cx.control.holds(cx.iface, addr) => {} + Nud::Stale(_) | Nud::Unreachable(_) if releasing => {} Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => { remove(i, cx, addr); route::refresh_active(i, cx); @@ -338,9 +426,27 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } } +/// The turn of entry `id`'s oldest released datagram: it leaves now, to `addr`'s MAC of this +/// moment. `None` when that entry is gone. +pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, id: u64) -> Option { + let n = i.neighbours.get_mut(&addr).filter(|n| n.id == id)?; + let mac = n.state.mac()?; + let released = n.state.released_mut()?; + let mut held = released.pop()?; + let last = released.0.is_empty(); + if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { + *destination = mac.0; + } + i.held = i.held.saturating_sub(1); + if last { + drained(i, cx, addr); + } + Some(held) +} + /// The last datagram released toward `addr` left. Every STALE or quiescent UNREACHABLE entry has /// a deadline armed but one whose idle lifetime passed while it held them, which goes now. -pub(crate) fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { +fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let Some(n) = i.neighbours.get(&addr) else { return }; let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.queued && cx.timers.get(timer(cx, addr)).is_none(); if idle { @@ -352,8 +458,7 @@ pub(crate) fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { /// PROBE's first unicast request, now or at the spacing boundary. fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { if at <= cx.now { - let mac = i.neighbours.get(&addr).and_then(|n| n.state.mac()); - request(i, cx, addr, mac); + request(i, cx, addr); } else { cx.timers.arm(timer(cx, addr), at); } @@ -368,9 +473,9 @@ fn drop_held(cx: &mut Cx<'_>, held: Held, counter: Counter) { } /// INCOMPLETE gave up: every held datagram is dropped. -fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: VecDeque) { +fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: Pending) { let now = cx.now; - for held in pending { + for held in pending.0 { i.held = i.held.saturating_sub(1); drop_held(cx, held, Counter::NbPendingDropped); } @@ -380,16 +485,22 @@ fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: VecDeque, addr: Ipv4Addr, mac: MacAddr, pending: VecDeque) { - for mut held in pending { - if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { - *destination = mac.0; +/// What a state entering at `mac` takes over: INCOMPLETE's datagrams, each given its turn in +/// arrival order ahead of any sent later (§6.5), or what its resolved predecessor still holds. +fn inherit(cx: &mut Cx<'_>, addr: Ipv4Addr, n: &mut Neighbour, mac: MacAddr) -> Released { + let Nud::Incomplete(s) = &mut n.state else { + if let Some(was) = n.state.mac().filter(|was| *was != mac) { + mac_changed(cx, addr, was, mac); } - cx.control.release(cx.iface, addr, held); + return n.state.take_released(); + }; + let pending = core::mem::take(&mut s.pending).0; + for _ in &pending { + cx.control.hold(Item::Turn(Turn { iface: cx.iface, next_hop: addr, id: n.id })); } cx.log.count(Counter::NbResolved); cx.log.event(Event::Resolved { iface: cx.iface, next_hop: addr }); + Released(pending) } fn mac_changed(cx: &mut Cx<'_>, addr: Ipv4Addr, old: MacAddr, new: MacAddr) { @@ -401,16 +512,9 @@ fn reach(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { let now = cx.now; let reachable = i.reachable_time(cx.draws, now); let Some(n) = i.neighbours.get_mut(&addr) else { return }; - let old = core::mem::replace(&mut n.state, Nud::Reachable(Reachable { mac, confirmed: now })); + let released = inherit(cx, addr, n, mac); + n.state = Nud::Reachable(Reachable { mac, confirmed: now, released }); cx.timers.arm(timer(cx, addr), now.after(reachable)); - match old { - Nud::Incomplete(s) => resolved(cx, addr, mac, s.pending), - other => { - if let Some(was) = other.mac().filter(|was| *was != mac) { - mac_changed(cx, addr, was, mac); - } - } - } route::refresh_active(i, cx); } @@ -418,16 +522,9 @@ fn reach(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { fn stale(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: MacAddr) { let now = cx.now; let Some(n) = i.neighbours.get_mut(&addr) else { return }; - let old = core::mem::replace(&mut n.state, Nud::Stale(Linked { mac })); + let released = inherit(cx, addr, n, mac); + n.state = Nud::Stale(Linked { mac, released }); cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); - match old { - Nud::Incomplete(s) => resolved(cx, addr, mac, s.pending), - other => { - if let Some(was) = other.mac().filter(|was| *was != mac) { - mac_changed(cx, addr, was, mac); - } - } - } route::refresh_active(i, cx); } @@ -463,7 +560,7 @@ pub(crate) fn learn(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, mac: Mac return; } let now = cx.now; - i.neighbours.insert(addr, Neighbour { state: Nud::Stale(Linked { mac }), last_request: None, queued: false, used: now, hint: None }); + insert(i, addr, Nud::Stale(Linked { mac, released: Released::default() }), now, None); cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } @@ -481,7 +578,8 @@ pub(crate) fn advise(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, confirm } (state @ (Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_)), false) => { if let Some(mac) = state.mac() { - *state = Nud::Probe(Probing { mac, requests: 0 }); + let released = state.take_released(); + *state = Nud::Probe(Probing { mac, requests: 0, released }); probe(i, cx, addr, spaced); } } @@ -493,14 +591,14 @@ pub(crate) fn advise(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, confirm pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>) { for (addr, n) in core::mem::take(&mut i.neighbours) { cx.timers.cancel(timer(cx, addr)); - if let Nud::Incomplete(s) = n.state { - for held in s.pending { - drop_held(cx, held, Counter::NbPendingDropped); - } + let held = match n.state { + Nud::Incomplete(s) => s.pending.0, + mut state => state.take_released().0, + }; + for held in held { + drop_held(cx, held, Counter::NbPendingDropped); } } - for held in cx.control.purge(cx.iface) { - drop_held(cx, held, Counter::NbPendingDropped); - } + cx.control.purge(cx.iface); i.held = 0; } diff --git a/toyos-net-ip/tests/acd.rs b/toyos-net-ip/tests/acd.rs index f58a8d6f723..c5d4b72c470 100644 --- a/toyos-net-ip/tests/acd.rs +++ b/toyos-net-ip/tests/acd.rs @@ -6,8 +6,8 @@ use std::net::Ipv4Addr; use std::time::Duration; use common::*; -use toyos_net_ip::limits::acd::{ANNOUNCE_INTERVAL, ANNOUNCE_WAIT, PROBE_MAX, PROBE_MIN, PROBE_WAIT}; -use toyos_net_ip::{AddrState, Counter, Event, Instant, Peer, Source}; +use toyos_net_ip::limits::acd::{ANNOUNCE_WAIT, PROBE_MAX, PROBE_MIN, PROBE_WAIT}; +use toyos_net_ip::{limits, AddrState, Counter, Event, Instant, Peer, Source}; use toyos_net_wire::ethernet::MacAddr; fn ip4(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr { @@ -42,6 +42,9 @@ fn first_probe(h: &mut H) -> Instant { #[test] fn s_ip_acd_001_the_ruled_schedule() { + // IP-D1: RFC 5227's 1 s and 2 s times 200/7,000, in whole nanoseconds. + let (one, two) = (Duration::from_nanos(28_571_428), Duration::from_nanos(57_142_857)); + assert_eq!([PROBE_WAIT, PROBE_MIN, PROBE_MAX, ANNOUNCE_WAIT], [one, one, two, two]); let mut h = probing(); let t0 = h.clock(); let out = run_exact(&mut h, t0.after(Duration::from_secs(5))); @@ -49,14 +52,14 @@ fn s_ip_acd_001_the_ruled_schedule() { let announcements: Vec = out.iter().filter(|(_, f)| *f == hex(V_ARP_ANNOUNCE)).map(|(at, _)| *at).collect(); assert_eq!(probes.len(), 3); assert_eq!(announcements.len(), 2); - assert!(probes[0].since(t0) <= PROBE_WAIT); + assert!(probes[0].since(t0) <= one); for pair in probes.windows(2) { - assert!((PROBE_MIN..=PROBE_MAX).contains(&pair[1].since(pair[0]))); + assert!((one..=two).contains(&pair[1].since(pair[0]))); } - assert_eq!(announcements[0].since(probes[2]), ANNOUNCE_WAIT); - assert_eq!(announcements[1].since(announcements[0]), ANNOUNCE_INTERVAL); + assert_eq!(announcements[0].since(probes[2]), two); + assert_eq!(announcements[1].since(announcements[0]), Duration::from_secs(2)); let usable = announcements[0].since(t0); - assert!(usable >= PROBE_MIN * 2 + ANNOUNCE_WAIT && usable <= Duration::from_millis(200), "{usable:?}"); + assert!(usable >= Duration::from_nanos(114_285_713) && usable <= Duration::from_millis(200), "{usable:?}"); assert!(h.events.contains(&Event::Verified { iface: h.if0, addr: A })); assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); } @@ -264,3 +267,56 @@ fn s_ip_acd_017_detection_starts_at_the_first_announcement() { assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Announcing)); assert_eq!(h.count(Counter::AcdDefended), 1); } + +#[test] +fn s_ip_acd_018_a_full_control_queue_holds_probes_and_announcements() { + // if1 fills the control queue every interface shares; then if0 probes for A. + let mut h = H::raw(); + let if1 = h.add_if1(); + h.assign(if1, ip4(198, 51, 100, 1), 24, 0); + h.settle(); + for n in 0..limits::CONTROL_QUEUE as u8 { + let m = MacAddr([2, 1, 0, 0, 0, n]); + let request = arp_packet(1, m, ip4(198, 51, 100, 100 + n), MacAddr::ZERO, ip4(198, 51, 100, 1)); + let _ = h.ip.receive(h.clock(), if1, ð(MacAddr::BROADCAST, m, 0x0806, &request)); + } + h.ip.add_address(h.clock(), h.if0, A, 24).unwrap(); + let t0 = h.clock(); + let at = t0.after(Duration::from_secs(1)); + while let Some(d) = h.ip.next_deadline().filter(|d| *d <= at) { + h.ip.fire(d); + } + h.collect(); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Tentative), "never verified without its probes on the wire"); + assert!(!h.events.iter().any(|e| matches!(e, Event::Verified { .. }))); + assert_eq!(h.count(Counter::IpControlQueueFull), 0, "the probe waits; it is not dropped"); + + let mut out = Vec::new(); + h.ip.transmit(at, limits::CONTROL_QUEUE, |iface, f| out.push((iface, f.to_vec()))); + assert_eq!(out.len(), limits::CONTROL_QUEUE); + assert!(out.iter().all(|(iface, _)| *iface == if1), "the replies queued first"); + out.clear(); + h.ip.transmit(at, 1, |iface, f| out.push((iface, f.to_vec()))); + assert_eq!(out, [(h.if0, hex(V_ARP_PROBE))]); + let rest: Vec> = run_exact(&mut h, at.after(Duration::from_secs(5))).into_iter().map(|(_, f)| f).collect(); + assert_eq!(rest, [hex(V_ARP_PROBE), hex(V_ARP_PROBE), hex(V_ARP_ANNOUNCE), hex(V_ARP_ANNOUNCE)]); + assert!(h.events.contains(&Event::Verified { iface: h.if0, addr: A })); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); + + // A defence due while the queue is full waits too, one at a time. + let mut h = H::fixture_i(); + for n in 0..limits::CONTROL_QUEUE as u8 { + let m = MacAddr([2, 1, 0, 0, 0, n]); + h.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, ip4(192, 0, 2, 100 + n), MacAddr::ZERO, A))); + } + h.frame(&hex(V_ARP_CONFLICT_B)); + h.at(10_001); + h.frame(&hex(V_ARP_CONFLICT_B)); + assert_eq!(h.count(Counter::AcdDefended), 2); + assert_eq!(h.count(Counter::IpControlQueueFull), 0); + let out = h.out(); + assert_eq!(out.len(), limits::CONTROL_QUEUE + 1); + assert!(out[..limits::CONTROL_QUEUE].iter().all(|o| o.arp().is_some_and(|a| a.operation == toyos_net_wire::arp::Operation::Reply))); + assert_eq!(out[limits::CONTROL_QUEUE].frame, hex(V_ARP_ANNOUNCE)); + assert_eq!(h.ip.address(h.if0, A), Some(AddrState::Assigned)); +} diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index d7007dd5778..a5ca567b563 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -6,7 +6,7 @@ mod common; use std::net::Ipv4Addr; use common::*; -use toyos_net_ip::{limits, Cast, Counter, Delivery, MTU}; +use toyos_net_ip::{limits, Cast, Counter, Delivery, Nud, MTU}; use toyos_net_wire::ethernet::MacAddr; use toyos_net_wire::ipv4::{Ecn, Ttl}; @@ -542,17 +542,20 @@ fn s_ip_out_008_released_datagrams_wait_at_the_head_of_their_queue() { let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); } h.frame(&hex(V_ARP_REPLY)); - assert!(h.is_reachable(B)); + assert!(matches!(h.state(B), Some(Nud::Reachable(r)) if r.released.queued() == 2), "B REACHABLE with only 1 and 2 queued"); let dropped = [Counter::IpControlQueueFull, Counter::NbPendingOverflow, Counter::NbPendingDropped, Counter::NbPendingEvicted]; assert!(dropped.iter().all(|&c| h.count(c) == 0), "nothing dropped"); - let first = h.out_with(limits::CONTROL_QUEUE + 1); - assert!(first[..limits::CONTROL_QUEUE].iter().all(|o| o.arp().is_some()), "the 64 queued frames first"); - let mut data: Vec> = first[limits::CONTROL_QUEUE..].iter().map(|o| o.ip().unwrap().payload()[8..].to_vec()).collect(); + // B's request, queued before the reply, never leaves: nothing is sent to maintain REACHABLE. + let first = h.out_with(limits::CONTROL_QUEUE); + let others = limits::CONTROL_QUEUE - 1; + assert!(first[..others].iter().all(|o| o.arp().is_some() && !o.requests(B)), "the other 63 queued frames first"); + let mut data: Vec> = first[others..].iter().map(|o| o.ip().unwrap().payload()[8..].to_vec()).collect(); assert_eq!(data, [b"1"]); // The next opportunity: [ip]'s frames, then datagram 3 from the transport. data.extend(h.out_with(1).iter().map(|o| o.ip().unwrap().payload()[8..].to_vec())); data.push(ip_of(&h.send(A, B, 5001, 5001, b"3").unwrap().unwrap()).unwrap().payload()[8..].to_vec()); assert_eq!(data, [b"1", b"2", b"3"]); + assert!(h.out().is_empty()); assert!(dropped.iter().all(|&c| h.count(c) == 0)); } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index cc771d4c911..b11d4056a2d 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -23,7 +23,7 @@ fn s_ip_nud_001_resolution_releases_the_datagram() { let out = h.out(); assert_eq!(out.len(), 1); assert_eq!(out[0].frame, hex(V_ARP_REQ)); - assert!(matches!(h.state(B), Some(Nud::Incomplete(s)) if s.queued() == 1)); + assert!(matches!(h.state(B), Some(Nud::Incomplete(s)) if s.pending.queued() == 1)); h.at(5); h.frame(&hex(V_ARP_REPLY)); assert!(h.is_reachable(B)); @@ -550,7 +550,7 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { ask_wide(&mut h, neighbour(n)); } announce_wide(&mut h, b); - assert!(h.is_stale(b)); + assert!(matches!(h.state(b), Some(Nud::Stale(s)) if s.released.queued() == 2)); let new = neighbour(9_000); assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Ok(None)); assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); @@ -558,12 +558,28 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { assert!(h.state(b).is_none()); assert_eq!(h.count(Counter::NbPendingEvicted), 2); assert_eq!(h.events.iter().filter(|e| **e == Event::Unreachable(flow)).count(), 2); - assert!(h.out().iter().all(|o| o.to() != wide_mac(b))); + // A later entry for B's address resolves while B's turns still wait: its datagram + // keeps its own turn, behind a defence queued after them. + for t in [1_511, 2_511, 3_511] { + h.ip.fire(H::instant(t)); + } + assert!(matches!(h.state(new), Some(Nud::Failed))); + h.at(3_511); + h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, WIDE_A, MacAddr::ZERO, WIDE_A))); + assert_eq!(h.send(WIDE_A, b, 5001, 5001, b"3"), Ok(None)); + h.frame(ð(MAC_A, wide_mac(b), 0x0806, &arp_packet(2, wide_mac(b), b, MAC_A, WIDE_A))); + let out = h.out(); + assert_eq!(out.len(), limits::CONTROL_QUEUE + 2); + assert!(out[..limits::CONTROL_QUEUE].iter().all(|o| o.to() != wide_mac(b)), "1 and 2 never leave"); + assert!(out[limits::CONTROL_QUEUE].arp().is_some_and(|a| a.sender_ip == WIDE_A && a.target_ip == WIDE_A), "the defence"); + assert_eq!(out[limits::CONTROL_QUEUE + 1].to(), wide_mac(b)); + assert_eq!(payload(&out[limits::CONTROL_QUEUE + 1]), b"3"); } else { assert!(h.state(neighbour(1)).is_none(), "the longest unused of the others"); - assert!(h.is_stale(b)); + assert!(matches!(h.state(b), Some(Nud::Stale(s)) if s.released.queued() == 2), "B keeps 1 and 2"); let to_b: Vec> = h.out().iter().filter(|o| o.to() == wide_mac(b)).map(payload).collect(); assert_eq!(to_b, [b"1", b"2"]); + assert!(h.is_stale(b), "its idle lifetime has not passed: it stays once they have left"); assert_eq!(h.count(Counter::NbPendingEvicted), 0); } } @@ -571,24 +587,43 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { #[test] fn s_ip_nud_030_released_datagrams_keep_their_entry() { - let mut h = H::fixture_i(); - for data in [b"1", b"2"] { - assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); - } - h.out(); - for n in 0..limits::CONTROL_QUEUE as u8 { - let m = MacAddr([2, 1, 0, 0, 0, n]); - h.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, Ipv4Addr::new(192, 0, 2, 100 + n), MacAddr::ZERO, A))); + // B STALE since t = 0, and B quiescent UNREACHABLE since t = 4,000: each idle lifetime passes + // while B holds 1 and 2. + for unreachable in [false, true] { + let mut h = H::fixture_i(); + for data in [b"1", b"2"] { + assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); + } + h.out(); + for n in 0..limits::CONTROL_QUEUE as u8 { + let m = MacAddr([2, 1, 0, 0, 0, n]); + h.frame(ð(MacAddr::BROADCAST, m, 0x0806, &arp_packet(1, m, Ipv4Addr::new(192, 0, 2, 100 + n), MacAddr::ZERO, A))); + } + let held = |h: &H| match h.state(B) { + Some(Nud::Stale(s)) if !unreachable => Some(s.released.queued()), + Some(Nud::Unreachable(u)) if unreachable && u.quiescent() => Some(u.released.queued()), + _ => None, + }; + let idle = if unreachable { + h.frame(&hex(V_ARP_REPLY)); + h.ip.advise(h.clock(), B, Advice::Reverify); + for t in [1_000, 2_000, 3_000, 4_000] { + h.ip.fire(H::instant(t)); + } + 4_000 + 600_000 + } else { + h.frame(ð(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, MAC_B, B, MacAddr::ZERO, B))); + 600_000 + }; + assert_eq!(held(&h), Some(2)); + h.ip.fire(H::instant(idle)); + h.at(idle); + assert_eq!(held(&h), Some(2), "not idle while it holds 1 and 2"); + assert!(h.out_with(limits::CONTROL_QUEUE).iter().all(|o| o.to() != MAC_B), "the queued frames first"); + assert_eq!(held(&h), Some(2), "1 and 2 are not out yet"); + assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"1"]); + assert_eq!(held(&h), Some(1)); + assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"2"]); + assert!(h.state(B).is_none(), "deleted as its queue drains"); } - h.frame(ð(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, MAC_B, B, MacAddr::ZERO, B))); - assert!(h.is_stale(B)); - h.ip.fire(H::instant(600_000)); - h.at(600_000); - assert!(h.is_stale(B), "not idle while it holds 1 and 2"); - assert!(h.out_with(limits::CONTROL_QUEUE).iter().all(|o| o.to() != MAC_B), "the queued frames first"); - assert!(h.is_stale(B), "1 and 2 are not out yet"); - assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"1"]); - assert!(h.is_stale(B)); - assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"2"]); - assert!(h.state(B).is_none(), "deleted as its queue drains"); } diff --git a/toyos-net-tcp/src/lib.rs b/toyos-net-tcp/src/lib.rs index 2a20993ff21..f5861ac4912 100644 --- a/toyos-net-tcp/src/lib.rs +++ b/toyos-net-tcp/src/lib.rs @@ -220,7 +220,6 @@ pub enum Error { Closing, WouldBlock, AddrInUse, - /// The remote is not a unicast address, or names the local endpoint. InvalidRemote, Failed(Failure), } diff --git a/toyos-net-tcp/src/stack.rs b/toyos-net-tcp/src/stack.rs index d7398f132e8..2a992d2d427 100644 --- a/toyos-net-tcp/src/stack.rs +++ b/toyos-net-tcp/src/stack.rs @@ -10,6 +10,7 @@ use alloc::vec::Vec; use core::net::Ipv4Addr; use core::time::Duration; +use toyos_net_wire::addr::is_host; use toyos_net_wire::icmp::UnreachableCode; use toyos_net_wire::tcp::{Control, EstablishedOptions, RawWindow, TcpBuilder, TcpSegment}; use toyos_net_wire::Port; @@ -171,10 +172,6 @@ pub struct Tcp { scratch: Vec, } -fn unicast(addr: Ipv4Addr) -> bool { - !(addr.is_unspecified() || addr.is_broadcast() || addr.is_multicast() || addr.octets()[0] >= 240) -} - fn slot(slots: &mut [Slot], index: u32, generation: u32) -> Option<&mut T> { slots.get_mut(usize::try_from(index).ok()?).filter(|s| s.generation == generation).and_then(|s| s.value.as_mut()) } @@ -557,7 +554,7 @@ impl Tcp { /// An active open from `local` (chosen by [ip]'s route lookup), from `port` or an ephemeral one. pub fn connect(&mut self, now: Instant, local: Ipv4Addr, port: Option, remote: Endpoint) -> Result { - if !unicast(remote.addr) { + if !is_host(remote.addr) { return Err(Error::InvalidRemote); } let port = match port { diff --git a/toyos-net-wire/src/addr.rs b/toyos-net-wire/src/addr.rs index c945fc7551a..bee580bbc3f 100644 --- a/toyos-net-wire/src/addr.rs +++ b/toyos-net-wire/src/addr.rs @@ -1,5 +1,4 @@ -//! IPv4 address classes and prefixes (RFC 1122 §3.2.1.3, RFC 6890, RFC 4632, RFC 3021): the one -//! place a net crate reads them from. +//! IPv4 address classes and prefixes (RFC 1122 §3.2.1.3, RFC 6890, RFC 4632, RFC 3021). use core::net::Ipv4Addr; From 3fe7a75d331cec03ac261b381adb494f20ca0b3d Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 19:12:52 +0200 Subject: [PATCH 13/20] toyos-net-ip: NBR-08 pins where a waiting released datagram goes A datagram resolution released waits in its entry's queue and leaves to the MAC the entry holds when its turn comes. NBR-08 gains the arm that pins it: R resolves to MAC R while the control queue is full, moves to MAC X, and the datagram leaves to MAC X. Stamping the MAC at resolution turns it red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/tests/nbr.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/toyos-net-ip/tests/nbr.rs b/toyos-net-ip/tests/nbr.rs index 73c69963a45..1ad12f795d2 100644 --- a/toyos-net-ip/tests/nbr.rs +++ b/toyos-net-ip/tests/nbr.rs @@ -89,6 +89,21 @@ fn s_ip_nbr_008_a_known_router_moving() { assert_eq!(h.mac_of(R), Some(MAC_X)); assert_eq!(h.count(Counter::ArpMacChanged), 1); assert_eq!(h.refusals(Counter::ArpMacChanged)[0].peer, Peer::MacChange { ip: R, old: MAC_R, new: MAC_X }); + + // A datagram released to R that still waits for room leaves to the MAC R holds then. + let mut h = H::fixture_i(); + assert_eq!(h.send(A, R, 5001, 5001, b"1"), Ok(None)); + h.out(); + for n in 0..toyos_net_ip::limits::CONTROL_QUEUE as u8 { + h.frame(&request(MacAddr([2, 1, 0, 0, 0, n]), ip4(192, 0, 2, 100 + n), A)); + } + h.frame(&request(MAC_R, R, A)); + assert!(matches!(h.state(R), Some(Nud::Stale(s)) if s.released.queued() == 1)); + h.frame(&hex(V_ARP_ROUTER_MOVED)); + assert_eq!(h.mac_of(R), Some(MAC_X)); + let out = h.out(); + assert_eq!(out.last().map(|o| (o.to(), o.ip().is_some())), Some((MAC_X, true))); + assert!(out.iter().all(|o| o.to() != MAC_R)); } #[test] From 0e065c968d0b3e576140a42d800a37d129cbe822 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 19:14:56 +0200 Subject: [PATCH 14/20] toyos-net-tcp: HS-35 pins the 0/8 and 127/8 refusals is_host brings connect now refuses what `toyos_net_wire::addr::is_host` refuses, which takes in 0/8 and 127/8 beside HS-35's broadcast, multicast and unspecified remotes (MUST-46). HS-35 adds 127.0.0.1 and 0.1.2.3, so the old predicate turns it red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-tcp/tests/handshake.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toyos-net-tcp/tests/handshake.rs b/toyos-net-tcp/tests/handshake.rs index e2833fca19b..c7557e2de29 100644 --- a/toyos-net-tcp/tests/handshake.rs +++ b/toyos-net-tcp/tests/handshake.rs @@ -367,7 +367,7 @@ fn s_hs_034_synack_give_up() { #[test] fn s_hs_035_connect_needs_a_unicast_remote() { let mut h = H::new(65_535); - for addr in [Ipv4Addr::BROADCAST, Ipv4Addr::new(224, 0, 0, 1), Ipv4Addr::UNSPECIFIED] { + for addr in [Ipv4Addr::BROADCAST, Ipv4Addr::new(224, 0, 0, 1), Ipv4Addr::UNSPECIFIED, Ipv4Addr::LOCALHOST, Ipv4Addr::new(0, 1, 2, 3)] { assert_eq!(h.tcp.connect(h.now(), A, None, ep(addr, 80)), Err(Error::InvalidRemote)); } nothing(&h.transmit()); From 3bd75d3bc940578bbfdb819b6f04e12799528f43 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 19:18:11 +0200 Subject: [PATCH 15/20] toyos-net-ip: an evicted entry's turns count against PENDING_TOTAL A turn whose entry was evicted stays in the control queue until the transmit opportunity reaches it; it leaves nothing then. Until the eviction counted it out of the interface's `held`, so repeated last-resort evictions behind a stalled transmit could stack dead turns without bound. `held` now counts pending datagrams and every turn not yet reached, and a turn is spent when it is reached, so PENDING_TOTAL bounds them together. NUD-29 gains an arm: after B's eviction the interface takes 62 datagrams, not 64, until B's two turns are reached. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/iface.rs | 4 +-- toyos-net-ip/src/nud.rs | 9 +++---- toyos-net-ip/tests/nud.rs | 52 ++++++++++++++++++++++++++------------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs index 1b44ec0ddda..ea0aeb57b09 100644 --- a/toyos-net-ip/src/iface.rs +++ b/toyos-net-ip/src/iface.rs @@ -25,8 +25,8 @@ pub(crate) struct Interface { pub neighbours: BTreeMap, /// Neighbour entries created so far: the next one's id. pub entries: u64, - /// Datagrams waiting in [ip] for this interface: held for resolution, or released and not - /// yet left. + /// Datagrams held for resolution, and the turns in the control queue not yet reached, an + /// evicted entry's included: PENDING_TOTAL bounds them together. pub held: usize, pub reachable: Duration, pub reachable_drawn: Instant, diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 207c048ccf1..5ad06473667 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -308,12 +308,11 @@ fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { } /// Deletes `addr`'s entry, and with it the released datagrams its queue still holds: their turns -/// name an entry that is gone. +/// name an entry that is gone, and count in `held` until they are reached. fn remove(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { cx.timers.cancel(timer(cx, addr)); let Some(mut n) = i.neighbours.remove(&addr) else { return }; for held in n.state.take_released().0 { - i.held = i.held.saturating_sub(1); drop_held(cx, held, Counter::NbPendingEvicted); } } @@ -426,9 +425,10 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } } -/// The turn of entry `id`'s oldest released datagram: it leaves now, to `addr`'s MAC of this -/// moment. `None` when that entry is gone. +/// The turn of entry `id`'s oldest released datagram came, and is spent: the datagram leaves now, +/// to `addr`'s MAC of this moment. `None` when that entry is gone. pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, id: u64) -> Option { + i.held = i.held.saturating_sub(1); let n = i.neighbours.get_mut(&addr).filter(|n| n.id == id)?; let mac = n.state.mac()?; let released = n.state.released_mut()?; @@ -437,7 +437,6 @@ pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, id: u64) if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { *destination = mac.0; } - i.held = i.held.saturating_sub(1); if last { drained(i, cx, addr); } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index b11d4056a2d..5fb418bc939 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -531,7 +531,10 @@ fn s_ip_nud_027_prop_reachable_time_is_drawn_every_two_hours() { fn s_ip_nud_029_released_datagrams_are_evicted_last() { let b = neighbour(0); let flow = Flow { source: WIDE_A, source_port: Port::new(5001).unwrap(), destination: b, destination_port: Port::new(5001).unwrap() }; - for others_reachable in [false, true] { + // The other 511 STALE; then REACHABLE, twice: for a later entry at B's address, and for the + // interface's bound. + for arm in 0..3 { + let others_reachable = arm > 0; let mut h = wide(); for data in [b"1", b"2"] { assert_eq!(h.send(WIDE_A, b, 5001, 5001, data), Ok(None)); @@ -554,16 +557,25 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { let new = neighbour(9_000); assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Ok(None)); assert!(matches!(h.state(new), Some(Nud::Incomplete(_)))); - if others_reachable { - assert!(h.state(b).is_none()); - assert_eq!(h.count(Counter::NbPendingEvicted), 2); - assert_eq!(h.events.iter().filter(|e| **e == Event::Unreachable(flow)).count(), 2); - // A later entry for B's address resolves while B's turns still wait: its datagram - // keeps its own turn, behind a defence queued after them. - for t in [1_511, 2_511, 3_511] { - h.ip.fire(H::instant(t)); - } - assert!(matches!(h.state(new), Some(Nud::Failed))); + if !others_reachable { + assert!(h.state(neighbour(1)).is_none(), "the longest unused of the others"); + assert!(matches!(h.state(b), Some(Nud::Stale(s)) if s.released.queued() == 2), "B keeps 1 and 2"); + let to_b: Vec> = h.out().iter().filter(|o| o.to() == wide_mac(b)).map(payload).collect(); + assert_eq!(to_b, [b"1", b"2"]); + assert!(h.is_stale(b), "its idle lifetime has not passed: it stays once they have left"); + assert_eq!(h.count(Counter::NbPendingEvicted), 0); + continue; + } + assert!(h.state(b).is_none()); + assert_eq!(h.count(Counter::NbPendingEvicted), 2); + assert_eq!(h.events.iter().filter(|e| **e == Event::Unreachable(flow)).count(), 2); + for t in [1_511, 2_511, 3_511] { + h.ip.fire(H::instant(t)); + } + assert!(matches!(h.state(new), Some(Nud::Failed))); + if arm == 1 { + // A later entry at B's address resolves while B's turns still wait: its datagram keeps + // its own turn, behind a defence queued after them. h.at(3_511); h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, WIDE_A, MacAddr::ZERO, WIDE_A))); assert_eq!(h.send(WIDE_A, b, 5001, 5001, b"3"), Ok(None)); @@ -575,12 +587,18 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { assert_eq!(out[limits::CONTROL_QUEUE + 1].to(), wide_mac(b)); assert_eq!(payload(&out[limits::CONTROL_QUEUE + 1]), b"3"); } else { - assert!(h.state(neighbour(1)).is_none(), "the longest unused of the others"); - assert!(matches!(h.state(b), Some(Nud::Stale(s)) if s.released.queued() == 2), "B keeps 1 and 2"); - let to_b: Vec> = h.out().iter().filter(|o| o.to() == wide_mac(b)).map(payload).collect(); - assert_eq!(to_b, [b"1", b"2"]); - assert!(h.is_stale(b), "its idle lifetime has not passed: it stays once they have left"); - assert_eq!(h.count(Counter::NbPendingEvicted), 0); + // B's two turns count against PENDING_TOTAL until the control queue reaches them. By + // 46,000 the FAILED entry is gone and the others are STALE, so entries can be made. + for t in [23_511, 46_000] { + h.ip.fire(H::instant(t)); + } + h.at(46_000); + let mut accepted = 0; + while h.send(WIDE_A, neighbour(10_000 + accepted / 8), 5001, 5001, b"y") == Ok(None) { + accepted += 1; + } + assert_eq!(h.count(Counter::NbPendingFull), 1); + assert_eq!(accepted as usize, limits::nud::PENDING_TOTAL - 2); } } } From 8ea5e93f6455636d93178f10050971abb8ae5f82 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 20:43:53 +0200 Subject: [PATCH 16/20] toyos-net-ip: an evicted entry's turns go with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 5's review upheld a BLOCKER: an evicted entry's turns stayed in the control queue and counted against PENDING_TOTAL until the transmit opportunity reached them, so NUD-29 pinned a refusal with 62 datagrams queued where ip §6.5 refuses only at 64. `remove` now drops the entry's turns from the control queue, both from the queue itself and from those waiting for room, refills it, and takes them out of the interface's count. Every other removal happens with the entry's queue empty, so the turns for an address are always its current entry's. No turn outlives its entry, and the id that told a later entry's turns from an evicted one's goes: `Neighbour::id`, `Interface::entries`, `insert`'s counter, `Turn::id` and `leave`'s filter. NUD-29's second arm now accepts PENDING_TOTAL datagrams while the control queue still holds everything it held before the eviction. Its first arm, a later entry at B's address behind a defence, now goes red if the turns stay: the first of them would carry the new entry's datagram out ahead of the defence. A request queued behind released datagrams could strand a STALE entry: if its idle lifetime passed while the datagrams waited, the last one's leaving kept the entry because the request was still queued, and the request's drop at egress deleted nothing. `request_leaves` now calls `drained` when it drops a request, and `drained` checks the released queue itself, so `leave` calls it after every datagram rather than only after the last. NUD-30's third arm pins the deletion. OUT-08's second arm pins the other half of the request form chosen at departure: a request queued in INCOMPLETE and still queued when the entry reaches PROBE leaves unicast to the cached MAC, and it is the only request that leaves. `Nud::released()` had one caller and is folded into `releasing()`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/egress.rs | 26 +++++++++++++----- toyos-net-ip/src/iface.rs | 6 ++--- toyos-net-ip/src/lib.rs | 1 - toyos-net-ip/src/nud.rs | 54 ++++++++++++++++---------------------- toyos-net-ip/tests/io.rs | 18 +++++++++++++ toyos-net-ip/tests/nud.rs | 31 ++++++++++++++++++---- 6 files changed, 89 insertions(+), 47 deletions(-) diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs index 692ced2fd4c..995ebab21f4 100644 --- a/toyos-net-ip/src/egress.rs +++ b/toyos-net-ip/src/egress.rs @@ -5,8 +5,7 @@ //! counted, and its producer moves on as if it had left. Three kinds are never dropped: a //! released datagram's turn, an ACD probe and an ACD announcement. One that finds the FIFO full //! waits behind it and enters as room appears, ahead of any frame queued later (§5.4, RFC 5227 -//! §2.1). A turn holds no datagram: it names the entry whose queue does, and a turn whose entry -//! was evicted leaves nothing and spends no credit. +//! §2.1). A turn holds no datagram: it names the entry whose queue does, and goes with that entry. //! //! Every datagram is atomic: DF set, identification 0 (`toyos-net-wire`'s one IPv4 form). @@ -63,13 +62,12 @@ impl Item { } } -/// A released datagram's place in the FIFO: the oldest of entry `id`'s released datagrams leaves +/// A released datagram's place in the FIFO: the oldest of `next_hop`'s released datagrams leaves /// when it comes (§6.5). #[derive(Debug)] pub(crate) struct Turn { pub iface: IfIndex, pub next_hop: Ipv4Addr, - pub id: u64, } #[derive(Debug, Default)] @@ -133,6 +131,22 @@ impl Control { } } + /// Drops the turns of `next_hop`'s entry on `iface`, which is gone; returns how many. + pub fn drop_turns(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> usize { + let mut dropped = 0usize; + let mut keep = |item: &Item| { + let theirs = matches!(item, Item::Turn(t) if t.iface == iface && t.next_hop == next_hop); + if theirs { + dropped = dropped.saturating_add(1); + } + !theirs + }; + self.items.retain(&mut keep); + self.waiting.retain(&mut keep); + self.refill(); + dropped + } + /// Drops everything waiting for `iface`, whose link went down. pub fn purge(&mut self, iface: IfIndex) { let (gone, kept): (Vec, Vec) = self.items.drain(..).partition(|item| item.iface() == iface); @@ -258,8 +272,8 @@ impl Ip { sent(&mut cx, kind); true } - Item::Turn(Turn { next_hop, id, .. }) => { - let Some(held) = nud::leave(i, &mut cx, next_hop, id) else { return false }; + Item::Turn(Turn { next_hop, .. }) => { + let Some(held) = nud::leave(i, &mut cx, next_hop) else { return false }; sink(iface, &held.frame); sent(&mut cx, held.kind); true diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs index ea0aeb57b09..5fb847f7635 100644 --- a/toyos-net-ip/src/iface.rs +++ b/toyos-net-ip/src/iface.rs @@ -23,10 +23,8 @@ pub(crate) struct Interface { pub gateways: Vec, pub active: Option, pub neighbours: BTreeMap, - /// Neighbour entries created so far: the next one's id. - pub entries: u64, - /// Datagrams held for resolution, and the turns in the control queue not yet reached, an - /// evicted entry's included: PENDING_TOTAL bounds them together. + /// Datagrams its entries queue, held for resolution or released and not yet left: + /// PENDING_TOTAL bounds them (§6.5). pub held: usize, pub reachable: Duration, pub reachable_drawn: Instant, diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs index 9829c456d9f..e7eb754ecf7 100644 --- a/toyos-net-ip/src/lib.rs +++ b/toyos-net-ip/src/lib.rs @@ -342,7 +342,6 @@ impl Ip { gateways: Vec::new(), active: None, neighbours: BTreeMap::new(), - entries: 0, held: 0, reachable, reachable_drawn: now, diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 5ad06473667..32a3cda1160 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -166,7 +166,7 @@ impl Nud { matches!(self, Self::Incomplete(_) | Self::Probe(_) | Self::Unreachable(_)) } - fn released(&self) -> Option<&Released> { + fn released_mut(&mut self) -> Option<&mut Released> { match self { Self::Reachable(Reachable { released, .. }) | Self::Stale(Linked { released, .. }) @@ -177,22 +177,18 @@ impl Nud { } } - fn released_mut(&mut self) -> Option<&mut Released> { + /// Released datagrams have yet to leave. + fn releasing(&self) -> bool { match self { Self::Reachable(Reachable { released, .. }) | Self::Stale(Linked { released, .. }) | Self::Delay(Linked { released, .. }) | Self::Probe(Probing { released, .. }) - | Self::Unreachable(Unreachable { released, .. }) => Some(released), - Self::Incomplete(_) | Self::Failed => None, + | Self::Unreachable(Unreachable { released, .. }) => !released.0.is_empty(), + Self::Incomplete(_) | Self::Failed => false, } } - /// Released datagrams have yet to leave. - fn releasing(&self) -> bool { - self.released().is_some_and(|r| !r.0.is_empty()) - } - /// A resolved state's released datagrams, taken for the state that follows it. fn take_released(&mut self) -> Released { self.released_mut().map(core::mem::take).unwrap_or_default() @@ -202,9 +198,6 @@ impl Nud { #[derive(Debug)] pub(crate) struct Neighbour { pub state: Nud, - /// Names this entry apart from any earlier one for the same address: a turn of an evicted - /// entry must not hand a later one's datagram its place. - pub id: u64, pub last_request: Option, /// A request waits in the control queue. pub queued: bool, @@ -250,6 +243,7 @@ pub(crate) fn request_leaves(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) Nud::Probe(p) => p.mac, Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed => { n.queued = false; + drained(i, cx, addr); return None; } }; @@ -307,20 +301,20 @@ fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { true } -/// Deletes `addr`'s entry, and with it the released datagrams its queue still holds: their turns -/// name an entry that is gone, and count in `held` until they are reached. +/// Deletes `addr`'s entry, and with it the released datagrams its queue still holds and their +/// turns: no turn outlives its entry. fn remove(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { cx.timers.cancel(timer(cx, addr)); let Some(mut n) = i.neighbours.remove(&addr) else { return }; + let turns = cx.control.drop_turns(cx.iface, addr); + i.held = i.held.saturating_sub(turns); for held in n.state.take_released().0 { drop_held(cx, held, Counter::NbPendingEvicted); } } fn insert(i: &mut Interface, addr: Ipv4Addr, state: Nud, now: Instant, hint: Option) { - let id = i.entries; - i.entries = i.entries.wrapping_add(1); - i.neighbours.insert(addr, Neighbour { state, id, last_request: None, queued: false, used: now, hint }); + i.neighbours.insert(addr, Neighbour { state, last_request: None, queued: false, used: now, hint }); } /// A datagram or a flow wants `addr` now (§6.3 "send"): creates INCOMPLETE, moves STALE to @@ -425,29 +419,27 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { } } -/// The turn of entry `id`'s oldest released datagram came, and is spent: the datagram leaves now, -/// to `addr`'s MAC of this moment. `None` when that entry is gone. -pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, id: u64) -> Option { +/// The turn of `addr`'s oldest released datagram came, and is spent: the datagram leaves now, to +/// the MAC of this moment. +pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) -> Option { i.held = i.held.saturating_sub(1); - let n = i.neighbours.get_mut(&addr).filter(|n| n.id == id)?; + let n = i.neighbours.get_mut(&addr)?; let mac = n.state.mac()?; - let released = n.state.released_mut()?; - let mut held = released.pop()?; - let last = released.0.is_empty(); + let mut held = n.state.released_mut()?.pop()?; if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { *destination = mac.0; } - if last { - drained(i, cx, addr); - } + drained(i, cx, addr); Some(held) } -/// The last datagram released toward `addr` left. Every STALE or quiescent UNREACHABLE entry has -/// a deadline armed but one whose idle lifetime passed while it held them, which goes now. +/// A datagram or a request of `addr`'s left the control queue. Every STALE or quiescent +/// UNREACHABLE entry has a deadline armed but one whose idle lifetime passed while its queue held +/// datagrams, which goes once nothing of it waits there. fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let Some(n) = i.neighbours.get(&addr) else { return }; - let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.queued && cx.timers.get(timer(cx, addr)).is_none(); + let waits = n.state.releasing() || n.queued; + let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !waits && cx.timers.get(timer(cx, addr)).is_none(); if idle { remove(i, cx, addr); route::refresh_active(i, cx); @@ -495,7 +487,7 @@ fn inherit(cx: &mut Cx<'_>, addr: Ipv4Addr, n: &mut Neighbour, mac: MacAddr) -> }; let pending = core::mem::take(&mut s.pending).0; for _ in &pending { - cx.control.hold(Item::Turn(Turn { iface: cx.iface, next_hop: addr, id: n.id })); + cx.control.hold(Item::Turn(Turn { iface: cx.iface, next_hop: addr })); } cx.log.count(Counter::NbResolved); cx.log.event(Event::Resolved { iface: cx.iface, next_hop: addr }); diff --git a/toyos-net-ip/tests/io.rs b/toyos-net-ip/tests/io.rs index a5ca567b563..bcbbcafcf77 100644 --- a/toyos-net-ip/tests/io.rs +++ b/toyos-net-ip/tests/io.rs @@ -558,4 +558,22 @@ fn s_ip_out_008_released_datagrams_wait_at_the_head_of_their_queue() { assert_eq!(data, [b"1", b"2", b"3"]); assert!(h.out().is_empty()); assert!(dropped.iter().all(|&c| h.count(c) == 0)); + + // B's request, queued in INCOMPLETE and still queued in PROBE, leaves as PROBE's: to MAC B. + let mut h = H::fixture_i(); + assert_eq!(h.udp_to(B), Ok(None)); + h.frame(ð(MacAddr::BROADCAST, MAC_B, 0x0806, &arp_packet(1, MAC_B, B, MacAddr::ZERO, B))); + assert!(h.is_stale(B)); + assert!(h.udp_to(B).unwrap().is_some_and(|frame| destination_of(&frame) == MAC_B)); + assert!(matches!(h.state(B), Some(Nud::Delay(_)))); + h.ip.fire(H::instant(5_000)); + h.at(5_000); + assert!(matches!(h.state(B), Some(Nud::Probe(_)))); + let mut poll = hex(V_ARP_REQ); + poll[..6].copy_from_slice(&MAC_B.0); + let out = h.out(); + assert_eq!(out.len(), 2, "one request, then the datagram released to B"); + assert_eq!(out[0].frame, poll); + assert_eq!(out[1].to(), MAC_B); + assert_eq!(out[1].ip().unwrap().payload()[8..], *b"hi"); } diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 5fb418bc939..c5cb9687511 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -574,8 +574,8 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { } assert!(matches!(h.state(new), Some(Nud::Failed))); if arm == 1 { - // A later entry at B's address resolves while B's turns still wait: its datagram keeps - // its own turn, behind a defence queued after them. + // A later entry at B's address resolves: B's turns went with B, so its datagram waits + // for its own, behind a defence queued first. h.at(3_511); h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, WIDE_A, MacAddr::ZERO, WIDE_A))); assert_eq!(h.send(WIDE_A, b, 5001, 5001, b"3"), Ok(None)); @@ -587,8 +587,9 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { assert_eq!(out[limits::CONTROL_QUEUE + 1].to(), wide_mac(b)); assert_eq!(payload(&out[limits::CONTROL_QUEUE + 1]), b"3"); } else { - // B's two turns count against PENDING_TOTAL until the control queue reaches them. By - // 46,000 the FAILED entry is gone and the others are STALE, so entries can be made. + // B's two datagrams left the count with B, though the control queue has reached + // nothing since. By 46,000 the FAILED entry is gone and the others are STALE, so + // entries can be made. for t in [23_511, 46_000] { h.ip.fire(H::instant(t)); } @@ -598,7 +599,7 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { accepted += 1; } assert_eq!(h.count(Counter::NbPendingFull), 1); - assert_eq!(accepted as usize, limits::nud::PENDING_TOTAL - 2); + assert_eq!(accepted as usize, limits::nud::PENDING_TOTAL); } } } @@ -644,4 +645,24 @@ fn s_ip_nud_030_released_datagrams_keep_their_entry() { assert_eq!(h.out_with(1).iter().map(payload).collect::>(), [b"2"]); assert!(h.state(B).is_none(), "deleted as its queue drains"); } + + // B STALE at MAC X since 1,000 holds 1 and 2, with a request queued behind them by the advice + // before: its idle lifetime passes, and it goes when that request is taken out. + let mut h = H::fixture_i(); + for data in [b"1", b"2"] { + assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); + } + h.out(); + h.at(5); + h.frame(&hex(V_ARP_REPLY)); + h.at(1_000); + h.ip.advise(h.clock(), B, Advice::Reverify); + assert!(matches!(h.state(B), Some(Nud::Probe(_)))); + h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, B, MacAddr::ZERO, B))); + h.ip.fire(H::instant(601_000)); + h.at(601_000); + assert!(matches!(h.state(B), Some(Nud::Stale(s)) if s.released.queued() == 2), "not idle while it holds 1 and 2"); + let out: Vec<(MacAddr, Vec)> = h.out().iter().map(|o| (o.to(), payload(o))).collect(); + assert_eq!(out, [(MAC_X, b"1".to_vec()), (MAC_X, b"2".to_vec())], "the request is not sent"); + assert!(h.state(B).is_none(), "deleted as the request behind 1 and 2 is taken out"); } From c1474cd4858282a56c917f9077bfbbec66842c08 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 30 Sep 2026 22:25:40 +0200 Subject: [PATCH 17/20] toyos-net-ip: a drained entry goes with its last datagram Ruling 2 deletes an entry whose idle lifetime passed while its queue held released datagrams at drain. `drained` also kept it while a request of its waited in the control queue (`|| n.queued`), which gave the tree a second idle rule beside `fire`'s, which already deletes an idle entry whose request still waits. The term goes, and with it the only reason `request_leaves` called `drained`. A request that outlives its entry finds none when it leaves and sends nothing, as it does after `fire`. NUD-30's third arm now takes 1 and 2 out alone and asserts that B is gone as 2 leaves, before the request behind them is taken out. NUD-29 gains two arms for `Control::drop_turns`, which no test pinned: - RTE-004's overlapping prefixes put X on both interfaces. X on if1 holds 1 and 2 whose turns wait when X on if0 ends its FAILED hold-down; if1's datagrams must still leave to X on if1. This pins the `t.iface == iface` filter. - B resolves while the control queue has room, so its turns are inside it; 62 replies fill it behind them and a defence waits. Evicting B frees their room, which is the defence's: it must leave ahead of the new entry's request. This pins the `refill` in `drop_turns`. The two arms and the three earlier ones share their setup through `b_and_511`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/nud.rs | 10 ++--- toyos-net-ip/tests/nud.rs | 87 ++++++++++++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 32a3cda1160..4b971417636 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -243,7 +243,6 @@ pub(crate) fn request_leaves(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) Nud::Probe(p) => p.mac, Nud::Reachable(_) | Nud::Stale(_) | Nud::Delay(_) | Nud::Failed => { n.queued = false; - drained(i, cx, addr); return None; } }; @@ -433,13 +432,12 @@ pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) -> Optio Some(held) } -/// A datagram or a request of `addr`'s left the control queue. Every STALE or quiescent -/// UNREACHABLE entry has a deadline armed but one whose idle lifetime passed while its queue held -/// datagrams, which goes once nothing of it waits there. +/// A datagram of `addr`'s left the control queue. Every STALE or quiescent UNREACHABLE entry has a +/// deadline armed but one whose idle lifetime passed while its queue held datagrams, which goes +/// with the last of them. fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { let Some(n) = i.neighbours.get(&addr) else { return }; - let waits = n.state.releasing() || n.queued; - let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !waits && cx.timers.get(timer(cx, addr)).is_none(); + let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.state.releasing() && cx.timers.get(timer(cx, addr)).is_none(); if idle { remove(i, cx, addr); route::refresh_active(i, cx); diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index c5cb9687511..d294c66c676 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -5,7 +5,7 @@ mod common; use std::net::Ipv4Addr; use common::*; -use toyos_net_ip::{limits, Advice, Counter, Event, Flow, Nud, Resolution}; +use toyos_net_ip::{limits, Advice, Counter, Event, Flow, IfIndex, Nud, Resolution}; use toyos_net_wire::Port; fn flow_to(destination: Ipv4Addr) -> Flow { @@ -527,6 +527,26 @@ fn s_ip_nud_027_prop_reachable_time_is_drawn_every_two_hours() { } } +/// wide() with B, neighbour(0), holding 1 and 2 unresolved, and the other 511 entries made one per +/// ms behind it: REACHABLE, or STALE. +fn b_and_511(reachable: bool) -> H { + let mut h = wide(); + for data in [b"1", b"2"] { + assert_eq!(h.send(WIDE_A, neighbour(0), 5001, 5001, data), Ok(None)); + } + h.out(); + for n in 1..limits::nud::TABLE_MAX as u32 { + h.at(u64::from(n)); + if reachable { + reach_wide(&mut h, neighbour(n)); + } else { + ask_wide(&mut h, neighbour(n)); + h.out(); + } + } + h +} + #[test] fn s_ip_nud_029_released_datagrams_are_evicted_last() { let b = neighbour(0); @@ -535,20 +555,7 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { // interface's bound. for arm in 0..3 { let others_reachable = arm > 0; - let mut h = wide(); - for data in [b"1", b"2"] { - assert_eq!(h.send(WIDE_A, b, 5001, 5001, data), Ok(None)); - } - h.out(); - for n in 1..limits::nud::TABLE_MAX as u32 { - h.at(u64::from(n)); - if others_reachable { - reach_wide(&mut h, neighbour(n)); - } else { - ask_wide(&mut h, neighbour(n)); - h.out(); - } - } + let mut h = b_and_511(others_reachable); for n in 1..=limits::CONTROL_QUEUE as u32 { ask_wide(&mut h, neighbour(n)); } @@ -602,6 +609,47 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { assert_eq!(accepted as usize, limits::nud::PENDING_TOTAL); } } + + // B resolves while the control queue has room, so its turns are inside it; 62 replies fill it + // behind them, and a defence waits. The room B's eviction frees is the defence's, so it leaves + // ahead of the new entry's request. + let mut h = b_and_511(true); + announce_wide(&mut h, b); + for n in 1..=limits::CONTROL_QUEUE as u32 - 2 { + ask_wide(&mut h, neighbour(n)); + } + h.frame(ð(MacAddr::BROADCAST, MAC_X, 0x0806, &arp_packet(1, MAC_X, WIDE_A, MacAddr::ZERO, WIDE_A))); + let new = neighbour(9_000); + assert_eq!(h.send(WIDE_A, new, 5001, 5001, b"x"), Ok(None)); + assert!(h.state(b).is_none()); + let out = h.out(); + assert_eq!(out.len(), limits::CONTROL_QUEUE); + assert!(out[limits::CONTROL_QUEUE - 2].arp().is_some_and(|a| a.sender_ip == WIDE_A && a.target_ip == WIDE_A), "the defence"); + assert!(out[limits::CONTROL_QUEUE - 1].requests(new)); + + // RTE-004's prefixes put X on both interfaces. X on if1 holds 1 and 2, whose turns wait, when + // X on if0 ends its FAILED hold-down: if0's entry goes, and if1's datagrams still leave to X. + let mut h = H::raw(); + let (if0, if1) = (h.if0, h.add_if1()); + let a1 = Ipv4Addr::new(192, 0, 2, 129); + h.assign(if0, A, 24, 0); + h.assign(if1, a1, 25, 5_000); + h.settle(); + let x = Ipv4Addr::new(192, 0, 2, 200); + let _ = h.ip.resolve(h.clock(), if0, x); + h.run(3_000); + assert!(matches!(h.ip.neighbour(if0, x), Some(Nud::Failed))); + for data in [b"1", b"2"] { + assert_eq!(h.send(a1, x, 5001, 5001, data), Ok(None)); + } + h.out(); + let _ = h.ip.receive(h.clock(), if1, ð(MAC_A1, MAC_X, 0x0806, &arp_packet(2, MAC_X, x, MAC_A1, a1))); + assert!(matches!(h.ip.neighbour(if1, x), Some(Nud::Reachable(r)) if r.released.queued() == 2)); + h.ip.fire(H::instant(23_000)); + assert!(h.ip.neighbour(if0, x).is_none()); + h.at(23_000); + let to_x: Vec<(IfIndex, Vec)> = h.out().iter().filter(|o| o.to() == MAC_X).map(|o| (o.iface, payload(o))).collect(); + assert_eq!(to_x, [(if1, b"1".to_vec()), (if1, b"2".to_vec())]); } #[test] @@ -647,7 +695,7 @@ fn s_ip_nud_030_released_datagrams_keep_their_entry() { } // B STALE at MAC X since 1,000 holds 1 and 2, with a request queued behind them by the advice - // before: its idle lifetime passes, and it goes when that request is taken out. + // before: its idle lifetime passes, and it goes as 2 leaves. let mut h = H::fixture_i(); for data in [b"1", b"2"] { assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); @@ -662,7 +710,8 @@ fn s_ip_nud_030_released_datagrams_keep_their_entry() { h.ip.fire(H::instant(601_000)); h.at(601_000); assert!(matches!(h.state(B), Some(Nud::Stale(s)) if s.released.queued() == 2), "not idle while it holds 1 and 2"); - let out: Vec<(MacAddr, Vec)> = h.out().iter().map(|o| (o.to(), payload(o))).collect(); - assert_eq!(out, [(MAC_X, b"1".to_vec()), (MAC_X, b"2".to_vec())], "the request is not sent"); - assert!(h.state(B).is_none(), "deleted as the request behind 1 and 2 is taken out"); + let out: Vec<(MacAddr, Vec)> = h.out_with(2).iter().map(|o| (o.to(), payload(o))).collect(); + assert_eq!(out, [(MAC_X, b"1".to_vec()), (MAC_X, b"2".to_vec())]); + assert!(h.state(B).is_none(), "deleted as 2 leaves"); + assert!(h.out().is_empty(), "the request finds no entry"); } From 7fab2160c882c213c1159b02b451da470a9a45f6 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:16:28 +0200 Subject: [PATCH 18/20] toyos-net-ip: `leave` deletes the idle entry its last datagram drains `drained` had one caller, `leave`, which already holds the entry. Its condition moves into `leave` over that borrow, so the second map lookup and the doc comment go, and `fire`'s comment names `leave` instead. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/nud.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 4b971417636..741a37f56b9 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -409,7 +409,7 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { s.backoff = false; cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); } - // Not idle while released datagrams are its: `drained` deletes it once they have left. + // Not idle while released datagrams are its: `leave` deletes it once they have left. Nud::Stale(_) | Nud::Unreachable(_) if releasing => {} Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => { remove(i, cx, addr); @@ -428,20 +428,12 @@ pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) -> Optio if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { *destination = mac.0; } - drained(i, cx, addr); - Some(held) -} - -/// A datagram of `addr`'s left the control queue. Every STALE or quiescent UNREACHABLE entry has a -/// deadline armed but one whose idle lifetime passed while its queue held datagrams, which goes -/// with the last of them. -fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { - let Some(n) = i.neighbours.get(&addr) else { return }; let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.state.releasing() && cx.timers.get(timer(cx, addr)).is_none(); if idle { remove(i, cx, addr); route::refresh_active(i, cx); } + Some(held) } /// PROBE's first unicast request, now or at the spacing boundary. From 2c2fb83c43f2ab5929874105ced45bb4f1e6fec1 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:16:48 +0200 Subject: [PATCH 19/20] toyos-net-ip tests: NUD-29's setup is named for B in a full table `b_and_511` stated a count that `limits::nud::TABLE_MAX` owns: its loop reads the limit, and the name and its doc comment went false the moment the limit moved. `b_in_a_full_table` and its doc name what it builds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/tests/nud.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index d294c66c676..6651a70960f 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -527,9 +527,9 @@ fn s_ip_nud_027_prop_reachable_time_is_drawn_every_two_hours() { } } -/// wide() with B, neighbour(0), holding 1 and 2 unresolved, and the other 511 entries made one per -/// ms behind it: REACHABLE, or STALE. -fn b_and_511(reachable: bool) -> H { +/// wide() with B, neighbour(0), holding 1 and 2 unresolved, and the rest of a full table made one +/// per ms behind it: REACHABLE, or STALE. +fn b_in_a_full_table(reachable: bool) -> H { let mut h = wide(); for data in [b"1", b"2"] { assert_eq!(h.send(WIDE_A, neighbour(0), 5001, 5001, data), Ok(None)); @@ -555,7 +555,7 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { // interface's bound. for arm in 0..3 { let others_reachable = arm > 0; - let mut h = b_and_511(others_reachable); + let mut h = b_in_a_full_table(others_reachable); for n in 1..=limits::CONTROL_QUEUE as u32 { ask_wide(&mut h, neighbour(n)); } @@ -613,7 +613,7 @@ fn s_ip_nud_029_released_datagrams_are_evicted_last() { // B resolves while the control queue has room, so its turns are inside it; 62 replies fill it // behind them, and a defence waits. The room B's eviction frees is the defence's, so it leaves // ahead of the new entry's request. - let mut h = b_and_511(true); + let mut h = b_in_a_full_table(true); announce_wide(&mut h, b); for n in 1..=limits::CONTROL_QUEUE as u32 - 2 { ask_wide(&mut h, neighbour(n)); From 754ce5e93b29d76721a4a2ca2d3cee3dd06d6ae0 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:18:19 +0200 Subject: [PATCH 20/20] toyos-net-ip: an entry's queued request goes with the entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `remove` dropped an entry's turns but left its queued `Item::Request`. `fire`, eviction and `leave` each delete an entry whose request may still wait, and a later INCOMPLETE entry at the same address sets `queued` again. The orphan then left as that entry's request, from its own earlier slot in the FIFO, and the new entry's item sent nothing. Count, form and spacing still came from the live entry; ip.md §5.4's order did not. `Control::drop_turns` becomes `purge_entry`: it drops the entry's requests with its turns, and still returns only the turns, since a request never counted toward `held`. No item outlives its entry. NUD-30's third arm goes on past B's deletion: a host asks for A, which queues a reply, and then a datagram to B makes a new entry there. The reply must leave first and the new entry's request after it. Without this change the orphan leaves as B's request ahead of the reply, and the test fails at the reply's assertion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-net-ip/src/egress.rs | 20 +++++++++++--------- toyos-net-ip/src/nud.rs | 6 +++--- toyos-net-ip/tests/nud.rs | 10 ++++++++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs index 995ebab21f4..23c09690462 100644 --- a/toyos-net-ip/src/egress.rs +++ b/toyos-net-ip/src/egress.rs @@ -131,20 +131,22 @@ impl Control { } } - /// Drops the turns of `next_hop`'s entry on `iface`, which is gone; returns how many. - pub fn drop_turns(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> usize { - let mut dropped = 0usize; - let mut keep = |item: &Item| { - let theirs = matches!(item, Item::Turn(t) if t.iface == iface && t.next_hop == next_hop); - if theirs { - dropped = dropped.saturating_add(1); + /// Drops the turns and requests of `next_hop`'s entry on `iface`, which is gone; returns how + /// many turns. + pub fn purge_entry(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> usize { + let mut turns = 0usize; + let mut keep = |item: &Item| match item { + Item::Turn(t) if t.iface == iface && t.next_hop == next_hop => { + turns = turns.saturating_add(1); + false } - !theirs + Item::Request { iface: on, target } => *on != iface || *target != next_hop, + _ => true, }; self.items.retain(&mut keep); self.waiting.retain(&mut keep); self.refill(); - dropped + turns } /// Drops everything waiting for `iface`, whose link went down. diff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs index 741a37f56b9..d598e9d80a1 100644 --- a/toyos-net-ip/src/nud.rs +++ b/toyos-net-ip/src/nud.rs @@ -300,12 +300,12 @@ fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool { true } -/// Deletes `addr`'s entry, and with it the released datagrams its queue still holds and their -/// turns: no turn outlives its entry. +/// Deletes `addr`'s entry, and with it the released datagrams its queue still holds, their turns +/// and its queued requests: no item outlives its entry. fn remove(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) { cx.timers.cancel(timer(cx, addr)); let Some(mut n) = i.neighbours.remove(&addr) else { return }; - let turns = cx.control.drop_turns(cx.iface, addr); + let turns = cx.control.purge_entry(cx.iface, addr); i.held = i.held.saturating_sub(turns); for held in n.state.take_released().0 { drop_held(cx, held, Counter::NbPendingEvicted); diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs index 6651a70960f..96760766551 100644 --- a/toyos-net-ip/tests/nud.rs +++ b/toyos-net-ip/tests/nud.rs @@ -695,7 +695,8 @@ fn s_ip_nud_030_released_datagrams_keep_their_entry() { } // B STALE at MAC X since 1,000 holds 1 and 2, with a request queued behind them by the advice - // before: its idle lifetime passes, and it goes as 2 leaves. + // before: its idle lifetime passes, and it goes as 2 leaves, its request with it. A later entry + // at B's address sends its own request, behind a reply queued first. let mut h = H::fixture_i(); for data in [b"1", b"2"] { assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); @@ -713,5 +714,10 @@ fn s_ip_nud_030_released_datagrams_keep_their_entry() { let out: Vec<(MacAddr, Vec)> = h.out_with(2).iter().map(|o| (o.to(), payload(o))).collect(); assert_eq!(out, [(MAC_X, b"1".to_vec()), (MAC_X, b"2".to_vec())]); assert!(h.state(B).is_none(), "deleted as 2 leaves"); - assert!(h.out().is_empty(), "the request finds no entry"); + h.frame(ð(MacAddr::BROADCAST, MAC_DNS, 0x0806, &arp_packet(1, MAC_DNS, DNS, MacAddr::ZERO, A))); + assert_eq!(h.send(A, B, 5001, 5001, b"3"), Ok(None)); + let out = h.out(); + assert_eq!(out.len(), 2); + assert!(out[0].arp().is_some_and(|a| a.target_ip == DNS), "the reply"); + assert!(out[1].requests(B)); }