Back to the blog
Behind the scenesLevel 3 August 14, 2026 · 6 min

Why leave WireGuard?

The full route: WireGuard, a detour through Iroh, then a fork of Quinn.

Why WarrenGuard is not built on WireGuard, what QUIC gives the transport, and the three patches in our Quinn fork that matter well beyond a VPN.

by poka · Developer for Warren
Bulă, goggles pushed up, tunes a geared machine; on the wall the WireGuard and Iroh notes are crossed out, Quinn is ticked, and labels read BBR, QUIC, TLS, DATAGRAM
Contents

To write a VPN, the first thing you do is open the WireGuard page. It is small, it is audited, it is fast, it is in the Linux kernel. So that is where we started. Then we took another road, with a few mistakes along the way, one in particular; we ended up forking Quinn.

Here is the full route, with a few clear numbers. We are publishing it for whatever use you can make of it: even if you never touch Warren, you should come away better equipped. You might, for instance, be shipping a BBR bug to production without knowing it.

The white paper is far more detailed and complete. This article exists to make it more approachable, while still speaking to a technical reader. We are sharing all of it openly so that you can challenge it, test it, and keep hardening the choices we made.

#Three reasons, driven by our goals

We did not make this decision because of some major flaw in WireGuard. What we wanted to build simply led us elsewhere.

The signature. A bare WireGuard tunnel gets blocked or throttled on networks that filter UDP, or as soon as DPI recognises its signature. Making it stealthy means stacking an obfuscation layer on top, and therefore maintaining that layer forever, separately from the transport. And a stealth mode you have to switch on is a stealth mode you can forget to switch on.

The missing functions. End-to-end encrypted multi-hop, in-band port forwarding, defence against traffic analysis (DAITA). WireGuard offers no capability negotiation, no in-band control channel, no handshake extensibility. Each of those three functions would have to be grafted on top, with its own control channel and its own maintenance. Three grafts, three attack surfaces, three things to age together.

The double identity. A WireGuard tunnel's identity rests on a key specific to the tunnel, distinct from the user's cryptographic identity. Two identities to keep in sync, so two correlation surfaces. We only wanted one.

The three converge on the same conclusion, and it shaped every choice we made afterwards: the properties we need must not be grafted onto the transport, they have to be carried by the transport.

#An early mistake: Iroh

That reasoning leads naturally to QUIC.

Our first approach was to take Iroh, a P2P stack built on QUIC. On paper it is perfect: node identity already aligned on a public key, native NAT traversal and multipath.

In practice, what Iroh mostly brought was weight. Thousands of lines unused in our case. An API that moves every week. A class of NAT traversal bugs we never managed to solve. And a long tail of transitive dependencies.

The point that took us a while to name: in Warren's topology, a client talks to a known exit server. NAT traversal and multipath, Iroh's two headline functions, were of no use whatsoever. We were paying full price for features the app never called.

#The pivot, May 2026

Iroh dropped, Quinn adopted: the reference Rust implementation of QUIC, without the P2P layer on top. The migration took a few days.

The results:

  • throughput variability sharply reduced
  • binaries cut by more than half
  • dependency tree down from eighty crates to about twenty-five

The rule we took away from it applies well beyond a VPN: prefer a modest, understood foundation to an ambitious, hazy one. A dependency you do not fully master is a security debt as much as a technical one.

#What QUIC gives us

Five structural properties, and none of them is an option you have to remember to turn on.

Native stealth. The traffic blends into HTTP/3 on port 443. The obfuscation is the protocol itself.

An in-band control channel. Reliable streams, capability negotiation, an extensible handshake. Multi-hop and port forwarding live inside the protocol instead of being bolted alongside it.

A single identity. TLS 1.3 is built into the transport, and mutual authentication uses Raw Public Keys (RFC 7250), Ed25519, verified directly with no CA.

IP datagrams. IP packets travel 1:1 inside DATAGRAM frames (RFC 9221).

Connection migration. A move from Wi-Fi to Ethernet or to cellular is detected and absorbed.

On performance, on 10 GbE bare metal with an AMD EPYC Zen4, between two machines in the same datacentre, a single tunnel holds 5.5 to 7 Gbit/s, steadily. One server carries more than five thousand concurrent sessions at under ten percent CPU load. And with 2% loss injected into a flow, where a classic congestion controller collapses to a few megabits per second, BBR holds several hundred.

#Our fork of Quinn

The fork is here: github.com/WarrenBrowse/warren-quinn

It is published under MIT OR Apache-2.0, like upstream, with renamed crates (warren-quinn, warren-quinn-proto, warren-quinn-udp) so that nobody needs a [patch.crates-io]. Every divergence is isolated in a documented patch, and the fork is resynchronised whenever upstream moves.

Three deltas are worth the detour even outside a VPN.

The BBR bug. Two defects where an app-limited connection stays stuck in STARTUP and lets its cwnd grow without bound. In production we observed windows of half a gigabyte. Run Quinn with BBR on app-limited traffic and you can watch it happen. The patch follows the spec and is meant for upstream.

If you run Quinn with BBR on app-limited traffic, an idle tunnel for example, go and look at your cwnd before taking our word for it. The symptom is silent: nothing breaks, the window just inflates.

AQM on the datagram queue. FQ-CoDel (RFC 8289 and RFC 8290), per-flow queues, DRR scheduling, flow isolation. Queue heads whose sojourn time keeps the queue above target for a full interval get head-dropped. Without that, one greedy flow ruins the latency of every other flow in the tunnel.

A send buffer sized on the BDP. clamp(4 × EWMA(bw × min_rtt), 1 MiB, configured). On slow last miles, that drops queue depth from 16 MiB to roughly 1 MiB. That is bufferbloat removed, for free.

There is also GSO sizing (MAX_TRANSMIT_DATAGRAMS from 20 to 80, MAX_TRANSMIT_SEGMENTS from 10 to 40), socket buffer auto-sizing on Unix and Windows, and an Apple fast datapath.

#The MTU

Carrying IP packets inside QUIC DATAGRAM frames is easy. Carrying them without breaking half the internet is much less so.

What we do:

  • We start from a cautious floor of 1280 bytes, probe upwards when the native path allows it, and never go below 1200 bytes, the minimum QUIC guarantees end to end.
  • For TCP flows, MSS clamping on the SYNs, on both sides of the tunnel. Both endpoints size themselves without depending on ICMP, which settles the case of paths where ICMP is filtered and PMTUD drives into a wall.
  • For other flows, an oversized packet is dropped and its sender gets a real ICMP Fragmentation Needed on IPv4, or ICMPv6 Packet Too Big on IPv6, carrying the right size.
  • On the TCP fallback, where the stack can fragment on its own, PMTUD is disabled and the internal budget capped at 1100 bytes, the size that crosses that transport without falling into a blackhole.

#The handshake

The SNI and the start of the ClientHello are split across at least two padded UDP datagrams, with a padding floor on the Initial packet and a cap on the size of the first CRYPTO fragment.

This is a precise defence against a documented GFW SNI extractor, and against a passive extraction attack published in 2025. It is on by default, on every connection, with nothing to configure.

#warrenguard#quic#wireguard#quinn#bbr