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

nordisk/edda

crates/wg-appliance-core/examples/hub_relay.rs

//! `hub_relay` — the [`HubRelay`] as a process, for a bench that drives guests.
//!
//! ```text
//!   hub_relay --key FILE --port N --endpoint IP --subnet CIDR --conf-out FILE [--conf-only]
//!             [--peers FILE] [--verdict-out FILE] [--hub-addr IP]
//! ```
//!
//! * `--key FILE`: the hub's 32-byte private key, MINTED on first use (0600) and
//!   read after, so the `hub.conf` a medium was composed with stays valid.
//! * `--conf-out FILE`: the site `hub.conf` (ceremony dialect) the medium bakes,
//!   `Endpoint = <endpoint>:<port>`, `AllowedIPs = <subnet>`.
//! * `--conf-only`: write the conf and exit (the compose step).
//! * `--peers FILE`: the `[Peer]` list `gunnar control tunnel enrol --hub-peers`
//!   appends to; re-read every half second, each new `PublicKey`/`AllowedIPs`
//!   pair is listed on the relay (a real hub is told to reload; this one looks).
//! * `--verdict-out FILE`: the verdict, rewritten every half second — one line
//!   per peer (`peer <addr> handshakes N rx_bytes N rx_packets N tx_bytes N
//!   tx_packets N keepalives N`) and one `totals refused N spoofed N no_route N
//!   to_hub N`.
//!
//! Userspace, no root. Runs until killed.

use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::time::Duration;

use wg_appliance_core::hubrelay::HubRelay;
use wg_appliance_core::keys::{decode_key, generate_keypair, KEY_LEN};

fn flag(args: &[String], name: &str) -> Option<String> {
    args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned()
}

fn load_or_mint(path: &PathBuf) -> Result<[u8; KEY_LEN], String> {
    match std::fs::read(path) {
        Ok(b) => b.try_into().map_err(|_| format!("{} does not hold a 32-byte key", path.display())),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            use std::io::Write as _;
            use std::os::unix::fs::OpenOptionsExt as _;
            let (private, _) = generate_keypair();
            let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path).map_err(|e| format!("{}: {e}", path.display()))?;
            f.write_all(&private).map_err(|e| e.to_string())?;
            Ok(private)
        }
        Err(e) => Err(format!("{}: {e}", path.display())),
    }
}

/// `[Peer]` blocks of a `--hub-peers` file: `(PublicKey, the /32's address)`.
fn read_peers(text: &str) -> Vec<([u8; KEY_LEN], Ipv4Addr)> {
    let mut out = Vec::new();
    let (mut key, mut addr) = (None, None);
    let mut flush = |key: &mut Option<[u8; KEY_LEN]>, addr: &mut Option<Ipv4Addr>| {
        if let (Some(k), Some(a)) = (key.take(), addr.take()) {
            out.push((k, a));
        }
    };
    for line in text.lines().map(str::trim) {
        if line == "[Peer]" {
            flush(&mut key, &mut addr);
        } else if let Some(v) = line.strip_prefix("PublicKey") {
            key = decode_key(v.trim_start_matches([' ', '=']).trim()).ok();
        } else if let Some(v) = line.strip_prefix("AllowedIPs") {
            addr = v.trim_start_matches([' ', '=']).trim().split('/').next().and_then(|a| a.parse().ok());
        }
    }
    flush(&mut key, &mut addr);
    out
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let need = |n: &str| flag(&args, n).unwrap_or_else(|| {
        eprintln!("hub_relay: {n} is required");
        std::process::exit(2)
    });
    let key_path = PathBuf::from(need("--key"));
    let port: u16 = need("--port").parse().expect("--port N");
    let endpoint: Ipv4Addr = need("--endpoint").parse().expect("--endpoint IP");
    let subnet = need("--subnet");
    let hub_addr: Ipv4Addr = flag(&args, "--hub-addr").unwrap_or_else(|| "10.77.0.1".into()).parse().expect("--hub-addr IP");
    let private = load_or_mint(&key_path).unwrap_or_else(|e| {
        eprintln!("hub_relay: {e}");
        std::process::exit(2)
    });
    let conf_out = PathBuf::from(need("--conf-out"));
    let conf = wg_appliance_core::hubfixture::render_hub_conf(
        &wg_appliance_core::keys::encode_key(&wg_appliance_core::keys::public_from_private_bytes(&private)),
        endpoint,
        port,
        &subnet,
        5,
        None,
    );
    std::fs::write(&conf_out, &conf).expect("write --conf-out");
    if args.iter().any(|a| a == "--conf-only") {
        println!("{}", conf_out.display());
        return;
    }
    let relay = HubRelay::spawn(port, private, hub_addr).unwrap_or_else(|e| {
        eprintln!("hub_relay: binding udp {port}: {e}");
        std::process::exit(2)
    });
    println!("hub_relay listening udp 0.0.0.0:{} key {} (hub.conf {})", relay.port(), relay.public_b64, conf_out.display());
    let peers_file = flag(&args, "--peers").map(PathBuf::from);
    let verdict_out = flag(&args, "--verdict-out").map(PathBuf::from);
    let mut listed: Vec<[u8; KEY_LEN]> = Vec::new();
    loop {
        if let Some(p) = &peers_file {
            if let Ok(text) = std::fs::read_to_string(p) {
                for (k, a) in read_peers(&text) {
                    if !listed.contains(&k) {
                        match relay.add_peer(k, a) {
                            Ok(()) => println!("hub_relay listed {a} {}", wg_appliance_core::keys::encode_key(&k)),
                            Err(e) => println!("hub_relay refused to list {a}: {e}"),
                        }
                        listed.push(k);
                    }
                }
            }
        }
        if let Some(out) = &verdict_out {
            let v = relay.verdict();
            let mut s = String::new();
            for (a, _, c) in &v.peers {
                s.push_str(&format!(
                    "peer {a} handshakes {} rx_bytes {} rx_packets {} tx_bytes {} tx_packets {} keepalives {}\n",
                    c.handshakes, c.rx_bytes, c.rx_packets, c.tx_bytes, c.tx_packets, c.keepalives
                ));
            }
            s.push_str(&format!("totals refused {} spoofed {} no_route {} to_hub {}\n", v.refused, v.dropped_spoofed, v.dropped_no_route, v.to_hub));
            let tmp = out.with_extension("tmp");
            if std::fs::write(&tmp, s).is_ok() {
                let _ = std::fs::rename(&tmp, out);
            }
        }
        std::thread::sleep(Duration::from_millis(500));
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn the_enrol_peer_file_is_read() {
        let (_, pubk) = wg_appliance_core::keys::generate_keypair();
        let text = format!("[Peer]\n# gunnar main — registered by `gunnar control tunnel enrol`\nPublicKey = {}\nAllowedIPs = 10.77.0.2/32\n", wg_appliance_core::keys::encode_key(&pubk));
        assert_eq!(super::read_peers(&text), vec![(pubk, "10.77.0.2".parse().unwrap())]);
    }
}