holger-estate-front/tests/the_front_pairs_with_two_holger_boxes.rs
//! **holger's front and two holger boxes FINISH an install, in one process.**
//!
//! The REAL installer (`nordisk_pairing::installer::run`) meets THREE VMs over
//! the real bootstrap listener on loopback: vm1 is holger's front — Skidbladnir
//! `frontside::Front` over holger's `HolgerFront`, kind `front`, its role
//! running (`holger_estate_front::role`: the relay on the front's OWN tunnel
//! key, a real userspace hub on a UDP port) — and vm2/vm3 are `Kind::Holger`
//! boxes on the shared box side (`boxside::Installed` + the `boxloop`, the
//! shape of Skidbladnir's tests/holger_boxes_pair.rs). The run closes only
//! because ALL THREE report PAIRED: the front once its relay is up and its feed
//! lists every box; the boxes by their own loops. Then:
//!
//! * the relay listed both boxes by their overlay addresses, with the keys
//! they presented;
//! * the front offered its build, and handed the installer its kept web
//! certificate as the SECRET offer for its successor (never the public one);
//! * with no console bearer fed, the front says so and certifies nothing.
//!
//! The boxes' tunnels are the in-process no-op (a TUN needs CAP_NET_ADMIN);
//! everything else is the production code.
use std::collections::BTreeMap;
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use holger_estate_front::front::HolgerFront;
use holger_estate_front::FrontVm;
use nordisk_pairing::bootstrap::SecretOffer;
use nordisk_pairing::boxloop::{self, BoxDoors, Steps};
use nordisk_pairing::boxside::{Installed, Product};
use nordisk_pairing::door::Door;
use nordisk_pairing::elect::Capacity;
use nordisk_pairing::installer::{run, CloseWhen, Material, Plan, PlanVm, Sink};
use nordisk_pairing::installer_link::{DirectPath, Path};
use nordisk_pairing::join::JoinReply;
use nordisk_pairing::mtls::InstallerCa;
use nordisk_pairing::seed::{Kind, Seed, TunnelKind};
use nordisk_pairing::signal::Signal;
use nordisk_pairing::tunnel::{NoTunnel, Tunnel};
use nordisk_pairing::Role;
const ESTATE: &str = "holger.test";
const H: Duration = Duration::from_secs(3600);
/// A holger box as the installer sees it: its kind, a real tunnel key, its capacity.
struct HolgerLike {
cores: u32,
key: String,
}
impl Product for HolgerLike {
fn kind(&self) -> Kind {
Kind::Holger
}
fn tunnel_pubkey(&self) -> String {
self.key.clone()
}
fn capacity(&self) -> Result<Option<Capacity>, String> {
Ok(Some(Capacity {
cores: self.cores,
ram_bytes: 1 << 30,
data_bytes: 1 << 30,
disk_mib_s: 0,
}))
}
fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn fed(&self, _reply: &JoinReply) {}
}
struct Ready;
impl Steps for Ready {
fn primary_ready(&self, _feed: &BTreeMap<String, String>) -> Result<bool, String> {
Ok(true)
}
fn become_twin(&self, _primary: Ipv4Addr) -> Result<(), String> {
Ok(())
}
fn follow_state(&self) -> Result<(u64, u64), String> {
Ok((1, 1))
}
}
#[derive(Default)]
struct Collect {
said: Mutex<Vec<String>>,
offers: Mutex<BTreeMap<String, BTreeMap<String, String>>>,
secrets: Mutex<BTreeMap<String, Vec<String>>>,
}
impl Sink for Collect {
fn lines(&self, _vm: &str, _lines: &[String]) {}
fn status(&self, _vm: &str, _s: Option<&Signal>, _l: &str) {}
fn ledger(&self, _d: &Door) {}
fn say(&self, line: &str) {
self.said.lock().unwrap().push(line.to_string());
}
fn offer(&self, vm: &str, offer: &BTreeMap<String, String>) {
self.offers
.lock()
.unwrap()
.insert(vm.to_string(), offer.clone());
}
fn secret_offer(&self, vm: &str, o: &SecretOffer) -> Result<(), String> {
self.secrets
.lock()
.unwrap()
.insert(vm.to_string(), o.keys());
Ok(())
}
}
fn seed(ca: &InstallerCa, kind: Kind) -> Seed {
let boot = ca.issue_bootstrap(H).unwrap();
Seed {
estate: ESTATE.into(),
tunnel: TunnelKind::Wireguard,
front: Default::default(),
kind,
installer_ca_pem: ca.cert_pem.clone(),
bootstrap_cert_pem: boot.cert_pem,
bootstrap_key_pem: boot.key_pem,
peers_expected: 3,
mullvad: None,
tailscale: None,
installer_link: None,
}
}
fn listen() -> (TcpListener, SocketAddrV4) {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let std::net::SocketAddr::V4(at) = l.local_addr().unwrap() else {
panic!()
};
(l, at)
}
fn leak_log() -> &'static nordisk_pairing::bootlog::BootLog {
Box::leak(Box::new(nordisk_pairing::bootlog::BootLog::new(100)))
}
fn a_key(n: u8) -> String {
wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(
&[n; 32],
))
}
/// One holger box on loopback with its own loop.
fn a_box(
ca: &InstallerCa,
name: &str,
cores: u32,
key: String,
done: Arc<AtomicBool>,
) -> (Arc<Installed<HolgerLike>>, SocketAddrV4, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let s = seed(ca, Kind::Holger);
let installed =
Arc::new(Installed::open(dir.path(), s.clone(), HolgerLike { cores, key }).unwrap());
let (l, at) = listen();
let (vm, ca_pem, log) = (installed.clone(), ca.cert_pem.clone(), leak_log());
std::thread::spawn(move || {
nordisk_pairing::bootstrap::serve(vm.as_ref(), log, &ca_pem, l, &|_| {})
});
let (i, root) = (installed.clone(), dir.path().to_path_buf());
let _ = name;
std::thread::spawn(move || {
let tunnel: boxloop::TunnelFactory =
Box::new(|_, _| Ok(Box::new(NoTunnel::default()) as Box<dyn Tunnel>));
let doors = BoxDoors::new(i, Ready, tunnel);
boxloop::run(&root, &s, &doors, &|_| {}, &|d| {
if !done.load(Ordering::SeqCst) {
std::thread::sleep(d.min(Duration::from_millis(100)));
}
});
});
(installed, at, dir)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_installer_closes_holgers_front_and_two_holger_boxes_only_once_all_three_report_paired()
{
let ca = InstallerCa::mint(ESTATE, H).unwrap();
let done = Arc::new(AtomicBool::new(false));
// ── vm1: holger's front, a kept web certificate on its data disk ────────
let fdir = tempfile::tempdir().unwrap();
let (chain, key) = estate_front::web::certs::self_signed(&[ESTATE, "repo.holger.test"], 80);
estate_front::web::certs::keep(
fdir.path(),
ESTATE,
&estate_front::web::certs::Cert::parse(ESTATE, &chain, &key, std::time::SystemTime::now())
.unwrap(),
)
.unwrap();
let fseed = seed(&ca, Kind::Front);
let front: Arc<FrontVm> = Arc::new(
nordisk_pairing::frontside::Front::open(
fdir.path(),
fseed.clone(),
HolgerFront::new(fdir.path(), TunnelKind::Wireguard),
)
.unwrap(),
);
let (l, front_at) = listen();
let (v, ca_pem, log) = (front.clone(), ca.cert_pem.clone(), leak_log());
std::thread::spawn(move || {
nordisk_pairing::bootstrap::serve(v.as_ref(), log, &ca_pem, l, &|_| {})
});
holger_estate_front::role::start(
front.clone(),
tokio::runtime::Handle::current(),
holger_estate_front::role::RoleConfig {
relay_port: 0,
..Default::default()
},
);
assert!(
holger_estate_front::role::is_running(),
"the front's role is a listed duty (A28 M18)"
);
// ── vm2, vm3: two holger boxes; the weaker becomes the twin ─────────────
let (main, main_at, _md) = a_box(&ca, "vm2", 4, a_key(2), done.clone());
let (twin, twin_at, _td) = a_box(&ca, "vm3", 1, a_key(3), done.clone());
let vm = |name: &str, last: u8, overlay: Ipv4Addr, kind: Kind| PlanVm {
name: name.into(),
public: Ipv4Addr::new(127, 0, 0, last),
utility: Ipv4Addr::new(10, 13, 8, last),
overlay,
kind,
feed: BTreeMap::new(),
};
let plan = Plan {
estate: ESTATE.into(),
tunnel: TunnelKind::Wireguard,
vms: vec![
vm("vm1", 1, nordisk_pairing::OVERLAY_FRONT, Kind::Front),
vm("vm2", 2, nordisk_pairing::OVERLAY_BOX_A, Kind::Holger),
vm("vm3", 3, nordisk_pairing::OVERLAY_BOX_B, Kind::Holger),
],
feed: BTreeMap::new(),
front: None,
port: 0,
link_port: 0,
budget_s: 90,
close_when: CloseWhen::Paired,
box_cert_valid_s: None,
front_waits_for: Vec::new(),
};
let m = Material {
ca: ca.clone(),
me: ca.issue_installer(H).unwrap(),
link: None,
};
let paths: Vec<(PlanVm, Box<dyn Path>)> = vec![
(plan.vms[0].clone(), Box::new(DirectPath { at: front_at })),
(plan.vms[1].clone(), Box::new(DirectPath { at: main_at })),
(plan.vms[2].clone(), Box::new(DirectPath { at: twin_at })),
];
let sink = Collect::default();
let out = tokio::task::spawn_blocking({
let plan = plan.clone();
let sink = Arc::new(sink);
let s2 = sink.clone();
move || (run(&plan, &m, paths, s2.as_ref()), sink)
})
.await
.unwrap();
let (out, sink) = out;
let out = out.unwrap_or_else(|e| panic!("{e}\n{:#?}", sink.said.lock().unwrap()));
done.store(true, Ordering::SeqCst);
assert!(out.vms.iter().all(|v| v.closed && v.admitted), "{out:?}");
assert!(
sink.said
.lock()
.unwrap()
.iter()
.any(|l| l.contains("every VM is paired")),
"closed on PAIRED: {:#?}",
sink.said.lock().unwrap()
);
let fs = Signal::parse(
&std::fs::read_to_string(fdir.path().join(nordisk_pairing::boxside::SIGNAL_REL)).unwrap(),
)
.unwrap();
assert!(
fs.paired && fs.name == "vm1" && fs.role == Some(Role::Front),
"the front's own signal: {fs:?}"
);
assert_eq!(front.latest().and_then(|r| r.role), Some(Role::Front));
assert_eq!(
(
main.latest().and_then(|r| r.role),
twin.latest().and_then(|r| r.role)
),
(Some(Role::Primary), Some(Role::Twin)),
"the weaker box is the twin"
);
// The relay listed both boxes, by the keys they presented.
let listed = nordisk_pairing::frontside::relay_list(&front.latest().unwrap().peers);
assert_eq!(
listed
.iter()
.map(|(n, k, _)| (n.as_str(), k.clone()))
.collect::<Vec<_>>(),
vec![("vm2", a_key(2)), ("vm3", a_key(3))]
);
// Every box was fed the front as its relay peer, on the front's own key.
let front_key = wg_appliance_core::ceremony::load_or_mint_identity(
&fdir.path().join(holger_estate_front::front::WG_REL),
)
.unwrap()
.public_b64;
assert_eq!(
main.latest()
.unwrap()
.front()
.map(|f| f.tunnel_pubkey.clone()),
Some(front_key),
"the relay key IS the key the front presented"
);
// Offers: the build, public only; the secret offer: the kept certificate, for the successor.
let offers = sink.offers.lock().unwrap().clone();
assert_eq!(offers["vm1"]["version"], holger_estate_front::GIT_HASH);
assert!(
offers["vm1"].values().all(|v| !v.contains("PRIVATE")),
"{offers:?}"
);
assert_eq!(
sink.secrets.lock().unwrap()["vm1"],
vec![format!("tls.{ESTATE}.cert"), format!("tls.{ESTATE}.key")],
"the kept certificate is handed over for the next front"
);
// No console bearer fed: nothing certified, and nothing pretended.
assert!(!holger_estate_front::certify::record_path(fdir.path()).exists());
}