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

nordisk/holger

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());
}