quic-ech adds Encrypted Client Hello (ECH) support to any quic-go-based server. It handles key generation, ECHConfig serialization, automatic rotation, and DNS record output.
Without ECH, the server hostname (SNI) is sent in plaintext during the TLS handshake — visible to anyone on the network. ECH encrypts the entire ClientHello using a public key published in DNS, so observers only see a generic outer domain (e.g. cloudflare.com).
For QUIC-based proxies like Hysteria2, server-side ECH has been missing until now. This library bridges Go 1.24's native EncryptedClientHelloKeys API with quic-go's recently fixed ECH support.
- Go 1.24+
- quic-go v0.48+
go get github.com/HaizakiKu/quic-echprovider, err := ech.NewProvider(ech.Config{
PublicName: "cloudflare.com",
KeyFile: "/etc/myserver/ech.key",
})
tlsConfig.EncryptedClientHelloKeys = provider.Keys()
defer provider.Close()Then add an HTTPS DNS record to your domain:
@ HTTPS 1 . ech=<value from provider.DNSRecord()>
| Field | Type | Default | Description |
|---|---|---|---|
PublicName |
string |
— | Outer SNI shown to observers. Required. |
KeyFile |
string |
"" |
Path to persist keys across restarts. |
RotateInterval |
time.Duration |
24h |
How often to rotate ECH keys. |
RetainCount |
int |
2 |
Number of old keys to keep for cached clients. |
provider.Keys() // → []tls.EncryptedClientHelloKey (for tls.Config)
provider.GetKeys(...) // → callback for GetEncryptedClientHelloKeys (Go 1.25+)
provider.ECHConfigList() // → []byte (raw ECHConfigList for clients)
provider.ECHConfigListBase64() // → string (base64url, for DNS record)
provider.DNSRecord() // → "1 . ech=AEn+DQ..."
provider.Close() // stop rotation goroutine// Keys rotate automatically in the background.
// Use GetKeys for zero-downtime rotation:
tlsConfig.GetEncryptedClientHelloKeys = provider.GetKeys- quic-go — QUIC implementation this library targets
- c2FmZQ/ech — Client-side ECH for QUIC (complementary)
- apernet/hysteria — Primary integration target
MIT License