crates/installer/nordisk-pairing/src/tunnel.rs
//! **The tunnel the estate meets inside** — a driver trait, and the one
//! driver that needs no kernel: `none`.
//!
//! Only the FRONT needs a known address (its endpoint is in every seed); the
//! other boxes dial it and the driver learns their endpoints from the
//! handshake. Inside, the addresses are the overlay constants. The choice of
//! driver is a build-thing setting of the medium (the kernel module or the
//! user-space engine has to be IN the image), and the seed says which one the
//! image was composed for.
//!
//! **What is here and what is not** (2026-09-24):
//!
//! | kind | driver (feature) | status |
//! |-------------|-------------------------------------------|--------|
//! | `none` | [`NoTunnel`] | works — the cloud's own addresses |
//! | `wireguard` | `wireguard::WireGuard` (`wireguard`) | works — wg-appliance-core, userspace |
//! | `mullvad` | `mullvad::Mullvad` (`mullvad`) | works — the estate MESH stays WireGuard via the front; Mullvad is each box's EXIT, a second interface |
//! | `tailscale` | `tailscale::Tailscale` (`tailscale`) | REFUSED `[tunnel-tailscale-unimplemented]` — no pure-Rust coordination client exists |
//!
//! A kind the image was not composed with is `REFUSED [tunnel-not-built]`.
//! No driver answers "up" without bringing anything up — that would be the
//! failure shape this whole crate exists to end.
//!
//! **Configuration** is not the trait's business: [`driver_for`] builds the
//! driver from the seed's public choices (`[mullvad]`, `[tailscale]`) and the
//! door's FEED (the credentials — never on the medium). [`driver`] is the same
//! with neither, for the kinds that need none.
use crate::join::Peer;
use crate::seed::{Seed, TunnelKind};
use std::collections::BTreeMap;
use std::fmt;
use std::net::Ipv4Addr;
/// This box's tunnel identity: the public half goes in the join request; the
/// private half is minted on the box and never leaves it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
pub overlay: Ipv4Addr,
pub pubkey: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TunnelStatus {
pub kind: TunnelKind,
pub up: bool,
/// Peers with a completed handshake, by name.
pub reachable: Vec<String>,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TunnelError {
/// The image was not composed with this driver.
NotBuilt(TunnelKind),
/// The driver is built and cannot work yet; `missing` names the piece.
Unimplemented { kind: TunnelKind, missing: &'static str },
/// The driver needs a value the seed or the door's feed did not give.
NotConfigured { kind: TunnelKind, what: String },
Failed(String),
/// 0.4.0: `tunnel = none` gives the installer no link, and the installer
/// reaches a VM through its WireGuard link under mutual TLS ALWAYS.
InstallerLinkNone,
}
impl fmt::Display for TunnelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TunnelError::NotBuilt(k) => write!(
f,
"REFUSED [tunnel-not-built] the seed asks for `{}` and this image carries no such driver — compose the medium with it (a build-thing setting), do not guess at runtime",
k.as_str()
),
TunnelError::Unimplemented { kind, missing } => write!(f, "REFUSED [tunnel-{}-unimplemented] the `{}` driver is built but cannot bring a tunnel up: missing {missing}", kind.as_str(), kind.as_str()),
TunnelError::NotConfigured { kind, what } => write!(f, "REFUSED [tunnel-{}-unconfigured] {what}", kind.as_str()),
TunnelError::Failed(e) => write!(f, "REFUSED [tunnel-failed] {e}"),
TunnelError::InstallerLinkNone => write!(f, "REFUSED [installer-link-none] the installer reaches a VM through its WireGuard link, under mutual TLS both ways, ALWAYS (owner 2026-09-25); `tunnel = none` gives it no link and is refused — lay the estate with `tunnel = wireguard`"),
}
}
}
impl std::error::Error for TunnelError {}
pub trait Tunnel {
fn kind(&self) -> TunnelKind;
/// **The key this box presents in its join request** — the public half of
/// its mesh tunnel identity, known BEFORE `up` (the box joins first and
/// raises the tunnel second, `crate::pair`). Base64, WireGuard's own
/// encoding; empty when the kind has no such key (`none`; `tailscale`,
/// whose node key is the coordination server's business). Default: empty,
/// so an implementation that predates this method still compiles.
fn public_key(&self) -> String {
String::new()
}
/// Bring the interface up with this identity and these peers. Idempotent.
fn up(&mut self, me: &Identity, peers: &[Peer]) -> Result<(), TunnelError>;
/// A peer learned after `up` (the join reply names the ones that joined earlier;
/// later joiners arrive through the front's peer updates).
fn add_peer(&mut self, peer: &Peer) -> Result<(), TunnelError>;
fn status(&self) -> TunnelStatus;
/// **Re-point a tunnel at these peers, in this process** (0.5.17, A7 row 7; Gunnar
/// 907f4f99 `Tunnel::repoint`): the front's relay moved (a FRONT recharged onto new
/// addresses) or was re-keyed, and a box re-fed after it dials the new relay at once, never
/// after a restart. The running pump is stopped and the new one raised on the same
/// interface and address; the box keeps its own key. `Ok(true)` when the dial changed,
/// `Ok(false)` when it already dials these peers, or when no pump runs yet (the raise
/// takes them). Default: handed to [`Tunnel::up`] (`true`).
fn repoint(&mut self, me: &Identity, peers: &[Peer]) -> Result<bool, TunnelError> {
self.up(me, peers).map(|()| true)
}
/// **The pre-shared key of this box's link to the front** (0.5.17, A7 row 13; Gunnar
/// 356e8dd2), as the installer fed it (`crate::installer::FEED_WG_PSK`, base64, 32
/// bytes), mixed into every handshake: WireGuard's handshake is X25519 alone, and a key
/// both ends hold — fed over the installer's post-quantum TLS — is what a recording cannot
/// be opened without. `Ok(true)` when it changed (a running pump takes it at once). A value
/// that is not a key is refused by name, never quoted. Default: a driver with no such key
/// (`none`, `tailscale`) takes nothing (`false`).
fn set_preshared_key(&mut self, _psk: Option<&str>) -> Result<bool, TunnelError> {
Ok(false)
}
/// **How the installer reaches a VM of this kind during bootstrap**
/// (0.3.0): through its own WireGuard link (`wireguard`, `mullvad`),
/// straight to the public address (`none`), or refused by name
/// (`tailscale`). One answer per kind, [`crate::installer_link::reach`];
/// a driver overrides it only if its kind ever reaches otherwise.
fn installer_reach(&self) -> Result<crate::installer_link::Reach, TunnelError> {
crate::installer_link::reach(self.kind())
}
}
/// **No tunnel**: the boxes speak over the addresses the cloud gave them.
/// `up` records the peers so `status` can name them; nothing else exists to
/// bring up, and "reachable" is every peer, because there is no handshake to
/// wait for.
#[derive(Debug, Default)]
pub struct NoTunnel {
peers: Vec<Peer>,
up: bool,
}
impl Tunnel for NoTunnel {
fn kind(&self) -> TunnelKind {
TunnelKind::None
}
fn up(&mut self, _me: &Identity, peers: &[Peer]) -> Result<(), TunnelError> {
self.peers = peers.to_vec();
self.up = true;
Ok(())
}
fn add_peer(&mut self, peer: &Peer) -> Result<(), TunnelError> {
if !self.peers.iter().any(|p| p.name == peer.name) {
self.peers.push(peer.clone());
}
Ok(())
}
fn status(&self) -> TunnelStatus {
TunnelStatus {
kind: TunnelKind::None,
up: self.up,
reachable: self.peers.iter().map(|p| p.name.clone()).collect(),
detail: "no tunnel: the cloud's own addresses".into(),
}
}
}
/// The driver for a seed's tunnel kind with NO configuration, or the refusal
/// that the image was composed without it. For `mullvad` and `tailscale` use
/// [`driver_for`]: without their seed section and the door's feed, their `up`
/// refuses by name (`[tunnel-mullvad-unconfigured]`) rather than guess.
pub fn driver(kind: TunnelKind) -> Result<Box<dyn Tunnel>, TunnelError> {
build(kind, None, &BTreeMap::new())
}
/// **The driver a box raises**, built from its seed's public choices and the
/// credentials the door FED it at the join (`crate::seed::FEED_MULLVAD_ACCOUNT`,
/// `crate::seed::FEED_TAILSCALE_AUTHKEY`). Keys are minted fresh; a box that
/// kept its keys constructs the concrete driver itself (`WireGuard::with_key`,
/// `Mullvad::new`).
pub fn driver_for(seed: &Seed, feed: &BTreeMap<String, String>) -> Result<Box<dyn Tunnel>, TunnelError> {
build(seed.tunnel, Some(seed), feed)
}
#[allow(unused_variables)]
fn build(kind: TunnelKind, seed: Option<&Seed>, feed: &BTreeMap<String, String>) -> Result<Box<dyn Tunnel>, TunnelError> {
match kind {
TunnelKind::None => Ok(Box::new(NoTunnel::default())),
#[cfg(feature = "wireguard")]
TunnelKind::Wireguard => Ok(Box::new(crate::wireguard::WireGuard::mint(None))),
#[cfg(feature = "mullvad")]
TunnelKind::Mullvad => Ok(Box::new(crate::mullvad::Mullvad::new(
crate::wireguard::WireGuard::mint(None),
None,
seed.and_then(|s| s.mullvad.clone()).unwrap_or_default(),
feed.get(crate::seed::FEED_MULLVAD_ACCOUNT).cloned(),
))),
#[cfg(feature = "tailscale")]
TunnelKind::Tailscale => Ok(Box::new(crate::tailscale::Tailscale::new(
seed.and_then(|s| s.tailscale.clone()).unwrap_or_default(),
feed.get(crate::seed::FEED_TAILSCALE_AUTHKEY).cloned(),
))),
#[allow(unreachable_patterns)]
other => Err(TunnelError::NotBuilt(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Role;
#[test]
fn no_tunnel_is_up_at_once_and_names_every_peer_and_a_missing_driver_is_refused_by_name() {
let mut t = driver(TunnelKind::None).unwrap();
assert_eq!(t.public_key(), "", "`none` has no key to present");
let me = Identity { overlay: Ipv4Addr::new(10, 13, 9, 90), pubkey: String::new() };
let front = Peer { name: "front".into(), role: Role::Front, tunnel_pubkey: String::new(), overlay: Ipv4Addr::new(10, 13, 8, 179), endpoint: None, utility: None };
t.up(&me, std::slice::from_ref(&front)).unwrap();
t.add_peer(&front).unwrap();
let s = t.status();
assert!(s.up && s.reachable == vec!["front".to_string()], "{s:?}");
for (kind, built) in [
(TunnelKind::Wireguard, cfg!(feature = "wireguard")),
(TunnelKind::Mullvad, cfg!(feature = "mullvad")),
(TunnelKind::Tailscale, cfg!(feature = "tailscale")),
] {
match driver(kind) {
Ok(d) => assert!(built && d.kind() == kind, "{kind:?} built without its feature"),
Err(e) => {
let e = e.to_string();
assert!(!built && e.starts_with("REFUSED [tunnel-not-built]") && e.contains(kind.as_str()), "{e}");
}
}
}
}
#[test]
fn the_refusals_carry_their_names() {
let e = TunnelError::Unimplemented { kind: TunnelKind::Tailscale, missing: "x" }.to_string();
assert!(e.starts_with("REFUSED [tunnel-tailscale-unimplemented]"), "{e}");
let e = TunnelError::NotConfigured { kind: TunnelKind::Mullvad, what: "no account".into() }.to_string();
assert!(e.starts_with("REFUSED [tunnel-mullvad-unconfigured] no account"), "{e}");
}
}