Architecture & Principles
How OpenSnell's server and client are put together — the layers, where the work happens, and the deliberate choices behind them.
OpenSnell is a from-scratch Go implementation of Surge's Snell proxy protocol, version 4 and 5. This page walks through the shape of the codebase, the request flow through the server and the client, and the design choices that aren't obvious from the README.
Two binaries, one wire
OpenSnell ships two commands:
cmd/snell-server ← inbound TCP + UDP/QUIC listeners
cmd/snell-client ← local SOCKS5 listener that tunnels through a serverBoth link the same protocol package (components/snell). The wire
format is identical to the official Surge snell-server v5.0.1, so
either side can interoperate with the closed-source reference binary —
verified by CI on every commit.
Server request flow (TCP)
┌──────────┐ snell TCP envelope ┌──────────────────────────┐
│ snell │ ───────────────────────► │ snell-server │
│ client │ │ 1. accept TCP │
│ (Go │ │ 2. read 16B salt │
│ or │ │ 3. derive AES key │
│ Surge) │ │ 4. parse first frame │
└──────────┘ │ 5. read CONNECT host:p │
│ 6. dial upstream │
│ 7. splice both halves │
└────────────┬─────────────┘
▼
upstream TCP targetPer accepted TCP connection the server runs two goroutines — one
per direction — copying decrypted bytes between the snell stream and
the upstream socket. Reuse mode (CommandConnectV2) keeps that pair
alive across multiple sequential CONNECTs on the same TCP, draining
the half-close zero frame in between so the next CONNECT starts on a
clean frame boundary.
Server request flow (QUIC, v5 only)
QUIC mode is structurally different — there's no long-lived TCP. The
server's ServeQUIC loop owns one UDP socket on the same port as the
TCP listener:
┌──────────┐ snell-encrypted QUIC envelope (1×) ┌──────────────────┐
│ Surge │ ───────────────────────────────────► │ snell-server │
│ client │ │ ServeQUIC() │
└────┬─────┘ │ │
│ raw QUIC packets (everything after) │ 1. decrypt envelope
│ ◄─────────────────────────────────────────►│ 2. parse host:port
│ │ 3. dial upstream UDP
│ │ 4. remember mapping
│ │ (src,sport)→upstream
│ │ 5. relay raw QUIC
│ └────────┬─────────┘
▼
upstream QUIC server
(Cloudflare, YouTube, …)The reverse mapping pairs are stored in a single in-process map keyed
by (client_src_ip, client_src_port) — once it's there, both
directions forward without touching crypto.
See the QUIC page for the envelope layout and how it was reverse-engineered.
Client (Go SOCKS5)
The Go client is intentionally narrow. It exposes a local SOCKS5 listener and turns every accepted SOCKS5 request into a snell CONNECT:
local app ──SOCKS5──► snell-client ──snell envelope──► snell-server ──► upstreamIt supports CONNECT, reuse (CommandConnectV2 with a 2-cap pool to
mirror Surge), and snell's UDP-over-TCP datagram mode for UDP
ASSOCIATE. It does not implement QUIC envelope construction — use
Surge when you want HTTP/3 acceleration.
Why the deliberate omissions
OpenSnell is opinionated about what it doesn't do. The README spells it out, but the short version:
- No v1 / v2 / v3. Pre-v4 stream framing is trivially fingerprintable and is no longer GFW-reliable. If you have legacy v1/v2 you can't retire, open-snell still implements those.
- No QUIC client in Go. Surge already has a well-tested QUIC stack and a UI for proxy routing; re-implementing it in Go would duplicate effort and risk drift from the real Surge envelope.
- No mux. Snell doesn't have one. Reuse-mode is serial per TCP (one CONNECT at a time); concurrency uses a connection pool, not multiplexed streams. This matters for TCP Brutal on the alpha branch — see that page for the rate-per-connection caveat.
Source map
cmd/
├── snell-server/ main, config parsing, TCP+UDP listener setup
└── snell-client/ main, config parsing, SOCKS5 listener
components/
├── snell/ protocol (v4/v5 TCP frames, QUIC envelope, key derivation)
├── socks5/ minimal SOCKS5 server for the client
├── obfs/ http/tls fake handshakes
├── dialer/ egress-interface + ipv6 toggle + DNS resolution
└── pool/ buffer pool, conn pool for reuse modeThe components/snell package is the only one that has to byte-match
the reference binary. Everything else (SOCKS5, obfs, pool, dialer) is
ordinary infrastructure code.