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

nordisk/holger

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:?}");
}