[package]
name = "tunnr-mullvad"
version = "0.2.0"
description = "Mullvad interop for WireGuard: the public relay list, the device-registration shapes, the measured facts about their network (address range, in-tunnel resolvers, the five relay port ranges), the Mullvad-shaped config audit, and BOTH KEMs of their quantum-resistant pre-shared key (ML-KEM-1024 + HQC-256). Produces a peer set for wg-appliance-core's data plane and contains no packet code, no HTTP client and no place to put a secret. Its facts and audit are `default-features = false`, dependency-free pure std, so a distroless PID 1 can take them for nothing."
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://codeberg.org/nordisk/tunnr-mullvad"
keywords = ["wireguard", "mullvad", "vpn", "interop"]
categories = ["network-programming"]
# THE POINT OF THIS SPLIT, and it is not tidiness.
#
# tunnr's `wg-appliance` is PID 1 in a distroless image, and its provider module
# says in its own docs that it adds "no HTTP client, no TLS, no new dependency on
# the distroless PID 1". Moving the Mullvad facts out of that payload and into
# this crate must not quietly break that promise — a `serde` and an `ml-kem` in
# an appliance's init would be a real cost paid for a refactor.
#
# So `default-features = false` is the WHOLE audit surface — `net`, `facts`,
# `audit` — and it is pure `std` with zero dependencies. Everything that costs
# something is a feature the host-side provisioning tool turns on.
[features]
default = ["relays", "peerset", "pq"]
# Parsing the relay list and the device-registration shapes. Costs serde.
relays = ["dep:serde", "dep:serde_json"]
# Producing the appliance's own `PeerSpec`/`TunnelSpec`. Costs the data plane
# crate (and, through boringtun, `ring`).
peerset = ["dep:wg-appliance-core", "relays"]
# Both KEMs of the post-quantum pre-shared key. Costs ml-kem + hqc-kem + rand.
pq = ["dep:ml-kem", "dep:hqc-kem", "dep:rand"]
[dependencies]
# THE SEAM. `PeerSpec`/`TunnelSpec` are the appliance's own types and this
# crate imports them rather than declaring a lookalike: two providers must
# never become two tunnel implementations. `default-features = false` drops
# the hub fixture (a UDP test responder), which has no business in anything
# that links this.
# PATH, not registry: edda is the sibling checkout every other tunnr crate reaches
# for this crate through, and two sources of `wg-appliance-core` would be two
# distinct `PeerSpec` types — the exact failure this workspace has been bitten by
# before. The `version` stays so a published copy still resolves.
wg-appliance-core = { path = "../edda/crates/wg-appliance-core", version = "0.1.3", default-features = false, optional = true }
# The relay list and the registration shapes are JSON. Pure Rust, permissive,
# and no TLS or HTTP comes with them — the transport is deliberately the
# caller's, so nothing here links C on this account and nothing here can hold a
# credential.
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
# ML-KEM-1024 (FIPS 203), pure Rust, no `-sys` crate anywhere in its own tree.
# One HALF of Mullvad's post-quantum construction.
ml-kem = { version = "0.3", features = ["getrandom", "zeroize"], optional = true }
# HQC-256, the OTHER half — and it is pure Rust after all.
#
# ⚠ CORRECTION, 2026-09-05 evening. This crate shipped saying "there is no
# pure-Rust HQC-256, `pqcrypto-hqc` wraps PQClean's C", and `pq::HqcGap` existed
# because of it. That was WRONG: it was inherited from a survey taken before
# these crates were published, and repeated three times without being checked.
# MEASURED against crates.io the same evening — four candidates, and NONE of
# them builds C:
#
# hqc-kem 0.1.0-rc.0 27 crates 0 C NIST .rsp vectors for 128/192/256
# backbone-hqc 0.2.0 31 crates 0 C no vector file
# lib-q-hqc 0.0.11 37 crates 0 C no vector file
# pqc-kem 0.2.0 63 crates 0 C no vectors, 0 mention of constant time
# (and 2089 lines for FIVE algorithms)
#
# `hqc-kem` is taken because it is the only one that can be CHECKED: it ships
# the official `.rsp` known-answer files and tests all three levels against
# them. Run here on 2026-09-05: `test_hqc256_kat ... ok` over 100 vectors, plus
# `implicit_rejection_256` and `stack_budget_256`. RED-PROVEN, because a KAT
# file that exists is not a KAT that is read: corrupting one `ss` line in
# `kat/hqc-5.rsp` turns that test FAILED, restoring it turns it green.
#
# Its 6 `unsafe` blocks are all PCLMULQDQ carry-less-multiply intrinsics for the
# GF(2) polynomial product — the expected use, behind a runtime feature check.
# Its `DecapsulationKey` and `SharedSecret` do NOT derive `Debug` and both
# `Zeroize` on drop, which is more care than this constellation's own crate took
# until `wg-appliance-core 0.1.3` earlier the same evening.
#
# It is an `-rc` release. That is the honest risk here — not licence (MIT OR
# Apache-2.0), not C, not copyleft: an implementation young enough that nobody
# has audited it. `pq::derive_psk` still refuses, so nothing ships a PSK from it.
hqc-kem = { version = "0.1.0-rc.0", optional = true }
# `hqc256::generate_key` takes an RNG rather than reaching for one itself, which
# is the right shape — so the caller must supply `rand::rng()`. Pure Rust.
rand = { version = "0.10", optional = true }
[dev-dependencies]
# THE LIVE PROBE ONLY. `ureq` brings TLS, and TLS on today's Rust brings C
# (`ring` or `aws-lc-sys`). That is acceptable in a DEV dependency — it is
# linked into the test binary and never into anything that ships — and it is
# exactly why the crate's own transport is the caller's. If this line ever
# moves to `[dependencies]`, a host-side tool's choice becomes an appliance's
# build requirement.
ureq = "3"
[lints.rust]
missing_docs = "warn"