Shadowsocks traffic on the public internet is fingerprintable: even with an encrypted payload, the TCP-level handshake, packet sizes, and timing make it detectable to active probers and DPI. A common defense is to wrap the shadowsocks stream in an ordinary-looking TLS+WebSocket flow on port 443 so the connection looks like a generic HTTPS website. But the TLS ClientHello itself still leaks the destination via SNI, and SNI is increasingly inspected and blocked.
A SIP003-compatible plugin (Rust) that wraps each shadowsocks stream in a
WebSocket tunnel over TLS, with the TLS handshake protected by ECH
(Encrypted Client Hello). To external observers the connection looks
like a TLS request to a benign public name (e.g. front.example.com);
the real tunnel domain is encrypted inside the ECH-wrapped inner
ClientHello.
- G1: One-binary deployment. The same binary runs as both the server-side and client-side plugin; mode is selected by an option key.
- G2: Drop-in SIP003 plugin compatible with
ssserver/sslocalfromshadowsocks-rust(andss-libevas a stretch). - G3: Auto-TLS via ACME / Let's Encrypt using TLS-ALPN-01 (RFC 8737), so issuance and renewal happen on the existing port-443 listener with no port 80 dependency. No manual cert copying for production.
- G4: ECH enabled for every connection in the standard configuration.
- G5: Stealth: HTTPS probes that don't hit the secret WS path get a fake nginx 404, indistinguishable from a stock nginx default.
- G6: Localhost end-to-end test using real
shadowsocks-rust, self-signed cert, andcurl. Reproducible without VPS access. - G7: No separate config file. The plugin reads everything it needs
from
SS_PLUGIN_OPTIONS. This keeps deployment to one shadowsocks config edit and removes a class of "where does the config live" questions.
- Multiplexing many shadowsocks connections in a single WebSocket.
- DNS HTTPS-record auto-publication of the ECH ConfigList.
- DoH-based ConfigList retrieval on the client.
- DNS-01 ACME challenge.
- TUI setup wizard.
- Operator of a single VPS who wants
ssserver + pluginrunning behind one DNS record and a Let's Encrypt cert. - End user running
sslocal + pluginon macOS / Linux / Android (Android delivered later via a separate packaging effort).
tests/sip003_e2e.rspasses locally and in CI.- Manual VPS test:
curl --socks5-hostnameover the tunnel succeeds; Wireshark capture shows outer SNI =public_name, ECHClientHello extension present, real domain not in cleartext. - p50 added latency vs direct TLS+WS (no ECH): <5 ms localhost, <15 ms WAN.
- In scope: passive DPI, SNI-based blocking, basic active probing (an attacker that connects to the IP and inspects the response).
- Out of scope: traffic-analysis attacks (packet timing/sizes), GFW-style replay-and-correlate, compromise of the VPS.
- Pure-Rust where practical, with one carve-out: BoringSSL via
boringtokio-boringfor the TLS layer, because rustls server-side ECH is not yet stable. ACME stays pure Rust viainstant-acme.
- TLS-ALPN-01 challenges share the main port-443 listener via
BoringSSL's ALPN-select callback (
SSL_set_SSL_CTX); no second port. - Single-process: no external broker, no shared state across instances.