OpenSnell

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 server

Both 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 target

Per 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  ──►  upstream

It 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 mode

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

On this page