Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/src/sync/downstreams.rs

//! **How each downstream reaches this Holger, enrolled, invited and revoked** (lane
//! HOL-WGDS, owner 2026-10-27).
//!
//! A downstream is a `sync.downstreams` entry (the RON) or a RECORD the console added
//! (`<data_root>/holger-sync/downstreams/<name>.ron`, the same RON shape, written once):
//!
//! ```ron
//! (name: "lab-1", key: "<its ack key, hex>", via: Front),
//! (name: "lab-2", key: "<its ack key, hex>", via: Wireguard(key: "<its WireGuard public key>")),
//! ```
//!
//! `via` defaults to `Front`: the downstream syncs through the front's public door. A
//! `Wireguard` downstream reaches the front over the front's own downstream interface
//! (holger-downstream `via::HUB_IFACE`), and through it only main's sync door. It enrols
//! ONCE with a signed invite ([`SyncHub::invite`]: its address there, the front's key and
//! endpoint, the door), and the front lists exactly the peers [`SyncHub::wg_peers`] names.
//!
//! **Revocation** ([`SyncHub::revoke_downstream`]) is audited first, then kept as a record
//! (`revoked/<name>.ron`, with the keys it revokes): from then on the downstream takes no
//! manifest and gives no ack, and the front drops its WireGuard peer at its next look. A
//! revoked RON entry stays revoked while the RON names the same keys; new keys are a new
//! enrolment.

use std::net::Ipv4Addr;

use holger_downstream_rules::via::{self, DownstreamDecl, Via};
use holger_errcode::SYNC_DOWNSTREAM_REFUSED;

use super::*;

/// The console's records, under [`STATE_DIR`].
pub const RECORDS_DIR: &str = "downstreams";
/// The revoked downstreams, under [`RECORDS_DIR`].
pub const REVOKED_DIR: &str = "revoked";
/// The WireGuard enrolments (each downstream's address on the front's interface), under
/// [`RECORDS_DIR`].
pub const WG_DIR: &str = "wg";
/// How long an invite may be taken.
pub const INVITE_TTL_SECS: i64 = 7 * 24 * 60 * 60;
/// **The front's downstream interface, as the front offered it** (`offer.<front>.downstream_hub_key`
/// and `downstream_hub_endpoint`), under [`STATE_DIR`]: an estate box's pairing keeps it
/// ([`keep_front_hub`]), and an invite asked without them takes them from here.
pub const FRONT_HUB_FILE: &str = "front-hub.ron";
/// The offer keys the front says its downstream interface under.
pub const OFFER_HUB_KEY: &str = "downstream_hub_key";
pub const OFFER_HUB_ENDPOINT: &str = "downstream_hub_endpoint";

/// The front's downstream interface: its WireGuard key and its UDP endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontHub {
    pub key: String,
    pub endpoint: String,
}

/// **Keep the front's downstream interface** under `data_root` (only when it changed).
/// `Ok(false)`: nothing to keep (the front offered none, or the same).
pub fn keep_front_hub(data_root: &Path, key: &str, endpoint: &str) -> Result<bool, String> {
    via::wg_key(key)
        .map_err(|why| format!("the front's downstream key is not a WireGuard key: {why}"))?;
    endpoint
        .trim()
        .parse::<std::net::SocketAddrV4>()
        .map_err(|_| format!("the front's downstream endpoint {endpoint:?} is not `ip:port`"))?;
    let path = data_root.join(STATE_DIR).join(FRONT_HUB_FILE);
    let hub = FrontHub {
        key: key.trim().to_string(),
        endpoint: endpoint.trim().to_string(),
    };
    if std::fs::read_to_string(&path)
        .ok()
        .and_then(|t| ron::from_str::<FrontHub>(&t).ok())
        .as_ref()
        == Some(&hub)
    {
        return Ok(false);
    }
    write_ron(&path, &hub, false).map_err(|e| e.tagged())?;
    Ok(true)
}

/// A revoked downstream: the keys it held when it was revoked.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Revoked {
    pub name: String,
    pub key: String,
    pub via: Via,
    pub revoked_at: i64,
    pub revoked_by: String,
}

/// **An invite the doors hand out** ([`SyncHub::invite_for`]): the signed document, and what
/// it says for the admin who carries it (no pre-shared key: that is inside the signed bytes).
#[derive(Debug, Clone)]
pub struct IssuedInvite {
    pub signed: Signed,
    /// `holger.downstream-invite/v1` (direct) or `/v2` (WireGuard, with the pre-shared key).
    pub schema: String,
    /// The door the downstream reaches its upstream at.
    pub door: String,
    /// A WireGuard downstream's address on the front's interface.
    pub address: Option<String>,
    /// The upstream's name and sync key: what the downstream's RON trusts.
    pub upstream: String,
    pub key: String,
}

/// A WireGuard downstream's enrolment: its address on the front's downstream interface.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WgEnrolment {
    pub name: String,
    /// Its WireGuard public key (base64).
    pub key: String,
    pub address: Ipv4Addr,
    pub invited_at: i64,
    pub invited_by: String,
    /// The link's WireGuard pre-shared key (lane WG-PSK), minted with the enrolment: a new
    /// enrolment (a first invite, or a new WireGuard key) gets a new one. `None` in a record
    /// from before the lane: that link runs without one until the downstream enrols again.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psk: Option<holger_sync::invite::WgPsk>,
}

/// **One peer as the front's relay lists it** (`GET /-/sync/downstreams/wg`, the front's
/// console principal only): [`WgPeer`] and the link's pre-shared key. The console's own lists
/// carry [`WgPeer`], never the key.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelayPeer {
    pub name: String,
    pub key: String,
    pub address: Ipv4Addr,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psk: Option<holger_sync::invite::WgPsk>,
}

/// One peer the front lists on its downstream interface.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WgPeer {
    pub name: String,
    pub key: String,
    pub address: Ipv4Addr,
}

fn refused(status: u16, why: impl Into<String>) -> SyncFailure {
    SyncFailure::new(&SYNC_DOWNSTREAM_REFUSED, status, why)
}

fn io(e: impl std::fmt::Display) -> SyncFailure {
    SyncError::Io(e.to_string()).into()
}

pub(super) fn read_ron_dir<T: serde::de::DeserializeOwned>(
    dir: &Path,
) -> Result<Vec<T>, SyncFailure> {
    let mut out = Vec::new();
    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(out),
        Err(e) => return Err(io(format!("read {}: {e}", dir.display()))),
    };
    let mut paths: Vec<PathBuf> = entries
        .flatten()
        .map(|e| e.path())
        .filter(|p| p.extension().is_some_and(|x| x == "ron"))
        .collect();
    paths.sort();
    for p in paths {
        let text =
            std::fs::read_to_string(&p).map_err(|e| io(format!("read {}: {e}", p.display())))?;
        // An unreadable record is a refusal by name, never a silent skip: a downstream
        // that quietly vanished (or a revocation that did) is the failure.
        out.push(ron::from_str(&text).map_err(|e| {
            refused(
                500,
                format!("the record {} is not readable: {e}", p.display()),
            )
        })?);
    }
    Ok(out)
}

pub(super) fn write_ron<T: Serialize>(path: &Path, v: &T, once: bool) -> Result<(), SyncFailure> {
    let text = ron::ser::to_string_pretty(v, ron::ser::PrettyConfig::default()).map_err(io)?;
    if let Some(dir) = path.parent() {
        std::fs::create_dir_all(dir).map_err(|e| io(format!("create {}: {e}", dir.display())))?;
    }
    if once {
        use std::io::Write as _;
        let mut f = std::fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(path)
            .map_err(|e| match e.kind() {
                std::io::ErrorKind::AlreadyExists => refused(
                    409,
                    format!("{} exists: a downstream is enrolled once", path.display()),
                ),
                _ => io(format!("write {}: {e}", path.display())),
            })?;
        f.write_all(text.as_bytes())
            .and_then(|()| f.sync_all())
            .map_err(|e| io(format!("write {}: {e}", path.display())))
    } else {
        let tmp = path.with_extension(format!("tmp-{}", std::process::id()));
        std::fs::write(&tmp, text)
            .and_then(|()| std::fs::rename(&tmp, path))
            .map_err(|e| io(format!("write {}: {e}", path.display())))
    }
}

fn wg_key_of(v: &Via) -> Option<&str> {
    match v {
        Via::Wireguard { key } => Some(key.as_str()),
        Via::Front => None,
    }
}

/// Pure: **is `peer` revoked by `r`?** The same name AND the same keys: an entry with new
/// keys is a new enrolment.
pub fn is_revoked_by(peer: &SyncPeer, r: &Revoked) -> bool {
    peer.name == r.name
        && peer.key.trim().eq_ignore_ascii_case(r.key.trim())
        && wg_key_of(&peer.via).map(str::trim) == wg_key_of(&r.via).map(str::trim)
}

/// Pure: **the lowest free address** on the downstreams' subnet (`.2` up, `.1` is the front).
pub fn free_address(taken: &[Ipv4Addr]) -> Option<Ipv4Addr> {
    let (net, _) = via::HUB_NET;
    let base = u32::from(net);
    (2u32..=254)
        .map(|n| Ipv4Addr::from(base + n))
        .find(|a| !taken.contains(a) && *a != via::HUB_ADDR)
}

/// Pure: **the `downstreams` as the via rule sees them** (`holger-downstream-5`).
pub fn decls(peers: &[SyncPeer]) -> Vec<DownstreamDecl> {
    peers
        .iter()
        .map(|p| DownstreamDecl {
            name: p.name.clone(),
            via: p.via.clone(),
        })
        .collect()
}

/// Pure: **the console's enrolment form as one RON entry** (lane OX-WGDS2): its `name`, its
/// ack `key`, `via` (`front`, the default, or `wireguard`) and a `wireguard` one's
/// `wireguard_key`. Checked by [`SyncHub::add_downstream`] like the RON; here only the shape.
pub fn peer_from_form(
    name: &str,
    key: &str,
    via: &str,
    wireguard_key: &str,
) -> Result<SyncPeer, SyncFailure> {
    let via = match via.trim() {
        "" | "front" => Via::Front,
        "wireguard" => Via::Wireguard {
            key: wireguard_key.trim().to_string(),
        },
        other => {
            return Err(refused(
                400,
                format!(
                    "`via={other}`: a downstream reaches this Holger via `front` or `wireguard`"
                ),
            ))
        }
    };
    Ok(SyncPeer {
        name: name.trim().to_string(),
        key: key.trim().to_string(),
        url: None,
        token_file: None,
        via,
        invite: None,
    })
}

impl SyncHub {
    fn records_dir(&self) -> PathBuf {
        self.dir.join(RECORDS_DIR)
    }

    /// The revoked downstreams.
    pub fn revoked(&self) -> Result<Vec<Revoked>, SyncFailure> {
        read_ron_dir(&self.records_dir().join(REVOKED_DIR))
    }

    /// ★ **The live downstreams**: the RON's and the console's records, without the revoked.
    pub fn downstreams(&self) -> Result<Vec<SyncPeer>, SyncFailure> {
        let mut all = self.cfg.downstreams.clone();
        for r in read_ron_dir::<SyncPeer>(&self.records_dir())? {
            if all.iter().any(|p| p.name == r.name) {
                return Err(refused(500, format!("downstream '{}' is declared in the RON and recorded by the console: one name, one downstream", r.name)));
            }
            all.push(r);
        }
        let revoked = self.revoked()?;
        all.retain(|p| !revoked.iter().any(|r| is_revoked_by(p, r)));
        Ok(all)
    }

    /// The `via` of downstream `name` (live), else `Front`.
    pub fn via_of(&self, name: &str) -> Via {
        self.downstreams()
            .ok()
            .and_then(|d| d.into_iter().find(|p| p.name == name))
            .map(|p| p.via)
            .unwrap_or_default()
    }

    /// ★ **Enrol a downstream from the console**: one RON record, checked like the RON
    /// (`holger-sync-11` / `holger-downstream-5` by name), audited first.
    pub fn add_downstream(
        &self,
        peer: SyncPeer,
        actor: &str,
        source: &str,
    ) -> Result<SyncPeer, SyncFailure> {
        if !is_key_hex(&peer.key) {
            return Err(refused(
                400,
                format!(
                    "downstream '{}': `key` must be its Ed25519 ack key as 64 hex digits",
                    peer.name
                ),
            ));
        }
        if peer.url.is_some() || peer.token_file.is_some() || peer.invite.is_some() {
            return Err(refused(
                400,
                format!(
                    "downstream '{}': `url` and `token_file` belong to an upstream",
                    peer.name
                ),
            ));
        }
        let _one = self.serial.lock_or_recover();
        let mut all = self.downstreams()?;
        all.push(peer.clone());
        via::validate(&decls(&all)).map_err(|e| refused(400, e))?;
        let path = self.records_dir().join(format!("{}.ron", peer.name));
        // A record its revocation left behind is no enrolment: the twin's follow never
        // deletes, so a main rebuilt from its twin may hold one (lane OX-WGDS2).
        if let Some(old) = std::fs::read_to_string(&path)
            .ok()
            .and_then(|t| ron::from_str::<SyncPeer>(&t).ok())
        {
            if self.revoked()?.iter().any(|r| is_revoked_by(&old, r)) {
                std::fs::remove_file(&path)
                    .map_err(|e| io(format!("remove {}: {e}", path.display())))?;
            }
        }
        self.record(self.event(
            actor,
            &peer.name,
            source,
            format!(
                "sync downstream '{}' enrolled via {}",
                peer.name,
                peer.via.word()
            ),
        ))?;
        write_ron(&path, &peer, true)?;
        Ok(peer)
    }

    /// ★ **Revoke a downstream** (the console's verb): audited first, then kept. From now
    /// on it takes no manifest, its ack is refused, and the front drops its WireGuard peer.
    pub fn revoke_downstream(
        &self,
        name: &str,
        actor: &str,
        source: &str,
    ) -> Result<Revoked, SyncFailure> {
        let _one = self.serial.lock_or_recover();
        let Some(peer) = self.downstreams()?.into_iter().find(|p| p.name == name) else {
            return Err(refused(404, format!("'{name}' is not a live downstream of this Holger (never enrolled, or revoked already)")));
        };
        let now = holger_sync::now_secs();
        let r = Revoked {
            name: peer.name.clone(),
            key: peer.key.clone(),
            via: peer.via.clone(),
            revoked_at: now,
            revoked_by: actor.to_string(),
        };
        self.record(self.event(actor, name, source, format!("sync downstream '{name}' REVOKED (via {}): it takes no manifest and gives no ack from now on", peer.via.word())))?;
        write_ron(
            &self
                .records_dir()
                .join(REVOKED_DIR)
                .join(format!("{name}.ron")),
            &r,
            false,
        )?;
        // A console record goes with it; the WireGuard address is free again.
        let _ = std::fs::remove_file(self.records_dir().join(format!("{name}.ron")));
        let _ = std::fs::remove_file(self.records_dir().join(WG_DIR).join(format!("{name}.ron")));
        Ok(r)
    }

    fn enrolments(&self) -> Result<Vec<WgEnrolment>, SyncFailure> {
        read_ron_dir(&self.records_dir().join(WG_DIR))
    }

    /// ★ **The peers the front lists on its downstream interface**: every live
    /// `Wireguard` downstream that was invited, at its address, under the key it was
    /// invited for (a declaration whose key changed is not listed until it is invited again).
    pub fn wg_peers(&self) -> Result<Vec<WgPeer>, SyncFailure> {
        Ok(self
            .live_enrolments()?
            .into_iter()
            .map(|e| WgPeer {
                name: e.name,
                key: e.key,
                address: e.address,
            })
            .collect())
    }

    /// The enrolments [`SyncHub::wg_peers`] lists: invited, live, under the key invited for.
    fn live_enrolments(&self) -> Result<Vec<WgEnrolment>, SyncFailure> {
        let live = self.downstreams()?;
        Ok(self
            .enrolments()?
            .into_iter()
            .filter(|e| {
                live.iter().any(|p| {
                    p.name == e.name && wg_key_of(&p.via).map(str::trim) == Some(e.key.trim())
                })
            })
            .collect())
    }

    /// ★ **[`SyncHub::wg_peers`] as the front's relay lists them**: each with its link's
    /// pre-shared key.
    pub fn wg_relay_peers(&self) -> Result<Vec<RelayPeer>, SyncFailure> {
        Ok(self
            .live_enrolments()?
            .into_iter()
            .map(|e| RelayPeer {
                name: e.name,
                key: e.key,
                address: e.address,
                psk: e.psk,
            })
            .collect())
    }

    /// ★ **The signed invite downstream `name` is handed, whichever way it syncs**
    /// (HOL-PRODUCTS-7, the doors' one act): a `via: Front` downstream's direct invite
    /// ([`SyncHub::invite_direct`]: schema v1, its `door`), a `via: Wireguard` one's
    /// ([`SyncHub::invite`]: schema v2, the link's pre-shared key in it, the front's
    /// `hub_key` and `endpoint`). Returns the signed bytes' document and what it says (the
    /// schema, the door, the WireGuard address), never the pre-shared key.
    pub fn invite_for(
        &self,
        name: &str,
        door: &str,
        hub_key: &str,
        endpoint: &str,
        actor: &str,
        source: &str,
    ) -> Result<IssuedInvite, SyncFailure> {
        let via = self
            .downstreams()?
            .into_iter()
            .find(|p| p.name == name)
            .map(|p| p.via);
        // A name it does not know is refused by the act its arguments ask for.
        let wireguard = match via {
            Some(v) => v != Via::Front,
            None => !hub_key.trim().is_empty() || !endpoint.trim().is_empty(),
        };
        let (signed, address) = match wireguard {
            true => {
                let (s, e) = self.invite(name, hub_key, endpoint, actor, source)?;
                (s, Some(e.address.to_string()))
            }
            false => (self.invite_direct(name, door, actor, source)?, None),
        };
        let trusted = [holger_sync::TrustedPeer {
            name: self.cfg.name.clone(),
            key: self.public_key(),
        }];
        let said: holger_sync::invite::DownstreamInvite =
            signed.open(holger_sync::Kind::Invite, &trusted)?;
        Ok(IssuedInvite {
            schema: said.schema,
            door: said.door,
            address,
            upstream: self.cfg.name.clone(),
            key: self.public_key(),
            signed,
        })
    }

    /// ★ **The signed direct invite of `Front` downstream `name`** (HOL-PRODUCTS-7): a plain
    /// server's invite, in the estate's format (schema v1: no tunnel, so no pre-shared key),
    /// signed with this Holger's sync key, for the downstream's sync key, its `door` the given
    /// one or this Holger's own ([`SyncHub::with_door`]). The issuing is audited. A WireGuard
    /// downstream's invite is [`SyncHub::invite`]'s (it needs the front's interface).
    pub fn invite_direct(
        &self,
        name: &str,
        door: &str,
        actor: &str,
        source: &str,
    ) -> Result<Signed, SyncFailure> {
        let _one = self.serial.lock_or_recover();
        let Some(peer) = self.downstreams()?.into_iter().find(|p| p.name == name) else {
            return Err(refused(
                404,
                format!("'{name}' is not a live downstream of this Holger"),
            ));
        };
        if peer.via != Via::Front {
            return Err(refused(400, format!("downstream '{name}' syncs `via: Wireguard(..)`: its invite names the front's interface (the estate's `/-/sync/downstreams/invite` with the front's hub)")));
        }
        let door = if door.trim().is_empty() {
            self.door().unwrap_or("").to_string()
        } else {
            door.trim().to_string()
        };
        if !(door.starts_with("http://") || door.starts_with("https://"))
            || door.len() < "http://x".len()
        {
            return Err(refused(400, format!("the invite's door {door:?} is no `http://host:port` or `https://host:port`: name it with `door`")));
        }
        let now = holger_sync::now_secs();
        let invite = holger_sync::invite::DownstreamInvite::direct(
            &self.cfg.name,
            name,
            &peer.key,
            &door,
            now,
            now + INVITE_TTL_SECS,
        );
        let signed = self.key().sign_invite(&invite)?;
        self.record(self.event(
            actor,
            name,
            source,
            format!("sync downstream '{name}' invited directly: its door {door}"),
        ))?;
        Ok(signed)
    }

    /// ★ **The signed invite of `Wireguard` downstream `name`**: its address on the front's
    /// downstream interface (allocated once, kept), the front's `hub_key` and `endpoint`, the
    /// door; signed by this Holger's sync key, which the downstream trusts already.
    pub fn invite(
        &self,
        name: &str,
        hub_key: &str,
        endpoint: &str,
        actor: &str,
        source: &str,
    ) -> Result<(Signed, WgEnrolment), SyncFailure> {
        // Not given: the front's own, as its offer was kept ([`keep_front_hub`]).
        let kept = if hub_key.trim().is_empty() || endpoint.trim().is_empty() {
            let path = self.dir.join(FRONT_HUB_FILE);
            let text = std::fs::read_to_string(&path).map_err(|_| refused(409, format!("no front has offered its downstream interface yet ({} is absent): the invite names `hub_key` and `endpoint`, or waits for the front", path.display())))?;
            Some(
                ron::from_str::<FrontHub>(&text).map_err(|e| {
                    refused(500, format!("{} is not readable: {e}", path.display()))
                })?,
            )
        } else {
            None
        };
        // Each one not given is the kept one's: an endpoint the admin names (a downstream behind
        // a relay or a NAT, lane OX-WGDS3) wins over the front's own address.
        let pick = |given: &'_ str, kept: Option<&'_ str>| {
            if given.trim().is_empty() {
                kept.unwrap_or("").to_string()
            } else {
                given.trim().to_string()
            }
        };
        let (hub_key, endpoint) = (
            pick(hub_key, kept.as_ref().map(|h| h.key.as_str())),
            pick(endpoint, kept.as_ref().map(|h| h.endpoint.as_str())),
        );
        let (hub_key, endpoint) = (hub_key.as_str(), endpoint.as_str());
        via::wg_key(hub_key).map_err(|why| {
            refused(
                400,
                format!("the front's downstream key is not a WireGuard key: {why}"),
            )
        })?;
        if endpoint.trim().parse::<std::net::SocketAddrV4>().is_err() {
            return Err(refused(
                400,
                format!("the front's downstream endpoint {endpoint:?} is not `ip:port`"),
            ));
        }
        let _one = self.serial.lock_or_recover();
        let Some(peer) = self.downstreams()?.into_iter().find(|p| p.name == name) else {
            return Err(refused(
                404,
                format!("'{name}' is not a live downstream of this Holger"),
            ));
        };
        let Via::Wireguard { key } = &peer.via else {
            return Err(refused(400, format!("downstream '{name}' syncs `via: Front`; an invite is for a `via: Wireguard(key: ..)` downstream")));
        };
        let now = holger_sync::now_secs();
        let mut all = self.enrolments()?;
        let enrolment = match all.iter().position(|e| e.name == name) {
            Some(i) if all[i].key.trim() == key.trim() => all[i].clone(),
            found => {
                if let Some(i) = found {
                    all.remove(i);
                }
                let taken: Vec<Ipv4Addr> = all.iter().map(|e| e.address).collect();
                let address = free_address(&taken)
                    .ok_or_else(|| refused(409, "the downstreams' subnet has no free address"))?;
                // A new enrolment (a first invite, or a new WireGuard key): a new link, a new
                // pre-shared key — it rotates with the peer's key, never derived from it.
                WgEnrolment {
                    name: name.to_string(),
                    key: key.clone(),
                    address,
                    invited_at: now,
                    invited_by: actor.to_string(),
                    psk: Some(holger_sync::invite::WgPsk::mint()?),
                }
            }
        };
        let invite = holger_sync::invite::DownstreamInvite {
            schema: holger_sync::invite::DownstreamInvite::schema_for(enrolment.psk.as_ref())
                .into(),
            upstream: self.cfg.name.clone(),
            downstream: name.to_string(),
            downstream_key: key.clone(),
            hub_key: hub_key.trim().to_string(),
            endpoint: endpoint.trim().to_string(),
            address: enrolment.address.to_string(),
            hub_address: via::HUB_ADDR.to_string(),
            door: format!("http://{}:{}", via::HUB_ADDR, via::DOOR_PORT),
            issued_at: now,
            expires_at: now + INVITE_TTL_SECS,
            psk: enrolment.psk.clone(),
        };
        let signed = self.key().sign_invite(&invite)?;
        self.record(self.event(
            actor,
            name,
            source,
            format!(
                "sync downstream '{name}' invited over WireGuard at {} (front {})",
                enrolment.address, invite.endpoint
            ),
        ))?;
        let record = self.records_dir().join(WG_DIR).join(format!("{name}.ron"));
        write_ron(&record, &enrolment, false)?;
        // The record holds the link's pre-shared key: readable by this Holger alone.
        {
            use std::os::unix::fs::PermissionsExt as _;
            std::fs::set_permissions(&record, std::fs::Permissions::from_mode(0o600))
                .map_err(|e| io(format!("chmod {}: {e}", record.display())))?;
        }
        Ok((signed, enrolment))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audit::MemoryAuditLog;

    const WG_A: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
    const WG_HUB: &str = "HyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4=";

    fn peer(name: &str, via: Via) -> SyncPeer {
        SyncPeer {
            name: name.into(),
            key: SyncKey::from_seed([name.len() as u8; 32]).public_hex(),
            url: None,
            token_file: None,
            via,
            invite: None,
        }
    }

    fn hub(dir: &Path, downstreams: Vec<SyncPeer>) -> (SyncHub, Arc<MemoryAuditLog>) {
        let audit = Arc::new(MemoryAuditLog::new());
        let cfg = SyncConfig {
            name: "cloud".into(),
            key: None,
            downstreams,
            upstreams: vec![],
            ack_deadline_secs: 60,
            tick_secs: 30,
        };
        (
            SyncHub::new(cfg, dir, audit.clone(), Weak::new()).unwrap(),
            audit,
        )
    }

    /// ★ The RON's `via` reads, defaults to `Front`, and a bad one is refused at load by name.
    #[test]
    fn the_sync_block_names_each_downstreams_via_and_a_bad_one_is_refused_at_load() {
        let ron = format!(
            r#"(name: "cloud", downstreams: [ (name: "lab-1", key: "{k}", via: Front), (name: "lab-2", key: "{k2}", via: Wireguard(key: "{WG_A}")), (name: "lab-3", key: "{k3}") ])"#,
            k = "a".repeat(64),
            k2 = "b".repeat(64),
            k3 = "c".repeat(64)
        );
        let cfg: SyncConfig = ron::from_str(&ron).unwrap();
        assert_eq!(
            cfg.downstreams
                .iter()
                .map(|p| p.via.word())
                .collect::<Vec<_>>(),
            ["front", "wireguard", "front"]
        );
        let d = tempfile::tempdir().unwrap();
        let audit: Arc<dyn AuditLog> = Arc::new(crate::audit::NoopAuditLog);
        assert!(SyncHub::new(cfg.clone(), d.path(), audit.clone(), Weak::new()).is_ok());
        let mut bad = cfg.clone();
        bad.downstreams[1].via = Via::Wireguard { key: "nope".into() };
        let e = SyncHub::new(bad, d.path(), audit.clone(), Weak::new())
            .err()
            .unwrap()
            .tagged();
        assert!(
            e.contains("holger-downstream-5") && e.contains("lab-2"),
            "{e}"
        );
        let mut up = cfg;
        up.upstreams = vec![SyncPeer {
            name: "u".into(),
            key: "d".repeat(64),
            url: None,
            token_file: None,
            via: Via::Wireguard { key: WG_A.into() },
            invite: None,
        }];
        let e = SyncHub::new(up, d.path(), audit, Weak::new())
            .err()
            .unwrap()
            .tagged();
        assert!(e.contains("upstream 'u'") && e.contains("invite"), "{e}");
        let e = ron::from_str::<SyncConfig>(
            r#"(name: "c", downstreams: [ (name: "x", key: "", via: Carrier) ])"#,
        )
        .unwrap_err()
        .to_string();
        assert!(e.contains("Carrier"), "{e}");
    }

    /// ★ A console-enrolled downstream is a record; a WireGuard one is invited once with a
    /// signed invite, listed for the front at its address, and a revocation (audited) cuts
    /// it off: no manifest, no ack, no WireGuard peer.
    #[test]
    fn a_wireguard_downstream_is_invited_listed_for_the_front_and_revoked() {
        let d = tempfile::tempdir().unwrap();
        let (h, audit) = hub(d.path(), vec![peer("lab-1", Via::Front)]);
        let lab2 = h
            .add_downstream(
                peer("lab-2", Via::Wireguard { key: WG_A.into() }),
                "console",
                "10.44.0.1",
            )
            .unwrap();
        assert!(d
            .path()
            .join(STATE_DIR)
            .join(RECORDS_DIR)
            .join("lab-2.ron")
            .exists());
        assert_eq!(
            h.downstreams()
                .unwrap()
                .iter()
                .map(|p| p.name.as_str())
                .collect::<Vec<_>>(),
            ["lab-1", "lab-2"]
        );
        assert!(h
            .add_downstream(peer("lab-2", Via::Front), "console", "x")
            .unwrap_err()
            .tagged()
            .contains("declared twice"));
        // No invite for a Front downstream; the WireGuard one gets its address and the door.
        assert!(h
            .invite("lab-1", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap_err()
            .tagged()
            .contains("via: Front"));
        let (signed, e) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert_eq!(e.address, Ipv4Addr::new(10, 46, 0, 2));
        let trusted = vec![TrustedPeer {
            name: "cloud".into(),
            key: h.public_key(),
        }];
        let inv = holger_sync::invite::open_invite(
            &signed.to_bytes(),
            &trusted,
            "lab-2",
            WG_A,
            holger_sync::now_secs(),
        )
        .unwrap();
        assert_eq!(
            (
                inv.door.as_str(),
                inv.hub_key.as_str(),
                inv.endpoint.as_str()
            ),
            ("http://10.46.0.1:50070", WG_HUB, "203.0.113.7:51830")
        );
        let (_, again) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert_eq!(again.address, e.address, "the address is allocated once");
        assert_eq!(
            h.wg_peers().unwrap(),
            vec![WgPeer {
                name: "lab-2".into(),
                key: WG_A.into(),
                address: e.address
            }]
        );
        assert_eq!(h.via_of("lab-2").word(), "wireguard");
        // Revoked: audited, kept, and gone from every list.
        let r = h
            .revoke_downstream("lab-2", "console:rickard", "10.44.0.1")
            .unwrap();
        assert_eq!(r.key, lab2.key);
        assert!(
            audit
                .events()
                .iter()
                .any(|ev| ev.detail.contains("'lab-2' REVOKED")),
            "the revocation is audited"
        );
        assert!(h.wg_peers().unwrap().is_empty(), "the front drops the peer");
        assert!(
            matches!(h.pull_manifest("lab-2", 0), Err(f) if f.code.code == "holger-sync-5"),
            "no manifest for a revoked downstream"
        );
        assert!(h
            .revoke_downstream("lab-2", "console", "x")
            .unwrap_err()
            .tagged()
            .contains("holger-sync-11"));
        // A revoked RON entry stays revoked while the RON names the same keys.
        h.revoke_downstream("lab-1", "console", "x").unwrap();
        let (again, _) = hub(d.path(), vec![peer("lab-1", Via::Front)]);
        assert!(again.downstreams().unwrap().is_empty());
        let mut fresh = peer("lab-1", Via::Front);
        fresh.key = "e".repeat(64);
        let (rekeyed, _) = hub(d.path(), vec![fresh]);
        assert_eq!(
            rekeyed.downstreams().unwrap().len(),
            1,
            "new keys are a new enrolment"
        );
    }

    /// The front's offered interface is kept, and an invite asked without one takes it.
    #[test]
    fn an_invite_without_the_fronts_interface_takes_the_one_its_offer_kept() {
        let d = tempfile::tempdir().unwrap();
        let (h, _) = hub(
            d.path(),
            vec![peer("lab-2", Via::Wireguard { key: WG_A.into() })],
        );
        assert!(h
            .invite("lab-2", "", "", "console", "x")
            .unwrap_err()
            .tagged()
            .contains("no front has offered"));
        assert_eq!(
            keep_front_hub(d.path(), WG_HUB, "198.51.100.4:51830"),
            Ok(true)
        );
        assert_eq!(
            keep_front_hub(d.path(), WG_HUB, "198.51.100.4:51830"),
            Ok(false),
            "unchanged"
        );
        assert!(keep_front_hub(d.path(), "x", "198.51.100.4:51830").is_err());
        let (signed, _) = h.invite("lab-2", "", "", "console", "x").unwrap();
        let trusted = vec![TrustedPeer {
            name: "cloud".into(),
            key: h.public_key(),
        }];
        let inv = holger_sync::invite::open_invite(
            &signed.to_bytes(),
            &trusted,
            "lab-2",
            WG_A,
            holger_sync::now_secs(),
        )
        .unwrap();
        assert_eq!(inv.endpoint, "198.51.100.4:51830");
        // ★ An endpoint the admin names wins over the kept one, the kept key still used (lane
        // OX-WGDS3, the KVM gate: the front offers its interface's own address, which a downstream
        // behind a relay or a NAT cannot dial; the invite named the relay and was signed with the
        // front's address anyway).
        let (signed, _) = h
            .invite("lab-2", "", "10.0.2.2:39275", "console", "x")
            .unwrap();
        let inv = holger_sync::invite::open_invite(
            &signed.to_bytes(),
            &trusted,
            "lab-2",
            WG_A,
            holger_sync::now_secs(),
        )
        .unwrap();
        assert_eq!(
            (inv.endpoint.as_str(), inv.hub_key.as_str()),
            ("10.0.2.2:39275", WG_HUB)
        );
    }

    const WG_B: &str = "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=";
    const WG_C: &str = "QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=";

    /// ★ **Each WireGuard enrolment gets its own pre-shared key** (lane WG-PSK): minted with
    /// the enrolment, kept in its record (0600), carried by its invite (schema v2) and by the
    /// front's relay list, never by the console's lists. The same key invited again keeps it;
    /// another downstream gets another; a NEW WireGuard key is a new enrolment with a new PSK
    /// (the PSK rotates with the peer's key). A record from before the lane stays without one
    /// (its downstream keeps its kept v1 invite: the link is never cut by the update).
    #[test]
    fn each_wireguard_enrolment_gets_its_own_preshared_key_in_its_invite_and_the_fronts_list_only()
    {
        let d = tempfile::tempdir().unwrap();
        let wg = |k: &str| Via::Wireguard { key: k.into() };
        let (h, _) = hub(
            d.path(),
            vec![peer("lab-2", wg(WG_A)), peer("lab-4", wg(WG_B))],
        );
        let trusted = vec![TrustedPeer {
            name: "cloud".into(),
            key: h.public_key(),
        }];
        let open = |s: &Signed, own: &str| {
            holger_sync::invite::open_invite(
                &s.to_bytes(),
                &trusted,
                "lab-2",
                own,
                holger_sync::now_secs(),
            )
            .unwrap()
        };
        let (signed, e) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        let inv = open(&signed, WG_A);
        assert_eq!(inv.schema, holger_sync::invite::INVITE_SCHEMA_PSK);
        let psk = inv
            .psk
            .clone()
            .expect("a new enrolment's invite carries its pre-shared key");
        assert_eq!(psk.bytes().unwrap().len(), 32);
        assert_eq!(e.psk.as_ref(), Some(&psk), "kept in the enrolment's record");
        let record = d
            .path()
            .join(STATE_DIR)
            .join(RECORDS_DIR)
            .join(WG_DIR)
            .join("lab-2.ron");
        use std::os::unix::fs::PermissionsExt as _;
        assert_eq!(
            std::fs::metadata(&record).unwrap().permissions().mode() & 0o777,
            0o600,
            "the record holds a secret"
        );
        let relay = h.wg_relay_peers().unwrap();
        assert_eq!(
            relay
                .iter()
                .find(|p| p.name == "lab-2")
                .and_then(|p| p.psk.clone()),
            Some(psk.clone()),
            "the front's relay list carries it"
        );
        assert!(
            !serde_json::to_string(&h.wg_peers().unwrap())
                .unwrap()
                .contains(&psk.0),
            "the console's list never carries it"
        );
        assert!(
            !format!("{e:?} {relay:?}").contains(&psk.0),
            "no Debug prints it"
        );
        let (again, e2) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert_eq!(
            (open(&again, WG_A).psk, e2.psk),
            (Some(psk.clone()), Some(psk.clone())),
            "the same enrolment invited again keeps its key"
        );
        let (_, other) = h
            .invite("lab-4", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert!(
            other.psk.is_some() && other.psk != Some(psk.clone()),
            "another downstream, another key"
        );
        // A new WireGuard key for lab-2: a new enrolment, a new PSK.
        let (rekeyed, _) = hub(
            d.path(),
            vec![peer("lab-2", wg(WG_C)), peer("lab-4", wg(WG_B))],
        );
        let (_, e3) = rekeyed
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert!(
            e3.psk.is_some() && e3.psk != Some(psk),
            "the PSK rotates with the peer's key"
        );
        // A record from before the lane: listed without a key, invited again without one.
        let old = WgEnrolment {
            name: "lab-2".into(),
            key: WG_A.into(),
            address: Ipv4Addr::new(10, 46, 0, 2),
            invited_at: 1,
            invited_by: "console".into(),
            psk: None,
        };
        let d2 = tempfile::tempdir().unwrap();
        write_ron(
            &d2.path()
                .join(STATE_DIR)
                .join(RECORDS_DIR)
                .join(WG_DIR)
                .join("lab-2.ron"),
            &old,
            false,
        )
        .unwrap();
        let (h2, _) = hub(d2.path(), vec![peer("lab-2", wg(WG_A))]);
        assert_eq!(
            h2.wg_relay_peers().unwrap(),
            vec![RelayPeer {
                name: "lab-2".into(),
                key: WG_A.into(),
                address: old.address,
                psk: None
            }]
        );
        let (v1, kept) = h2
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        let trusted2 = vec![TrustedPeer {
            name: "cloud".into(),
            key: h2.public_key(),
        }];
        let i = holger_sync::invite::open_invite(
            &v1.to_bytes(),
            &trusted2,
            "lab-2",
            WG_A,
            holger_sync::now_secs(),
        )
        .unwrap();
        assert_eq!(
            (i.schema.as_str(), i.psk, kept.psk),
            (holger_sync::invite::INVITE_SCHEMA, None, None)
        );
    }

    /// ★ **A main rebuilt from its twin keeps every link's pre-shared key** (lane WG-PSK, the
    /// owner's lifecycle rule): the enrolment rides the estate, so the rebuilt main lists the
    /// same key for the front and invites with it again.
    #[test]
    fn a_main_rebuilt_from_its_twin_keeps_each_downstreams_preshared_key() {
        let (main, twin, rebuilt) = (
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
        );
        let (h, _) = hub(main.path(), vec![]);
        h.add_downstream(
            peer("lab-2", Via::Wireguard { key: WG_A.into() }),
            "console",
            "x",
        )
        .unwrap();
        let (_, e) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        let psk = e.psk.clone().expect("minted");
        let _ = hub(twin.path(), vec![]);
        pull_estate(main.path(), twin.path());
        let (r, _) = hub(rebuilt.path(), vec![]);
        pull_estate(twin.path(), rebuilt.path());
        assert_eq!(
            r.wg_relay_peers()
                .unwrap()
                .into_iter()
                .map(|p| p.psk)
                .collect::<Vec<_>>(),
            vec![Some(psk.clone())]
        );
        let (_, again) = r
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert_eq!(again.psk, Some(psk));
    }

    /// The estate pull a twin's follow (and a recharge) runs, disk to disk.
    fn pull_estate(from: &Path, to: &Path) {
        let m = crate::estate_wire::manifest(from).unwrap();
        let r = crate::estate_wire::pull(&m, to, &mut |p, off| {
            crate::estate_wire::read_range(from, p, off, 0)
        })
        .unwrap();
        assert!(r.failed.is_empty(), "{:?}", r.failed);
    }

    /// ★ **A main rebuilt from its twin is the same Holger to its downstreams after a REAL
    /// start** (lane HOL-REBUILT-SYNC-KEY, `gate holger --downstreams` 2026-10-31 15:43Z on
    /// holger 70235ded: `ds-main-rebuilt-from-its-twin` red, the rebuilt main served under a
    /// NEW sync key). Every box of an estate starts by seeding the medium's Sigstore trust root
    /// (HOL-OCI-EVIDENCE, `sigstore_trust::seed` from `sandbox_boot`) beside the key it mints.
    /// The rebuilt main must still be fresh for its recharge (the boot hold asks
    /// `Follower::recharge_blocker`: else it is not held, serves empty and is never filled),
    /// take the twin's estate, sign under the old key, and a downstream that pinned the old key
    /// accepts what it signs.
    #[cfg(feature = "export-push")]
    #[test]
    fn a_main_rebuilt_after_a_start_that_seeded_the_trust_root_keeps_its_sync_key() {
        let medium = tempfile::tempdir().unwrap();
        let signer = tempfile::tempdir().unwrap();
        let trusted_root = std::fs::read(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../provenance/tests/fixtures/sigstore/trusted_root.json"
        ))
        .unwrap();
        let policy = crate::sigstore_trust::make(
            &trusted_root,
            signer.path(),
            &medium.path().join(crate::sigstore_trust::BUNDLE_FILE),
            7,
            30,
            None,
        )
        .unwrap();
        std::fs::write(
            medium.path().join(crate::sigstore_trust::POLICY_FILE),
            policy.to_ron().unwrap(),
        )
        .unwrap();
        // What a start does on every estate box, from the medium.
        let start = |root: &Path| {
            let (h, a) = hub(root, vec![]);
            let m = medium.path().to_path_buf();
            crate::sigstore_trust::seed(root, &|n: &str| std::fs::read(m.join(n)).ok())
                .unwrap()
                .expect("the medium carries the trust root");
            (h, a)
        };
        let (main, twin, rebuilt) = (
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
        );
        let (h, _) = start(main.path());
        h.add_downstream(peer("lab-1", Via::Front), "console", "x")
            .unwrap();
        let key = h.public_key();
        let pinned = vec![TrustedPeer {
            name: "cloud".into(),
            key: key.clone(),
        }];
        let first = h.bundle_manifest(vec![], "usb").unwrap();
        assert_eq!(
            holger_sync::open_manifest(&first.to_bytes(), &pinned)
                .unwrap()
                .generation,
            1
        );

        // The twin starts (its own key, the medium's trust root), is elected, follows.
        let _ = start(twin.path());
        assert_eq!(
            crate::twin::held_state(twin.path()),
            None,
            "a started twin is fresh for its election"
        );
        pull_estate(main.path(), twin.path());

        // The main dies. The box laid again starts the same way and must be HELD for its
        // recharge: what `Follower::recharge_blocker` asks finds nothing of its own.
        let (r, _) = start(rebuilt.path());
        assert_ne!(r.public_key(), key, "a fresh start mints a key of its own");
        assert_eq!(
            crate::twin::held_state_except(
                rebuilt.path(),
                &[
                    "holger-audit",
                    crate::twin_follow::RECHARGED,
                    crate::twin_follow::RECHARGING
                ]
            ),
            None,
            "a started main laid again is fresh for its recharge, so the boot hold holds it"
        );
        pull_estate(twin.path(), rebuilt.path());

        assert_eq!(
            r.public_key(),
            key,
            "the rebuilt main signs under the key its downstreams pinned"
        );
        let next = r.bundle_manifest(vec![], "usb").unwrap();
        assert_eq!(
            holger_sync::open_manifest(&next.to_bytes(), &pinned)
                .unwrap()
                .generation,
            2,
            "a downstream pinned to the old key accepts the rebuilt main's next generation"
        );
        assert_eq!(
            r.downstreams()
                .unwrap()
                .iter()
                .map(|p| p.name.as_str())
                .collect::<Vec<_>>(),
            ["lab-1"]
        );
    }

    /// ★ **TWIN PARITY** (lane OX-WGDS2, the owner's lifecycle rule: a dead main is rebuilt
    /// from its twin). The sync state rides the estate: a main rebuilt from its twin (a fresh
    /// box that minted a key of its own at its start, then recharged) keeps every enrolment,
    /// every revocation, the WireGuard addresses, the ledger's generations and the sync key
    /// its downstreams trust. The downstream still syncs; a revoked one stays revoked.
    #[test]
    fn a_main_rebuilt_from_its_twin_keeps_its_downstreams_and_a_revoked_one_stays_revoked() {
        let (main, twin, rebuilt) = (
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
            tempfile::tempdir().unwrap(),
        );
        let (h, _) = hub(main.path(), vec![]);
        h.add_downstream(peer("lab-1", Via::Front), "console", "x")
            .unwrap();
        h.add_downstream(
            peer("lab-2", Via::Wireguard { key: WG_A.into() }),
            "console",
            "x",
        )
        .unwrap();
        h.add_downstream(peer("lab-3", Via::Front), "console", "x")
            .unwrap();
        let (_, wg) = h
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        h.revoke_downstream("lab-3", "console", "x").unwrap();
        let first = h.bundle_manifest(vec![], "usb").unwrap();
        let key = h.public_key();
        let trusted = vec![TrustedPeer {
            name: "cloud".into(),
            key: key.clone(),
        }];
        assert_eq!(
            holger_sync::open_manifest(&first.to_bytes(), &trusted)
                .unwrap()
                .generation,
            1
        );

        // The twin's own start minted its own key; that keeps it fresh for its election.
        let _ = hub(twin.path(), vec![]);
        assert_eq!(
            crate::twin::held_state(twin.path()),
            None,
            "a key minted at the start is no history"
        );
        pull_estate(main.path(), twin.path());

        // The main dies. A fresh box starts (a key of its own again), is fresh for the
        // recharge, and pulls its twin's estate while it runs.
        let (r, _) = hub(rebuilt.path(), vec![]);
        assert_ne!(r.public_key(), key);
        assert_eq!(
            crate::twin::held_state(rebuilt.path()),
            None,
            "a fresh main with only its minted key is fresh for a recharge"
        );
        pull_estate(twin.path(), rebuilt.path());

        assert_eq!(
            r.public_key(),
            key,
            "the rebuilt main signs under the key its downstreams trust"
        );
        assert_eq!(
            r.downstreams()
                .unwrap()
                .iter()
                .map(|p| p.name.as_str())
                .collect::<Vec<_>>(),
            ["lab-1", "lab-2"]
        );
        // The ledger came along: the next generation is 2 (a fresh ledger's 1 is a rollback
        // every downstream refuses), signed under the old key.
        let next = r.bundle_manifest(vec![], "usb").unwrap();
        assert_eq!(
            holger_sync::open_manifest(&next.to_bytes(), &trusted)
                .unwrap()
                .generation,
            2
        );
        // The downstream still syncs: its ack for generation 2 is taken.
        let ack = |name: &str| {
            let a = holger_sync::SyncAck {
                schema: holger_sync::ACK_SCHEMA.into(),
                downstream: name.into(),
                upstream: "cloud".into(),
                generation: 2,
                applied_at: holger_sync::now_secs(),
                packages_added: 0,
                invalidations_applied: vec![],
                quarantined: vec![],
                carrier: "usb".into(),
            };
            SyncKey::from_seed([name.len() as u8; 32])
                .sign_ack(&a)
                .unwrap()
                .to_bytes()
        };
        r.import_ack(&ack("lab-1"), "usb", "x").unwrap();
        // A revoked one stays revoked: no manifest, no ack, no WireGuard peer.
        assert!(
            matches!(r.pull_manifest("lab-3", 0), Err(f) if f.code.code == "holger-sync-5"),
            "no manifest for a revoked downstream"
        );
        assert!(
            r.import_ack(&ack("lab-3"), "usb", "x").is_err(),
            "no ack from a revoked downstream"
        );
        assert_eq!(
            r.wg_peers().unwrap(),
            vec![WgPeer {
                name: "lab-2".into(),
                key: WG_A.into(),
                address: wg.address
            }],
            "the WireGuard one keeps its address"
        );
        let (signed, again) = r
            .invite("lab-2", WG_HUB, "203.0.113.7:51830", "console", "x")
            .unwrap();
        assert_eq!(again.address, wg.address);
        holger_sync::invite::open_invite(
            &signed.to_bytes(),
            &trusted,
            "lab-2",
            WG_A,
            holger_sync::now_secs(),
        )
        .unwrap();
        // The revoked one's old record came along beside its revocation (a follow never
        // deletes): it is no enrolment, and new keys enrol it again.
        let mut rekeyed = peer("lab-3", Via::Front);
        rekeyed.key = "e".repeat(64);
        r.add_downstream(rekeyed, "console", "x").unwrap();
        assert_eq!(r.downstreams().unwrap().len(), 3);
    }

    /// ★ The console's enrolment form reads as the RON entry, and holger checks it the same way.
    #[test]
    fn the_consoles_enrolment_form_is_the_ron_entry_checked_the_same_way() {
        let d = tempfile::tempdir().unwrap();
        let (h, _) = hub(d.path(), vec![]);
        let k = "a".repeat(64);
        assert_eq!(
            peer_from_form(" lab-1 ", &k, "", "x").unwrap().via,
            Via::Front
        );
        let wg = peer_from_form("lab-2", &k, "wireguard", WG_A).unwrap();
        assert_eq!(wg.via, Via::Wireguard { key: WG_A.into() });
        assert_eq!(h.add_downstream(wg, "console", "x").unwrap().name, "lab-2");
        assert!(peer_from_form("lab-3", &k, "carrier", "")
            .unwrap_err()
            .tagged()
            .contains("`front` or `wireguard`"));
        let e = h
            .add_downstream(
                peer_from_form("lab-4", &k, "wireguard", "nope").unwrap(),
                "console",
                "x",
            )
            .unwrap_err()
            .tagged();
        assert!(
            e.contains("holger-downstream-5") && e.contains("lab-4"),
            "{e}"
        );
        assert!(h
            .add_downstream(
                peer_from_form("lab-5", "short", "front", "").unwrap(),
                "console",
                "x"
            )
            .unwrap_err()
            .tagged()
            .contains("64 hex digits"));
    }

    #[test]
    fn the_lowest_free_address_is_taken_and_the_front_never() {
        assert_eq!(free_address(&[]), Some(Ipv4Addr::new(10, 46, 0, 2)));
        assert_eq!(
            free_address(&[Ipv4Addr::new(10, 46, 0, 2), Ipv4Addr::new(10, 46, 0, 4)]),
            Some(Ipv4Addr::new(10, 46, 0, 3))
        );
        let all: Vec<Ipv4Addr> = (2u8..=254).map(|n| Ipv4Addr::new(10, 46, 0, n)).collect();
        assert_eq!(free_address(&all), None);
    }
}