use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::sync::Mutex;
use std::time::Duration;
use crate::door::Door;
use crate::installer::{Material, Plan, Sink};
use crate::mtls::{InstallerCa, Issued};
use crate::seed::Seed;
pub const BUNDLE_V: &str = "installer-material/1";
pub const SEED_FILE_PREFIX: &str = "seed.";
pub const MATERIAL_VALID: Duration = Duration::from_secs(180 * 24 * 3600);
pub const READMIT_BUDGET_S: u64 = 2 * 3600;
#[derive(Clone)]
pub struct Kept {
pub files: BTreeMap<String, String>,
pub ledger: String,
pub plan: Plan,
}
impl std::fmt::Debug for Kept {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Kept").field("files", &self.files.keys().collect::<Vec<_>>()).field("estate", &self.plan.estate).finish_non_exhaustive()
}
}
pub fn seed_file(name: &str) -> String {
format!("{SEED_FILE_PREFIX}{name}.toml")
}
impl Kept {
pub fn parse(bytes: &[u8]) -> Result<Kept, String> {
let v: serde_json::Value = serde_json::from_slice(bytes).map_err(|e| format!("the kept bundle is not JSON: {e}"))?;
if v["v"] != BUNDLE_V {
return Err(format!("the kept bundle is {:?}, not {BUNDLE_V}", v["v"]));
}
Ok(Kept {
files: serde_json::from_value(v["files"].clone()).map_err(|e| format!("the kept files: {e}"))?,
ledger: v["ledger"].as_str().ok_or("the kept bundle holds no ledger")?.to_string(),
plan: serde_json::from_value(v["plan"].clone()).map_err(|e| format!("the kept plan: {e}"))?,
})
}
pub fn render(&self) -> Result<Vec<u8>, String> {
serde_json::to_vec(&serde_json::json!({"v": BUNDLE_V, "files": self.files, "ledger": self.ledger, "plan": self.plan})).map_err(|e| e.to_string())
}
fn file(&self, name: &str) -> Result<&str, String> {
self.files.get(name).map(String::as_str).ok_or_else(|| format!("the kept bundle holds no {name}"))
}
pub fn ca(&self) -> Result<InstallerCa, String> {
Ok(InstallerCa { cert_pem: self.file(crate::installer::CA_CERT)?.to_string(), key_pem: self.file(crate::installer::CA_KEY)?.to_string() })
}
pub fn material(&self) -> Result<Material, String> {
let ca = self.ca()?;
let me = ca.issue_installer(MATERIAL_VALID).map_err(|e| e.to_string())?;
let link = Material::link_of(self.file(crate::installer::LINK_KEYS)?)?;
Ok(Material { ca, me, link: Some(link) })
}
pub fn fresh_seed(&self, name: &str) -> Result<String, String> {
let file = seed_file(name);
let mut seed = Seed::parse(self.file(&file)?).map_err(|e| format!("the kept {file}: {e}"))?;
let boot: Issued = self.ca()?.issue_bootstrap(MATERIAL_VALID).map_err(|e| e.to_string())?;
seed.bootstrap_cert_pem = boot.cert_pem;
seed.bootstrap_key_pem = boot.key_pem;
seed.check().map_err(|e| e.to_string())?;
seed.render().map_err(|e| e.to_string())
}
pub fn door(&self) -> Result<Door, String> {
toml::from_str(&self.ledger).map_err(|e| format!("the kept ledger: {e}"))
}
}
pub fn moved(plan: &Plan, role: &str, public: Ipv4Addr, utility: Ipv4Addr) -> Result<Plan, String> {
use crate::installer::{feed_wg_psk_for, mint_wg_psk, FEED_WG_PSK};
use crate::seed::Kind;
let mut p = plan.clone();
let vm = p.vms.iter_mut().find(|v| v.name == role).ok_or_else(|| format!("the kept plan names no {role}"))?;
vm.public = public;
vm.utility = utility;
if vm.kind != Kind::Front {
let psk = mint_wg_psk()?;
vm.feed.insert(FEED_WG_PSK.to_string(), psk.clone());
if let Some(front) = p.vms.iter_mut().find(|v| v.kind == Kind::Front) {
front.feed.insert(feed_wg_psk_for(role), psk);
}
}
p.budget_s = p.budget_s.max(READMIT_BUDGET_S);
p.check()?;
Ok(p)
}
pub struct ReadmitLog {
said: Mutex<std::collections::BTreeSet<u64>>,
status: Mutex<BTreeMap<String, String>>,
}
impl ReadmitLog {
pub const KEEP: usize = 20_000;
pub const fn new() -> ReadmitLog {
ReadmitLog { said: Mutex::new(std::collections::BTreeSet::new()), status: Mutex::new(BTreeMap::new()) }
}
pub fn once(&self, line: &str) -> bool {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
line.hash(&mut h);
let mut said = self.said.lock().unwrap_or_else(|p| p.into_inner());
if said.len() >= Self::KEEP {
said.clear();
}
said.insert(h.finish())
}
pub fn changed(&self, vm: &str, now: &str) -> bool {
let mut status = self.status.lock().unwrap_or_else(|p| p.into_inner());
if status.get(vm).is_some_and(|was| was == now) {
return false;
}
status.insert(vm.to_string(), now.to_string());
true
}
}
impl Default for ReadmitLog {
fn default() -> ReadmitLog {
ReadmitLog::new()
}
}
pub static READMIT_LOG: ReadmitLog = ReadmitLog::new();
fn to_stderr(line: &str) {
eprintln!("{line}");
}
pub struct MemSink {
pub ledger: Mutex<Option<String>>,
log: &'static ReadmitLog,
out: fn(&str),
}
impl Default for MemSink {
fn default() -> MemSink {
MemSink::with(&READMIT_LOG, to_stderr)
}
}
impl MemSink {
pub fn with(log: &'static ReadmitLog, out: fn(&str)) -> MemSink {
MemSink { ledger: Mutex::new(None), log, out }
}
fn say_once(&self, line: String) {
if self.log.once(&line) {
(self.out)(&line);
}
}
pub fn take(&self) -> Result<String, String> {
self.ledger.lock().unwrap_or_else(|p| p.into_inner()).clone().ok_or_else(|| "the re-admission left no ledger".to_string())
}
}
pub fn is_echo(line: &str) -> bool {
line.match_indices("readmit").any(|(at, _)| {
let rest = &line[at + "readmit".len()..];
if rest.starts_with(": ") {
return true;
}
let Some(rest) = rest.strip_prefix(' ') else { return false };
let vm = rest.split(": ").next().unwrap_or("");
rest.len() > vm.len() && !vm.is_empty() && vm.chars().all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
})
}
impl Sink for MemSink {
fn lines(&self, vm: &str, lines: &[String]) {
for l in lines.iter().filter(|l| !is_echo(l)) {
self.say_once(format!("readmit {vm}: {l}"));
}
}
fn status(&self, vm: &str, signal: Option<&crate::signal::Signal>, last_line: &str) {
let now = format!("{} | {last_line}", signal.map(|s| s.line()).unwrap_or_else(|| "no signal".into()));
if self.log.changed(vm, &now) {
(self.out)(&format!("readmit {vm}: status {now}"));
}
}
fn ledger(&self, door: &Door) {
if let Ok(t) = toml::to_string(door) {
*self.ledger.lock().unwrap_or_else(|p| p.into_inner()) = Some(t);
}
}
fn say(&self, line: &str) {
self.say_once(format!("readmit: {line}"));
}
fn secret_offer(&self, vm: &str, _offer: &crate::bootstrap::SecretOffer) -> Result<(), String> {
self.say_once(format!("readmit {vm}: a secret offer (kept by its box)"));
Ok(())
}
}
#[cfg(feature = "installer-wireguard")]
pub type BoxPath = Box<dyn crate::installer_link::Path>;
#[cfg(feature = "installer-wireguard")]
pub fn paths_for(plan: &Plan, link: &crate::installer_link::LinkKeys) -> Result<Vec<(crate::installer::PlanVm, BoxPath)>, String> {
use std::net::SocketAddrV4;
let mut out: Vec<(crate::installer::PlanVm, BoxPath)> = Vec::new();
for vm in &plan.vms {
let path: BoxPath = if vm.kind == crate::seed::Kind::Front {
Box::new(crate::installer_link::DirectPath { at: SocketAddrV4::new(crate::installer_link::LINK_VM, crate::bootstrap::PORT) })
} else {
Box::new(crate::installer_link::WgPath::new(SocketAddrV4::new(vm.utility, plan.link_port), link.clone())?)
};
out.push((vm.clone(), path));
}
Ok(out)
}
#[cfg(feature = "installer-wireguard")]
pub fn readmit(kept: &Kept, role: &str, public: Ipv4Addr, utility: Ipv4Addr, seed_name: &str, seed: &str) -> Result<(Kept, String), String> {
let m = kept.material()?;
let plan = moved(&kept.plan, role, public, utility)?;
let paths = paths_for(&plan, m.link.as_ref().ok_or("the kept material holds no link keys")?)?;
let sink = MemSink::default();
let (readmitted, outcome) = crate::installer::readmit(&plan, kept.door()?, &m, role, paths, &sink)?;
let mut after = kept.clone();
after.ledger = sink.take()?;
after.plan = plan;
after.files.insert(seed_file(seed_name), seed.to_string());
let words = format!(
"{role} re-admitted by the front: from {} to {} / {}{}; {} box(es) closed",
readmitted.was.0,
readmitted.now.0,
readmitted.now.1,
readmitted.recharge_from.as_deref().map(|f| format!(", told to fill from its twin at {f}")).unwrap_or_default(),
outcome.vms.iter().filter(|v| v.closed).count()
);
Ok((after, words))
}
#[cfg(feature = "installer-wireguard")]
pub fn readmit_upgraded(kept: &Kept, role: &str) -> Result<(Kept, String), String> {
let m = kept.material()?;
let paths = paths_for(&kept.plan, m.link.as_ref().ok_or("the kept material holds no link keys")?)?;
let sink = MemSink::default();
let (_readmitted, outcome) = crate::installer::readmit_kept(&kept.plan, kept.door()?, &m, role, paths, &sink)?;
let mut after = kept.clone();
after.ledger = sink.take()?;
Ok((after, format!("{role} re-admitted by the front after its upgrade (its data kept); {} box(es) closed", outcome.vms.iter().filter(|v| v.closed).count())))
}
#[cfg(feature = "installer-wireguard")]
pub fn readmit_together(kept: &Kept, roles: &[&str]) -> Result<(Kept, String), String> {
if roles.is_empty() {
return Err("REFUSED [installer-readmit] no box is named to re-admit".into());
}
if let Some(r) = roles.iter().find(|r| !kept.plan.vms.iter().any(|v| v.name == **r)) {
return Err(format!("REFUSED [installer-readmit] the kept plan names no {r}"));
}
let m = kept.material()?;
let paths = paths_for(&kept.plan, m.link.as_ref().ok_or("the kept material holds no link keys")?)?;
let sink = MemSink::default();
let names: Vec<String> = roles.iter().map(|r| r.to_string()).collect();
let (_readmitted, outcome) = crate::installer::readmit_kept_together(&kept.plan, kept.door()?, &m, &names, paths, &sink)?;
let mut after = kept.clone();
after.ledger = sink.take()?;
Ok((after, format!("{} re-admitted by the front after its upgrade (its data kept); {} box(es) closed", roles.join(" and "), outcome.vms.iter().filter(|v| v.closed).count())))
}
#[cfg(test)]
mod tests {
use super::*;
static LOOP_LOG: ReadmitLog = ReadmitLog::new();
static SAID: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn collect(line: &str) {
SAID.lock().unwrap().push(line.to_string());
}
#[test]
fn a_loop_of_readmission_rounds_says_each_line_once() {
const N: usize = 50;
for round in 0..N {
let sink = MemSink::with(&LOOP_LOG, collect);
sink.lines("main", &["korp-installer: INSTALL-OK".into(), "REFUSED [install-path] this listener has /install/hello".into()]);
sink.status("main", None, "REFUSED [install-path] this listener has /install/hello");
sink.say("main: reaching 10.13.8.2:4443");
sink.say("main: not yet — REFUSED [install-path]");
if round == N - 1 {
sink.status("main", None, "gunnar: first boot");
sink.status("main", None, "gunnar: first boot");
}
}
let said = SAID.lock().unwrap().clone();
assert_eq!(said.len(), 6, "each line once and the changed status once over {N} rounds: {said:#?}");
assert_eq!(said.iter().filter(|l| l.contains("status")).count(), 2, "the status once, then once more when it changed");
assert!(said.iter().all(|l| l.starts_with("readmit")), "{said:#?}");
}
#[test]
fn the_readmit_log_forgets_at_its_bound() {
let log = ReadmitLog::new();
assert!(log.once("a") && !log.once("a"));
for i in 0..ReadmitLog::KEEP {
log.once(&format!("line {i}"));
}
assert!(log.once("a"), "forgotten at the bound, said once more");
assert!(log.changed("main", "x") && !log.changed("main", "x") && log.changed("main", "y") && log.changed("twin", "y"));
}
static ECHO_LOG: ReadmitLog = ReadmitLog::new();
static FRONT_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
static ECHOED: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn into_front_log(line: &str) {
ECHOED.lock().unwrap().push(line.to_string());
FRONT_LOG.lock().unwrap().push(format!("2026-10-25T21:04:05.123Z INFO stderr: {line}"));
}
#[test]
fn the_sinks_own_lines_read_back_from_the_fronts_log_are_not_said_again() {
FRONT_LOG.lock().unwrap().extend(["2026-10-25T21:04:00.000Z WARN gunnar_front::recharge: recharge: the box is gone; the front asks the shop to lay it again".to_string(), "korp-installer: INSTALL-OK".to_string()]);
let mut cursor = 0;
for _ in 0..50 {
let sink = MemSink::with(&ECHO_LOG, into_front_log);
let new: Vec<String> = FRONT_LOG.lock().unwrap()[cursor..].to_vec();
cursor += new.len();
sink.lines("front", &new);
sink.say("front: reaching 10.13.8.1:4443");
}
let said = ECHOED.lock().unwrap().clone();
assert_eq!(said.len(), 3, "the two real lines and the narration, once each: {said:#?}");
assert!(said.iter().all(|l| l.matches("readmit").count() == 1), "nothing nested: {said:#?}");
assert!(FRONT_LOG.lock().unwrap().len() < 10, "the front's log stops growing");
}
#[test]
fn an_echo_is_the_sinks_own_line_and_only_that() {
for l in ["readmit main: korp-installer: INSTALL-OK", "readmit: main: reaching 10.13.8.2:4443", "2026-10-25T21:04:05Z INFO stderr: readmit twin: status no signal | x", "<14>readmit front: a secret offer (kept by its box)"] {
assert!(is_echo(l), "{l}");
}
for l in ["korp-installer: INSTALL-OK", "recharge: main re-admitted by the front: from 192.0.2.2 to 192.0.2.9", "REFUSED [installer-readmit] the kept plan names no x", "readmitted", "readmitting main now", "readmit : x"] {
assert!(!is_echo(l), "{l}");
}
}
use crate::installer::PlanVm;
use crate::seed::{Front, Kind, LinkSeed, TunnelKind};
fn bundle() -> (Vec<u8>, InstallerCa) {
let ca = InstallerCa::mint("acme.holger.example", MATERIAL_VALID).unwrap();
let me = ca.issue_installer(MATERIAL_VALID).unwrap();
let boot = ca.issue_bootstrap(MATERIAL_VALID).unwrap();
let seed = Seed {
estate: "acme.holger.example".into(),
tunnel: TunnelKind::Wireguard,
front: Front::default(),
kind: Kind::Holger,
installer_ca_pem: ca.cert_pem.clone(),
bootstrap_cert_pem: boot.cert_pem.clone(),
bootstrap_key_pem: boot.key_pem.clone(),
peers_expected: 3,
mullvad: None,
tailscale: None,
installer_link: Some(LinkSeed { installer_pubkey: "A".repeat(43) + "=", vm_private: "B".repeat(43) + "=", port: crate::installer_link::LINK_PORT, psk: None }),
};
let vm = |name: &str, kind: Kind, last: u8| PlanVm { name: name.into(), public: Ipv4Addr::new(192, 0, 2, last), utility: Ipv4Addr::new(10, 13, 8, last), overlay: Ipv4Addr::new(10, 44, 0, last), kind, feed: BTreeMap::new() };
let plan = Plan {
estate: "acme.holger.example".into(),
tunnel: TunnelKind::Wireguard,
vms: vec![vm("front", Kind::Front, 1), vm("main", Kind::Holger, 2), vm("twin", Kind::Holger, 3)],
feed: BTreeMap::new(),
front: None,
port: crate::bootstrap::PORT,
link_port: crate::installer_link::LINK_PORT,
budget_s: 600,
close_when: Default::default(),
box_cert_valid_s: None,
front_waits_for: Vec::new(),
};
let files: BTreeMap<String, String> = [
(crate::installer::CA_CERT.to_string(), ca.cert_pem.clone()),
(crate::installer::CA_KEY.to_string(), ca.key_pem.clone()),
(crate::installer::INSTALLER_CERT.to_string(), me.cert_pem),
(crate::installer::INSTALLER_KEY.to_string(), me.key_pem),
(crate::installer::LINK_KEYS.to_string(), Material::link_file(&("C".repeat(43) + "="), &("D".repeat(43) + "="))),
(seed_file("holger"), seed.render().unwrap()),
]
.into();
(serde_json::to_vec(&serde_json::json!({"v": BUNDLE_V, "files": files, "ledger": "", "plan": plan})).unwrap(), ca)
}
#[test]
fn a_box_laid_again_gets_a_new_preshared_key_and_the_front_the_same_one() {
use crate::installer::{feed_wg_psk_for, FEED_WG_PSK};
let (b, _) = bundle();
let k = Kept::parse(&b).unwrap();
let (old_main, old_twin) = (crate::installer::mint_wg_psk().unwrap(), crate::installer::mint_wg_psk().unwrap());
let plan = k.plan.clone().with_wg_psks(&BTreeMap::from([("main".to_string(), old_main.clone()), ("twin".to_string(), old_twin.clone())])).unwrap();
plan.check().unwrap();
let get = |p: &Plan, n: &str| p.vms.iter().find(|v| v.name == n).unwrap().feed.clone();
let p = moved(&plan, "main", Ipv4Addr::new(192, 0, 2, 9), Ipv4Addr::new(10, 13, 8, 9)).unwrap();
let new_main = get(&p, "main").get(FEED_WG_PSK).cloned().expect("the moved box is fed a PSK");
assert_ne!(new_main, old_main, "a new machine, a new key: the PSK rotates");
assert_eq!(get(&p, "front").get(&feed_wg_psk_for("main")), Some(&new_main), "the front holds the same one");
assert_eq!(get(&p, "twin").get(FEED_WG_PSK), Some(&old_twin), "the twin's link stands");
assert_eq!(get(&p, "front").get(&feed_wg_psk_for("twin")), Some(&old_twin));
p.check().unwrap();
let mut back = k.clone();
back.plan = p.clone();
let again = Kept::parse(&back.render().unwrap()).unwrap();
assert_eq!(get(&again.plan, "main").get(FEED_WG_PSK), Some(&new_main), "the kept bundle carries it (the twin's copy)");
assert!(!format!("{again:?}").contains(&new_main), "never a PSK in Debug");
let bare = moved(&k.plan, "main", Ipv4Addr::new(192, 0, 2, 9), Ipv4Addr::new(10, 13, 8, 9)).unwrap();
let fed = get(&bare, "main").get(FEED_WG_PSK).cloned().expect("a PSK for the box laid again");
assert_eq!(get(&bare, "front").get(&feed_wg_psk_for("main")), Some(&fed));
assert!(!get(&bare, "twin").contains_key(FEED_WG_PSK), "the standing twin is untouched");
bare.check().unwrap();
}
#[test]
fn a_fresh_seed_and_the_kept_material_keep_the_links_preshared_key() {
let (b, _) = bundle();
let mut k = Kept::parse(&b).unwrap();
let psk = crate::installer::mint_wg_psk().unwrap();
let mut seed = Seed::parse(&k.files[&seed_file("holger")]).unwrap();
seed.installer_link.as_mut().unwrap().psk = Some(psk.clone());
k.files.insert(seed_file("holger"), seed.render().unwrap());
let (ip, vp) = (wg_appliance_core::keys::generate_keypair_b64().0, wg_appliance_core::keys::generate_keypair_b64().1);
k.files.insert(crate::installer::LINK_KEYS.to_string(), Material::link_file_with_psk(&ip, &vp, Some(&psk)));
let fresh = Seed::parse(&k.fresh_seed("holger").unwrap()).unwrap();
assert_eq!(fresh.installer_link.unwrap().psk, Some(psk.clone()), "the fresh seed keeps the link's PSK");
assert_eq!(k.material().unwrap().link.unwrap().psk, Some(wg_appliance_core::decode_key(&psk).unwrap()), "the kept material dials with it");
let again = Kept::parse(&k.render().unwrap()).unwrap();
assert_eq!(Seed::parse(&again.fresh_seed("holger").unwrap()).unwrap().installer_link.unwrap().psk, Some(psk), "the bundle as the twin keeps it");
}
#[test]
fn a_kept_bundle_gives_a_fresh_seed_fresh_material_and_a_moved_plan() {
let (b, ca) = bundle();
let k = Kept::parse(&b).expect("parses");
assert!(!format!("{k:?}").contains("PRIVATE"), "never a key in Debug");
let fresh = Seed::parse(&k.fresh_seed("holger").unwrap()).unwrap();
let kept = Seed::parse(&k.files[&seed_file("holger")]).unwrap();
assert_eq!((fresh.estate.as_str(), fresh.kind, fresh.installer_ca_pem.as_str()), (kept.estate.as_str(), Kind::Holger, ca.cert_pem.as_str()));
assert_ne!(fresh.bootstrap_cert_pem, kept.bootstrap_cert_pem, "a NEW bootstrap certificate");
assert!(k.fresh_seed("gunnar").unwrap_err().contains("seed.gunnar.toml"), "a seed the bundle does not hold is named");
let m = k.material().unwrap();
assert_ne!(m.me.cert_pem, k.files[crate::installer::INSTALLER_CERT], "a fresh installer certificate");
assert!(m.link.is_some());
let p = moved(&k.plan, "main", Ipv4Addr::new(192, 0, 2, 9), Ipv4Addr::new(10, 13, 8, 9)).unwrap();
let main = p.vms.iter().find(|v| v.name == "main").unwrap();
assert_eq!((main.public, main.utility), (Ipv4Addr::new(192, 0, 2, 9), Ipv4Addr::new(10, 13, 8, 9)));
assert_eq!(p.vms.iter().find(|v| v.name == "twin").unwrap().utility, Ipv4Addr::new(10, 13, 8, 3), "the twin stands where it stood");
assert_eq!(p.budget_s, READMIT_BUDGET_S);
assert!(moved(&k.plan, "nobody", Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST).is_err());
let again = Kept::parse(&k.render().unwrap()).unwrap();
assert_eq!(again.files, k.files);
assert!(Kept::parse(br#"{"v":"installer-material/0"}"#).unwrap_err().contains(BUNDLE_V));
}
#[cfg(feature = "installer-wireguard")]
#[test]
fn a_together_readmission_refuses_by_name_before_it_dials() {
let (b, _) = bundle();
let k = Kept::parse(&b).unwrap();
assert!(readmit_together(&k, &[]).unwrap_err().contains("[installer-readmit]"), "no box named");
assert!(readmit_together(&k, &["nas"]).unwrap_err().contains("nas"), "a box the plan does not hold");
let mut keyless = k.clone();
keyless.files.remove(crate::installer::LINK_KEYS);
assert!(readmit_together(&keyless, &["main", "twin"]).unwrap_err().contains(crate::installer::LINK_KEYS), "no link keys");
}
}