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

nordisk/Skidbladnir

crates/installer/nordisk-pairing/tests/three_boxes_pair.rs

//! **Three boxes find each other, in one process** — the medium-wide seed
//! (owner 2026-09-24): ONE seed for every box, nothing per box in it. The
//! laptop lays one slot per utility address on the front's door; each box
//! joins with the estate token from the address it has, and the door tells it
//! its name, its overlay address and — once both gunnar boxes have joined —
//! its role. Each box's state machine then walks to `Paired` while its signal
//! says where it is. The medium is WireGuard (0.4.0: `none` is refused), but
//! the tunnel is raised in-process with the no-op driver; the draupnir fleet test runs
//! the same protocol over real containers and a real WireGuard relay.

use nordisk_pairing::door::Door;
use nordisk_pairing::elect::Capacity;
use nordisk_pairing::join::{JoinRequest, Ledger, Presented, Slot};
use nordisk_pairing::mtls::{box_request, InstallerCa};
use nordisk_pairing::pair::{Event, Machine, Step};
use nordisk_pairing::seed::{Front, Kind, LinkSeed, Seed, TunnelKind};
use nordisk_pairing::signal::Signal;
use nordisk_pairing::tunnel::{driver, Identity};
use nordisk_pairing::Role;
use std::net::Ipv4Addr;

const ESTATE: &str = "gunnar.rs";
const FRONT_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 8, 179);
const MAIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 90);
const TWIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 87);

fn ca() -> &'static InstallerCa {
    static CA: std::sync::OnceLock<InstallerCa> = std::sync::OnceLock::new();
    CA.get_or_init(|| InstallerCa::mint(ESTATE, std::time::Duration::from_secs(3600)).unwrap())
}

fn bootstrap() -> &'static nordisk_pairing::mtls::Issued {
    static B: std::sync::OnceLock<nordisk_pairing::mtls::Issued> = std::sync::OnceLock::new();
    B.get_or_init(|| ca().issue_bootstrap(std::time::Duration::from_secs(3600)).unwrap())
}

fn seed(kind: Kind) -> Seed {
    Seed {
        estate: ESTATE.into(),
        tunnel: TunnelKind::Wireguard,
        front: Front { door: "10.13.8.179:8474".into() },
        kind,
        installer_ca_pem: ca().cert_pem.clone(),
        bootstrap_cert_pem: bootstrap().cert_pem.clone(),
        bootstrap_key_pem: bootstrap().key_pem.clone(),
        peers_expected: 3,
        mullvad: None,
        tailscale: None,
        installer_link: Some(LinkSeed { installer_pubkey: format!("{}=", "A".repeat(43)), vm_private: format!("{}=", "B".repeat(43)), port: 51821, psk: None }),
    }
}

/// One box: the medium's seed, the address the cloud gave it, its machine.
struct Box_ {
    seed: Seed,
    util: Ipv4Addr,
    capacity: Capacity,
    machine: Machine,
    name: Option<String>,
    role: Option<Role>,
}

/// The gunnar side, faked: the primary's admin is there from the seed;
/// BecomeTwin needs the primary paired first; the follower catches up on its
/// second poll.
#[derive(Default)]
struct ControlPlanes {
    primary_paired: bool,
    twin_polls: u32,
}

impl Box_ {
    fn tick(&mut self, door: &mut Door, planes: &mut ControlPlanes, now: u64) -> Step {
        let step = self.machine.next();
        let event = match &step {
            Step::BringTunnelUp => {
                // In-process: the no-op driver stands in for the WireGuard the seed names.
                let mut t = driver(TunnelKind::None).unwrap();
                t.up(&Identity { overlay: self.util, pubkey: String::new() }, &[]).unwrap();
                if t.status().up { Event::TunnelUp } else { Event::Transient("tunnel not up".into()) }
            }
            Step::Join | Step::FrontAwaitPeers => {
                let _ = now;
                // The first ask on the bootstrap certificate (with a request for
                // the box's own key), every later one as the box itself.
                let (who, csr) = match &self.name {
                    None => (Presented::Bootstrap, box_request().unwrap().0),
                    Some(n) => (Presented::Box(n.clone()), String::new()),
                };
                let req = JoinRequest { estate: ESTATE.into(), kind: self.seed.kind, tunnel_pubkey: format!("key-{}", self.util), capacity: Some(self.capacity), csr_pem: csr };
                match door.join(&req, &who, self.util) {
                    Ok(reply) => {
                        self.name = Some(reply.me.name.clone());
                        match reply.role {
                            None => Event::Transient("the election has not spoken yet — the other box has not joined".into()),
                            Some(r) => {
                                self.role = Some(r);
                                Event::Joined { role: r, primary: reply.primary_overlay, peers_admitted: door.ledger.admitted.len() as u8, peers_expected: self.seed.peers_expected }
                            }
                        }
                    }
                    Err(e) => Event::JoinRefusedForGood(e.to_string()),
                }
            }
            Step::PrimaryCheckAdmin => {
                planes.primary_paired = true;
                Event::AdminPresent
            }
            Step::TwinBecome { .. } => if planes.primary_paired { Event::TwinConferred } else { Event::Transient("primary not listening".into()) },
            Step::TwinCertify { .. } => Event::TwinCertified,
            Step::TwinFollow { .. } => {
                planes.twin_polls += 1;
                if planes.twin_polls >= 2 { Event::Following { applied: 199, head: 199 } } else { Event::Following { applied: 100, head: 199 } }
            }
            Step::Nothing => return step,
        };
        self.machine.apply(event);
        step
    }

    fn signal(&self) -> Signal {
        Signal::of(self.name.as_deref().unwrap_or("?"), self.seed.tunnel, &self.machine)
    }
}

#[test]
fn three_boxes_on_one_medium_wide_seed_join_by_address_get_elected_and_pair_without_a_laptop() {
    // The laptop lays the slots when it lays the estate: address → name, kind, overlay.
    let mut ledger = Ledger::new(ESTATE);
    ledger.lay(FRONT_UTIL, Slot { name: "front".into(), kind: Kind::Front, overlay: nordisk_pairing::OVERLAY_FRONT, endpoint: Some("95.111.211.189:51820".into()), utility: None });
    ledger.lay(MAIN_UTIL, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: nordisk_pairing::OVERLAY_BOX_A, endpoint: None, utility: None });
    ledger.lay(TWIN_UTIL, Slot { name: "twin".into(), kind: Kind::Gunnar, overlay: nordisk_pairing::OVERLAY_BOX_B, endpoint: None, utility: None });
    let mut door = Door::new(ledger).with_ca(ca().clone());

    let big = Capacity { cores: 4, ram_bytes: 8 << 30, data_bytes: 40 << 30, disk_mib_s: 900 };
    let small = Capacity { cores: 1, ram_bytes: 2 << 30, data_bytes: 20 << 30, disk_mib_s: 400 };
    // ONE gunnar seed for both gunnar boxes — the same medium.
    let gunnar_seed = seed(Kind::Gunnar);
    let mut boxes = [
        Box_ { seed: seed(Kind::Front), util: FRONT_UTIL, capacity: big, machine: Machine::default(), name: None, role: None },
        // The box terraform named `main` is the WEAKER one this time: the election, not the name, decides.
        Box_ { seed: gunnar_seed.clone(), util: MAIN_UTIL, capacity: small, machine: Machine::default(), name: None, role: None },
        Box_ { seed: gunnar_seed.clone(), util: TWIN_UTIL, capacity: big, machine: Machine::default(), name: None, role: None },
    ];
    assert_eq!(Seed::parse(&gunnar_seed.render().unwrap()).unwrap(), gunnar_seed, "the seed round-trips through its wire form");

    let mut planes = ControlPlanes::default();
    let mut lines = Vec::new();
    for round in 0..40u64 {
        for b in boxes.iter_mut() {
            b.tick(&mut door, &mut planes, 1_000 + round);
            lines.push(b.signal().line());
        }
        if boxes.iter().all(|b| b.machine.paired()) {
            break;
        }
    }
    let last: Vec<String> = boxes.iter().map(|b| b.signal().line()).collect();
    assert!(boxes.iter().all(|b| b.machine.paired()), "not every box paired:\n{}", last.join("\n"));

    // The door named each box by its address, and the election chose the weaker box.
    assert_eq!(boxes.iter().map(|b| b.name.clone().unwrap()).collect::<Vec<_>>(), ["front", "main", "twin"]);
    let v = door.verdict.as_ref().unwrap();
    assert_eq!((v.twin.as_str(), v.primary.as_str()), ("main", "twin"), "{}", v.why);
    assert_eq!((boxes[1].role, boxes[2].role), (Some(Role::Twin), Some(Role::Primary)));
    assert!(door.ledger.complete());

    // The signal told the story: a box waited for the election, then paired.
    assert!(lines.iter().any(|l| l.contains("election has not spoken")), "{}", lines.join("\n"));
    assert!(last.iter().all(|l| l.ends_with("PAIRED")), "{}", last.join("\n"));

    // Bootstrap only during bootstrap: once every slot has joined, the
    // medium's certificate is withdrawn everywhere.
    let req = JoinRequest { estate: ESTATE.into(), kind: Kind::Gunnar, tunnel_pubkey: "another".into(), capacity: Some(small), csr_pem: box_request().unwrap().0 };
    assert!(door.join(&req, &Presented::Bootstrap, MAIN_UTIL).unwrap_err().to_string().contains("REFUSED [join-withdrawn]"));
    assert!(door.join(&req, &Presented::Box("twin".into()), MAIN_UTIL).unwrap_err().to_string().contains("REFUSED [join-forged]"));
}