A small HTTP reverse proxy that forwards every request to a fixed upstream over
TLS with mandatory Encrypted Client Hello (ECH). The ECH outer SNI
(public_name) and the HPKE public key are independently overridable.
Mandatory means fail-closed: the connection to the upstream is only made if ECH
is actually negotiated. If the upstream rejects ECH, the request fails with
502 Bad Gateway rather than falling back to a plaintext SNI. This is enforced
by crypto/tls (a rejected ECH handshake returns an error) and re-asserted via
ConnectionState.ECHAccepted.
go build -o anyech .
Requires Go 1.25+ (client-side ECH lives in the standard crypto/tls).
anyech -upstream https://host [flags]
| Flag | Description |
|---|---|
-listen |
Address to listen on for inbound HTTP (default :8080). |
-upstream |
Upstream URL to forward all requests to, e.g. https://example.com. Must be https. Its hostname is the inner (real) SNI and the forwarded Host. Required. |
-ech-config-list |
Base64 ECHConfigList to use as the source config (e.g. the ech value from a DNS HTTPS/SVCB record). |
-ech-from-dns |
Fetch the source ECHConfigList from the upstream's DNS HTTPS record over DoH. Mutually exclusive with -ech-config-list. |
-doh |
DNS-over-HTTPS resolver endpoint for -ech-from-dns (default https://cloudflare-dns.com/dns-query; must be https). |
-ech-dns-name |
Name to query for the HTTPS record (default: the upstream host). |
-outer-sni |
Override the ECH outer SNI (public_name) sent in cleartext. |
-hpke-public-key |
Override the HPKE public key (base64). |
Base64 input accepts standard or URL-safe alphabets, with or without padding.
- The source config list comes from exactly one of:
-ech-from-dns(theechSvcParam of the upstream's DNSHTTPSrecord, fetched over DoH),-ech-config-list, or — if neither is given — a single config synthesized withconfig_id = 0, theDHKEM(X25519, HKDF-SHA256)KEM, and theHKDF-SHA256KDF with theAES-128-GCMandChaCha20Poly1305AEADs. -outer-sniand-hpke-public-key, when set, replace the corresponding field in every config (so you can, e.g., DNS-fetch the real key andconfig_idbut front with a different outer SNI).- The result must have a non-empty outer SNI and HPKE public key, so when no
source list is supplied both
-outer-sniand-hpke-public-keyare required.
-ech-from-dnsperforms a DoH lookup of the upstream name at startup, which reveals that name to the configured resolver. Go's standard library does not auto-discover ECH from DNS, so this lookup is explicit and opt-in.
The from-scratch path uses
config_id = 0. A server selects its decryption key byconfig_id, so to force ECH against a server that publishes a differentconfig_id(most do), pass that server's real-ech-config-listand override only the field(s) you need.
Auto-fetch the ECHConfigList from the upstream's DNS HTTPS record:
anyech -upstream https://tls-ech.dev -ech-from-dns
Forward to a host using an ECHConfigList supplied directly (e.g. the ech
value from a DNS HTTPS record):
anyech -upstream https://tls-ech.dev \
-ech-config-list 'AEn+DQBF...AAA='
Use that config but override the outer SNI (e.g. to front a different public name):
anyech -upstream https://tls-ech.dev \
-ech-config-list 'AEn+DQBF...AAA=' \
-outer-sni public.tls-ech.dev
Synthesize a config from just the outer SNI and HPKE public key (for a
config_id = 0 server):
anyech -upstream https://example.internal \
-outer-sni public.example \
-hpke-public-key 'AViB1Bo+...Jig='