OpenSnell

QUIC Proxy Mode

How OpenSnell's v5 QUIC envelope was reverse-engineered from real Surge HTTP/3 captures, and how the server interoperates with the official client.

QUIC proxy mode is the marquee v5 feature: it lets a Surge client send HTTP/3 traffic through a snell proxy without TCP fallback. The official Surge snell-server ships it; OpenSnell's server implements the same wire format, reverse-engineered from real captures.

The problem QUIC mode solves

Snell's TCP wire format is fine for CONNECT host:port over TCP. It breaks down the moment the application protocol is QUIC (RFC 9000), because:

  • QUIC is a UDP-based transport. You can't tunnel it through a TCP CONNECT without re-encapsulating, which destroys QUIC's loss-recovery and congestion-control assumptions.
  • HTTP/3, the dominant QUIC user, requires the SNI / target hostname to reach the destination so it can pick the right TLS cert. If the proxy just forwarded raw UDP packets, you'd leak the SNI on the wire to the proxy operator and to anyone on the path between client and proxy.

The v5 QUIC envelope solves both: the first UDP packet on a flow is wrapped in a snell-encrypted envelope that carries (host, port); every subsequent UDP packet in either direction is forwarded as raw QUIC, so QUIC's transport behaves as if the proxy weren't there.

Envelope layout (v5, client → server, first packet of a flow)

[salt(16B random)]
[AEAD-Seal(K, nonce=0, [0x04, 0, 0, padLen_be, payloadLen_be]) || 16B tag]
[padding(padLen)]
[AEAD-Seal(K, nonce=1, request_header || inner_QUIC_packet) || 16B tag]

request_header = [0x01, 0x01, 0x00, hostlen, host, port_be]
K              = Argon2id(psk_utf8, salt, 3, 8 KiB, 1, 32)[:16]
AEAD           = AES-128-GCM

The first frame is the same shape as the TCP v4/v5 frame header (type=0x04, padding-length, payload-length), just with payloadLen sized to cover the inner request header plus the wrapped QUIC Initial.

Server-side state machine

After OpenSnell's server decodes the first envelope:

  1. Verify the AEAD tag against K. Reject silently on failure (this makes brute-forcing the PSK indistinguishable from random UDP noise on the wire).
  2. Parse request_header to extract (host, port).
  3. Dial the upstream UDP socket (with the same egress-interface / ipv6 policy as TCP).
  4. Forward the inner QUIC Initial packet to the upstream as-is.
  5. Store (client_src_ip, client_src_port) → upstream_conn in an in-process map.

Every subsequent UDP packet:

  • From the client's (src_ip, src_port): look up the upstream, forward raw bytes (no snell framing, no crypto).
  • From the upstream toward the client: same map in reverse.

The map's TTL matches QUIC's idle-timeout convention; entries are garbage-collected after a few seconds of bidirectional silence.

How OpenSnell adapts to the official format

There's no published spec for the v5 QUIC envelope — only the deployed binary. The format above was reverse-engineered by:

  1. Running Surge against a known-PSK OpenSnell server, with HTTP/3 enabled on a known target.
  2. Capturing the resulting UDP traffic to the snell port with tcpdump.
  3. Decrypting the first packet with the PSK and AES-128-GCM, walking the byte layout until the inner payload's first 5 bytes matched the QUIC long-header pattern (1100 ????).
  4. Re-implementing the layout in Go and round-tripping live HTTP/3 traffic through it.

A real 1359-byte capture is committed as a fixture in components/snell/quic_test.go, so any future change to the envelope parser is regression-tested against a packet that genuinely came off the wire from Surge.

Why Argon2id?

The key-derivation function is Argon2id with parameters (t=3, m=8 KiB, p=1) — the exact same parameters the official server uses, derived from the PSK as a UTF-8 string (not base64). This matters because:

  • The KDF runs once per envelope (which is once per flow, not once per packet), so the 8 KiB memory cost is amortized away in real traffic — but it raises the brute-force cost per attempted PSK by several orders of magnitude compared to a single SHA-256 round.
  • Matching the parameters byte-for-byte is what makes interop work; any deviation produces an AEAD tag mismatch on packet 1 and the connection is silently dropped.

Adapting in both directions

OpenSnell's QUIC mode is unique in two ways relative to the official binary:

  1. Open AEAD path. The Go AES-GCM stdlib (crypto/aes + crypto/cipher) hits AES-NI on amd64 and ARMv8 crypto extensions on aarch64, so per-packet decode is hardware-accelerated and on par with the OpenSSL GCM module for x86_64 the official binary links to.

  2. Same upstream socket policy. egress-interface, ipv6 toggle, and dns = … all apply identically to QUIC upstream dials and TCP upstream dials — there's no second-class path. This lets you, for example, force HTTP/3 traffic out a specific WAN interface on a multi-homed server without separate UDP-specific config.

Limits

  • Only the server speaks QUIC. The Go client is SOCKS5-only.
  • QUIC mode only handles the snell envelope; OpenSnell does not parse any QUIC frames internally and does not multiplex. One flow per (client_src_ip, client_src_port) upstream, identical to how a stateful firewall would behave.
  • The server intentionally does not retry envelopes; if the first packet is dropped, Surge's QUIC stack handles the retransmit and the second copy of the envelope decrypts identically (same salt → same key → same tag).

On this page