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 echConfigList value for clients
  • ECH KEYS — the echServerKeys value 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:

  • full for strict privacy
  • half for tolerant operation
  • none for 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) and echConfigList (client) in TLS settings.
  • Key generation uses the xray tls ech command from main/commands/all/tls/ech.go, producing base64-encoded key material.
  • Server configuration requires echServerKeys in inbound TLS settings, processed by ConvertToGoECHKeys in transport/internet/tls/ech.go.
  • Client configuration supports direct embedding or DNS-HTTPS lookup via QueryRecord, with caching through GlobalECHConfigCache.
  • Failure handling is controlled by echForceQuery: full fails closed for privacy, half allows fallback, none disables 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →