use crate::elect::{weakest, Verdict};
use crate::join::{Admission, Assigned, JoinError, JoinReply, JoinRequest, Ledger, Peer, Presented};
use crate::mtls::{self, InstallerCa, Issued};
use crate::seed::Kind;
use crate::Role;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
pub const JOIN_PATH: &str = "/pair/join";
pub const SIGNAL_PATH: &str = "/pair/signal";
pub const MAX_REQUEST_BYTES: usize = 64 * 1024;
pub const BOX_CERT_VALID: Duration = Duration::from_secs(24 * 3600);
pub const RECHARGE_FROM_FEED: &str = "recharge_from";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Readmitted {
pub name: String,
pub was: (Ipv4Addr, Ipv4Addr),
pub now: (Ipv4Addr, Ipv4Addr),
pub forgotten: bool,
pub recharge_from: Option<String>,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Door {
pub ledger: Ledger,
#[serde(default)]
pub verdict: Option<Verdict>,
#[serde(default)]
pub feed: BTreeMap<String, String>,
#[serde(skip)]
pub own_feed: BTreeMap<String, BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub front: Option<crate::join::FrontTunnel>,
#[serde(skip)]
ca: Option<InstallerCa>,
#[serde(skip)]
box_cert_valid: Option<Duration>,
#[serde(skip)]
pub in_place: std::collections::BTreeSet<String>,
}
impl std::fmt::Debug for Door {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let own: BTreeMap<&String, Vec<&String>> = self.own_feed.iter().map(|(n, m)| (n, m.keys().collect())).collect();
f.debug_struct("Door").field("ledger", &self.ledger).field("verdict", &self.verdict).field("feed", &self.feed).field("front", &self.front).field("own_feed (keys)", &own).finish_non_exhaustive()
}
}
impl PartialEq for Door {
fn eq(&self, o: &Door) -> bool {
self.ledger == o.ledger && self.verdict == o.verdict && self.feed == o.feed && self.front == o.front
}
}
impl Door {
pub fn new(ledger: Ledger) -> Door {
Door { ledger, ..Default::default() }
}
pub fn with_ca(mut self, ca: InstallerCa) -> Door {
self.ca = Some(ca);
self
}
pub fn with_box_cert_valid(mut self, valid_for: Duration) -> Door {
self.box_cert_valid = Some(valid_for);
self
}
pub fn box_cert_valid(&self) -> Duration {
self.box_cert_valid.unwrap_or(BOX_CERT_VALID)
}
pub fn forget(&mut self, name: &str) -> Result<Option<crate::join::Admitted>, JoinError> {
if self.ledger.slot_of(name).is_none() {
return Err(JoinError::NotLaid(name.to_string()));
}
Ok(self.ledger.admitted.remove(name))
}
pub fn readmit(&mut self, name: &str, public: Ipv4Addr, utility: Ipv4Addr) -> Result<Readmitted, String> {
let bad = |why: String| Err(format!("REFUSED [door-readmit] {why}"));
let Some((&old_public, slot)) = self.ledger.slot_of(name) else {
return bad(format!("`{name}` is not a box this estate laid"));
};
let mut slot = slot.clone();
let old_utility = slot.utility_or(old_public);
if slot.kind.is_product() && self.verdict.is_none() {
return bad(format!("the estate has no verdict yet: `{name}` is re-admitted into a CLOSED estate, and this one never elected (run the install)"));
}
if public != old_public {
if let Some(other) = self.ledger.slots.get(&public) {
return bad(format!("{public} is where `{}` is laid; two boxes are never laid at one address", other.name));
}
}
self.ledger.slots.remove(&old_public);
slot.utility = Some(utility);
self.ledger.slots.insert(public, slot);
let forgotten = self.ledger.admitted.remove(name).is_some();
let prefix = crate::bootstrap::fed_offer_key(name, "");
self.feed.retain(|k, _| !k.starts_with(&prefix));
let primary = self.verdict.as_ref().is_some_and(|v| v.primary == name);
let recharge_from = if primary {
self.verdict.as_ref().and_then(|v| self.ledger.slot_of(&v.twin)).map(|(k, s)| format!("{}:{}", s.utility_or(*k), crate::CONTROL_PORT))
} else {
None
};
if let Some(at) = &recharge_from {
self.own_feed.entry(name.to_string()).or_default().insert(RECHARGE_FROM_FEED.to_string(), at.clone());
}
Ok(Readmitted { name: name.to_string(), was: (old_public, old_utility), now: (public, utility), forgotten, recharge_from })
}
pub fn readmit_kept(&mut self, name: &str) -> Result<Readmitted, String> {
let Some((&public, slot)) = self.ledger.slot_of(name) else {
return Err(format!("REFUSED [door-readmit] `{name}` is not a box this estate laid"));
};
let utility = slot.utility_or(public);
let mut r = self.readmit(name, public, utility)?;
if let Some(own) = self.own_feed.get_mut(name) {
own.remove(RECHARGE_FROM_FEED);
}
r.recharge_from = None;
self.in_place.insert(name.to_string());
Ok(r)
}
pub fn rejoin_in_place(&mut self, req: &JoinRequest, cn: &str, from: Ipv4Addr) -> Result<JoinReply, JoinError> {
if req.estate != self.ledger.estate {
return Err(JoinError::WrongEstate { want: self.ledger.estate.clone(), got: req.estate.clone() });
}
let slot = self.ledger.slots.get(&from).ok_or(JoinError::Unknown(from))?.clone();
if slot.name != cn || !self.in_place.contains(cn) || self.ledger.admitted.contains_key(cn) {
return Err(JoinError::Forged(format!("{from} is `{}`'s address, and it presents the certificate of `{cn}`", slot.name)));
}
if req.kind != slot.kind && slot.kind != Kind::Front {
return Err(JoinError::Forged(format!("{from} presents kind {:?}; the slot `{}` laid there is {:?}", req.kind, slot.name, slot.kind)));
}
self.ledger.admitted.insert(slot.name.clone(), crate::join::Admitted { from, tunnel_pubkey: req.tunnel_pubkey.clone(), capacity: req.capacity });
self.elect_if_ready();
Ok(self.reply_for(&slot.name))
}
pub fn load(path: &Path) -> Result<Door, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
toml::from_str(&text).map_err(|e| format!("{}: not a door ledger: {e}", path.display()))
}
pub fn save(&self, path: &Path) -> Result<(), String> {
let text = toml::to_string(self).map_err(|e| e.to_string())?;
appliance_pid1::durable::write_atomic(path, text.as_bytes(), None).map_err(|e| format!("{}: {e}", path.display()))
}
pub fn join(&mut self, req: &JoinRequest, who: &Presented, from: Ipv4Addr) -> Result<JoinReply, JoinError> {
let signed = if *who == Presented::Bootstrap && !self.ledger.admitted.values().any(|a| a.from == from) {
let name = self.ledger.slots.get(&from).map(|s| s.name.clone()).ok_or(JoinError::Unknown(from))?;
let ca = self.ca.as_ref().ok_or_else(|| JoinError::Sign("the door holds no installer CA".into()))?;
Some(ca.sign_box(&req.csr_pem, &name, self.box_cert_valid()).map_err(|e| JoinError::Sign(e.to_string()))?)
} else {
None
};
let admission = self.ledger.admit(req, who, from)?;
self.elect_if_ready();
let mut reply = self.reply_for(admission.name());
if let Admission::First(_) = admission {
reply.cert_pem = signed;
}
Ok(reply)
}
fn elect_if_ready(&mut self) {
if self.verdict.is_some() {
return;
}
let products: Vec<(String, Ipv4Addr, crate::elect::Capacity, Kind)> = self
.ledger
.admitted
.iter()
.filter_map(|(name, a)| {
let (_, slot) = self.ledger.slot_of(name)?;
slot.kind.is_product().then_some((name.clone(), slot.overlay, a.capacity?, slot.kind))
})
.collect();
if let [a, b] = &products[..] {
if a.3 != b.3 {
return;
}
self.verdict = Some(weakest((&a.0, a.1, &a.2), (&b.0, b.1, &b.2)));
}
}
pub fn role_of(&self, name: &str) -> Option<Role> {
let (_, slot) = self.ledger.slot_of(name)?;
if !self.ledger.admitted.contains_key(name) {
return None;
}
match (slot.kind, &self.verdict) {
(Kind::Front, _) => Some(Role::Front),
(Kind::Box(_), _) => Some(Role::Primary),
(k, Some(v)) if k.is_product() && v.twin == name => Some(Role::Twin),
(k, Some(v)) if k.is_product() && v.primary == name => Some(Role::Primary),
_ => None,
}
}
pub fn front_tunnel(&self) -> Option<crate::join::FrontTunnel> {
if let Some(f) = &self.front {
return Some(f.clone());
}
self.ledger.slots.iter().filter(|(_, s)| s.kind == Kind::Front).find_map(|(addr, s)| {
let a = self.ledger.admitted.get(&s.name)?;
if a.tunnel_pubkey.trim().is_empty() {
return None;
}
let endpoint = s.endpoint.clone().unwrap_or_else(|| format!("{}:{}", s.utility_or(*addr), crate::RELAY_PORT));
Some(crate::join::FrontTunnel { tunnel_pubkey: a.tunnel_pubkey.clone(), endpoint })
})
}
pub fn peers(&self) -> Vec<Peer> {
let front = self.front_tunnel().map(|f| f.as_peer());
front
.into_iter()
.chain(self.ledger.admitted.iter().filter_map(|(name, a)| {
let (key, slot) = self.ledger.slot_of(name)?;
let role = self.role_of(name)?;
(role != Role::Front).then(|| Peer { name: name.clone(), role, tunnel_pubkey: a.tunnel_pubkey.clone(), overlay: slot.overlay, endpoint: slot.endpoint.clone(), utility: Some(slot.utility_or(*key)) })
}))
.collect()
}
pub fn reply_for(&self, name: &str) -> JoinReply {
let (_, slot) = self.ledger.slot_of(name).expect("reply_for is called for a laid name");
let primary_slot = self.verdict.as_ref().and_then(|v| self.ledger.slot_of(&v.primary));
let primary_overlay = primary_slot.map(|(_, s)| s.overlay);
let primary_utility = primary_slot.map(|(u, s)| s.utility_or(*u));
JoinReply {
estate: self.ledger.estate.clone(),
me: Assigned { name: name.to_string(), overlay: slot.overlay },
cert_pem: None,
role: self.role_of(name),
primary_overlay,
primary_utility,
peers: self.peers(),
feed: self.feed_for(name),
}
}
pub fn feed_for(&self, name: &str) -> BTreeMap<String, String> {
let mut f = self.feed.clone();
if let Some(own) = self.own_feed.get(name) {
f.extend(own.iter().map(|(k, v)| (k.clone(), v.clone())));
}
f
}
pub fn signal(&self) -> String {
let q = |it: Vec<&String>| it.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ");
let mut out = format!(
"estate = \"{}\"\njoined = [{}]\nexpected = [{}]\n",
self.ledger.estate,
q(self.ledger.admitted.keys().collect()),
q(self.ledger.slots.values().map(|s| &s.name).collect())
);
match &self.verdict {
Some(v) => out.push_str(&format!("twin = \"{}\"\nprimary = \"{}\"\nwhy = \"{}\"\n", v.twin, v.primary, v.why.replace('"', "'"))),
None => out.push_str("verdict = \"pending\"\n"),
}
out.push_str(&format!("complete = {}\nbootstrap = \"{}\"\n", self.ledger.complete(), if self.ledger.complete() { "withdrawn" } else { "open" }));
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub method: String,
pub path: String,
pub body: Vec<u8>,
}
pub fn parse_request(bytes: &[u8]) -> Option<Result<Request, String>> {
let head_end = bytes.windows(4).position(|w| w == b"\r\n\r\n")?;
let head = String::from_utf8_lossy(&bytes[..head_end]).to_string();
let mut lines = head.split("\r\n");
let start = lines.next().unwrap_or_default();
let mut parts = start.split_whitespace();
let (Some(method), Some(path)) = (parts.next(), parts.next()) else {
return Some(Err(format!("not an HTTP request line: `{start}`")));
};
let len: usize = lines
.filter_map(|l| l.split_once(':'))
.find(|(k, _)| k.trim().eq_ignore_ascii_case("content-length"))
.and_then(|(_, v)| v.trim().parse().ok())
.unwrap_or(0);
let body_start = head_end + 4;
if bytes.len() < body_start + len {
return None;
}
Some(Ok(Request { method: method.to_string(), path: path.to_string(), body: bytes[body_start..body_start + len].to_vec() }))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub status: u16,
pub body: String,
}
impl Response {
pub fn bytes(&self) -> Vec<u8> {
let reason = match self.status {
200 => "OK",
400 => "Bad Request",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
_ => "Error",
};
format!(
"HTTP/1.1 {} {reason}\r\ncontent-type: application/toml\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
self.status,
self.body.len(),
self.body
)
.into_bytes()
}
pub fn parse(raw: &[u8]) -> Result<Response, String> {
let text = String::from_utf8_lossy(raw);
let (head, body) = text.split_once("\r\n\r\n").ok_or("no end of headers in the reply")?;
let status = head.split_whitespace().nth(1).and_then(|s| s.parse().ok()).ok_or("no status in the reply")?;
Ok(Response { status, body: body.to_string() })
}
}
pub fn respond(door: &mut Door, req: &Request, who: &Presented, from: Ipv4Addr) -> Response {
match (req.method.as_str(), req.path.as_str()) {
("POST", JOIN_PATH) => {
let jr = match JoinRequest::parse(&String::from_utf8_lossy(&req.body)) {
Ok(j) => j,
Err(e) => return Response { status: 400, body: format!("{e}\n") },
};
match door.join(&jr, who, from).and_then(|r| r.render()) {
Ok(text) => Response { status: 200, body: text },
Err(e) => Response { status: 403, body: format!("{e}\n") },
}
}
("GET", SIGNAL_PATH) => Response { status: 200, body: door.signal() },
(_, JOIN_PATH) | (_, SIGNAL_PATH) => Response { status: 405, body: "REFUSED [door-method] POST /pair/join or GET /pair/signal\n".into() },
_ => Response { status: 404, body: "REFUSED [door-path] this door has /pair/join and /pair/signal\n".into() },
}
}
pub fn serve_one(door: &mut Door, cfg: Arc<rustls::ServerConfig>, stream: TcpStream) -> Result<bool, String> {
let from = match stream.peer_addr() {
Ok(SocketAddr::V4(a)) => *a.ip(),
other => return Err(format!("not an IPv4 peer: {other:?}")),
};
let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
let _ = stream.set_write_timeout(Some(Duration::from_secs(10)));
let conn = rustls::ServerConnection::new(cfg).map_err(|e| e.to_string())?;
let mut tls = rustls::StreamOwned::new(conn, stream);
let mut bytes = Vec::new();
let mut chunk = [0u8; 4096];
let req = loop {
let n = tls.read(&mut chunk).map_err(|e| format!("{from}: {e}"))?;
if n == 0 {
return Err(format!("{from}: closed before a request"));
}
bytes.extend_from_slice(&chunk[..n]);
if bytes.len() > MAX_REQUEST_BYTES {
return Err(format!("{from}: request too large"));
}
if let Some(r) = parse_request(&bytes) {
break r?;
}
};
let who = mtls::peer_cn(&tls.conn).map(|cn| Presented::from_cn(&cn)).ok_or_else(|| format!("{from}: no client certificate"))?;
let before = door.clone();
let r = respond(door, &req, &who, from);
tls.write_all(&r.bytes()).map_err(|e| e.to_string())?;
tls.conn.send_close_notify();
let _ = tls.flush();
Ok(*door != before)
}
pub fn post_join(door: SocketAddrV4, ca_pem: &str, identity: &Issued, req: &JoinRequest, timeout: Duration) -> Result<JoinReply, String> {
let cfg = mtls::client_config(ca_pem, identity).map_err(|e| e.to_string())?;
let body = req.render().map_err(|e| e.to_string())?;
let s = TcpStream::connect_timeout(&SocketAddr::V4(door), timeout).map_err(|e| format!("could not be reached: {door}: {e}"))?;
let _ = s.set_read_timeout(Some(timeout));
let _ = s.set_write_timeout(Some(timeout));
let conn = rustls::ClientConnection::new(cfg, mtls::door_name(*door.ip())).map_err(|e| e.to_string())?;
let mut tls = rustls::StreamOwned::new(conn, s);
let raw = format!("POST {JOIN_PATH} HTTP/1.1\r\nhost: {door}\r\ncontent-type: application/toml\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", body.len());
tls.write_all(raw.as_bytes()).map_err(|e| format!("could not be reached: TLS to {door}: {e}"))?;
let mut buf = Vec::new();
match tls.read_to_end(&mut buf) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof && !buf.is_empty() => {}
Err(e) => return Err(format!("could not be reached: reading from {door}: {e}")),
}
let r = Response::parse(&buf)?;
if r.status != 200 {
return Err(r.body.trim().to_string());
}
JoinReply::parse(&r.body).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elect::Capacity;
use crate::join::Slot;
const H: Duration = Duration::from_secs(3600);
const FRONT: Ipv4Addr = Ipv4Addr::new(10, 13, 8, 179);
const MAIN: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 90);
const TWIN: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 87);
fn door(ca: &InstallerCa) -> Door {
let mut l = Ledger::new("gunnar.rs");
l.lay(FRONT, Slot { name: "front".into(), kind: Kind::Front, overlay: crate::OVERLAY_FRONT, endpoint: Some("95.111.211.189:51820".into()), utility: None });
l.lay(MAIN, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None });
l.lay(TWIN, Slot { name: "twin".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_B, endpoint: None, utility: None });
let mut d = Door::new(l).with_ca(ca.clone());
d.feed.insert("monetize_pubkey".into(), "eb16f371b856".into());
d.front = Some(crate::join::FrontTunnel { tunnel_pubkey: "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".into(), endpoint: "95.111.211.189:51820".into() });
d
}
fn req(kind: Kind, key: &str, cores: u32) -> JoinRequest {
let (csr, _) = mtls::box_request().unwrap();
JoinRequest { estate: "gunnar.rs".into(), kind, tunnel_pubkey: key.into(), capacity: Some(Capacity { cores, ram_bytes: 4 << 30, data_bytes: 10 << 30, disk_mib_s: 500 }), csr_pem: csr }
}
fn post(d: &mut Door, r: &JoinRequest, who: &Presented, from: Ipv4Addr) -> Response {
let body = r.render().unwrap();
let raw = format!("POST {JOIN_PATH} HTTP/1.1\r\ncontent-length: {}\r\n\r\n{body}", body.len());
respond(d, &parse_request(raw.as_bytes()).unwrap().unwrap(), who, from)
}
#[test]
fn two_holger_boxes_are_elected_and_a_gunnar_never_pairs_with_a_holger() {
let ca = InstallerCa::mint("holger.rs", H).unwrap();
let mut d = door(&ca);
for s in [MAIN, TWIN] {
d.ledger.slots.get_mut(&s).unwrap().kind = Kind::Holger;
}
d.join(&req(Kind::Gunnar, "KF", 2), &Presented::Bootstrap, FRONT).unwrap();
d.join(&req(Kind::Holger, "KM", 4), &Presented::Bootstrap, MAIN).unwrap();
let twin = d.join(&req(Kind::Holger, "KT", 1), &Presented::Bootstrap, TWIN).unwrap();
assert_eq!(twin.role, Some(Role::Twin), "the weakest holger box is the twin");
assert_eq!(d.role_of("main"), Some(Role::Primary));
let mut fresh = door(&ca);
fresh.ledger.slots.get_mut(&MAIN).unwrap().kind = Kind::Holger;
assert!(matches!(fresh.join(&req(Kind::Gunnar, "X", 4), &Presented::Bootstrap, MAIN), Err(JoinError::Forged(_))), "a holger slot demands a holger box");
let mut mixed = door(&ca);
mixed.ledger.slots.get_mut(&TWIN).unwrap().kind = Kind::Holger;
mixed.join(&req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN).unwrap();
mixed.join(&req(Kind::Holger, "KT", 1), &Presented::Bootstrap, TWIN).unwrap();
assert_eq!((mixed.role_of("main"), mixed.role_of("twin")), (None, None), "no election across products");
}
#[test]
fn a_box_kind_stands_alone_as_its_own_primary_and_never_joins_an_election() {
let ca = InstallerCa::mint("lake.rs", H).unwrap();
let be = Kind::named("be").unwrap();
let mut d = door(&ca);
for s in [MAIN, TWIN] {
d.ledger.slots.get_mut(&s).unwrap().kind = be;
}
d.join(&req(Kind::Gunnar, "KF", 2), &Presented::Bootstrap, FRONT).unwrap();
let main = d.join(&req(be, "KM", 4), &Presented::Bootstrap, MAIN).unwrap();
assert_eq!(main.role, Some(Role::Primary), "a box kind is fed its role at once: it waits for no twin");
let twin = d.join(&req(be, "KT", 1), &Presented::Bootstrap, TWIN).unwrap();
assert_eq!(twin.role, Some(Role::Primary), "two boxes of one box kind are two primaries, not an election");
assert_eq!(d.verdict, None, "no election among box kinds");
let mut fresh = door(&ca);
fresh.ledger.slots.get_mut(&MAIN).unwrap().kind = be;
assert!(matches!(fresh.join(&req(Kind::Gunnar, "X", 4), &Presented::Bootstrap, MAIN), Err(JoinError::Forged(_))), "a box slot demands its own kind");
let mut other = door(&ca);
other.ledger.slots.get_mut(&MAIN).unwrap().kind = be;
assert!(matches!(other.join(&req(Kind::named("fe").unwrap(), "X", 4), &Presented::Bootstrap, MAIN), Err(JoinError::Forged(_))), "another box kind is forged too");
assert!(d.readmit("main", Ipv4Addr::new(10, 13, 8, 99), Ipv4Addr::new(10, 13, 9, 99)).is_ok(), "a box kind is re-admitted with no verdict");
}
#[test]
fn vm1_on_the_gunnar_medium_becomes_the_front_and_its_key_is_the_relay_fed_to_the_boxes() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
d.front = None;
d.ledger.slots.get_mut(&FRONT).unwrap().endpoint = None;
assert_eq!(d.front_tunnel(), None, "no front key before vm1 presented one");
let r = d.join(&req(Kind::Gunnar, "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=", 2), &Presented::Bootstrap, FRONT).unwrap();
assert_eq!(r.role, Some(Role::Front), "the slot, not the medium, makes the front");
d.join(&req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN).unwrap();
let twin = d.join(&req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN).unwrap();
let front = twin.front().expect("the front is fed");
assert_eq!(front.tunnel_pubkey, "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=");
assert_eq!(front.endpoint.as_deref(), Some("10.13.8.179:51820"));
assert_eq!(twin.peers.iter().filter(|p| p.role == Role::Front).count(), 1, "vm1 listed once, as the front: {:?}", twin.peers);
assert_eq!(twin.peers.len(), 3);
let fr = d.reply_for("front");
let at = |role: Role| fr.peers.iter().find(|p| p.role == role).and_then(|p| p.utility);
assert_eq!((at(Role::Twin), at(Role::Primary), at(Role::Front)), (Some(TWIN), Some(MAIN), None), "{:?}", fr.peers);
let mut e = door(&ca);
assert!(matches!(e.join(&req(Kind::Front, "K", 1), &Presented::Bootstrap, MAIN), Err(JoinError::Forged(_))));
}
#[test]
fn joins_by_address_elect_the_weakest_sign_each_box_feed_the_estate_and_withdraw_bootstrap_at_completion() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
let r = post(&mut d, &req(Kind::Front, "KF", 2), &Presented::Bootstrap, FRONT);
assert_eq!(JoinReply::parse(&r.body).unwrap().role, Some(Role::Front), "{}", r.body);
let r = post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN);
let reply = JoinReply::parse(&r.body).unwrap();
assert_eq!((reply.me.name.as_str(), reply.role), ("main", None), "admitted, election pending");
assert!(reply.cert_pem.as_deref().unwrap_or("").contains("BEGIN CERTIFICATE"), "the box's own certificate on its first join");
assert_eq!(reply.feed.get("monetize_pubkey").map(String::as_str), Some("eb16f371b856"), "the estate value is fed, not baked");
let front = reply.front().expect("the front's tunnel is fed on every reply, before any election");
assert_eq!((front.tunnel_pubkey.as_str(), front.endpoint.as_deref()), ("xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=", Some("95.111.211.189:51820")), "the lay's relay, fed — not on the medium");
let again = JoinReply::parse(&post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Box("main".into()), MAIN).body).unwrap();
assert!(again.cert_pem.is_none());
let r = post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN);
let reply = JoinReply::parse(&r.body).unwrap();
assert_eq!((reply.me.name.as_str(), reply.role, reply.primary_overlay), ("twin", Some(Role::Twin), Some(crate::OVERLAY_BOX_A)));
assert!(d.ledger.complete());
assert!(d.signal().contains("bootstrap = \"withdrawn\""));
let r = post(&mut d, &req(Kind::Gunnar, "X", 4), &Presented::Bootstrap, MAIN);
assert!(r.status == 403 && r.body.contains("REFUSED [join-withdrawn]"), "{}", r.body);
let p = std::env::temp_dir().join(format!("door-{}.toml", std::process::id()));
d.save(&p).unwrap();
let back = Door::load(&p).unwrap();
assert_eq!(back, d, "the ledger round-trips; the CA key is not in it");
assert!(!std::fs::read_to_string(&p).unwrap().contains("PRIVATE KEY"), "the CA key never lands in the ledger file");
let _ = std::fs::remove_file(&p);
}
#[test]
fn a_box_joins_over_real_mutual_tls_then_asks_again_as_itself() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let door_cert = ca.issue_door(Ipv4Addr::LOCALHOST, H).unwrap();
let boot = ca.issue_bootstrap(H).unwrap();
let mut l = Ledger::new("gunnar.rs");
l.lay(Ipv4Addr::LOCALHOST, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None });
let d = std::sync::Arc::new(std::sync::Mutex::new(Door::new(l).with_ca(ca.clone())));
let scfg = mtls::server_config(&ca.cert_pem, &door_cert).unwrap();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let SocketAddr::V4(addr) = listener.local_addr().unwrap() else { panic!() };
let dd = d.clone();
let server = std::thread::spawn(move || {
for _ in 0..3 {
let (s, _) = listener.accept().unwrap();
let _ = serve_one(&mut dd.lock().unwrap(), scfg.clone(), s);
}
});
let (csr, box_key) = mtls::box_request().unwrap();
let r = JoinRequest { estate: "gunnar.rs".into(), kind: Kind::Gunnar, tunnel_pubkey: "KM".into(), capacity: None, csr_pem: csr };
let first = post_join(addr, &ca.cert_pem, &boot, &r, Duration::from_secs(5)).unwrap();
let mine = Issued { cert_pem: first.cert_pem.clone().expect("a certificate on the first join"), key_pem: box_key };
let second = post_join(addr, &ca.cert_pem, &mine, &JoinRequest { csr_pem: String::new(), ..r.clone() }, Duration::from_secs(5)).unwrap();
assert_eq!((second.me.name.as_str(), second.cert_pem.is_none()), ("main", true));
let stranger = InstallerCa::mint("stranger", H).unwrap().issue_bootstrap(H).unwrap();
assert!(post_join(addr, &ca.cert_pem, &stranger, &r, Duration::from_secs(5)).is_err(), "another CA's certificate never reaches the door's logic");
server.join().unwrap();
assert_eq!(d.lock().unwrap().ledger.admitted.len(), 1);
}
fn not_after(pem: &str) -> i64 {
let der = x509_parser::pem::parse_x509_pem(pem.as_bytes()).unwrap().1;
der.parse_x509().unwrap().validity().not_after.timestamp()
}
#[test]
fn the_box_certificate_lifetime_is_the_callers_with_a_24h_default() {
let ca = InstallerCa::mint("gunnar.rs", 48 * H).unwrap();
let now = time::OffsetDateTime::now_utc().unix_timestamp();
let mut d = door(&ca);
assert_eq!(d.box_cert_valid(), Duration::from_secs(24 * 3600));
let r = JoinReply::parse(&post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN).body).unwrap();
let left = not_after(&r.cert_pem.unwrap()) - now;
assert!((24 * 3600 - 60..=24 * 3600 + 60).contains(&left), "default 24 h, got {left} s");
let mut d = door(&ca).with_box_cert_valid(36 * H);
let r = JoinReply::parse(&post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN).body).unwrap();
let left = not_after(&r.cert_pem.unwrap()) - now;
assert!((36 * 3600 - 60..=36 * 3600 + 60).contains(&left), "the caller's 36 h, got {left} s");
let p = std::env::temp_dir().join(format!("door-valid-{}.toml", std::process::id()));
d.save(&p).unwrap();
assert!(!std::fs::read_to_string(&p).unwrap().contains("box_cert_valid"), "a process parameter, not ledger state");
let _ = std::fs::remove_file(&p);
}
#[test]
fn a_dead_main_is_readmitted_at_its_new_address_as_the_primary_and_told_where_its_twin_is() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
post(&mut d, &req(Kind::Front, "KF", 2), &Presented::Bootstrap, FRONT);
post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN);
post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN);
d.feed.insert("offer.main.ticket_pubkey".into(), "dead-mains-key".into());
d.feed.insert("offer.front.plugin_door".into(), "10.13.8.179:8475".into());
let verdict = d.verdict.clone().expect("elected");
assert!(d.readmit("stranger", MAIN, MAIN).unwrap_err().contains("[door-readmit]"));
assert!(d.readmit("main", TWIN, TWIN).unwrap_err().contains("where `twin` is laid"), "two boxes at one address");
let mut unelected = door(&ca);
assert!(unelected.readmit("main", MAIN, MAIN).unwrap_err().contains("never elected"));
let (new_public, new_util) = (Ipv4Addr::new(127, 80, 99, 40), Ipv4Addr::new(10, 13, 9, 140));
let r = d.readmit("main", new_public, new_util).unwrap();
assert_eq!(r, Readmitted { name: "main".into(), was: (MAIN, MAIN), now: (new_public, new_util), forgotten: true, recharge_from: Some(format!("{TWIN}:{}", crate::CONTROL_PORT)) });
assert!(!d.ledger.slots.contains_key(&MAIN) && d.ledger.slots[&new_public].name == "main", "the slot moved");
assert_eq!(d.verdict, Some(verdict.clone()), "the verdict is kept");
assert!(!d.feed.contains_key("offer.main.ticket_pubkey"), "the dead box's offers are gone");
assert!(d.feed.contains_key("offer.front.plugin_door"), "the others' are kept");
assert!(!d.ledger.complete(), "the bootstrap certificate is open for that slot");
assert!(post(&mut d, &req(Kind::Gunnar, "X", 1), &Presented::Bootstrap, TWIN).body.contains("join-spent"), "…and only that slot");
let reply = d.join(&req(Kind::Gunnar, "KM2", 2), &Presented::Bootstrap, new_public).unwrap();
assert!(reply.cert_pem.as_deref().unwrap_or("").contains("BEGIN CERTIFICATE"), "a certificate of its own");
assert_eq!(reply.role, Some(Role::Primary), "the primary it was, even weaker than before");
assert_eq!(reply.feed.get(RECHARGE_FROM_FEED).map(String::as_str), Some(format!("{TWIN}:{}", crate::CONTROL_PORT).as_str()));
assert_eq!(reply.primary_utility, Some(new_util));
assert!(d.ledger.complete());
let twin = d.reply_for("twin");
assert_eq!(twin.primary_utility, Some(new_util), "the twin's next answer names the new main");
assert!(!twin.feed.contains_key(RECHARGE_FROM_FEED), "only the re-admitted primary is told");
let p = twin.peers.iter().find(|p| p.name == "main").unwrap();
assert_eq!((p.utility, p.tunnel_pubkey.as_str()), (Some(new_util), "KM2"), "the relay learns the new key");
let t = d.readmit("twin", TWIN, TWIN).unwrap();
assert_eq!(t.recharge_from, None, "a twin is not filled from anybody");
assert!(!d.feed_for("twin").contains_key(RECHARGE_FROM_FEED));
}
#[test]
fn an_upgraded_box_is_readmitted_in_place_and_a_primary_is_never_told_to_recharge() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
post(&mut d, &req(Kind::Front, "KF", 2), &Presented::Bootstrap, FRONT);
post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN);
post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN);
d.feed.insert("offer.main.ticket_pubkey".into(), "the-old-build".into());
d.own_feed.entry("main".into()).or_default().insert(RECHARGE_FROM_FEED.into(), "stale".into());
let r = d.readmit_kept("main").unwrap();
assert_eq!(r, Readmitted { name: "main".into(), was: (MAIN, MAIN), now: (MAIN, MAIN), forgotten: true, recharge_from: None });
assert!(!d.feed_for("main").contains_key(RECHARGE_FROM_FEED), "a kept primary is filled from nobody");
assert!(!d.feed.contains_key("offer.main.ticket_pubkey"), "the upgraded box offers anew");
assert!(!d.ledger.complete(), "the bootstrap certificate is open for that slot");
let reply = d.join(&req(Kind::Gunnar, "KM2", 4), &Presented::Bootstrap, MAIN).unwrap();
assert_eq!(reply.role, Some(Role::Primary));
assert!(!reply.feed.contains_key(RECHARGE_FROM_FEED));
assert!(d.readmit_kept("stranger").unwrap_err().contains("[door-readmit]"));
}
#[test]
fn a_box_of_an_in_place_readmission_that_shows_its_own_certificate_is_admitted_as_itself() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
post(&mut d, &req(Kind::Front, "KF", 2), &Presented::Bootstrap, FRONT);
post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN);
post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN);
let own = |key: &str| JoinRequest { csr_pem: String::new(), ..req(Kind::Gunnar, key, 1) };
d.forget("twin").unwrap();
assert!(matches!(d.rejoin_in_place(&own("KT2"), "twin", TWIN), Err(JoinError::Forged(_))), "no re-admission names the twin");
d.readmit_kept("twin").unwrap();
assert!(matches!(d.rejoin_in_place(&own("KT2"), "main", TWIN), Err(JoinError::Forged(_))), "the main's certificate at the twin's address");
assert!(matches!(d.join(&own("KT2"), &Presented::Box("twin".into()), TWIN), Err(JoinError::Forged(_))), "the door's own join still refuses it");
let reply = d.rejoin_in_place(&own("KT2"), "twin", TWIN).unwrap();
assert_eq!((reply.me.name.as_str(), reply.role), ("twin", Some(Role::Twin)), "its role back");
assert!(reply.cert_pem.is_none(), "it keeps the certificate it has");
assert_eq!(d.ledger.admitted["twin"].tunnel_pubkey, "KT2", "the tunnel key it shows now");
assert!(d.ledger.complete());
assert!(matches!(d.rejoin_in_place(&own("KT3"), "twin", TWIN), Err(JoinError::Forged(_))), "admitted already: nothing to re-key");
assert_eq!(d.ledger.admitted["twin"].tunnel_pubkey, "KT2");
}
#[test]
fn forget_clears_a_reinstalled_boxs_admission_keeps_the_verdict_and_lets_it_rejoin_once() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let mut d = door(&ca);
post(&mut d, &req(Kind::Front, "KF", 2), &Presented::Bootstrap, FRONT);
post(&mut d, &req(Kind::Gunnar, "KM", 4), &Presented::Bootstrap, MAIN);
post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Bootstrap, TWIN);
assert!(d.ledger.complete());
let verdict = d.verdict.clone().expect("elected");
let e = d.forget("stranger").unwrap_err();
assert_eq!(e, JoinError::NotLaid("stranger".into()));
assert!(e.to_string().starts_with("REFUSED [door-forget-unknown]"), "{e}");
let old = d.forget("twin").unwrap().expect("twin had joined");
assert_eq!(old.tunnel_pubkey, "KT");
assert_eq!(d.forget("twin"), Ok(None), "forgetting twice is nothing");
assert!(!d.ledger.complete() && d.signal().contains("bootstrap = \"open\""));
assert_eq!(d.role_of("twin"), None, "not admitted, no role");
let r = post(&mut d, &req(Kind::Gunnar, "KT", 1), &Presented::Box("twin".into()), TWIN);
assert!(r.status == 403 && r.body.contains("join-forged"), "{}", r.body);
let r = post(&mut d, &req(Kind::Gunnar, "X", 4), &Presented::Bootstrap, MAIN);
assert!(r.status == 403 && r.body.contains("join-spent"), "{}", r.body);
let r = post(&mut d, &req(Kind::Gunnar, "KT2", 8), &Presented::Bootstrap, TWIN);
let reply = JoinReply::parse(&r.body).unwrap();
assert!(reply.cert_pem.as_deref().unwrap_or("").contains("BEGIN CERTIFICATE"), "a new certificate for the new box");
assert_eq!(reply.role, Some(Role::Twin), "the verdict is kept even though the new box is stronger");
assert_eq!(d.verdict, Some(verdict));
assert_eq!(d.ledger.admitted["twin"].tunnel_pubkey, "KT2", "the new key is recorded");
assert!(d.ledger.complete() && d.signal().contains("bootstrap = \"withdrawn\""));
}
#[test]
fn two_writers_of_one_ledger_never_collide_and_the_ledger_stays_whole() {
let ca = InstallerCa::mint("gunnar.rs", H).unwrap();
let d = std::sync::Arc::new(door(&ca));
let dir = std::env::temp_dir().join(format!("door-durable-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("ledger.toml");
let failed: Vec<String> = std::thread::scope(|s| {
let hs: Vec<_> = (0..8).map(|_| s.spawn(|| (0..40).filter_map(|_| d.save(&path).err()).collect::<Vec<_>>())).collect();
hs.into_iter().flat_map(|h| h.join().unwrap()).collect()
});
assert!(failed.is_empty(), "{} saves failed, e.g. {:?}", failed.len(), failed.first());
assert!(Door::load(&path).is_ok(), "the ledger is whole");
let left: Vec<_> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name()).collect();
assert_eq!(left.len(), 1, "no staging file is left: {left:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}