rustls ECH Demo

Proxy metadata:
Outer SNI: proxied-b.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-b.ech.jelle.dev
ECH status: not offered

Demo endpoints

Domain (h2 + h3):444 (h2 only)Mode
proxied-avisitvisitsplit-mode ECH
proxied-bvisitvisitsplit-mode ECH
directvisitvisitpassthrough ECH
noechvisitvisitno ECH (GREASE)
publicvisitvisitouter SNI endpoint

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

Setup

All traffic enters through an ECH split-mode proxy on port :443 (TCP+UDP) and :444 (TCP). 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 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,
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]
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,
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]
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]
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, ech=[version=0xfe0d config_id=1 kem=X25519 public_name=public.ech.jelle.dev max_name_len=64]
_444._https.public.ech.jelle.dev. 60 IN HTTPS 1 public.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]

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