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:
| b1 | b2 | b3 | b4 | |
|---|---|---|---|---|
| Profile-derivation hash | keyed BLAKE2b | fast integer hash (wyhash + splitmix64) | unchanged from b2 | unchanged from b3 |
| Server runtime | single-threaded libuv | multi-core (SO_REUSEPORT + io_uring) | unchanged from b2 | unchanged from b3 |
| Linking | dynamic (libsodium / libuv / OpenSSL 1.1) | static (no extra shared libs) | static | static |
| Packaging | — | — | UPX-packed (~1.25 MB) | unpacked (~2.85 MB, UPX shell removed) |
| Throughput | one core saturates (~30–42 MB/s) | scales across cores (~8.5× b1) | same as b2 in default mode | same as b3 |
| TCP Fast Open | — | — | enabled by default (server setsockopt(TCP_FASTOPEN)) | unchanged from b3 |
| Encryption mode | shaped only | shaped only | mode 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):
mode | What's on the wire | Use it when |
|---|---|---|
default | PSK-derived shaping + AES-128-GCM (the full v6 obfuscated wire) | Default. Anti-fingerprinting matters (e.g. behind the GFW). |
unshaped | AES-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-raw | Cleartext — no encryption, no obfuscation | Trusted 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)
| v5 | v6 |
|---|---|
obfs = off / http / tls | removed (the v6 server ignores it) |
ipv6 = true / false | dns-ip-preference = default | prefer-ipv4 | prefer-ipv6 | ipv4-only | ipv6-only |
| PSK any length | PSK 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 = defaultClient 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 = 6What 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, andunsafe-raweach 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.0b4OpenSnell'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.
Performance
Where the Go implementation stood vs. the official C/libuv binary, what we changed, and where we land today.
v5 vs v6 against the GFW
Is Snell v6 actually safer than v5 against the Great Firewall? An honest, axis-by-axis answer — v6's per-deployment diversity defeats protocol-class fingerprinting, but it shares v5's non-TLS / no-replay fundamentals.