crates/installer/nordisk-pairing/Cargo.toml
[package]
name = "nordisk-pairing"
# 0.5.28 (2026-10-27, R5G): crates.io's 0.5.27 lacks RA4-13 (egress Slots, Slot, Limits, Said, serve_with:
# a per-peer cap, a total head deadline, an idle splice timeout, a refusal said once per window) and
# A32-8. Additive.
# 0.5.27 (2026-10-26, A3, R5 prep): crates.io's 0.5.26 is Skidbladnir 4fc391f; what landed after
# it under the same number (P4c batch 2: `frontside::{mesh_probe, recharge, renew}`, features
# `mesh-probe`, `front-renew`, `kept`, `boxside/`; P4-REST: `kept::readmit_together`, `MemSink`,
# `ReadmitLog`, the shared re-admission in `frontside::update`) is 0.5.27. Additive.
# 0.5.26, P4a (2026-10-25, Holger first; 0.5.26 is not published yet, so the additions ride it and
# no lock that pins 0.5.26 by path moves — Gunnar's included): two pieces both products carried, shared.
# `seed::metadata` — the ONE seed reader of a release medium (`staged_text`, `staged`, `read`,
# `of_user_data`, `user_data`, `http_get`, METADATA_*): Holger's `metadata_seed.rs` and Gunnar's
# `pairing.rs` seed reader (A28 L12). `frontside::update` (feature `frontside-update`) — the
# front's update step (`decide`, `staged_of`, `declared_of`, `wait_confirmed`, `step`, `look`,
# `Hands`, `Shop`; A22 W8), Holger's lane-B5 version, the front's keep words the product's
# (`Hands::front_keep`). Additive.
# 0.5.26 + P3a (2026-10-25, A28 H13; no version bump, so no consumer lock goes stale):
# `seed::Kind::Box(BoxRole)`, a product's own box kind beside Front/Gunnar/Holger, so any
# product (the Lake) pairs. Spelled by its own name on the wire (`kind = "fe"`); the old kinds
# keep their spellings; `Kind` stays Copy; an untagged VM is still a gunnar box. A box kind
# is not elected: it stands alone as its own primary (`Kind::is_box`, `is_elected`,
# `named`, `as_str`). Additive: a reader before P3a refuses a box kind by name.
# 0.5.25 (2026-10-21, lane B5): a live update carries what its release declares —
# `live::Declared` (the release declaration's bundle and signature, the release's
# korp.env.* tokens) on the stage body (`Stage::encode_declared` / `decode_declared`; an
# unknown line refused by name), `Vm` / `Product` / `FrontProduct::live_stage_declared`
# (default: an undeclared stage as before, a declared one refused `[live-declared-unsupported]`),
# `Client::live_stage_declared`, and `falls_to_keep` (the refusals a front answers with a keep
# boot). Additive: `Stage` keeps its fields, a reader before 0.5.25 ignores the new lines.
# 0.5.24 (2026-10-21, lane G11): `autofill` — Gunnar's front autofill decision shared
# (`due`, `Awaits`, `FEED_REPAIR_AUTO`, `LOOK_EVERY`): a re-laid main is filled by the front.
# 0.5.23 (2026-10-21, lane G10, Gunnar f21d8d09 shared; 0.5.22 is lane B3's Narrator): `BootLog::for_install()`, the log
# an installed process keeps (the head and the tail at every default bound), and the head is
# sealed by the listener itself: `POST /install/close` seals before it answers, and a restart
# after the close (`serve_after_close_at`) seals before the log door opens. Additive.
# 0.5.18 (2026-10-21, lane G7, rickard-81 before Gunnar deletes its copies): the operator lines
# Gunnar says, said here through `tracing` (target `nordisk_pairing::…`): a fed file that
# cannot be read (ERROR, still taken as absent: `fed_at_rest::read`); the first boot's keep
# boot (INFO), a keep boot whose ESP marker could not be written (ERROR) and a kept feed that
# cannot be read and is cleared (WARN) (`boxside::reset_if_fresh_install_of`). Through the
# rested loop's `say`: the re-point names the old and the new peer (`RE-POINTED from
# [<key>@<endpoint>] to [<key>@<endpoint>]`, `BoxDoors::follow_the_front`), and the rested
# loop watches the re-pointed tunnel (`boxloop::WATCH_EVERY` x `WATCH_ATTEMPTS`) and says ONCE
# that the front answered (`HANDSHAKE COMPLETED`) or that nobody did (`NO handshake`).
# Additive.
# 0.5.17 (2026-10-21, lane G5, A7 rows 6, 7, 10, 13, 21 and G4's durability finding), Gunnar's
# behaviour shared: `fed_at_rest::{write, read, read_checked}` seals `<data>/pairing/joined.toml`
# (AES-256-GCM, Gunnar's `gsk1` format, the file's path as associated data, the KEK in
# `pairing/secrets.kek` 0600 or `NORDISK_SECRET_KEK` / `GUNNAR_SECRET_KEK`) on box and front;
# `boxside::reset_if_fresh_install_of(esp, data, state, seed, marker)`: a keep boot of the SAME
# estate keeps the feed, key and certificate; `boxside::mark_esp`; every pairing file, the
# door ledger and the installer's run files written through edda appliance-pid1 `durable`
# (staged, fsync, rename, fsync of the directory); `Tunnel::repoint` and
# `BoxDoors::follow_the_front` (a box re-fed after a FRONT recharge re-points wg0 at once, from
# the rested loop: `Doors::rest`); the link's PSK at both ends (`Tunnel::set_preshared_key`,
# `wireguard::box_plan_with_psk`, `WireGuard::box_via_front_with_psk`,
# `frontside::relay_list_with_psk`); `frontside::busy::{on, busy}`. Additive.
# 0.5.16 (2026-10-20, G2, A7 row 1): `elect::pull_role` — the role a box pulls and reports
# as (the flag, else its own twin record, else the door's election; `None` while not
# elected), moved here from monetize-embed `fleet::pull_role` so a community build reads it
# without linking a monetize crate. `feedbus::offered` is now exactly Gunnar's
# `feed::offered`: `offer..<key>` and `offer.a.b.<key>` name no VM and offer nothing.
# 0.5.15 (2026-10-20, G1, A7 row 8): THE FEED BUS, shared (`feedbus`, also
# `boxside::feedbus`): `Bus<T>::{publish, on_published, on_change, current}` — a new or
# changed value reaches every subscriber, the same value again nobody; `on_change` runs
# for one view of it (one key). `Installed::feed_bus()` publishes the reply kept at open
# and every feed and re-feed after, so a value fed after the start (the front's offer:
# the monetize key, the fleet door) is applied live. `feedbus::global()` is the
# process-wide `Bus<Feed>` in Gunnar's `feed::{publish, on_published, current}` shape
# (lane L10 moves Gunnar onto it). Additive.
# 0.5.14 (2026-10-18, L20): the live door of a SHARED box and front — `boxside::Product` and
# `frontside::FrontProduct` take `live_running` / `live_stage` (default: refused by name,
# `[live-unsupported]`), and `Installed<P>` / `Front<P>` answer `/live/running` and
# `/live/stage` with them, so Holger's boxes and front take the live update Gunnar's do.
# Additive.
# 0.5.11 (2026-10-03, holger lifecycle, picked onto the release-3 merge): the
# boxes wait for the FRONT's late offer (`installer::boxes_wait_for`,
# HOLD_FOR_OFFER_FEED — a monetize_pubkey the front feeds after it is up), and
# `readmit_kept` — a box upgraded IN PLACE is re-admitted at the address it has
# and a primary is never told to recharge (`Door::readmit_kept`,
# `installer::readmit_kept`). Additive.
# 0.5.10 (2026-09-30, DEV-NARRATE): `bootstrap::REACHED`, the line a VM says ONCE
# (INFO) the first time the installer's own certificate reached its listener —
# the installer's narration (Skidbladnir nordisk-narrate) reads a box log
# without it as "stopped before WireGuard". Additive.
# 0.5.9 (2026-09-29, release-3): the ONE number for the joined line. Two branches had
# each called their join 0.5.8 — recharge's (release-2's pinned-head boot log + the
# holger line's 0.5.5 box loop / 0.5.7 frontside + readmit / re-feed: Door::readmit,
# installer::readmit, POST /install/refeed) and money's (the pinned head, byte bounds
# + the holger line's frontside, boxloop). release-3 carries both, so it is numbered
# anew and 0.5.8 is never published: no version names two contents.
# (release-3 also carries monetize-ui d874f15: an estate of ONE — a front and no
# product box — is complete once admitted.)
# 0.5.7 (2026-09-27): the FRONT medium's side, shared (`frontside`): `Front<P:
# FrontProduct>` is vm1's `Vm` (kind front, only role front, PAIRED once the
# relay is up and every box is listed), `verdict` reads the election from the
# front's feed, `relay_list` is what the relay lists (the relay itself is edda
# `estate-front::relay`). Moved out of gunnar-front vm.rs + gunnar-pairing-door
# front.rs when holger's estate grew its own front. Additive.
# 0.5.5 (2026-09-27): the box-side pairing loop AFTER the feed, shared
# (`boxloop`: `turn`, `run`, `start`, `BoxDoors` over `boxside::Installed`, and
# the product's `Steps` — primary ready, become twin, certified, follow):
# gunnar-server's pairing.rs made product-neutral. The signal it writes is what
# `Vm::status` answers, so `CloseWhen::Paired` closes. The wg0 driver of a box
# on the front's relay (`WireGuard::box_via_front`, `wireguard::box_plan`): the
# overlay address ON wg0 (/24) and the front the one peer owning 10.44.0.0/24 —
# the fix gunnar found. The boot-log tap (`bootlog::Tap`,
# `BootLog::capture_esp`). Additive: nothing existing changes shape.
# 0.5.5 (2026-09-28, MEASURED on test2's main VM: `… 2243 lines dropped`, the
# pairing lines gone after the close, 8 502 of 20 008 held lines a pubkey-class
# marker): `BootLog::with_head` keeps a PINNED HEAD (the install and the
# pairing; `seal_head` when the install is over) that runtime noise can never
# evict, and every log is bounded in BYTES too (`DEFAULT_BYTES`, `HEAD_BYTES`;
# before, 20 000 lines x 4 KiB was 80 MiB at worst). Lines are cleaned ON THE
# VM before they are held: a public key's VALUE is withheld inside its line
# (`redact::cleaned`, idempotent, markers clean to themselves), and a run of
# whole-line-withheld lines of one class is ONE counted marker
# (`redact::withheld_run`; the installer folds the same way,
# `redact::clean_lines`). Additive: `BootLog::new` keeps its meaning.
# 0.5.0 (2026-09-25, owner): a PER-VM feed and the front's certificate
# hand-over. `PlanVm::feed` = values that VM alone is fed (over `Plan::feed`),
# kept by the door as `Door::own_feed` (never serialized into the ledger);
# `tls.*`, `plugin.*`, `plugin_root.*` and `console_admin_token` are refused in
# the shared feed (`[feed-front-only-shared]`) and on a box
# (`[feed-front-only-on-a-box]`). A VM's SECRET offer (`GET
# /install/secret-offer`, `Vm::secret_offer`, `bootstrap::SecretOffer`) is
# asked once before the close, handed ONLY to the REQUIRED
# `Sink::secret_offer` (FolderSink: `<run>/secret-offers.toml`, 0600), never
# fed on, ledgered or logged; `installer::successor_feed` carries it into the
# next run's front feed. Breaking: `PlanVm { .. }` literals add `feed`, every
# `Sink` impl adds `secret_offer`.
# 0.4.2 (2026-09-25): the front every box is fed (derived from the FRONT slot)
# is dialled on vm1's UTILITY address:51820, not the public one the installer
# dials — the public one leaves the provider and a mock guest cannot route it.
# 0.4.1 (2026-09-25): `Peer::utility` — every box peer carries its utility
# address, so the front (vm1, same medium) can certify the twin against the
# primary on the utility network from what it is fed. Additive on the wire
# (serde default); a struct literal `Peer { .. }` must add `utility: None`.
# 0.4.0 (2026-09-25, owner): WireGuard + mutual TLS ALWAYS — `tunnel = none`
# is refused by name (`[installer-link-none]`, `TunnelError::InstallerLinkNone`),
# every seed carries the installer link, `Reach::Direct` is gone; vm1 boots the
# SAME medium and a FRONT slot makes it the front (`Door::front_tunnel` derives
# the relay key and endpoint from its presented tunnel key). Breaking.
# 0.3.1 (2026-09-25): the VM OFFERS the installer public values only it can
# mint (`GET /install/offer`, `Vm::offer`); the installer feeds each offer to
# the other VMs as `offer.<vm>.<key>` and hands it to `Sink::offer`. Additive.
# 0.3.0 (2026-09-24, late): the installer connects OUT to the VMs — a bootstrap
# listener on every VM, the installer's side as a library (src/installer.rs),
# the installer a temporary WireGuard peer (src/installer_link.rs).
# 0.2.0 (2026-09-24 evening): the seed carries no per-lay value — the front's
# tunnel key and endpoint are FED by the door (`Door::front`, the reply's
# `front` peer), and the installer CA is per medium. A 0.1 seed still parses.
version = "0.5.29"
edition = "2021"
description = "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)."
license = "MIT OR Apache-2.0"
repository = "https://git.gunnar.rs/nordisk/Skidbladnir"
keywords = ["pairing", "wireguard", "estate", "seed", "join"]
categories = ["network-programming"]
exclude = ["/.nornir/"]
[dependencies]
# The seed is TOML on the install medium's seed disk; the join messages are
# TOML too, so one reader serves both and nothing here needs a second format.
serde = { version = "1", features = ["derive"] }
toml = "1"
# The installer's mutual TLS (src/mtls.rs), valid during bootstrap only: the CA,
# the door's and the bootstrap certificate, a box's own from its request — and
# TLS 1.3 over blocking sockets on `ring`, which an OS-less PID 1 can drive.
# Held with Skidbladnir's rcgen 0.13 (see the root manifest, 2026-10-02 W3); x509-parser
# stays on the major rcgen 0.13's `x509-parser` feature links.
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "ring", "x509-parser"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
x509-parser = "0.18"
time = "0.3"
# **X25519MLKEM768** (src/pqtls.rs, 2026-10-06): the hybrid TLS group every
# rustls end here offers first — composed from RustCrypto's ML-KEM-768 (FIPS
# 203; the crate russh's `mlkem768x25519-sha256` runs on, already in every
# graph that has russh) and rustls's ring X25519. Pure Rust: rustls's own
# X25519MLKEM768 lives in its aws-lc-rs provider, which is C. `getrandom` is
# the OS CSPRNG for the key pair and the encapsulation; `zeroize` wipes the
# decapsulation key on drop.
ml-kem = { version = "0.3.2", features = ["getrandom", "zeroize"] }
zeroize = "1"
# **The feed at rest** (src/fed_at_rest.rs, 0.5.17): AES-256-GCM and the OS CSPRNG from `ring`,
# the provider rustls and rcgen already run on here (Gunnar's own seal is aws-lc-rs; the
# sealed bytes are the same `gsk1` format).
ring = "0.17"
# **Durable writes** (0.5.17, G4): edda's ONE writer a power cut leaves the old or the new
# file of — staged, fsync, rename, fsync of the directory. Default features: std and libc.
appliance-pid1 = { path = "../../../../edda/crates/appliance-pid1", version = "0.1.7" }
# **The operator's lines** (0.5.18, G7): what this crate decides where no `say` reaches (a fed
# file that cannot be read, the first boot's verdict) is said through `tracing`, the way the
# products already log. No proc macros: `std` only.
tracing = { version = "0.1", default-features = false, features = ["std"] }
# **THE userspace WireGuard of the suite** — what tunnr's PID 1 and gunnar's
# OS-less appliance already run on: x25519 keys, the boringtun pump, the TUN.
# Optional and off by default so a front (an OS with a kernel WireGuard) or a
# `tunnel = none` estate links no crypto it will not use.
wg-appliance-core = { path = "../../../../edda/crates/wg-appliance-core", version = "0.1.5", default-features = false, optional = true }
# **Mullvad** (feature `mullvad`): the suite's Mullvad interop — the relay
# list, the device-registration shapes, `tunnel_for_relay` → a `TunnelSpec`
# for the SAME data plane. From crates.io (0.2.0 is byte-identical to
# ~/git/tunnr-mullvad); its `wg-appliance-core` is patched to this
# workspace's at the edda root (`[patch.crates-io]`), so its `TunnelSpec` IS
# ours — a consumer building `mullvad` from another root needs the same patch,
# or the build fails loudly on two `TunnelSpec` types. `peerset` only: no PQ.
tunnr-mullvad = { version = "0.2", default-features = false, features = ["peerset"], optional = true }
serde_json = { version = "1", optional = true }
# The HTTP client tunnr-mullvad deliberately does not carry (its transport is
# the caller's): ureq on rustls + ring + webpki-roots, already in the lock.
# Only the `mullvad` feature links it.
ureq = { version = "3", default-features = false, features = ["rustls"], optional = true }
# The installer's user-space end of the bootstrap link (feature
# `installer-wireguard`): boringtun is the one the suite already runs (through
# wg-appliance-core, same version); smoltcp is a pure-Rust TCP/IP stack, so a
# TCP connection runs inside the tunnel with no TUN device and no root.
boringtun = { version = "0.7.1", default-features = false, optional = true }
# `dup2` over fd 2 (feature `stderr-capture`).
rustix = { version = "1", default-features = false, features = ["std", "stdio"], optional = true }
smoltcp = { version = "0.14", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "socket-tcp"], optional = true }
# **The update step's pin** (feature `frontside-update`, 0.5.26/P4a): a release's live app is named by
# its blake3, as korp's slots name it. The sha256 beside it is ring's, already linked.
blake3 = { version = "1", optional = true }
[features]
default = []
# **The update step** (0.5.26, P4a): `frontside::update`, the front's half of the update the ONE
# box opens (decide, stage, wait confirmed, the keep fallback) — Holger's and Gunnar's fronts.
frontside-update = ["dep:blake3"]
# **fd 2 into the boot log** (`bootlog::capture_stderr`): a PID 1 that embeds a
# library logging with `eprintln!` (monetize-server in a product's front) has
# those lines read by the installer's log door too, not only the serial line.
# rustix (pure-Rust syscalls, no libc) for the one `dup2` std does not offer.
stderr-capture = ["dep:rustix"]
# **The installer as a TEMPORARY PEER of the tunnel** (0.3.0, owner
# 2026-09-24): the operator's machine is behind NAT and holds no TUN device,
# so the installer speaks WireGuard in user space (boringtun, one session per
# VM) and TCP over it in user space (smoltcp) — src/installer_link.rs. The VM
# side of the same link rides `wireguard` (a TUN on the VM).
installer-wireguard = ["wireguard", "dep:boringtun", "dep:smoltcp"]
# The userspace driver (tunnr / OS-less boxes). On an OS the same `Tunnel`
# trait is implemented over the kernel's WireGuard (netlink) — a different
# implementation of one contract, as the owner ruled 2026-09-24.
wireguard = ["dep:wg-appliance-core"]
# **The kept installer material** (src/kept.rs): the bundle a sold estate keeps in its
# primary's sealed slot, read by its front to re-admit a lost or upgraded box (its re-admission
# rides `installer-wireguard`). Gunnar's and Holger's fronts read the same bundle.
kept = ["dep:serde_json"]
# **The front's mesh probe** (`frontside::mesh_probe`, P4c): it DIALS the estate's boxes (plain IP
# must be refused, the tunnel must answer), so it is off by default; the product's config turns
# the probe on (Holger: `wg_only`).
mesh-probe = []
# **The paired links' renewal** (`frontside::renew`, P4c): the schedule and the steps that renew
# the installer CA and the boxes' certificates over the paths the product hands in; off by default.
front-renew = []
# The fleet probe: the protocol with real sockets and a real tunnel, run by
# draupnir containers (tests/fleet_draupnir.rs). The relay is the front's half.
probe = ["wireguard", "installer-wireguard", "wg-appliance-core/hubfixture"]
# The Tailscale driver: the seed's control URL and the fed auth key, and a
# refusal by name — no pure-Rust coordination client exists (src/tailscale.rs).
tailscale = []
# The Mullvad driver: each box leaves through a Mullvad relay (a second
# interface) while the estate mesh stays WireGuard via the front (src/mullvad.rs).
mullvad = ["wireguard", "dep:tunnr-mullvad", "dep:serde_json", "dep:ureq"]
[[bin]]
name = "pairing-probe"
path = "src/bin/pairing-probe.rs"
required-features = ["probe"]
[dev-dependencies]
# `frontside::update`'s own tests run on every `cargo test` (the module is compiled for tests).
blake3 = "1"
# The tests hear the operator lines this crate says through `tracing` (0.5.18, lane G7).
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "std"] }
# The fleet test (tests/fleet_draupnir.rs): draupnir's container backend boots
# the three probes; wg-appliance-core mints the front's key and the token.
draupnir = { path = "../../../../draupnir", version = "0.2", default-features = false, features = ["backend-oci"] }
wg-appliance-core = { path = "../../../../edda/crates/wg-appliance-core", version = "0.1.5" }
# tests/installer_link_goes_quiet.rs catches what the box's link pump says
# (boringtun's handshake lines are `tracing` events). Already in the tree via
# wg-appliance-core and boringtun.
tracing = "0.1"