nordisk-pairing 0.5.29

The boxes of an estate find each other: a seed each box is installed with, a one-time join against the front, a tunnel (WireGuard by default) they meet inside, roles decided once, a pairing loop each box runs itself, and the signal it reports. Shared by gunnar and holger; the laptop's xtask only lays machines, writes seeds and reads the signal. Design: .nornir/pairing-design.md (2026-09-24).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! **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);
    }
}