Skip to content

Add TLS Encrypted Client Hello (ECH) support#1527

Open
gfw-report wants to merge 2 commits intoapernet:masterfrom
gfw-report:master
Open

Add TLS Encrypted Client Hello (ECH) support#1527
gfw-report wants to merge 2 commits intoapernet:masterfrom
gfw-report:master

Conversation

@gfw-report
Copy link
Copy Markdown

Summary

This PR adds support for TLS Encrypted Client Hello (ECH) to both the Hysteria client and server, allowing the true SNI to be encrypted inside the TLS handshake. This makes it harder for passive network observers to determine which server a client is connecting to.

  • Server side: accepts an ECH key file via the ech.keyFile config field and passes the parsed keys to the QUIC/TLS listener.
  • Client side: accepts an ECH config file via the tls.ech.configFile config field and sends the encrypted ClientHello accordingly.
  • Key generation: a new hysteria generate ech-keypair CLI command generates a matching ECH config (for clients) and key (for the server) in PEM format.

Usage

1. Generate an ECH key pair

# Write to files
./hysteria generate ech-keypair example.com --outConfig ech.config --outKey ech.key

# Or print to stdout (default)
./hysteria generate ech-keypair example.com

The example.com argument is the "public name" (outer SNI) visible to observers instead of the real server name.

2. Server configuration

tls:
  cert: /path/to/cert.pem
  key: /path/to/key.pem

ech:
  keyFile: /path/to/ech.key

Note: ech is a top-level field, not nested under tls because tls and acme are mutually exclusive, but both can benefit from ECH.

3. Client configuration

server: real-server.example.com:443

tls:
  sni: real-server.example.com
  ech:
    configFile: /path/to/ech.config

With ECH enabled, a passive observer sees the public name generated with the ECH key and config (e.g. example.com) as the outer SNI, while the actual SNI (real-server.example.com) is encrypted. Note that server: real-server.example.com:443 can still expose your real domain name via plaintext DNS lookup. One may consider using only an IP address in this case or use encrypted DNS.

Files changed

File Description
app/internal/utils/ech.go ECH key generation, PEM parsing, and config marshalling
app/cmd/generate.go New generate parent command
app/cmd/generate_ech.go generate ech-keypair subcommand
app/cmd/server.go Server-side ECH config struct and loader
app/cmd/client.go Client-side ECH config struct and loader
core/server/config.go, core/server/server.go Pass ECH keys to tls.Config
core/client/config.go, core/client/client.go Pass ECH config list to tls.Config

Note on active probing

A censor could 1) first observe the QUIC connections between a user and a server, and then 2) active probe the server by connecting with the observed outer SNI but a dummy (invalid) ECH payload. If the server fails the TLS handshake (because it cannot present a certificate for the outer SNI domain), the censor can confirm the server is using ECH and thus block by the combination of outer SNI and server IP address.

One possible mitigation may be for the server to relay such requests to the real outer SNI server when ECH decryption fails, rather than returning a TLS error. But this is out of scope for this PR.

That said, to our knowledge no censor other than China's GFW has deployed active probing at scale, and even the GFW has not been observed to actively probe ECH yet as of March 2026. ECH is still valuable for users in countries where censors rely only on passive observation. This limitation should not block the PR.

Example usage:

  generate ech-keypair outer-sni.com --outConfig ech.config --outKey ech.key
@gfw-report
Copy link
Copy Markdown
Author

The corresponding PR for documentation: apernet/hysteria-website#49

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant