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/src/frontside.rs

//! **The FRONT medium's side of the installer** (0.5.6) — vm1, as the installer
//! sees it, with no product type in it.
//!
//! [`crate::boxside`] is a PRODUCT box's half (main and twin); this is vm1's.
//! Until 0.5.6 it existed only inside gunnar (`gunnar-front/src/vm.rs`, the
//! front's `Vm`, and the relay half of `gunnar-pairing-door/src/front.rs`); holger's
//! estate grew a front of its own (holger `holger-estate-front`, 2026-09-27) and
//! the parts that know nothing of either product moved here:
//!
//! * [`Front`] — the [`Vm`]: it presents kind **`front`** with the relay's
//!   tunnel key and no capacity (the election compares the product boxes
//!   only), keeps what it is fed in `<data>/pairing/joined.toml`, sealed
//!   ([`crate::fed_at_rest`], 0.5.17; the same files as a box, [`crate::boxside`]), refuses any role but `front` BY NAME
//!   (this medium laid where a product box belongs would make vm1 half of a
//!   pair), and reports PAIRED once its relay is up and the fed reply lists
//!   every box of the estate (`peers_expected` in the seed);
//! * [`Verdict`] — the election as the front reads it from its feed: which box
//!   is primary and which is twin, at which utility address — what the front
//!   certifies the twin against;
//! * [`relay_list`] — what the estate's WireGuard relay lists (the relay is edda
//!   `estate-front::relay`) on
//!   udp/[`crate::RELAY_PORT`], keyed with the front's own tunnel key (the key
//!   its presentation carried, so the key the door feeds every box as the
//!   `front` peer), listing each fed box by its overlay /32.
//!
//! What stays the product's ([`FrontProduct`]): the tunnel key's public half,
//! what the front OFFERS (its build, its keys, where its doors are), what it
//! hands its successor as a SECRET (the web certificate), and what a feed
//! means to it.

use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;

use crate::bootstrap::{SecretOffer, Vm};
use crate::boxside::{write_atomic, CLOSED_REL, JOINED_REL, OWN_CERT_REL, OWN_KEY_REL, SIGNAL_REL};
use crate::join::{JoinReply, JoinRequest};
use crate::mtls::{self, Issued};
use crate::pair::{Event, Machine, State};
use crate::seed::{Kind, Seed};
use crate::signal::Signal;
use crate::Role;

/// **What only the product knows** about its front.
pub trait FrontProduct: Send + Sync {
    /// The relay's public key (WireGuard base64) — what the door derives the
    /// `front` peer every box dials from. Empty for `tunnel = none`.
    fn tunnel_pubkey(&self) -> String;
    /// What the front offers the installer on every visit (`offer.<vm>.<key>`):
    /// PUBLIC values only it can mint. Never a private key: the offer is logged.
    fn offer(&self, fed: Option<&JoinReply>) -> BTreeMap<String, String>;
    /// What its SUCCESSOR must be fed (asked once, before the close): the web
    /// certificate and key per host. Empty by default.
    fn secret_offer(&self) -> SecretOffer {
        SecretOffer::default()
    }
    /// Called after a feed was KEPT (idempotent; apply live, never restart).
    fn fed(&self, _reply: &JoinReply) {}
    /// **What the front runs** (0.5.14, live update): the default refuses by name.
    fn live_running(&self) -> Result<crate::live::Running, String> {
        Err("REFUSED [live-unsupported] this VM takes no live update".into())
    }
    /// **A new app for the front's other slot** (0.5.14).
    fn live_stage(&self, _stage: &crate::live::Stage) -> Result<String, String> {
        Err("REFUSED [live-unsupported] this VM takes no live update".into())
    }
    /// **A new app with what its release declares** (lane B5): the default takes an
    /// undeclared stage and refuses a declared one by name ([`crate::live::declared_unsupported`]).
    fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
        if declared.is_empty() {
            self.live_stage(stage)
        } else {
            Err(crate::live::declared_unsupported())
        }
    }
}

/// Pure: whether a feed may be taken by the FRONT medium. Only role `front`.
pub fn refuses(reply: &JoinReply) -> Option<String> {
    (reply.role != Some(Role::Front)).then(|| {
        format!(
            "REFUSED [front-medium-is-only-the-front] the installer fed `{}` role {:?}; this VM booted the FRONT medium, which is vm1 and nothing else. Lay main and twin with the product medium",
            reply.me.name, reply.role
        )
    })
}

/// **The election, as the front reads it from its feed** — which box is the
/// primary and which the twin, and where each one's utility address is. `None`
/// until the verdict is fed with both addresses (a 0.4.0 feed carries none).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
    pub primary: String,
    /// The primary's utility address — the reply's `primary_utility` when the
    /// door set it (the twin's own loop becomes the twin of exactly that), else
    /// the peer's.
    pub primary_utility: Ipv4Addr,
    pub twin: String,
    pub twin_utility: Ipv4Addr,
}

/// Pure: the [`Verdict`] in `reply`.
pub fn verdict(reply: &JoinReply) -> Option<Verdict> {
    let of = |role: Role| reply.peers.iter().find(|p| p.role == role);
    let twin = of(Role::Twin)?;
    let primary = of(Role::Primary)?;
    Some(Verdict { primary: primary.name.clone(), primary_utility: reply.primary_utility.or(primary.utility)?, twin: twin.name.clone(), twin_utility: twin.utility? })
}

/// Pure: the primary's utility address alone — known before a twin is.
pub fn primary_utility(reply: &JoinReply) -> Option<Ipv4Addr> {
    reply.primary_utility.or_else(|| reply.peers.iter().find(|p| p.role == Role::Primary).and_then(|p| p.utility))
}

/// Pure: **what the front's relay lists** — `(name, tunnel key, overlay)` of
/// every fed box peer with a key, the front's own entry excluded. The relay
/// itself is edda `estate-front::relay` (wg-appliance-core's HubRelay).
pub fn relay_list(peers: &[crate::join::Peer]) -> Vec<(String, String, Ipv4Addr)> {
    peers.iter().filter(|p| p.role != Role::Front && !p.tunnel_pubkey.trim().is_empty()).map(|p| (p.name.clone(), p.tunnel_pubkey.trim().to_string(), p.overlay)).collect()
}

/// Pure: **what the front's relay lists, each box with its pre-shared key** (0.5.17, A7 row
/// 13; Gunnar 356e8dd2 `FrontRole::take`): [`relay_list`], and for each box the PSK the
/// installer fed the FRONT for it (`wg_psk.<box>`, [`crate::installer::feed_wg_psk_for`]),
/// trimmed; `None` when none was fed (an estate laid before PSKs: exactly the old listing), a
/// blank one handed on as the blank it is (lane D14), which the relay refuses.
/// The relay (edda `estate-front::relay::Relay::take_with_psk`) keys every session with it.
pub fn relay_list_with_psk(reply: &JoinReply) -> Vec<(String, String, Ipv4Addr, Option<String>)> {
    relay_list(&reply.peers)
        .into_iter()
        .map(|(name, key, overlay)| {
            // A fed but BLANK key stays `Some("")` (lane D14, decision c1): the relay refuses
            // the box, never lists it without one.
            let psk = reply.feed.get(&crate::installer::feed_wg_psk_for(&name)).map(|p| p.trim().to_string());
            (name, key, overlay, psk)
        })
        .collect()
}

/// **The box an update works on is BUSY** (0.5.17, A7 row 10; Gunnar aa14357b
/// `update::busy`): while a front's update step holds a role ([`on`]), the recharge watcher
/// leaves that box alone — a box in its keep boot or its re-exec is DOWN, not lost — and a
/// daily upgrade waits ([`busy`]`(None)`). Counted per role, process-wide.
/// **The update step** every product's front runs (0.5.26, P4a; feature `frontside-update`).
#[cfg(any(feature = "frontside-update", test))]
pub mod update;
/// The front proves the tunnel is the only path (P4c, A28 M11; it dials the boxes: off by default).
#[cfg(any(feature = "mesh-probe", test))]
pub mod mesh_probe;
/// The paired links' renewal: the schedule and the steps (P4c, A28 M11; off by default).
#[cfg(any(feature = "front-renew", test))]
pub mod renew;
/// The front re-admits a lost box from the kept material (P4c, A28 M11).
#[cfg(feature = "kept")]
pub mod recharge;

pub mod busy {
    use std::collections::BTreeMap;
    use std::sync::Mutex;

    static BUSY: Mutex<BTreeMap<String, u32>> = Mutex::new(BTreeMap::new());

    /// While held, its role is busy; dropped, it is not (once every holder dropped).
    #[must_use = "the role is busy only while the guard lives"]
    pub struct Guard(String);

    impl Drop for Guard {
        fn drop(&mut self) {
            let mut b = BUSY.lock().unwrap_or_else(|p| p.into_inner());
            if let Some(n) = b.get_mut(&self.0) {
                *n = n.saturating_sub(1);
                if *n == 0 {
                    b.remove(&self.0);
                }
            }
        }
    }

    /// The update works on `role` from now until the guard drops.
    pub fn on(role: &str) -> Guard {
        *BUSY.lock().unwrap_or_else(|p| p.into_inner()).entry(role.to_string()).or_default() += 1;
        Guard(role.to_string())
    }

    /// Whether an update works on `role` now (`None`: on any box).
    pub fn busy(role: Option<&str>) -> bool {
        let b = BUSY.lock().unwrap_or_else(|p| p.into_inner());
        match role {
            Some(r) => b.get(r).is_some_and(|n| *n > 0),
            None => b.values().any(|n| *n > 0),
        }
    }
}

/// **vm1, as the installer sees it.**
pub struct Front<P: FrontProduct> {
    root: PathBuf,
    seed: Seed,
    own_key: String,
    fed: Mutex<Option<JoinReply>>,
    machine: Mutex<Machine>,
    relay_up: AtomicBool,
    pub product: P,
}

impl<P: FrontProduct> Front<P> {
    /// This front's own key (minted once, 0600) and what it was fed before —
    /// only a front feed of THIS estate counts.
    pub fn open(root: &Path, seed: Seed, product: P) -> Result<Front<P>, String> {
        let own_key = match std::fs::read_to_string(root.join(OWN_KEY_REL)) {
            Ok(k) if k.contains("PRIVATE KEY") => k,
            _ => {
                let (_, key) = mtls::box_request().map_err(|e| e.to_string())?;
                write_atomic(root, OWN_KEY_REL, key.as_bytes(), true).map_err(|e| format!("{}: {e}", root.join(OWN_KEY_REL).display()))?;
                key
            }
        };
        let fed = crate::fed_at_rest::read(root, JOINED_REL).and_then(|t| JoinReply::parse(&t).ok()).filter(|r| r.estate == seed.estate && refuses(r).is_none());
        let f = Front { root: root.to_path_buf(), seed, own_key, fed: Mutex::new(fed), machine: Mutex::new(Machine::default()), relay_up: AtomicBool::new(false), product };
        f.advance();
        Ok(f)
    }

    pub fn root(&self) -> &Path {
        &self.root
    }

    pub fn seed(&self) -> &Seed {
        &self.seed
    }

    /// The installer CA (PEM): what the front's doors chain to.
    pub fn installer_ca_pem(&self) -> &str {
        &self.seed.installer_ca_pem
    }

    /// What the installer fed last.
    pub fn latest(&self) -> Option<JoinReply> {
        self.fed.lock().ok().and_then(|f| f.clone())
    }

    /// Whether the installer closed this front's listener on this installation.
    pub fn closed(&self) -> bool {
        self.root.join(CLOSED_REL).exists()
    }

    fn own_cert(&self) -> Option<String> {
        std::fs::read_to_string(self.root.join(OWN_CERT_REL)).ok().filter(|c| c.contains("BEGIN CERTIFICATE"))
    }

    /// The front's own identity — `None` while it holds only the medium's
    /// bootstrap certificate (which is nobody's).
    pub fn own_identity(&self) -> Option<Issued> {
        self.own_cert().map(|cert_pem| Issued { cert_pem, key_pem: self.own_key.clone() })
    }

    /// The relay is up: the front's "tunnel".
    pub fn relay_started(&self) {
        self.relay_up.store(true, Ordering::SeqCst);
        self.advance();
    }

    /// **Walk the pairing machine** from what is known now — the fed reply and
    /// whether the relay is up — and write the signal when it moved.
    pub fn advance(&self) {
        let Some(reply) = self.latest() else { return };
        let Ok(mut m) = self.machine.lock() else { return };
        let before = m.clone();
        let joined = || Event::Joined { role: Role::Front, primary: None, peers_admitted: reply.peers.len().min(u8::MAX as usize) as u8, peers_expected: self.seed.peers_expected };
        if m.state == State::Booted {
            m.apply(joined());
        }
        if matches!(m.state, State::Joined { .. }) && self.relay_up.load(Ordering::SeqCst) {
            m.apply(Event::TunnelUp);
        }
        if matches!(m.state, State::RoleKnown { .. }) {
            m.apply(joined());
        }
        if *m != before {
            let s = Signal::of(&reply.me.name, self.seed.tunnel, &m);
            if let Some(text) = s.render() {
                let _ = write_atomic(&self.root, SIGNAL_REL, text.as_bytes(), false);
            }
        }
    }
}

impl<P: FrontProduct> Vm for Front<P> {
    fn live_running(&self) -> Result<crate::live::Running, String> {
        self.product.live_running()
    }

    fn live_stage(&self, stage: &crate::live::Stage) -> Result<String, String> {
        self.product.live_stage(stage)
    }

    fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
        self.product.live_stage_declared(stage, declared)
    }

    fn hello(&self) -> Result<JoinRequest, String> {
        let csr = if self.own_cert().is_some() { String::new() } else { mtls::box_request_for(&self.own_key).map_err(|e| e.to_string())?.0 };
        // No capacity: the election compares the product boxes only.
        Ok(JoinRequest { estate: self.seed.estate.clone(), kind: Kind::Front, tunnel_pubkey: self.product.tunnel_pubkey(), capacity: None, csr_pem: csr })
    }

    fn fed(&self, reply: &JoinReply) -> Result<(), String> {
        if reply.estate != self.seed.estate {
            return Err(format!("REFUSED [installer-estate] this VM is of `{}`, the installer fed `{}`", self.seed.estate, reply.estate));
        }
        if let Some(why) = refuses(reply) {
            return Err(why);
        }
        if let Some(cert) = &reply.cert_pem {
            write_atomic(&self.root, OWN_CERT_REL, cert.as_bytes(), true).map_err(|e| e.to_string())?;
        }
        let text = reply.render().map_err(|e| e.to_string())?;
        crate::fed_at_rest::write(&self.root, JOINED_REL, &text)?;
        *self.fed.lock().map_err(|e| e.to_string())? = Some(reply.clone());
        self.advance();
        self.product.fed(reply);
        Ok(())
    }

    fn status(&self) -> Signal {
        let name = self.latest().map(|r| r.me.name).unwrap_or_else(|| "?".into());
        let m = self.machine.lock().map(|m| m.clone()).unwrap_or_default();
        Signal::of(&name, self.seed.tunnel, &m)
    }

    fn close(&self) -> Result<(), String> {
        write_atomic(&self.root, CLOSED_REL, b"closed by the installer\n", false).map_err(|e| e.to_string())
    }

    fn identity(&self) -> Issued {
        self.own_identity().unwrap_or_else(|| Issued { cert_pem: self.seed.bootstrap_cert_pem.clone(), key_pem: self.seed.bootstrap_key_pem.clone() })
    }

    fn offer(&self) -> BTreeMap<String, String> {
        self.product.offer(self.latest().as_ref())
    }

    fn secret_offer(&self) -> SecretOffer {
        self.product.secret_offer()
    }

    /// 0.5.8: after the close, the front learns what changed around it — a
    /// re-admitted box's new address and tunnel key, which its relay lists and
    /// its routes dial — kept exactly like a feed, once
    /// [`crate::bootstrap::refeed_refusal`] found nothing that changes vm1.
    fn refed(&self, reply: &JoinReply) -> Result<(), String> {
        if let Some(why) = crate::bootstrap::refeed_refusal(self.latest().as_ref(), reply) {
            return Err(why);
        }
        self.fed(reply)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::join::{Assigned, Peer};
    use crate::seed::TunnelKind;
    use std::time::Duration;

    struct P;
    impl FrontProduct for P {
        fn tunnel_pubkey(&self) -> String {
            "KF".into()
        }
        fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
            BTreeMap::from([("version".into(), "t".into())])
        }
        fn secret_offer(&self) -> SecretOffer {
            SecretOffer::new(BTreeMap::from([("tls.x.cert".into(), "C".into())]))
        }
    }

    /// A front whose box carries a live slot.
    struct LiveF;
    impl FrontProduct for LiveF {
        fn tunnel_pubkey(&self) -> String {
            String::new()
        }
        fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
            BTreeMap::new()
        }
        fn live_running(&self) -> Result<crate::live::Running, String> {
            Ok(crate::live::Running { base: "b".into(), app: "f".into(), slot: "b".into(), live_update: true })
        }
        fn live_stage(&self, s: &crate::live::Stage) -> Result<String, String> {
            Ok(format!("front staged {}", s.pin_hex))
        }
    }

    /// ★ The front answers the installer's live door with its product's slot; one without
    /// refuses by name (0.5.14).
    #[test]
    fn the_front_answers_the_live_door_with_its_products_slot_or_refuses_by_name() {
        let d = dir("live");
        let f = Front::open(&d, seed(), LiveF).unwrap();
        assert_eq!(f.live_running().unwrap().slot, "b");
        let st = crate::live::Stage { pin_hex: "9".repeat(64), signature: vec![1], base: "b".into(), archive: vec![2] };
        assert!(f.live_stage(&st).unwrap().starts_with("front staged"));
        let plain = Front::open(&d, seed(), P).unwrap();
        assert!(plain.live_running().unwrap_err().contains("[live-unsupported]"));
        let _ = std::fs::remove_dir_all(&d);
    }

    fn seed() -> Seed {
        let ca = mtls::InstallerCa::mint("front.test", Duration::from_secs(3600)).unwrap();
        let boot = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
        Seed {
            estate: "front.test".into(),
            tunnel: TunnelKind::Wireguard,
            front: Default::default(),
            kind: Kind::Front,
            installer_ca_pem: ca.cert_pem,
            bootstrap_cert_pem: boot.cert_pem,
            bootstrap_key_pem: boot.key_pem,
            peers_expected: 3,
            mullvad: None,
            tailscale: None,
            installer_link: None,
        }
    }

    fn peer(name: &str, role: Role, util: Option<[u8; 4]>) -> Peer {
        Peer { name: name.into(), role, tunnel_pubkey: String::new(), overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: util.map(Ipv4Addr::from) }
    }

    fn fed(role: Option<Role>, peers: Vec<Peer>) -> JoinReply {
        JoinReply { estate: "front.test".into(), me: Assigned { name: "vm1".into(), overlay: crate::OVERLAY_FRONT }, cert_pem: None, role, primary_overlay: None, primary_utility: None, peers, feed: BTreeMap::new() }
    }

    fn dir(tag: &str) -> PathBuf {
        let d = std::env::temp_dir().join(format!("frontside-{tag}-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&d);
        d
    }

    #[test]
    fn the_front_presents_kind_front_with_its_relay_key_no_capacity_and_refuses_any_other_role() {
        let d = dir("refuse");
        let f = Front::open(&d, seed(), P).unwrap();
        let h = f.hello().unwrap();
        assert_eq!((h.kind, h.tunnel_pubkey.as_str(), h.capacity.is_none(), h.csr_pem.is_empty()), (Kind::Front, "KF", true, false));
        for role in [None, Some(Role::Primary), Some(Role::Twin)] {
            assert!(f.fed(&fed(role, vec![])).unwrap_err().contains("[front-medium-is-only-the-front]"));
        }
        assert!(f.latest().is_none() && !d.join(JOINED_REL).exists(), "nothing kept");
        assert_eq!(f.secret_offer().keys(), vec!["tls.x.cert".to_string()], "the product's secret offer");
        let _ = std::fs::remove_dir_all(&d);
    }

    #[test]
    fn the_front_is_paired_when_its_relay_is_up_and_every_box_is_listed_and_resumes_after_a_restart() {
        let d = dir("paired");
        let f = Front::open(&d, seed(), P).unwrap();
        f.fed(&fed(Some(Role::Front), vec![peer("vm1", Role::Front, None)])).unwrap();
        assert!(f.status().joined && !f.status().paired);
        f.relay_started();
        assert!(!f.status().paired, "main and twin not listed yet");
        f.fed(&fed(Some(Role::Front), vec![peer("vm1", Role::Front, None), peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
        assert!(f.status().paired, "{:?}", f.status());
        assert!(d.join(SIGNAL_REL).exists());
        let again = Front::open(&d, seed(), P).unwrap();
        assert_eq!(again.latest().map(|r| r.me.name).as_deref(), Some("vm1"));
        again.relay_started();
        assert!(again.status().paired);
        let _ = std::fs::remove_dir_all(&d);
    }

    /// 0.5.8: a closed front is re-fed a re-admitted primary's new address —
    /// its verdict follows at once — and never a role but its own.
    #[test]
    fn a_closed_front_takes_a_refeed_with_the_new_primary_and_refuses_a_change_of_itself() {
        let d = dir("refeed");
        let f = Front::open(&d, seed(), P).unwrap();
        f.fed(&fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
        f.close().unwrap();
        f.refed(&fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 9])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
        assert_eq!(f.latest().as_ref().and_then(primary_utility), Some(Ipv4Addr::new(10, 0, 0, 9)), "the front dials the new primary");
        assert!(f.refed(&fed(Some(Role::Primary), vec![])).unwrap_err().contains("REFUSED ["));
        assert_eq!(f.latest().as_ref().and_then(primary_utility), Some(Ipv4Addr::new(10, 0, 0, 9)));
        let _ = std::fs::remove_dir_all(&d);
    }

    #[test]
    fn the_verdict_names_primary_and_twin_on_their_utility_addresses() {
        let r = fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))]);
        let v = verdict(&r).unwrap();
        assert_eq!((v.primary.as_str(), v.primary_utility, v.twin.as_str(), v.twin_utility), ("vm2", Ipv4Addr::new(10, 0, 0, 2), "vm3", Ipv4Addr::new(10, 0, 0, 3)));
        assert_eq!(primary_utility(&r), Some(Ipv4Addr::new(10, 0, 0, 2)));
        let no_twin_address = fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, None)]);
        assert!(verdict(&no_twin_address).is_none());
    }

    #[test]
    fn the_relay_list_is_every_fed_box_with_a_key_never_the_front() {
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
        let reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm1", Role::Front, None) }, Peer { tunnel_pubkey: format!(" {k} "), ..peer("vm2", Role::Primary, None) }, peer("vm3", Role::Twin, None)]);
        assert_eq!(relay_list(&reply.peers), vec![("vm2".to_string(), k.to_string(), crate::OVERLAY_BOX_A)]);
    }

    /// ★ 0.5.17 (A7 row 21): what the front was fed (the shop's key, the tokens, each box's
    /// PSK) is SEALED at rest and the front reads it back after a restart.
    #[test]
    fn the_front_keeps_its_feed_sealed_at_rest() {
        let d = dir("sealed");
        let f = Front::open(&d, seed(), P).unwrap();
        let mut r = fed(Some(Role::Front), vec![]);
        r.feed.insert("secret.shop_client_key".into(), "shop-key-NEVER-in-the-clear".into());
        f.fed(&r).unwrap();
        let raw = std::fs::read(d.join(JOINED_REL)).unwrap();
        assert!(crate::fed_at_rest::is_sealed(&raw) && !String::from_utf8_lossy(&raw).contains("NEVER-in-the-clear"));
        let again = Front::open(&d, seed(), P).unwrap();
        assert_eq!(again.latest().and_then(|r| r.feed.get("secret.shop_client_key").cloned()).as_deref(), Some("shop-key-NEVER-in-the-clear"));
        let _ = std::fs::remove_dir_all(&d);
    }

    /// ★ 0.5.17 (A7 row 13, Gunnar 356e8dd2): the relay lists each box with the PSK the
    /// installer fed the front for it (`wg_psk.<box>`); a box with none is listed without.
    #[test]
    fn the_relay_lists_each_box_with_the_psk_the_front_was_fed_for_it() {
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
        let psk = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
        let mut reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm2", Role::Primary, None) }, Peer { tunnel_pubkey: k.into(), overlay: crate::OVERLAY_BOX_B, ..peer("vm3", Role::Twin, None) }]);
        reply.feed.insert(crate::installer::feed_wg_psk_for("vm2"), format!(" {psk} "));
        let list = relay_list_with_psk(&reply);
        assert_eq!(list[0], ("vm2".to_string(), k.to_string(), crate::OVERLAY_BOX_A, Some(psk.to_string())));
        assert_eq!(list[1].3, None, "no PSK fed for vm3");
    }

    /// ★ Lane D14, decision c1: a `wg_psk.<box>` fed BLANK is handed to the relay as the blank
    /// it is (which refuses the box), never as "no PSK" (which would list it without one).
    #[test]
    fn a_blank_fed_psk_reaches_the_relay_as_blank_never_as_none() {
        let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
        let mut reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm2", Role::Primary, None) }]);
        reply.feed.insert(crate::installer::feed_wg_psk_for("vm2"), "   ".into());
        assert_eq!(relay_list_with_psk(&reply)[0].3.as_deref(), Some(""));
    }

    /// ★ 0.5.17 (A7 row 10, Gunnar aa14357b): a box is BUSY while an update holds it, per
    /// role and counted, and only then.
    #[test]
    fn a_box_is_busy_while_an_update_holds_it_and_only_then() {
        assert!(!busy::busy(Some("g5-main")));
        let one = busy::on("g5-main");
        let two = busy::on("g5-main");
        assert!(busy::busy(Some("g5-main")) && busy::busy(None));
        assert!(!busy::busy(Some("g5-twin")), "per role");
        drop(one);
        assert!(busy::busy(Some("g5-main")), "counted: the second holder still works on it");
        drop(two);
        assert!(!busy::busy(Some("g5-main")));
    }
}