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

nordisk/edda

crates/wg-appliance-core/src/hubfixture.rs

//! A **WireGuard hub fixture** — the "other end of the wire" a ceremony proof
//! (or the deploy-test panel) stands up itself: one UDP socket and one
//! responder [`Tunn`], NO tun device, NO root, NO kernel WireGuard.
//!
//! # Why it exists
//!
//! "Configured" is not "connected", and — measured in this suite — a completed
//! HANDSHAKE is not a working TUNNEL either: a sabotaged AllowedIPs router
//! still handshakes while losing every packet. A proof therefore needs a real
//! peer that can say, cryptographically, what actually happened. This fixture
//! is that peer, and its verdict vocabulary is deliberately three-valued:
//!
//! * [`HubVerdict::initiations`] — valid handshake initiations received (the
//!   MAC verified against OUR key; garbage and foreign-key traffic instead
//!   lands in `refused`).
//! * [`HubVerdict::responses`] — handshake responses we sent back.
//! * [`HubVerdict::sealed_packets`] — datagrams AFTER the handshake that
//!   **decrypted under the session keys** (keepalives or data). Only a peer
//!   holding the completed session can produce one, so this — not the
//!   handshake — is the "traffic crossed the tunnel" floor. IP-layer traffic
//!   (a ping THROUGH the tunnel) additionally needs an address on the far
//!   side's interface, which is the suite's named address-writer remainder;
//!   this fixture still counts and exposes any real payload it decrypts.
//!
//! # One dialect, one writer
//!
//! [`HubFixture::hub_conf`] renders the `hub.conf` a consumer should feed the
//! appliance — through the same constants the ceremony parses, and its own
//! test round-trips it through [`crate::ceremony::parse_hub_config`], so the
//! fixture can never drift into a private dialect.

use std::io;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};

use boringtun::noise::handshake::parse_handshake_anon;
use boringtun::noise::{Packet, Tunn, TunnResult};
use boringtun::x25519::{PublicKey, StaticSecret};

use crate::keys::{encode_key, generate_keypair, KEY_LEN};

/// The ICMP identifier every fixture ping carries — how the pump tells its own
/// echo replies from stray decrypted traffic.
const PING_IDENT: u16 = 0xE77A;

/// How long the pump sleeps in `recv_timeout` per pass — also the timer tick.
const TICK: Duration = Duration::from_millis(100);

/// What the hub saw, cryptographically. See the module doc for the vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HubVerdict {
    /// Valid handshake initiations received (MAC verified against our key).
    pub initiations: u64,
    /// Handshake responses sent back.
    pub responses: u64,
    /// Post-handshake datagrams that DECRYPTED under the session — keepalives
    /// or data. The "traffic crossed" floor.
    pub sealed_packets: u64,
    /// ICMP echo REPLIES to this fixture's own pings ([`HubFixture::send_ping`])
    /// that came back THROUGH the tunnel — the full IP-layer proof: the far
    /// kernel routed, answered, and the answer sealed its way home.
    pub echo_replies: u64,
    /// Datagrams boringtun refused — garbage, or a peer keyed to someone else.
    pub refused: u64,
    /// **The appliance this hub is talking to**, when the hub was spawned
    /// OPEN ([`HubFixture::spawn_open`]) and learned it from the first valid
    /// initiation — the static public key that decrypted under OUR private
    /// key. `None` until then, and always `None` for a hub spawned toward a
    /// known key. A proof compares it to the identity the appliance announced,
    /// so an open hub still cannot green-chip the wrong box silently.
    pub learned_peer: Option<[u8; KEY_LEN]>,
}

struct Counters {
    initiations: AtomicU64,
    responses: AtomicU64,
    sealed: AtomicU64,
    echo_replies: AtomicU64,
    refused: AtomicU64,
    /// The learned peer's public key (open hubs), published by the pump.
    learned_peer: std::sync::Mutex<Option<[u8; KEY_LEN]>>,
}

/// Whom the pump's `Tunn` talks to: a peer known at spawn, or one to be
/// LEARNED from the first initiation that decrypts under the hub's key.
enum Peer {
    Known(Tunn),
    /// Not yet: the hub's private key, to parse the anonymous initiation with.
    Open { hub_private: [u8; KEY_LEN], hub_public: [u8; KEY_LEN] },
}

/// A live hub. Dropping it stops the pump thread.
pub struct HubFixture {
    /// The hub's public key, spelled for a `hub.conf` / an operator's screen.
    pub public_b64: String,
    port: u16,
    counters: Arc<Counters>,
    stop: Arc<AtomicBool>,
    pings: mpsc::Sender<(Ipv4Addr, Ipv4Addr, u16)>,
    thread: Option<std::thread::JoinHandle<()>>,
}

/// A hub identity + endpoint reserved BEFORE any appliance exists — what a
/// per-site ISO build needs (the hub's public key and port go into the baked
/// `hub.conf`); activated per appliance via [`HubFixture::spawn_on`].
pub struct HubReservation {
    /// The hub's private key — hand it to [`HubFixture::spawn_on`].
    pub hub_private: [u8; KEY_LEN],
    /// The hub's public key, spelled for a `hub.conf`.
    pub public_b64: String,
    /// The reserved UDP port.
    pub port: u16,
}

/// The ONE hub.conf renderer (the ceremony dialect) — shared by the live
/// fixture and a [`HubReservation`], so the file an ISO bakes and the file a
/// running hub would hand out can never drift.
pub fn render_hub_conf(
    public_b64: &str,
    endpoint_ip: Ipv4Addr,
    port: u16,
    allowed_ips: &str,
    keepalive_secs: u16,
    appliance_address: Option<(Ipv4Addr, u8)>,
) -> String {
    let interface = match appliance_address {
        Some((a, p)) => format!("[Interface]\nAddress = {a}/{p}\n\n"),
        None => String::new(),
    };
    format!(
        "{interface}[Peer]\nPublicKey = {public_b64}\nEndpoint = {endpoint_ip}:{port}\nAllowedIPs = {allowed_ips}\nPersistentKeepalive = {keepalive_secs}\n",
    )
}

impl HubReservation {
    /// Render the `hub.conf` a per-site ISO should bake to reach the hub this
    /// reservation will become — same renderer as the live fixture's.
    pub fn hub_conf(
        &self,
        endpoint_ip: Ipv4Addr,
        allowed_ips: &str,
        keepalive_secs: u16,
        appliance_address: Option<(Ipv4Addr, u8)>,
    ) -> String {
        render_hub_conf(&self.public_b64, endpoint_ip, self.port, allowed_ips, keepalive_secs, appliance_address)
    }
}

impl HubFixture {
    /// Mint a hub keypair, bind an ephemeral UDP port on all interfaces, and
    /// start the responder pump toward ONE expected appliance (identified by
    /// its public key — a fixture that accepted anyone could green-chip the
    /// wrong box).
    pub fn spawn(appliance_public: [u8; KEY_LEN]) -> io::Result<HubFixture> {
        let (hub_private, _hub_public) = generate_keypair();
        Self::spawn_on(0, hub_private, appliance_public)
    }

    /// **The two-phase shape a real fleet needs**: a hub's identity and
    /// endpoint exist BEFORE any appliance is burned — the per-site ISO bakes
    /// the hub's public key and address, and only when a machine first boots
    /// does its OWN key exist to authorize. `reserve()` mints the keypair and
    /// discovers a free port (bind-and-drop; the tiny race is a test
    /// fixture's to accept), so `hub.conf` can be written and the ISO
    /// mastered; [`spawn_on`](Self::spawn_on) then activates the pump for the
    /// appliance key the first boot minted.
    pub fn reserve() -> io::Result<HubReservation> {
        let (hub_private, hub_public) = generate_keypair();
        let probe = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))?;
        let port = probe.local_addr()?.port();
        drop(probe);
        Ok(HubReservation { hub_private, public_b64: encode_key(&hub_public), port })
    }

    /// Activate the responder pump on `port` (0 = ephemeral) with `hub_private`
    /// as the hub's identity, toward ONE expected appliance.
    pub fn spawn_on(
        port: u16,
        hub_private: [u8; KEY_LEN],
        appliance_public: [u8; KEY_LEN],
    ) -> io::Result<HubFixture> {
        let hub_public = crate::keys::public_from_private_bytes(&hub_private);
        let socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port))?;
        let port = socket.local_addr()?.port();
        socket.set_read_timeout(Some(TICK))?;

        let tunn = Tunn::new(
            StaticSecret::from(hub_private),
            PublicKey::from(appliance_public),
            None,
            None,
            0,
            None,
        );
        Self::start(socket, port, hub_public, Peer::Known(tunn))
    }

    /// **The pre-boot shape of a fixture**: a hub whose appliance does not
    /// exist yet. A per-site ISO bakes this hub's public key and endpoint
    /// ([`HubReservation`]), the appliance mints ITS key on first boot, and
    /// the hub learns that key from the first initiation that decrypts under
    /// its own private key (`parse_handshake_anon` — the static-key AEAD is
    /// the cryptographic check; a foreign hub's initiation does not decrypt
    /// and lands in `refused`). The learned key is published in
    /// [`HubVerdict::learned_peer`], so a proof asserts it EQUALS the identity
    /// the appliance printed — an open hub still names whom it talked to.
    /// Exactly one peer is learned; later initiations from other keys are
    /// refused.
    pub fn spawn_open(port: u16, hub_private: [u8; KEY_LEN]) -> io::Result<HubFixture> {
        let hub_public = crate::keys::public_from_private_bytes(&hub_private);
        let socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port))?;
        let port = socket.local_addr()?.port();
        socket.set_read_timeout(Some(TICK))?;
        Self::start(socket, port, hub_public, Peer::Open { hub_private, hub_public })
    }

    fn start(
        socket: UdpSocket,
        port: u16,
        hub_public: [u8; KEY_LEN],
        peer: Peer,
    ) -> io::Result<HubFixture> {
        let counters = Arc::new(Counters {
            initiations: AtomicU64::new(0),
            responses: AtomicU64::new(0),
            sealed: AtomicU64::new(0),
            echo_replies: AtomicU64::new(0),
            refused: AtomicU64::new(0),
            learned_peer: std::sync::Mutex::new(None),
        });
        let stop = Arc::new(AtomicBool::new(false));
        let (ping_tx, ping_rx) = mpsc::channel();

        let t_counters = counters.clone();
        let t_stop = stop.clone();
        // A32-14, stays a thread: the test hub's pump, a fixture (feature hubfixture), never in a payload.
        let thread = std::thread::Builder::new()
            .name("wg-hub-fixture".into())
            .spawn(move || pump(socket, peer, t_counters, t_stop, ping_rx))?;

        Ok(HubFixture {
            public_b64: encode_key(&hub_public),
            port,
            counters,
            stop,
            pings: ping_tx,
            thread: Some(thread),
        })
    }

    /// Queue an ICMP echo request `src → dst` INTO the tunnel (sealed under
    /// the session by the pump). `src` should be the hub's own tunnel address
    /// — the far kernel's reply dials it back, and the appliance's AllowedIPs
    /// must cover it. The reply is counted in [`HubVerdict::echo_replies`].
    pub fn send_ping(&self, src: Ipv4Addr, dst: Ipv4Addr, seq: u16) {
        let _ = self.pings.send((src, dst, seq));
    }

    /// Block until an echo reply came back through the tunnel, or the
    /// deadline. Returns the verdict either way.
    pub fn wait_for_echo_reply(&self, timeout: Duration) -> HubVerdict {
        let deadline = Instant::now() + timeout;
        loop {
            let v = self.verdict();
            if v.echo_replies > 0 || Instant::now() >= deadline {
                return v;
            }
            std::thread::sleep(TICK);
        }
    }

    /// The UDP port the hub listens on.
    pub fn port(&self) -> u16 {
        self.port
    }

    /// Render the `hub.conf` an appliance should be handed to reach THIS hub —
    /// the ceremony's own dialect, one writer (round-tripped through
    /// `parse_hub_config` in this module's tests). `endpoint_ip` is the
    /// address the APPLIANCE reaches the hub at (10.0.2.2 from a
    /// slirp4netns'd container, the host's LAN address from metal, …).
    /// `appliance_address` — the site-assigned `[Interface] Address`, with its
    /// REAL prefix (the far kernel's connected route is what answers pings).
    pub fn hub_conf(
        &self,
        endpoint_ip: Ipv4Addr,
        allowed_ips: &str,
        keepalive_secs: u16,
        appliance_address: Option<(Ipv4Addr, u8)>,
    ) -> String {
        render_hub_conf(&self.public_b64, endpoint_ip, self.port, allowed_ips, keepalive_secs, appliance_address)
    }

    /// The current verdict.
    pub fn verdict(&self) -> HubVerdict {
        HubVerdict {
            initiations: self.counters.initiations.load(Ordering::Relaxed),
            responses: self.counters.responses.load(Ordering::Relaxed),
            sealed_packets: self.counters.sealed.load(Ordering::Relaxed),
            echo_replies: self.counters.echo_replies.load(Ordering::Relaxed),
            refused: self.counters.refused.load(Ordering::Relaxed),
            learned_peer: *self.counters.learned_peer.lock().unwrap_or_else(|e| e.into_inner()),
        }
    }

    /// Block until at least one valid initiation arrived (the hub answered a
    /// real appliance), or the deadline. Returns the verdict either way.
    pub fn wait_for_initiation(&self, timeout: Duration) -> HubVerdict {
        let deadline = Instant::now() + timeout;
        loop {
            let v = self.verdict();
            if v.initiations > 0 || Instant::now() >= deadline {
                return v;
            }
            std::thread::sleep(TICK);
        }
    }

    /// Block until at least one post-handshake packet DECRYPTED (the
    /// traffic-crossed floor), or the deadline passes. Returns the verdict
    /// either way — the caller asserts, this only waits.
    pub fn wait_for_sealed(&self, timeout: Duration) -> HubVerdict {
        let deadline = Instant::now() + timeout;
        loop {
            let v = self.verdict();
            if v.sealed_packets > 0 || Instant::now() >= deadline {
                return v;
            }
            std::thread::sleep(TICK);
        }
    }

    /// Stop the pump. Also runs on drop.
    pub fn stop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
    }
}

impl Drop for HubFixture {
    fn drop(&mut self) {
        self.stop();
    }
}

/// The responder pump: one socket, one `Tunn`, counters instead of a tun.
fn pump(
    socket: UdpSocket,
    mut peer: Peer,
    counters: Arc<Counters>,
    stop: Arc<AtomicBool>,
    pings: mpsc::Receiver<(Ipv4Addr, Ipv4Addr, u16)>,
) {
    let mut inbuf = vec![0u8; 65536];
    let mut outbuf = vec![0u8; 65536];
    let mut last_src: Option<SocketAddr> = None;

    while !stop.load(Ordering::Relaxed) {
        // Queued pings → sealed into the tunnel. Only meaningful once a
        // session exists; encapsulate before that queues inside boringtun and
        // flushes with the handshake, which is fine too. An OPEN hub with no
        // peer yet has nothing to seal under; the ping is dropped.
        while let Ok((src, dst, seq)) = pings.try_recv() {
            let Peer::Known(tunn) = &mut peer else { continue };
            let packet = craft_icmp_echo_request(src, dst, PING_IDENT, seq);
            if let TunnResult::WriteToNetwork(p) = tunn.encapsulate(&packet, &mut outbuf) {
                if let Some(to) = last_src {
                    let _ = socket.send_to(p, to);
                }
            }
        }

        match socket.recv_from(&mut inbuf) {
            Ok((n, src)) => {
                // WireGuard message type 1 = handshake initiation — recorded
                // BEFORE decapsulation so the counter distinguishes "a valid
                // initiation we answered" from "sealed traffic" below.
                let is_initiation = n >= 4 && inbuf[0] == 1;
                // An OPEN hub learns its peer here: the initiation's encrypted
                // static must decrypt under OUR key, and the key it reveals
                // becomes the one peer this hub talks to. Anything else before
                // a peer is known is refused — counted, not silent.
                if let Peer::Open { hub_private, hub_public } = &peer {
                    let learned = match Tunn::parse_incoming_packet(&inbuf[..n]) {
                        Ok(Packet::HandshakeInit(init)) => parse_handshake_anon(
                            &StaticSecret::from(*hub_private),
                            &PublicKey::from(*hub_public),
                            &init,
                        )
                        .ok()
                        .map(|half| half.peer_static_public),
                        _ => None,
                    };
                    match learned {
                        Some(appliance_public) => {
                            let tunn = Tunn::new(
                                StaticSecret::from(*hub_private),
                                PublicKey::from(appliance_public),
                                None,
                                None,
                                0,
                                None,
                            );
                            *counters.learned_peer.lock().unwrap_or_else(|e| e.into_inner()) =
                                Some(appliance_public);
                            peer = Peer::Known(tunn);
                        }
                        None => {
                            counters.refused.fetch_add(1, Ordering::Relaxed);
                            continue;
                        }
                    }
                }
                let Peer::Known(tunn) = &mut peer else { unreachable!("a peer is known here") };
                let mut accepted = false;
                let mut responded = false;
                // The feed idiom from the data plane: hand the datagram over,
                // then flush with empty datagrams until Done.
                let mut first = true;
                loop {
                    let r = tunn.decapsulate(
                        if first { Some(src.ip()) } else { None },
                        if first { &inbuf[..n] } else { &[] },
                        &mut outbuf,
                    );
                    first = false;
                    match r {
                        TunnResult::WriteToNetwork(packet) => {
                            accepted = true;
                            responded = true;
                            let _ = socket.send_to(packet, src);
                        }
                        // Decrypted payload. A fixture has no tun to write it
                        // to; counting it IS the point — and an echo REPLY to
                        // our own ping is the full IP-through-the-tunnel rung.
                        TunnResult::WriteToTunnelV4(packet, _) => {
                            accepted = true;
                            counters.sealed.fetch_add(1, Ordering::Relaxed);
                            if is_icmp_echo_reply_to(packet, PING_IDENT) {
                                counters.echo_replies.fetch_add(1, Ordering::Relaxed);
                            }
                        }
                        TunnResult::WriteToTunnelV6(..) => {
                            accepted = true;
                            counters.sealed.fetch_add(1, Ordering::Relaxed);
                        }
                        TunnResult::Done => {
                            accepted = true;
                            break;
                        }
                        TunnResult::Err(_) => {
                            if !accepted {
                                counters.refused.fetch_add(1, Ordering::Relaxed);
                            }
                            break;
                        }
                    }
                }
                if accepted {
                    last_src = Some(src);
                    if is_initiation {
                        counters.initiations.fetch_add(1, Ordering::Relaxed);
                        if responded {
                            counters.responses.fetch_add(1, Ordering::Relaxed);
                        }
                    } else {
                        // A non-initiation datagram boringtun accepted: it
                        // decrypted under the session (keepalive or data) —
                        // the WriteToTunnel arm above already counted real
                        // payload; a keepalive surfaces as Done, counted here.
                        counters.sealed.fetch_add(1, Ordering::Relaxed);
                    }
                }
            }
            Err(e)
                if e.kind() == io::ErrorKind::WouldBlock
                    || e.kind() == io::ErrorKind::TimedOut => {}
            Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
            Err(_) => break,
        }

        // Timers each pass — the responder side rekeys and expires too.
        if let Peer::Known(tunn) = &mut peer {
            if let TunnResult::WriteToNetwork(packet) = tunn.update_timers(&mut outbuf) {
                if let Some(dst) = last_src {
                    let _ = socket.send_to(packet, dst);
                }
            }
        }
    }
}

/// One's-complement checksum over 16-bit words (RFC 1071) — IPv4 header and
/// ICMP both use it.
fn inet_checksum(data: &[u8]) -> u16 {
    let mut sum: u32 = 0;
    for chunk in data.chunks(2) {
        let word = if chunk.len() == 2 {
            u16::from_be_bytes([chunk[0], chunk[1]])
        } else {
            u16::from_be_bytes([chunk[0], 0])
        };
        sum += u32::from(word);
    }
    while sum >> 16 != 0 {
        sum = (sum & 0xffff) + (sum >> 16);
    }
    !(sum as u16)
}

/// A minimal IPv4 + ICMP echo-request packet, checksums computed — what the
/// fixture seals into the tunnel as its ping.
pub fn craft_icmp_echo_request(src: Ipv4Addr, dst: Ipv4Addr, ident: u16, seq: u16) -> Vec<u8> {
    let payload = b"wg-appliance-hub-fixture";
    let icmp_len = 8 + payload.len();
    let total_len = 20 + icmp_len;
    let mut p = vec![0u8; total_len];
    // IPv4 header.
    p[0] = 0x45; // version 4, IHL 5
    p[2..4].copy_from_slice(&(total_len as u16).to_be_bytes());
    p[8] = 64; // TTL
    p[9] = 1; // protocol: ICMP
    p[12..16].copy_from_slice(&src.octets());
    p[16..20].copy_from_slice(&dst.octets());
    let hcs = inet_checksum(&p[..20]);
    p[10..12].copy_from_slice(&hcs.to_be_bytes());
    // ICMP echo request.
    p[20] = 8; // type
    p[24..26].copy_from_slice(&ident.to_be_bytes());
    p[26..28].copy_from_slice(&seq.to_be_bytes());
    p[28..].copy_from_slice(payload);
    let ics = inet_checksum(&p[20..]);
    p[22..24].copy_from_slice(&ics.to_be_bytes());
    p
}

/// Is `packet` an IPv4 ICMP echo REPLY carrying `ident`?
fn is_icmp_echo_reply_to(packet: &[u8], ident: u16) -> bool {
    if packet.len() < 28 || packet[0] >> 4 != 4 || packet[9] != 1 {
        return false;
    }
    let ihl = usize::from(packet[0] & 0x0f) * 4;
    let icmp = &packet[ihl..];
    icmp.len() >= 8
        && icmp[0] == 0 // echo reply
        && u16::from_be_bytes([icmp[4], icmp[5]]) == ident
}

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

    /// Drive a MANUAL client `Tunn` over real localhost UDP — no tun device,
    /// no root: initiation out, response decapsulated, then a keepalive sealed
    /// under the session. The fixture's verdict must climb every rung.
    #[test]
    fn a_real_handshake_and_a_sealed_keepalive_reach_the_verdict() {
        let (app_private, app_public) = generate_keypair();
        let hub = HubFixture::spawn(app_public).unwrap();

        let mut client = Tunn::new(
            StaticSecret::from(app_private),
            PublicKey::from(crate::keys::decode_key(&hub.public_b64).unwrap()),
            None,
            None,
            0,
            None,
        );
        let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
        sock.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
        let hub_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, hub.port());

        // Initiation → hub.
        let mut buf = [0u8; 2048];
        let init = match client.format_handshake_initiation(&mut buf, false) {
            TunnResult::WriteToNetwork(p) => p.to_vec(),
            other => panic!("no initiation: {other:?}"),
        };
        sock.send_to(&init, hub_addr).unwrap();

        // Response ← hub; feed it through the client to complete the session.
        let mut resp = [0u8; 2048];
        let (n, _) = sock.recv_from(&mut resp).expect("the hub answers an initiation");
        let mut out = [0u8; 2048];
        match client.decapsulate(None, &resp[..n], &mut out) {
            // Completing the handshake makes the client emit its first
            // post-handshake packet (the queue flush / keepalive) — send it.
            TunnResult::WriteToNetwork(p) => {
                sock.send_to(p, hub_addr).unwrap();
            }
            TunnResult::Done => {
                // No queued packet: seal one explicitly (an empty payload is
                // a keepalive).
                if let TunnResult::WriteToNetwork(p) = client.encapsulate(&[], &mut buf) {
                    sock.send_to(p, hub_addr).unwrap();
                }
            }
            other => panic!("the response did not complete the handshake: {other:?}"),
        }

        let v = hub.wait_for_sealed(Duration::from_secs(5));
        assert!(v.initiations >= 1, "the hub saw the initiation: {v:?}");
        assert!(v.responses >= 1, "the hub answered it: {v:?}");
        assert!(
            v.sealed_packets >= 1,
            "the traffic-crossed floor: a packet DECRYPTED under the session: {v:?}"
        );
        assert_eq!(v.refused, 0, "nothing was refused in the green run: {v:?}");

        // ── the full IP rung, hand-driven: fixture pings; this "kernel" (the
        // test) decapsulates the request, crafts the reply, seals it back;
        // the fixture must count exactly the reply — not the request, not a
        // keepalive.
        hub.send_ping("10.77.0.1".parse().unwrap(), "10.77.0.7".parse().unwrap(), 1);
        let mut request = None;
        let deadline = Instant::now() + Duration::from_secs(5);
        while request.is_none() && Instant::now() < deadline {
            let mut wire = [0u8; 2048];
            let Ok((n, _)) = sock.recv_from(&mut wire) else { continue };
            let mut plain = [0u8; 2048];
            match client.decapsulate(None, &wire[..n], &mut plain) {
                TunnResult::WriteToTunnelV4(p, _) if p.len() >= 28 && p[20] == 8 => {
                    request = Some(p.to_vec());
                }
                TunnResult::WriteToNetwork(p) => {
                    sock.send_to(p, hub_addr).unwrap();
                }
                _ => {}
            }
        }
        let request = request.expect("the fixture's echo request reached the far side");
        // Reply: swap src/dst, type 0, recompute both checksums.
        let mut reply = request.clone();
        let (src, dst) = (reply[12..16].to_vec(), reply[16..20].to_vec());
        reply[12..16].copy_from_slice(&dst);
        reply[16..20].copy_from_slice(&src);
        reply[10] = 0;
        reply[11] = 0;
        let hcs = inet_checksum(&reply[..20]);
        reply[10..12].copy_from_slice(&hcs.to_be_bytes());
        reply[20] = 0; // echo reply
        reply[22] = 0;
        reply[23] = 0;
        let ics = inet_checksum(&reply[20..]);
        reply[22..24].copy_from_slice(&ics.to_be_bytes());
        let mut sealed = [0u8; 2048];
        if let TunnResult::WriteToNetwork(p) = client.encapsulate(&reply, &mut sealed) {
            sock.send_to(p, hub_addr).unwrap();
        }
        let v = hub.wait_for_echo_reply(Duration::from_secs(5));
        assert!(v.echo_replies >= 1, "the reply crossed back and was recognized: {v:?}");
    }

    /// The ICMP craft/parse pair agree with themselves and with the shapes
    /// they must reject — the controls that keep `echo_replies` honest.
    #[test]
    fn icmp_craft_and_recognition_agree() {
        let req = craft_icmp_echo_request(
            "10.77.0.1".parse().unwrap(),
            "10.77.0.7".parse().unwrap(),
            PING_IDENT,
            7,
        );
        assert_eq!(inet_checksum(&req[..20]), 0, "a valid IPv4 header sums to zero");
        assert_eq!(inet_checksum(&req[20..]), 0, "a valid ICMP body sums to zero");
        assert!(!is_icmp_echo_reply_to(&req, PING_IDENT), "a REQUEST is not a reply");
        let mut reply = req.clone();
        reply[20] = 0;
        assert!(is_icmp_echo_reply_to(&reply, PING_IDENT));
        assert!(!is_icmp_echo_reply_to(&reply, 0x1234), "a foreign ident is not ours");
        assert!(!is_icmp_echo_reply_to(&reply[..10], PING_IDENT), "a runt is not a reply");
    }

    /// RED — an appliance keyed to the WRONG hub never completes: the fixture
    /// refuses its initiation (the MAC is keyed to the hub's real public key),
    /// and nothing seals. The green test above is this test's control.
    #[test]
    fn a_client_aimed_at_the_wrong_hub_key_is_refused_and_nothing_seals() {
        let (app_private, app_public) = generate_keypair();
        let (_, wrong_hub_public) = generate_keypair();
        let hub = HubFixture::spawn(app_public).unwrap();

        let mut client = Tunn::new(
            StaticSecret::from(app_private),
            PublicKey::from(wrong_hub_public),
            None,
            None,
            0,
            None,
        );
        let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
        sock.set_read_timeout(Some(Duration::from_millis(500))).unwrap();
        let hub_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, hub.port());

        let mut buf = [0u8; 2048];
        if let TunnResult::WriteToNetwork(p) = client.format_handshake_initiation(&mut buf, false) {
            sock.send_to(p, hub_addr).unwrap();
        }
        let mut resp = [0u8; 2048];
        assert!(
            sock.recv_from(&mut resp).is_err(),
            "the hub must not answer an initiation keyed to someone else"
        );
        let v = hub.wait_for_sealed(Duration::from_millis(700));
        assert_eq!(v.initiations, 0, "{v:?}");
        assert_eq!(v.sealed_packets, 0, "{v:?}");
        assert!(v.refused >= 1, "the refusal is COUNTED, not silent: {v:?}");
    }

    /// An OPEN hub (no appliance key at spawn — the pre-boot fixture shape)
    /// learns the peer from the first initiation that decrypts under its
    /// key, answers it, seals, and NAMES the learned key; a foreign-keyed
    /// initiation before that is refused and learns nothing (the red).
    #[test]
    fn an_open_hub_learns_the_peer_from_the_first_valid_initiation_and_names_it() {
        let (hub_private, _) = generate_keypair();
        let hub = HubFixture::spawn_open(0, hub_private).unwrap();
        let hub_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, hub.port());
        let hub_public = crate::keys::decode_key(&hub.public_b64).unwrap();

        // RED first: an initiation aimed at a DIFFERENT hub key does not
        // decrypt under ours — refused, nothing learned.
        let (stranger_private, _) = generate_keypair();
        let (_, other_hub_public) = generate_keypair();
        let mut stranger = Tunn::new(
            StaticSecret::from(stranger_private),
            PublicKey::from(other_hub_public),
            None,
            None,
            0,
            None,
        );
        let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
        sock.set_read_timeout(Some(Duration::from_millis(500))).unwrap();
        let mut buf = [0u8; 2048];
        if let TunnResult::WriteToNetwork(p) = stranger.format_handshake_initiation(&mut buf, false) {
            sock.send_to(p, hub_addr).unwrap();
        }
        let mut resp = [0u8; 2048];
        assert!(sock.recv_from(&mut resp).is_err(), "a foreign-keyed initiation is not answered");
        let v = hub.wait_for_initiation(Duration::from_millis(700));
        assert_eq!(v.initiations, 0, "{v:?}");
        assert_eq!(v.learned_peer, None, "nothing learned from a stranger: {v:?}");
        assert!(v.refused >= 1, "and the refusal is counted: {v:?}");

        // GREEN: the real appliance, keyed to THIS hub, minted after the hub.
        let (app_private, app_public) = generate_keypair();
        let mut client = Tunn::new(
            StaticSecret::from(app_private),
            PublicKey::from(hub_public),
            None,
            None,
            0,
            None,
        );
        sock.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
        let init = match client.format_handshake_initiation(&mut buf, false) {
            TunnResult::WriteToNetwork(p) => p.to_vec(),
            other => panic!("no initiation: {other:?}"),
        };
        sock.send_to(&init, hub_addr).unwrap();
        let (n, _) = sock.recv_from(&mut resp).expect("the open hub answers the initiation");
        let mut out = [0u8; 2048];
        match client.decapsulate(None, &resp[..n], &mut out) {
            TunnResult::WriteToNetwork(p) => {
                sock.send_to(p, hub_addr).unwrap();
            }
            TunnResult::Done => {
                if let TunnResult::WriteToNetwork(p) = client.encapsulate(&[], &mut buf) {
                    sock.send_to(p, hub_addr).unwrap();
                }
            }
            other => panic!("the response did not complete the handshake: {other:?}"),
        }
        let v = hub.wait_for_sealed(Duration::from_secs(5));
        assert!(v.initiations >= 1 && v.responses >= 1, "{v:?}");
        assert!(v.sealed_packets >= 1, "traffic crossed: {v:?}");
        assert_eq!(
            v.learned_peer,
            Some(app_public),
            "the hub NAMES the appliance it learned — the proof compares this to the identity line"
        );
    }

    /// The rendered hub.conf is the ceremony's own dialect — round-tripped
    /// through the shared parser, so the fixture cannot drift into a private
    /// spelling.
    #[test]
    fn the_rendered_hub_conf_round_trips_through_the_ceremony_parser() {
        let (_, app_public) = generate_keypair();
        let hub = HubFixture::spawn(app_public).unwrap();
        let addr = Some(("10.77.0.7".parse().unwrap(), 16));
        let text = hub.hub_conf("10.0.2.2".parse().unwrap(), "10.77.0.0/16", 1, addr);
        let peer = parse_hub_config("fixture", &text).unwrap();
        assert_eq!(encode_key(&peer.public_key), hub.public_b64);
        assert_eq!(
            peer.endpoint,
            Some(SocketAddrV4::new("10.0.2.2".parse().unwrap(), hub.port()))
        );
        assert_eq!(peer.allowed_ips, vec![("10.77.0.0".parse().unwrap(), 16)]);
        assert_eq!(peer.persistent_keepalive, Some(1));
        assert_eq!(peer.appliance_address, addr, "the [Interface] Address rode along");
        // And the address-free form stays legal.
        let bare = hub.hub_conf("10.0.2.2".parse().unwrap(), "10.77.0.0/16", 1, None);
        assert_eq!(parse_hub_config("fixture", &bare).unwrap().appliance_address, None);
    }
}