OpenSnell

Snell vs Shadowsocks 2022

A protocol-level comparison of Snell v4/v5 and Shadowsocks 2022 (SIP022) — what each defends against, what each leaks on the wire, and which threat model picks which.

This page is an investigation, not advocacy. OpenSnell implements Snell — that's our bias up front. The findings below try to be honest about where Snell loses, with line-level citations into both codebases so any claim is verifiable.

The two protocols compared:

  • Snell v4 / v5 — Surge's proprietary proxy protocol, re-implemented in OpenSnell from components/snell/v4.go and cipher.go. v4 and v5 share the same TCP frame format — we use "Snell" to mean either.
  • Shadowsocks 2022 / SIP022 — the AEAD-2022 protocol family (specification SIP022), read from shadowsocks-rust's crates/shadowsocks/src/relay/{tcprelay,udprelay}/aead_2022.rs. We do not cover the older SIP004 AEAD scheme.

TL;DR

DimensionWinner
Cryptographic protocol designShadowsocks 2022
Surviving the GFW with no external wrapperSnell
Surviving the GFW with a modern wrapperShadowsocks 2022 + Reality / shadow-TLS
Recovering plaintext from a capture (no PSK)Tie — both unbreakable
Identifying the protocol from a captureSee § The high-entropy paradox — the question splits in two
Multi-user / key rotationShadowsocks 2022 (EIH)
Implementation maturity / auditsShadowsocks 2022

The verdict is contextual. There is no axis on which one protocol is strictly better than the other.

How to read this page

"Secure" and "uncensorable" use the same word for different things. Each finding below is tagged with which adversary it applies to:

  • Passive censor. Logs traffic, applies offline classifiers. The GFW circa 2010.
  • Active censor. Probes flagged endpoints, attempts replay, fingerprints handshakes. The GFW since ~2020.
  • Network observer / researcher. Captures pcaps and tries to identify what protocol is in use. No interaction with the server.
  • Cryptographic adversary (no PSK). The standard threat model. What can they do with ciphertext alone?
  • Adversary with PSK. Has somehow obtained the pre-shared key. What can they still not do?

The two protocols make very different tradeoffs across these.

First packet on the wire

A side-by-side of what the very first TCP segment looks like for each:

Snell (first frame)

+-------------+----------------------------+----------------------+----------------------+
| salt (16B)  | AEAD(header_7B) + tag(16B) | padding(256..511 B)  | AEAD(payload) + tag  |
+-------------+----------------------------+----------------------+----------------------+
| plaintext   | header_7B = [0x04, 0, 0,   | bytes at *even*      | first payload is the |
|             |   padLen_be, payloadLen_be]| indices SWAPPED with | CONNECT request:     |
|             |                            | even-indexed payload | [ver, cmd, cid_len,  |
|             |                            | ciphertext bytes —   |  cid, hostlen, host, |
|             |                            | padding never        |  port_be]            |
|             |                            | contiguous on wire   |                      |
+-------------+----------------------------+----------------------+----------------------+

The padding bytes are picked by makeV4Padding to push the overall ones/zeros ratio of salt + padding + payload toward 1.6 or 0.4 — explicitly away from the 1.0 ratio that uniform-random AEAD output produces. The first segment is shaped to not look like high-entropy noise.

Shadowsocks 2022 (first request)

+-------------+--------------------------------+-----------------------------------+
| salt        | AEAD(fixed_header) + tag(16B)  | AEAD(variable_header) + tag(16B)  |
| (16 or 32B) | fixed_header (11B) =           | variable_header =                 |
|             |  [type=0, ts_be(8), len_be(2)] |  [ATYP, addr, port_be,            |
|             |                                |   padLen_be(2), padding(0..900),  |
|             |                                |   payload]                        |
+-------------+--------------------------------+-----------------------------------+
| plaintext   | exactly 27 ciphertext bytes    | uniform random bytes throughout   |
+-------------+--------------------------------+-----------------------------------+

The padding is uniform random. There is no statistical shaping. From byte 0 onward (after the salt prefix), every byte is indistinguishable from /dev/urandom output.

Cryptographic primitives

Key derivation

PropertySnellShadowsocks 2022
KDFArgon2id(t=3, m=8KiB, p=1)BLAKE3::derive_key
InputsPSK string + 16 B saltPSK bytes + per-stream salt
PSK assumptionArbitrary string (weak OK)High-entropy 16 / 32 B (openssl rand -base64 16)
Cost per connectionOne Argon2 call (~ms, 8 KiB RAM)One BLAKE3 call (~µs)

The choice reflects a different threat model assumption:

  • Snell assumes the operator might pick a weak PSK like mypassword123. Argon2id makes that PSK expensive to brute-force from a captured handshake. It costs ~1 ms and 8 KiB of memory per new TCP connection on the server.
  • Shadowsocks 2022 assumes the PSK is already a uniformly random 16- or 32-byte key. Under that assumption, Argon2 buys nothing — you can't brute-force a 128-bit random key regardless of how slow the KDF is. The spec explicitly forbids the EVP_BytesToKey password expansion that Shadowsocks AEAD-2017 used.

Neither approach is wrong. They're optimized for different operator populations.

AEAD

PropertySnellShadowsocks 2022
CipherAES-128-GCM onlyAES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 (+ optional ChaCha8)
Nonce12 B counter, init 0, per-AEAD-op increment12 B counter, init 0, per-AEAD-op increment
Max plaintext / chunk0x3FFF (16383)0xFFFF (65535)

Both use the standard counter-nonce pattern, which is safe for AES-GCM provided the key is fresh (which it is — derived from a per-stream random salt).

Algorithm agility is where the gap shows: Shadowsocks 2022 supports ChaCha20 for ARM / embedded systems without AES-NI hardware acceleration, and AES-256 for operators with a stronger key requirement. Snell is hard-coded to AES-128-GCM.

The 4× larger chunk size (65535 vs 16383) cuts framing overhead proportionally for bulk transfers — relevant on long-haul links where goodput matters.

Protocol-level defenses

This is where the gap is widest, and where Shadowsocks 2022 unambiguously wins.

Replay protection

DefenseSnellShadowsocks 2022
Per-stream salt
Server salt cache✅ (60 s)
Timestamp window✅ (30 s, aead_2022.rs:377-379)

What an attacker can do against Snell, without ever decrypting:

  1. Capture a complete client-initiated TCP session.
  2. Replay the exact byte sequence to the same server hours later.
  3. The server will Argon2-derive a fresh session key from the salt (the salt is in the capture), accept every frame's GCM tag (they verify, since the replayed nonce sequence matches), execute the CONNECT to whatever host was originally requested, and faithfully reproduce the original session — possibly fetching the same URL, re-triggering side effects on the upstream.

This is a real protocol-level weakness in Snell. Shadowsocks 2022 catches the same replay because (a) the salt is in the 60-second cache during the legitimate session and (b) the embedded timestamp will be well outside the ±30 s window on replay.

Direction binding

DefenseSnellShadowsocks 2022
Type byte in header (0=client/1=server)

Shadowsocks 2022 puts a type byte at the start of every AEAD'd header chunk: 0 for client→server, 1 for server→client. The receiver checks it (aead_2022.rs:367-373). An attacker can't take a server-side ciphertext and feed it back to the server as if it were client traffic.

Snell has no such field. The first byte of the AEAD'd header is a fixed 0x04 (version constant), which is identical in both directions. The two streams are still cryptographically separated because each direction has its own salt → its own key → its own nonce counter, so a swap would fail tag verification. But the defense is implicit (from the key separation) rather than explicit (from a domain separator), which historically has been the source of cross-protocol attacks.

Request → response binding

Shadowsocks 2022 server responses embed the client's request salt inside the encrypted response header (aead_2022.rs:382-387). The client checks it before trusting the response. This binds every response to its specific request — preventing an active attacker who controls the network from splicing one server's response to a different client's request.

Snell has no equivalent.

Multi-user / identity

Shadowsocks 2022 ships Extensible Identity Headers (EIH, SIP022-2). A single server can hold N user keys; each request carries an encrypted hash that identifies which user it's from. This lets you:

  • Run one shared server for many users without one user's PSK compromise affecting the others.
  • Rotate keys without downtime by adding a new user, migrating clients, then removing the old one.

Snell has a single PSK. One leak → everyone re-keys.

Anti-censorship behavior

This is the axis where Snell catches up — and on the bare-protocol level, surpasses.

What Snell does

  • First-frame padding shaping. makeV4Padding computes a target number of 1 bits for the padding such that the combined salt + padding + payload bit distribution lands at roughly 61.5 % ones (or 28.5 % if zeros outnumber ones in the AEAD output). The intent is explicit: avoid the ~50/50 ratio that uniform-random AEAD output produces, because "perfectly uniform random bytes" is itself a DPI heuristic for encrypted-proxy traffic.
  • Padding interleave. Even-indexed padding bytes are swapped with even-indexed bytes of the payload ciphertext (swapPadding). Padding bytes never appear contiguously on the wire, so a passive observer can't count "runs of suspiciously-random bytes" and isolate the padding region.
  • Dynamic Record Sizing (v5). The first frame on a stream is small (~1.5 KB), with subsequent frames in the same burst growing until they saturate the 16 KB chunk cap. Idle for 30 s resets back to small. This mimics the size distribution of normal HTTPS application records rather than the "constant max-size frames" pattern of bulk proxies.
  • Built-in http / tls obfs. Static fake-handshake wrappers for TCP. These are 2014-era templates and don't fool a 2025-era DPI on their own, but they layer over the AEAD frames at zero extra cost.

What Shadowsocks 2022 does

At the protocol level: nothing. The first packet after the salt prefix is uniform-random ciphertext. The optional 0–900 B request padding is random bytes, not statistically shaped.

The Shadowsocks 2022 design intentionally delegates anti-censorship to external transports:

  • shadow-tls — wraps the Shadowsocks stream inside a real TLS 1.3 handshake against a chosen upstream domain.
  • v2ray-plugin — WebSocket / mKCP / QUIC carriers.
  • REALITY — TLS handshake stealing from a real upstream, with the proxy data ride-along.

With any of these wrappers, Shadowsocks 2022 is at least as hard to detect as Snell's static tls obfs — usually meaningfully harder, because the wrappers do real TLS rather than replaying a 2014 ClientHello template.

Net result

ScenarioVerdict
Bare protocol over plain TCP, GFW circa 2025Snell (probably) — the entropy shaping was designed for exactly this
With a modern wrapper (shadow-TLS, REALITY, etc.)Shadowsocks 2022 — better wrappers exist in its ecosystem
With Snell's built-in http / tls obfs onlyMarginal improvement over bare Snell — the templates are old enough to fingerprint

Note the "probably" on the first row. We have no public, current data on whether the GFW actually misses Snell v4/v5 today. Snell's anti-fingerprinting design is from 2018-era thinking; the GFW has had seven years to adapt. The bit-count shaping itself is a fingerprint (see next section).

The high-entropy paradox

Both protocols can be observed in pcap. Without the PSK, neither is decryptable — AES-GCM holds. So the meaningful question becomes "can the observer tell what protocol this is?" — and that question splits into two opposing answers:

Answer 1 (the GFW's perspective). Can a passive classifier flag this as "an encrypted proxy"?

  • Shadowsocks 2022 has perfect uniform-random byte distribution after the salt. This is, ironically, easier to flag — it matches the signature of every encrypted proxy in existence. The Wu et al. USENIX Security 2023 paper "How the Great Firewall of China Detects and Blocks Fully Encrypted Traffic" documents the GFW using exactly this heuristic against bare Shadowsocks variants.
  • Snell's bit-count shaping deliberately pulls the distribution away from 50/50, into a range that looks more like real TCP/TLS payload. A naive entropy classifier sees it as more "normal" traffic.

Answer 2 (the protocol-identification perspective). Can someone who already knows what to look for tell which specific protocol this is?

  • Shadowsocks 2022 has no static field anywhere in the visible bytes. Once a session is past the salt, there's nothing structural that says "this is Shadowsocks". You can only confirm by attempting decryption — which requires the PSK.
  • Snell's bit-count shaping is itself a fingerprint. Any first-frame whose salt + padding + payload lands at roughly 61.5 % or 28.5 % ones, with a padLen field consistent with that interval, is identifiable as Snell. Once a researcher publishes that classifier, passive identification becomes feasible.

These two answers point in opposite directions:

Asked questionEasier-to-flagHarder-to-flag
"Is this an encrypted proxy?"Shadowsocks 2022Snell
"Is this Snell vs. Shadowsocks vs. something else?"SnellShadowsocks 2022

The GFW cares about question 1 (block-by-class). A reverse-engineer cares about question 2 (identify-by-protocol). The protocols make opposite tradeoffs.

Findings matrix

AdversarySnellShadowsocks 2022
Cryptographic (no PSK)Plaintext not recoverablePlaintext not recoverable
Cryptographic (with PSK)Full plaintext + all session data + can replay sessionsFull plaintext, but cannot replay (timestamp + salt cache)
Passive censor (entropy classifier)Lower detection probability (shaped padding)High detection probability (uniform random)
Active censor (replay probe)Vulnerable — full session replay worksResistant — salt cache + timestamp window
Active censor (handshake fingerprint)Built-in http/tls obfs is dated and fingerprintableNo built-in obfs; must combine with shadow-TLS / REALITY
Researcher (passive identification)Bit-count shaping is itself a fingerprintNo static field; identification only via decrypt attempt
Multi-tenant operatorSingle PSK, no per-user keysEIH — N keys, hot-rotatable

When to pick which

  • Pick Snell if your bias is toward "must just work in the Chinese network today, with minimal extra infrastructure", you're the only user, and you accept the cryptographic protocol's age (no replay protection, no direction binding, single PSK).
  • Pick Shadowsocks 2022 if you need multi-tenancy, you want a protocol that's been independently implemented and audited, or you plan to layer a modern transport (shadow-TLS, REALITY, etc.) and don't care that the bare protocol is easy to flag.
  • Pick both, on the same host, on different ports, if you have the budget for it. The threats they fail against barely overlap.

Caveats and limits of this analysis

  • No live GFW data. This page asserts that Snell's anti-entropy shaping helps "in 2025" without giving you a measurement. We do not have a current, controlled study of which protocols the GFW actually flags. The relative ranking here is based on what each protocol makes available to a classifier, not on observed block rates.
  • No active probing analysis. We did not test what either server does in response to an active probing attempt (replayed handshake, malformed first frame, etc.). Both protocols should close the connection silently, but probing-resistance is its own research area.
  • Snell is a black-box protocol. It has no public specification. All Snell-related claims here are derived from reverse-engineering documented at MetaCubeX/mihomo#2816 and from this repository's own implementation. Surge could change the wire format and invalidate any of these findings in a future release.
  • Shadowsocks 2022 is a moving target too. The SIP022 spec has been extended (EIH, UDP) since the original 2022 publication. We reference the current master of shadowsocks-rust and the live spec on the Shadowsocks-NET/shadowsocks-specs repo as of this writing.
  • "Better" is a contextual word. Every "winner" cell in the tables above is conditional on the threat model in its row. There is no overall winner.

References

On this page