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]"));
}