server/lib/tests/a_plain_server_invites_its_downstream.rs
//! ★ **A plain server invites its downstream** (lane HOL-PRODUCTS-7, owner go 2026-10-30).
//!
//! A server with no estate front has no WireGuard interface for its downstreams, so until now
//! only a `via: Wireguard` downstream behind a front got a signed invite. The plain server now
//! gives a `via: Front` (direct) downstream the same signed invite, in the estate's format:
//! `holger.downstream-invite/v1` (no pre-shared key: no tunnel), signed with the server's sync
//! key, for the downstream's own sync key, its `door` the server's sync door. The downstream
//! names it in `sync.upstreams[].invite`, verifies it once under the key it trusts and keeps
//! it, and reaches its upstream at the invite's door (no `url:` to write by hand).
//!
//! * the invite is signed by the upstream, for that downstream and its sync key, door = the
//! server's door (or the door the admin names), and the issuing is audited;
//! * a WireGuard downstream still needs the front (its invite is v2, the PSK in it);
//! * the downstream keeps a good invite once and refuses another, a forged or a stranger's;
//! * a downstream with no `url:` reaches its upstream at the kept invite's door.
use std::path::Path;
use std::sync::{Arc, Weak};
use holger_downstream_rules::via::Via;
use holger_server_lib::audit::MemoryAuditLog;
use holger_server_lib::sync::{invited, SyncConfig, SyncHub, SyncPeer};
use holger_sync::invite::{open_invite, INVITE_SCHEMA};
use holger_sync::{SyncKey, TrustedPeer};
const DOOR: &str = "http://127.0.0.1:18080";
const WG_A: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
fn upstream(dir: &Path, downstreams: Vec<SyncPeer>) -> (SyncHub, Arc<MemoryAuditLog>) {
let audit = Arc::new(MemoryAuditLog::new());
let cfg = SyncConfig {
name: "hq".into(),
key: None,
downstreams,
upstreams: vec![],
ack_deadline_secs: 60,
tick_secs: 30,
};
(
SyncHub::new(cfg, dir, audit.clone(), Weak::new())
.unwrap()
.with_door(DOOR),
audit,
)
}
fn peer(name: &str, key: &str, via: Via) -> SyncPeer {
SyncPeer {
name: name.into(),
key: key.into(),
url: None,
token_file: None,
via,
invite: None,
}
}
#[test]
fn the_plain_server_signs_a_direct_invite_for_its_downstream() {
let (up_dir, down_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let lab_key = SyncKey::from_seed([7; 32]).public_hex();
let (hq, audit) = upstream(
up_dir.path(),
vec![
peer("lab", &lab_key, Via::Front),
peer(
"wg",
&SyncKey::from_seed([8; 32]).public_hex(),
Via::Wireguard { key: WG_A.into() },
),
],
);
let signed = hq.invite_direct("lab", "", "adm", "127.0.0.1").unwrap();
let trusted = vec![TrustedPeer {
name: "hq".into(),
key: hq.public_key(),
}];
let inv = open_invite(
&signed.to_bytes(),
&trusted,
"lab",
&lab_key,
holger_sync::now_secs(),
)
.unwrap();
assert_eq!(
inv.schema, INVITE_SCHEMA,
"no tunnel, no pre-shared key: schema v1"
);
assert!(inv.is_direct() && inv.psk.is_none(), "{inv:?}");
assert_eq!(inv.door, DOOR, "the server's own sync door");
assert!(
audit
.events()
.iter()
.any(|e| e.detail.contains("'lab' invited directly")),
"the issuing is audited"
);
// The admin may name the door the downstream reaches (a proxy, another name).
let other = hq
.invite_direct("lab", "https://hq.example:8443", "adm", "x")
.unwrap();
assert_eq!(
open_invite(
&other.to_bytes(),
&trusted,
"lab",
&lab_key,
holger_sync::now_secs()
)
.unwrap()
.door,
"https://hq.example:8443"
);
// A WireGuard downstream's invite needs the front's interface; a stranger gets none.
assert!(hq
.invite_direct("wg", "", "adm", "x")
.unwrap_err()
.tagged()
.contains("Wireguard"));
assert!(hq
.invite_direct("nobody", "", "adm", "x")
.unwrap_err()
.tagged()
.contains("not a live downstream"));
assert!(hq
.invite_direct("lab", "ftp://x", "adm", "x")
.unwrap_err()
.tagged()
.contains("http"));
// The downstream: its RON names the invite and no url; it keeps it once.
let invite_path = down_dir.path().join("lab.invite.json");
std::fs::write(&invite_path, signed.to_bytes()).unwrap();
let mut hq_peer = peer("hq", &hq.public_key(), Via::Front);
hq_peer.invite = Some(invite_path.display().to_string());
let said = invited::start_direct(
down_dir.path(),
"lab",
&lab_key,
std::slice::from_ref(&hq_peer),
);
assert!(
said.iter()
.any(|l| l.contains("invited this downstream directly") && l.contains(DOOR)),
"{said:?}"
);
assert_eq!(
invited::door_of(down_dir.path(), "lab", &lab_key, &hq_peer).as_deref(),
Some(DOOR),
"the kept invite's door is the upstream's url"
);
// Another invite in its place is refused by name; the kept one stays the door.
std::fs::write(&invite_path, other.to_bytes()).unwrap();
let said = invited::start_direct(
down_dir.path(),
"lab",
&lab_key,
std::slice::from_ref(&hq_peer),
);
assert!(
said.iter()
.any(|l| l.contains("REFUSED") && l.contains("another one")),
"{said:?}"
);
assert_eq!(
invited::door_of(down_dir.path(), "lab", &lab_key, &hq_peer).as_deref(),
Some(DOOR)
);
// An invite signed by a key this downstream does not trust is refused.
let fresh = tempfile::tempdir().unwrap();
let mut stranger = hq_peer.clone();
stranger.key = SyncKey::from_seed([9; 32]).public_hex();
std::fs::write(&invite_path, signed.to_bytes()).unwrap();
let said = invited::start_direct(
fresh.path(),
"lab",
&lab_key,
std::slice::from_ref(&stranger),
);
assert!(said.iter().any(|l| l.contains("REFUSED")), "{said:?}");
assert_eq!(
invited::door_of(fresh.path(), "lab", &lab_key, &stranger),
None
);
}
/// **An invite never names a wildcard door** (lane hol-312fix, field test F5). In the field the
/// server listened on `0.0.0.0:8080`, its sync door became that listen address, and the invite
/// told the downstream to dial `http://0.0.0.0:8080`, which no downstream can reach. An
/// unspecified (wildcard) address in the door, whether the server's own or the one the admin
/// names, is refused by name, and the refusal says to name a reachable door.
#[test]
fn an_invite_refuses_a_wildcard_door_by_name() {
let dir = tempfile::tempdir().unwrap();
let lab_key = SyncKey::from_seed([7; 32]).public_hex();
let cfg = SyncConfig {
name: "hq".into(),
key: None,
downstreams: vec![peer("lab", &lab_key, Via::Front)],
upstreams: vec![],
ack_deadline_secs: 60,
tick_secs: 30,
};
let audit = Arc::new(MemoryAuditLog::new());
let hq = SyncHub::new(cfg, dir.path(), audit.clone(), Weak::new())
.unwrap()
.with_door("http://0.0.0.0:8080");
// The server's own door is its listen address, the wildcard: refused, naming it.
let e = hq
.invite_direct("lab", "", "adm", "x")
.unwrap_err()
.tagged();
assert!(
e.contains("0.0.0.0") && e.contains("wildcard") && e.contains("door"),
"{e}"
);
// A wildcard the admin names is refused the same way, IPv6 and portless too.
for door in [
"http://[::]:8443",
"https://0.0.0.0",
"http://0.0.0.0:8080/",
] {
let e = hq
.invite_direct("lab", door, "adm", "x")
.unwrap_err()
.tagged();
assert!(e.contains("wildcard"), "{door}: {e}");
}
// A reachable door is still signed, and nothing was audited for the refusals.
hq.invite_direct("lab", "http://192.168.1.20:8080", "adm", "x")
.unwrap();
let invited: Vec<_> = audit
.events()
.into_iter()
.filter(|e| e.detail.contains("invited directly"))
.collect();
assert_eq!(invited.len(), 1, "{invited:?}");
}