rustls ECH Demo

Proxy metadata:
Outer SNI: proxied-a.ech.jelle.dev
Proxy ECH result: not offered (no ECH extension)
Route mode: split (inner CH forwarded)
Backend metadata:
Protocol: TLS (HTTP/2)
server_name (SNI): proxied-a.ech.jelle.dev
ECH status: not offered

Demo endpoints

Domain:443 (h2 + h3):444 (h2 only):446 (h3 only)Mode
proxied-avisitvisitvisitsplit-mode ECH
proxied-bvisitvisitvisitsplit-mode ECH
directvisitvisitvisitpassthrough ECH
noechvisitvisitvisitno ECH (GREASE)
publicvisitvisitvisitouter SNI, no ECH

:443 = TCP + UDP (browser upgrades to h3 via alt-svc)  |  :444 = TCP only (no QUIC)  |  :446 = QUIC only (no TCP fallback)

H3 notes: The :446 endpoints rely entirely on HTTPS DNS records. Your browser needs a resolver that supports them (e.g. DoH) or these links won't work. Even then, both Firefox and Chrome may fail to load the noech and public endpoints on :446, since their HTTPS records lack ECH configs and browsers tend to treat non-ECH records as optional. The ECH-enabled endpoints (proxied-a, proxied-b, direct) work most reliably over H3. On :443, both HTTPS records and alt-svc headers advertise H3, so upgrades are more likely, but your browser may still prefer H2.

Setup

All traffic enters through an ECH split-mode proxy on port :443 (TCP+UDP), :444 (TCP only), and :446 (QUIC only). The proxy peeks at the TLS/QUIC ClientHello, attempts ECH decryption, and routes by SNI:

Backend servers support HTTP/2 (via ALPN) and advertise alt-svc: h3 on :443 so browsers upgrade to QUIC/HTTP3. The :444 backends do not advertise alt-svc. The :446 port is QUIC-only (no TCP fallback) for testing H3 directly.

The proxy communicates metadata to backends via PROXY protocol v2 (TLS) or a metadata UDP datagram (QUIC), shown in the blue 'Proxy metadata' box above.

All components are built with rustls (TLS 1.3 + ECH), quinn (QUIC), and hickory-dns (DNS + DoH). Source: rustls-server-ech-demo.

DNS records

Served by our own authoritative DNS + DoH server. Browsers query these via DoH to discover ECH configs and ALPN protocols.

ech.jelle.dev. 60 IN A 165.232.148.125
ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1,
_444._https.ech.jelle.dev. 60 IN HTTPS 1 ech.jelle.dev. alpn=h2,http/1.1,
_446._https.ech.jelle.dev. 60 IN HTTPS 1 ech.jelle.dev. alpn=h3,
direct.ech.jelle.dev. 60 IN A 165.232.148.125
direct.ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
direct.ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_444._https.direct.ech.jelle.dev. 60 IN HTTPS 1 direct.ech.jelle.dev. alpn=h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_446._https.direct.ech.jelle.dev. 60 IN HTTPS 1 direct.ech.jelle.dev. alpn=h3, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
noech.ech.jelle.dev. 60 IN A 165.232.148.125
noech.ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
noech.ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1,
_444._https.noech.ech.jelle.dev. 60 IN HTTPS 1 noech.ech.jelle.dev. alpn=h2,http/1.1,
_446._https.noech.ech.jelle.dev. 60 IN HTTPS 1 noech.ech.jelle.dev. alpn=h3,
proxied-a.ech.jelle.dev. 60 IN A 165.232.148.125
proxied-a.ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
proxied-a.ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_444._https.proxied-a.ech.jelle.dev. 60 IN HTTPS 1 proxied-a.ech.jelle.dev. alpn=h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_446._https.proxied-a.ech.jelle.dev. 60 IN HTTPS 1 proxied-a.ech.jelle.dev. alpn=h3, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
proxied-b.ech.jelle.dev. 60 IN A 165.232.148.125
proxied-b.ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
proxied-b.ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_444._https.proxied-b.ech.jelle.dev. 60 IN HTTPS 1 proxied-b.ech.jelle.dev. alpn=h2,http/1.1, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_446._https.proxied-b.ech.jelle.dev. 60 IN HTTPS 1 proxied-b.ech.jelle.dev. alpn=h3, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
public.ech.jelle.dev. 60 IN A 165.232.148.125
public.ech.jelle.dev. 60 IN AAAA 2604:a880:4:1d0:0:2:4b59:1000
public.ech.jelle.dev. 60 IN HTTPS 1 . alpn=h3,h2,http/1.1,
_444._https.public.ech.jelle.dev. 60 IN HTTPS 1 public.ech.jelle.dev. alpn=h2,http/1.1,
_446._https.public.ech.jelle.dev. 60 IN HTTPS 1 public.ech.jelle.dev. alpn=h3,

Port-prefix records (_444._https.*, _446._https.*) use the bare domain as TargetName (required for Chrome compatibility).