{"id":49455414,"url":"https://github.com/shadowsocks/ech-tls-tunnel","last_synced_at":"2026-05-05T06:01:52.427Z","repository":{"id":354564830,"uuid":"1224180504","full_name":"shadowsocks/ech-tls-tunnel","owner":"shadowsocks","description":"SIP003 plugin for shadowsocks: WebSocket-over-TLS with ECH and ACME auto-renewal","archived":false,"fork":false,"pushed_at":"2026-05-02T01:02:23.000Z","size":143,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-04T05:00:21.430Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shadowsocks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T03:15:06.000Z","updated_at":"2026-05-02T01:02:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/shadowsocks/ech-tls-tunnel","commit_stats":null,"previous_names":["shadowsocks/ech-tls-tunnel"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/shadowsocks/ech-tls-tunnel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shadowsocks%2Fech-tls-tunnel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shadowsocks%2Fech-tls-tunnel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shadowsocks%2Fech-tls-tunnel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shadowsocks%2Fech-tls-tunnel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shadowsocks","download_url":"https://codeload.github.com/shadowsocks/ech-tls-tunnel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shadowsocks%2Fech-tls-tunnel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32595202,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-30T05:01:09.052Z","updated_at":"2026-05-04T05:01:41.211Z","avatar_url":"https://github.com/shadowsocks.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ech-tls-tunnel\n\nA SIP003 plugin for [shadowsocks](https://shadowsocks.org/) that wraps\neach stream in a WebSocket-over-TLS connection on port 443, with the\nTLS handshake protected by **ECH** (Encrypted Client Hello). To passive\nobservers the connection looks like a TLS request to a benign public\nname; the real tunnel domain is encrypted inside the ECH-protected\nClientHelloInner.\n\n- Auto-issues and renews a Let's Encrypt cert via **TLS-ALPN-01**, on\n  the same port-443 listener — no port 80 needed.\n- Stealth: probes that don't hit the secret WebSocket path get a fake\n  nginx 404, indistinguishable from a default nginx install.\n- Single config surface: every option lives in `SS_PLUGIN_OPTIONS`.\n  No YAML, no separate config file.\n\n## What's in this repo\n\n| | |\n|---|---|\n| `src/server.rs` + `src/client.rs` | event loops |\n| `src/tls_server.rs` + `src/tls_client.rs` | BoringSSL wrappers |\n| `src/ws.rs` | WebSocket ↔ AsyncRead/Write adapter |\n| `src/acme.rs` + `src/challenge.rs` | TLS-ALPN-01 issuance + renewal |\n| `src/ech.rs` | HPKE keygen, ECHConfig (un)marshaling, FFI |\n| `src/sip003.rs` + `src/config.rs` | SIP003 env / plugin options |\n| `tests/sip003_e2e.rs` | full localhost test against `shadowsocks-rust` |\n\n## Install\n\nPick a release binary from the\n[Releases page](https://github.com/shadowsocks/ech-tls-tunnel/releases),\nextract, and put `ech-tls-tunnel` somewhere on `PATH`:\n\n```sh\n# Linux x86_64 (glibc — Ubuntu/Debian/RHEL/etc.)\ncurl -L https://github.com/shadowsocks/ech-tls-tunnel/releases/latest/download/ech-tls-tunnel-linux-amd64.tar.gz | tar xz\nsudo mv ech-tls-tunnel /usr/local/bin/\n\n# Linux x86_64 (musl, fully static — Alpine, OpenWrt, embedded)\ncurl -L https://github.com/shadowsocks/ech-tls-tunnel/releases/latest/download/ech-tls-tunnel-linux-amd64-musl.tar.gz | tar xz\nsudo mv ech-tls-tunnel /usr/local/bin/\n\n# macOS arm64\ncurl -L https://github.com/shadowsocks/ech-tls-tunnel/releases/latest/download/ech-tls-tunnel-darwin-arm64.tar.gz | tar xz\nsudo mv ech-tls-tunnel /usr/local/bin/\n```\n\n`linux-arm64` and `linux-arm64-musl` builds are also published.\n\nOr build from source:\n\n```sh\ncargo install --git https://github.com/shadowsocks/ech-tls-tunnel\n```\n\n## Quick start\n\n### 1. Server side (VPS, port 443 free, A record points at it)\n\n```sh\n# Generate the HPKE keypair + ECHConfigList\nsudo mkdir -p /var/lib/ech-tls-tunnel\nech-tls-tunnel ech-gen-keys \\\n    --public-name front.example.com \\\n    --out /var/lib/ech-tls-tunnel/ech\n\n# Run ssserver with the plugin\nssserver \\\n    -s 0.0.0.0:443 \\\n    -k '\u003cpassword\u003e' \\\n    -m aes-128-gcm \\\n    --plugin ech-tls-tunnel \\\n    --plugin-opts \"mode=server;\\\ndomain=tunnel.example.com;\\\npath=/ws-tunnel-CHANGE-ME;\\\nacme_email=admin@example.com;\\\nacme_cache=/var/lib/ech-tls-tunnel/acme;\\\nech_public_name=front.example.com;\\\nech_key=/var/lib/ech-tls-tunnel/ech/ech.key\"\n```\n\nThe first run blocks on the ACME order; subsequent runs reuse the\ncached cert and renew transparently.\n\n### 2. Client side (any device)\n\nCopy the base64 `ECHConfigList` printed by `ech-gen-keys` (or the\ncontents of `/var/lib/ech-tls-tunnel/ech/ech.config_list` after\nbase64-encoding) and pass it as `ech_config=`:\n\n```sh\nsslocal \\\n    -b 127.0.0.1:1080 \\\n    -s tunnel.example.com:443 \\\n    -k '\u003cpassword\u003e' \\\n    -m aes-128-gcm \\\n    --protocol socks \\\n    --plugin ech-tls-tunnel \\\n    --plugin-opts \"mode=client;\\\nsni=tunnel.example.com;\\\npath=/ws-tunnel-CHANGE-ME;\\\nech_config=\u003cpaste base64 ECHConfigList here\u003e\"\n```\n\nNow `127.0.0.1:1080` is a SOCKS5 proxy whose traffic looks (to anyone\non the wire) like an HTTPS connection to `front.example.com`.\n\n## Plugin options reference\n\n### Common\n\n| Key | Default | Notes |\n|---|---|---|\n| `mode` | (required) | `server` or `client` |\n| `path` | (required) | Secret WS path, must start with `/`. Anything else gets fake nginx 404. |\n| `fast_open` | `false` | Enable TCP Fast Open on listener and outgoing connections. Linux benefits most. |\n\n### Server-only\n\n| Key | Default | Notes |\n|---|---|---|\n| `domain` | (required) | Real tunnel domain — inner SNI; appears as a SAN on the production cert. |\n| `cert` + `key` | — | Static cert/key on disk. Mutually exclusive with `acme_email`. |\n| `acme_email` | — | Contact email; enables ACME (Let's Encrypt) via TLS-ALPN-01. |\n| `acme_cache` | `/var/lib/ech-tls-tunnel/acme` | Where the ACME account + cert live across restarts. |\n| `acme_staging` | `false` | Use Let's Encrypt staging — set `true` while testing to avoid rate limits. |\n| `acme_cover_san` | `true` | Include `ech_public_name` as a SAN on the ACME cert. Set `false` when the cover name is a domain you don't own (e.g. `www.baidu.com`); the cert then only covers `domain`. |\n| `ech_public_name` | — | Outer SNI advertised to public observers. Required (with `ech_key`) to enable ECH. Owning the name (with a SAN on the cert) holds up under active probing; an unowned cover name only hides the SNI from passive observers. |\n| `reject_non_ech` | `true` | Only meaningful when ECH is enabled. TCP-RST any inbound TLS handshake whose ClientHello lacks the `encrypted_client_hello` extension (and isn't an ACME `acme-tls/1` validator), so active probes can't observe the production cert. |\n| `ech_key` | — | Path to the HPKE private key from `ech-gen-keys`. |\n| `server_name` | `nginx/1.24.0` | Value of the `Server` header in fake-404 responses. |\n\n### Client-only\n\n| Key | Default | Notes |\n|---|---|---|\n| `sni` | (required) | Real upstream hostname — sent as inner SNI inside the ECH-protected ClientHello. |\n| `ech_config` | — | Base64 ECHConfigList from the server. Either this or `ech_config_file`. |\n| `ech_config_file` | — | Path to a binary ECHConfigList file (alternative to `ech_config`). |\n| `ca_file` | — | Pin to a specific CA bundle (PEM). Mutually exclusive with `insecure`. |\n| `insecure` | `false` | DEV/TEST ONLY — skip cert verification. |\n| `fingerprint` | — | Browser-fingerprint shaping for the TLS ClientHello. One of `chrome`, `firefox`, `safari`, `ios`, `android`, `edge`, or `random`. Versioned aliases (`chrome120`, `safari16`, …) also accepted. See [src/fingerprint.rs](src/fingerprint.rs) for the profile bodies. |\n\n## CLI subcommands\n\n```\nech-tls-tunnel ech-gen-keys --public-name \u003cNAME\u003e --out \u003cDIR\u003e\n```\n\nGenerates an HPKE X25519 keypair, writes `ech.key` (binary private\nkey) and `ech.config_list` (binary ECHConfigList) under `\u003cDIR\u003e`, and\nprints the base64 ConfigList ready to paste into the client's\n`ech_config=` plugin option.\n\n## Running under systemd (Linux)\n\nThe plugin itself is a child process of `ssserver` — write the\nsystemd unit for `ssserver`, not the plugin:\n\n```ini\n# /etc/systemd/system/ssserver-ech.service\n[Unit]\nDescription=Shadowsocks server with ech-tls-tunnel plugin\nAfter=network.target\n\n[Service]\nExecStart=/usr/local/bin/ssserver \\\n    -s 0.0.0.0:443 \\\n    -k YOUR_PASSWORD \\\n    -m aes-128-gcm \\\n    --plugin /usr/local/bin/ech-tls-tunnel \\\n    --plugin-opts \"mode=server;domain=tunnel.example.com;path=/ws-secret;acme_email=admin@example.com;ech_public_name=front.example.com;ech_key=/var/lib/ech-tls-tunnel/ech/ech.key\"\nRestart=on-failure\nRestartSec=5\nLimitNOFILE=65536\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n```\n\n```sh\nsudo systemctl daemon-reload\nsudo systemctl enable --now ssserver-ech\n```\n\n## How it works\n\n```\nsslocal ──TCP──▶ ech-tls-tunnel (client mode) ──TLS+ECH+WS──▶ ech-tls-tunnel (server mode) ──TCP──▶ ssserver\n                                                  │\n                                                  ▼\n                              ACME (TLS-ALPN-01 on the same port 443)\n```\n\n- TLS termination uses BoringSSL via the `boring`/`tokio-boring`\n  crates from Cloudflare. BoringSSL's mature ECH support\n  (`SSL_marshal_ech_config`, `SSL_ECH_KEYS_*`,\n  `SSL_set1_ech_config_list`) is what makes server-side ECH possible\n  in pure Rust today.\n- The ACME flow (instant-acme) uses TLS-ALPN-01: when the ACME server\n  validates a domain, it offers ALPN `acme-tls/1`. A\n  `ChallengeStore` keyed by the SAN being validated lets the\n  ALPN-select callback hot-swap the active SSL_CTX to a self-signed\n  cert carrying `SHA-256(keyAuthorization)` in the `acmeIdentifier`\n  extension. After validation, the entry is removed and traffic\n  resumes on the production cert.\n- Cert renewals hot-swap the production `SslAcceptor` via `arc-swap`;\n  in-flight connections keep the old cert, new ones get the renewed.\n\n## Threat model\n\nIn scope: passive DPI, SNI-based blocking, basic active probing\n(connecting to your IP and inspecting the response).\n\nOut of scope: traffic-analysis attacks (packet sizes, timing),\nGFW-style replay-and-correlate, host compromise.\n\n## Development\n\n```sh\ncargo fmt --all -- --check\ncargo clippy --all-targets -- -D warnings\ncargo test --lib --tests       # 58 tests, end-to-end against shadowsocks-rust\n```\n\nThe full e2e test (`tests/sip003_e2e.rs`) requires `ssserver`,\n`sslocal`, and `curl` on `PATH`; it skips with a clear message\notherwise.\n\nSee [`docs/PRD.md`](docs/PRD.md), [`docs/ROADMAP.md`](docs/ROADMAP.md),\nand [`docs/TODO.md`](docs/TODO.md) for the design.\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshadowsocks%2Fech-tls-tunnel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshadowsocks%2Fech-tls-tunnel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshadowsocks%2Fech-tls-tunnel/lists"}