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

nordisk/gunnar

crates/gunnar-front/src/vm.rs

//! **The installer's view of vm1** — edda `nordisk-pairing`'s [`Vm`], the
//! front's side of it.
//!
//! The installer runs on the operator's machine and connects OUT to this
//! box's bootstrap listener over its WireGuard link, under mutual TLS
//! (gunnar-server's `installer_listener::serve_installer`, the one listener
//! both media run). This box presents kind **`front`** — the door admits a
//! front only on the FRONT slot, so this medium laid on a box slot is refused
//! there (`[join-forged]`) — and it is fed its name, overlay address, its own
//! certificate, role `front` and the peers. Every feed is kept in
//! `<data>/pairing/joined.toml`, so a reboot resumes as itself.
//!
//! PAIRED is the front's own reading: once its relay is up and the fed reply
//! lists every box of the estate (`peers_expected` in the seed — front, main
//! and twin).

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

use nordisk_pairing::bootstrap::Vm;
use nordisk_pairing::join::{JoinReply, JoinRequest};
use nordisk_pairing::mtls::{self, Issued};
use nordisk_pairing::pair::{Event, Machine, State};
use nordisk_pairing::seed::{Kind, Seed, TunnelKind};
use nordisk_pairing::signal::Signal;
use nordisk_pairing::Role;

use gunnar_server::installer_listener::CLOSED_REL;
use gunnar_server::pairing::{JOINED_REL, OWN_CERT_REL, OWN_KEY_REL, SIGNAL_REL};

/// **vm1, as the installer sees it.**
pub struct FrontVm {
    root: PathBuf,
    seed: Seed,
    own_key: String,
    fed: Mutex<Option<JoinReply>>,
    machine: Mutex<Machine>,
    relay_up: AtomicBool,
    clock: Mutex<Option<crate::clock::ClockOffer>>,
}

/// Pure: whether a feed may be taken by the FRONT medium. Only role `front`:
/// anything else means the installer laid this medium where a gunnar box
/// belongs, and taking it would make vm1 half of a pair.
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 (`gunnar-front-installer-open`), which is vm1 and nothing else. Lay main and twin with the gunnar medium",
            reply.me.name, reply.role
        )
    })
}

impl FrontVm {
    /// This box's own key (minted once, 0600) and what was fed before.
    pub fn open(root: &Path, seed: Seed) -> anyhow::Result<FrontVm> {
        let own_key = match std::fs::read_to_string(root.join(OWN_KEY_REL)) {
            Ok(k) if !k.trim().is_empty() => k,
            _ => {
                let (_, key) = mtls::box_request().map_err(|e| anyhow::anyhow!("{e}"))?;
                gunnar_server::pairing::write_secret(root, OWN_KEY_REL, &key)?;
                key
            }
        };
        let fed = gunnar_server::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 vm = FrontVm {
            root: root.to_path_buf(),
            seed,
            own_key,
            fed: Mutex::new(fed),
            machine: Mutex::new(Machine::default()),
            relay_up: AtomicBool::new(false),
            clock: Mutex::new(None),
        };
        vm.advance();
        Ok(vm)
    }

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

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

    /// **Replace one fed value** — the operator door's rotation (h-admin-token):
    /// the kept reply and `<data>/pairing/joined.toml` (sealed) both, so the
    /// console takes it on its next look. `Ok(true)` when it changed.
    pub fn rotate(&self, key: &str, value: &str) -> Result<bool, String> {
        let mut fed = self.fed.lock().map_err(|e| e.to_string())?;
        let reply = fed.as_mut().ok_or_else(|| {
            "REFUSED [feed-rotate-unfed] this front was never fed: there is nothing to rotate"
                .to_string()
        })?;
        if reply.feed.get(key).map(String::as_str) == Some(value) {
            return Ok(false);
        }
        let mut next = reply.clone();
        next.feed.insert(key.to_string(), value.to_string());
        let text = next.render().map_err(|e| e.to_string())?;
        gunnar_server::fed_at_rest::write(&self.root, JOINED_REL, &text)
            .map_err(|e| format!("{e:#}"))?;
        *reply = next;
        Ok(true)
    }

    /// Whether the installer closed this box's INSTALL on this installation
    /// (the log door stays, `installer_listener::Door::LogOnly`).
    pub fn closed(&self) -> bool {
        self.root.join(CLOSED_REL).exists()
    }

    /// The clock started: its key and peer are offered from now on.
    pub fn clock_started(&self, offer: crate::clock::ClockOffer) {
        if let Ok(mut c) = self.clock.lock() {
            *c = Some(offer);
        }
    }

    /// The relay is up ([`crate::role`]): 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);
            tracing::info!("{}", s.line().trim());
            if let Some(text) = s.render() {
                let _ = gunnar_server::durable::write_atomic_unframed(
                    &self.root,
                    SIGNAL_REL,
                    text.as_bytes(),
                );
            }
        }
    }

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

impl Vm for FrontVm {
    /// Live update (owner 2026-10-12): what this front runs.
    #[cfg(target_os = "linux")]
    fn live_running(&self) -> Result<nordisk_pairing::live::Running, String> {
        gunnar_server::live_update::running()
    }

    /// Live update: a new app for the slot not running, then the graceful re-exec.
    #[cfg(target_os = "linux")]
    fn live_stage(&self, stage: &nordisk_pairing::live::Stage) -> Result<String, String> {
        gunnar_server::live_update::stage(stage)
    }

    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
        };
        Ok(JoinRequest {
            estate: self.seed.estate.clone(),
            kind: Kind::Front,
            // The relay's key: the door derives the `front` peer every box
            // dials from it (edda `Door::front_tunnel`).
            tunnel_pubkey: if self.seed.tunnel == TunnelKind::Wireguard {
                gunnar_server::pairing::tunnel_pubkey(&self.root)
            } else {
                String::new()
            },
            // The election compares the two gunnar boxes only.
            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 {
            gunnar_server::pairing::write_secret(&self.root, OWN_CERT_REL, cert)
                .map_err(|e| format!("{e:#}"))?;
        }
        let text = reply.render().map_err(|e| e.to_string())?;
        gunnar_server::fed_at_rest::write(&self.root, JOINED_REL, &text)
            .map_err(|e| format!("{e:#}"))?;
        tracing::info!(name = %reply.me.name, overlay = %reply.me.overlay, peers = reply.peers.len(), "installer: fed the FRONT role — kept in <data>/pairing/joined.toml");
        *self.fed.lock().map_err(|e| e.to_string())? = Some(reply.clone());
        self.advance();
        Ok(())
    }

    /// **A later answer to this CLOSED front** (nordisk-pairing 0.5.8,
    /// `installer::readmit`): a box of the estate died and was laid again at a
    /// new address with a new tunnel key. Kept like a feed — the role thread
    /// ([`crate::role`]) re-keys the relay and certifies the moved pair from it
    /// — and never a change of what this VM is (`refeed_refusal`).
    fn refed(&self, reply: &JoinReply) -> Result<(), String> {
        if let Some(why) = nordisk_pairing::bootstrap::refeed_refusal(self.latest().as_ref(), reply)
        {
            return Err(why);
        }
        if refeed_said_loud(std::time::Instant::now()) {
            tracing::warn!(peers = reply.peers.len(), primary = ?reply.primary_utility, "installer: RE-FED — a box of this estate was laid again; the relay and the routes follow it");
        } else {
            tracing::info!(peers = reply.peers.len(), primary = ?reply.primary_utility, "installer: RE-FED again — the same re-lay still settling; the relay and the routes follow it");
        }
        // A re-feed ADDS what changed: a value the install fed this front
        // (its prices, its vendor, its console bearer) is never dropped by a
        // re-admission that did not carry it.
        let mut merged = reply.clone();
        if let Some(kept) = self.latest() {
            for (k, v) in kept.feed {
                merged.feed.entry(k).or_insert(v);
            }
        }
        self.fed(&merged)
    }

    /// **A renewed certificate for this front's own key** (nordisk-pairing 0.5.12,
    /// `installer::renew`): kept where the install kept the first; the listener serves it from
    /// the next connection. `renewal_refusal` already found the same key and name.
    fn renewed(&self, cert_pem: &str) -> Result<(), String> {
        gunnar_server::pairing::write_secret(&self.root, OWN_CERT_REL, cert_pem)
            .map_err(|e| format!("{e:#}"))?;
        tracing::info!(
            "installer: this front's own certificate was RENEWED (same key, a new lifetime)"
        );
        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> {
        gunnar_server::durable::write_atomic_unframed(
            &self.root,
            CLOSED_REL,
            b"closed by the installer\n",
        )
        .map_err(|e| format!("{e:#}"))?;
        // The front's boot log keeps what was said up to here for life
        // (nordisk-pairing 0.5.5, the pinned head).
        gunnar_server::installer_listener::seal_install_head();
        Ok(())
    }

    /// **A fresh bootstrap kept for the next join** (F4, nordisk-pairing 0.5.13): the seed on
    /// disk takes the pair the installer handed with a renewal, so a front booted again from
    /// its medium (an upgrade, a recharge) re-joins after the medium's own bootstrap has ended.
    fn rebootstrapped(&self, boot: &mtls::Issued) -> Result<(), String> {
        let rel = gunnar_server::pairing::SEED_REL;
        let on_disk = std::fs::read_to_string(self.root.join(rel))
            .ok()
            .and_then(|t| Seed::parse(&t).ok())
            .filter(|s| s.estate == self.seed.estate);
        let mut seed = on_disk.unwrap_or_else(|| self.seed.clone());
        seed.bootstrap_cert_pem = boot.cert_pem.clone();
        seed.bootstrap_key_pem = boot.key_pem.clone();
        seed.check()
            .map_err(|e| format!("the seed with the fresh bootstrap: {e}"))?;
        let rendered = seed
            .render()
            .map_err(|e| format!("the seed with the fresh bootstrap: {e}"))?;
        gunnar_server::pairing::write_secret(&self.root, rel, &rendered)
            .map_err(|e| format!("{e:#}"))?;
        tracing::info!(
            "installer: the front keeps a FRESH bootstrap in its seed for its next join"
        );
        Ok(())
    }

    fn identity(&self) -> Issued {
        match self.own_cert() {
            Some(cert) => Issued {
                cert_pem: cert,
                key_pem: self.own_key.clone(),
            },
            None => Issued {
                cert_pem: self.seed.bootstrap_cert_pem.clone(),
                key_pem: self.seed.bootstrap_key_pem.clone(),
            },
        }
    }

    /// **What the front offers** — public values only it can mint, every visit:
    ///
    /// | key | value | who takes it |
    /// |---|---|---|
    /// | `version` | the build this binary is (`gunnar_common::BUILD_REF`) | lane B's install check |
    /// | `certify_pubkey` | the certify key's OpenSSH line, `… front-certify` | the primary: principal `front-certify` |
    /// | `clock_key` | the time key's OpenSSH line | every box's clock |
    /// | `clock_peer` | `peer://<utility>:50123` | every box's clock |
    fn offer(&self) -> BTreeMap<String, String> {
        let mut offer = BTreeMap::new();
        offer.insert(
            gunnar_pairing_door::OFFER_VERSION.to_string(),
            gunnar_common::BUILD_REF.to_string(),
        );
        match crate::keys::certify_pubkey(&self.root) {
            Ok(line) => {
                offer.insert(
                    gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY.to_string(),
                    line,
                );
            }
            Err(e) => {
                tracing::error!(error = %format!("{e:#}"), "installer: the front's certify key cannot be minted — not offered; the twin cannot be certified")
            }
        }
        if let Some(c) = self.clock.lock().ok().and_then(|c| c.clone()) {
            offer.insert(crate::clock::OFFER_CLOCK_KEY.to_string(), c.key_line);
            if let Some(peer) = c.peer {
                offer.insert(crate::clock::OFFER_CLOCK_PEER.to_string(), peer);
            }
        }
        // The console's own key and its guest key (`console_pubkey`,
        // `guest_pubkey`): the primary enrols `gunnar-ui` and `gunnar-guest`.
        #[cfg(feature = "console")]
        offer.extend(crate::console::offers(&self.root));
        // The ONE monetize (M-D35): the Fleet door, the estate and the key shop-pairing
        // fed this front go to every box — last, so the ONE's key wins over any of its own.
        // And on the mock where they dial it and the certificate it serves (fleet_at, fleet_ca; A3).
        if let Some(fed) = self.latest() {
            for k in [
                gunnar_server::feed::FEED_FLEET,
                gunnar_server::feed::FEED_FLEET_INSTANCE,
                gunnar_server::feed::FEED_MONETIZE,
                gunnar_server::feed::FEED_FLEET_AT,
                gunnar_server::feed::FEED_FLEET_CA,
            ] {
                if let Some(v) = fed.feed.get(k).map(|v| v.trim()).filter(|v| !v.is_empty()) {
                    offer.insert(k.to_string(), v.to_string());
                }
            }
        }
        offer
    }

    /// **The hand-over** (edda nordisk-pairing 0.5.0): at the end of the
    /// install, right before the close, the installer asks for what this
    /// front's SUCCESSOR must be fed — every valid certificate kept on the
    /// data disk and its private key, `tls.<host>.cert` + `tls.<host>.key`
    /// ([`crate::web::certs::handover`]). Over the WireGuard link, mutual TLS;
    /// the installer keeps it in the operator's 0600 store only and feeds it
    /// to the next front (the Let's Encrypt limit). Logged by host, never a
    /// byte of it.
    fn secret_offer(&self) -> nordisk_pairing::bootstrap::SecretOffer {
        let (values, hosts) = crate::web::certs::handover(&self.root, std::time::SystemTime::now());
        if hosts.is_empty() {
            tracing::info!("installer: no valid web certificate kept on the data disk — nothing to hand over to the next front");
        } else {
            tracing::info!(hosts = ?hosts, "installer: the web certificate(s) and key(s) handed over for the next front (values not logged)");
        }
        nordisk_pairing::bootstrap::SecretOffer::new(values)
    }
}

/// **The first boot after an installation** (`pid1`): the pairing state of the previous
/// installation is cleared ([`crate::PAIRING_STATE`]) so a front installed for a NEW estate
/// starts unpaired (MEASURED 2026-09-24: an old `joined.toml` resumed "as itself" and never
/// joined the new estate). `esp` is `/boot`, `data` the data root, `seed` the seed this boot
/// runs with. Returns what was cleared, or `None` when this is not a first boot.
///
/// **A KEEP boot of the same estate clears nothing** (owner 2026-10-17, the ONE box's
/// front-first update): the shop keep-boots the front from the release's front-keep medium —
/// a new ESP, so this IS a first boot, but /data is kept and the installer stages nothing
/// onto a kept volume (korp-installer: "staged declarations NOT written"), so the seed this
/// boot runs with is the kept one. When the kept, readable feed names that same estate, it
/// is still this front's: clearing it left the front unfed for ever ("the console waits for
/// the installer: no primary is known yet") — nobody feeds a front again, and it is the box
/// that pulls `SoldEstate.UpdateTake` and takes twin and main. Only the ESP is marked, so the
/// question is asked once. A feed that cannot be read, or names another estate, is cleared as
/// before.
pub fn first_boot(
    esp: &Path,
    data: &Path,
    seed: &Seed,
    write_marker: &dyn Fn(&Path) -> bool,
) -> Option<Vec<String>> {
    let first = esp.join("korp").is_dir()
        && !esp
            .join(gunnar_server::installer_listener::ESP_CLEARED)
            .exists();
    if first {
        let kept = gunnar_server::fed_at_rest::read_checked(data, JOINED_REL)
            .ok()
            .flatten()
            .and_then(|t| JoinReply::parse(&t).ok());
        if kept
            .as_ref()
            .is_some_and(|r| r.estate == seed.estate && refuses(r).is_none())
        {
            if !write_marker(&esp.join(gunnar_server::installer_listener::ESP_CLEARED)) {
                tracing::error!("pairing: a keep boot of this estate and the ESP marker could not be written — the pairing state is KEPT");
            }
            tracing::info!(estate = %seed.estate, "front: a keep boot of the same estate onto its kept data — the installer's feed, this box's key and certificate are KEPT");
            return Some(Vec::new());
        }
    }
    gunnar_server::installer_listener::reset_if_fresh_install_of(
        esp,
        data,
        &crate::PAIRING_STATE,
        write_marker,
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use nordisk_pairing::join::{Assigned, Peer};
    use std::net::Ipv4Addr;
    use std::time::Duration;

    fn a_seed() -> Seed {
        let ca = mtls::InstallerCa::mint("gunnar.rs", Duration::from_secs(3600)).unwrap();
        let boot = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
        Seed {
            estate: "gunnar.rs".into(),
            tunnel: TunnelKind::Wireguard,
            front: nordisk_pairing::seed::Front::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) -> Peer {
        Peer {
            name: name.into(),
            role,
            tunnel_pubkey: String::new(),
            overlay: nordisk_pairing::OVERLAY_BOX_A,
            endpoint: None,
            utility: Some(Ipv4Addr::new(10, 13, 8, 2)),
        }
    }

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

    /// **A rotated value is kept** in the reply the front serves (the console and
    /// monetize read it) and on disk, sealed (h-admin-token, h-feed-at-rest).
    #[test]
    fn a_rotated_value_is_kept_in_the_served_feed_and_sealed_on_disk() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        let mut reply = fed(Some(Role::Front), vec![]);
        reply
            .feed
            .insert("console_admin_token".into(), "a".repeat(64));
        vm.fed(&reply).unwrap();
        assert_eq!(vm.rotate("console_admin_token", &"b".repeat(64)), Ok(true));
        assert_eq!(
            vm.rotate("console_admin_token", &"b".repeat(64)),
            Ok(false),
            "the same value again is UNCHANGED"
        );
        assert_eq!(
            vm.latest().unwrap().feed["console_admin_token"],
            "b".repeat(64)
        );
        let raw = std::fs::read(dir.path().join(JOINED_REL)).unwrap();
        assert!(
            !String::from_utf8_lossy(&raw).contains(&"b".repeat(64)),
            "sealed on disk"
        );
        // A restart reads the rotated value back.
        let again = FrontVm::open(dir.path(), a_seed()).unwrap();
        assert_eq!(
            again.latest().unwrap().feed["console_admin_token"],
            "b".repeat(64)
        );
        // Nothing fed yet: nothing to rotate.
        let empty = tempfile::tempdir().unwrap();
        assert!(FrontVm::open(empty.path(), a_seed())
            .unwrap()
            .rotate("console_admin_token", "x")
            .is_err());
    }

    /// vm1 presents kind `front` with the relay's key, and no capacity: the
    /// election is between the two gunnar boxes.
    #[test]
    fn the_front_presents_kind_front_with_its_relay_key_and_no_capacity() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        let h = vm.hello().unwrap();
        assert_eq!(h.kind, Kind::Front);
        assert_eq!(
            h.tunnel_pubkey,
            gunnar_server::pairing::tunnel_pubkey(dir.path()),
            "the key the relay runs on"
        );
        assert!(!h.tunnel_pubkey.is_empty() && h.capacity.is_none() && !h.csr_pem.is_empty());
    }

    /// The front medium is vm1 and nothing else: a feed with any other role
    /// is refused by name and not kept.
    #[test]
    fn the_front_medium_refuses_any_role_but_front_and_keeps_nothing() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        for role in [None, Some(Role::Primary), Some(Role::Twin)] {
            let e = vm.fed(&fed(role, vec![])).unwrap_err();
            assert!(e.contains("[front-medium-is-only-the-front]"), "{e}");
        }
        assert!(vm.latest().is_none() && !dir.path().join(JOINED_REL).exists());
    }

    /// **A closed front takes the installer's RE-FEED** (nordisk-pairing
    /// 0.5.8, `installer::readmit`): the main was laid again at a new address
    /// with a new tunnel key; the front keeps the new answer (the role thread
    /// re-keys the relay and re-certifies from it) and refuses a re-feed that
    /// would change what it is.
    #[test]
    fn a_closed_front_takes_a_refeed_naming_the_recharged_main() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        let mut first = fed(
            Some(Role::Front),
            vec![
                peer("vm1", Role::Front),
                peer("vm2", Role::Primary),
                peer("vm3", Role::Twin),
            ],
        );
        first
            .feed
            .insert("plugin.gunnar.prices".into(), "[prices]".into());
        vm.fed(&first).unwrap();
        let moved = Peer {
            utility: Some(Ipv4Addr::new(10, 13, 9, 140)),
            tunnel_pubkey: "NEWKEY".into(),
            ..peer("vm2", Role::Primary)
        };
        let refed = JoinReply {
            primary_utility: Some(Ipv4Addr::new(10, 13, 9, 140)),
            ..fed(
                Some(Role::Front),
                vec![peer("vm1", Role::Front), moved, peer("vm3", Role::Twin)],
            )
        };
        vm.refed(&refed).expect("the re-feed is taken");
        assert_eq!(
            vm.latest().and_then(|r| r.primary_utility),
            Some(Ipv4Addr::new(10, 13, 9, 140))
        );
        // joined.toml is kept checked at rest (ops-hardening fed_at_rest): read it that way.
        let kept = JoinReply::parse(
            &gunnar_server::fed_at_rest::read_checked(dir.path(), JOINED_REL)
                .unwrap()
                .expect("kept"),
        )
        .unwrap();
        assert_eq!(
            kept.primary_utility,
            Some(Ipv4Addr::new(10, 13, 9, 140)),
            "kept for the next start"
        );
        assert_eq!(
            kept.feed.get("plugin.gunnar.prices").map(String::as_str),
            Some("[prices]"),
            "a re-feed ADDS what changed; the front's own values are never dropped by it"
        );
        let renamed = JoinReply {
            me: Assigned {
                name: "front2".into(),
                overlay: nordisk_pairing::OVERLAY_FRONT,
            },
            ..refed
        };
        assert!(vm
            .refed(&renamed)
            .unwrap_err()
            .contains("[refeed-changes-the-vm]"));
    }

    /// PAIRED once the relay is up and the reply lists front + main + twin —
    /// and it survives a restart from `joined.toml`.
    #[test]
    fn the_front_is_paired_when_its_relay_is_up_and_every_box_is_listed() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        vm.fed(&fed(Some(Role::Front), vec![peer("vm1", Role::Front)]))
            .unwrap();
        assert!(!vm.status().paired && vm.status().joined);
        vm.relay_started();
        assert!(!vm.status().paired, "main and twin are not listed yet");
        vm.fed(&fed(
            Some(Role::Front),
            vec![
                peer("vm1", Role::Front),
                peer("vm2", Role::Primary),
                peer("vm3", Role::Twin),
            ],
        ))
        .unwrap();
        assert!(vm.status().paired, "{:?}", vm.status());
        assert!(dir.path().join(SIGNAL_REL).exists());
        // A restart resumes as itself and is paired again once its relay is.
        let again = FrontVm::open(dir.path(), a_seed()).unwrap();
        assert_eq!(again.latest().map(|r| r.me.name).as_deref(), Some("vm1"));
        again.relay_started();
        assert!(again.status().paired);
    }

    /// **The certificate hand-over, over the REAL bootstrap listener** (edda
    /// nordisk-pairing 0.5.0): the front hands the installer every VALID
    /// certificate it keeps, with its key, as `tls.<host>.cert` + `.key` on
    /// the SECRET offer only (the public offer never carries it); the
    /// successor front, fed exactly that, serves it at once and asks Let's
    /// Encrypt nothing.
    #[test]
    fn the_front_hands_its_kept_certificate_and_key_to_the_installer_for_its_successor() {
        use crate::web::certs::{self, Cert};
        use std::time::SystemTime;
        let h = Duration::from_secs(3600);
        let ca = mtls::InstallerCa::mint("gunnar.rs", h).unwrap();
        let boot = ca.issue_bootstrap(h).unwrap();
        let seed = Seed {
            installer_ca_pem: ca.cert_pem.clone(),
            bootstrap_cert_pem: boot.cert_pem,
            bootstrap_key_pem: boot.key_pem,
            ..a_seed()
        };
        let dir = tempfile::tempdir().unwrap();
        let vm = std::sync::Arc::new(FrontVm::open(dir.path(), seed.clone()).unwrap());
        assert!(
            vm.secret_offer().is_empty(),
            "nothing kept, nothing handed over"
        );
        // The certificate this front serves, kept on its data disk ...
        let (c, k) = certs::self_signed(&["gunnar.rs", "www.gunnar.rs"], 80);
        certs::keep(
            dir.path(),
            "gunnar.rs",
            &Cert::parse("gunnar.rs", &c, &k, SystemTime::now()).unwrap(),
        )
        .unwrap();
        // ... and one that has run out, which is not handed over.
        let (old, old_key) = certs::self_signed(&["old.gunnar.rs"], 0);
        certs::keep(
            dir.path(),
            "old.gunnar.rs",
            &Cert::parse(
                "old.gunnar.rs",
                &old,
                &old_key,
                SystemTime::now() - Duration::from_secs(7200),
            )
            .unwrap(),
        )
        .unwrap();
        std::thread::sleep(Duration::from_millis(1100));

        let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let std::net::SocketAddr::V4(at) = l.local_addr().unwrap() else {
            panic!()
        };
        let log = nordisk_pairing::bootlog::BootLog::new(16);
        let (v, ca_pem) = (vm.clone(), seed.installer_ca_pem.clone());
        let served = std::thread::spawn(move || {
            nordisk_pairing::bootstrap::serve(v.as_ref(), &log, &ca_pem, l, &|_| {})
        });
        let me = ca.issue_installer(h).unwrap();
        // A hang guard, not a deadline (R5.1, 2026-10-07): the test waits for the front's answer.
        // At load 19 on dwarf this test took 36 s and the old 5 s read timeout failed a front
        // that was only slow; a front that never answers still fails, after two minutes.
        const HANG_GUARD: Duration = Duration::from_secs(120);
        let mut client = nordisk_pairing::installer::Client::new(
            Box::new(nordisk_pairing::installer_link::DirectPath { at }),
            &ca.cert_pem,
            &me,
            HANG_GUARD,
        )
        .unwrap();
        let handed = client.secret_offer().unwrap();
        assert_eq!(
            handed.keys(),
            vec![
                "tls.gunnar.rs.cert".to_string(),
                "tls.gunnar.rs.key".to_string()
            ],
            "the valid one, whole; not the expired one"
        );
        assert_eq!(handed.expose()["tls.gunnar.rs.key"], k);
        assert_eq!(handed.expose()["tls.gunnar.rs.cert"], c);
        assert!(
            !format!("{handed:?}").contains("PRIVATE"),
            "its Debug shows keys only"
        );
        let public = client.offer().unwrap();
        assert!(
            public.keys().all(|k| !k.starts_with("tls."))
                && public.values().all(|v| !v.contains("PRIVATE")),
            "the public offer never carries it: {:?}",
            public.keys()
        );
        client.close().unwrap();
        served.join().unwrap();

        // The successor: fed what was handed over, it serves it and does not ask ACME.
        let next = tempfile::tempdir().unwrap();
        let feed = handed.into_inner();
        let web = crate::web::Web::new(next.path());
        let cfg = crate::web::WebConfig::from("gunnar.rs", &feed);
        let (acme_due, lines) = web.look(&cfg, &feed, SystemTime::now());
        assert!(
            !acme_due,
            "no Let's Encrypt certificate is spent: {lines:?}"
        );
        assert!(web.resolver.any(), "served at once");
        assert_eq!(
            certs::kept(next.path(), "gunnar.rs", SystemTime::now())
                .unwrap()
                .unwrap()
                .chain_pem,
            c,
            "and kept on the successor's disk, so it hands it over in turn"
        );
    }

    /// Every visit: the build, the certify key; the clock's key and peer once
    /// the clock has started. Public values only.
    #[test]
    fn the_front_offers_its_build_its_certify_key_and_its_clock() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        let o = vm.offer();
        assert_eq!(
            o[gunnar_pairing_door::OFFER_VERSION],
            gunnar_common::BUILD_REF
        );
        assert!(o[gunnar_pairing_door::front::OFFER_CERTIFY_PUBKEY].ends_with(" front-certify"));
        assert!(
            !o.contains_key(crate::clock::OFFER_CLOCK_KEY),
            "no clock yet"
        );
        #[cfg(feature = "console")]
        assert!(
            o[crate::console::OFFER_CONSOLE_PUBKEY].ends_with(" gunnar-ui")
                && o[crate::console::OFFER_GUEST_PUBKEY].ends_with(" gunnar-guest"),
            "the console's keys are offered: {o:?}"
        );
        vm.clock_started(crate::clock::ClockOffer {
            key_line: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0 x".into(),
            peer: Some(crate::clock::peer_spelling(Ipv4Addr::new(10, 13, 8, 1))),
        });
        let o = vm.offer();
        assert_eq!(o[crate::clock::OFFER_CLOCK_PEER], "peer://10.13.8.1:50123");
        assert!(o[crate::clock::OFFER_CLOCK_KEY].starts_with("ssh-ed25519 "));
        assert!(
            o.values().all(|v| !v.contains("PRIVATE")),
            "never a private key: {o:?}"
        );
        // What a gunnar box takes from these offers (gunnar-server feed.rs).
        let feed: BTreeMap<String, String> = o
            .iter()
            .map(|(k, v)| (format!("offer.vm1.{k}"), v.clone()))
            .collect();
        let f = gunnar_server::feed::Fed::from_feed(&feed);
        assert_eq!(f.clock_peer.as_deref(), Some("peer://10.13.8.1:50123"));
        assert!(f.clock_key.is_some());
    }

    /// ONE monetize (M-D35; BROKEN "gunnar-front must take fleet, fleet_instance and
    /// monetize_pubkey from its feed and hand them to main and twin"): what shop-pairing
    /// fed the front is offered to every box, the ONE's key over any key of the front's own.
    #[test]
    fn the_front_hands_the_ones_fleet_door_its_estate_and_its_key_to_every_box() {
        let dir = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(dir.path(), a_seed()).unwrap();
        let one_key = "43cb00000000000000000000000000000000000000000000000000000000842a";
        let feed = BTreeMap::from([
            (
                gunnar_server::feed::FEED_FLEET.to_string(),
                "https://monetize.ignalina.dk:8443".to_string(),
            ),
            (
                gunnar_server::feed::FEED_FLEET_INSTANCE.to_string(),
                "acme".to_string(),
            ),
            (
                gunnar_server::feed::FEED_MONETIZE.to_string(),
                one_key.to_string(),
            ),
        ]);
        *vm.fed.lock().unwrap() = Some(JoinReply {
            estate: "gunnar.rs".into(),
            me: Assigned {
                name: "front".into(),
                overlay: nordisk_pairing::OVERLAY_FRONT,
            },
            cert_pem: None,
            role: Some(Role::Front),
            primary_overlay: None,
            primary_utility: None,
            peers: vec![],
            feed,
        });
        let o = vm.offer();
        let boxes: BTreeMap<String, String> = o
            .iter()
            .map(|(k, v)| (format!("offer.front.{k}"), v.clone()))
            .collect();
        let f = gunnar_server::feed::Fed::from_feed(&boxes);
        assert_eq!(
            f.fleet.as_deref(),
            Some("https://monetize.ignalina.dk:8443"),
            "{o:?}"
        );
        assert_eq!(f.fleet_instance.as_deref(), Some("acme"));
        assert_eq!(
            f.monetize_pubkey.as_deref(),
            Some(one_key),
            "the ONE's key, never the front's own"
        );

        // ★ A3 (the mock's private buy, 2026-10-11): where the boxes DIAL the ONE box and the
        // certificate it serves there (`fleet_at`, `fleet_ca` — the mock's seam, fed by the
        // shop's sale feed) reach every box too, or a sold main reports to nobody on the mock.
        let mut fed = vm.fed.lock().unwrap().clone().unwrap();
        fed.feed.insert(
            gunnar_server::feed::FEED_FLEET_AT.to_string(),
            "10.6.0.9:443".to_string(),
        );
        fed.feed.insert(
            gunnar_server::feed::FEED_FLEET_CA.to_string(),
            "-----BEGIN CERTIFICATE-----\nX\n-----END CERTIFICATE-----\n".to_string(),
        );
        *vm.fed.lock().unwrap() = Some(fed);
        let boxes: BTreeMap<String, String> = vm
            .offer()
            .iter()
            .map(|(k, v)| (format!("offer.front.{k}"), v.clone()))
            .collect();
        let f = gunnar_server::feed::Fed::from_feed(&boxes);
        assert_eq!(f.fleet_at.as_deref(), Some("10.6.0.9:443"));
        assert!(f
            .fleet_ca
            .as_deref()
            .is_some_and(|c| c.contains("BEGIN CERTIFICATE")));

        let unfed = tempfile::tempdir().unwrap();
        let o = FrontVm::open(unfed.path(), a_seed()).unwrap().offer();
        assert!(
            !o.contains_key(gunnar_server::feed::FEED_FLEET)
                && !o.contains_key(gunnar_server::feed::FEED_FLEET_INSTANCE),
            "nothing fed, nothing offered: {o:?}"
        );
    }
    /// ★ **A front KEEP-BOOTED onto its own /data comes up FED** (owner 2026-10-17, the ONE
    /// box's front-first update): the shop keep-boots the front from the release's front-keep
    /// medium — a new ESP (so a "first boot after an installation"), /data kept, its seed and
    /// its sealed `joined.toml` untouched. The front must still be fed after it: it is the one
    /// that pulls `SoldEstate.UpdateTake` and takes twin and main, and nobody feeds it again.
    /// Seen RED on the mock lane: "the console waits for the installer: no primary is known
    /// yet" for ever, twin and main `waiting`.
    #[test]
    fn a_front_keep_booted_onto_its_kept_data_comes_up_fed_and_can_reach_its_shop() {
        let data = tempfile::tempdir().unwrap();
        let seed = a_seed();
        let vm = FrontVm::open(data.path(), seed.clone()).unwrap();
        let mut reply = fed(
            Some(Role::Front),
            vec![peer("vm2", Role::Primary), peer("vm3", Role::Twin)],
        );
        reply.feed.insert(
            "shop_door.toml".into(),
            "door = \"https://one.example:443\"\n".into(),
        );
        reply
            .feed
            .insert("secret.shop_client_key".into(), "k".into());
        vm.fed(&reply).unwrap();
        let own_key = std::fs::read_to_string(data.path().join(OWN_KEY_REL)).unwrap();
        drop(vm);

        // The keep boot: a new ESP (korp/ present, no cleared marker), the same /data.
        let esp = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(esp.path().join("korp")).unwrap();
        let cleared = first_boot(esp.path(), data.path(), &seed, &|p| {
            std::fs::write(p, b"x").is_ok()
        });
        assert!(
            cleared.as_ref().is_none_or(|c| c.is_empty()),
            "a keep boot of the same estate clears nothing: {cleared:?}"
        );
        assert!(
            esp.path()
                .join(gunnar_server::installer_listener::ESP_CLEARED)
                .exists(),
            "the boot is marked, so it is decided once"
        );

        let after = FrontVm::open(data.path(), seed.clone()).unwrap();
        let back = after.latest().expect("the keep-booted front is still fed");
        assert_eq!(back.peers.len(), 2);
        assert_eq!(
            std::fs::read_to_string(data.path().join(OWN_KEY_REL)).unwrap(),
            own_key,
            "its own key kept"
        );
        assert!(
            back.feed.contains_key("shop_door.toml"),
            "the fed shop door is there to pull UpdateTake from"
        );
        #[cfg(feature = "metered")]
        assert!(
            crate::recharge::shop_door(&back.feed).is_some(),
            "the update loop finds its shop door in the kept feed"
        );
    }

    /// The other half of the rule, unchanged: a front installed for a DIFFERENT estate onto a
    /// kept volume starts unpaired (MEASURED 2026-09-24: an old `joined.toml` resumed "as
    /// itself" and never joined the new estate).
    #[test]
    fn a_front_installed_for_another_estate_onto_kept_data_still_starts_unpaired() {
        let data = tempfile::tempdir().unwrap();
        let vm = FrontVm::open(data.path(), a_seed()).unwrap();
        vm.fed(&fed(Some(Role::Front), vec![])).unwrap();
        drop(vm);
        let mut other = a_seed();
        other.estate = "test2.gunnar.rs".into();
        let esp = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(esp.path().join("korp")).unwrap();
        let cleared = first_boot(esp.path(), data.path(), &other, &|p| {
            std::fs::write(p, b"x").is_ok()
        })
        .expect("a first boot");
        assert!(cleared.iter().any(|c| c == JOINED_REL), "{cleared:?}");
        assert!(!data.path().join(JOINED_REL).exists());
    }
}

/// How long after one loud RE-FED the next ones of the same re-lay are said at INFO. A recharge
/// re-feeds the front once per step of the moved box (mock gate chain 19, 2026-10-25: 7 in 60 s,
/// which the log gate refuses as WARN spam); the operator needs to hear the re-lay once.
pub const REFEED_LOUD_EVERY: std::time::Duration = std::time::Duration::from_secs(300);

static LAST_LOUD_REFEED: std::sync::Mutex<Option<std::time::Instant>> = std::sync::Mutex::new(None);

/// Whether a RE-FED at `now` is said at WARN: the first, and then one per [`REFEED_LOUD_EVERY`].
fn refeed_said_loud(now: std::time::Instant) -> bool {
    let Ok(mut last) = LAST_LOUD_REFEED.lock() else {
        return true;
    };
    loud_after(&mut last, now)
}

fn loud_after(last: &mut Option<std::time::Instant>, now: std::time::Instant) -> bool {
    match *last {
        Some(t) if now.saturating_duration_since(t) < REFEED_LOUD_EVERY => false,
        _ => {
            *last = Some(now);
            true
        }
    }
}

#[cfg(test)]
mod refeed_loudness_tests {
    use super::*;

    #[test]
    fn a_re_lay_is_said_loud_once_then_quietly_until_the_window_passes() {
        let t0 = std::time::Instant::now();
        let mut last = None;
        assert!(loud_after(&mut last, t0), "the first re-feed is a WARN");
        for s in [2u64, 5, 7, 14, 21, 51] {
            assert!(
                !loud_after(&mut last, t0 + std::time::Duration::from_secs(s)),
                "+{s} s: INFO"
            );
        }
        assert!(
            loud_after(&mut last, t0 + REFEED_LOUD_EVERY),
            "a re-lay after the window is loud again"
        );
    }
}