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

nordisk/edda

crates/estate-front/src/relay.rs

//! **The estate's WireGuard relay** — the relay half of gunnar's
//! `gunnar-pairing-door/src/front.rs` `FrontRole`, moved here as is.
//!
//! On udp/51820 (`nordisk-pairing`'s `RELAY_PORT`), keyed with the FRONT's own
//! tunnel key — the key its presentation to the installer carried, so the key
//! the installer's door feeds every box as the `front` peer — owning the
//! overlay's `.1`, listing each fed box by its overlay /32. It is edda
//! `wg-appliance-core`'s userspace [`HubRelay`](wg_appliance_core::hubrelay::HubRelay):
//! one UDP socket, no TUN, so the front needs no `wg0` of its own.

use std::collections::BTreeSet;
use std::net::Ipv4Addr;

/// **The relay, running.** Owned for the life of the process: dropping it
/// stops the relay.
pub struct Relay {
    hub: wg_appliance_core::hubrelay::HubRelay,
    listed: BTreeSet<String>,
    /// The key (and the PSK, in memory only, as the fed reply itself is) listed at each
    /// overlay address — so a NEW key at a known address is said as the relisting it is.
    at: std::collections::BTreeMap<Ipv4Addr, (String, Option<String>)>,
    /// The name each listed overlay address was fed under, for the line a forget says.
    names: std::collections::BTreeMap<Ipv4Addr, String>,
}

/// The relay's private key from its base64 spelling (the ceremony's `wg0.key`).
pub fn key_from_b64(b64: &str) -> Result<[u8; 32], String> {
    wg_appliance_core::decode_key(b64.trim()).map_err(|e| format!("the front's tunnel key is not a WireGuard key: {}", e.0))
}

impl Relay {
    /// Bind udp/`port` (0 = any, for tests) with `private`, owning `hub_addr`
    /// on the overlay.
    pub fn start(port: u16, private: [u8; 32], hub_addr: Ipv4Addr) -> Result<Relay, String> {
        Relay::start_with_hub(port, private, hub_addr, None)
    }

    /// [`Relay::start`] with the front's OWN overlay interface attached (`hub`, a TUN
    /// owning `hub_addr`): the front then reaches every listed box over the tunnel
    /// itself — what a WireGuard-only estate's front dials its boxes on. `None`: the
    /// relay only, no interface (as [`Relay::start`]).
    pub fn start_with_hub(port: u16, private: [u8; 32], hub_addr: Ipv4Addr, hub: Option<std::sync::Arc<dyn wg_appliance_core::hubrelay::HubPort>>) -> Result<Relay, String> {
        // A32-14, stays a thread: HubRelay::spawn is the WireGuard relay's own UDP loop (wg-appliance-core),
        // owned by this Relay for the front's life and stopped when it drops; it is no job.
        let hub = match hub {
            Some(p) => wg_appliance_core::hubrelay::HubRelay::spawn_with_port(port, private, hub_addr, p),
            None => wg_appliance_core::hubrelay::HubRelay::spawn(port, private, hub_addr),
        }
        .map_err(|e| format!("the relay could not bind udp/{port}: {e}"))?;
        Ok(Relay { hub, listed: BTreeSet::new(), at: std::collections::BTreeMap::new(), names: std::collections::BTreeMap::new() })
    }

    /// The relay's public key (base64) — the front's own tunnel key.
    pub fn public(&self) -> &str {
        &self.hub.public_b64
    }

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

    /// **The relay's counters, one line** (lane D12): per listed box its handshakes and
    /// traffic, then what was REFUSED (an initiation not sealed to this relay's key, or
    /// from a key it does not list) — the relay drops those silently, so this line is the
    /// only place a key mismatch shows.
    pub fn counters(&self) -> String {
        wg_appliance_core::hubrelay::describe(&self.hub.verdict())
    }

    /// **List the fed boxes with their pre-shared keys** (lane G5, A7 row 13; Gunnar 356e8dd2
    /// `FrontRole::take`) — `(name, tunnel key, overlay, PSK)`, what Skidbladnir
    /// nordisk-pairing `frontside::relay_list_with_psk` reads off the front's feed
    /// (`wg_psk.<box>`). The relay keys every session with the box's PSK (WireGuard's
    /// `PresharedKey`, the post-quantum hardening of the X25519 handshake), remembered with
    /// the key: the same feed again (a restart) changes nothing, a new PSK relists the box, and
    /// a fed value that is not a key, or a blank one (lane D14), is said and the box is not listed. Said
    /// "with its pre-shared key", never the key. `None`: listed as [`Relay::take`] always did.
    pub fn take_with_psk(&mut self, boxes: &[(String, String, Ipv4Addr, Option<String>)]) -> Vec<String> {
        let mut said = Vec::new();
        for (name, key, overlay, psk_b64) in boxes {
            let key = key.trim();
            let psk_b64 = psk_b64.as_deref().map(str::trim).map(str::to_string);
            // A key that was fed but is blank is refused (lane D14, decision c1): never read as
            // "no PSK", which would list the box without one, a silent downgrade.
            if psk_b64.as_deref().is_some_and(str::is_empty) {
                said.push(format!("`{name}` was fed a blank pre-shared key — not listed (a link without its key is refused)"));
                continue;
            }
            let memo = (key.to_string(), psk_b64.clone());
            // Listed once per (address, key, PSK): a restart re-feeds the same.
            if key.is_empty() || self.at.get(overlay) == Some(&memo) || (self.listed.contains(key) && wg_appliance_core::decode_key(key).is_err()) {
                continue;
            }
            let psk = match psk_b64.as_deref().map(wg_appliance_core::decode_key) {
                None => None,
                Some(Ok(p)) => Some(p),
                Some(Err(_)) => {
                    said.push(format!("`{name}` was fed a value that is not a WireGuard pre-shared key — not listed with it"));
                    continue;
                }
            };
            let with = if psk.is_some() { " with its pre-shared key" } else { "" };
            match wg_appliance_core::decode_key(key) {
                // A box laid again at its address (the dead main re-admitted,
                // nordisk-pairing `installer::readmit`) brings a NEW key to the SAME
                // overlay address: relisted there, the old key refused from then on.
                // Under its fed NAME (rickard-81; Gunnar lists with `add_named_peer_with_psk`):
                // boringtun's timer lines say `relay:main:<fp>`, never the address.
                Ok(k) => match self.hub.relist_named_peer_with_psk(k, psk, *overlay, name) {
                    Ok(true) if self.at.get(overlay).is_some_and(|(old, _)| old != key) => said.push(format!("`{name}` relisted on the relay at {overlay} with its new key{with} (the old key is refused)")),
                    Ok(true) if self.at.contains_key(overlay) => said.push(format!("`{name}` takes a new pre-shared key on the relay at {overlay}")),
                    Ok(_) => said.push(format!("`{name}` listed on the relay as {overlay}{with}")),
                    Err(e) => said.push(format!("`{name}` was NOT listed on the relay: {e}")),
                },
                Err(_) => said.push(format!("`{name}` was fed a tunnel key that is not a WireGuard key — not listed")),
            }
            self.listed.insert(key.to_string());
            self.at.insert(*overlay, memo);
            self.names.insert(*overlay, name.clone());
        }
        let named: Vec<Ipv4Addr> = boxes.iter().filter(|(_, k, _, _)| !k.trim().is_empty()).map(|(_, _, o, _)| *o).collect();
        said.extend(self.retain(&named));
        said
    }

    /// **Forget every listed box whose overlay address is not in `named`** (A7 row 27,
    /// Gunnar a1fa525f): a box the installer's re-feed no longer names with a key DIED and is
    /// being laid again. Its session is forgotten now (wg-appliance-core
    /// `HubRelay::forget_peer`), not after two minutes of handshakes toward a machine that is
    /// gone; its new key lists it again ([`Relay::take_with_psk`]). [`Relay::take_with_psk`]
    /// calls this with the whole fed list, so a caller hands it the WHOLE feed each time.
    /// Answers what was forgotten, to be said.
    pub fn retain(&mut self, named: &[Ipv4Addr]) -> Vec<String> {
        let gone: Vec<Ipv4Addr> = self.at.keys().filter(|o| !named.contains(o)).copied().collect();
        let mut said = Vec::new();
        for overlay in gone {
            if let Some((key, _)) = self.at.remove(&overlay) {
                self.listed.remove(&key);
            }
            let name = self.names.remove(&overlay).unwrap_or_else(|| overlay.to_string());
            if self.hub.forget_peer(overlay) {
                said.push(format!("`{name}` forgotten on the relay at {overlay}: the installer's feed no longer names it with a key (the box is being laid again)"));
            }
        }
        said
    }

    /// Each listed slot's address and its relay label (`relay:<name>:<fp>`, the name
    /// boringtun's timer lines carry).
    pub fn labels(&self) -> Vec<(Ipv4Addr, String)> {
        self.hub.labels()
    }

    /// The overlay addresses listed with a pre-shared key.
    pub fn with_psk(&self) -> Vec<Ipv4Addr> {
        self.hub.verdict().with_psk
    }

    /// **List the fed boxes** — `(name, tunnel key, overlay)`, each key once (a
    /// restart re-feeds; a box listed before is there either way). Answers what
    /// was done, to be said. [`Relay::take_with_psk`] with no PSK.
    pub fn take(&mut self, boxes: &[(String, String, Ipv4Addr)]) -> Vec<String> {
        let plain: Vec<_> = boxes.iter().map(|(n, k, o)| (n.clone(), k.clone(), *o, None)).collect();
        self.take_with_psk(&plain)
    }
}

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

    /// The relay runs on the front's own key and lists a fed box once; a box
    /// without a key, or with one that is not WireGuard's, is not listed.
    #[test]
    fn the_relay_runs_on_the_fronts_own_key_and_lists_each_fed_box_once() {
        let private = [7u8; 32];
        let public = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&private));
        let mut r = Relay::start(0, private, Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        assert_eq!(r.public(), public);
        assert!(key_from_b64(&public).is_ok());
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
        let boxes = vec![("vm2".to_string(), k, Ipv4Addr::new(10, 44, 0, 2)), ("vm3".to_string(), String::new(), Ipv4Addr::new(10, 44, 0, 3)), ("vm4".to_string(), "nope".to_string(), Ipv4Addr::new(10, 44, 0, 4))];
        let said = r.take(&boxes);
        assert_eq!(said[0], "`vm2` listed on the relay as 10.44.0.2");
        assert!(said[1].contains("`vm4` was fed a tunnel key that is not a WireGuard key"), "{said:?}");
        assert_eq!(said.len(), 2, "no key, not listed: {said:?}");
        assert!(r.take(&boxes).is_empty(), "listed once");
    }

    /// ★ **A box laid again at its address brings a new key, and the relay takes
    /// it** (the dead main re-admitted into its closed estate, nordisk-pairing
    /// `installer::readmit`: same overlay address, a fresh tunnel key). The relay
    /// refused a second key at one address (`add_peer`) and still SAID it listed
    /// it, so the new main was never reached through vm1. Now it is relisted there
    /// (`HubRelay::relist_peer`: the old key refused from then on), said so, once.
    #[test]
    fn a_box_laid_again_at_its_address_is_relisted_with_its_new_key() {
        let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let old = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
        let new = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[9u8; 32]));
        let at = Ipv4Addr::new(10, 44, 0, 2);
        assert_eq!(r.take(&[("vm2".to_string(), old.clone(), at)]), vec!["`vm2` listed on the relay as 10.44.0.2".to_string()]);
        let said = r.take(&[("vm2".to_string(), new.clone(), at)]);
        assert_eq!(said, vec!["`vm2` relisted on the relay at 10.44.0.2 with its new key (the old key is refused)".to_string()], "{said:?}");
        assert!(r.take(&[("vm2".to_string(), new.clone(), at)]).is_empty(), "relisted once");
        // The listed key offered at ANOTHER address is refused by name (one box, one
        // address), never said as listed.
        // (The whole feed: vm2 still named, so it is not forgotten.)
        let back = r.take(&[("vm2".to_string(), new.clone(), at), ("vm9".to_string(), new, Ipv4Addr::new(10, 44, 0, 9))]);
        assert!(back.len() == 1 && back[0].contains("NOT listed"), "{back:?}");
    }

    /// ★ **The relay says its counters per box and what it refused** (lane D12): an
    /// initiation sealed to ANOTHER front key is dropped without a word, so the counters
    /// line must show it as refused; a listed box's own handshake shows as its handshake.
    #[test]
    fn the_relay_counts_handshakes_per_box_and_refuses_a_wrong_front_key_visibly() {
        let front = [7u8; 32];
        let mut r = Relay::start(0, front, Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let boxk = [9u8; 32];
        let box_pub = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&boxk));
        r.take(&[("main".to_string(), box_pub, Ipv4Addr::new(10, 44, 0, 2))]);
        let at: std::net::SocketAddrV4 = format!("127.0.0.1:{}", r.port()).parse().unwrap();
        // A box that seals to a WRONG front key: refused.
        let wrong = wg_appliance_core::keys::public_from_private_bytes(&[5u8; 32]);
        let mut t = boringtun::noise::Tunn::new(boringtun::x25519::StaticSecret::from(boxk), boringtun::x25519::PublicKey::from(wrong), None, None, 1, None);
        let sock = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
        let mut buf = vec![0u8; 2048];
        if let boringtun::noise::TunnResult::WriteToNetwork(p) = t.format_handshake_initiation(&mut buf, false) {
            sock.send_to(p, at).unwrap();
        }
        // The right key: a handshake.
        let mut ok = boringtun::noise::Tunn::new(boringtun::x25519::StaticSecret::from(boxk), boringtun::x25519::PublicKey::from(wg_appliance_core::keys::public_from_private_bytes(&front)), None, None, 2, None);
        if let boringtun::noise::TunnResult::WriteToNetwork(p) = ok.format_handshake_initiation(&mut buf, false) {
            sock.send_to(p, at).unwrap();
        }
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
        let mut line = r.counters();
        while std::time::Instant::now() < deadline && !(line.contains("handshakes 1") && line.contains("refused 1")) {
            std::thread::sleep(std::time::Duration::from_millis(50));
            line = r.counters();
        }
        assert!(line.contains("10.44.0.2") && line.contains("handshakes 1"), "the listed box's handshake: {line}");
        assert!(line.contains("refused 1"), "the wrong-key initiation is counted as refused: {line}");
    }

    /// **The front's own overlay interface rides the relay** (a WireGuard-only estate's
    /// front dials its boxes over the tunnel): the relay starts with the hub port
    /// attached, on the front's own key, and lists boxes as before.
    #[test]
    fn the_relay_starts_with_the_fronts_own_overlay_interface_attached() {
        let port: std::sync::Arc<dyn wg_appliance_core::hubrelay::HubPort> = wg_appliance_core::hubrelay::MemPort::new();
        let mut r = Relay::start_with_hub(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1), Some(port)).unwrap();
        assert_ne!(r.port(), 0);
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
        assert_eq!(r.take(&[("main".to_string(), k, Ipv4Addr::new(10, 44, 0, 2))]), vec!["`main` listed on the relay as 10.44.0.2".to_string()]);
    }

    /// ★ **A7 row 27 (Gunnar a1fa525f): a box the re-feed no longer names with a key is
    /// forgotten on the relay at once, and its new key lists it again.** The main died; the
    /// installer re-fed the front with the twin only (the new main has no key until it pairs).
    /// The relay kept the dead main's session and sealed the twin's every packet to it
    /// (`HANDSHAKE(REKEY_TIMEOUT)` every 5 s, then `ERROR CONNECTION_EXPIRED`, measured 2 min).
    #[test]
    fn a_box_the_refeed_no_longer_names_is_forgotten_and_its_new_key_lists_it_again() {
        let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let key = |seed: u8| wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[seed; 32]));
        let (main_at, twin_at) = (Ipv4Addr::new(10, 44, 0, 2), Ipv4Addr::new(10, 44, 0, 3));
        let twin = ("twin".to_string(), key(3), twin_at);
        assert_eq!(r.take(&[("main".to_string(), key(2), main_at), twin.clone()]).len(), 2);
        // The re-feed after main died: main with no key (being laid again), or not named at all.
        let said = r.take(&[("main".to_string(), String::new(), main_at), twin.clone()]);
        assert!(said.iter().any(|l| l.contains("`main`") && l.contains("forgotten") && l.contains("10.44.0.2")), "{said:?}");
        assert!(r.take(std::slice::from_ref(&twin)).is_empty(), "forgotten once");
        // The new main's key lists it again, at the same slot.
        let said = r.take(&[("main".to_string(), key(4), main_at), twin.clone()]);
        assert!(said.iter().any(|l| l.contains("`main`") && l.contains("10.44.0.2") && !l.contains("forgotten")), "{said:?}");
        // The same key coming back after a forget is listed again too (a feed that dropped it).
        r.take(std::slice::from_ref(&twin));
        let said = r.take(&[("main".to_string(), key(4), main_at), twin]);
        assert!(said.iter().any(|l| l.contains("`main`") && l.contains("10.44.0.2")), "{said:?}");
    }

    /// ★ **Each fed box is listed under its name** (lane G9, rickard-81; Gunnar `FrontRole::take`
    /// lists with `add_named_peer_with_psk`): boringtun's timer lines for a relay peer say
    /// `wg{peer=relay:main:<fp>}`, never the address. A box laid again keeps the fed name.
    #[test]
    fn each_fed_box_is_listed_on_the_relay_under_its_name() {
        let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let key = |seed: u8| wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[seed; 32]));
        let (main_at, twin_at) = (Ipv4Addr::new(10, 44, 0, 2), Ipv4Addr::new(10, 44, 0, 3));
        r.take_with_psk(&[("main".to_string(), key(2), main_at, Some(PSK.to_string())), ("twin".to_string(), key(3), twin_at, None)]);
        let label = |r: &Relay, at: Ipv4Addr| r.labels().into_iter().find(|(o, _)| *o == at).map(|(_, l)| l).unwrap_or_default();
        assert!(label(&r, main_at).starts_with("relay:main:"), "{:?}", r.labels());
        assert!(label(&r, twin_at).starts_with("relay:twin:"), "{:?}", r.labels());
        // Laid again: forgotten, then its new key at the same address, still `main`.
        r.take(&[("twin".to_string(), key(3), twin_at)]);
        r.take(&[("main".to_string(), key(4), main_at), ("twin".to_string(), key(3), twin_at)]);
        assert!(label(&r, main_at).starts_with("relay:main:"), "{:?}", r.labels());
    }

    const PSK: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
    const PSK2: &str = "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=";

    /// ★ **Each box is listed with the pre-shared key the installer fed the front for it**
    /// (lane G5, A7 row 13; Gunnar 356e8dd2 `each_fed_box_is_listed_with_its_preshared_key_and_a_new_one_relists_it`):
    /// said so, never the key. The same feed again changes nothing; a new PSK for the same box
    /// relists it; a fed value that is not a key is said and the box is not listed with it.
    #[test]
    fn each_fed_box_is_listed_with_its_preshared_key_and_a_new_one_relists_it() {
        let mut r = Relay::start(0, [6u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
        let at = Ipv4Addr::new(10, 44, 0, 2);
        let fed = |psk: Option<&str>| vec![("main".to_string(), k.clone(), at, psk.map(str::to_string))];
        let said = r.take_with_psk(&fed(Some(PSK)));
        assert_eq!(said, vec!["`main` listed on the relay as 10.44.0.2 with its pre-shared key".to_string()]);
        assert!(!said.concat().contains(PSK));
        assert_eq!(r.with_psk(), vec![at]);
        assert!(r.take_with_psk(&fed(Some(PSK))).is_empty(), "the same feed: nothing to do");
        let said = r.take_with_psk(&fed(Some(PSK2)));
        assert!(said.iter().any(|l| l.contains("`main`") && l.contains("pre-shared key")) && !said.concat().contains(PSK2), "{said:?}");
        assert_eq!(r.with_psk(), vec![at]);
        let said = r.take_with_psk(&fed(Some("not-a-key")));
        assert!(said.iter().any(|l| l.contains("`main`") && l.contains("not a WireGuard pre-shared key")) && !said.concat().contains("not-a-key"), "{said:?}");
        // A box fed no PSK is listed as before.
        // (The whole feed: main is still named with its last good PSK, so it is not forgotten.)
        let mut whole = fed(Some(PSK2));
        whole.push(("twin".to_string(), wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[9u8; 32])), Ipv4Addr::new(10, 44, 0, 3), None));
        let plain = r.take_with_psk(&whole);
        assert_eq!(plain, vec!["`twin` listed on the relay as 10.44.0.3".to_string()]);
        assert_eq!(r.with_psk(), vec![at], "the twin carries none");
    }

    /// ★ **A blank fed pre-shared key REFUSES the box** (lane D14, decision c1): a `wg_psk.<box>`
    /// that was fed but is empty or whitespace is never read as "no PSK" (a silent downgrade
    /// on a security gateway). The box is not listed and that is said; a box fed NO key at all
    /// (an estate laid before PSKs) is still listed as before.
    #[test]
    fn a_blank_fed_preshared_key_refuses_the_box_instead_of_listing_it_without_one() {
        let mut r = Relay::start(0, [6u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
        for blank in ["", "   "] {
            let said = r.take_with_psk(&[("main".to_string(), k.clone(), Ipv4Addr::new(10, 44, 0, 2), Some(blank.to_string()))]);
            assert!(said.iter().any(|l| l.contains("`main`") && l.contains("blank pre-shared key") && l.contains("not listed")), "{said:?}");
            assert!(r.hub.verdict().peers.is_empty(), "nothing listed for a blank key: {said:?}");
        }
    }
}