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