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

nordisk/Skidbladnir

crates/installer/nordisk-pairing/src/mullvad.rs

//! **The Mullvad driver — each box leaves through Mullvad; the estate meets
//! inside its own WireGuard.**
//!
//! # What Mullvad is in an estate (the decision, 2026-09-24)
//!
//! Mullvad is an EXIT VPN: a client reaches the internet through a relay, and
//! the relays do not carry traffic from one client to another. So Mullvad
//! cannot be the mesh the boxes pair inside — two gunnar boxes on the same
//! account still cannot reach each other's tunnel addresses. `tunnel =
//! "mullvad"` therefore means:
//!
//! * **the MESH stays WireGuard via the front** — the same
//!   [`WireGuard`](crate::wireguard::WireGuard) driver on `wg0`, the same
//!   overlay addresses, the same peers from the join reply;
//! * **each box is ALSO a Mullvad client** — a second key, registered with the
//!   estate's Mullvad account (the account number is FED by the door as
//!   [`FEED_MULLVAD_ACCOUNT`](crate::seed::FEED_MULLVAD_ACCOUNT), never on the
//!   medium), a relay chosen by the seed's `[mullvad] location`, and a second
//!   interface [`EXIT_IFACE`] on the same userspace data plane
//!   (`tunnr_mullvad::peers::tunnel_for_relay` → `wg_appliance_core::dataplane`).
//!
//! `up` is "up" only when BOTH are: the mesh pump is running and the exit pump
//! is running with its registered address on [`EXIT_IFACE`].
//!
//! # What is NOT done here, said out loud
//!
//! * **The default route.** wg-appliance-core writes addresses and the
//!   connected route only (`addr.rs`: routes beyond the connected subnet need
//!   netlink `RTM_NEWROUTE`, which nothing in the suite writes — tunnr's F15).
//!   So the exit tunnel is up and addressed, and making it the box's default
//!   egress (with host routes for the relay and the front via the underlay) is
//!   the caller's — `status().detail` says so every time.
//! * **The PQ pre-shared key.** `tunnr_mullvad::pq::derive_psk` still refuses;
//!   the exit is classic WireGuard.
//! * **Live registration** is unproven: this host holds no Mullvad account.
//!   The token and device endpoints below are written from Mullvad's public
//!   REST paths, not measured; the flow is tested against a fake API
//!   (`shape_only_*`), and the relay list against the live endpoint
//!   (`--ignored`). Their home is `tunnr_mullvad::facts` (one writer of
//!   Mullvad numbers); they are here until that crate carries them.

use crate::join::Peer;
use crate::seed::{MullvadSeed, TunnelKind};
use crate::tunnel::{Identity, Tunnel, TunnelError, TunnelStatus};
use crate::wireguard::{WireGuard, MTU};
use std::net::Ipv4Addr;
use tunnr_mullvad::device::{RegisterDevice, RegisteredDevice};
use tunnr_mullvad::relays::{self, Relay, RelayList};
use wg_appliance_core::{addr, dataplane, encode_key, generate_keypair, TunDevice, TunnelHandle, TunnelSpec, KEY_LEN};

/// The Mullvad API's host. UNVERIFIED paths below (no account on this host).
pub const API: &str = "https://api.mullvad.net";
/// Account number → access token.
pub const TOKEN_PATH: &str = "/auth/v1/token";
/// The account's devices (POST registers one, GET lists them).
pub const DEVICES_PATH: &str = "/accounts/v1/devices";
/// The error code the device endpoint answers when the key is already
/// registered — the box rebooted with the exit key it kept.
pub const PUBKEY_IN_USE: &str = "PUBKEY_IN_USE";
/// The exit interface (the mesh is `wg0`).
pub const EXIT_IFACE: &str = "wg1";
/// The relay list can be several MB.
const MAX_BODY: u64 = 32 << 20;

/// A refusal from the API: the status (if one came) and the body.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiError {
    pub status: Option<u16>,
    pub body: String,
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.status {
            Some(s) => write!(f, "HTTP {s}: {}", self.body.trim()),
            None => write!(f, "{}", self.body.trim()),
        }
    }
}

/// **The transport**, a seam: [`HttpApi`] on the wire, a fake in the tests.
/// Every method returns the response BODY; the parsing is tunnr-mullvad's.
pub trait MullvadApi {
    fn relay_list(&self) -> Result<String, ApiError>;
    /// The account number, exchanged for an access token (the body is JSON
    /// with `access_token`).
    fn access_token(&self, account: &str) -> Result<String, ApiError>;
    fn register(&self, token: &str, req: &RegisterDevice) -> Result<String, ApiError>;
    fn devices(&self, token: &str) -> Result<String, ApiError>;
}

/// **The smallest honest HTTP client**: ureq on rustls/ring with the webpki
/// roots, blocking, no cookies, no proxy. tunnr-mullvad deliberately has
/// none (its transport is the caller's); this is that caller.
pub struct HttpApi {
    agent: ureq::Agent,
    base: String,
}

impl Default for HttpApi {
    fn default() -> Self {
        HttpApi::new(API)
    }
}

impl HttpApi {
    pub fn new(base: &str) -> HttpApi {
        let agent = ureq::Agent::config_builder()
            .http_status_as_error(false)
            .timeout_global(Some(std::time::Duration::from_secs(30)))
            .build()
            .new_agent();
        HttpApi { agent, base: base.trim_end_matches('/').to_string() }
    }

    fn done(r: Result<ureq::http::Response<ureq::Body>, ureq::Error>) -> Result<String, ApiError> {
        let mut r = r.map_err(|e| ApiError { status: None, body: format!("could not be reached: {e}") })?;
        let status = r.status().as_u16();
        let body = r.body_mut().with_config().limit(MAX_BODY).read_to_string().map_err(|e| ApiError { status: Some(status), body: format!("reading the body: {e}") })?;
        if (200..300).contains(&status) {
            Ok(body)
        } else {
            Err(ApiError { status: Some(status), body })
        }
    }

    fn post_json(&self, path: &str, bearer: Option<&str>, body: String) -> Result<String, ApiError> {
        let mut req = self.agent.post(&format!("{}{path}", self.base)).header("content-type", "application/json");
        if let Some(t) = bearer {
            req = req.header("authorization", &format!("Bearer {t}"));
        }
        HttpApi::done(req.send(body))
    }
}

impl MullvadApi for HttpApi {
    fn relay_list(&self) -> Result<String, ApiError> {
        HttpApi::done(self.agent.get(tunnr_mullvad::facts::RELAY_LIST_URL).call())
    }

    fn access_token(&self, account: &str) -> Result<String, ApiError> {
        let body = serde_json::json!({ "account_number": account }).to_string();
        self.post_json(TOKEN_PATH, None, body)
    }

    fn register(&self, token: &str, req: &RegisterDevice) -> Result<String, ApiError> {
        let body = serde_json::to_string(req).map_err(|e| ApiError { status: None, body: e.to_string() })?;
        self.post_json(DEVICES_PATH, Some(token), body)
    }

    fn devices(&self, token: &str) -> Result<String, ApiError> {
        HttpApi::done(self.agent.get(&format!("{}{DEVICES_PATH}", self.base)).header("authorization", &format!("Bearer {token}")).call())
    }
}

/// **The exit, planned** — everything `up` needs, with no interface touched.
pub struct ExitPlan {
    pub relay: String,
    pub address: (Ipv4Addr, u8),
    pub device: String,
    pub spec: TunnelSpec,
}

impl std::fmt::Debug for ExitPlan {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ExitPlan {{ relay: {}, address: {}/{}, device: {}, spec: {} }}", self.relay, self.address.0, self.address.1, self.device, tunnr_mullvad::peers::redacted(&self.spec))
    }
}

/// **The relay a box leaves through**: usable (active, a 32-byte key), in the
/// seed's location — a code (`se-sto`) or a country code (`se`), empty = any —
/// and of those the heaviest (Mullvad's own share), ties by hostname so every
/// box of an estate reading the same list picks the same relay.
pub fn choose_relay<'a>(list: &'a RelayList, location: &str) -> Result<&'a Relay, TunnelError> {
    let country = format!("{location}-");
    list.usable()
        .filter(|r| location.is_empty() || r.location == location || (!location.contains('-') && r.location.starts_with(&country)))
        .max_by(|a, b| a.weight.cmp(&b.weight).then_with(|| b.hostname.cmp(&a.hostname)))
        .ok_or_else(|| TunnelError::NotConfigured { kind: TunnelKind::Mullvad, what: format!("no usable Mullvad relay in `{}` ({} relays listed)", if location.is_empty() { "any location" } else { location }, list.wireguard.relays.len()) })
}

/// The Mullvad driver: the mesh ([`WireGuard`] on `wg0`) and the exit.
pub struct Mullvad {
    mesh: WireGuard,
    exit_private: [u8; KEY_LEN],
    exit_public: String,
    seed: MullvadSeed,
    account: Option<String>,
    api: Box<dyn MullvadApi>,
    /// The registration, kept so a failed raise does not register again.
    planned: Option<(String, (Ipv4Addr, u8), String)>,
    exit: Option<TunnelHandle>,
}

impl Mullvad {
    /// `mesh` is the estate's WireGuard (its key is what the box joins with);
    /// `exit_key` is the key the box KEPT for Mullvad (`None` mints one — the
    /// caller persists [`Mullvad::exit_private_key`], or every boot registers
    /// a new device and the account's device limit fills); `account` is what
    /// the door fed.
    pub fn new(mesh: WireGuard, exit_key: Option<[u8; KEY_LEN]>, seed: MullvadSeed, account: Option<String>) -> Mullvad {
        Mullvad::with_api(mesh, exit_key, seed, account, Box::new(HttpApi::default()))
    }

    pub fn with_api(mesh: WireGuard, exit_key: Option<[u8; KEY_LEN]>, seed: MullvadSeed, account: Option<String>, api: Box<dyn MullvadApi>) -> Mullvad {
        let exit_private = exit_key.unwrap_or_else(|| generate_keypair().0);
        let exit_public = encode_key(&wg_appliance_core::keys::public_from_private_bytes(&exit_private));
        let account = account.map(|a| a.chars().filter(|c| !c.is_whitespace()).collect::<String>()).filter(|a| !a.is_empty());
        Mullvad { mesh, exit_private, exit_public, seed, account, api, planned: None, exit: None }
    }

    pub fn exit_private_key(&self) -> &[u8; KEY_LEN] {
        &self.exit_private
    }

    pub fn exit_public_key(&self) -> &str {
        &self.exit_public
    }

    pub fn mesh(&self) -> &WireGuard {
        &self.mesh
    }

    /// **Plan the exit**: the relay, the registration (or the device this key
    /// already is), the spec. Talks to the API; touches no interface.
    pub fn plan_exit(&mut self) -> Result<ExitPlan, TunnelError> {
        let fail = |what: &str, e: ApiError| TunnelError::Failed(format!("mullvad: {what}: {e}"));
        let account = self.account.clone().ok_or_else(|| TunnelError::NotConfigured {
            kind: TunnelKind::Mullvad,
            what: format!("no Mullvad account number: the door feeds it as `{}` (never on the medium)", crate::seed::FEED_MULLVAD_ACCOUNT),
        })?;
        let list = relays::parse(&self.api.relay_list().map_err(|e| fail("the relay list", e))?).map_err(|e| TunnelError::Failed(format!("mullvad: the relay list is not the shape tunnr-mullvad reads: {e}")))?;
        let relay = choose_relay(&list, &self.seed.location)?;
        let (device, address) = match &self.planned {
            Some((_, address, device)) => (device.clone(), *address),
            None => {
                let token_body = self.api.access_token(&account).map_err(|e| fail("the account", e))?;
                let token = serde_json::from_str::<serde_json::Value>(&token_body)
                    .ok()
                    .and_then(|v| v.get("access_token").and_then(|t| t.as_str()).map(str::to_string))
                    .ok_or_else(|| TunnelError::Failed("mullvad: the token answer has no `access_token`".into()))?;
                let req = RegisterDevice::new(self.exit_public.clone());
                let dev: RegisteredDevice = match self.api.register(&token, &req) {
                    Ok(body) => serde_json::from_str(&body).map_err(|e| TunnelError::Failed(format!("mullvad: the registration answer: {e}")))?,
                    // The box kept its exit key across a reboot: the device
                    // exists. Find it rather than registering another.
                    Err(e) if e.body.contains(PUBKEY_IN_USE) => {
                        let body = self.api.devices(&token).map_err(|e| fail("listing the account's devices", e))?;
                        let all: Vec<RegisteredDevice> = serde_json::from_str(&body).map_err(|e| TunnelError::Failed(format!("mullvad: the device list: {e}")))?;
                        all.into_iter().find(|d| d.echoes(&req)).ok_or_else(|| TunnelError::Failed("mullvad: the key is `in use` and no device of this account has it — another account holds it".into()))?
                    }
                    Err(e) => return Err(fail("registering this box's key", e)),
                };
                let address = dev.assigned_ipv4_for(&req).map_err(|e| TunnelError::Failed(format!("mullvad: {e}")))?;
                self.planned = Some((relay.hostname.clone(), address, dev.name.clone()));
                (dev.name, address)
            }
        };
        let spec = tunnr_mullvad::peers::tunnel_for_relay(self.exit_private, relay, self.seed.port, None).map_err(|e| TunnelError::Failed(format!("mullvad: {e}")))?;
        Ok(ExitPlan { relay: relay.hostname.clone(), address, device, spec })
    }

    fn raise_exit(&mut self, plan: &ExitPlan) -> Result<(), TunnelError> {
        let f = |e: std::io::Error| TunnelError::Failed(format!("{EXIT_IFACE}: {e} (CAP_NET_ADMIN?)"));
        let tun = TunDevice::create(EXIT_IFACE, MTU).map_err(f)?;
        let handle = dataplane::spawn(tun, &plan.spec).map_err(f)?;
        addr::assign_ipv4(EXIT_IFACE, plan.address.0, plan.address.1).map_err(f)?;
        addr::link_up(EXIT_IFACE).map_err(f)?;
        self.exit = Some(handle);
        Ok(())
    }
}

impl Tunnel for Mullvad {
    fn kind(&self) -> TunnelKind {
        TunnelKind::Mullvad
    }

    /// The MESH key: the one the box joins the estate with.
    fn public_key(&self) -> String {
        self.mesh.public_key().to_string()
    }

    /// The mesh first (the estate needs it; Mullvad is only the egress), then
    /// the exit, once. Idempotent.
    fn up(&mut self, me: &Identity, peers: &[Peer]) -> Result<(), TunnelError> {
        self.mesh.up(me, peers)?;
        if self.exit.is_none() {
            let plan = self.plan_exit()?;
            self.raise_exit(&plan)?;
        }
        Ok(())
    }

    fn add_peer(&mut self, peer: &Peer) -> Result<(), TunnelError> {
        self.mesh.add_peer(peer)
    }

    fn status(&self) -> TunnelStatus {
        let mesh = self.mesh.status();
        let exit = match (&self.exit, &self.planned) {
            (Some(h), Some((relay, (a, p), _))) => format!("exit {EXIT_IFACE} via {relay} as {a}/{p}, {}; the default route is the caller's", if h.handshaken() { "handshaken" } else { "no handshake yet" }),
            (None, Some((relay, _, _))) => format!("exit registered via {relay}, not raised"),
            _ => format!("exit not up ({})", if self.account.is_some() { "account fed" } else { "no account fed" }),
        };
        TunnelStatus { kind: TunnelKind::Mullvad, up: mesh.up && self.exit.is_some(), reachable: mesh.reachable, detail: format!("{} · {exit}", mesh.detail) }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::Cell;
    use std::rc::Rc;

    fn key() -> String {
        encode_key(&generate_keypair().1)
    }

    fn list(relays: &[(&str, &str, bool, u64)]) -> String {
        let rs: Vec<serde_json::Value> = relays
            .iter()
            .enumerate()
            .map(|(i, (host, loc, active, weight))| serde_json::json!({ "hostname": host, "location": loc, "active": active, "ipv4_addr_in": format!("185.195.233.{}", 70 + i), "public_key": key(), "weight": weight }))
            .collect();
        serde_json::json!({ "locations": { "se-sto": { "country": "Sweden", "city": "Stockholm" }, "se-got": { "country": "Sweden", "city": "Gothenburg" }, "de-fra": { "country": "Germany", "city": "Frankfurt" } }, "wireguard": { "ipv4_gateway": "10.64.0.1", "ipv6_gateway": "fc00:bbbb:bbbb:bb01::1", "relays": rs } }).to_string()
    }

    /// A fake Mullvad: counts registrations, can answer `PUBKEY_IN_USE`.
    struct Fake {
        relays: String,
        in_use: bool,
        registered: Rc<Cell<u32>>,
        echo_other_key: bool,
        /// A device of the account already holding this key (a kept key).
        kept: Option<String>,
    }

    impl MullvadApi for Fake {
        fn relay_list(&self) -> Result<String, ApiError> {
            Ok(self.relays.clone())
        }
        fn access_token(&self, account: &str) -> Result<String, ApiError> {
            if account == "1234123412341234" {
                Ok(r#"{"access_token":"mva_T","expiry":"2026-09-25T00:00:00Z"}"#.into())
            } else {
                Err(ApiError { status: Some(400), body: r#"{"code":"INVALID_ACCOUNT"}"#.into() })
            }
        }
        fn register(&self, token: &str, req: &RegisterDevice) -> Result<String, ApiError> {
            assert_eq!(token, "mva_T");
            self.registered.set(self.registered.get() + 1);
            if self.in_use {
                return Err(ApiError { status: Some(400), body: format!(r#"{{"code":"{PUBKEY_IN_USE}"}}"#) });
            }
            let pubkey = if self.echo_other_key { key() } else { req.pubkey.clone() };
            Ok(serde_json::json!({ "id": "d1", "name": "cuddly-lemming", "pubkey": pubkey, "ipv4_address": "10.67.114.172/32" }).to_string())
        }
        fn devices(&self, _token: &str) -> Result<String, ApiError> {
            let kept = self.kept.clone().unwrap_or_else(key);
            Ok(serde_json::json!([{ "id": "d0", "name": "other", "pubkey": key(), "ipv4_address": "10.64.0.9/32" }, { "id": "d1", "name": "kept-key", "pubkey": kept, "ipv4_address": "10.64.0.10/32" }]).to_string())
        }
    }

    fn fake(in_use: bool) -> (Box<Fake>, Rc<Cell<u32>>) {
        let n = Rc::new(Cell::new(0));
        (Box::new(Fake { relays: list(&[("se-sto-wg-001", "se-sto", true, 100), ("se-sto-wg-002", "se-sto", true, 300), ("se-got-wg-001", "se-got", true, 900), ("de-fra-wg-001", "de-fra", false, 5000)]), in_use, registered: n.clone(), echo_other_key: false, kept: None }), n)
    }

    fn driver(location: &str, account: Option<&str>, api: Box<Fake>) -> Mullvad {
        Mullvad::with_api(WireGuard::mint(None), None, MullvadSeed { location: location.into(), port: 51820 }, account.map(str::to_string), api)
    }

    #[test]
    fn shape_only_a_box_registers_its_exit_key_once_and_plans_the_heaviest_relay_in_its_location() {
        let (api, n) = fake(false);
        let mut m = driver("se-sto", Some("1234 1234 1234 1234"), api);
        let plan = m.plan_exit().unwrap();
        assert_eq!(plan.relay, "se-sto-wg-002", "the heaviest usable relay in se-sto");
        assert_eq!(plan.address, ("10.67.114.172".parse().unwrap(), 32));
        assert_eq!(plan.device, "cuddly-lemming");
        assert_eq!(plan.spec.peers.len(), 1);
        assert_eq!(plan.spec.peers[0].allowed_ips, vec![(Ipv4Addr::UNSPECIFIED, 0)], "an exit takes everything");
        assert_eq!(plan.spec.peers[0].endpoint.unwrap().port(), 51820);
        assert_eq!(&plan.spec.private_key, m.exit_private_key(), "the EXIT key, not the mesh key");
        assert_ne!(m.exit_public_key(), m.mesh().public_key(), "two identities: the estate's and Mullvad's");
        assert_eq!(Tunnel::public_key(&m), m.mesh().public_key(), "the box joins with its MESH key");
        assert!(!format!("{plan:?}").contains(&format!("{:?}", plan.spec.private_key)), "the plan never prints the private key");
        m.plan_exit().unwrap();
        assert_eq!(n.get(), 1, "planned again (a failed raise): no second device");
    }

    #[test]
    fn shape_only_a_country_code_picks_across_its_cities_and_an_inactive_relay_is_never_chosen() {
        let (api, _) = fake(false);
        let l = relays::parse(&api.relays).unwrap();
        assert_eq!(choose_relay(&l, "se").unwrap().hostname, "se-got-wg-001");
        assert_eq!(choose_relay(&l, "").unwrap().hostname, "se-got-wg-001", "the inactive de-fra relay is heavier and skipped");
        let e = choose_relay(&l, "de").unwrap_err().to_string();
        assert!(e.starts_with("REFUSED [tunnel-mullvad-unconfigured] no usable Mullvad relay in `de`"), "{e}");
        assert!(choose_relay(&l, "s").is_err(), "a country code is a whole prefix, not a letter");
    }

    #[test]
    fn shape_only_a_kept_key_already_registered_is_found_among_the_accounts_devices() {
        let (mut api, n) = fake(true);
        let kept = generate_keypair().0;
        api.kept = Some(encode_key(&wg_appliance_core::keys::public_from_private_bytes(&kept)));
        let mut m = Mullvad::with_api(WireGuard::mint(None), Some(kept), MullvadSeed { location: "se-sto".into(), port: 51820 }, Some("1234123412341234".into()), api);
        let plan = m.plan_exit().unwrap();
        assert_eq!((plan.device.as_str(), plan.address), ("kept-key", ("10.64.0.10".parse().unwrap(), 32)));
        assert_eq!(n.get(), 1);
        // a key "in use" that none of the account's devices has is refused
        let mut m = driver("se-sto", Some("1234123412341234"), fake(true).0);
        let e = m.plan_exit().unwrap_err().to_string();
        assert!(e.contains("another account holds it"), "{e}");
    }

    #[test]
    fn shape_only_no_account_a_bad_account_and_an_answer_for_another_key_are_refused_by_name() {
        let mut m = driver("se-sto", None, fake(false).0);
        let e = m.plan_exit().unwrap_err().to_string();
        assert!(e.starts_with("REFUSED [tunnel-mullvad-unconfigured]") && e.contains("mullvad_account"), "{e}");
        let me = m.mesh().identity(crate::OVERLAY_BOX_A);
        assert!(m.up(&me, &[]).is_err());
        assert!(!m.status().up && m.status().detail.contains("no account fed"), "{}", m.status().detail);
        let mut m = driver("se-sto", Some("0000"), fake(false).0);
        let e = m.plan_exit().unwrap_err().to_string();
        assert!(e.starts_with("REFUSED [tunnel-failed] mullvad: the account: HTTP 400") && !e.contains("0000"), "{e}");
        let (mut api, _) = fake(false);
        api.echo_other_key = true;
        let mut m = driver("se-sto", Some("1234123412341234"), api);
        let e = m.plan_exit().unwrap_err().to_string();
        assert!(e.contains("different device"), "{e}");
    }

    #[test]
    fn driver_for_builds_mullvad_from_the_seed_and_the_fed_account() {
        let (mut seed, _) = crate::seed::tests::a_seed(crate::seed::Kind::Gunnar);
        seed.tunnel = TunnelKind::Mullvad;
        seed.mullvad = Some(MullvadSeed { location: "se-sto".into(), port: 51820 });
        let d = crate::tunnel::driver_for(&seed, &Default::default()).unwrap();
        assert_eq!(d.kind(), TunnelKind::Mullvad);
        assert!(!d.public_key().is_empty(), "the mesh key, known before up");
        assert!(d.status().detail.contains("no account fed"));
    }

    /// LIVE (no account needed): the relay list from api.mullvad.net goes
    /// through this crate's HTTP client and the relay choice.
    #[test]
    #[ignore = "network: GET api.mullvad.net/app/v1/relays"]
    fn live_api_mullvad_net_relay_list_through_our_client_yields_a_relay_in_sweden() {
        let body = HttpApi::default().relay_list().expect("reached api.mullvad.net");
        let l = relays::parse(&body).expect("parses");
        let r = choose_relay(&l, "se").expect("a usable Swedish relay");
        assert!(r.hostname.starts_with("se-"), "{}", r.hostname);
        tunnr_mullvad::peers::tunnel_for_relay(generate_keypair().0, r, 51820, None).expect("a spec for the data plane");
    }
}