use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use crate::bootstrap::{SecretOffer, Vm};
use crate::boxside::{write_atomic, CLOSED_REL, JOINED_REL, OWN_CERT_REL, OWN_KEY_REL, SIGNAL_REL};
use crate::join::{JoinReply, JoinRequest};
use crate::mtls::{self, Issued};
use crate::pair::{Event, Machine, State};
use crate::seed::{Kind, Seed};
use crate::signal::Signal;
use crate::Role;
pub trait FrontProduct: Send + Sync {
fn tunnel_pubkey(&self) -> String;
fn offer(&self, fed: Option<&JoinReply>) -> BTreeMap<String, String>;
fn secret_offer(&self) -> SecretOffer {
SecretOffer::default()
}
fn fed(&self, _reply: &JoinReply) {}
fn live_running(&self) -> Result<crate::live::Running, String> {
Err("REFUSED [live-unsupported] this VM takes no live update".into())
}
fn live_stage(&self, _stage: &crate::live::Stage) -> Result<String, String> {
Err("REFUSED [live-unsupported] this VM takes no live update".into())
}
fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
if declared.is_empty() {
self.live_stage(stage)
} else {
Err(crate::live::declared_unsupported())
}
}
}
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, which is vm1 and nothing else. Lay main and twin with the product medium",
reply.me.name, reply.role
)
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
pub primary: String,
pub primary_utility: Ipv4Addr,
pub twin: String,
pub twin_utility: Ipv4Addr,
}
pub fn verdict(reply: &JoinReply) -> Option<Verdict> {
let of = |role: Role| reply.peers.iter().find(|p| p.role == role);
let twin = of(Role::Twin)?;
let primary = of(Role::Primary)?;
Some(Verdict { primary: primary.name.clone(), primary_utility: reply.primary_utility.or(primary.utility)?, twin: twin.name.clone(), twin_utility: twin.utility? })
}
pub fn primary_utility(reply: &JoinReply) -> Option<Ipv4Addr> {
reply.primary_utility.or_else(|| reply.peers.iter().find(|p| p.role == Role::Primary).and_then(|p| p.utility))
}
pub fn relay_list(peers: &[crate::join::Peer]) -> Vec<(String, String, Ipv4Addr)> {
peers.iter().filter(|p| p.role != Role::Front && !p.tunnel_pubkey.trim().is_empty()).map(|p| (p.name.clone(), p.tunnel_pubkey.trim().to_string(), p.overlay)).collect()
}
pub fn relay_list_with_psk(reply: &JoinReply) -> Vec<(String, String, Ipv4Addr, Option<String>)> {
relay_list(&reply.peers)
.into_iter()
.map(|(name, key, overlay)| {
let psk = reply.feed.get(&crate::installer::feed_wg_psk_for(&name)).map(|p| p.trim().to_string());
(name, key, overlay, psk)
})
.collect()
}
#[cfg(any(feature = "frontside-update", test))]
pub mod update;
pub mod busy {
use std::collections::BTreeMap;
use std::sync::Mutex;
static BUSY: Mutex<BTreeMap<String, u32>> = Mutex::new(BTreeMap::new());
#[must_use = "the role is busy only while the guard lives"]
pub struct Guard(String);
impl Drop for Guard {
fn drop(&mut self) {
let mut b = BUSY.lock().unwrap_or_else(|p| p.into_inner());
if let Some(n) = b.get_mut(&self.0) {
*n = n.saturating_sub(1);
if *n == 0 {
b.remove(&self.0);
}
}
}
}
pub fn on(role: &str) -> Guard {
*BUSY.lock().unwrap_or_else(|p| p.into_inner()).entry(role.to_string()).or_default() += 1;
Guard(role.to_string())
}
pub fn busy(role: Option<&str>) -> bool {
let b = BUSY.lock().unwrap_or_else(|p| p.into_inner());
match role {
Some(r) => b.get(r).is_some_and(|n| *n > 0),
None => b.values().any(|n| *n > 0),
}
}
}
pub struct Front<P: FrontProduct> {
root: PathBuf,
seed: Seed,
own_key: String,
fed: Mutex<Option<JoinReply>>,
machine: Mutex<Machine>,
relay_up: AtomicBool,
pub product: P,
}
impl<P: FrontProduct> Front<P> {
pub fn open(root: &Path, seed: Seed, product: P) -> Result<Front<P>, String> {
let own_key = match std::fs::read_to_string(root.join(OWN_KEY_REL)) {
Ok(k) if k.contains("PRIVATE KEY") => k,
_ => {
let (_, key) = mtls::box_request().map_err(|e| e.to_string())?;
write_atomic(root, OWN_KEY_REL, key.as_bytes(), true).map_err(|e| format!("{}: {e}", root.join(OWN_KEY_REL).display()))?;
key
}
};
let fed = crate::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 f = Front { root: root.to_path_buf(), seed, own_key, fed: Mutex::new(fed), machine: Mutex::new(Machine::default()), relay_up: AtomicBool::new(false), product };
f.advance();
Ok(f)
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn seed(&self) -> &Seed {
&self.seed
}
pub fn installer_ca_pem(&self) -> &str {
&self.seed.installer_ca_pem
}
pub fn latest(&self) -> Option<JoinReply> {
self.fed.lock().ok().and_then(|f| f.clone())
}
pub fn closed(&self) -> bool {
self.root.join(CLOSED_REL).exists()
}
fn own_cert(&self) -> Option<String> {
std::fs::read_to_string(self.root.join(OWN_CERT_REL)).ok().filter(|c| c.contains("BEGIN CERTIFICATE"))
}
pub fn own_identity(&self) -> Option<Issued> {
self.own_cert().map(|cert_pem| Issued { cert_pem, key_pem: self.own_key.clone() })
}
pub fn relay_started(&self) {
self.relay_up.store(true, Ordering::SeqCst);
self.advance();
}
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);
if let Some(text) = s.render() {
let _ = write_atomic(&self.root, SIGNAL_REL, text.as_bytes(), false);
}
}
}
}
impl<P: FrontProduct> Vm for Front<P> {
fn live_running(&self) -> Result<crate::live::Running, String> {
self.product.live_running()
}
fn live_stage(&self, stage: &crate::live::Stage) -> Result<String, String> {
self.product.live_stage(stage)
}
fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
self.product.live_stage_declared(stage, declared)
}
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, tunnel_pubkey: self.product.tunnel_pubkey(), 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 {
write_atomic(&self.root, OWN_CERT_REL, cert.as_bytes(), true).map_err(|e| e.to_string())?;
}
let text = reply.render().map_err(|e| e.to_string())?;
crate::fed_at_rest::write(&self.root, JOINED_REL, &text)?;
*self.fed.lock().map_err(|e| e.to_string())? = Some(reply.clone());
self.advance();
self.product.fed(reply);
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> {
write_atomic(&self.root, CLOSED_REL, b"closed by the installer\n", false).map_err(|e| e.to_string())
}
fn identity(&self) -> Issued {
self.own_identity().unwrap_or_else(|| Issued { cert_pem: self.seed.bootstrap_cert_pem.clone(), key_pem: self.seed.bootstrap_key_pem.clone() })
}
fn offer(&self) -> BTreeMap<String, String> {
self.product.offer(self.latest().as_ref())
}
fn secret_offer(&self) -> SecretOffer {
self.product.secret_offer()
}
fn refed(&self, reply: &JoinReply) -> Result<(), String> {
if let Some(why) = crate::bootstrap::refeed_refusal(self.latest().as_ref(), reply) {
return Err(why);
}
self.fed(reply)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::join::{Assigned, Peer};
use crate::seed::TunnelKind;
use std::time::Duration;
struct P;
impl FrontProduct for P {
fn tunnel_pubkey(&self) -> String {
"KF".into()
}
fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
BTreeMap::from([("version".into(), "t".into())])
}
fn secret_offer(&self) -> SecretOffer {
SecretOffer::new(BTreeMap::from([("tls.x.cert".into(), "C".into())]))
}
}
struct LiveF;
impl FrontProduct for LiveF {
fn tunnel_pubkey(&self) -> String {
String::new()
}
fn offer(&self, _fed: Option<&JoinReply>) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn live_running(&self) -> Result<crate::live::Running, String> {
Ok(crate::live::Running { base: "b".into(), app: "f".into(), slot: "b".into(), live_update: true })
}
fn live_stage(&self, s: &crate::live::Stage) -> Result<String, String> {
Ok(format!("front staged {}", s.pin_hex))
}
}
#[test]
fn the_front_answers_the_live_door_with_its_products_slot_or_refuses_by_name() {
let d = dir("live");
let f = Front::open(&d, seed(), LiveF).unwrap();
assert_eq!(f.live_running().unwrap().slot, "b");
let st = crate::live::Stage { pin_hex: "9".repeat(64), signature: vec![1], base: "b".into(), archive: vec![2] };
assert!(f.live_stage(&st).unwrap().starts_with("front staged"));
let plain = Front::open(&d, seed(), P).unwrap();
assert!(plain.live_running().unwrap_err().contains("[live-unsupported]"));
let _ = std::fs::remove_dir_all(&d);
}
fn seed() -> Seed {
let ca = mtls::InstallerCa::mint("front.test", Duration::from_secs(3600)).unwrap();
let boot = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
Seed {
estate: "front.test".into(),
tunnel: TunnelKind::Wireguard,
front: Default::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, util: Option<[u8; 4]>) -> Peer {
Peer { name: name.into(), role, tunnel_pubkey: String::new(), overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: util.map(Ipv4Addr::from) }
}
fn fed(role: Option<Role>, peers: Vec<Peer>) -> JoinReply {
JoinReply { estate: "front.test".into(), me: Assigned { name: "vm1".into(), overlay: crate::OVERLAY_FRONT }, cert_pem: None, role, primary_overlay: None, primary_utility: None, peers, feed: BTreeMap::new() }
}
fn dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("frontside-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
#[test]
fn the_front_presents_kind_front_with_its_relay_key_no_capacity_and_refuses_any_other_role() {
let d = dir("refuse");
let f = Front::open(&d, seed(), P).unwrap();
let h = f.hello().unwrap();
assert_eq!((h.kind, h.tunnel_pubkey.as_str(), h.capacity.is_none(), h.csr_pem.is_empty()), (Kind::Front, "KF", true, false));
for role in [None, Some(Role::Primary), Some(Role::Twin)] {
assert!(f.fed(&fed(role, vec![])).unwrap_err().contains("[front-medium-is-only-the-front]"));
}
assert!(f.latest().is_none() && !d.join(JOINED_REL).exists(), "nothing kept");
assert_eq!(f.secret_offer().keys(), vec!["tls.x.cert".to_string()], "the product's secret offer");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn the_front_is_paired_when_its_relay_is_up_and_every_box_is_listed_and_resumes_after_a_restart() {
let d = dir("paired");
let f = Front::open(&d, seed(), P).unwrap();
f.fed(&fed(Some(Role::Front), vec![peer("vm1", Role::Front, None)])).unwrap();
assert!(f.status().joined && !f.status().paired);
f.relay_started();
assert!(!f.status().paired, "main and twin not listed yet");
f.fed(&fed(Some(Role::Front), vec![peer("vm1", Role::Front, None), peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
assert!(f.status().paired, "{:?}", f.status());
assert!(d.join(SIGNAL_REL).exists());
let again = Front::open(&d, seed(), P).unwrap();
assert_eq!(again.latest().map(|r| r.me.name).as_deref(), Some("vm1"));
again.relay_started();
assert!(again.status().paired);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_closed_front_takes_a_refeed_with_the_new_primary_and_refuses_a_change_of_itself() {
let d = dir("refeed");
let f = Front::open(&d, seed(), P).unwrap();
f.fed(&fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
f.close().unwrap();
f.refed(&fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 9])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))])).unwrap();
assert_eq!(f.latest().as_ref().and_then(primary_utility), Some(Ipv4Addr::new(10, 0, 0, 9)), "the front dials the new primary");
assert!(f.refed(&fed(Some(Role::Primary), vec![])).unwrap_err().contains("REFUSED ["));
assert_eq!(f.latest().as_ref().and_then(primary_utility), Some(Ipv4Addr::new(10, 0, 0, 9)));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn the_verdict_names_primary_and_twin_on_their_utility_addresses() {
let r = fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, Some([10, 0, 0, 3]))]);
let v = verdict(&r).unwrap();
assert_eq!((v.primary.as_str(), v.primary_utility, v.twin.as_str(), v.twin_utility), ("vm2", Ipv4Addr::new(10, 0, 0, 2), "vm3", Ipv4Addr::new(10, 0, 0, 3)));
assert_eq!(primary_utility(&r), Some(Ipv4Addr::new(10, 0, 0, 2)));
let no_twin_address = fed(Some(Role::Front), vec![peer("vm2", Role::Primary, Some([10, 0, 0, 2])), peer("vm3", Role::Twin, None)]);
assert!(verdict(&no_twin_address).is_none());
}
#[test]
fn the_relay_list_is_every_fed_box_with_a_key_never_the_front() {
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
let reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm1", Role::Front, None) }, Peer { tunnel_pubkey: format!(" {k} "), ..peer("vm2", Role::Primary, None) }, peer("vm3", Role::Twin, None)]);
assert_eq!(relay_list(&reply.peers), vec![("vm2".to_string(), k.to_string(), crate::OVERLAY_BOX_A)]);
}
#[test]
fn the_front_keeps_its_feed_sealed_at_rest() {
let d = dir("sealed");
let f = Front::open(&d, seed(), P).unwrap();
let mut r = fed(Some(Role::Front), vec![]);
r.feed.insert("secret.shop_client_key".into(), "shop-key-NEVER-in-the-clear".into());
f.fed(&r).unwrap();
let raw = std::fs::read(d.join(JOINED_REL)).unwrap();
assert!(crate::fed_at_rest::is_sealed(&raw) && !String::from_utf8_lossy(&raw).contains("NEVER-in-the-clear"));
let again = Front::open(&d, seed(), P).unwrap();
assert_eq!(again.latest().and_then(|r| r.feed.get("secret.shop_client_key").cloned()).as_deref(), Some("shop-key-NEVER-in-the-clear"));
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn the_relay_lists_each_box_with_the_psk_the_front_was_fed_for_it() {
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
let psk = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
let mut reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm2", Role::Primary, None) }, Peer { tunnel_pubkey: k.into(), overlay: crate::OVERLAY_BOX_B, ..peer("vm3", Role::Twin, None) }]);
reply.feed.insert(crate::installer::feed_wg_psk_for("vm2"), format!(" {psk} "));
let list = relay_list_with_psk(&reply);
assert_eq!(list[0], ("vm2".to_string(), k.to_string(), crate::OVERLAY_BOX_A, Some(psk.to_string())));
assert_eq!(list[1].3, None, "no PSK fed for vm3");
}
#[test]
fn a_blank_fed_psk_reaches_the_relay_as_blank_never_as_none() {
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
let mut reply = fed(Some(Role::Front), vec![Peer { tunnel_pubkey: k.into(), ..peer("vm2", Role::Primary, None) }]);
reply.feed.insert(crate::installer::feed_wg_psk_for("vm2"), " ".into());
assert_eq!(relay_list_with_psk(&reply)[0].3.as_deref(), Some(""));
}
#[test]
fn a_box_is_busy_while_an_update_holds_it_and_only_then() {
assert!(!busy::busy(Some("g5-main")));
let one = busy::on("g5-main");
let two = busy::on("g5-main");
assert!(busy::busy(Some("g5-main")) && busy::busy(None));
assert!(!busy::busy(Some("g5-twin")), "per role");
drop(one);
assert!(busy::busy(Some("g5-main")), "counted: the second holder still works on it");
drop(two);
assert!(!busy::busy(Some("g5-main")));
}
}