Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/appliance-pid1/src/dhcp.rs

//! **The PID-1 DHCPv4 client**: how the appliance gets an address when the
//! declaration says `<NET>=eth0:dhcp` instead of baking one in.
//!
//! One handshake, four datagrams, RFC 2131: DISCOVER → OFFER → REQUEST → ACK.
//! The lease that comes back is handed to [`crate::net::apply`] — the **same**
//! netlink writer the static path uses, so there is exactly one piece of code
//! that puts an address on an interface (one implementation), and this file never touches
//! `AF_NETLINK` at all.
//!
//! # Why the messages are hand-rolled rather than a crate
//!
//! The obvious crate is `dhcproto` (MIT, maintained, checked 2026-08-12:
//! 0.15.0). Its mandatory dependencies are `hickory-proto` — a complete DNS
//! protocol stack — plus `rand`, `ipnet`, `thiserror` and a proc-macro crate,
//! none of which the appliance graph carries today, all for the two fixed-shape
//! packets and one option walk below. That is the same trade `pid1.rs` already
//! settled for netlink ("a binding crate would be a dependency in the appliance
//! graph for eighteen integers"), and it lands the same way here. Nothing else
//! in the nordisk suite parses DHCP (grepped edda, draupnir, tunnr,
//! korp-collectors, 2026-08-12), so there is no in-house parser to reuse either.
//!
//! # The socket shape, and why it is not a raw socket
//!
//! A UDP socket bound to `0.0.0.0:68` with `SO_BROADCAST` and
//! `SO_BINDTODEVICE`, sending to `255.255.255.255:67`. `SO_BINDTODEVICE` is
//! what makes the limited broadcast routable while the interface has **no
//! address**: the kernel special-cases `255.255.255.255` with an explicit
//! output device and skips the FIB lookup that would otherwise return
//! `ENETUNREACH`. The DISCOVER/REQUEST carry the BROADCAST flag (RFC 2131
//! §4.1), so a compliant server broadcasts its replies too and the kernel
//! delivers them to this socket even though `10.0.2.15` is not yet a local
//! address. No `AF_PACKET`, no checksum arithmetic, no capability beyond what
//! PID 1 already has.
//!
//! `rustix` (the rest of pid1's syscall layer) has no `SO_BINDTODEVICE` helper
//! as of 1.1.4, so that one setsockopt comes from `socket2` — already resolved
//! in this tree under tokio/hyper, so the edge adds no new crate to the graph.
//!
//! # The resolver, and why a DHCP client writes a file
//!
//! The ACK carries option 6, the recursive resolvers, and until 2026-09-08 this
//! parser walked straight past it. The bill came in as
//! `job mirror-1 FAILED: … could not be reached for the advertisement: Dns` on
//! gunnar.rs: an appliance with an address, a route and no way to spell
//! `github.com`. The lease now carries [`Lease::dns`], and
//! [`crate::resolver`] puts it where libc looks — `/etc/resolv.conf` on the RAM
//! root, a directory this file's caller creates because korp-installer's
//! initramfs lays only `dev`, `proc`, `sys` and `init`.
//!
//! Writing a file feels like a layering violation in a module that has been
//! proud of touching no `AF_NETLINK`. It is the opposite. Every dialler in this
//! server — `ureq` in `mirror_http`, the russh dialler in `mirror_fetch`, the
//! billing client, the webhook sender — bottoms out in `getaddrinfo`, so libc
//! **is** the one resolver implementation on this box (one implementation). An in-process
//! resolver would be a second one, and it would only take effect at the dial
//! sites rewritten to use it: every one of them, forever, including the ones a
//! dependency owns. One 60-byte file makes all of them work with no new code
//! path at all. [`crate::resolver`]'s module doc argues that trade in full.
//!
//! # Renewal
//!
//! The lease is kept alive per RFC 2131 §4.4.5. The timers and transitions —
//! BOUND → RENEWING at T1 (unicast REQUEST to the lease's server) → REBINDING
//! at T2 (broadcast REQUEST) → re-DISCOVER at expiry, NAK anywhere ⇒ immediate
//! re-DISCOVER — live in [`renew`], a state machine that is pure over injected
//! time and an injected transport, so every transition is unit-tested without a
//! network or a sleep. This file stays what it was: packet builders, the
//! parser, and one bounded socket op per leg ([`WireTransport`]). The driver on
//! the tokio clock is `crate::net::dhcp_renewal`, and a renewed lease that *changes*
//! the address goes back through `crate::net::apply` — still the one netlink writer.
//! The single deliberate RFC deviation (what happens when the lease expires and
//! nobody answers) is documented where it lives, in [`renew`]'s module doc.

use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use std::time::{Duration, Instant};

use anyhow::{bail, Context, Result};

// ── the wire constants (RFC 2131 / RFC 2132) ────────────────────────────────

const BOOTREQUEST: u8 = 1;
const BOOTREPLY: u8 = 2;
const HTYPE_ETHER: u8 = 1;
/// `flags` bit 15: "broadcast the reply, I cannot receive unicast yet".
const FLAG_BROADCAST: u16 = 0x8000;
/// 99.130.83.99, the four bytes that make a BOOTP packet a DHCP packet.
const MAGIC_COOKIE: [u8; 4] = [99, 130, 83, 99];
/// Options start here: 236 fixed BOOTP bytes + the cookie.
const OPTIONS_AT: usize = 240;
/// RFC 951 mandates a minimum packet; ancient relays drop shorter ones.
const MIN_PACKET: usize = 300;

const OPT_PAD: u8 = 0;
const OPT_SUBNET_MASK: u8 = 1;
const OPT_ROUTER: u8 = 3;
/// RFC 2132 §3.8: the recursive resolvers, in the server's order of
/// preference. Four bytes each, at least one. This is the option that decides
/// whether the appliance can spell a hostname at all — see [`Lease::dns`].
const OPT_DOMAIN_NAME_SERVERS: u8 = 6;
/// RFC 2132 §3.14: the name the server wants this host to carry.
const OPT_HOST_NAME: u8 = 12;
const OPT_REQUESTED_IP: u8 = 50;
const OPT_LEASE_TIME: u8 = 51;
const OPT_MESSAGE_TYPE: u8 = 53;
const OPT_SERVER_ID: u8 = 54;
const OPT_PARAM_REQUEST: u8 = 55;
const OPT_MESSAGE: u8 = 56;
const OPT_RENEWAL_T1: u8 = 58;
const OPT_REBINDING_T2: u8 = 59;
const OPT_CLIENT_ID: u8 = 61;
/// **Option 121, classless static routes (RFC 3442).** The option that tells a
/// client about a subnet its own prefix does not cover. See [`Lease::routes`].
const OPT_CLASSLESS_STATIC_ROUTES: u8 = 121;
const OPT_END: u8 = 255;

const DHCPDISCOVER: u8 = 1;
const DHCPOFFER: u8 = 2;
const DHCPREQUEST: u8 = 3;
const DHCPACK: u8 = 5;
const DHCPNAK: u8 = 6;

const SERVER_PORT: u16 = 67;
const CLIENT_PORT: u16 = 68;

/// Total worst-case wall time before the client gives up and the boot goes on
/// (unreachable, loudly — see `crate::net::bring_up_network`). Four attempts of up to
/// 3 s each: bounded, so a cable that goes nowhere costs ~12 s, not a hang.
const ATTEMPTS: u32 = 4;
const ATTEMPT_TIMEOUT: Duration = Duration::from_secs(3);

// ── what a successful handshake yields ──────────────────────────────────────

/// The applied result of one DISCOVER→ACK exchange.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lease {
    /// `yiaddr` from the ACK.
    pub address: Ipv4Addr,
    /// From option 1 (subnet mask), converted; the ACK is refused without it.
    pub prefix: u8,
    /// First address of option 3 (router), if the server named one.
    pub gateway: Option<Ipv4Addr>,
    /// **Option 121, the classless static routes — why the appliance could not
    /// tell the time.**
    ///
    /// Option 3 names ONE gateway and option 1 names the interface's own
    /// prefix. Between them they describe a flat network, and a cloud's private
    /// network is not flat. MEASURED on gunnar.rs, 2026-09-20: the appliance's
    /// utility NIC leases `10.13.8.99/22` and the front's leases
    /// `10.13.7.210/22` — two DIFFERENT /22s, reachable from each other only
    /// through `10.0.0.0/8 via 10.13.4.1`, which UpCloud sends as option 121
    /// and which this client read and discarded.
    ///
    /// The consequence was total and silent in one direction only. The front
    /// (Ubuntu, systemd-networkd, which honours 121) reached the appliance's
    /// gRPC port perfectly, so the network looked fine. The appliance's own
    /// packets to `10.13.7.210` matched no route but the default one and left
    /// through the PUBLIC interface, where a private destination is dropped.
    /// So: the appliance never asked the front's `gunnar-clock` for signed time
    /// and sat +7 198 668 ms out (a two-hour boot skew this estate has measured
    /// before), which made every credential `7198751 ms away from this server's
    /// clock; the window is ±300000 ms` and stopped the bring-up dead at
    /// `pair`; and `crate::net::ship_boot_narration` shipped its datagrams into the
    /// same hole, so the boot-narration sink — built that very morning to say
    /// whether an install had stalled — recorded `NOTHING HAS EVER ARRIVED`.
    ///
    /// Empty is not an error, for the same reason an empty `dns` is not: a
    /// server that sends no 121 has said something true about a flat network.
    pub routes: Vec<Route>,
    /// Option 54, the server that granted the lease.
    pub server: Ipv4Addr,
    /// Option 51, seconds. The ACK is refused without it: a lease with no
    /// duration is not a lease, and inventing "infinite" here would be exactly
    /// the fabricated value this client exists to never produce.
    pub lease_secs: u32,
    /// Option 58 (T1), seconds, if the server sent one. When absent the RFC
    /// default applies: 0.5 · lease ([`renew::Machine`] computes it).
    pub t1_secs: Option<u32>,
    /// Option 59 (T2), seconds, if the server sent one. Default 0.875 · lease.
    pub t2_secs: Option<u32>,
    /// Option 12 (host name), if the server offered one. Not required: a
    /// lease with no name is still a lease; the box then names itself
    /// (`crate::net::hostname_for`).
    pub host_name: Option<String>,
    /// Option 6 (domain name servers), in the server's order — empty when the
    /// ACK named none.
    ///
    /// **This field is why `mirror sync` could not reach github.** Measured on
    /// gunnar.rs 2026-09-08, straight after the 1.4.0 re-image: `job mirror-1
    /// FAILED: https://github.com/git/git.git/info/refs?service=git-upload-pack
    /// could not be reached for the advertisement: Dns`. The lease was good,
    /// the route was good, and the box could not spell a name — because this
    /// client read options 1, 3, 51, 54, 58 and 59 out of the ACK and dropped
    /// option 6 on the floor. Nothing under this binary writes
    /// `/etc/resolv.conf` (there is no distro and no `/etc` at all on the RAM
    /// root), so musl's `getaddrinfo` — which every dialler in this server
    /// ends up in, `ureq` in `mirror_http`, the russh dialler in
    /// `mirror_fetch`, the billing client and the webhook sender — had no
    /// server to ask.
    ///
    /// A `Vec` and not an `Option<Vec>`: "the server named none" and "the
    /// server named an empty list" are the same fact and `is_empty()` says it
    /// once. Ordering is load-bearing — RFC 2132 §3.8 lists them "in order of
    /// preference" and `resolv.conf` reads them in file order, so the wire
    /// order is preserved verbatim by [`crate::resolver::render`].
    pub dns: Vec<Ipv4Addr>,
}

// ── pure builders ────────────────────────────────────────────────────────────
//
// `Vec<u8>` in, no socket touched, same doctrine as pid1's netlink encoders: a
// DHCP bug is a byte-layout bug, and the only honest assertion is against the
// bytes. The tests below hold offsets from RFC 2131 §2's figure, not from this
// code.

/// The 236-byte BOOTP header + magic cookie shared by every request.
///
/// `flags`/`ciaddr` are what tell the acquisition shape from the renewal
/// shape: DISCOVER/SELECTING carry the BROADCAST flag and a zero `ciaddr`
/// (no address to unicast a reply to yet); RENEWING/REBINDING carry a zero
/// flag and the held address in `ciaddr` (RFC 2131 §4.3.2, §4.4.5).
fn bootp_header(xid: u32, secs: u16, mac: [u8; 6], flags: u16, ciaddr: Ipv4Addr) -> Vec<u8> {
    let mut p = Vec::with_capacity(MIN_PACKET);
    p.push(BOOTREQUEST);
    p.push(HTYPE_ETHER);
    p.push(6); // hlen
    p.push(0); // hops
    p.extend_from_slice(&xid.to_be_bytes());
    p.extend_from_slice(&secs.to_be_bytes());
    p.extend_from_slice(&flags.to_be_bytes());
    p.extend_from_slice(&ciaddr.octets());
    p.extend_from_slice(&[0u8; 12]); // yiaddr, siaddr, giaddr: all zero
    p.extend_from_slice(&mac);
    p.extend_from_slice(&[0u8; 10]); // chaddr padding to 16
    p.extend_from_slice(&[0u8; 64]); // sname
    p.extend_from_slice(&[0u8; 128]); // file
    p.extend_from_slice(&MAGIC_COOKIE);
    p
}

fn option(p: &mut Vec<u8>, code: u8, payload: &[u8]) {
    p.push(code);
    p.push(payload.len() as u8);
    p.extend_from_slice(payload);
}

/// Options every request carries: who we are and what we want to be told.
///
/// The parameter request list asks for exactly what [`Lease`] holds: mask,
/// router, DNS, lease time, server id, T1, T2. Asking for less makes servers
/// omit the mask; asking for more is cargo cult.
///
/// Option 6 is in the list because a server is entitled to send only what was
/// asked for (RFC 2131 §3.5: the client "SHOULD" list the parameters it wants,
/// and ISC dhcpd's `request` statement is exactly this list). Most servers
/// volunteer option 6 anyway — which is why the gap was invisible until a box
/// with a resolver-less lease had to spell `github.com`.
fn common_options(p: &mut Vec<u8>, mac: [u8; 6]) {
    let mut client_id = [0u8; 7];
    client_id[0] = HTYPE_ETHER;
    client_id[1..].copy_from_slice(&mac);
    option(p, OPT_CLIENT_ID, &client_id);
    option(
        p,
        OPT_PARAM_REQUEST,
        &[
            OPT_SUBNET_MASK,
            OPT_ROUTER,
            OPT_DOMAIN_NAME_SERVERS,
            OPT_LEASE_TIME,
            OPT_SERVER_ID,
            OPT_RENEWAL_T1,
            OPT_HOST_NAME,
            OPT_REBINDING_T2,
            // Asked for by name, because a server is free to withhold an option
            // nobody requested — the same reason option 6 is on this list.
            OPT_CLASSLESS_STATIC_ROUTES,
        ],
    );
}

fn finish(mut p: Vec<u8>) -> Vec<u8> {
    p.push(OPT_END);
    if p.len() < MIN_PACKET {
        p.resize(MIN_PACKET, 0);
    }
    p
}

/// DHCPDISCOVER: "anyone out there?"
pub fn discover_message(xid: u32, secs: u16, mac: [u8; 6]) -> Vec<u8> {
    let mut p = bootp_header(xid, secs, mac, FLAG_BROADCAST, Ipv4Addr::UNSPECIFIED);
    option(&mut p, OPT_MESSAGE_TYPE, &[DHCPDISCOVER]);
    common_options(&mut p, mac);
    finish(p)
}

/// DHCPREQUEST in the SELECTING state: "I take the offer from *that* server."
///
/// Options 50 (requested address) and 54 (server id) are what make it
/// selecting; RFC 2131 §4.3.2 says both MUST be present here, and MUST both be
/// absent from the renewal shape below — the two REQUESTs are different
/// sentences spelled with the same message type.
pub fn request_message(
    xid: u32,
    secs: u16,
    mac: [u8; 6],
    requested: Ipv4Addr,
    server: Ipv4Addr,
) -> Vec<u8> {
    let mut p = bootp_header(xid, secs, mac, FLAG_BROADCAST, Ipv4Addr::UNSPECIFIED);
    option(&mut p, OPT_MESSAGE_TYPE, &[DHCPREQUEST]);
    option(&mut p, OPT_REQUESTED_IP, &requested.octets());
    option(&mut p, OPT_SERVER_ID, &server.octets());
    common_options(&mut p, mac);
    finish(p)
}

/// DHCPREQUEST in the RENEWING/REBINDING states: "I still hold this address."
///
/// RFC 2131 §4.3.2: `ciaddr` MUST carry the client's address, options 50 and
/// 54 MUST NOT appear, and the BROADCAST flag is off — the client has a
/// working address to receive a unicast ACK on. RENEWING and REBINDING send
/// the *same* packet; where it is sent (unicast to the lease's server vs.
/// broadcast) is the whole difference, and that is the caller's line.
pub fn renew_message(xid: u32, secs: u16, mac: [u8; 6], ciaddr: Ipv4Addr) -> Vec<u8> {
    let mut p = bootp_header(xid, secs, mac, 0, ciaddr);
    option(&mut p, OPT_MESSAGE_TYPE, &[DHCPREQUEST]);
    common_options(&mut p, mac);
    finish(p)
}

// ── pure parser ──────────────────────────────────────────────────────────────

/// A server reply that passed the "is this for us" gate: right direction,
/// right xid, right hardware address, right cookie.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
    /// Option 53: [`DHCPOFFER`], [`DHCPACK`] or [`DHCPNAK`].
    pub kind: u8,
    /// `yiaddr` — the address being offered or acknowledged.
    pub yiaddr: Ipv4Addr,
    pub subnet_mask: Option<Ipv4Addr>,
    pub router: Option<Ipv4Addr>,
    /// Option 6, the resolvers, in wire order. Empty = the ACK named none.
    pub dns: Vec<Ipv4Addr>,
    /// Option 121, in wire order: `(destination, prefix, gateway)`.
    pub routes: Vec<Route>,
    pub server_id: Option<Ipv4Addr>,
    pub lease_secs: Option<u32>,
    pub t1_secs: Option<u32>,
    pub t2_secs: Option<u32>,
    /// Option 56, a server's human-readable reason on a NAK.
    pub message: Option<String>,
    /// Option 12, the host name the server assigns, trimmed; empty = absent.
    pub host_name: Option<String>,
}

/// **One route out of option 121** — a destination prefix and the next hop that
/// reaches it. See [`Lease::routes`] for why this estate cannot work without it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Route {
    /// The destination network, already masked to `prefix` bits.
    pub destination: Ipv4Addr,
    /// The prefix length, 0..=32. `0` is the default route.
    pub prefix: u8,
    /// The next hop. `0.0.0.0` means "on-link" (RFC 3442 allows it).
    pub gateway: Ipv4Addr,
}

impl Route {
    /// A default route — `0.0.0.0/0`. Only the interface that OWNS the default
    /// route may install one, which is `pid1`'s decision and not this module's.
    pub fn is_default(&self) -> bool {
        self.prefix == 0
    }
}

/// **Option 121's payload, decoded (RFC 3442).**
///
/// The encoding is deliberately compact and is the one part of DHCP that is
/// easy to get wrong: each entry is ONE prefix-length byte followed by only as
/// many destination bytes as that length needs — `ceil(prefix / 8)` of them,
/// so `0.0.0.0/0` contributes zero bytes and `10.0.0.0/8` exactly one — and
/// then four bytes of gateway. A decoder that assumes four destination bytes
/// reads the gateway as part of the network and every route after it is
/// garbage.
///
/// A malformed tail is dropped and the entries already read are kept: a server
/// that sent three good routes and a truncated fourth has still said three true
/// things, and refusing the lease over the fourth would leave the box with no
/// address at all.
fn opt_classless_routes(payload: &[u8]) -> Vec<Route> {
    let mut out = Vec::new();
    let mut at = 0usize;
    while at < payload.len() {
        let prefix = payload[at];
        if prefix > 32 {
            break; // not a prefix length; the rest cannot be trusted either
        }
        let dst_bytes = usize::from(prefix).div_ceil(8);
        let Some(rest) = payload.get(at + 1..at + 1 + dst_bytes + 4) else {
            break; // truncated entry — keep what was whole
        };
        let mut dst = [0u8; 4];
        dst[..dst_bytes].copy_from_slice(&rest[..dst_bytes]);
        let gw = &rest[dst_bytes..];
        // Masked here, once, so nothing downstream has to wonder whether the
        // host bits a server left in are meaningful. They are not.
        let mask = u32::MAX.checked_shl(32 - u32::from(prefix)).unwrap_or(0);
        out.push(Route {
            destination: Ipv4Addr::from(u32::from_be_bytes(dst) & mask),
            prefix,
            gateway: Ipv4Addr::new(gw[0], gw[1], gw[2], gw[3]),
        });
        at += 1 + dst_bytes + 4;
    }
    out
}

fn opt_ipv4(payload: &[u8]) -> Option<Ipv4Addr> {
    payload
        .get(..4)
        .map(|b| Ipv4Addr::new(b[0], b[1], b[2], b[3]))
}

/// Option 6's payload: a run of four-byte addresses (RFC 2132 §3.8).
///
/// **A length that is not a multiple of four is TRUNCATED, not refused**, and
/// the reason is a grading, not a shrug. `lease_from_ack` refuses an ACK with
/// no mask or no lease time because those are what an address *means* — a
/// guessed prefix is a silent unreachability. A resolver is not that: an
/// appliance with an address and no name service still serves git over `IP:port`,
/// which is how `gunnar-cli` talks to it anyway — no hostnames in `Endpoint`,
/// `IP:port` only. Throwing the whole lease away
/// over a server that padded option 6 wrong would cost the box its *address* to
/// punish a bad *hint*. So the complete addresses are kept in order and the
/// trailing partial one — which cannot be completed without inventing bytes,
/// measured, not assumed — is dropped.
///
/// `0.0.0.0` is dropped for the same reason `lease_from_ack` refuses a
/// `0.0.0.0` yiaddr: it is not an address. Servers pad short lists with it, and
/// a `nameserver 0.0.0.0` line is a lookup musl will actually attempt and
/// actually spend its timeout on.
fn opt_ipv4_list(payload: &[u8]) -> Vec<Ipv4Addr> {
    payload
        .chunks_exact(4)
        .map(|b| Ipv4Addr::new(b[0], b[1], b[2], b[3]))
        .filter(|a| !a.is_unspecified())
        .collect()
}

fn opt_u32(payload: &[u8]) -> Option<u32> {
    payload
        .get(..4)
        .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}

/// Parse one datagram from `0.0.0.0:68`'s receive queue.
///
/// `Err` means "not the reply to *our* request" as well as "malformed": the
/// caller's loop treats both the same way — keep listening until the deadline —
/// because port 68 is a party line and someone else's OFFER is not an error in
/// the world, only in this packet.
pub fn parse_reply(buf: &[u8], xid: u32, mac: [u8; 6]) -> Result<Reply> {
    if buf.len() < OPTIONS_AT {
        bail!("{} bytes is shorter than a BOOTP header", buf.len());
    }
    if buf[0] != BOOTREPLY {
        bail!("op {} is not BOOTREPLY", buf[0]);
    }
    if buf[1] != HTYPE_ETHER || buf[2] != 6 {
        bail!("htype/hlen {}/{} is not ethernet", buf[1], buf[2]);
    }
    let got_xid = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
    if got_xid != xid {
        bail!("xid {got_xid:#010x} is not ours ({xid:#010x})");
    }
    if buf[28..34] != mac {
        bail!("chaddr is not this interface's MAC; a reply to somebody else");
    }
    if buf[236..240] != MAGIC_COOKIE {
        bail!("no DHCP magic cookie; a plain BOOTP reply");
    }

    let yiaddr = Ipv4Addr::new(buf[16], buf[17], buf[18], buf[19]);
    let mut reply = Reply {
        kind: 0,
        yiaddr,
        subnet_mask: None,
        router: None,
        dns: Vec::new(),
        routes: Vec::new(),
        server_id: None,
        lease_secs: None,
        t1_secs: None,
        t2_secs: None,
        message: None,
        host_name: None,
    };

    let mut at = OPTIONS_AT;
    while at < buf.len() {
        let code = buf[at];
        match code {
            OPT_PAD => {
                at += 1;
                continue;
            }
            OPT_END => break,
            _ => {}
        }
        let len =
            *buf.get(at + 1)
                .with_context(|| format!("option {code} has no length byte"))? as usize;
        let payload = buf
            .get(at + 2..at + 2 + len)
            .with_context(|| format!("option {code} claims {len} bytes past the packet end"))?;
        match code {
            OPT_MESSAGE_TYPE => reply.kind = *payload.first().unwrap_or(&0),
            OPT_SUBNET_MASK => reply.subnet_mask = opt_ipv4(payload),
            OPT_ROUTER => reply.router = opt_ipv4(payload),
            OPT_DOMAIN_NAME_SERVERS => reply.dns = opt_ipv4_list(payload),
            OPT_CLASSLESS_STATIC_ROUTES => reply.routes = opt_classless_routes(payload),
            OPT_SERVER_ID => reply.server_id = opt_ipv4(payload),
            OPT_LEASE_TIME => reply.lease_secs = opt_u32(payload),
            OPT_RENEWAL_T1 => reply.t1_secs = opt_u32(payload),
            OPT_REBINDING_T2 => reply.t2_secs = opt_u32(payload),
            OPT_MESSAGE => reply.message = Some(String::from_utf8_lossy(payload).into_owned()),
            OPT_HOST_NAME => {
                let name = String::from_utf8_lossy(payload)
                    .trim_end_matches('\0')
                    .trim()
                    .to_owned();
                reply.host_name = (!name.is_empty()).then_some(name);
            }
            _ => {}
        }
        at += 2 + len;
    }
    if reply.kind == 0 {
        bail!("no message-type option; not a DHCP reply");
    }
    Ok(reply)
}

/// `255.255.255.0` → 24. Refuses a non-contiguous mask rather than rounding it:
/// an address applied under the wrong prefix is a subtle unreachability, which
/// is worse than the loud kind.
pub fn prefix_from_mask(mask: Ipv4Addr) -> Result<u8> {
    let m = u32::from(mask);
    let prefix = m.leading_ones() as u8;
    if m != prefix_bits(prefix) {
        bail!("subnet mask {mask} is not contiguous; refusing to guess a prefix");
    }
    Ok(prefix)
}

const fn prefix_bits(prefix: u8) -> u32 {
    if prefix == 0 {
        0
    } else {
        u32::MAX << (32 - prefix as u32)
    }
}

// ── the client ───────────────────────────────────────────────────────────────

/// A transaction id from the entropy std already has, not a `rand` dependency:
/// `RandomState` seeds from the OS per process. Uniqueness only has to hold
/// among concurrent clients on one broadcast domain.
fn fresh_xid() -> u32 {
    use std::hash::{BuildHasher, Hasher};
    let mut h = std::collections::hash_map::RandomState::new().build_hasher();
    h.write_u32(std::process::id());
    h.finish() as u32
}

/// The socket described in the module doc: UDP `0.0.0.0:68`, broadcast on,
/// bound to `interface` so the limited broadcast has an output device before
/// the interface has an address.
fn client_socket(interface: &str) -> Result<UdpSocket> {
    use socket2::{Domain, Protocol, Socket, Type};
    let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))
        .context("opening the DHCP client socket")?;
    sock.set_broadcast(true).context("SO_BROADCAST")?;
    sock.bind_device(Some(interface.as_bytes()))
        .with_context(|| format!("SO_BINDTODEVICE {interface}"))?;
    sock.bind(&SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, CLIENT_PORT).into())
        .context("binding 0.0.0.0:68")?;
    Ok(sock.into())
}

/// **The IPv4 + UDP frame of a client message**: 0.0.0.0:68 → 255.255.255.255:67, the
/// header checksum computed, the UDP checksum 0 (optional over IPv4). What
/// [`send_from_zero`] puts on the wire, so the source is 0.0.0.0 whatever else on the box
/// holds an address.
pub(crate) fn zero_source_frame(payload: &[u8]) -> Vec<u8> {
    let udp_len = 8 + payload.len();
    let total = 20 + udp_len;
    let mut f = Vec::with_capacity(total);
    f.extend_from_slice(&[0x45, 0x10]);
    f.extend_from_slice(&(total as u16).to_be_bytes());
    f.extend_from_slice(&[0, 0, 0, 0, 64, 17, 0, 0]);
    f.extend_from_slice(&[0, 0, 0, 0]);
    f.extend_from_slice(&[255, 255, 255, 255]);
    let sum: u32 = f.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]]) as u32).sum();
    let folded = (sum & 0xffff) + (sum >> 16);
    let csum = !(((folded & 0xffff) + (folded >> 16)) as u16);
    f[10..12].copy_from_slice(&csum.to_be_bytes());
    f.extend_from_slice(&CLIENT_PORT.to_be_bytes());
    f.extend_from_slice(&SERVER_PORT.to_be_bytes());
    f.extend_from_slice(&(udp_len as u16).to_be_bytes());
    f.extend_from_slice(&[0, 0]);
    f.extend_from_slice(payload);
    f
}

/// **Send a client message from source 0.0.0.0** on `interface`, through a packet socket
/// (the frame from [`zero_source_frame`], to the Ethernet broadcast). A UDP socket bound
/// to 0.0.0.0:68 only says "the kernel picks": once ANY interface holds an address (wg0,
/// the other NIC's lease) the kernel picks that one, and the DISCOVER of an interface
/// still without a lease leaves with a source nothing answers (estate-box D4, measured
/// 2026-10-01). Falls back to `udp`'s own send when no packet socket can be opened.
fn send_from_zero(udp: &UdpSocket, interface: &str, payload: &[u8]) -> Result<()> {
    let raw = (|| -> Result<()> {
        use socket2::{Domain, Protocol, SockAddr, Socket, Type};
        const ETH_P_IP: u16 = 0x0800;
        // SIOCGIFINDEX in this process's own network namespace (crate::net), not
        // /sys/class/net: sysfs shows the namespace it was mounted in, which need not be ours.
        let ifindex = crate::net::interface_index(interface)? as i32;
        let sock = Socket::new(Domain::PACKET, Type::DGRAM, Some(Protocol::from(ETH_P_IP.to_be() as i32))).context("opening a packet socket")?;
        // struct sockaddr_ll: family, protocol (be), ifindex, hatype, pkttype, halen, addr[8].
        let mut storage: libc_sockaddr_storage = unsafe { std::mem::zeroed() };
        let ll = &mut storage as *mut libc_sockaddr_storage as *mut SockaddrLl;
        // SAFETY: `storage` is zeroed, large and aligned enough for a sockaddr_ll.
        unsafe {
            (*ll).sll_family = 17; // AF_PACKET
            (*ll).sll_protocol = ETH_P_IP.to_be();
            (*ll).sll_ifindex = ifindex;
            (*ll).sll_halen = 6;
            (*ll).sll_addr = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0];
        }
        // SAFETY: the storage holds a valid sockaddr_ll of the length given.
        let addr = unsafe { SockAddr::new(std::mem::transmute::<libc_sockaddr_storage, socket2::SockAddrStorage>(storage), std::mem::size_of::<SockaddrLl>() as u32) };
        sock.send_to(&zero_source_frame(payload), &addr).context("sending on the packet socket")?;
        Ok(())
    })();
    match raw {
        Ok(()) => Ok(()),
        Err(e) => {
            tracing::debug!(interface, error = %format!("{e:#}"), "DHCP: no packet socket — sent from the UDP socket (the kernel picks the source)");
            udp.send_to(payload, SocketAddrV4::new(Ipv4Addr::BROADCAST, SERVER_PORT)).map(|_| ()).context("sending to 255.255.255.255:67")
        }
    }
}

/// `struct sockaddr_ll` (linux/if_packet.h).
#[repr(C)]
struct SockaddrLl {
    sll_family: u16,
    sll_protocol: u16,
    sll_ifindex: i32,
    sll_hatype: u16,
    sll_pkttype: u8,
    sll_halen: u8,
    sll_addr: [u8; 8],
}

/// `struct sockaddr_storage`'s size and alignment, for building a [`SockaddrLl`] in place.
#[repr(C, align(8))]
struct libc_sockaddr_storage([u8; 128]);

/// Wait (until `deadline`) for a reply of `wanted` kind to transaction `xid`.
///
/// A NAK for our xid is returned immediately *as a reply* — the server said
/// no, and waiting the timeout out after a "no" is just a slower "no". It
/// comes back as data rather than an error because the renewal loop must tell
/// "the server refused" (⇒ re-DISCOVER now) from "nobody answered" (⇒ retry,
/// then rebind); [`refuse_nak`] turns it into the error the handshake wants.
/// Everything that is not ours or not the wanted kind is skipped in silence.
fn await_reply(
    sock: &UdpSocket,
    xid: u32,
    mac: [u8; 6],
    wanted: u8,
    deadline: Instant,
) -> Result<Reply> {
    let mut buf = [0u8; 1536];
    loop {
        let left = deadline
            .checked_duration_since(Instant::now())
            .filter(|d| !d.is_zero())
            .context("timed out waiting for a reply")?;
        sock.set_read_timeout(Some(left))
            .context("setting the receive timeout")?;
        let n = match sock.recv(&mut buf) {
            Ok(n) => n,
            Err(e)
                if e.kind() == std::io::ErrorKind::WouldBlock
                    || e.kind() == std::io::ErrorKind::TimedOut =>
            {
                bail!("timed out waiting for a reply")
            }
            Err(e) => return Err(e).context("receiving on 0.0.0.0:68"),
        };
        let Ok(reply) = parse_reply(&buf[..n], xid, mac) else {
            continue; // somebody else's conversation, or noise
        };
        if reply.kind == wanted || reply.kind == DHCPNAK {
            return Ok(reply);
        }
    }
}

/// The handshake's reading of a NAK: an error naming the server's reason.
fn refuse_nak(reply: Reply) -> Result<Reply> {
    if reply.kind == DHCPNAK {
        bail!(
            "the server answered DHCPNAK{}",
            reply
                .message
                .as_deref()
                .map(|m| format!(": {m}"))
                .unwrap_or_default()
        );
    }
    Ok(reply)
}

/// Hold an ACK to what a lease *means*, and convert it.
///
/// One reader for the initial handshake and every renewal leg (one implementation). A
/// missing mask or lease time is not defaulted (the identity-value trap: a
/// guessed /24 is indistinguishable from a real one right up until it isn't) —
/// it is a refusal.
fn lease_from_ack(ack: &Reply, fallback_server: Ipv4Addr) -> Result<Lease> {
    if ack.yiaddr.is_unspecified() {
        bail!("the ACK's yiaddr is 0.0.0.0; that is not an address");
    }
    let mask = ack
        .subnet_mask
        .context("the ACK carried no subnet mask (option 1)")?;
    let lease_secs = ack
        .lease_secs
        .context("the ACK carried no lease time (option 51)")?;
    Ok(Lease {
        address: ack.yiaddr,
        prefix: prefix_from_mask(mask)?,
        gateway: ack.router,
        server: ack.server_id.unwrap_or(fallback_server),
        lease_secs,
        t1_secs: ack.t1_secs,
        t2_secs: ack.t2_secs,
        host_name: ack.host_name.clone(),
        // NOT refused when empty (see `opt_ipv4_list`): a lease with no
        // resolver is still a lease. Who says so out loud, and once, is
        // `crate::net::configure_via_dhcp` — the boot path, where "once" is a fact
        // rather than a hope; every renewal comes back through here too.
        dns: ack.dns.clone(),
        // Same rule as `dns`: carried whole, in the server's order, and judged
        // by the boot path. `pid1` decides which of them it may install.
        routes: ack.routes.clone(),
    })
}

/// Run the whole handshake on `interface` and return the lease.
///
/// Bounded: [`ATTEMPTS`] rounds of DISCOVER, each waiting [`ATTEMPT_TIMEOUT`]
/// for its OFFER and again for its ACK, worst case ~24 s, typical failure
/// (silent network) 12 s. The interface must already be UP — that is
/// `crate::net::bring_link_up`'s job, the same half the static path uses.
pub fn obtain(interface: &str, mac: [u8; 6]) -> Result<Lease> {
    obtain_with(interface, mac, false)
}

/// **The level a failed attempt is said at** (Gunnar c040144f / feff3b82): INFO in the
/// boot's handshake (the boot's failure is said ONCE, at WARN, by the bring-up: "the
/// handshake FAILED at boot") and debug in the background retry (`quiet`), whose own
/// bounded lines come from [`crate::net::keep_asking`]. Never WARN: four per interface
/// per retry was the spam the retry exists to avoid.
pub fn attempt_failed_level(quiet: bool) -> tracing::Level {
    if quiet {
        tracing::Level::DEBUG
    } else {
        tracing::Level::INFO
    }
}

/// [`obtain`], with each failed attempt said at [`attempt_failed_level`]`(quiet)`.
pub fn obtain_with(interface: &str, mac: [u8; 6], quiet: bool) -> Result<Lease> {
    let sock = client_socket(interface)?;
    let started = Instant::now();
    let mut last_err = None;

    for attempt in 1..=ATTEMPTS {
        let xid = fresh_xid();
        let secs = started.elapsed().as_secs().min(u16::MAX as u64) as u16;

        let round: Result<Lease> = (|| {
            send_from_zero(&sock, interface, &discover_message(xid, secs, mac))
                .context("sending DHCPDISCOVER to 255.255.255.255:67")?;
            let deadline = Instant::now() + ATTEMPT_TIMEOUT;
            let offer = await_reply(&sock, xid, mac, DHCPOFFER, deadline)
                .and_then(refuse_nak)
                .context("waiting for a DHCPOFFER")?;
            let offered = offer.yiaddr;
            let server = offer
                .server_id
                .context("the OFFER carried no server identifier (option 54)")?;
            tracing::info!(
                interface,
                offered = %offered,
                server = %server,
                "DHCP OFFER received"
            );

            // Seen red before green (2026-08-12): with this REQUEST sabotaged
            // to carry `xid ^ 1`, the appliance booted, received four OFFERs,
            // matched no ACK, warned "handshake FAILED … no lease on eth0
            // after 4 attempts (12 s)" on serial and served unreachable with
            // NO address — the honest-failure path is real, not assumed.
            send_from_zero(&sock, interface, &request_message(xid, secs, mac, offered, server))
                .context("sending DHCPREQUEST")?;
            let ack = await_reply(&sock, xid, mac, DHCPACK, deadline)
                .and_then(refuse_nak)
                .context("waiting for the DHCPACK")?;

            // The ACK is held to what a lease means — `lease_from_ack`, the
            // same reader every renewal leg uses. A refusal here keeps the
            // boot honest-unreachable.
            lease_from_ack(&ack, server)
        })();

        match round {
            Ok(lease) => {
                tracing::info!(
                    interface,
                    address = %lease.address,
                    prefix = lease.prefix,
                    gateway = ?lease.gateway,
                    server = %lease.server,
                    lease_secs = lease.lease_secs,
                    t1_secs = ?lease.t1_secs,
                    t2_secs = ?lease.t2_secs,
                    "DHCP ACK: lease taken. The renewal loop keeps it alive per RFC \
                     2131 §4.4.5 — unicast REQUEST at T1, broadcast at T2, \
                     re-DISCOVER at expiry — see the 'renewal scheduled' line"
                );
                return Ok(lease);
            }
            Err(e) => {
                if attempt_failed_level(quiet) == tracing::Level::DEBUG {
                    tracing::debug!(interface, attempt, error = %format!("{e:#}"), "DHCP attempt failed");
                } else {
                    tracing::info!(interface, attempt, error = %format!("{e:#}"), "DHCP attempt failed");
                }
                last_err = Some(e);
            }
        }
    }
    Err(last_err.unwrap_or_else(|| anyhow::anyhow!("no DHCP attempts ran"))).with_context(|| {
        format!(
            "no lease on {interface} after {ATTEMPTS} attempts ({:.0} s)",
            started.elapsed().as_secs_f32()
        )
    })
}

// ── the renewal transport ────────────────────────────────────────────────────

pub mod renew;

/// [`renew::Transport`] over the real socket: one bounded REQUEST leg per
/// call, on the same `client_socket` shape the handshake uses.
///
/// Each leg opens its own socket rather than holding one across the hours
/// between T1s: a bound-forever `0.0.0.0:68` buys nothing (nothing competes
/// for the port on this appliance) and a per-leg open means a leg can never
/// inherit a socket some earlier failure left in a strange state. Blocking by
/// design, like all of this file — the driver (`crate::net::dhcp_renewal`) runs legs
/// in `spawn_blocking`, which is the shape the existing handshake code already
/// has; making these few short socket ops async would fork the socket code
/// into two dialects for no bound that `ATTEMPT_TIMEOUT` does not already give.
pub struct WireTransport {
    /// The interface the lease lives on.
    pub interface: String,
    /// Its MAC, for `chaddr` and reply filtering.
    pub mac: [u8; 6],
}

impl WireTransport {
    fn one_leg(&self, lease: &Lease, broadcast: bool) -> Result<Reply> {
        let sock = client_socket(&self.interface)?;
        let xid = fresh_xid();
        let to = if broadcast {
            SocketAddrV4::new(Ipv4Addr::BROADCAST, SERVER_PORT)
        } else {
            SocketAddrV4::new(lease.server, SERVER_PORT)
        };
        tracing::info!(
            interface = %self.interface,
            address = %lease.address,
            to = %to,
            state = if broadcast { "REBINDING" } else { "RENEWING" },
            "DHCP renewal: sending {} REQUEST",
            if broadcast { "broadcast" } else { "unicast" }
        );
        sock.send_to(&renew_message(xid, 0, self.mac, lease.address), to)
            .context("sending the renewal DHCPREQUEST")?;
        await_reply(
            &sock,
            xid,
            self.mac,
            DHCPACK,
            Instant::now() + ATTEMPT_TIMEOUT,
        )
    }
}

impl renew::Transport for WireTransport {
    fn request(&mut self, lease: &Lease, broadcast: bool) -> renew::Outcome {
        match self.one_leg(lease, broadcast) {
            Ok(reply) if reply.kind == DHCPNAK => renew::Outcome::Nak(reply.message),
            Ok(reply) => match lease_from_ack(&reply, lease.server) {
                Ok(granted) => renew::Outcome::Ack(granted),
                Err(err) => {
                    // An ACK that does not amount to a lease is treated as no
                    // answer: the timers keep running and the retry ladder
                    // decides, exactly as if the packet had been lost.
                    tracing::warn!(
                        interface = %self.interface,
                        error = %format!("{err:#}"),
                        "DHCP renewal ACK was malformed; treating it as no reply"
                    );
                    renew::Outcome::NoReply
                }
            },
            Err(err) => {
                tracing::debug!(
                    interface = %self.interface,
                    error = %format!("{err:#}"),
                    "DHCP renewal leg got no usable reply"
                );
                renew::Outcome::NoReply
            }
        }
    }

    fn rediscover(&mut self) -> Result<Lease> {
        obtain(&self.interface, self.mac)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const MAC: [u8; 6] = [0x52, 0x54, 0x00, 0x12, 0x34, 0x56];

    // ── the builders, against RFC 2131 §2's figure, not against the code ────

    #[test]
    fn discover_has_the_bootp_shape_at_the_rfc_offsets() {
        let p = discover_message(0x1234_5678, 7, MAC);
        assert!(p.len() >= MIN_PACKET, "RFC 951 minimum packet");
        assert_eq!(p[0], 1, "op = BOOTREQUEST");
        assert_eq!((p[1], p[2], p[3]), (1, 6, 0), "htype/hlen/hops");
        assert_eq!(&p[4..8], &[0x12, 0x34, 0x56, 0x78], "xid, network order");
        assert_eq!(&p[8..10], &[0, 7], "secs");
        assert_eq!(
            &p[10..12],
            &[0x80, 0x00],
            "the BROADCAST flag must be set: without it a server unicasts its \
             reply to an address the kernel does not have yet, and the reply is \
             dropped before this socket ever sees it"
        );
        assert_eq!(
            &p[12..28],
            &[0u8; 16],
            "ciaddr/yiaddr/siaddr/giaddr all zero"
        );
        assert_eq!(&p[28..34], &MAC, "chaddr");
        assert_eq!(&p[34..44], &[0u8; 10], "chaddr padding");
        assert_eq!(&p[236..240], &MAGIC_COOKIE, "the cookie after sname+file");
        assert_eq!(
            &p[240..243],
            &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER],
            "first option: message type DISCOVER"
        );
        let end = p.iter().skip(240).position(|&b| b == OPT_END);
        assert!(end.is_some(), "options must be terminated");
    }

    #[test]
    fn request_names_the_offered_address_and_the_chosen_server() {
        let p = request_message(
            1,
            0,
            MAC,
            Ipv4Addr::new(10, 0, 2, 15),
            Ipv4Addr::new(10, 0, 2, 2),
        );
        assert_eq!(&p[240..243], &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST]);
        assert_eq!(
            &p[243..249],
            &[OPT_REQUESTED_IP, 4, 10, 0, 2, 15],
            "option 50: RFC 2131 §4.3.2, MUST in SELECTING"
        );
        assert_eq!(
            &p[249..255],
            &[OPT_SERVER_ID, 4, 10, 0, 2, 2],
            "option 54: which server's offer is being taken"
        );
    }

    /// RFC 2131 §4.3.2's renewal column: `ciaddr` filled, options 50/54
    /// absent, BROADCAST flag off. A renewal REQUEST that carries option 54
    /// is a SELECTING REQUEST — servers are entitled to treat it as one, and
    /// some NAK it — so the absences are asserted, not assumed.
    #[test]
    fn a_renewal_request_fills_ciaddr_and_omits_the_selecting_options() {
        let p = renew_message(9, 0, MAC, Ipv4Addr::new(10, 0, 2, 15));
        assert_eq!(
            &p[10..12],
            &[0, 0],
            "the BROADCAST flag must be OFF: this client holds a working \
             address and asks for its ACK unicast"
        );
        assert_eq!(
            &p[12..16],
            &[10, 0, 2, 15],
            "ciaddr carries the held address"
        );
        assert_eq!(&p[16..28], &[0u8; 12], "yiaddr/siaddr/giaddr stay zero");
        assert_eq!(&p[240..243], &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST]);
        let options = &p[240..];
        let mut at = 0;
        while at < options.len() && options[at] != OPT_END {
            let code = options[at];
            if code == OPT_PAD {
                at += 1;
                continue;
            }
            assert_ne!(
                code, OPT_REQUESTED_IP,
                "option 50 MUST NOT appear when renewing"
            );
            assert_ne!(
                code, OPT_SERVER_ID,
                "option 54 MUST NOT appear when renewing"
            );
            at += 2 + options[at + 1] as usize;
        }
    }

    // ── a synthetic server, so parse is proven against bytes we did not
    //    build with the parser's own vocabulary ─────────────────────────────

    fn reply_packet(kind: u8, xid: u32, mac: [u8; 6], yiaddr: [u8; 4], opts: &[u8]) -> Vec<u8> {
        let mut p = vec![0u8; 240];
        p[0] = BOOTREPLY;
        p[1] = HTYPE_ETHER;
        p[2] = 6;
        p[4..8].copy_from_slice(&xid.to_be_bytes());
        p[16..20].copy_from_slice(&yiaddr);
        p[28..34].copy_from_slice(&mac);
        p[236..240].copy_from_slice(&MAGIC_COOKIE);
        p.extend_from_slice(&[OPT_MESSAGE_TYPE, 1, kind]);
        p.extend_from_slice(opts);
        p.push(OPT_END);
        p
    }

    /// QEMU user-net's actual answer, byte for byte in the fields that matter.
    #[test]
    fn an_ack_parses_into_the_whole_lease() {
        let p = reply_packet(
            DHCPACK,
            0xabcd,
            MAC,
            [10, 0, 2, 15],
            &[
                OPT_SERVER_ID,
                4,
                10,
                0,
                2,
                2, //
                OPT_SUBNET_MASK,
                4,
                255,
                255,
                255,
                0, //
                OPT_ROUTER,
                4,
                10,
                0,
                2,
                2, //
                OPT_LEASE_TIME,
                4,
                0,
                1,
                0x51,
                0x80, // 86400
                OPT_RENEWAL_T1,
                4,
                0,
                0,
                0xa8,
                0xc0, // 43200
                OPT_REBINDING_T2,
                4,
                0,
                1,
                0x27,
                0x50, // 75600
            ],
        );
        let r = parse_reply(&p, 0xabcd, MAC).expect("a well-formed ACK");
        assert_eq!(r.kind, DHCPACK);
        assert_eq!(r.yiaddr, Ipv4Addr::new(10, 0, 2, 15));
        assert_eq!(r.subnet_mask, Some(Ipv4Addr::new(255, 255, 255, 0)));
        assert_eq!(r.router, Some(Ipv4Addr::new(10, 0, 2, 2)));
        assert_eq!(r.server_id, Some(Ipv4Addr::new(10, 0, 2, 2)));
        assert_eq!(r.lease_secs, Some(86400));
        assert_eq!(r.t1_secs, Some(43200));
        assert_eq!(r.t2_secs, Some(75600));
    }

    /// The option that was on the floor until 2026-09-08. `mirror sync` on
    /// gunnar.rs failed with `could not be reached … : Dns` because this walk
    /// had no arm for code 6, so a perfectly good ACK produced a lease that
    /// could not name anything.
    #[test]
    fn option_6_reaches_the_lease_with_both_servers_in_the_servers_order() {
        let p = reply_packet(
            DHCPACK,
            0x99,
            MAC,
            [10, 0, 2, 15],
            &[
                OPT_SERVER_ID,
                4,
                10,
                0,
                2,
                2, //
                OPT_SUBNET_MASK,
                4,
                255,
                255,
                255,
                0, //
                OPT_LEASE_TIME,
                4,
                0,
                0,
                0x0e,
                0x10, // 3600
                OPT_DOMAIN_NAME_SERVERS,
                8,
                10,
                0,
                2,
                3, //
                1,
                1,
                1,
                1,
            ],
        );
        let r = parse_reply(&p, 0x99, MAC).expect("a well-formed ACK");
        assert_eq!(
            r.dns,
            vec![Ipv4Addr::new(10, 0, 2, 3), Ipv4Addr::new(1, 1, 1, 1)],
            "both servers, in the order the server sent them (RFC 2132 §3.8 \
             calls that order preference, and resolv.conf honours file order)"
        );

        let lease = lease_from_ack(&r, Ipv4Addr::new(10, 0, 2, 2)).expect("a lease");
        assert_eq!(
            lease.dns,
            vec![Ipv4Addr::new(10, 0, 2, 3), Ipv4Addr::new(1, 1, 1, 1)],
            "the parse is only half of it: the LEASE is what pid1 publishes, \
             and a Reply field that never reaches it fixes nothing"
        );
    }

    /// Some servers simply do not send option 6. That is a box with an address
    /// and no name service — worth saying out loud once, never worth refusing.
    #[test]
    fn an_ack_with_no_option_6_yields_an_empty_set_and_a_perfectly_good_lease() {
        let p = reply_packet(
            DHCPACK,
            0x9a,
            MAC,
            [10, 0, 2, 15],
            &[
                OPT_SERVER_ID,
                4,
                10,
                0,
                2,
                2, //
                OPT_SUBNET_MASK,
                4,
                255,
                255,
                255,
                0, //
                OPT_LEASE_TIME,
                4,
                0,
                0,
                0x0e,
                0x10,
            ],
        );
        let r = parse_reply(&p, 0x9a, MAC).expect("an ACK without option 6 is well-formed");
        assert!(
            r.dns.is_empty(),
            "no option 6 means no servers, not an error"
        );
        let lease = lease_from_ack(&r, Ipv4Addr::new(10, 0, 2, 2))
            .expect("the ABSENCE of a resolver must never cost the box its address");
        assert!(lease.dns.is_empty());
        assert_eq!(lease.address, Ipv4Addr::new(10, 0, 2, 15));
    }

    /// TRUNCATED, not refused — and the grading is the point of the test.
    /// Option 6 is a hint; options 1 and 51 are what an address MEANS. Throwing
    /// the lease away over a server that padded its resolver list wrong would
    /// leave the appliance with no address at all, which is strictly worse than
    /// the one resolver it could still be told about.
    #[test]
    fn a_malformed_option_6_length_is_truncated_to_whole_addresses_not_refused() {
        let p = reply_packet(
            DHCPACK,
            0x9b,
            MAC,
            [10, 0, 2, 15],
            &[
                OPT_SERVER_ID,
                4,
                10,
                0,
                2,
                2, //
                OPT_SUBNET_MASK,
                4,
                255,
                255,
                255,
                0, //
                OPT_LEASE_TIME,
                4,
                0,
                0,
                0x0e,
                0x10, //
                OPT_DOMAIN_NAME_SERVERS,
                6, // six bytes: one address and half of another
                10,
                0,
                2,
                3, //
                1,
                1,
            ],
        );
        let r = parse_reply(&p, 0x9b, MAC).expect("the ACK stands; only the hint is malformed");
        assert_eq!(
            r.dns,
            vec![Ipv4Addr::new(10, 0, 2, 3)],
            "the complete address is kept and the two orphan bytes are dropped: \
             completing them would mean inventing the other two (measured, not asserted)"
        );

        // Under four bytes there is no complete address at all, and that is an
        // empty set — never a `1.1.0.0` assembled out of padding.
        let short = reply_packet(
            DHCPACK,
            0x9c,
            MAC,
            [10, 0, 2, 15],
            &[OPT_DOMAIN_NAME_SERVERS, 2, 1, 1],
        );
        let r = parse_reply(&short, 0x9c, MAC).expect("still a parseable reply");
        assert!(r.dns.is_empty());
    }

    /// Servers pad a short list with zeros. `nameserver 0.0.0.0` is a line musl
    /// will actually try, and actually spend its timeout on.
    #[test]
    fn padding_zeros_in_option_6_are_not_addresses() {
        let p = reply_packet(
            DHCPACK,
            0x9d,
            MAC,
            [10, 0, 2, 15],
            &[
                OPT_DOMAIN_NAME_SERVERS,
                8,
                10,
                0,
                2,
                3, //
                0,
                0,
                0,
                0,
            ],
        );
        let r = parse_reply(&p, 0x9d, MAC).expect("a parseable reply");
        assert_eq!(r.dns, vec![Ipv4Addr::new(10, 0, 2, 3)]);
    }

    /// A server is entitled to answer with only what was asked for, so the ask
    /// is part of the fix and not an ornament on it.
    #[test]
    fn the_parameter_request_list_asks_for_option_6() {
        let p = discover_message(1, 0, MAC);
        let options = &p[240..];
        let mut at = 0;
        let mut asked = None;
        while at < options.len() && options[at] != OPT_END {
            if options[at] == OPT_PAD {
                at += 1;
                continue;
            }
            let len = options[at + 1] as usize;
            if options[at] == OPT_PARAM_REQUEST {
                asked = Some(options[at + 2..at + 2 + len].to_vec());
            }
            at += 2 + len;
        }
        let asked = asked.expect("every request carries a parameter request list");
        assert!(
            asked.contains(&OPT_DOMAIN_NAME_SERVERS),
            "option 55 must name option 6, or a server that sends strictly what \
             was requested leaves this appliance with no resolver: {asked:?}"
        );
        assert!(
            asked.contains(&OPT_CLASSLESS_STATIC_ROUTES),
            "option 55 must name option 121, or a server that sends strictly what was requested \
             leaves this appliance reaching only its own prefix: {asked:?}"
        );
    }

    /// **RFC 3442's variable-length destination, which is the whole trap.**
    ///
    /// Each entry is one prefix byte, then only `ceil(prefix/8)` destination
    /// bytes, then four gateway bytes. A decoder that assumes four destination
    /// bytes reads the gateway as network and loses every route after it. The
    /// vector below is the real one from UpCloud's utility network — the route
    /// whose absence left the appliance two hours out of time — followed by a
    /// `/24`, a `/32` and a `/0` so all four length classes are exercised in
    /// one pass.
    #[test]
    fn option_121_decodes_a_variable_length_destination_and_keeps_the_order() {
        let payload = [
            8, 10, 10, 13, 4, 1, // 10.0.0.0/8       via 10.13.4.1
            24, 192, 168, 9, 10, 13, 4, 1, // 192.168.9.0/24   via 10.13.4.1
            32, 169, 254, 169, 254, 10, 13, 4, 1, // 169.254.169.254/32 via 10.13.4.1
            0, 10, 13, 4, 1, // 0.0.0.0/0        via 10.13.4.1
        ];
        let gw = Ipv4Addr::new(10, 13, 4, 1);
        assert_eq!(
            opt_classless_routes(&payload),
            vec![
                Route { destination: Ipv4Addr::new(10, 0, 0, 0), prefix: 8, gateway: gw },
                Route { destination: Ipv4Addr::new(192, 168, 9, 0), prefix: 24, gateway: gw },
                Route { destination: Ipv4Addr::new(169, 254, 169, 254), prefix: 32, gateway: gw },
                Route { destination: Ipv4Addr::UNSPECIFIED, prefix: 0, gateway: gw },
            ]
        );
        assert!(opt_classless_routes(&payload)[3].is_default());
        assert!(!opt_classless_routes(&payload)[0].is_default());
    }

    /// Host bits a server leaves in the destination are not meaningful, and are
    /// masked off ONCE, here, so nothing downstream has to wonder.
    #[test]
    fn option_121_masks_the_destination_to_its_prefix() {
        let r = opt_classless_routes(&[24, 192, 168, 9, 10, 13, 4, 1]);
        assert_eq!(r[0].destination, Ipv4Addr::new(192, 168, 9, 0));
        let r = opt_classless_routes(&[12, 10, 0xFF, 10, 13, 4, 1]);
        assert_eq!(r[0].destination, Ipv4Addr::new(10, 240, 0, 0), "a /12 keeps twelve bits");
    }

    /// A truncated tail loses the tail and NOTHING ELSE. A server that said
    /// three true things and then ran out of bytes has still said three true
    /// things, and refusing the lease over the fourth would leave the box with
    /// no address at all.
    #[test]
    fn a_truncated_option_121_keeps_the_routes_that_were_whole() {
        let good = [8, 10, 10, 13, 4, 1];
        let mut truncated = good.to_vec();
        truncated.extend_from_slice(&[24, 192, 168, 9, 10]); // one gateway byte short
        let r = opt_classless_routes(&truncated);
        assert_eq!(r.len(), 1, "the whole entry survives, the truncated one does not: {r:?}");
        assert_eq!(r[0].destination, Ipv4Addr::new(10, 0, 0, 0));
        // …and a byte that cannot be a prefix length stops the walk rather than
        // being read as one.
        assert!(opt_classless_routes(&[33, 1, 2, 3, 4, 5]).is_empty());
        assert!(opt_classless_routes(&[]).is_empty());
    }

    /// An ACK carrying option 121 lands the routes on the LEASE, in order —
    /// the whole point being that `pid1` can then install them.
    #[test]
    fn option_121_reaches_the_lease() {
        let opts = [OPT_CLASSLESS_STATIC_ROUTES, 6, 8, 10, 10, 13, 4, 1];
        let p = reply_packet(DHCPACK, 1, MAC, [10, 13, 8, 99], &opts);
        let r = parse_reply(&p, 1, MAC).expect("the ACK parses");
        assert_eq!(r.routes.len(), 1);
        assert_eq!(r.routes[0].gateway, Ipv4Addr::new(10, 13, 4, 1));
    }

    #[test]
    fn pad_options_are_stepped_over_not_read_as_zero_length_options() {
        let mut p = reply_packet(DHCPOFFER, 1, MAC, [10, 0, 2, 15], &[]);
        // Splice pads before the end marker.
        let end = p.len() - 1;
        p.splice(end..end, [OPT_PAD, OPT_PAD, OPT_PAD]);
        let r = parse_reply(&p, 1, MAC).expect("pads are legal filler");
        assert_eq!(r.kind, DHCPOFFER);
    }

    /// Measure, never assert on the "is this for us" gate: every wrong field must reject,
    /// or the gate is decoration and any broadcast on port 68 becomes ours.
    #[test]
    fn replies_that_are_not_ours_are_refused() {
        let good = reply_packet(DHCPACK, 7, MAC, [10, 0, 2, 15], &[]);
        assert!(
            parse_reply(&good, 7, MAC).is_ok(),
            "the baseline must parse"
        );

        assert!(
            parse_reply(&good, 8, MAC).is_err(),
            "wrong xid accepted: another client's lease would be applied here"
        );
        let mut other_mac = good.clone();
        other_mac[30] ^= 0xff;
        assert!(
            parse_reply(&other_mac, 7, MAC).is_err(),
            "wrong chaddr accepted"
        );
        let mut not_reply = good.clone();
        not_reply[0] = BOOTREQUEST;
        assert!(
            parse_reply(&not_reply, 7, MAC).is_err(),
            "our own broadcast echoed back would be accepted as a reply"
        );
        let mut no_cookie = good.clone();
        no_cookie[236] = 0;
        assert!(
            parse_reply(&no_cookie, 7, MAC).is_err(),
            "plain BOOTP accepted"
        );
        assert!(
            parse_reply(&good[..200], 7, MAC).is_err(),
            "a truncated header must not parse"
        );
        let no_type = {
            let mut p = reply_packet(DHCPACK, 7, MAC, [10, 0, 2, 15], &[]);
            p[241] = 0; // zero out the message-type length ⇒ type stays 0
            p[240] = OPT_PAD;
            p
        };
        assert!(
            parse_reply(&no_type, 7, MAC).is_err(),
            "a reply with no message type is not a DHCP reply"
        );
    }

    #[test]
    fn an_option_running_past_the_packet_end_is_an_error_not_a_panic() {
        let mut p = reply_packet(DHCPACK, 7, MAC, [10, 0, 2, 15], &[]);
        p.pop(); // drop OPT_END
        p.extend_from_slice(&[OPT_SERVER_ID, 200]); // claims 200 bytes, has none
        assert!(parse_reply(&p, 7, MAC).is_err());
        // And a lone code byte with no length byte at the very end:
        let mut q = reply_packet(DHCPACK, 7, MAC, [10, 0, 2, 15], &[]);
        q.pop();
        q.push(OPT_SERVER_ID);
        assert!(parse_reply(&q, 7, MAC).is_err());
    }

    #[test]
    fn masks_convert_to_prefixes_and_holes_are_refused() {
        for (mask, prefix) in [
            ([255, 255, 255, 0], 24u8),
            ([255, 255, 0, 0], 16),
            ([255, 255, 255, 255], 32),
            ([0, 0, 0, 0], 0),
            ([255, 255, 255, 252], 30),
        ] {
            assert_eq!(
                prefix_from_mask(Ipv4Addr::from(mask)).unwrap(),
                prefix,
                "{mask:?}"
            );
        }
        assert!(
            prefix_from_mask(Ipv4Addr::new(255, 0, 255, 0)).is_err(),
            "a non-contiguous mask must be refused, not rounded to /16 or /24"
        );
        assert!(prefix_from_mask(Ipv4Addr::new(0, 255, 255, 255)).is_err());
    }
}

#[cfg(test)]
mod zero_source_tests {
    use super::*;

    /// **A DISCOVER leaves from 0.0.0.0, whatever else holds an address** (estate-box D4
    /// 2026-10-01: once wg0 held 10.44.0.3, the twin's retried DISCOVERs from a UDP socket
    /// bound to 0.0.0.0:68 left with source 10.44.0.3 and nothing answered — measured in a
    /// namespace: from 0.0.0.0 received, with another address held not received). The
    /// frame is built here: IPv4 + UDP, 0.0.0.0:68 → 255.255.255.255:67, a valid header
    /// checksum, the payload as given.
    #[test]
    fn a_dhcp_frame_carries_source_zero_and_a_valid_ip_checksum() {
        let payload = b"BOOTP";
        let f = zero_source_frame(payload);
        assert_eq!(f.len(), 20 + 8 + payload.len());
        assert_eq!(f[0], 0x45, "IPv4, 20-byte header");
        assert_eq!(u16::from_be_bytes([f[2], f[3]]) as usize, f.len(), "total length");
        assert_eq!(f[9], 17, "UDP");
        assert_eq!(&f[12..16], &[0, 0, 0, 0], "source 0.0.0.0");
        assert_eq!(&f[16..20], &[255, 255, 255, 255], "limited broadcast");
        assert_eq!(u16::from_be_bytes([f[20], f[21]]), 68);
        assert_eq!(u16::from_be_bytes([f[22], f[23]]), 67);
        assert_eq!(u16::from_be_bytes([f[24], f[25]]) as usize, 8 + payload.len(), "UDP length");
        let sum: u32 = f[..20].chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]]) as u32).sum();
        let folded = (sum & 0xffff) + (sum >> 16);
        assert_eq!(((folded & 0xffff) + (folded >> 16)) as u16, 0xffff, "the header checksum verifies");
        assert_eq!(&f[28..], payload);
    }
}

/// **Two interfaces, the first leased first** (the ONE box 2026-10-17: eth0 took
/// 81.27.107.63, then eth1's four DISCOVERs got no OFFER and the box ran without its
/// utility address). Run for real in a user + network namespace of its own: `eth0` a
/// dummy holding the public lease, `eth1` one end of a veth pair, and on the other end
/// (`util`) a DHCP server that behaves as UpCloud's utility network does — source-IP
/// filtering: a frame whose source is not 0.0.0.0 (nor the interface's own lease) is
/// dropped. `obtain("eth1")` must come back with eth1's own lease.
#[cfg(test)]
mod two_interface_tests {
    use super::*;
    use std::ffi::CString;
    use std::net::Ipv4Addr;
    use std::sync::{Arc, Mutex};

    const CHILD: &str = "APPLIANCE_PID1_NETNS_CHILD";
    const PUBLIC: Ipv4Addr = Ipv4Addr::new(81, 27, 107, 63);
    const SERVER: [u8; 4] = [10, 13, 8, 1];
    const UTILITY: [u8; 4] = [10, 13, 8, 78];
    const ETH_P_IP: u16 = 0x0800;

    #[test]
    fn eth1_gets_its_own_lease_while_eth0_already_holds_one() {
        match std::env::var(CHILD).as_deref() {
            Ok("eth1") => in_the_namespace(),
            Ok(_) => {}
            Err(_) => run_in_a_namespace_of_its_own("dhcp::two_interface_tests::eth1_gets_its_own_lease_while_eth0_already_holds_one", "eth1"),
        }
    }

    /// **A live-update RE-EXEC** (the ONE box 2026-10-17): the new PID 1 starts while the
    /// kernel still holds eth0's lease (address and default route). `apply` sent
    /// CREATE|EXCL, the kernel said EEXIST, the boot said "eth0 NOT up" and NO renewal loop
    /// ran for eth0. The same lease again is applied, and its session (the renewal) comes back.
    #[test]
    fn a_re_exec_with_the_lease_still_on_eth0_brings_eth0_up_and_keeps_its_renewal() {
        match std::env::var(CHILD).as_deref() {
            Ok("reexec") => re_exec_in_the_namespace(),
            Ok(_) => {}
            Err(_) => run_in_a_namespace_of_its_own("dhcp::two_interface_tests::a_re_exec_with_the_lease_still_on_eth0_brings_eth0_up_and_keeps_its_renewal", "reexec"),
        }
    }

    /// Re-run ONE test in a fresh user + net + uts namespace (unshare in the forked child,
    /// single-threaded there, then mapped to root of its own namespace); the child says
    /// `NETNS-LEASE-OK` when it got there.
    fn run_in_a_namespace_of_its_own(test: &str, which: &str) {
        use std::os::unix::process::CommandExt as _;
        let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
        let files: Vec<(CString, Vec<u8>)> = vec![
            (CString::new("/proc/self/setgroups").unwrap(), b"deny".to_vec()),
            (CString::new("/proc/self/uid_map").unwrap(), format!("0 {uid} 1").into_bytes()),
            (CString::new("/proc/self/gid_map").unwrap(), format!("0 {gid} 1").into_bytes()),
        ];
        let mut cmd = std::process::Command::new(std::env::current_exe().unwrap());
        cmd.args([test, "--exact", "--nocapture", "--test-threads=1"]).env(CHILD, which);
        // SAFETY: between fork and exec only async-signal-safe libc calls on data built before the fork.
        unsafe {
            cmd.pre_exec(move || {
                if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET | libc::CLONE_NEWUTS) != 0 {
                    return Err(std::io::Error::last_os_error());
                }
                for (path, body) in &files {
                    let fd = libc::open(path.as_ptr(), libc::O_WRONLY);
                    if fd < 0 || libc::write(fd, body.as_ptr().cast(), body.len()) != body.len() as isize {
                        return Err(std::io::Error::last_os_error());
                    }
                    libc::close(fd);
                }
                Ok(())
            });
        }
        let out = match cmd.output() {
            Ok(o) => o,
            Err(e) => {
                eprintln!("SKIPPED {test}: no user + net namespace on this machine ({e})");
                return;
            }
        };
        let said = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
        if let Some(l) = said.lines().find(|l| l.contains("NETNS-SKIPPED")) {
            eprintln!("SKIPPED {test}: {l}");
            return;
        }
        assert!(out.status.success() && said.contains("NETNS-LEASE-OK"), "in the namespace:\n{said}");
        eprintln!("{}", said.lines().find(|l| l.contains("NETNS-LEASE-OK")).unwrap_or_default().trim_start_matches(|c: char| c != 'N'));
    }

    /// The box's rp_filter (0) in the namespace, else `false` (said as SKIPPED).
    fn box_rp_filter() -> bool {
        let rp = |f: &str| std::fs::write(format!("/proc/sys/net/ipv4/conf/{f}/rp_filter"), "0");
        if let Err(e) = rp("all").and_then(|_| rp("default")) {
            let now = std::fs::read_to_string("/proc/sys/net/ipv4/conf/all/rp_filter").unwrap_or_default();
            if now.trim() != "0" {
                println!("NETNS-SKIPPED: rp_filter is {} and /proc/sys is not writable here ({e}) — run with /proc/sys writable (podman --security-opt unmask=/proc/sys)", now.trim());
                return false;
            }
        }
        true
    }

    /// `RTM_NEWLINK` for `name` of `linkinfo`'s kind.
    fn new_link(nl: &rustix::fd::OwnedFd, seq: u32, name: &str, linkinfo: Vec<u8>) {
        let mut body = vec![0u8; 16];
        body.extend(crate::net::rtattr(3, &[name.as_bytes(), &[0]].concat())); // IFLA_IFNAME
        body.extend(crate::net::rtattr(18, &linkinfo)); // IFLA_LINKINFO
        let m = crate::net::netlink_message(16, 0x1 | 0x4 | 0x200 | 0x400, seq, &body); // RTM_NEWLINK, REQUEST|ACK|EXCL|CREATE
        crate::net::request(nl, &m, &format!("create {name}")).unwrap();
    }

    /// A veth pair `name` <-> `peer`.
    fn veth(nl: &rustix::fd::OwnedFd, seq: u32, name: &str, peer: &str) {
        let mut p = vec![0u8; 16];
        p.extend(crate::net::rtattr(3, &[peer.as_bytes(), &[0]].concat()));
        new_link(nl, seq, name, [crate::net::rtattr(1, b"veth"), crate::net::rtattr(2, &crate::net::rtattr(1, &p))].concat());
    }

    /// The utility-style DHCP server on `util`, on a thread; what it dropped.
    fn start_server() -> Arc<Mutex<Vec<Ipv4Addr>>> {
        let dropped = Arc::new(Mutex::new(Vec::<Ipv4Addr>::new()));
        let util = packet_socket("util");
        let d = dropped.clone();
        std::thread::spawn(move || serve(util, &d));
        dropped
    }

    fn re_exec_in_the_namespace() {
        if !box_rp_filter() {
            return;
        }
        let nl = crate::net::netlink_socket().unwrap();
        veth(&nl, 11, "eth0", "util");
        let _ = std::fs::write("/proc/sys/net/ipv4/conf/util/rp_filter", "0");
        let _ = std::fs::write("/proc/sys/net/ipv4/conf/eth0/rp_filter", "0");
        crate::net::bring_link_up("util").unwrap();
        // What the kernel still holds when the new PID 1 starts: the lease's address and
        // default route, put there by the PID 1 before the re-exec (the same writer).
        let before = crate::net::NetPlan { interface: "eth0".into(), address: Ipv4Addr::from(UTILITY), prefix: 22, gateway: Some(Ipv4Addr::from(SERVER)) };
        crate::net::apply(&before).unwrap();
        let _dropped = start_server();
        let narrated = Mutex::new(Vec::<String>::new());
        let narrate = |l: &str| narrated.lock().unwrap().push(l.to_string());
        let how = crate::net::Bringup { hostname_prefix: "t", resolver_probe: None, narrate: &narrate };
        let sessions = crate::net::bring_up_network(crate::net::NetConfig::parse_list("eth0:dhcp").unwrap(), &how).unwrap();
        let said = narrated.lock().unwrap().join(" | ");
        assert_eq!(sessions.len(), 1, "eth0's session (its renewal) after a re-exec; narrated: {said}");
        assert!(said.contains("network up: eth0") && !said.contains("NOT up"), "{said}");
        assert_eq!(crate::net::interface_ipv4("eth0").unwrap(), Some(Ipv4Addr::from(UTILITY)));
        println!("NETNS-LEASE-OK eth0 {} re-applied after a re-exec, renewal session kept", sessions[0].lease.address);
    }

    fn in_the_namespace() {
        if !box_rp_filter() {
            return;
        }
        let nl = crate::net::netlink_socket().unwrap();
        new_link(&nl, 11, "eth0", crate::net::rtattr(1, b"dummy"));
        veth(&nl, 12, "eth1", "util");
        for f in ["eth1", "util"] {
            std::fs::write(format!("/proc/sys/net/ipv4/conf/{f}/rp_filter"), "0").unwrap();
        }
        // eth0 is leased first: up, with the public address and its default route.
        let eth0 = crate::net::bring_link_up("eth0").unwrap();
        crate::net::request(&nl, &crate::net::address_message(30, eth0, PUBLIC, 23), "eth0 address").unwrap();
        crate::net::request(&nl, &crate::net::default_route_message(31, eth0, Ipv4Addr::new(81, 27, 106, 1)), "eth0 default route").unwrap();
        crate::net::bring_link_up("util").unwrap();
        crate::net::bring_link_up("eth1").unwrap();
        let dropped = start_server();
        let mac = crate::net::interface_mac("eth1").unwrap();
        match obtain("eth1", mac) {
            Ok(lease) => {
                assert_eq!(lease.address, Ipv4Addr::from(UTILITY), "eth1's own lease");
                println!("NETNS-LEASE-OK eth1 {}", lease.address);
            }
            Err(e) => panic!("eth1 got no lease: {e:#}; the utility server dropped DISCOVER/REQUEST from {:?} (source-IP filtering)", dropped.lock().unwrap()),
        }
    }

    fn packet_socket(name: &str) -> i32 {
        let ifindex = crate::net::interface_index(name).unwrap() as i32;
        unsafe {
            let fd = libc::socket(libc::AF_PACKET, libc::SOCK_DGRAM, (ETH_P_IP.to_be()) as i32);
            assert!(fd >= 0, "packet socket: {}", std::io::Error::last_os_error());
            let mut ll: libc::sockaddr_ll = std::mem::zeroed();
            ll.sll_family = libc::AF_PACKET as u16;
            ll.sll_protocol = ETH_P_IP.to_be();
            ll.sll_ifindex = ifindex;
            assert_eq!(libc::bind(fd, (&ll as *const libc::sockaddr_ll).cast(), std::mem::size_of::<libc::sockaddr_ll>() as u32), 0);
            fd
        }
    }

    /// The utility network's DHCP server: OFFER/ACK to a DISCOVER/REQUEST from 0.0.0.0,
    /// anything from another source dropped (and noted).
    fn serve(fd: i32, dropped: &Mutex<Vec<Ipv4Addr>>) {
        let ifindex = crate::net::interface_index("util").unwrap() as i32;
        let mut buf = [0u8; 2048];
        loop {
            let n = unsafe { libc::recv(fd, buf.as_mut_ptr().cast(), buf.len(), 0) };
            if n < 28 + 240 {
                continue;
            }
            let f = &buf[..n as usize];
            if f[9] != 17 || u16::from_be_bytes([f[22], f[23]]) != SERVER_PORT {
                continue;
            }
            let src = Ipv4Addr::new(f[12], f[13], f[14], f[15]);
            if !src.is_unspecified() {
                dropped.lock().unwrap().push(src);
                continue;
            }
            let p = &f[28..];
            let xid = u32::from_be_bytes([p[4], p[5], p[6], p[7]]);
            let mut mac = [0u8; 6];
            mac.copy_from_slice(&p[28..34]);
            let kind = p[240..].windows(3).find(|w| w[0] == OPT_MESSAGE_TYPE && w[1] == 1).map(|w| w[2]);
            let answer = match kind {
                Some(DHCPDISCOVER) => DHCPOFFER,
                Some(DHCPREQUEST) => DHCPACK,
                _ => continue,
            };
            let reply = reply(answer, xid, mac);
            let frame = frame_from_server(&reply);
            unsafe {
                let mut ll: libc::sockaddr_ll = std::mem::zeroed();
                ll.sll_family = libc::AF_PACKET as u16;
                ll.sll_protocol = ETH_P_IP.to_be();
                ll.sll_ifindex = ifindex;
                ll.sll_halen = 6;
                ll.sll_addr[..6].copy_from_slice(&[0xff; 6]);
                libc::sendto(fd, frame.as_ptr().cast(), frame.len(), 0, (&ll as *const libc::sockaddr_ll).cast(), std::mem::size_of::<libc::sockaddr_ll>() as u32);
            }
        }
    }

    fn reply(kind: u8, xid: u32, mac: [u8; 6]) -> Vec<u8> {
        let mut p = vec![0u8; 240];
        p[0] = 2; // BOOTREPLY
        p[1] = 1;
        p[2] = 6;
        p[4..8].copy_from_slice(&xid.to_be_bytes());
        p[16..20].copy_from_slice(&UTILITY);
        p[28..34].copy_from_slice(&mac);
        p[236..240].copy_from_slice(&[99, 130, 83, 99]);
        p.extend_from_slice(&[OPT_MESSAGE_TYPE, 1, kind]);
        p.extend_from_slice(&[54, 4, SERVER[0], SERVER[1], SERVER[2], SERVER[3]]); // server id
        p.extend_from_slice(&[1, 4, 255, 255, 252, 0]); // mask /22
        p.extend_from_slice(&[3, 4, SERVER[0], SERVER[1], SERVER[2], SERVER[3]]); // router
        p.extend_from_slice(&[51, 4, 0, 1, 81, 128]); // 86400 s
        p.push(255);
        p
    }

    /// IPv4 + UDP, 10.13.8.1:67 → 255.255.255.255:68.
    fn frame_from_server(payload: &[u8]) -> Vec<u8> {
        let total = 28 + payload.len();
        let mut f = vec![0x45, 0x10];
        f.extend_from_slice(&(total as u16).to_be_bytes());
        f.extend_from_slice(&[0, 0, 0, 0, 64, 17, 0, 0]);
        f.extend_from_slice(&SERVER);
        f.extend_from_slice(&[255, 255, 255, 255]);
        let sum: u32 = f.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]]) as u32).sum();
        let folded = (sum & 0xffff) + (sum >> 16);
        let csum = !(((folded & 0xffff) + (folded >> 16)) as u16);
        f[10..12].copy_from_slice(&csum.to_be_bytes());
        f.extend_from_slice(&SERVER_PORT.to_be_bytes());
        f.extend_from_slice(&CLIENT_PORT.to_be_bytes());
        f.extend_from_slice(&((8 + payload.len()) as u16).to_be_bytes());
        f.extend_from_slice(&[0, 0]);
        f.extend_from_slice(payload);
        f
    }
}

#[cfg(test)]
mod quiet_retry_tests {
    use super::*;

    /// ★ **Row 28: one WARN at boot, then quiet retries** (Gunnar c040144f / feff3b82,
    /// estate-box D4 2026-10-01). Each failed attempt of the boot's handshake is INFO (the
    /// boot's failure is said ONCE, at WARN, by the bring-up: "the handshake FAILED at
    /// boot"), and the background retry's attempts are debug (`obtain_with` quiet): four
    /// WARN "DHCP attempt failed" per retry was the spam the D12 retry made, 7 a minute.
    #[test]
    fn a_failed_attempt_is_info_at_boot_and_debug_in_the_retry_never_warn() {
        assert_eq!(attempt_failed_level(false), tracing::Level::INFO);
        assert_eq!(attempt_failed_level(true), tracing::Level::DEBUG);
        let _: fn(&str, [u8; 6], bool) -> Result<Lease> = obtain_with;
    }
}