//! **The relay-list parser against Mullvad's actual bytes.**
//!
//! Two halves, and the split between them is the honest part:
//!
//! * `captured_*` — `tests/fixtures/relays-2026-09-05.json`, three relay
//! objects lifted **verbatim** out of a 550 378-byte `GET
//! https://api.mullvad.net/app/v1/relays` on 2026-09-05, with the real
//! `port_ranges`, `shadowsocks_port_ranges` and both gateways. Runs offline,
//! for ever, and pins the shape we were written against.
//! * `live_api_mullvad_net_*` — the same parse against the endpoint, now.
//! Requires the network **and fails when it cannot reach it**, because a
//! test that skips itself when the wire is unavailable reports green having
//! proven nothing about a stranger's implementation, which is this crate's
//! entire job.
//!
//! Set `TUNNR_NO_NETWORK=1` to turn the live half off. It does not go quiet:
//! it fails with a message saying the proof was declined, so an offline run is
//! visibly an offline run rather than a pass.
//!
//! The endpoint is public and read-only: no account, no key, no request body,
//! nothing sent but a GET.
use tunnr_mullvad::{facts, relays};
const CAPTURED: &str = include_str!("fixtures/relays-2026-09-05.json");
#[test]
fn captured_bytes_parse_into_relays_with_every_field_we_rely_on() {
let list = relays::parse(CAPTURED).expect("the captured response parses");
assert_eq!(list.wireguard.relays.len(), 3, "the fixture holds three relays");
let sto = list
.wireguard
.relays
.iter()
.find(|r| r.hostname == "se-sto-wg-001")
.expect("se-sto-wg-001");
// Every field a peer is built from, asserted against the captured bytes.
assert_eq!(sto.location, "se-sto");
assert!(sto.active);
assert!(sto.owned);
assert_eq!(sto.provider, "31173");
assert!(sto.stboot);
assert_eq!(sto.ipv4_addr_in.to_string(), "185.195.233.76");
assert_eq!(sto.public_key, "MkP/Jytkg51/Y/EostONjIN6YaFRpsAYiNKMX27/CAY=");
assert!(sto.is_usable());
// A relay with `daita: true` in the bytes — the recently added field that
// a strict parser would have choked on.
let tia = list
.wireguard
.relays
.iter()
.find(|r| r.hostname == "al-tia-wg-001")
.expect("al-tia-wg-001");
assert!(tia.daita, "the captured bytes say daita:true");
assert_eq!(tia.provider, "iRegister");
assert!(!tia.owned, "rented, not owned — the two are different fields");
// The location table resolves, which is what relay selection by country
// depends on.
assert_eq!(list.locations["se-sto"].city, "Stockholm");
assert_eq!(list.locations["se-sto"].country, "Sweden");
assert_eq!(list.in_country("Sweden").count(), 2);
assert_eq!(list.in_country("sweden").count(), 2, "case-insensitive");
assert_eq!(list.in_country("Norway").count(), 0);
assert_eq!(list.in_location("se-sto").count(), 1);
}
/// The constants in [`facts`] are only worth anything if they still agree with
/// the bytes they were read from. This is the test that catches a hand-edit.
#[test]
fn captured_bytes_agree_with_every_constant_read_from_them() {
let list = relays::parse(CAPTURED).expect("parses");
assert_eq!(list.wireguard.ipv4_gateway, facts::IPV4_GATEWAY);
assert_eq!(list.wireguard.ipv6_gateway, facts::IPV6_GATEWAY);
assert_eq!(list.wireguard.port_ranges, facts::PORT_RANGES);
assert_eq!(
list.wireguard.shadowsocks_port_ranges,
facts::SHADOWSOCKS_PORT_RANGES
);
// …and the derived predicates agree with the parsed ranges, not merely
// with the constants — which is a different assertion and the one that
// would catch `is_relay_port` being written against the wrong list.
for &(lo, hi) in &list.wireguard.port_ranges {
assert!(facts::is_relay_port(lo), "{lo} is served");
assert!(facts::is_relay_port(hi), "{hi} is served");
}
}
/// RED-WHEN-BROKEN — the parser must refuse a relay missing a field a peer
/// cannot be built without, rather than yielding one with a default in it.
/// A relay with an empty key or `0.0.0.0` would build a peer that fails every
/// handshake and looks like a network fault.
#[test]
fn a_relay_missing_a_load_bearing_field_fails_the_parse() {
for missing in ["public_key", "ipv4_addr_in", "hostname", "active"] {
// Remove exactly one required field from the captured bytes.
let broken = strip_field(CAPTURED, missing);
assert_ne!(broken, CAPTURED, "the {missing} field was actually removed");
assert!(
relays::parse(&broken).is_err(),
"a relay with no {missing} must fail the parse, not default"
);
}
// The identity guard: unmodified bytes still parse, or every assertion
// above is satisfied by a parser that rejects everything.
assert!(relays::parse(CAPTURED).is_ok());
}
/// An *unknown* field, by contrast, must be tolerated: Mullvad add fields, and
/// a parser that refuses one turns their feature launch into our outage.
#[test]
fn an_unknown_field_does_not_break_the_parse() {
let with_new_field = CAPTURED.replacen(
r#""hostname":"se-sto-wg-001""#,
r#""some_future_feature":{"enabled":true},"hostname":"se-sto-wg-001""#,
1,
);
assert_ne!(with_new_field, CAPTURED);
let list = relays::parse(&with_new_field).expect("an unfamiliar key is not an error");
assert_eq!(list.wireguard.relays.len(), 3);
}
/// Remove the first `"<field>":<value>` pair from one relay object, keeping the
/// JSON otherwise well formed.
fn strip_field(json: &str, field: &str) -> String {
let needle = format!("\"{field}\":");
let Some(start) = json.find(&needle) else {
return json.to_string();
};
// Scan to the next top-level comma inside this object.
let bytes = json.as_bytes();
let mut i = start + needle.len();
let mut depth = 0i32;
let mut in_string = false;
while i < bytes.len() {
match bytes[i] {
b'"' if !(i > 0 && bytes[i - 1] == b'\\') => in_string = !in_string,
b'[' | b'{' if !in_string => depth += 1,
b']' | b'}' if !in_string => {
if depth == 0 {
break;
}
depth -= 1;
}
b',' if !in_string && depth == 0 => {
i += 1;
break;
}
_ => {}
}
i += 1;
}
format!("{}{}", &json[..start], &json[i..])
}
// ── the live half ───────────────────────────────────────────────────────────
/// **The live endpoint, now.** The captured fixture proves we parse what
/// Mullvad sent in September; this proves we parse what Mullvad is sending.
///
/// Fails rather than skips when the network is unavailable — see the module
/// docs. `TUNNR_NO_NETWORK=1` turns it off, loudly.
#[test]
fn live_api_mullvad_net_relay_list_parses_and_still_has_the_shape_we_rely_on() {
if std::env::var_os("TUNNR_NO_NETWORK").is_some() {
panic!(
"TUNNR_NO_NETWORK is set, so the only test in this crate that talks to \
Mullvad did not run. Nothing here has been proven against their live \
implementation on this run — that is what this failure says, and it says it \
instead of going quiet"
);
}
let body = match fetch(facts::RELAY_LIST_URL) {
Ok(b) => b,
Err(e) => panic!(
"could not reach {} ({e}). This test asserts interop with a stranger's \
implementation; unreachable means unproven, not fine. Set \
TUNNR_NO_NETWORK=1 to state deliberately that this run has no network",
facts::RELAY_LIST_URL
),
};
// Real bytes really arrived. The fetch is fast enough (≈0.15 s from oden
// on 2026-09-05, against 0.184 s for the same GET through curl) that
// "surely it did not have time" is a reasonable doubt, so the doubt is
// answered by an assertion instead of a stopwatch: an empty or truncated
// body cannot satisfy this.
assert!(
body.len() > 100_000,
"the live response was only {} bytes — 550 378 were measured on {}; a short body \
means a proxy, an error page, or a truncated read, not a proof",
body.len(),
facts::MEASURED_ON
);
let list = relays::parse(&body).unwrap_or_else(|e| {
panic!(
"the LIVE relay list no longer parses ({e}) — Mullvad changed the response and \
a peer built from our understanding of it would be wrong. First 400 bytes: {}",
&body[..body.len().min(400)]
)
});
// The facts this crate ships, re-read from the live response.
assert_eq!(
list.wireguard.ipv4_gateway,
facts::IPV4_GATEWAY,
"the in-tunnel gateway moved since {}",
facts::MEASURED_ON
);
assert_eq!(
list.wireguard.ipv6_gateway,
facts::IPV6_GATEWAY,
"the v6 gateway moved since {}",
facts::MEASURED_ON
);
assert_eq!(
list.wireguard.port_ranges,
facts::PORT_RANGES,
"the relay port ranges changed since {} — `facts::PORT_RANGES` is now wrong, and \
a config audit built on it will flag correct endpoints",
facts::MEASURED_ON
);
assert_eq!(
list.wireguard.shadowsocks_port_ranges,
facts::SHADOWSOCKS_PORT_RANGES,
"the Shadowsocks port ranges changed since {}",
facts::MEASURED_ON
);
// The list is a real list, not an empty success. An endpoint that answered
// `{"wireguard":{"relays":[]}}` would satisfy every assertion above.
let usable = list.usable().count();
assert!(
usable > 100,
"only {usable} usable relays — {} were measured on {}; either the response is \
truncated or our `is_usable` has started rejecting real relays",
facts::RELAY_COUNT_MEASURED,
facts::MEASURED_ON
);
// Every live relay must survive the conversion to a peer, or the seam is
// wrong for some shape the fixture does not contain.
for relay in list.usable() {
let peer = tunnr_mullvad::peer_for_relay(relay, 51820, None)
.unwrap_or_else(|e| panic!("live relay {} does not convert: {e}", relay.hostname));
assert_eq!(
wg_appliance_core::keys::encode_key(&peer.public_key),
relay.public_key
);
}
}
/// A minimal GET. `ureq` is a **dev**-dependency: the TLS it brings links C,
/// which is fine in a test binary and is exactly why this crate's own
/// transport belongs to the caller.
fn fetch(url: &str) -> Result<String, String> {
let mut response = ureq::get(url)
.header("User-Agent", "tunnr-mullvad interop test (codeberg.org/nordisk/tunnr)")
.call()
.map_err(|e| e.to_string())?;
if response.status() != 200 {
return Err(format!("HTTP {}", response.status()));
}
response
.body_mut()
.with_config()
.limit(16 * 1024 * 1024)
.read_to_string()
.map_err(|e| e.to_string())
}