crates/installer/nordisk-pairing/src/join.rs
//! **The one-time join, over the installer's mutual TLS.** A box presents
//! itself to the front's door ONCE, over the utility network, before any
//! tunnel exists: what kind of box it is, its tunnel public key, what it
//! measured about itself, and a signing request for a key it minted itself.
//! The TLS handshake has already proven WHO is speaking ([`Presented`]): the
//! bootstrap certificate the medium carries, or a box's own certificate from
//! an earlier join. The door knows the box by the utility address it came
//! from (the laptop laid one SLOT per address), admits it, signs its own
//! certificate, and answers with its name and overlay address, its role once
//! the election has spoken, the peers, and the estate values it needs.
//!
//! **The bootstrap certificate is valid during bootstrap only** (owner
//! 2026-09-24), and the ledger is where that is enforced:
//! * it may only JOIN a slot that has not joined — a second bootstrap join on
//! a taken slot is `spent` (a copied medium);
//! * a box that has joined speaks with its OWN certificate, whose name must
//! be its slot's — anything else is `forged`;
//! * when every slot has joined, the bootstrap certificate is `withdrawn`.
//!
//! The wire format is TOML, like the seed.
use crate::elect::Capacity;
use crate::seed::Kind;
use crate::Role;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::net::Ipv4Addr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinRequest {
pub estate: String,
pub kind: Kind,
/// The tunnel public key this box minted at first boot (empty with `tunnel = none`).
pub tunnel_pubkey: String,
/// What the box measured about itself — the election's input.
pub capacity: Option<Capacity>,
/// A signing request for the box's OWN key (PEM). Present on the first
/// join; a box already holding its certificate may send it empty.
#[serde(default)]
pub csr_pem: String,
}
/// What the door assigned this box: its name and its address in the tunnel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Assigned {
pub name: String,
pub overlay: Ipv4Addr,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peer {
pub name: String,
pub role: Role,
pub tunnel_pubkey: String,
pub overlay: Ipv4Addr,
pub endpoint: Option<String>,
/// **Where the peer's control plane is on the utility network** (0.4.1):
/// the front — vm1 on the same medium since 0.4.0 — certifies the twin
/// against the primary over the utility network, and its reply carried no
/// twin address. `None` for the front peer and for a 0.4.0 ledger.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utility: Option<Ipv4Addr>,
}
/// **The front's tunnel identity, as the door FEEDS it** (owner 2026-09-24
/// evening: the medium is constant). The relay key is minted with the front
/// and the endpoint is the front's public address, so both change with every
/// lay; a seed that carried them made every lay a new medium. The laptop
/// writes this into the door's ledger when it prepares the installer, and
/// every join reply lists it as the peer named [`FRONT_PEER`] with role
/// `front` ([`JoinReply::front`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontTunnel {
/// The relay's public key (WireGuard's base64).
pub tunnel_pubkey: String,
/// Where a box dials it: `host:port`.
pub endpoint: String,
}
/// The name the front goes by in a join reply's peer list.
pub const FRONT_PEER: &str = "front";
impl FrontTunnel {
/// The front as a peer: the one dialled endpoint, the overlay's `.1`.
pub fn as_peer(&self) -> Peer {
Peer { name: FRONT_PEER.to_string(), role: Role::Front, tunnel_pubkey: self.tunnel_pubkey.clone(), overlay: crate::OVERLAY_FRONT, endpoint: Some(self.endpoint.clone()), utility: None }
}
/// Refused by name when it cannot be dialled.
pub fn check(&self) -> Result<(), JoinError> {
if self.tunnel_pubkey.trim().is_empty() {
return Err(JoinError::Wire("the door's front tunnel key is empty".into()));
}
if self.endpoint.rsplit_once(':').and_then(|(_, p)| p.parse::<u16>().ok()).is_none() {
return Err(JoinError::Wire(format!("the door's front endpoint `{}` is not host:port", self.endpoint)));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinReply {
pub estate: String,
/// This box, as the door knows it.
pub me: Assigned,
/// The box's own certificate (PEM), signed from its request at the first
/// admission. From now on the box speaks with it; the bootstrap
/// certificate is refused for its slot.
#[serde(default)]
pub cert_pem: Option<String>,
/// What this box IS, decided once. `None` = admitted, and the election has
/// not spoken yet because the other gunnar box has not joined: ask again,
/// with the box's own certificate.
pub role: Option<Role>,
/// Where the primary is inside the tunnel — the address a twin pairs against.
pub primary_overlay: Option<Ipv4Addr>,
/// Where the primary's CONTROL PLANE listens: its utility address. gunnar
/// binds :50051 on the private (utility) interface only, so a twin that
/// becomes the twin of the primary's OVERLAY address reaches nothing there
/// (MEASURED 2026-09-24, read in the audit). Replication still prefers the
/// tunnel through its own host list; the control plane is always utility.
#[serde(default)]
pub primary_utility: Option<Ipv4Addr>,
pub peers: Vec<Peer>,
/// **The estate values the installer feeds** — the monetize verifying key,
/// the clock key, the admin keys, … — so the medium carries none of them.
#[serde(default)]
pub feed: BTreeMap<String, String>,
}
impl JoinRequest {
pub fn render(&self) -> Result<String, JoinError> {
toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
}
pub fn parse(text: &str) -> Result<JoinRequest, JoinError> {
toml::from_str(text).map_err(|e| JoinError::Wire(format!("the body is not a join request: {e}")))
}
}
impl JoinReply {
pub fn parse(text: &str) -> Result<JoinReply, JoinError> {
toml::from_str(text).map_err(|e| JoinError::Wire(format!("the join reply is not a join reply: {e}")))
}
/// **The front, as the door fed it** — the peer with role `front`: its
/// tunnel key and the endpoint to dial. `None` from a door that was given
/// no front tunnel (a `tunnel = none` estate, or a ledger laid without it).
pub fn front(&self) -> Option<&Peer> {
self.peers.iter().find(|p| p.role == Role::Front)
}
pub fn render(&self) -> Result<String, JoinError> {
toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
}
}
/// **Who is speaking**, as the TLS handshake proved it: the bootstrap
/// certificate the medium carries, or a box's own certificate (its CN).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Presented {
Bootstrap,
Box(String),
}
impl Presented {
/// From the certificate's CN (`crate::mtls::peer_cn`).
pub fn from_cn(cn: &str) -> Presented {
if cn == crate::mtls::BOOTSTRAP_CN {
Presented::Bootstrap
} else {
Presented::Box(cn.to_string())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JoinError {
Wire(String),
/// The address is not a slot this estate laid.
Unknown(Ipv4Addr),
/// The slot has joined, and the bootstrap certificate came again — a copied medium.
Spent { name: String, from: Ipv4Addr },
/// The certificate does not belong to this slot, or the box is not the kind its slot is.
Forged(String),
/// Every slot has joined: the bootstrap certificate is no longer accepted anywhere.
Withdrawn,
WrongEstate { want: String, got: String },
/// The door could not sign the box's certificate.
Sign(String),
/// `Door::forget` was asked about a name the estate never laid.
NotLaid(String),
}
impl fmt::Display for JoinError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JoinError::Wire(e) => write!(f, "REFUSED [join-wire] the join message could not be rendered or read: {e}"),
JoinError::Unknown(a) => write!(f, "REFUSED [join-unknown] {a} is not an address this estate laid a box on"),
JoinError::Spent { name, from } => write!(f, "REFUSED [join-spent] `{name}` ({from}) has already joined; the bootstrap certificate is not accepted for it again — a copied medium"),
JoinError::Forged(why) => write!(f, "REFUSED [join-forged] {why}"),
JoinError::Withdrawn => write!(f, "REFUSED [join-withdrawn] every box has joined; the bootstrap certificate is valid during bootstrap only"),
JoinError::WrongEstate { want, got } => write!(f, "REFUSED [join-estate] this is `{want}`, the request says `{got}`"),
JoinError::Sign(e) => write!(f, "REFUSED [join-sign] {e}"),
JoinError::NotLaid(n) => write!(f, "REFUSED [door-forget-unknown] `{n}` is not a box this estate laid; there is no admission to clear"),
}
}
}
impl std::error::Error for JoinError {}
/// One box the laptop laid: what the door will call it and where it goes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Slot {
pub name: String,
pub kind: Kind,
pub overlay: Ipv4Addr,
/// The front's own tunnel endpoint, for the peer list (only the front has one).
#[serde(default)]
pub endpoint: Option<String>,
/// **Where the box is on the utility network** (0.3.0). Since the
/// installer connects OUT to each VM, a slot is keyed by the address the
/// installer DIALS — the VM's public one — while the boxes reach each
/// other's control planes on the utility network. `None` = the key is the
/// utility address (the 0.2 join, where the box dialled the door).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utility: Option<Ipv4Addr>,
}
impl Slot {
/// The slot's utility address: its own, or the key it is laid under.
pub fn utility_or(&self, key: Ipv4Addr) -> Ipv4Addr {
self.utility.unwrap_or(key)
}
}
/// What a box presented when it was admitted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admitted {
pub from: Ipv4Addr,
pub tunnel_pubkey: String,
pub capacity: Option<Capacity>,
}
/// What an admission did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Admission {
/// The first join of this slot: sign the box's request.
First(String),
/// The box, speaking with its own certificate: a read.
Again(String),
}
impl Admission {
pub fn name(&self) -> &str {
match self {
Admission::First(n) | Admission::Again(n) => n,
}
}
}
/// **The front's ledger**: one slot per utility address, each joined once.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ledger {
pub estate: String,
/// utility address → the box laid there
pub slots: BTreeMap<Ipv4Addr, Slot>,
/// name → what it presented, once admitted
pub admitted: BTreeMap<String, Admitted>,
}
impl Ledger {
pub fn new(estate: &str) -> Ledger {
Ledger { estate: estate.to_string(), ..Default::default() }
}
pub fn lay(&mut self, utility: Ipv4Addr, slot: Slot) {
self.slots.insert(utility, slot);
}
/// **The rule of the bootstrap certificate**, in one place. The checks are
/// in the order that leaks the least.
pub fn admit(&mut self, req: &JoinRequest, who: &Presented, from: Ipv4Addr) -> Result<Admission, JoinError> {
if req.estate != self.estate {
return Err(JoinError::WrongEstate { want: self.estate.clone(), got: req.estate.clone() });
}
if *who == Presented::Bootstrap && self.complete() {
return Err(JoinError::Withdrawn);
}
let slot = self.slots.get(&from).ok_or(JoinError::Unknown(from))?.clone();
// 0.4.0 (owner 2026-09-25, the one-medium ruling): the FRONT's role is
// an install parameter — vm1 boots the same medium as the boxes, and
// the slot the installer laid makes it the front. Any other slot still
// demands its own kind.
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)));
}
let joined = self.admitted.contains_key(&slot.name);
match (who, joined) {
(Presented::Bootstrap, true) => Err(JoinError::Spent { name: slot.name, from }),
(Presented::Bootstrap, false) => {
if req.csr_pem.trim().is_empty() {
return Err(JoinError::Forged(format!("the first join from {from} carries no signing request; a box must mint its own key")));
}
self.admitted.insert(slot.name.clone(), Admitted { from, tunnel_pubkey: req.tunnel_pubkey.clone(), capacity: req.capacity });
Ok(Admission::First(slot.name))
}
(Presented::Box(cn), true) if *cn == slot.name && self.admitted[&slot.name].from == from => {
// ★ **A tunnel key learned late is recorded — once, and only
// into an empty field.** MEASURED 2026-09-24 on gunnar.rs: the
// boxes' first join carried NO tunnel key (the loop minted it
// when it raised the tunnel, after the join), so the relay had
// nobody to list and the twin never reached the primary's
// overlay address. The box asking again AS ITSELF (its own
// certificate) may fill the empty field; a key already recorded
// is never replaced — that would let a box re-key itself away
// from what the relay holds.
let entry = self.admitted.get_mut(&slot.name).expect("joined was just checked");
if entry.tunnel_pubkey.is_empty() && !req.tunnel_pubkey.is_empty() {
entry.tunnel_pubkey = req.tunnel_pubkey.clone();
}
Ok(Admission::Again(slot.name))
}
(Presented::Box(cn), _) => Err(JoinError::Forged(format!("{from} is `{}`'s address, and it presents the certificate of `{cn}`", slot.name))),
}
}
/// The slot of a box, by name.
pub fn slot_of(&self, name: &str) -> Option<(&Ipv4Addr, &Slot)> {
self.slots.iter().find(|(_, s)| s.name == name)
}
/// Every laid slot has a box in it.
pub fn complete(&self) -> bool {
!self.slots.is_empty() && self.slots.values().all(|s| self.admitted.contains_key(&s.name))
}
}
#[cfg(test)]
mod tests {
use super::*;
const MAIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 90);
const TWIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 87);
fn ledger() -> Ledger {
let mut l = Ledger::new("gunnar.rs");
l.lay(MAIN_UTIL, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None });
l.lay(TWIN_UTIL, Slot { name: "twin".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_B, endpoint: None, utility: None });
l
}
fn req() -> JoinRequest {
JoinRequest {
estate: "gunnar.rs".into(),
kind: Kind::Gunnar,
tunnel_pubkey: "K1".into(),
capacity: Some(Capacity { cores: 2, ram_bytes: 4 << 30, data_bytes: 10 << 30, disk_mib_s: 400 }),
csr_pem: "-----BEGIN CERTIFICATE REQUEST-----".into(),
}
}
#[test]
fn the_bootstrap_certificate_joins_once_then_the_box_speaks_as_itself_and_at_completion_bootstrap_is_withdrawn() {
let mut l = ledger();
assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Ok(Admission::First("main".into())));
assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Spent { name: "main".into(), from: MAIN_UTIL }), "a copied medium");
assert_eq!(l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL), Ok(Admission::Again("main".into())));
assert!(matches!(l.admit(&req(), &Presented::Box("main".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "main's certificate from twin's address");
assert!(matches!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "a certificate for a slot that never joined");
assert_eq!(l.admit(&req(), &Presented::Bootstrap, TWIN_UTIL), Ok(Admission::First("twin".into())));
assert!(l.complete());
assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Withdrawn), "bootstrap only during bootstrap");
assert_eq!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Ok(Admission::Again("twin".into())), "a joined box still reads");
}
#[test]
fn a_box_asking_as_itself_fills_an_empty_tunnel_key_once_and_never_replaces_one() {
let mut l = ledger();
let mut first = req();
first.tunnel_pubkey.clear();
l.admit(&first, &Presented::Bootstrap, MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "");
l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "learned late, recorded");
let mut other = req();
other.tunnel_pubkey = "K2".into();
l.admit(&other, &Presented::Box("main".into()), MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "never replaced");
}
#[test]
fn an_unlaid_address_a_wrong_kind_another_estate_and_a_join_without_a_request_are_refused_by_name() {
let mut l = ledger();
assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Unknown(Ipv4Addr::new(10, 13, 9, 1))));
let mut front = req();
front.kind = Kind::Front;
assert!(matches!(l.admit(&front, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
let mut other = req();
other.estate = "holger.rs".into();
assert!(matches!(l.admit(&other, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::WrongEstate { .. })));
let mut bare = req();
bare.csr_pem.clear();
assert!(matches!(l.admit(&bare, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
assert!(!l.complete());
assert_eq!(Presented::from_cn("bootstrap"), Presented::Bootstrap);
assert_eq!(Presented::from_cn("main"), Presented::Box("main".into()));
assert!(JoinError::Withdrawn.to_string().starts_with("REFUSED [join-withdrawn]"));
}
#[test]
fn the_front_tunnel_is_a_dialled_peer_and_an_undialable_one_is_refused() {
let f = FrontTunnel { tunnel_pubkey: "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".into(), endpoint: "95.111.211.189:51820".into() };
assert_eq!(f.check(), Ok(()));
let p = f.as_peer();
assert_eq!((p.name.as_str(), p.role, p.overlay, p.endpoint.as_deref()), (FRONT_PEER, Role::Front, crate::OVERLAY_FRONT, Some("95.111.211.189:51820")));
let reply = JoinReply { estate: "e".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: None, role: None, primary_overlay: None, primary_utility: None, peers: vec![p.clone()], feed: BTreeMap::new() };
assert_eq!(reply.front(), Some(&p));
assert!(FrontTunnel { tunnel_pubkey: String::new(), ..f.clone() }.check().is_err());
assert!(FrontTunnel { endpoint: "nowhere".into(), ..f }.check().is_err());
}
#[test]
fn the_messages_round_trip() {
let r = req();
assert_eq!(JoinRequest::parse(&r.render().unwrap()).unwrap(), r);
let mut feed = BTreeMap::new();
feed.insert("monetize_pubkey".to_string(), "eb16…".to_string());
let reply = JoinReply { estate: "gunnar.rs".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: Some("PEM".into()), role: None, primary_overlay: None, primary_utility: None, peers: vec![], feed };
assert_eq!(JoinReply::parse(&reply.render().unwrap()).unwrap(), reply);
}
}