OpenSnell

Comparison with Other Protocols

A protocol-level comparison of OpenSnell's Snell v4/v5 with Shadowsocks 2022 (SIP022) and mieru TCP — 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 source citations into each codebase so any claim is verifiable.

The three 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.
  • mieru TCP — the TCP underlay of enfein/mieru, read from docs/protocol.md, pkg/cipher, pkg/protocol, and pkg/replay. mieru also has a UDP transport, but this page compares the no-TLS, TCP-on-the-wire case because that is the closest axis to Snell and bare Shadowsocks.

TL;DR

DimensionWinner
Cryptographic protocol designShadowsocks 2022
Bare TCP against passive entropy classifiersSnell / mieru — both deliberately avoid pure random-looking streams
Bare TCP against replay probesShadowsocks 2022 / mieru
Surviving the GFW with a modern wrapperShadowsocks 2022 + Reality / shadow-TLS
Recovering plaintext from a capture (no PSK)Tie — all three rely on modern AEAD
Identifying the protocol from a captureSee § The high-entropy paradox — the question splits in two
Multi-user / key rotationShadowsocks 2022 (EIH) and mieru (per-user credentials)
Implementation maturity / auditsShadowsocks 2022

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

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 / credential. Has somehow obtained the Snell/Shadowsocks pre-shared key or a mieru user credential. What can they still not do?

The three 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.

mieru TCP (first open-session segment)

+-------------+-----------------------------+----------------------+----------------------+
| nonce (24B) | AEAD(metadata_32B) + tag(16)| AEAD(payload) + tag  | padding(0..255 B)   |
+-------------+-----------------------------+----------------------+----------------------+
| visible     | metadata includes protocol  | optional first       | random bytes shaped |
|             | type, minute timestamp,     | payload can carry    | either toward a     |
|             | session ID, sequence,       | the SOCKS request    | printable run or a  |
|             | payloadLen, suffixLen       | and even 0-RTT data  | lower-bit target    |
+-------------+-----------------------------+----------------------+----------------------+

In the TCP underlay, the nonce appears once per direction, on the first encrypted metadata block; later metadata and payload AEAD calls increment that implicit 24-byte nonce (docs/protocol.md:37, api.go:95-105, cipher.go:99-132,323-334). The open-session segment may carry the SOCKS request as its encrypted payload when it fits within 1024 bytes (session.go:311-330).

The visible nonce is not necessarily uniform random. The default traffic-pattern path generates a nonce rule, and without unlockAll it chooses a printable or common-64 prefix of 6-12 bytes (trafficpattern/config.go:136-174, cipher.go:275-283). The last 4 bytes of the nonce are also a server-side user lookup hint: SHA256(username || nonce[0:16])[:4] (docs/protocol.md:23, api.go:36-39, cipher.go:336-347).

Cryptographic primitives

Key derivation

PropertySnellShadowsocks 2022mieru TCP
KDFArgon2id(t=3, m=8KiB, p=1)BLAKE3::derive_keySHA256(password || 0x00 || username) -> PBKDF2-SHA256
InputsPSK string + 16 B saltPSK bytes + per-stream saltuser password hash + 2-minute time salt
PSK assumptionArbitrary string (weak OK)High-entropy 16 / 32 B (openssl rand -base64 16)User password, but time sync required
Cost per connectionOne Argon2 call (~ms, 8 KiB RAM)One BLAKE3 call (~µs)Cached PBKDF2, 64 iterations, 32-byte key

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.
  • mieru hashes password || 0x00 || username with SHA-256, then derives a 32-byte key with PBKDF2-SHA256 using a salt derived from rounded system time. The protocol constants are 64 PBKDF2 iterations and a 2-minute key refresh interval; the server tries the previous, current, and next time salt (docs/protocol.md:13-21, keygen.go:27-38,55-73, cache.go:88-110). This makes old captures expire as the time window moves, but it is not a memory-hard password KDF. If the username/password are human-guessable and the attacker knows the capture time, the offline brute-force story is weaker than Snell's Argon2id story.

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

AEAD

PropertySnellShadowsocks 2022mieru TCP
CipherAES-128-GCM onlyAES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 (+ optional ChaCha8)XChaCha20-Poly1305 only
Nonce12 B counter, init 0, per-AEAD-op increment12 B counter, init 0, per-AEAD-op increment24 B nonce, sent once per direction, incremented per AEAD op
Max plaintext / chunk0x3FFF (16383)0xFFFF (65535)32768 B stream fragment; 1024 B open-session payload

Snell and Shadowsocks 2022 use the standard AES-GCM counter-nonce pattern, which is safe provided the key is fresh (which it is — derived from a per-stream random salt). mieru uses XChaCha20-Poly1305's 24-byte nonce and then increments the nonce in implicit-nonce TCP mode for later AEAD operations.

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. mieru is hard-coded to XChaCha20-Poly1305 (cipher.go:34-82), which is a good primitive and avoids AES-NI dependency, but still gives operators no cipher choice.

The 4× larger Shadowsocks chunk size (65535 vs Snell's 16383) cuts framing overhead proportionally for bulk transfers. mieru sits between them for stream payloads at 32768 bytes, while open-session payloads are capped at 1024 bytes so the first segment stays bounded.

Protocol-level defenses

This is where Snell is weakest. Shadowsocks 2022 and mieru both add freshness checks; they do it in very different ways.

Replay protection

DefenseSnellShadowsocks 2022mieru TCP
Fresh visible value16 B saltPer-stream salt24 B nonce
Server replay cache (60 s salt cache) (~6 min signature cache)
Timestamp window (30 s, aead_2022.rs:377-379) (encrypted minute timestamp, ±1 minute check)
Key expiry at protocol layer time-derived key, three adjacent salts

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.

mieru also rejects this class of replay. For TCP, the server keeps a global replay cache sized at 4M entries with an expiry of KeyRefreshInterval * 3 (2 min × 3 = ~6 min), and the stream reader turns a duplicated first-session signature into a replay error (underlay_stream.go:40-44,355-386). Its encrypted metadata also carries a minute timestamp and rejects values outside a ±1 minute window (metadata.go:140-165,213-240). After the key-refresh window moves, the server no longer derives the same AEAD key anyway (keygen.go:27-38,55-73).

Direction binding

DefenseSnellShadowsocks 2022mieru TCP
Direction field inside encrypted metadata (0=client, 1=server) (distinct protocol types, side-validated by session)

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.

mieru's encrypted metadata has separate protocol values for openSessionRequest, openSessionResponse, dataClientToServer, dataServerToClient, and both ACK directions (metadata.go:28-39). The session layer rejects server-bound packets with server-to-client protocols and vice versa (session.go:872-880). This is closer to Shadowsocks 2022 than to Snell, although the encoding is a richer session protocol rather than a single 0/1 type byte.

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.

mieru has a weaker equivalent: the open-session response carries the same encrypted session ID and sequence space as the request (session.go:1008-1045), but it does not echo the client nonce or salt in the response the way Shadowsocks 2022 does. So the request → response binding is operationally present through the session state, not cryptographically explicit in the first response header.

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.

Official Snell and OpenSnell's standalone stable config use a single PSK. One leak means everyone re-keys. OpenSnell's alpha branch adds a library-level multi-user server mode by trial-decrypting the first encrypted frame against a user store, but that is not an official Snell wire feature and carries no on-wire user hint.

mieru supports multiple users on one server (README.md:20, server-install.md:102-125). Each user has a separate username/password derived key, and v3.31.0 added a visible 4-byte nonce hint to accelerate server-side user lookup (server-install.md:329-337, cipher.go:336-347). That gives operators practical per-user rotation, but it is not the same design as Shadowsocks 2022 EIH: the identity hint is a short protocol artifact, not an extensible encrypted identity header family.

Anti-censorship behavior

This is the axis where Snell and mieru catch up. All three can run over plain TCP without TLS; only Shadowsocks 2022 normally expects a modern external wrapper when censorship resistance is the main goal.

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.

What mieru does

  • No TLS dependency. mieru explicitly advertises that it does not use TLS and does not require a domain name or fake website (README.md:16-19). That makes it operationally closer to Snell than to the modern Shadowsocks+REALITY deployment pattern.
  • Random padding with selectable shape. The documented segment format has optional padding before/around/after encrypted fields, and the TCP implementation adds up to 255 bytes of padding on stream segments (docs/protocol.md:27-39, padding.go:61-73, underlay_stream.go:570-603). For open-session segments, the padding strategy is stable per user and chooses either a printable run or a lower-bit probability target of 0.325 (padding.go:28-30,75-101).
  • Nonce pattern manipulation. The default generated traffic pattern avoids a fully random-looking nonce prefix by choosing printable or common-64 bytes unless unlockAll expands the range (trafficpattern/config.go:136-174, cipher.go:275-283).
  • Replay/probe friction. The replay cache and timestamp window mean a censor cannot simply replay a captured TCP opener and watch whether the server performs the same upstream action. After read errors, the stream underlay can also keep reading a random amount for a random time to make probes less crisp (underlay_stream.go:777-785).

The tradeoff is that mieru's shaped nonce and metadata cadence are also a fingerprint. It avoids the "pure random stream" smell, but a classifier trained on "24-byte visible nonce with printable/common prefix, 48-byte encrypted metadata, optional payload, then padding" has something concrete to look for.

Net result

ScenarioVerdict
Bare protocol over plain TCP, passive classifierSnell / mieru (probably) — both shape visible bytes away from uniform random
Bare protocol over plain TCP, replay probemieru / Shadowsocks 2022 — Snell lacks replay freshness
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 or mieru today. Snell's anti-fingerprinting design is from 2018-era thinking, and mieru's visible nonce/padding strategy is also something a classifier could learn. Shaping helps against naive entropy filters; it does not make a protocol magically unidentifiable.

The high-entropy paradox

All three protocols can be observed in pcap. Without the PSK or user credential, none should be decryptable — the AEAD layer is not the weak link. 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.
  • mieru's first segment also avoids pure random-looking bytes: the visible nonce can have a printable/common prefix, and padding can be shaped toward printable runs or a lower-bit target. A naive "is this just random bytes?" classifier has less to grab than it does with bare Shadowsocks 2022.

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.
  • mieru exposes a 24-byte nonce before encrypted metadata. With the default traffic pattern, the prefix is intentionally not uniform random, and the last 4 bytes carry the user hint. The fixed nonce + 48B encrypted metadata + optional encrypted payload + padding cadence is useful to the server, but also useful to a protocol-specific classifier.

These two answers point in opposite directions:

Asked questionEasier-to-flagHarder-to-flag
"Is this an encrypted proxy?"Shadowsocks 2022Snell / mieru
"Which specific proxy protocol is this?"Snell / mieruShadowsocks 2022

The GFW cares about question 1 (block-by-class). A reverse-engineer cares about question 2 (identify-by-protocol). Snell and mieru spend visible bytes to look less like random ciphertext; Shadowsocks 2022 spends no visible bytes and relies on wrappers when that matters.

Findings matrix

AdversarySnellShadowsocks 2022mieru TCP
Cryptographic (no PSK / credential)Plaintext not recoverablePlaintext not recoverablePlaintext not recoverable
Cryptographic (with PSK / credential)Full plaintext + all session data + can replay sessionsFull plaintext, but captured sessions cannot be replayed after freshness checksFull plaintext for that user's traffic; stale replay blocked by timestamp/cache/key window
Passive censor (entropy classifier)Lower detection probability (shaped padding)High detection probability (uniform random)Lower detection probability than pure random, but visible nonce/padding shape remains
Active censor (replay probe)Vulnerable — full session replay worksResistant — salt cache + timestamp windowResistant — replay cache + timestamp + time-derived key
Active censor (handshake fingerprint)Built-in http/tls obfs is dated and fingerprintableNo built-in obfs; must combine with shadow-TLS / REALITYNo TLS dependency; shaped nonce/padding and probe-drain behavior, but still protocol-specific
Researcher (passive identification)Bit-count shaping is itself a fingerprintNo static field; identification only via decrypt attemptNonce prefix/user hint and fixed metadata cadence are fingerprints
Multi-tenant operatorOfficial/stable: single PSK; OpenSnell alpha library can trial-decrypt N user PSKsEIH — N keys, hot-rotatableN users with separate credentials; practical rotation, not EIH

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, no official on-wire user identity).
  • 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 mieru if you want a no-TLS/no-fake-site TCP proxy like Snell, but you also want replay freshness and per-user credentials. The tradeoff is a newer, less-audited protocol with time-sync sensitivity and a visible nonce/padding fingerprint.
  • Run more than one protocol, on the same host or on different hosts, 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 any server does in response to an active probing attempt (replayed handshake, malformed first frame, etc.). These protocols should close the connection quietly, 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.
  • mieru TCP only. mieru also supports a UDP transport. This page only analyzes the TCP underlay because it is the closest comparison to Snell and bare Shadowsocks 2022 over TCP. mieru's generated traffic pattern can also change with configuration, seed, and version.
  • "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