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> {
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;
p.budget_s = p.budget_s.max(READMIT_BUDGET_S);
p.check()?;
Ok(p)
}
#[derive(Default)]
pub struct MemSink {
pub ledger: Mutex<Option<String>>,
}
impl Sink for MemSink {
fn lines(&self, vm: &str, lines: &[String]) {
for l in lines {
eprintln!("readmit {vm}: {l}");
}
}
fn status(&self, _vm: &str, _signal: Option<&crate::signal::Signal>, _last_line: &str) {}
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) {
eprintln!("readmit: {line}");
}
fn secret_offer(&self, vm: &str, _offer: &crate::bootstrap::SecretOffer) -> Result<(), String> {
eprintln!("readmit {vm}: a secret offer (kept by its box)");
Ok(())
}
}
impl MemSink {
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())
}
}
#[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(test)]
mod tests {
use super::*;
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 }),
};
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_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));
}
}