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

//! **The front re-admits a lost box itself** (feature `metered`) — Gunnar's pattern, Holger's
//! boxes. The sequence, its WHEN and the watch are SHARED, Skidbladnir
//! `nordisk_pairing::frontside::recharge` (feature `kept`; P4c, A28 M11); what is Holger's:
//! its seed, the shop over its `ShopDoor`, the estate over its sealed slots and bearers. A sold Holger estate keeps its installer material in its primary's sealed slot
//! ([`crate::sealed`]), and the twin follows that repository like every other; so when the
//! `main` or the `twin` is LOST, the front is the estate's installer:
//!
//! ```text
//!   1 material   the kept bundle, read from the box that STANDS (a lost main: the twin's copy)
//!   2 lay        SoldEstate.Recharge(role, a fresh seed)  the ONE box lays that one box again
//!   3 readmit    nordisk-pairing kept::readmit            the box joins at its new addresses;
//!                                                         a main is told its twin (recharge_from)
//!   4 store      the new ledger into the primary's sealed slot (a twin's primary at once; a new
//!                main's once its recharge released it — [`Watch`] seals it on a later look)
//!   5 done       SoldEstate.Recharge(done)                its release clone given back
//! ```
//!
//! A re-admitted main starts DEAD and HELD (holger-server's boot hold) and reports
//! `data=fresh`, naming the twin it awaits. It is filled from the twin by ONE recharge lane
//! (Skidbladnir `nordisk_twin::recharge`), whoever asks first: the ONE box's rebuild plan (New
//! server lays a lost main and orders its `recharge_from_twin`, which the main pulls and runs),
//! an operator's Recharge from twin, or — with the estate's fed `repair_auto = true` — THIS
//! front by itself ([`crate::autofill`], Gunnar's front autofill since c7c9e11a, lane G11). The
//! main's `Recharge` runs one at a time, and a lane that finds it recharged meanwhile moves
//! nothing.
//!
//! [`recharge`] is one recharge over a [`Shop`] and an [`Estate`]: pure enough for tests.
//! [`due`] decides WHEN: a box gone for [`GONE_FOR`] while the other answers (a whole estate
//! unreachable is the front's own link, never two lost boxes). The shop refuses a box that
//! still stands, so a link that merely broke lays nothing.

use std::net::Ipv4Addr;
use std::time::Duration;

use nordisk_pairing::kept::Kept;

pub use nordisk_pairing::frontside::recharge::{
    due, left_alone, Estate, Laid, Readmitted, Shop, Unsealed, AGAIN_AFTER, GONE_FOR,
};

/// The seed Holger's main and twin are laid with (`seed.holger.toml`).
pub const SEED_NAME: &str = crate::PRODUCT;

/// **One re-admission of `role`** with Holger's seed (the shared `recharge`).
pub fn recharge(
    shop: &dyn Shop,
    estate: &dyn Estate,
    role: &str,
) -> Result<(String, Option<Unsealed>), String> {
    nordisk_pairing::frontside::recharge::recharge(shop, estate, role, SEED_NAME)
}

/// **What the front remembers between looks** (the shared watch).
#[derive(Default)]
pub struct Watch(nordisk_pairing::frontside::recharge::Watch);

impl Watch {
    /// **One look** over the verdict: each of main and twin reached on its control port; a box
    /// due is recharged ([`recharge`] over the live [`ShopDoor`](crate::shop_door::ShopDoor)
    /// and [`LiveEstate`]). Answers what was done (`None`: nothing was due). Blocking.
    pub fn look(
        &self,
        door: &crate::shop_door::ShopDoor,
        reply: &nordisk_pairing::join::JoinReply,
        handle: &tokio::runtime::Handle,
    ) -> Option<(String, Result<String, String>)> {
        let main = crate::primary_control(reply)?;
        let twin = crate::twin_addr(reply).map(|ip| format!("{ip}:{}", crate::CONTROL_PORT))?;
        let bearer = crate::console_key::bearers(reply).ok().flatten()?;
        let estate = LiveEstate::of(handle, reply, bearer);
        self.0.look(
            |u| u.seal(&estate),
            || (answers(&main), answers(&twin)),
            |role| recharge(&Door(door), &estate, role),
        )
    }
}

/// Can `addr` be reached at all (a TCP connect)?
fn answers(addr: &str) -> bool {
    addr.parse::<std::net::SocketAddr>()
        .ok()
        .is_some_and(|a| std::net::TcpStream::connect_timeout(&a, Duration::from_secs(5)).is_ok())
}

/// **The shop's half over the ONE box's `SoldEstate` door** (the shared `ShopDoor`, wrapped:
/// both the trait and the door are shared crates').
pub struct Door<'a>(pub &'a crate::shop_door::ShopDoor);

impl Shop for Door<'_> {
    fn recharge(&self, role: &str, seed: &str, done: bool) -> Result<Laid, String> {
        let r = self.0.recharge_box(role, seed, done)?;
        laid_of(&r.public_ipv4, &r.utility_ipv4, r.words, done)
    }
}

/// Pure: **the box the shop laid, as its answer names it** (`SoldEstate.Recharge`).
pub fn laid_of(public: &str, utility: &str, words: String, done: bool) -> Result<Laid, String> {
    let ip = |s: &str, what: &str| -> Result<Ipv4Addr, String> {
        if done && s.trim().is_empty() {
            return Ok(Ipv4Addr::UNSPECIFIED);
        }
        s.trim()
            .parse()
            .map_err(|e| format!("the shop's {what} address {s:?}: {e}"))
    };
    let utility = ip(utility, "utility")?;
    // A lean Holger main or twin holds no public IPv4 (it leaves through the front): the
    // installer reaches it at its utility address, so that is the address it is re-admitted at.
    let public = if !done && public.trim().is_empty() {
        utility
    } else {
        ip(public, "public")?
    };
    Ok(Laid {
        public,
        utility,
        words,
    })
}

/// **The estate as the live front reaches it**: the sealed slots over holger's
/// RepositoryService, the re-admission over the installer's link.
pub struct LiveEstate {
    pub handle: tokio::runtime::Handle,
    /// The primary's and the twin's control planes, as the verdict names them.
    pub primary: Option<String>,
    pub twin: Option<String>,
    /// The console's bearer per box (A7 row 14).
    pub bearer: crate::console_key::Bearers,
}

impl LiveEstate {
    /// The estate the verdict fed now names.
    pub fn of(
        handle: &tokio::runtime::Handle,
        reply: &nordisk_pairing::join::JoinReply,
        bearer: crate::console_key::Bearers,
    ) -> LiveEstate {
        LiveEstate {
            handle: handle.clone(),
            primary: crate::primary_control(reply),
            twin: crate::twin_addr(reply).map(|ip| format!("{ip}:{}", crate::CONTROL_PORT)),
            bearer,
        }
    }

    fn slot_at(&self, control: String) -> crate::sealed::PrimarySlot {
        crate::sealed::PrimarySlot {
            handle: self.handle.clone(),
            bearer: self.bearer.at(&control).unwrap_or_default().to_string(),
            control,
            repository: crate::sealed::SEALED_REPOSITORY.into(),
            timeout: Duration::from_secs(60),
        }
    }
}

impl Estate for LiveEstate {
    fn kept(&self, lost: &str) -> Result<Vec<u8>, String> {
        let at = if lost == "main" {
            self.twin.clone()
        } else {
            self.primary.clone()
        }
        .ok_or("the verdict names no box that stands")?;
        self.slot_at(at.clone())
            .get(crate::sealed::MATERIAL_OBJECT)?
            .filter(|b| !b.is_empty())
            .ok_or_else(|| format!("no installer material is kept in the sealed slot at {at}"))
    }

    fn readmit(
        &self,
        kept: &Kept,
        role: &str,
        laid: &Laid,
        seed: &str,
    ) -> Result<Readmitted, String> {
        let (after, words) =
            nordisk_pairing::kept::readmit(kept, role, laid.public, laid.utility, SEED_NAME, seed)?;
        Ok(Readmitted {
            kept: after,
            words,
            control: format!("{}:{}", laid.utility, crate::CONTROL_PORT),
        })
    }

    fn store(&self, primary: &str, bundle: &[u8]) -> Result<(), String> {
        let at = if primary.is_empty() {
            self.primary.clone().ok_or("no primary is elected")?
        } else {
            primary.to_string()
        };
        self.slot_at(at).put(crate::sealed::MATERIAL_OBJECT, bundle)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    /// ★ **A lean Holger main is laid with NO public IPv4** (utility only, behind the front:
    /// `holger-monetize-guest` `behind_the_front_interfaces`), so the shop answers its recharge
    /// with an empty public address. The front must still re-admit it, at its utility address
    /// (the installer dials the utility address on the estate's link anyway: nordisk-pairing
    /// `kept::paths_for`). Gate ds5 (lane OX-WGDS3/4, 2026-10-28): the ONE box laid the new main
    /// 6 min after the kill and the front refused the answer ("the shop's public address \"\":
    /// invalid IPv4 address syntax"): the new main waited for its installer for 40 minutes.
    #[test]
    fn a_lean_main_laid_with_no_public_address_is_readmitted_at_its_utility_address() {
        let l = laid_of("", "10.6.0.9", "main laid".into(), false)
            .expect("a main with no public address is still laid");
        assert_eq!(
            (l.public, l.utility),
            (Ipv4Addr::new(10, 6, 0, 9), Ipv4Addr::new(10, 6, 0, 9))
        );
        // A box laid with a public address keeps it.
        let l = laid_of("192.0.2.9", "10.6.0.9", "w".into(), false).unwrap();
        assert_eq!(l.public, Ipv4Addr::new(192, 0, 2, 9));
        // No address at all is no box laid.
        assert!(laid_of("", "", "w".into(), false).is_err());
        assert!(laid_of("192.0.2.9", "nope", "w".into(), false).is_err());
        // The final `done` answers no addresses.
        assert_eq!(
            laid_of("", "", "w".into(), true).unwrap().public,
            Ipv4Addr::UNSPECIFIED
        );
    }

    struct Fake {
        calls: RefCell<Vec<String>>,
        bundle: Vec<u8>,
    }

    impl Shop for Fake {
        fn recharge(&self, role: &str, seed: &str, done: bool) -> Result<Laid, String> {
            self.calls
                .borrow_mut()
                .push(format!("shop {role} done={done} seed={}", !seed.is_empty()));
            Ok(Laid {
                public: Ipv4Addr::new(192, 0, 2, 9),
                utility: Ipv4Addr::new(10, 13, 8, 9),
                words: format!("{role} laid"),
            })
        }
    }

    impl Estate for Fake {
        fn kept(&self, lost: &str) -> Result<Vec<u8>, String> {
            self.calls
                .borrow_mut()
                .push(format!("kept while {lost} is lost"));
            Ok(self.bundle.clone())
        }
        fn readmit(
            &self,
            kept: &Kept,
            role: &str,
            laid: &Laid,
            seed: &str,
        ) -> Result<Readmitted, String> {
            assert!(seed.contains("kind = \"holger\""), "a holger seed: {seed}");
            self.calls
                .borrow_mut()
                .push(format!("readmit {role} at {}", laid.utility));
            Ok(Readmitted {
                kept: kept.clone(),
                words: "readmitted".into(),
                control: format!("{}:50051", laid.utility),
            })
        }
        fn store(&self, primary: &str, _: &[u8]) -> Result<(), String> {
            self.calls.borrow_mut().push(format!("store {primary}"));
            Ok(())
        }
    }

    #[test]
    fn a_lost_main_is_laid_readmitted_and_done_and_its_ledger_sealed_after_its_recharge() {
        let ca = nordisk_pairing::mtls::InstallerCa::mint(
            "acme.holger.example",
            nordisk_pairing::kept::MATERIAL_VALID,
        )
        .unwrap();
        let boot = ca
            .issue_bootstrap(nordisk_pairing::kept::MATERIAL_VALID)
            .unwrap();
        let seed = nordisk_pairing::seed::Seed {
            estate: "acme.holger.example".into(),
            tunnel: nordisk_pairing::seed::TunnelKind::Wireguard,
            front: Default::default(),
            kind: nordisk_pairing::seed::Kind::Holger,
            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: Some(nordisk_pairing::seed::LinkSeed {
                installer_pubkey: "A".repeat(43) + "=",
                vm_private: "B".repeat(43) + "=",
                port: nordisk_pairing::installer_link::LINK_PORT,
                psk: None,
            }),
        };
        let plan = serde_json::json!({"estate": "acme.holger.example", "tunnel": "wireguard", "vms": [], "budget_s": 60});
        let files = serde_json::json!({"ca.cert.pem": ca.cert_pem, "ca.key.pem": ca.key_pem, "seed.holger.toml": seed.render().unwrap()});
        let bundle = serde_json::to_vec(&serde_json::json!({"v": nordisk_pairing::kept::BUNDLE_V, "files": files, "ledger": "", "plan": plan})).unwrap();
        let f = Fake {
            calls: RefCell::default(),
            bundle,
        };
        let (words, unsealed) = recharge(&f, &f, "main").expect("re-admitted");
        assert_eq!(
            f.calls.borrow().as_slice(),
            ["kept while main is lost", "shop main done=false seed=true", "readmit main at 10.13.8.9", "shop main done=true seed=false"],
            "the re-admission fills nothing and writes nothing into a held main (the fill is the one lane's: the ONE box's order, or the front's autofill)"
        );
        assert!(
            words.contains("main laid") && words.contains("recharge_from_twin order"),
            "{words}"
        );
        let u = unsealed.expect("a new main's ledger waits for its recharge");
        assert_eq!(u.primary, "10.13.8.9:50051");
        f.calls.borrow_mut().clear();
        u.seal(&f).expect("sealed once released");
        assert_eq!(f.calls.borrow().as_slice(), ["store 10.13.8.9:50051"]);
        f.calls.borrow_mut().clear();
        let (_, none) = recharge(&f, &f, "twin").expect("a twin");
        assert!(
            none.is_none(),
            "a twin's ledger goes to the primary at once"
        );
        assert!(
            f.calls.borrow().iter().any(|c| c == "store "),
            "{:?}",
            f.calls.borrow()
        );
        assert!(recharge(&f, &f, "front").is_err());
    }
}