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);
}
}