crates/estate-front/src/relay.rs
//! **The estate's WireGuard relay** — the relay half of gunnar's
//! `gunnar-pairing-door/src/front.rs` `FrontRole`, moved here as is.
//!
//! On udp/51820 (`nordisk-pairing`'s `RELAY_PORT`), keyed with the FRONT's own
//! tunnel key — the key its presentation to the installer carried, so the key
//! the installer's door feeds every box as the `front` peer — owning the
//! overlay's `.1`, listing each fed box by its overlay /32. It is edda
//! `wg-appliance-core`'s userspace [`HubRelay`](wg_appliance_core::hubrelay::HubRelay):
//! one UDP socket, no TUN, so the front needs no `wg0` of its own.
use std::collections::BTreeSet;
use std::net::Ipv4Addr;
/// **The relay, running.** Owned for the life of the process: dropping it
/// stops the relay.
pub struct Relay {
hub: wg_appliance_core::hubrelay::HubRelay,
listed: BTreeSet<String>,
/// The key (and the PSK, in memory only, as the fed reply itself is) listed at each
/// overlay address — so a NEW key at a known address is said as the relisting it is.
at: std::collections::BTreeMap<Ipv4Addr, (String, Option<String>)>,
/// The name each listed overlay address was fed under, for the line a forget says.
names: std::collections::BTreeMap<Ipv4Addr, String>,
}
/// The relay's private key from its base64 spelling (the ceremony's `wg0.key`).
pub fn key_from_b64(b64: &str) -> Result<[u8; 32], String> {
wg_appliance_core::decode_key(b64.trim()).map_err(|e| format!("the front's tunnel key is not a WireGuard key: {}", e.0))
}
impl Relay {
/// Bind udp/`port` (0 = any, for tests) with `private`, owning `hub_addr`
/// on the overlay.
pub fn start(port: u16, private: [u8; 32], hub_addr: Ipv4Addr) -> Result<Relay, String> {
Relay::start_with_hub(port, private, hub_addr, None)
}
/// [`Relay::start`] with the front's OWN overlay interface attached (`hub`, a TUN
/// owning `hub_addr`): the front then reaches every listed box over the tunnel
/// itself — what a WireGuard-only estate's front dials its boxes on. `None`: the
/// relay only, no interface (as [`Relay::start`]).
pub fn start_with_hub(port: u16, private: [u8; 32], hub_addr: Ipv4Addr, hub: Option<std::sync::Arc<dyn wg_appliance_core::hubrelay::HubPort>>) -> Result<Relay, String> {
// A32-14, stays a thread: HubRelay::spawn is the WireGuard relay's own UDP loop (wg-appliance-core),
// owned by this Relay for the front's life and stopped when it drops; it is no job.
let hub = match hub {
Some(p) => wg_appliance_core::hubrelay::HubRelay::spawn_with_port(port, private, hub_addr, p),
None => wg_appliance_core::hubrelay::HubRelay::spawn(port, private, hub_addr),
}
.map_err(|e| format!("the relay could not bind udp/{port}: {e}"))?;
Ok(Relay { hub, listed: BTreeSet::new(), at: std::collections::BTreeMap::new(), names: std::collections::BTreeMap::new() })
}
/// The relay's public key (base64) — the front's own tunnel key.
pub fn public(&self) -> &str {
&self.hub.public_b64
}
/// The UDP port it listens on.
pub fn port(&self) -> u16 {
self.hub.port()
}
/// **The relay's counters, one line** (lane D12): per listed box its handshakes and
/// traffic, then what was REFUSED (an initiation not sealed to this relay's key, or
/// from a key it does not list) — the relay drops those silently, so this line is the
/// only place a key mismatch shows.
pub fn counters(&self) -> String {
wg_appliance_core::hubrelay::describe(&self.hub.verdict())
}
/// **List the fed boxes with their pre-shared keys** (lane G5, A7 row 13; Gunnar 356e8dd2
/// `FrontRole::take`) — `(name, tunnel key, overlay, PSK)`, what Skidbladnir
/// nordisk-pairing `frontside::relay_list_with_psk` reads off the front's feed
/// (`wg_psk.<box>`). The relay keys every session with the box's PSK (WireGuard's
/// `PresharedKey`, the post-quantum hardening of the X25519 handshake), remembered with
/// the key: the same feed again (a restart) changes nothing, a new PSK relists the box, and
/// a fed value that is not a key, or a blank one (lane D14), is said and the box is not listed. Said
/// "with its pre-shared key", never the key. `None`: listed as [`Relay::take`] always did.
pub fn take_with_psk(&mut self, boxes: &[(String, String, Ipv4Addr, Option<String>)]) -> Vec<String> {
let mut said = Vec::new();
for (name, key, overlay, psk_b64) in boxes {
let key = key.trim();
let psk_b64 = psk_b64.as_deref().map(str::trim).map(str::to_string);
// A key that was fed but is blank is refused (lane D14, decision c1): never read as
// "no PSK", which would list the box without one, a silent downgrade.
if psk_b64.as_deref().is_some_and(str::is_empty) {
said.push(format!("`{name}` was fed a blank pre-shared key — not listed (a link without its key is refused)"));
continue;
}
let memo = (key.to_string(), psk_b64.clone());
// Listed once per (address, key, PSK): a restart re-feeds the same.
if key.is_empty() || self.at.get(overlay) == Some(&memo) || (self.listed.contains(key) && wg_appliance_core::decode_key(key).is_err()) {
continue;
}
let psk = match psk_b64.as_deref().map(wg_appliance_core::decode_key) {
None => None,
Some(Ok(p)) => Some(p),
Some(Err(_)) => {
said.push(format!("`{name}` was fed a value that is not a WireGuard pre-shared key — not listed with it"));
continue;
}
};
let with = if psk.is_some() { " with its pre-shared key" } else { "" };
match wg_appliance_core::decode_key(key) {
// A box laid again at its address (the dead main re-admitted,
// nordisk-pairing `installer::readmit`) brings a NEW key to the SAME
// overlay address: relisted there, the old key refused from then on.
// Under its fed NAME (rickard-81; Gunnar lists with `add_named_peer_with_psk`):
// boringtun's timer lines say `relay:main:<fp>`, never the address.
Ok(k) => match self.hub.relist_named_peer_with_psk(k, psk, *overlay, name) {
Ok(true) if self.at.get(overlay).is_some_and(|(old, _)| old != key) => said.push(format!("`{name}` relisted on the relay at {overlay} with its new key{with} (the old key is refused)")),
Ok(true) if self.at.contains_key(overlay) => said.push(format!("`{name}` takes a new pre-shared key on the relay at {overlay}")),
Ok(_) => said.push(format!("`{name}` listed on the relay as {overlay}{with}")),
Err(e) => said.push(format!("`{name}` was NOT listed on the relay: {e}")),
},
Err(_) => said.push(format!("`{name}` was fed a tunnel key that is not a WireGuard key — not listed")),
}
self.listed.insert(key.to_string());
self.at.insert(*overlay, memo);
self.names.insert(*overlay, name.clone());
}
let named: Vec<Ipv4Addr> = boxes.iter().filter(|(_, k, _, _)| !k.trim().is_empty()).map(|(_, _, o, _)| *o).collect();
said.extend(self.retain(&named));
said
}
/// **Forget every listed box whose overlay address is not in `named`** (A7 row 27,
/// Gunnar a1fa525f): a box the installer's re-feed no longer names with a key DIED and is
/// being laid again. Its session is forgotten now (wg-appliance-core
/// `HubRelay::forget_peer`), not after two minutes of handshakes toward a machine that is
/// gone; its new key lists it again ([`Relay::take_with_psk`]). [`Relay::take_with_psk`]
/// calls this with the whole fed list, so a caller hands it the WHOLE feed each time.
/// Answers what was forgotten, to be said.
pub fn retain(&mut self, named: &[Ipv4Addr]) -> Vec<String> {
let gone: Vec<Ipv4Addr> = self.at.keys().filter(|o| !named.contains(o)).copied().collect();
let mut said = Vec::new();
for overlay in gone {
if let Some((key, _)) = self.at.remove(&overlay) {
self.listed.remove(&key);
}
let name = self.names.remove(&overlay).unwrap_or_else(|| overlay.to_string());
if self.hub.forget_peer(overlay) {
said.push(format!("`{name}` forgotten on the relay at {overlay}: the installer's feed no longer names it with a key (the box is being laid again)"));
}
}
said
}
/// Each listed slot's address and its relay label (`relay:<name>:<fp>`, the name
/// boringtun's timer lines carry).
pub fn labels(&self) -> Vec<(Ipv4Addr, String)> {
self.hub.labels()
}
/// The overlay addresses listed with a pre-shared key.
pub fn with_psk(&self) -> Vec<Ipv4Addr> {
self.hub.verdict().with_psk
}
/// **List the fed boxes** — `(name, tunnel key, overlay)`, each key once (a
/// restart re-feeds; a box listed before is there either way). Answers what
/// was done, to be said. [`Relay::take_with_psk`] with no PSK.
pub fn take(&mut self, boxes: &[(String, String, Ipv4Addr)]) -> Vec<String> {
let plain: Vec<_> = boxes.iter().map(|(n, k, o)| (n.clone(), k.clone(), *o, None)).collect();
self.take_with_psk(&plain)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The relay runs on the front's own key and lists a fed box once; a box
/// without a key, or with one that is not WireGuard's, is not listed.
#[test]
fn the_relay_runs_on_the_fronts_own_key_and_lists_each_fed_box_once() {
let private = [7u8; 32];
let public = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&private));
let mut r = Relay::start(0, private, Ipv4Addr::new(10, 44, 0, 1)).unwrap();
assert_eq!(r.public(), public);
assert!(key_from_b64(&public).is_ok());
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
let boxes = vec![("vm2".to_string(), k, Ipv4Addr::new(10, 44, 0, 2)), ("vm3".to_string(), String::new(), Ipv4Addr::new(10, 44, 0, 3)), ("vm4".to_string(), "nope".to_string(), Ipv4Addr::new(10, 44, 0, 4))];
let said = r.take(&boxes);
assert_eq!(said[0], "`vm2` listed on the relay as 10.44.0.2");
assert!(said[1].contains("`vm4` was fed a tunnel key that is not a WireGuard key"), "{said:?}");
assert_eq!(said.len(), 2, "no key, not listed: {said:?}");
assert!(r.take(&boxes).is_empty(), "listed once");
}
/// ★ **A box laid again at its address brings a new key, and the relay takes
/// it** (the dead main re-admitted into its closed estate, nordisk-pairing
/// `installer::readmit`: same overlay address, a fresh tunnel key). The relay
/// refused a second key at one address (`add_peer`) and still SAID it listed
/// it, so the new main was never reached through vm1. Now it is relisted there
/// (`HubRelay::relist_peer`: the old key refused from then on), said so, once.
#[test]
fn a_box_laid_again_at_its_address_is_relisted_with_its_new_key() {
let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let old = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
let new = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[9u8; 32]));
let at = Ipv4Addr::new(10, 44, 0, 2);
assert_eq!(r.take(&[("vm2".to_string(), old.clone(), at)]), vec!["`vm2` listed on the relay as 10.44.0.2".to_string()]);
let said = r.take(&[("vm2".to_string(), new.clone(), at)]);
assert_eq!(said, vec!["`vm2` relisted on the relay at 10.44.0.2 with its new key (the old key is refused)".to_string()], "{said:?}");
assert!(r.take(&[("vm2".to_string(), new.clone(), at)]).is_empty(), "relisted once");
// The listed key offered at ANOTHER address is refused by name (one box, one
// address), never said as listed.
// (The whole feed: vm2 still named, so it is not forgotten.)
let back = r.take(&[("vm2".to_string(), new.clone(), at), ("vm9".to_string(), new, Ipv4Addr::new(10, 44, 0, 9))]);
assert!(back.len() == 1 && back[0].contains("NOT listed"), "{back:?}");
}
/// ★ **The relay says its counters per box and what it refused** (lane D12): an
/// initiation sealed to ANOTHER front key is dropped without a word, so the counters
/// line must show it as refused; a listed box's own handshake shows as its handshake.
#[test]
fn the_relay_counts_handshakes_per_box_and_refuses_a_wrong_front_key_visibly() {
let front = [7u8; 32];
let mut r = Relay::start(0, front, Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let boxk = [9u8; 32];
let box_pub = wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&boxk));
r.take(&[("main".to_string(), box_pub, Ipv4Addr::new(10, 44, 0, 2))]);
let at: std::net::SocketAddrV4 = format!("127.0.0.1:{}", r.port()).parse().unwrap();
// A box that seals to a WRONG front key: refused.
let wrong = wg_appliance_core::keys::public_from_private_bytes(&[5u8; 32]);
let mut t = boringtun::noise::Tunn::new(boringtun::x25519::StaticSecret::from(boxk), boringtun::x25519::PublicKey::from(wrong), None, None, 1, None);
let sock = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let mut buf = vec![0u8; 2048];
if let boringtun::noise::TunnResult::WriteToNetwork(p) = t.format_handshake_initiation(&mut buf, false) {
sock.send_to(p, at).unwrap();
}
// The right key: a handshake.
let mut ok = boringtun::noise::Tunn::new(boringtun::x25519::StaticSecret::from(boxk), boringtun::x25519::PublicKey::from(wg_appliance_core::keys::public_from_private_bytes(&front)), None, None, 2, None);
if let boringtun::noise::TunnResult::WriteToNetwork(p) = ok.format_handshake_initiation(&mut buf, false) {
sock.send_to(p, at).unwrap();
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut line = r.counters();
while std::time::Instant::now() < deadline && !(line.contains("handshakes 1") && line.contains("refused 1")) {
std::thread::sleep(std::time::Duration::from_millis(50));
line = r.counters();
}
assert!(line.contains("10.44.0.2") && line.contains("handshakes 1"), "the listed box's handshake: {line}");
assert!(line.contains("refused 1"), "the wrong-key initiation is counted as refused: {line}");
}
/// **The front's own overlay interface rides the relay** (a WireGuard-only estate's
/// front dials its boxes over the tunnel): the relay starts with the hub port
/// attached, on the front's own key, and lists boxes as before.
#[test]
fn the_relay_starts_with_the_fronts_own_overlay_interface_attached() {
let port: std::sync::Arc<dyn wg_appliance_core::hubrelay::HubPort> = wg_appliance_core::hubrelay::MemPort::new();
let mut r = Relay::start_with_hub(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1), Some(port)).unwrap();
assert_ne!(r.port(), 0);
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
assert_eq!(r.take(&[("main".to_string(), k, Ipv4Addr::new(10, 44, 0, 2))]), vec!["`main` listed on the relay as 10.44.0.2".to_string()]);
}
/// ★ **A7 row 27 (Gunnar a1fa525f): a box the re-feed no longer names with a key is
/// forgotten on the relay at once, and its new key lists it again.** The main died; the
/// installer re-fed the front with the twin only (the new main has no key until it pairs).
/// The relay kept the dead main's session and sealed the twin's every packet to it
/// (`HANDSHAKE(REKEY_TIMEOUT)` every 5 s, then `ERROR CONNECTION_EXPIRED`, measured 2 min).
#[test]
fn a_box_the_refeed_no_longer_names_is_forgotten_and_its_new_key_lists_it_again() {
let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let key = |seed: u8| wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[seed; 32]));
let (main_at, twin_at) = (Ipv4Addr::new(10, 44, 0, 2), Ipv4Addr::new(10, 44, 0, 3));
let twin = ("twin".to_string(), key(3), twin_at);
assert_eq!(r.take(&[("main".to_string(), key(2), main_at), twin.clone()]).len(), 2);
// The re-feed after main died: main with no key (being laid again), or not named at all.
let said = r.take(&[("main".to_string(), String::new(), main_at), twin.clone()]);
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("forgotten") && l.contains("10.44.0.2")), "{said:?}");
assert!(r.take(std::slice::from_ref(&twin)).is_empty(), "forgotten once");
// The new main's key lists it again, at the same slot.
let said = r.take(&[("main".to_string(), key(4), main_at), twin.clone()]);
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("10.44.0.2") && !l.contains("forgotten")), "{said:?}");
// The same key coming back after a forget is listed again too (a feed that dropped it).
r.take(std::slice::from_ref(&twin));
let said = r.take(&[("main".to_string(), key(4), main_at), twin]);
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("10.44.0.2")), "{said:?}");
}
/// ★ **Each fed box is listed under its name** (lane G9, rickard-81; Gunnar `FrontRole::take`
/// lists with `add_named_peer_with_psk`): boringtun's timer lines for a relay peer say
/// `wg{peer=relay:main:<fp>}`, never the address. A box laid again keeps the fed name.
#[test]
fn each_fed_box_is_listed_on_the_relay_under_its_name() {
let mut r = Relay::start(0, [7u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let key = |seed: u8| wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[seed; 32]));
let (main_at, twin_at) = (Ipv4Addr::new(10, 44, 0, 2), Ipv4Addr::new(10, 44, 0, 3));
r.take_with_psk(&[("main".to_string(), key(2), main_at, Some(PSK.to_string())), ("twin".to_string(), key(3), twin_at, None)]);
let label = |r: &Relay, at: Ipv4Addr| r.labels().into_iter().find(|(o, _)| *o == at).map(|(_, l)| l).unwrap_or_default();
assert!(label(&r, main_at).starts_with("relay:main:"), "{:?}", r.labels());
assert!(label(&r, twin_at).starts_with("relay:twin:"), "{:?}", r.labels());
// Laid again: forgotten, then its new key at the same address, still `main`.
r.take(&[("twin".to_string(), key(3), twin_at)]);
r.take(&[("main".to_string(), key(4), main_at), ("twin".to_string(), key(3), twin_at)]);
assert!(label(&r, main_at).starts_with("relay:main:"), "{:?}", r.labels());
}
const PSK: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
const PSK2: &str = "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=";
/// ★ **Each box is listed with the pre-shared key the installer fed the front for it**
/// (lane G5, A7 row 13; Gunnar 356e8dd2 `each_fed_box_is_listed_with_its_preshared_key_and_a_new_one_relists_it`):
/// said so, never the key. The same feed again changes nothing; a new PSK for the same box
/// relists it; a fed value that is not a key is said and the box is not listed with it.
#[test]
fn each_fed_box_is_listed_with_its_preshared_key_and_a_new_one_relists_it() {
let mut r = Relay::start(0, [6u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
let at = Ipv4Addr::new(10, 44, 0, 2);
let fed = |psk: Option<&str>| vec![("main".to_string(), k.clone(), at, psk.map(str::to_string))];
let said = r.take_with_psk(&fed(Some(PSK)));
assert_eq!(said, vec!["`main` listed on the relay as 10.44.0.2 with its pre-shared key".to_string()]);
assert!(!said.concat().contains(PSK));
assert_eq!(r.with_psk(), vec![at]);
assert!(r.take_with_psk(&fed(Some(PSK))).is_empty(), "the same feed: nothing to do");
let said = r.take_with_psk(&fed(Some(PSK2)));
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("pre-shared key")) && !said.concat().contains(PSK2), "{said:?}");
assert_eq!(r.with_psk(), vec![at]);
let said = r.take_with_psk(&fed(Some("not-a-key")));
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("not a WireGuard pre-shared key")) && !said.concat().contains("not-a-key"), "{said:?}");
// A box fed no PSK is listed as before.
// (The whole feed: main is still named with its last good PSK, so it is not forgotten.)
let mut whole = fed(Some(PSK2));
whole.push(("twin".to_string(), wg_appliance_core::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&[9u8; 32])), Ipv4Addr::new(10, 44, 0, 3), None));
let plain = r.take_with_psk(&whole);
assert_eq!(plain, vec!["`twin` listed on the relay as 10.44.0.3".to_string()]);
assert_eq!(r.with_psk(), vec![at], "the twin carries none");
}
/// ★ **A blank fed pre-shared key REFUSES the box** (lane D14, decision c1): a `wg_psk.<box>`
/// that was fed but is empty or whitespace is never read as "no PSK" (a silent downgrade
/// on a security gateway). The box is not listed and that is said; a box fed NO key at all
/// (an estate laid before PSKs) is still listed as before.
#[test]
fn a_blank_fed_preshared_key_refuses_the_box_instead_of_listing_it_without_one() {
let mut r = Relay::start(0, [6u8; 32], Ipv4Addr::new(10, 44, 0, 1)).unwrap();
let k = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".to_string();
for blank in ["", " "] {
let said = r.take_with_psk(&[("main".to_string(), k.clone(), Ipv4Addr::new(10, 44, 0, 2), Some(blank.to_string()))]);
assert!(said.iter().any(|l| l.contains("`main`") && l.contains("blank pre-shared key") && l.contains("not listed")), "{said:?}");
assert!(r.hub.verdict().peers.is_empty(), "nothing listed for a blank key: {said:?}");
}
}
}