OpenSnell

Snell v6

What Snell v6 changed over v5 — the PSK-derived protocol profile, per-frame traffic shaping, and config changes. Fully reverse-engineered and reimplemented in Go, byte-exact with the official server, and kept closed-source.

Snell v6 (snell-server v6.0.0b1 / b2 / b3 / b4) is the latest generation of Surge's proxy protocol. We have fully reverse-engineered it and reimplemented it in Go — client and server, through the current v6.0.0b4 beta — with emission that is byte-for-byte identical to the official server on the wire.

The OpenSnell v6 implementation is closed-source and will not be published. Snell v6's entire purpose is anti-fingerprinting, and keeping a byte-exact emitter unpublished is consistent with that goal. OpenSnell's open-source build stays v4 / v5 (GPLv3); to run v6 today, the installer can deploy the official Surge snell-server v6.0.0b4.

What stayed the same

The inner request/response protocol and the cryptography are unchanged from v4/v5:

  • KDF — Argon2id (t=3, m=8 KiB, p=1); the key is the first 16 bytes.
  • AEAD — AES-128-GCM (12-byte little-endian counter nonce, 16-byte tag).
  • Inner framing — the same 7-byte plaintext header, the same address encoding, the same multi-frame streaming in both directions.

Snell v6 does not use ChaCha20-Poly1305. The cipher is the same AES-128-GCM Snell has used since v4. The official binary statically links the whole of OpenSSL, so it contains ChaCha20-Poly1305 as unused library code — but the protocol uses AES-128-GCM, which is exactly why an AES-128-GCM implementation interoperates with it byte-for-byte. (AES-128-GCM and ChaCha20-Poly1305 produce completely different ciphertext/tags from the same key, nonce, and plaintext, so a ChaCha server could never decrypt an AES-GCM client's frames.)

What v6 added: a PSK-derived "protocol profile"

v6's headline feature is deployment-level protocol diversity. Every deployment derives a unique protocol profile from its PSK alone — no negotiation, and no version byte on the wire. Two servers with different PSKs speak measurably different-looking wire formats, which is what makes v6 hard to fingerprint as a class. The profile drives three new outer-layer behaviours, all keyed off the PSK:

1. Salt scatter

In v4/v5 the 16-byte AEAD salt is sent verbatim at the start of each direction. In v6 the salt is scattered into a larger, PSK-sized region: a PSK-derived permutation places the 16 salt bytes at pseudo-random offsets, XOR-masked with a PSK-derived keystream. The receiver runs the inverse permutation to recover the salt. There is no contiguous salt on the wire.

2. Per-frame header-AAD sizing

Each frame's sealed header is authenticated with a variable-length random "header-AAD" prefix, whose length is a PSK-derived function of the frame index. The length sequence H(0), H(1), H(2), … is deterministic per deployment, so the header offsets drift frame-to-frame in a PSK-specific pattern.

3. Per-frame traffic shaping

This is the big change. On top of the sealed payload, each frame carries:

  • Padding — a PSK-derived amount of filler, sized so the frame reaches a per-deployment "write target". Small frames are topped up to plausible sizes; large frames pass through. The exact target is itself PSK-derived and varies per frame.
  • A padding↔ciphertext interleave ("swap") — padding bytes and ciphertext bytes are exchanged according to one of several PSK-derived modes (a block swap, or a periodic single-byte swap with a keyed per-round phase). The receiver inverts the swap before it authenticates the frame.

The net effect: frame sizes and byte distributions are randomised per deployment, defeating the size/timing fingerprints that worked against the fixed v4/v5 framing.

Wondering whether this actually makes you safer against the Great Firewall? See v5 vs v6 against the GFW for an honest, axis-by-axis answer — what the per-deployment diversity buys you, and what it does not.

b1 → b2 → b3 → b4

The betas evolved the internals and runtime more than the recipe:

b1b2b3b4
Profile-derivation hashkeyed BLAKE2bfast integer hash (wyhash + splitmix64)unchanged from b2unchanged from b3
Server runtimesingle-threaded libuvmulti-core (SO_REUSEPORT + io_uring)unchanged from b2unchanged from b3
Linkingdynamic (libsodium / libuv / OpenSSL 1.1)static (no extra shared libs)staticstatic
PackagingUPX-packed (~1.25 MB)unpacked (~2.85 MB, UPX shell removed)
Throughputone core saturates (~30–42 MB/s)scales across cores (~8.5× b1)same as b2 in default modesame as b3
TCP Fast Openenabled by default (server setsockopt(TCP_FASTOPEN))unchanged from b3
Encryption modeshaped onlyshaped onlymode directive (default / unshaped / unsafe-raw)unchanged from b3

v6.0.0b2 was wire-incompatible with b1 (the per-parameter hash changed). b3's default mode is the same shaped wire as b2 — its headline addition is the mode directive plus TFO-on-by-default.

v6.0.0b4 is a binary-only release: it refreshes all dependency libraries, fixes an internal robustness issue in UDP-forwarding mode (the wire-observable UDP behaviour is byte-identical to b2/b3), and removes the UPX shell so the binary now ships unpacked (~2.85 MB) to avoid extraction failures on certain servers — it is still statically linked. b4 is wire-identical to b3 (crypto, frame structure, padding/swap shaping and the mode selector are all unchanged), so OpenSnell interoperates with it unchanged and reports v6.0.0b4 as the latest v6.

So version = 6 should track the latest beta (b4); pinning b1/b2 selects an older, wire-incompatible build.

The mode directive (b3)

b3 adds a per-deployment encryption mode that must match between client and server (an empty/absent mode means default, matching the official server):

modeWhat's on the wireUse it when
defaultPSK-derived shaping + AES-128-GCM (the full v6 obfuscated wire)Default. Anti-fingerprinting matters (e.g. behind the GFW).
unshapedAES-128-GCM only, no PSK-derived shaping (~10% more throughput)The wire looks like plain AEAD (≈ v3); shaping isn't needed and you want the throughput.
unsafe-rawCleartext — no encryption, no obfuscationTrusted intranet or nested inside another encrypted tunnel only.

unsafe-raw ships your traffic in the clear. Only use it where another layer already provides confidentiality (a nested tunnel, a private link). unshaped still encrypts with AES-128-GCM but drops the anti-fingerprinting shaping, so it is identifiable as a fully-random encrypted stream just like bare AEAD proxies.

OpenSnell's Go client and server implement all three b3 modes byte-exactly; the unshaped and unsafe-raw paths simply skip the PSK-derived profile derivation since they carry no shaping.

Config changes (v5 → v6)

v5v6
obfs = off / http / tlsremoved (the v6 server ignores it)
ipv6 = true / falsedns-ip-preference = default | prefer-ipv4 | prefer-ipv6 | ipv4-only | ipv6-only
PSK any lengthPSK must be 16–255 bytes
mode = default | unshaped | unsafe-raw (b3, optional; defaults to default)
[snell-server]
listen = 0.0.0.0:8443
psk = a-16-to-255-byte-secret
dns-ip-preference = default
mode = default

Client side, bump the version (and set a matching mode if you changed it on the server):

ProxyName = snell, 1.2.3.4, 8443, psk = a-16-to-255-byte-secret, version = 6

What we built and validated

Against the unmodified official snell-server v6.0.0b4/b3 (and b2 in default mode, which shares the same shaped wire):

  • Byte-exact emission. Our per-frame padding length matches the server's on every frame — 100% across 24 PSKs spanning every swap-mode and write-mode, for small and large frames alike — and full HTTP fetches plus multi-hundred-KB echoes interoperate for every PSK. On the wire, our traffic is indistinguishable from the official server's.
  • All three b3 modes. default, unshaped, and unsafe-raw each interoperate with the official b3 binary in both directions.
  • Performance on par with the hand-tuned C server. The frame hot path is allocation-free — each frame is built in one reused per-connection buffer with in-place AEAD sealing — so at the same throughput the Go server's per-byte CPU matches official b3, with lower memory.

Running v6 today

OpenSnell's open-source distribution stays v4/v5. For v6, the one-line installer can deploy the official Surge snell-server v6.0.0b4 (a statically-linked binary — no extra shared libraries):

bash <(curl -fsSL https://s.ee/opensnell)
# choose option 3 — Surge official snell-server v6.0.0b4

OpenSnell's own client can speak the v6 wire byte-for-byte, but — like the v6 server — that path is not part of the open-source build.

On this page