nordisk-pairing 0.5.2

The boxes of an estate find each other: a seed each box is installed with, a one-time join against the front, a tunnel (WireGuard by default) they meet inside, roles decided once, a pairing loop each box runs itself, and the signal it reports. Shared by gunnar and holger; the laptop's xtask only lays machines, writes seeds and reads the signal. Design: .nornir/pairing-design.md (2026-09-24).
Documentation
//! **The WireGuard driver — on the suite's ONE userspace WireGuard.**
//!
//! `wg-appliance-core` (this workspace) already holds the x25519 keys, the
//! boringtun packet pump (`dataplane::spawn`), the TUN device and the
//! per-peer handshake accounting; tunnr's wg-appliance PID 1 and gunnar's
//! OS-less appliance run on it. This driver only SHAPES an estate into its
//! `TunnelSpec`: every peer the join reply named becomes a `PeerSpec` whose
//! `allowed_ips` is that peer's overlay address alone, the front is the one
//! peer with an endpoint (the others dial it and it learns theirs from the
//! handshake), and a keepalive keeps NATs open.
//!
//! **Mullvad is this data plane with another peer set.** `tunnr-mullvad`
//! (`tunnel_for_relay`) produces a `TunnelSpec` for the same pump;
//! [`WireGuard::from_spec`] takes it. The Mullvad DRIVER (`crate::mullvad`,
//! feature `mullvad`) holds one of these for the estate mesh and a second,
//! from a spec, for the exit. tunnr-mullvad depends on wg-appliance-core and
//! never on this crate, so there is no cycle.
//!
//! **Tailscale has no Rust component in the suite** — see `crate::tailscale`.
//!
//! Bringing a TUN up needs CAP_NET_ADMIN, so the tests here cover the SHAPE
//! (keys, peers, endpoints, allowed-ips) and the draupnir fleet test covers
//! the wire.

use crate::join::Peer;
use crate::seed::TunnelKind;
use crate::tunnel::{Identity, Tunnel, TunnelError, TunnelStatus};
use crate::Role;
use std::net::{Ipv4Addr, SocketAddrV4};
use wg_appliance_core::{dataplane, decode_key, encode_key, generate_keypair, PeerSpec, TunDevice, TunnelHandle, TunnelSpec, KEY_LEN};

/// The interface name every box uses; the port the FRONT listens on (the
/// others let the kernel pick, they only dial).
pub const IFACE: &str = "wg0";
pub const FRONT_PORT: u16 = 51820;
pub const MTU: u32 = 1420;
pub const KEEPALIVE_S: u16 = 25;

pub struct WireGuard {
    private: [u8; KEY_LEN],
    public_b64: String,
    listen_port: Option<u16>,
    overlay: Option<Ipv4Addr>,
    peers: Vec<Peer>,
    /// A spec handed in whole (Mullvad), used instead of shaping `peers`.
    given: Option<TunnelSpec>,
    handle: Option<TunnelHandle>,
}

impl WireGuard {
    /// A fresh identity. The caller persists the private key (`private_key`)
    /// so the box keeps its identity across reboots; peers pin the public one.
    pub fn mint(listen_port: Option<u16>) -> WireGuard {
        let (private, public) = generate_keypair();
        WireGuard { private, public_b64: encode_key(&public), listen_port, overlay: None, peers: Vec::new(), given: None, handle: None }
    }

    /// The identity a box kept from its first boot.
    pub fn with_key(private: [u8; KEY_LEN], listen_port: Option<u16>) -> WireGuard {
        let public = wg_appliance_core::keys::public_from_private_bytes(&private);
        WireGuard { private, public_b64: encode_key(&public), listen_port, overlay: None, peers: Vec::new(), given: None, handle: None }
    }

    /// A spec somebody else shaped — tunnr-mullvad's `tunnel_for_relay`.
    pub fn from_spec(spec: TunnelSpec) -> WireGuard {
        let public = wg_appliance_core::keys::public_from_private_bytes(&spec.private_key);
        WireGuard { private: spec.private_key, public_b64: encode_key(&public), listen_port: spec.listen_port, overlay: None, peers: Vec::new(), given: Some(spec), handle: None }
    }

    pub fn private_key(&self) -> &[u8; KEY_LEN] {
        &self.private
    }

    pub fn public_key(&self) -> &str {
        &self.public_b64
    }

    pub fn identity(&self, overlay: Ipv4Addr) -> Identity {
        Identity { overlay, pubkey: self.public_b64.clone() }
    }

    /// **The estate as a `TunnelSpec`.** One `PeerSpec` per peer that is not
    /// this box, allowed only its own overlay /32; the front's endpoint is
    /// dialled, every other peer is learned from its handshake.
    pub fn spec(&self) -> Result<TunnelSpec, TunnelError> {
        if let Some(g) = &self.given {
            return Ok(TunnelSpec { private_key: g.private_key, listen_port: g.listen_port, peers: g.peers.clone() });
        }
        let mut peers = Vec::new();
        for p in &self.peers {
            if p.tunnel_pubkey == self.public_b64 {
                continue;
            }
            let public_key = decode_key(&p.tunnel_pubkey).map_err(|e| TunnelError::Failed(format!("peer `{}`: {}", p.name, e.0)))?;
            let endpoint = match (&p.endpoint, p.role) {
                (Some(e), _) => Some(e.parse::<SocketAddrV4>().map_err(|_| TunnelError::Failed(format!("peer `{}`: endpoint `{e}` is not ipv4:port", p.name)))?),
                (None, Role::Front) => return Err(TunnelError::Failed(format!("peer `{}` is the front and names no endpoint; the front is the one box everybody dials", p.name))),
                (None, _) => None,
            };
            peers.push(PeerSpec {
                public_key,
                preshared_key: None,
                endpoint,
                allowed_ips: vec![(p.overlay, 32)],
                persistent_keepalive: Some(KEEPALIVE_S),
            });
        }
        if peers.is_empty() {
            return Err(TunnelError::Failed("no peers: a tunnel with nobody in it is not up".into()));
        }
        Ok(TunnelSpec { private_key: self.private, listen_port: self.listen_port, peers })
    }

    fn respawn(&mut self) -> Result<(), TunnelError> {
        let spec = self.spec()?;
        if let Some(mut h) = self.handle.take() {
            h.shutdown();
        }
        let tun = TunDevice::create(IFACE, MTU).map_err(|e| TunnelError::Failed(format!("{IFACE}: {e} (CAP_NET_ADMIN?)")))?;
        let handle = dataplane::spawn(tun, &spec).map_err(|e| TunnelError::Failed(format!("{IFACE}: {e}")))?;
        self.handle = Some(handle);
        Ok(())
    }
}

impl Tunnel for WireGuard {
    fn kind(&self) -> TunnelKind {
        TunnelKind::Wireguard
    }

    fn public_key(&self) -> String {
        self.public_b64.clone()
    }

    /// Idempotent: a tunnel that is up with these peers stays up.
    fn up(&mut self, me: &Identity, peers: &[Peer]) -> Result<(), TunnelError> {
        if me.pubkey != self.public_b64 {
            return Err(TunnelError::Failed("the identity handed in is not this driver's key".into()));
        }
        self.overlay = Some(me.overlay);
        if self.handle.is_some() && self.peers == peers {
            return Ok(());
        }
        self.peers = peers.to_vec();
        self.respawn()
    }

    /// A peer learned later: the spec is reshaped and the pump restarted
    /// (boringtun's pump takes its peer set at spawn; a restart is a few
    /// milliseconds and the handshakes come back on their own).
    fn add_peer(&mut self, peer: &Peer) -> Result<(), TunnelError> {
        if self.peers.iter().any(|p| p.name == peer.name && p.tunnel_pubkey == peer.tunnel_pubkey) {
            return Ok(());
        }
        self.peers.retain(|p| p.name != peer.name);
        self.peers.push(peer.clone());
        if self.handle.is_some() {
            self.respawn()?;
        }
        Ok(())
    }

    fn status(&self) -> TunnelStatus {
        let up = self.handle.is_some();
        let reachable = match &self.handle {
            Some(h) => {
                let stats = h.peers();
                self.peers
                    .iter()
                    .filter(|p| p.tunnel_pubkey != self.public_b64)
                    .enumerate()
                    .filter(|(i, _)| stats.get(*i).is_some_and(|s| s.handshaken()))
                    .map(|(_, p)| p.name.clone())
                    .collect()
            }
            None => Vec::new(),
        };
        TunnelStatus {
            kind: TunnelKind::Wireguard,
            up,
            reachable,
            detail: format!("{IFACE} · {} peer(s) · {}", self.peers.len(), self.overlay.map(|o| o.to_string()).unwrap_or_else(|| "no address yet".into())),
        }
    }
}

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

    fn peer(name: &str, role: Role, overlay: Ipv4Addr, endpoint: Option<&str>) -> Peer {
        let (_, public) = generate_keypair();
        Peer { name: name.into(), role, tunnel_pubkey: encode_key(&public), overlay, endpoint: endpoint.map(str::to_string), utility: None }
    }

    #[test]
    fn the_estate_is_shaped_into_a_spec_with_the_front_dialled_and_the_others_learned() {
        let mut wg = WireGuard::mint(None);
        let front = peer("front", Role::Front, crate::OVERLAY_FRONT, Some("95.111.211.189:51820"));
        let other = peer("twin", Role::Twin, crate::OVERLAY_BOX_B, None);
        let me = Peer { name: "main".into(), role: Role::Primary, tunnel_pubkey: wg.public_key().to_string(), overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None };
        wg.peers = vec![front.clone(), me, other.clone()];
        let spec = wg.spec().unwrap();
        assert_eq!(spec.peers.len(), 2, "this box is not its own peer");
        assert_eq!(spec.peers[0].endpoint, Some("95.111.211.189:51820".parse().unwrap()));
        assert_eq!(spec.peers[0].allowed_ips, vec![(crate::OVERLAY_FRONT, 32)]);
        assert_eq!(spec.peers[1].endpoint, None, "a box behind NAT is learned from its handshake");
        assert_eq!(spec.peers[1].allowed_ips, vec![(crate::OVERLAY_BOX_B, 32)]);
        assert_eq!(spec.peers[0].persistent_keepalive, Some(KEEPALIVE_S));
        assert_eq!(&spec.private_key, wg.private_key());
    }

    #[test]
    fn a_front_without_an_endpoint_a_bad_key_and_no_peers_are_refused_by_name() {
        let mut wg = WireGuard::mint(Some(FRONT_PORT));
        wg.peers = vec![peer("front", Role::Front, crate::OVERLAY_FRONT, None)];
        assert!(wg.spec().unwrap_err().to_string().contains("names no endpoint"));
        let mut bad = peer("twin", Role::Twin, crate::OVERLAY_BOX_B, None);
        bad.tunnel_pubkey = "not-a-key".into();
        wg.peers = vec![bad];
        assert!(wg.spec().unwrap_err().to_string().contains("peer `twin`"));
        wg.peers.clear();
        assert!(wg.spec().unwrap_err().to_string().contains("no peers"));
        let s = wg.status();
        assert!(!s.up && s.reachable.is_empty());
    }

    #[test]
    fn with_key_reproduces_the_same_public_key_and_from_spec_keeps_a_given_spec() {
        let a = WireGuard::mint(None);
        let b = WireGuard::with_key(*a.private_key(), None);
        assert_eq!(a.public_key(), b.public_key());
        let (_, public) = generate_keypair();
        let given = TunnelSpec {
            private_key: *a.private_key(),
            listen_port: None,
            peers: vec![PeerSpec { public_key: public, preshared_key: None, endpoint: Some("10.0.0.1:51820".parse().unwrap()), allowed_ips: vec![(Ipv4Addr::new(0, 0, 0, 0), 0)], persistent_keepalive: None }],
        };
        let m = WireGuard::from_spec(given);
        assert_eq!(m.spec().unwrap().peers[0].allowed_ips, vec![(Ipv4Addr::new(0, 0, 0, 0), 0)], "a Mullvad exit takes everything");
        assert_eq!(m.kind(), TunnelKind::Wireguard);
    }
}