ECH
ECH (Encrypted Client Hello) encrypts the TLS ClientHello, including the SNI (Server Name Indication), so that only a decoy "public name" is visible to middleboxes.
Without ECH, the SNI travels in cleartext in every handshake, making it possible for middleboxes to inspect and block. With ECH, the real server name is inside an encrypted payload.
Note that ECH only protects the SNI in the QUIC/TLS handshake. If you already use obfuscation, the entire connection is already unrecognizable as QUIC, so ECH provides no additional benefit. ECH is most useful in "bare" mode, where the QUIC handshake is standard and the SNI is the main plaintext fingerprint.
How it works
An ECH deployment has two artifacts, generated together as a key pair:
- A private key that stays on the server and is used to decrypt the real ClientHello.
- A config list (public) that clients use to encrypt their ClientHello. It also contains the public name - the decoy SNI that appears in cleartext.
When a client connects with ECH, it sends a ClientHello whose visible SNI is the public name (e.g. decoy.example.com), while the real SNI and the rest of the handshake are encrypted inside the ECH extension.
Generating keys
Starting with version 2.12.3, use Hysteria's built-in generator:
--public-name is required: this is the plaintext outer SNI, not the real server name.
The command creates ech.pem and prints ready-to-paste server and client config blocks.
The file contains both PEM blocks:
-----BEGIN ECH KEYS-----
...
-----END ECH KEYS-----
-----BEGIN ECH CONFIGS-----
...
-----END ECH CONFIGS-----
Keep ech.pem private. It contains the secret key. Share only the base64 value printed under the client tls.ech setting. The server reads the ECH KEYS block and derives the matching public config list from it.
Options:
| Option | Default | Meaning |
|---|---|---|
--public-name |
Required | Plaintext outer SNI. |
--output, -o |
ech.pem |
Private PEM output file. |
--config-id |
Random byte | Config identifier, 0-255; -1 selects a random ID. Use distinct IDs for concurrently active keys during rotation. |
--max-name-length |
0 |
Inner name padding hint, 0-255. Zero means unknown; this does not limit the real server name's length. |
--aead |
aes-128-gcm |
Comma-separated HPKE AEADs, in preference order: aes-128-gcm, aes-256-gcm, chacha20-poly1305. |
--overwrite |
Off | Explicitly replace an existing key file. |
The generated keys do not replace the server's TLS certificate. Keep the existing certificate and client verification settings. Replacing ECH keys requires distributing the new public config to clients; clients using only the old config will fail to connect after the server stops accepting it.
Key files generated by sing-box generate ech-keypair are also compatible with Hysteria.
Server setup
Add an ech block pointing to the key file.
On startup, the server logs the config list that clients need:
INFO ECH enabled, set the following config list on clients (tls.ech) {"configList": "AEz+DQBIAAAg..."}
Copy that base64 string - it is what you give to clients. (It is the same value as the ECH CONFIGS block, just base64-encoded on a single line.)
Client setup
Set tls.ech to the config list printed by hysteria ech or the server log. The value can be the base64 string directly, or a path to a file containing it (either the raw base64 or the ECH CONFIGS PEM block):
- Your real server name, used for certificate verification.
- The config list printed by
hysteria echor the server log, or a path to a file containing it.
The config list can also be carried in a sharing URI via the ech query parameter.
Important behaviors
-
Enabling ECH is backward-compatible. A server with ECH enabled still accepts clients that do not use ECH; those clients simply send their SNI in cleartext as before. This means you can enable ECH on the server without breaking existing clients.
-
Fail-closed. If a client is configured for ECH but the server rejects it (for example because the config list is stale after the keys were regenerated, or the server has no ECH configured at all), the connection fails.
-
insecuredoes not apply to ECH rejection. When ECH is rejected, the TLS stack performs a mandatory certificate check against the public name and ignorestls.insecure. If you are testing with a self-signed certificate and the server is not actually accepting ECH, you will see a certificate verification error.