How to Configure TLS with ECH (Encrypted Client Hello) in Xray-core
Xray-core supports native TLS with Encrypted Client Hello (ECH) through the echServerKeys, echConfigList, and echForceQuery configuration fields, with key generation handled by the built-in xray tls ech command.
This guide walks you through configuring TLS with ECH in Xray-core, the privacy-enhancing extension that encrypts the Server Name Indication (SNI) and other ClientHello fields. The implementation spans multiple source files including transport/internet/tls/ech.go for the core handshake logic and main/commands/all/tls/ech.go for key generation.
Understanding ECH Configuration Fields
Xray-core's ECH support is defined in transport/internet/tls/config.pb.go and exposed through three primary configuration fields. These fields control how servers publish encrypted keys and how clients retrieve and use them.
Field Reference Table
| Field | Direction | Purpose |
|---|---|---|
echServerKeys |
Server | Base64-encoded ECH key-set list for accepting encrypted ClientHello |
echConfigList |
Client | Base64-encoded ECH config list or DNS-HTTPS URL for lookup |
echForceQuery |
Client | Failure mode: none, half, or full |
In transport/internet/tls/ech.go, the ApplyECH function bridges these configuration values into Go's standard crypto/tls.Config before the handshake begins.
Generating ECH Keys with the CLI
Before configuring Xray-core, you must generate the cryptographic material. The xray tls ech command in main/commands/all/tls/ech.go handles this.
Basic Key Generation
# Generate new ECH keys for your domain
xray tls ech --serverName mydomain.com --pem
This outputs two base64-encoded blocks:
- ECH CONFIGS — the
echConfigListvalue for clients - ECH KEYS — the
echServerKeysvalue for servers
Restoring Existing Keys
# Reuse previously generated keys
xray tls ech --serverName mydomain.com -i "BASE64_ECH_SERVER_KEYS" --pem
The command uses X25519 for key encapsulation (hpke.DHKEM(ecdh.X25519())) and marshals the configuration using cryptobyte.Builder as seen in the source.
Server-Side TLS with ECH Configuration
On the server, configure echServerKeys in your inbound TLS settings. This enables the server to accept and decrypt ECH handshakes.
Minimal Server Configuration
{
"inbounds": [
{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [{ "id": "YOUR_UUID" }]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"certificates": [
{
"certificateFile": "/path/to/fullchain.pem",
"keyFile": "/path/to/key.pem"
}
],
"echServerKeys": "BASE64_ECH_SERVER_KEYS"
}
}
}
]
}
The echServerKeys value is processed by ConvertToGoECHKeys in transport/internet/tls/ech.go, which transforms the base64 data into Go's tls.EncryptedClientHelloKey struct and assigns it to config.EncryptedClientHelloKeys.
Client-Side TLS with ECH Configuration
Clients use echConfigList to specify how to obtain ECH configuration. You have two options: direct embedding or DNS-HTTPS lookup.
Option 1: Direct Config Embedding
{
"outbounds": [
{
"protocol": "freedom",
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"allowInsecure": false,
"echConfigList": "BASE64_ECH_CONFIG",
"echForceQuery": "full"
}
}
}
]
}
Option 2: DNS-HTTPS Lookup
{
"tlsSettings": {
"echConfigList": "mydomain.com+https://1.1.1.1/dns-query",
"echForceQuery": "full",
"echSocketSettings": {
"proxy": "dns://8.8.8.8"
}
}
}
When echConfigList contains a + separator, ApplyECH in transport/internet/tls/ech.go parses it as domain+DoH_endpoint and calls QueryRecord. This performs a type 65 HTTPS DNS lookup and caches results in GlobalECHConfigCache.
Understanding echForceQuery Behavior
The echForceQuery parameter controls failure modes when ECH configuration cannot be obtained. This is critical for privacy-preserving operation.
| Value | Behavior | Use Case |
|---|---|---|
none |
Skip ECH, use clear SNI | Compatibility mode, reduced privacy |
half |
Use cached config or fall back | Balanced approach |
full |
Force lookup, fail closed on error | Maximum privacy (default) |
In transport/internet/tls/ech.go, when echForceQuery is "full" and QueryRecord fails, the code injects an invalid config to deliberately break the connection rather than exposing the SNI in plaintext.
Step-by-Step Configuration Walkthrough
1. Generate ECH Cryptographic Material
xray tls ech --serverName mydomain.com --pem > ech-material.txt
Extract the two base64 blocks from the output.
2. Configure the Server Inbound
Add echServerKeys to your TLS inbound settings using the ECH KEYS block.
3. Configure the Client Outbound
Add echConfigList using either:
- The ECH CONFIGS block (direct)
- A DNS-HTTPS URL for dynamic lookup
4. Set Failure Mode
Choose echForceQuery based on your privacy requirements:
fullfor strict privacyhalffor tolerant operationnonefor testing only
5. Reload and Verify
systemctl restart xray
Verify ECH operation using traffic analysis tools. A successful ECH handshake will show "Encrypted Client Hello" rather than visible SNI data.
Key Source Files Reference
| File | Purpose | Location |
|---|---|---|
transport/internet/tls/config.pb.go |
Protobuf definition of ECH configuration fields | Link |
transport/internet/tls/ech.go |
Core logic: ApplyECH, QueryRecord, ConvertToGoECHKeys |
Link |
transport/internet/tls/ech_test.go |
Unit tests for ECH dialing and cache behavior | Link |
main/commands/all/tls/ech.go |
CLI command xray tls ech for key generation |
Link |
infra/conf/transport_internet.go |
Configuration parsing and wiring to TLS builder | Link |
Summary
- ECH in Xray-core is configured through
echServerKeys(server) andechConfigList(client) in TLS settings. - Key generation uses the
xray tls echcommand frommain/commands/all/tls/ech.go, producing base64-encoded key material. - Server configuration requires
echServerKeysin inbound TLS settings, processed byConvertToGoECHKeysintransport/internet/tls/ech.go. - Client configuration supports direct embedding or DNS-HTTPS lookup via
QueryRecord, with caching throughGlobalECHConfigCache. - Failure handling is controlled by
echForceQuery:fullfails closed for privacy,halfallows fallback,nonedisables ECH.
Frequently Asked Questions
What is Encrypted Client Hello and why does Xray-core support it?
Encrypted Client Hello (ECH) is a TLS extension that encrypts the initial handshake, hiding the Server Name Indication (SNI) from network observers. Xray-core implements ECH to provide enhanced privacy for proxy connections, preventing traffic analysis based on cleartext SNI values. The implementation uses Go's standard crypto/tls ECH support, configured through the fields defined in transport/internet/tls/config.pb.go.
How do I generate ECH keys for my Xray-core server?
Run the built-in command: xray tls ech --serverName yourdomain.com --pem. This generates two base64-encoded blocks from main/commands/all/tls/ech.go: "ECH CONFIGS" for clients and "ECH KEYS" for your server configuration. Save the output to a file and extract the values for use in your JSON configuration. You can also regenerate existing keys with -i flag followed by previous server keys.
Why would I use DNS-HTTPS lookup instead of embedding echConfigList directly?
DNS-HTTPS lookup enables dynamic ECH configuration rotation without updating client configurations. When echConfigList contains a URL like domain+https://1.1.1.1/dns-query, QueryRecord in transport/internet/tls/ech.go queries the HTTPS DNS record (type 65) and caches results in GlobalECHConfigCache. This allows servers to rotate keys frequently for improved security while clients automatically retrieve current configurations.
What happens if ECH configuration fails and echForceQuery is set to "full"?
When echForceQuery is "full" and ECH configuration cannot be obtained, ApplyECH in transport/internet/tls/ech.go injects an invalid configuration that causes the TLS handshake to fail entirely. This "fail closed" behavior prevents accidental cleartext SNI exposure, preserving privacy at the cost of connectivity. For less strict environments, use "half" to allow fallback or "none" to disable ECH entirely.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →