ADR 047: iroh QUIC replaces the bespoke populi mesh transport
ADR 047: iroh QUIC replaces the bespoke populi mesh transport
Section titled “ADR 047: iroh QUIC replaces the bespoke populi mesh transport”Status
Section titled “Status”Accepted (2026-09-04).
(Numbering note: drafted as ADR-046, renumbered on merge with origin/main,
which had taken 046 for Pareto-frontier reporting. Commit messages and the plan
predating the merge still say 046.)
- Supersedes ADR-008 (Mens transport) and ADR-020 (default transport posture). ADR-020 §Decision.1 requires this ADR by name and pre-authorises the move by naming QUIC as the future option, so this is the ADR it asked for.
- Partially supersedes ADR-017: the grant protocol only. The duplicate-execution guard survives — it is a scheduling invariant, not a transport concern.
- Upholds ADR-018. The GPU/CPU capability truth layer is the part vox actually contributes and is unchanged by this decision.
Context
Section titled “Context”vox populi’s mesh has been an HTTP control plane with a hand-rolled ed25519
envelope and a JWT auth matrix, none of it going through vox-crypto. As of
60d51c520 the daemon starts and curl /health returns 200 — but it binds
loopback, and that bind is the only thing containing an unauthenticated
FullAccess path that writes posted bytes to a temp file, chmod 0755s them,
and executes them with no timeout (fix F2 in the cross-machine handoff). There
is no cross-machine transport at all.
Building the missing half by hand means writing NAT traversal, hole punching, path migration, key-based addressing, and a congestion-controlled stream protocol. That work is precisely what iroh already vendors.
Decision
Section titled “Decision”Adopt iroh 1.1 (pinned; noq QUIC, not quinn) as the mesh transport,
together with iroh-tickets 1.0 for pairing and iroh-mdns-address-lookup for
LAN discovery. The EndpointId is the ed25519 public key, so transport and
identity are the same fact.
Non-negotiables carried from the design:
- Never
presets::N0, neverN0DisableRelay, neverinto_0rtt(). One detector, three patterns. presets::Minimalonly, withdefault-features = false.- Trust is checked at accept and per request, against
~/.vox/mesh_trust.jsonkeyed byEndpointId— nottrusted_nodes.json, which is keyed by anode_idfrom a different keyspace. - Mesh-received work is sandboxed by default. Pairing grants reachability, never native execution.
iroh-blobsandirpcare out of v1. A bi-stream plus postcard is four lines; blobs earn their place for model and checkpoint distribution, not for 10 MB payloads on a 3 ms LAN.
Deletion happens last — Phase 6, after the two-machine demo passes, and only
once the A2A mailbox, federation directory, queue stats, and PopuliHttpOp are
ported.
Consequences
Section titled “Consequences”Measured, not assumed
Section titled “Measured, not assumed”The Task 0.2 spike ran between a macOS and a Windows machine on f48dbc810;
full numbers in iroh spike findings.
- Zero third-party contact under
Minimal.Builder::empty()is documented as “no address lookup services, andRelayMode::Disabled”, andMinimal::applysets onlycrypto_provider.N0is what adds pkarr, DNS lookup, and relays. The listener advertised IP addresses and no relay URL. default-features = falsealso removes the portmapper, so there is no UPnP/SSDP multicast and therefore no macOS firewall dialog. Recommended feature set:["tls-ring", "fast-apple-datapath"].- Direct LAN connect in 12.8 ms with no relay; 300 MiB transferred.
- Placement telemetry is viable. Differentiating
udp_tx.bytesover 200 ms windows lands within 3.6 % of wall-clock truth at a coefficient of variation of 14.6 % — ample for a roughly 2× “is shipping worth it” decision.cwndis reachable only viacongestion_state(), which documents itself as debug-only, so the bandwidth-delay-product shortcut is closed. - Cost: 55 marginal crates (+3.8 %) and 71.95 s wall / 245.83 s user CPU
on a clean release build — an over-estimate, since it recompiles
tokio,serde,rustlsandhyperthat the workspace already builds. This is inside the ~90 s threshold the operator’s authorisation was conditional on.
Accepted costs
Section titled “Accepted costs”- Crate count goes up; hand-written security-critical code goes down. Trading roughly 7 000 first-party lines — including the layer that produced the RCE — for vendored, tested ones makes the literal dependency metric worse and the real one better.
- Two
ed25519-dalekmajors coexist.vox-cryptopins 2.x, iroh resolves 3.0.0. They cannot share a Rust type, but both round-trip through 32 raw bytes, so one stored seed derives both identities. Do not unify them: the mesh key must start headless whileNodeIdentityis password-sealed, so fusing them either breaks unattended start or unseals the signing key. iroh-mdns-address-lookupis pre-1.0 and must be pinned with=. It is also, as of 2026-09-05, wired but non-functional: it binds UDP 5353 and never announces, so discovery-by-EndpointIdfails and ticket-based pairing is the only working path. This ADR does not promise mDNS discovery. See the spike findings §Q4.
Traps this ADR exists to record
Section titled “Traps this ADR exists to record”Endpoint::bind(preset)takes no ALPNs. A server built that way refuses every connection at ALPN negotiation — silently, not as a compile error. UseEndpoint::builder(preset).alpns(...).ep.online().awaithangs forever underMinimal. Its contract is “has contacted a relay server”, and there is no relay. Never put it on this path.send.finish()does not flush. It signals end-of-stream; dropping theConnectionimmediately afterwards closes the connection before the bytes reach the wire, and the peer seesclosed by peer: 0with no payload. The responder must awaitconn.closed()or equivalent. This cost the spike a debugging cycle and will recur in the real handler.- Set
NetReportConfig::minimal()explicitly. Its defaults enable HTTPS latency probes and captive-portal checks; those are inert today only because the relay map is empty.
Alternatives rejected
Section titled “Alternatives rejected”- Keep the HTTP plane and add TLS + real auth. Does not address NAT traversal or hole punching, which is the actual missing capability, and leaves the hand-rolled envelope and JWT matrix in place.
- Depend on Tailscale. Rejected: the design must contact no third party, and the mesh has to work with both machines off the internet.
libp2p. Heavier, and its QUIC story is a subset of what iroh provides with a worse identity model for this use case.