diff --git a/Cargo.lock b/Cargo.lock index f22b64e4ae1..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" @@ -1129,6 +1138,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" @@ -1136,6 +1152,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 57ebd52d1eb..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", @@ -43,7 +44,9 @@ members = [ "toyos-manifest", "toyos-mdns", "toyos-mixer", + "toyos-net-ip", "toyos-net-tcp", + "toyos-net-udp", "toyos-net-wire", "toyos-pci", "toyos-pcid", 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..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,7 +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 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; `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: + +- 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/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..e9734d6aa75 --- /dev/null +++ b/toyos-dhcp/src/lib.rs @@ -0,0 +1,873 @@ +//! 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 a [`Refusal`]; 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::addr::{is_host, Cidr}; +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); + 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 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; + /// Refusals 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 struct Refusal { + pub rule: Counter, + pub 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, + refusals: Vec, +} + +/// §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 { + is_host(addr) && !addr.is_link_local() +} + +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(), + refusals: 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_refusals(&mut self) -> alloc::vec::Drain<'_, Refusal> { + self.refusals.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.refusals.len() >= limits::EVENTS { + self.counters.add(Counter::EventOverflow, 1); + } else { + self.refusals.push(Refusal { 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, 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 (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, timers, started.unwrap_or(now), true, draw); + } + if now >= timers.t1 { + return self.renew(now, lease, timers, 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 accepted = if !host(yiaddr) { + Err(Counter::YiaddrInvalid) + } else { + match (reply.lease, reply.mask) { + (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)), + }, + } + }; + let (cidr, lease) = accepted.inspect_err(|&rule| self.refuse(rule, peer)).ok()?; + Some(Offered { + cidr, + server, + lease, + 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.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.cidr.addr(), + secs: s.secs, + first: now, + step: 0, + deadline, + }); + Output { transmit: Some(transmit), ..Output::default() } + } + (State::Selecting(s), _) => { + 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.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.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.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) + } + (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) } + } + + /// §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 (cidr, address) = (offered.cidr, offered.cidr.addr()); + let mut router = None; + for candidate in offered.routers { + if host(candidate) && cidr.contains(candidate) && candidate != address { + router = Some(candidate); + break; + } + self.refuse(Counter::RouterInvalid, peer); + } + let mut dns = Vec::new(); + for server in offered.dns { + if !host(server) || Some(server) == cidr.broadcast() { + self.refuse(Counter::DnsInvalid, peer); + } else if dns.len() < limits::MAX_DNS { + dns.push(server); + } else { + self.counters.add(Counter::DnsTruncated, 1); + } + } + 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(offered.lease); + let t2 = match offered.t2 { + Some(t2) if t2 > 0 && t2 < offered.lease => ms(t2), + given => { + if given.is_some() { + self.refuse(Counter::TimerOptionInvalid, peer); + } + lease.saturating_mul(7).checked_div(8).unwrap_or(lease) + } + }; + let t1 = match offered.t1 { + Some(t1) if t1 > 0 && ms(t1) < t2 => ms(t1), + given => { + if given.is_some() { + 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: 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 { + cidr: Cidr, + server: Ipv4Addr, + lease: u32, + routers: Vec, + dns: Vec, + t1: Option, + t2: Option, +} 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..97e21a63ab4 --- /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, Lease, Peer, Phase, Refusal, 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.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()); + 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..35e472953d2 --- /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, HostName, Lease, Output, Phase, Refusal, 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 refusals: 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, refusals: 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_refusals().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.refusals.extend(self.client.drain_refusals()); + } + + pub fn count(&self, counter: Counter) -> u64 { + self.client.counters().get(counter) + } + + pub fn logged(&self, rule: Counter) -> usize { + self.refusals.iter().filter(|r| r.rule == 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..125e47d2802 --- /dev/null +++ b/toyos-dhcp/tests/parse.rs @@ -0,0 +1,320 @@ +//! §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::{limits, Config, Counter, Output, Peer, Phase, Refusal}; + +fn selecting() -> D { + D::ds(&[XID, 1_000]).0 +} + +/// 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:?}"); + 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); +} + +/// 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, 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, 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, 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, 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, false); +} + +#[test] +fn s_dhcp_dh_043_bootp_is_refused() { + 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, + ); +} + +#[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, true); +} + +#[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, 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, true); + } +} + +#[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, 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, true); + let mut nested = overload; + nested[126..130].copy_from_slice(&[52, 1, 1, 255]); + refused(&nested, Counter::OverloadInvalid, true); +} + +#[test] +fn s_dhcp_dh_052_message_types() { + 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)); + 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, true); +} + +#[test] +fn s_dhcp_dh_054_the_server_identifier() { + 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, true); + } +} + +#[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, true); + } +} + +#[test] +fn s_dhcp_dh_056_the_lease_time() { + 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, 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, 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)); + 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, 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, true); + 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..e50823dfb58 --- /dev/null +++ b/toyos-dhcp/tests/props.rs @@ -0,0 +1,201 @@ +//! 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 refused message names exactly one, an accepted one none. +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 phase = d.client.phase(); + let out = d.receive(now, &m); + 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) + } + }; + 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); +} 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..c3a093905e4 --- /dev/null +++ b/toyos-net-ip/src/acd.rs @@ -0,0 +1,192 @@ +//! 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. + +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); +} + +/// 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) { + 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 }); + } +} + +/// 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 }; + 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, defending: 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. +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; 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, 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)); + } 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, defending, .. } = &mut a.phase { + *owed = 0; + *queued = false; + *defending = 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..9fc622f06aa --- /dev/null +++ b/toyos-net-ip/src/addr.rs @@ -0,0 +1,44 @@ +//! An interface's address records. + +use toyos_net_wire::addr::Cidr; +use toyos_net_wire::Instant; + +#[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, 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)] +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..a04fb60cdd1 --- /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::addr::is_host; +use toyos_net_wire::arp::{Arp, Operation}; +use toyos_net_wire::ethernet::MacClass; + +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..c10f73f2ed5 --- /dev/null +++ b/toyos-net-ip/src/config.rs @@ -0,0 +1,170 @@ +//! 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::addr::{is_host, Cidr}; +use toyos_net_wire::ipv4::MulticastAddr; +use toyos_net_wire::Instant; + +use crate::addr::{Address, 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 = 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), + }; + 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 != cidr { + held.cidr = cidr; + 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); + 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 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..8029f1d12b8 --- /dev/null +++ b/toyos-net-ip/src/counters.rs @@ -0,0 +1,146 @@ +//! 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"; + NbPendingDropped = "nb.pending-dropped"; + NbPendingEvicted = "nb.pending-evicted"; + 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..23c09690462 --- /dev/null +++ b/toyos-net-ip/src/egress.rs @@ -0,0 +1,382 @@ +//! 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 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. 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 goes with that entry. +//! +//! 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 }, + 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 }, + Turn(Turn), +} + +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::Turn(Turn { iface, .. }) => *iface, + } + } +} + +/// 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, +} + +#[derive(Debug, Default)] +pub(crate) struct Control { + items: VecDeque, + echoes: usize, + /// What may not be dropped and found `items` full, in 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, .. }); + if echo && self.echoes >= ECHO_REPLIES { + log.count(Counter::IcmpEchoReplyDropped); + return false; + } + if self.full() { + log.count(Counter::IpControlQueueFull); + return false; + } + if echo { + self.echoes = self.echoes.saturating_add(1); + } + self.items.push_back(item); + true + } + + /// 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(item); + } else { + self.items.push_back(item); + } + } + + fn pop(&mut self) -> Option { + let item = self.items.pop_front()?; + self.forget(&item); + self.refill(); + Some(item) + } + + fn forget(&mut self, item: &Item) { + if matches!(item, Item::Frame { kind: FrameKind::Echo, .. }) { + self.echoes = self.echoes.saturating_sub(1); + } + } + + /// Room goes to the oldest item waiting for it. + fn refill(&mut self) { + while !self.full() { + let Some(item) = self.waiting.pop_front() else { return }; + self.items.push_back(item); + } + } + + /// 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 + } + Item::Request { iface: on, target } => *on != iface || *target != next_hop, + _ => true, + }; + self.items.retain(&mut keep); + self.waiting.retain(&mut keep); + self.refill(); + turns + } + + /// 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); + } + self.waiting.retain(|item| item.iface() != iface); + self.refill(); + } +} + +/// 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, .. } => { + 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); + } + 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::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 + } + } + } + + /// 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..ef9fcefd1bb --- /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::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::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..5fb847f7635 --- /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 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, + 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..5f9e9296e65 --- /dev/null +++ b/toyos-net-ip/src/igmp.rs @@ -0,0 +1,526 @@ +//! 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, + }; + // 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().take(IGMP_QUERY_SOURCES.saturating_add(1)).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) => { + for source in sources { + if list.len() > IGMP_QUERY_SOURCES { + break; + } + if !list.contains(&source) { + list.push(source); + } + } + 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..477b4dbdffc --- /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::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; +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::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 (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() + && 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 => {} + } + if is_martian(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, 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..e7eb754ecf7 --- /dev/null +++ b/toyos-net-ip/src/lib.rs @@ -0,0 +1,488 @@ +//! 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 arp; +mod config; +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::{Held, Incomplete, Linked, Nud, Pending, Probing, Reachable, Released, 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 + } + + /// 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 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.prefix_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; only INCOMPLETE takes datagrams into its + /// pending queue, and a resolved state's queue only drains. + /// + /// ``` + /// use toyos_net_ip::{Held, Linked, Nud}; + /// fn f(n: &mut Nud, d: Held) -> Option { + /// match n { + /// 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 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::Failed => None, + /// } + /// } + /// ``` + /// + /// ```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(_) = n {} } + /// ``` + /// + /// ```compile_fail + /// 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::{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::{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::{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/limiter.rs b/toyos-net-ip/src/limiter.rs new file mode 100644 index 00000000000..212a64c4d02 --- /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. + 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..d598e9d80a1 --- /dev/null +++ b/toyos-net-ip/src/nud.rs @@ -0,0 +1,585 @@ +//! 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, and a pending queue +//! 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; +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, Turn}; +use crate::iface::{Cx, Interface}; +use crate::limits::nud::{ + BACKOFF_MULTIPLE, BROADCAST_SOLICIT, DELAY_FIRST_PROBE, FAILED_HOLD, IDLE_LIFETIME, LOCKTIME, MAX_RETRANS, + PENDING_PER_NEIGHBOUR, 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), + /// Resolution failed; its hold-down is the entry's deadline. + 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, + pub pending: Pending, +} + +impl Incomplete { + pub fn requests(&self) -> u8 { + self.requests + } +} + +#[derive(Debug)] +pub struct Reachable { + mac: MacAddr, + confirmed: Instant, + pub released: Released, +} + +impl Reachable { + pub fn mac(&self) -> MacAddr { + self.mac + } + + pub fn confirmed(&self) -> Instant { + self.confirmed + } +} + +/// 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 { + 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, + pub released: Released, +} + +impl Unreachable { + /// No request pending: nothing is sent until the next datagram. + pub fn quiescent(&self) -> bool { + !self.backoff + } +} + +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(_)) + } + + 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 { + match self { + Self::Reachable(Reachable { released, .. }) + | Self::Stale(Linked { released, .. }) + | Self::Delay(Linked { released, .. }) + | Self::Probe(Probing { released, .. }) + | Self::Unreachable(Unreachable { released, .. }) => !released.0.is_empty(), + Self::Incomplete(_) | Self::Failed => false, + } + } + + /// 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, + 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`; 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 }, 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. +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, 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 = |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 n.state.releasing() { 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 Some(victim) = victim else { return false }; + remove(i, cx, victim); + true +} + +/// 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.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); + } +} + +fn insert(i: &mut Interface, addr: Ipv4Addr, state: Nud, now: Instant, hint: Option) { + 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 +/// 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); + } + insert(i, addr, Nud::Incomplete(Incomplete { requests: 0, pending: Pending::default() }), now, hint); + request(i, cx, addr); + 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, released }) => { + let mac = *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) + } + Nud::Unreachable(u) => { + let mac = u.mac; + if u.backoff { + u.sent = true; + } else if !queued { + request(i, cx, addr); + } + 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.push(held).is_some() { + i.held = i.held.saturating_sub(1); + cx.log.count(Counter::NbPendingOverflow); + } + 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); + let releasing = n.state.releasing(); + match &mut n.state { + 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; + fail(i, cx, addr, pending); + } + 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, 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, released: core::mem::take(&mut s.released) }); + probe(i, cx, addr, spaced); + } + Nud::Probe(s) if s.requests < UNICAST_SOLICIT => request(i, cx, addr), + Nud::Probe(s) => { + 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), + 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: `leave` deletes it once they have left. + Nud::Stale(_) | Nud::Unreachable(_) if releasing => {} + Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => { + remove(i, cx, addr); + route::refresh_active(i, cx); + } + } +} + +/// 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)?; + let mac = n.state.mac()?; + let mut held = n.state.released_mut()?.pop()?; + if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() { + *destination = mac.0; + } + 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. +fn probe(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, at: Instant) { + if at <= cx.now { + request(i, cx, addr); + } else { + cx.timers.arm(timer(cx, addr), at); + } +} + +/// A held datagram that will never leave: counted, and its sender told (§6.5, §9.6). +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)); + } +} + +/// INCOMPLETE gave up: every held datagram is dropped. +fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: Pending) { + let now = cx.now; + for held in pending.0 { + i.held = i.held.saturating_sub(1); + drop_held(cx, held, Counter::NbPendingDropped); + } + 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); +} + +/// 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); + } + 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 })); + } + 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) { + 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 released = inherit(cx, addr, n, mac); + n.state = Nud::Reachable(Reachable { mac, confirmed: now, released }); + cx.timers.arm(timer(cx, addr), now.after(reachable)); + 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 released = inherit(cx, addr, n, mac); + n.state = Nud::Stale(Linked { mac, released }); + cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME)); + 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; + insert(i, addr, Nud::Stale(Linked { mac, released: Released::default() }), now, 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() { + let released = state.take_released(); + *state = Nud::Probe(Probing { mac, requests: 0, released }); + probe(i, cx, addr, spaced); + } + } + _ => {} + } +} + +/// 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)); + 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); + } + } + cx.control.purge(cx.iface); + 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..f260510c661 --- /dev/null +++ b/toyos-net-ip/src/route.rs @@ -0,0 +1,173 @@ +//! 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::addr::{is_host, is_martian, Cidr}; +use toyos_net_wire::ipv4::MulticastAddr; +use toyos_net_wire::Instant; + +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 }); + } + if is_martian(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, 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 { + 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/acd.rs b/toyos-net-ip/tests/acd.rs new file mode 100644 index 00000000000..c5d4b72c470 --- /dev/null +++ b/toyos-net-ip/tests/acd.rs @@ -0,0 +1,322 @@ +//! §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_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 { + 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() { + // 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))); + 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) <= one); + for pair in probes.windows(2) { + assert!((one..=two).contains(&pair[1].since(pair[0]))); + } + 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 >= 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)); +} + +#[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(Duration::from_millis(60_000)))); +} + +#[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); +} + +#[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/addr.rs b/toyos-net-ip/tests/addr.rs new file mode 100644 index 00000000000..b36dc0fecd8 --- /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..a464762d381 --- /dev/null +++ b/toyos-net-ip/tests/clk.rs @@ -0,0 +1,231 @@ +//! §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. +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(); + 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)), + 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..6ac7f82f7ba --- /dev/null +++ b/toyos-net-ip/tests/common/mod.rs @@ -0,0 +1,803 @@ +//! 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 [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]])); + 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: std::collections::BTreeMap::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 { + 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. + 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: std::collections::BTreeMap::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, + } +} + +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..a0ef0834caf --- /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..869c64a2d9a --- /dev/null +++ b/toyos-net-ip/tests/igmp.rs @@ -0,0 +1,563 @@ +//! §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::{limits, Counter, Event, 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]); + + 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] +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(_)))); + + 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 new file mode 100644 index 00000000000..bcbbcafcf77 --- /dev/null +++ b/toyos-net-ip/tests/io.rs @@ -0,0 +1,579 @@ +//! §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, Nud, 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); +} + +/// 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_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); + 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); + + 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] +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!(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); +} + +#[test] +fn s_ip_out_008_released_datagrams_wait_at_the_head_of_their_queue() { + let mut h = H::fixture_i(); + for data in [b"1", b"2"] { + assert_eq!(h.send(A, B, 5001, 5001, data), Ok(None)); + } + for n in 1..limits::CONTROL_QUEUE as u8 { + let _ = h.ip.resolve(h.clock(), h.if0, ip4(192, 0, 2, 100 + n)); + } + h.frame(&hex(V_ARP_REPLY)); + 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"); + + // 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)); + + // 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/nbr.rs b/toyos-net-ip/tests/nbr.rs new file mode 100644 index 00000000000..1ad12f795d2 --- /dev/null +++ b/toyos-net-ip/tests/nbr.rs @@ -0,0 +1,261 @@ +//! §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 }); + + // 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] +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))); +} diff --git a/toyos-net-ip/tests/nud.rs b/toyos-net-ip/tests/nud.rs new file mode 100644 index 00000000000..96760766551 --- /dev/null +++ b/toyos-net-ip/tests/nud.rs @@ -0,0 +1,723 @@ +//! §6: the reachability machine. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_ip::{limits, Advice, Counter, Event, Flow, IfIndex, 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.pending.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_eq!(h.count(Counter::NbPendingDropped), 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))]); + + // 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"); + 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); + 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(_)))); +} + +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, WIDE_A, 16, 0); + h.settle(); + h +} + +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(); + 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_in_order() { + let mut h = wide(); + let (failed, unreachable, stale) = (neighbour(0), neighbour(1), neighbour(2)); + let _ = h.ip.resolve(h.clock(), h.if0, failed); + 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))); + 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 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(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, WIDE_A))); + 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); + 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] +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"); + } +} + +/// 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)); + } + 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); + let flow = Flow { source: WIDE_A, source_port: Port::new(5001).unwrap(), destination: b, destination_port: Port::new(5001).unwrap() }; + // 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 = b_in_a_full_table(others_reachable); + for n in 1..=limits::CONTROL_QUEUE as u32 { + ask_wide(&mut h, neighbour(n)); + } + announce_wide(&mut h, 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(_)))); + 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: 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)); + 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 { + // 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)); + } + 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); + } + } + + // 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_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)); + } + 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] +fn s_ip_nud_030_released_datagrams_keep_their_entry() { + // 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"); + } + + // 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, 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)); + } + 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_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"); + 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)); +} 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..1889ae7ffb8 100644 --- a/toyos-net-tcp/src/counters.rs +++ b/toyos-net-tcp/src/counters.rs @@ -4,60 +4,10 @@ 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}; -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 +68,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 { @@ -188,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 8c560599c60..f5861ac4912 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 counters::{Counter, Counters, Refusal, RefusalLog}; pub use seq::Seq; -pub use siphash::{siphash24, Key}; pub use stack::{ConnId, Info, ListenerId, Outgoing, Tcp}; +pub use toyos_net_wire::siphash::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)] @@ -246,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/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-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-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()); 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/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-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/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..17287645442 --- /dev/null +++ b/toyos-net-udp/src/lib.rs @@ -0,0 +1,705 @@ +//! 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. 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. +//! +//! **Refusals are values.** Every refusal is a named [`Counter`] and the [`Error`] the call +//! 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)] +#![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::addr::is_martian; +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"; + TxDiscardedOnClose = "udp.tx-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; + /// 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; + /// 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); + /// Refusals 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(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, +} + +/// 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, + /// 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, +} + +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_refusals(&mut self) -> alloc::vec::Drain<'_, Refusal> { + self.refusals.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.refusals.len() >= limits::EVENTS { + self.count(Counter::EventOverflow); + } else { + self.refusals.push(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 is_martian(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 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, + 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 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 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 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.turns.push_back(Turn::Socket(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, 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) }; + 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); + 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)); + 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(()) + } + + /// 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: 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 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) + } + }; + 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..f9800081134 --- /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_refusals().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..a92d917a15e --- /dev/null +++ b/toyos-net-udp/tests/recv.rs @@ -0,0 +1,533 @@ +//! §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_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()); +} + +#[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)); + 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(); + 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); + + 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(); + 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)); +} + +#[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[..])); + } + + // 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"); + + // 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(); + for _ in 0..2 { + u.udp.send_to(&mut u.ip, x, DNS, 53, b"x").unwrap(); + } + u.udp.close(x).unwrap(); + let y = u.bind(ANY, 50_002).unwrap(); + let z = u.bind(ANY, 50_003).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(); + assert_eq!(order(&mut u), "yzwyzw"); +} + +#[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..96dacd48248 --- /dev/null +++ b/toyos-net-udp/tests/send.rs @@ -0,0 +1,310 @@ +//! §U3 and §U4: connecting and sending. + +mod common; + +use std::net::Ipv4Addr; + +use common::*; +use toyos_net_udp::{limits, Binding, Counter, Error, 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_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] +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_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(); + } + 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(); + 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(); + 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); + + // 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(); + 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(); + 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]); +} 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..bee580bbc3f --- /dev/null +++ b/toyos-net-wire/src/addr.rs @@ -0,0 +1,74 @@ +//! IPv4 address classes and prefixes (RFC 1122 §3.2.1.3, RFC 6890, RFC 4632, RFC 3021). + +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/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 c42cd96f540..3f38ca174e3 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, IPv4 +//! address classes and prefixes, and the declaration of a crate's counters. #![no_std] #![forbid(unsafe_code)] @@ -48,6 +51,98 @@ 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; +/// `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:expr $(, $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))) + } + } + + #[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 + }; + (@logged) => { + false + }; +} + +pub mod addr; pub mod arp; pub mod checksum; mod emit; @@ -55,11 +150,41 @@ 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; +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); + +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]) 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); +}