Skip to content

ECH — sing-box

sing-box implements ECH on both sides as a nested sub-block of the tls: block. The structs share the deprecated PQ-signature toggles left over from the experimental TLS 1.3 draft phase — both are no-ops in current builds.

Inbound tls.ech

FieldTypeDefaultAllowed valuesDescription
enabledboolfalsetrue | falseTurn ECH on for this inbound.
keybadoption.Listable[string][]<base64 ECH key>Inline ECH key bundle. Accepts a single string or a list of strings (one PEM-style block per entry).
key_pathstring(unset)<file path>Path to an ECH key bundle file. Mutually exclusive with `key`.
pq_signature_schemes_enabledboolfalsetrue | falseDeprecated. Post-quantum signature schemes are not supported by the Go standard library.
dynamic_record_sizing_disabledboolfalsetrue | falseDeprecated. Setting this had no effect even when it was supported.

Source: option/tls.go:206-215 · pinned at v1.13.11 (553cfa1)

Outbound tls.ech

FieldTypeDefaultAllowed valuesDescription
enabledboolfalsetrue | falseTurn ECH on for this outbound.
configbadoption.Listable[string][]<base64 ECHConfigList>Pinned ECH config list. When unset, sing-box auto-discovers via HTTPS DNS records.
config_pathstring(unset)<file path>Path-form pinned config.
query_server_namestring(server_name)<hostname>Hostname used for the HTTPS-record DNS query during ECH auto-discovery. Defaults to the outbound's `tls.server_name`.
pq_signature_schemes_enabledboolfalsetrue | falseDeprecated.
dynamic_record_sizing_disabledboolfalsetrue | falseDeprecated.

Source: option/tls.go:217-227 · pinned at v1.13.11 (553cfa1)

Examples

Inbound — serve ECH using a key file:

json
{
  "inbounds": [{
    "type": "vless",
    "listen_port": 443,
    "users": [{ "uuid": "..." }],
    "tls": {
      "enabled": true,
      "server_name": "example.com",
      "certificate_path": "/etc/ssl/cert.pem",
      "key_path": "/etc/ssl/key.pem",
      "ech": {
        "enabled": true,
        "key_path": "/etc/sing-box/ech.key"
      }
    }
  }]
}

Outbound — opportunistic ECH (auto-discovery):

json
{
  "outbounds": [{
    "type": "vless",
    "server": "example.com",
    "server_port": 443,
    "uuid": "...",
    "tls": {
      "enabled": true,
      "server_name": "example.com",
      "ech": { "enabled": true }
    }
  }]
}

Outbound — pinned ECH config:

json
{
  "outbounds": [{
    "type": "vless",
    "server": "example.com",
    "server_port": 443,
    "uuid": "...",
    "tls": {
      "enabled": true,
      "server_name": "example.com",
      "ech": {
        "enabled": true,
        "config": ["<base64 ECHConfigList>"],
        "query_server_name": "cover.example.com"
      }
    }
  }]
}

Notes

  • ECH requires TLS 1.3. Set tls.min_version: "1.3" if you want to enforce it; on 1.2 the ECH extension is silently dropped.
  • The inbound's key field accepts a list because operators rotate ECH keys frequently — listing the new key alongside the old one lets clients with cached HKDF state finish their last few requests before switching.
  • Auto-discovery (outbound enabled: true with empty config) queries the HTTPS DNS record for query_server_name (or tls.server_name if the override is empty). The resolver chain follows the standard sing-box DNS configuration.
  • The two deprecated fields (pq_signature_schemes_enabled, dynamic_record_sizing_disabled) are kept for parser compatibility but have no runtime effect — feel free to remove them from existing configs.

Cross-core notes

  • Xray-core exposes ECH as four fields directly on tlsSettings rather than a sub-block. See ECH — Xray-core.
  • mihomo uses a per-proxy ech-opts block with the same three user-facing fields (enable, config, query-server-name). See ECH — mihomo.

Source: option/tls.go:206-227 · v1.13.11 (553cfa1)

Core Tutorial by Argsment