Documentation
¶
Overview ¶
Package ech implements a library to support Encrypted Client Hello with a Split Mode Topology (a.k.a. TLS Passthrough), along with secure client-side name resolution and network connections.
Split Mode Topology is defined in https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/#section-3.1
Client ----> Client-Facing Server ----> Backend Servers (public.example.com) (private1.example.com) (private2.example.com)
A ech.Conn handles the Client-Facing Server part. It transparently inspects the TLS handshake and decrypts/decodes Encrypted Client Hello messages. The decoded ServerName and/or ALPN protocols can then be used to route the TLS connection to the correct backend server which terminates the TLS connection.
A regular tls.Server Conn with EncryptedClientHelloKeys set in its tls.Config is required to handle the ECH Config PublicName. The other backend servers don't need the ECH keys.
ln, err := net.Listen("tcp", ":8443") if err != nil { // ... } defer ln.Close() for { serverConn, err := ln.Accept() if err != nil { // ... } go func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() conn, err := ech.NewConn(ctx, serverConn, ech.WithKeys(echKeys)) if err != nil { // ... return } log.Printf("ServerName: %s", conn.ServerName()) log.Printf("ALPNProtos: %s", conn.ALPNProtos()) switch host := conn.ServerName(); host { case "public.example.com": server := tls.Server(conn, &tls.Config{ Certificates: []tls.Certificate{tlsCert}, EncryptedClientHelloKeys: echKeys, }) fmt.Fprintf(server, "Hello, this is public.example.com\n") server.Close() default: // The TLS connection can terminate here, or conn could // be forwarded to another backend server. server := tls.Server(conn, &tls.Config{ Certificates: []tls.Certificate{tlsCert}, }) fmt.Fprintf(server, "Hello, this is %s\n", host) server.Close() } }() }
ECH Configs and ECH ConfigLists are created with ech.NewConfig and ech.ConfigList.
Clients can use ech.Resolve, ech.Dial, and/or ech.Transport to securely connect to services. They use RFC 8484 DNS-over-HTTPS (DoH) and RFC 9460 HTTPS Resource Records, along with traditional A, AAAA, CNAME records for name resolution. If a HTTPS record contains an ECH config list, it can be used automatically. ech.Dial also supports concurrent connection attempts to gracefully handle slow or unreachable addresses. See ech.Dialer for more details.
The example directory has working client and server examples.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidName = errors.New("invalid name") ErrFormatError = errors.New("format error") ErrServerFailure = errors.New("server failure") ErrNonExistentDomain = errors.New("non-existent domain") ErrNotImplemented = errors.New("not implemented") ErrQueryRefused = errors.New("query refused") )
var ( ErrUnexpectedMessage = errors.New("unexpected message") ErrIllegalParameter = errors.New("illegal parameter") ErrDecodeError = errors.New("decode error") ErrMissingExtension = errors.New("missing extension") ErrDecryptError = errors.New("decrypt error") )
var DefaultResolver = CloudflareResolver()
DefaultResolver is used by Resolve, Dial and github.com/c2FmZQ/ech/quic.Dial.
Functions ¶
func ConfigList ¶
Config returns a serialized Encrypted Client Hello (ECH) Config List.
Example ¶
_, config, err := NewConfig(1, []byte("example.com")) if err != nil { log.Fatalf("NewConfig: %v", err) } configList, err := ConfigList([]Config{config}) if err != nil { log.Fatalf("ConfigList: %v", err) } fmt.Println(base64.StdEncoding.EncodeToString(configList))
Output:
func Dial ¶
Dial connects to the given network and address. Name resolution is done with DefaultResolver. It uses HTTPS DNS records to retrieve the server's Encrypted Client Hello (ECH) Config List and uses it automatically if found.
If the name resolution returns multiple IP addresses, Dial iterates over them until a connection is successfully established.
Multiple comma-separated addresses may be provided. Dial attempts to connect to them in the order they are listed.
Dial is equivalent to:
NewDialer().Dial(...)
For finer control, instantiate a Dialer first. Then, call Dial:
dialer := NewDialer() dialer.RequireECH = true conn, err := dialer.Dial(...)
Example ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() conn, err := Dial(ctx, "tcp", "private.example.com", &tls.Config{}) if err != nil { log.Fatalf("Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
Example (Multiple) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() conn, err := Dial(ctx, "tcp", "private1.example.com,private2.example.com", &tls.Config{}) if err != nil { log.Fatalf("Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
Example (Multiple_ip_addresses) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() conn, err := Dial(ctx, "tcp", "192.168.0.1,192.168.0.2,192.168.0.3", &tls.Config{}) if err != nil { log.Fatalf("Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
Example (Uri) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() conn, err := Dial(ctx, "tcp", "https://private.example.com:8443", &tls.Config{}) if err != nil { log.Fatalf("Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
Example (With_ports) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() conn, err := Dial(ctx, "tcp", "private1.example.com:8443,private2.example.com:10443,192.168.0.3", &tls.Config{}) if err != nil { log.Fatalf("Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
Types ¶
type CipherSuite ¶
type Config ¶
type Config []byte
Config is a serialized Encrypted Client Hello (ECH) Config.
func NewConfig ¶
NewConfig generates an Encrypted Client Hello (ECH) Config and a private key. It currently supports:
- DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305.
- DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM.
- DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM.
func (Config) Spec ¶
func (cfg Config) Spec() (ConfigSpec, error)
Spec returns the structured version of cfg.
type ConfigSpec ¶
type ConfigSpec struct { Version uint16 ID uint8 KEM uint16 PublicKey []byte CipherSuites []CipherSuite MaximumNameLength uint8 PublicName []byte }
ConfigSpec represents an Encrypted Client Hello (ECH) Config. It is specified in Section 4 of https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/
func ParseConfigList ¶ added in v0.2.0
func ParseConfigList(configList []byte) ([]ConfigSpec, error)
ParseConfigList parses a serialized Encrypted Client Hello (ECH) Config List.
func (ConfigSpec) Bytes ¶
func (c ConfigSpec) Bytes() (Config, error)
Bytes returns the serialized version of the Encrypted Client Hello (ECH) Config.
type Conn ¶
type Conn struct { net.Conn // The underlying connection // contains filtered or unexported fields }
Conn manages Encrypted Client Hello in TLS connections, as defined in https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/ .
func NewConn ¶
NewConn returns a Conn that manages Encrypted Client Hello in TLS connections, as defined in https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/ .
Encrypted Client Hello handshake messages are decrypted and replaced with the ClientHelloInner transparently. If decryption fails, the HelloClientOuter is used instead.
When NewConn() returns, the first ClientHello message has already been processed. Conn continues to inspect the other handshake messages for retries. If ClientHello is retried, it will be processed similarly to the first one, with some extra restrictions.
The ctx is used while reading the initial ClientHello only. It is not used after New returns.
Example ¶
ctx := context.Background() privKey, config, err := NewConfig(1, []byte("public.example.com")) if err != nil { log.Fatalf("NewConfig: %v", err) } ln, err := net.Listen("tcp", "localhost:0") if err != nil { log.Fatalf("net.Listen: %v", err) } defer ln.Close() for { serverConn, err := ln.Accept() if err != nil { log.Fatalf("ln.Accept: %v", err) } conn, err := NewConn(ctx, serverConn, WithKeys([]Key{{ Config: config, PrivateKey: privKey.Bytes(), SendAsRetry: true, }})) if err != nil { log.Printf("NewConn: %v", err) continue } switch host := conn.ServerName(); host { case "public.example.com": // Forward conn to a tls.Server for public.example.com // ... default: // Forward conn to a tls.Server for conn.ServerName() // ... } }
Output:
func (*Conn) ALPNProtos ¶
ALPNProtos returns the ALPN protocol values extracted from the ClientHello.
func (*Conn) ECHAccepted ¶
ECHAccepted indicates whether the client's Encrypted Client Hello was successfully decrypted and validated.
func (*Conn) ECHPresented ¶
ECHPresented indicates whether the client presented an Encrypted Client Hello.
func (*Conn) ServerName ¶
ServerName returns the SNI value extracted from the ClientHello.
type Dialer ¶ added in v0.2.0
type Dialer[T any] struct { // RequireECH indicates that Encrypted Client Hello must be available // and successfully negotiated for Dial to return successfully. // By default, when RequireECH is false, Dial falls back to regular // plaintext Client Hello when a Config List isn't found. RequireECH bool // Resolver specifies the resolver to use for DNS lookups. If nil, // DefaultResolver is used. When Dialer is used by Transport, this // value is ignored. Resolver *Resolver // PublicName is used to fetch the ECH Config List from the server when // the Config List isn't specified in the tls.Config or in DNS. In // that case, Dial generates a fake (but valid) Config List with this // PublicName and use it to establish a TLS connection with the server, // which should return the real Config List in RetryConfigList. PublicName string // MaxConcurrency specifies the maximum number of connections that can // be attempted in parallel by Dial when the network address resolves to // multiple targets. The default value is 3. MaxConcurrency int // ConcurrencyDelay is the amount of time to wait before initiating a // new concurrent connection attempt. The default is 1s. ConcurrencyDelay time.Duration // Timeout is the amount of time to wait for a single connection to be // established. The default value is 30s. Timeout time.Duration // DialFunc must be set to a function that will be used to connect to // a network address. NewDialer automatically sets this value. DialFunc func(ctx context.Context, network, addr string, tc *tls.Config) (T, error) }
Dialer contains options for connecting to an address using Encrypted Client Hello. It retrieves the Encrypted Client Hello (ECH) Config List automatically from DNS, or from the remote server itself.
Dialer uses RFC 8484 DNS-over-HTTPS (DoH) and RFC 9460 HTTPS Resource Records, along with traditional A, AAAA, CNAME records for name resolution. If a HTTPS record contains an ECH config list, it can be used automatically. Dialer.Dial also supports concurrent connection attempts to gracefully handle slow or unreachable addresses.
func NewDialer ¶ added in v0.2.0
NewDialer returns a tls.Conn Dialer.
Example ¶
dialer := NewDialer() dialer.RequireECH = true dialer.PublicName = "public.example.com" ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) conn, err := dialer.Dial(ctx, "tcp", "private.example.com", &tls.Config{}) cancel() if err != nil { log.Fatalf("dialer.Dial: %v", err) } defer conn.Close() fmt.Fprintln(conn, "Hello!")
Output:
func (*Dialer[T]) Dial ¶ added in v0.2.0
Dial connects to the given network and address. It uses HTTPS DNS records to retrieve the server's Encrypted Client Hello (ECH) Config List and uses it automatically if found.
If the name resolution returns multiple IP addresses, Dial iterates over them until a connection is successfully established. See Dialer for finer control.
Multiple comma-separated addresses may be provided. Dial attempts to connect to them in the order they are listed.
type Key ¶
type Key = tls.EncryptedClientHelloKey
type Option ¶
type Option func(*Conn)
Option is a argument passed to NewConn.
type ResolveResult ¶
type ResolveResult struct { Port uint16 Address []net.IP HTTPS []dns.HTTPS Additional map[string][]net.IP }
ResolveResult contains the A and HTTPS records.
func Resolve ¶
func Resolve(ctx context.Context, name string) (ResolveResult, error)
Resolve is an alias for Resolver.Resolve with DefaultResolver.
type Resolver ¶ added in v0.1.2
type Resolver struct {
// contains filtered or unexported fields
}
Resolver is a RFC 8484 DNS-over-HTTPS (DoH) client.
The resolver uses HTTPS DNS Resource Records whenever possible to retrieve the service's current Encrypted Client Hello (ECH) ConfigList. It also follows the RFC 9460 specifications to interpret the other HTTPS RR fields.
The ResolveResult contains all the IP addresses, and final HTTPS records needed to establish a secure and private TLS connection using ECH.
func CloudflareResolver ¶ added in v0.1.2
func CloudflareResolver() *Resolver
CloudflareResolver uses Cloudflare's DNS-over-HTTPS service. https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/
func GoogleResolver ¶ added in v0.1.2
func GoogleResolver() *Resolver
GoogleResolver uses Google's DNS-over-HTTPS service. https://developers.google.com/speed/public-dns/docs/doh
func InsecureGoResolver ¶ added in v0.3.3
func InsecureGoResolver() *Resolver
InsecureGoResolver uses the default GO resolver. This option exists for testing purposes and for cases where DoH is not desired. It does NOT use HTTPS RRs.
func NewResolver ¶ added in v0.1.2
NewResolver returns a resolver that uses any RFC 8484 compliant DNS-over-HTTPS service. See https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers for a list of publicly available servers.
func WikimediaResolver ¶ added in v0.1.2
func WikimediaResolver() *Resolver
WikimediaResolver uses Wikimedia's DNS-over-HTTPS service. https://meta.wikimedia.org/wiki/Wikimedia_DNS
func (*Resolver) Resolve ¶ added in v0.1.2
Resolve uses DNS-over-HTTPS to resolve name.
The name argument can be any of:
- an IP address or a hostname
- an IP address or a hostname followed by a colon and a port number
- a fully qualified URI
Resolve uses the scheme and port number to locate the correct HTTPS RR as specified in RFC 9460 section 2.3. When left unspecified, the default scheme and port values are https and 443, respectively.
For example:
- example.com:8443 uses QNAME _8443._https.example.com
- foo://example.com:123 uses QNAME _123._foo.example.com
- example.com, example.com:433, example.com:80 all use QNAME example.com
If the scheme is either http or https and the port is either 80 or 443, the QNAME used is always the hostname by itself, without _port and _service.
A and AAAA RRs are looked up with just the hostname as QNAME.
Example ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() result, err := DefaultResolver.Resolve(ctx, "private.example.com") if err != nil { log.Fatalf("Resolve: %v", err) } for target := range result.Targets("tcp") { fmt.Printf("Address: %s ECH: %s\n", target.Address, base64.StdEncoding.EncodeToString(target.ECH)) }
Output:
Example (With_port) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() result, err := DefaultResolver.Resolve(ctx, "private.example.com:8443") if err != nil { log.Fatalf("Resolve: %v", err) } for target := range result.Targets("tcp") { fmt.Printf("Address: %s ECH: %s\n", target.Address, base64.StdEncoding.EncodeToString(target.ECH)) }
Output:
Example (With_uri) ¶
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() result, err := DefaultResolver.Resolve(ctx, "https://private.example.com:8443") if err != nil { log.Fatalf("Resolve: %v", err) } for target := range result.Targets("tcp") { fmt.Printf("Address: %s ECH: %s\n", target.Address, base64.StdEncoding.EncodeToString(target.ECH)) }
Output:
func (*Resolver) SetCacheSize ¶ added in v0.2.7
SetCacheSize sets the size of the DNS cache. The default size is 32. A zero or negative value disables caching.
type Transport ¶ added in v0.3.2
type Transport struct { // This http.Transport is used to execute the HTTP transaction. The // DialContext and DialTLSContext functions are set by NewTransport // and should not be modified. HTTPTransport *http.Transport // This RoundTripper is used to execute the HTTP transaction using the // HTTP/3 protocol. This value is optional. If set, it is used only when // the hostname has an HTTPS RR with h3 present in its ALPN list with a // lower Priority value than any with h2 or http/1.1. // See github.com/c2FmZQ/ech/quic/h3 NewTransport HTTP3Transport http.RoundTripper // This Resolver is used for DNS name resolution. NewTransport() sets // it to DefaultResolver. Any valid Resolver can be used. Resolver *Resolver // This Dialer is used to dial the TLS connection. Its parameters can // be modified as needed. Dialer *Dialer[*tls.Conn] // This tls.Config is used when dialing the TLS connection. A nil value // is generally fine. TLSConfig *tls.Config }
Transport is a http.RoundTripper that uses Resolver, Dialer, and http.Transport to execute an HTTP transaction using Encrypted Client Hello in the underlying TLS connection.
Example ¶
url := "https://private.example.com" transport := NewTransport() transport.Dialer.RequireECH = true client := &http.Client{Transport: transport} resp, err := client.Get(url) if err != nil { log.Fatalf("%q: %v", url, err) } defer resp.Body.Close() fmt.Printf("==== %s Status:%d ====\n", url, resp.StatusCode) io.Copy(os.Stdout, resp.Body)
Output:
func NewTransport ¶ added in v0.3.2
func NewTransport() *Transport
NewTransport returns a Transport that is ready to be used with http.Client.
By default, the returned Transport uses Encrypted Client Hello opportunistically and refuses to execute plaintext HTTP transactions. This behavior can be changed by modifiying the appropriate parameters.
For example, to require ECH, set Dialer.RequireECH = true. To allow plaintext HTTP, set HTTPTransport.DialContext = nil.
Source Files
¶
Directories
¶
Path | Synopsis |
---|---|
Package dns implements low-level DNS message encoding and decoding to interface with RFC 8484 "DNS Queries over HTTPS" (DoH) services.
|
Package dns implements low-level DNS message encoding and decoding to interface with RFC 8484 "DNS Queries over HTTPS" (DoH) services. |
cmd
This is an example showing how to send a DoH request using dns.DoH and dns.Message.
|
This is an example showing how to send a DoH request using dns.DoH and dns.Message. |
example
|
|
client
This is an example of a client using ech.Dial to connect to a TLS server using Encrypted Client Hello (ECH).
|
This is an example of a client using ech.Dial to connect to a TLS server using Encrypted Client Hello (ECH). |
decode
This is an example showing how to decode an Encrypted Client Hello (ECH) Config List.
|
This is an example showing how to decode an Encrypted Client Hello (ECH) Config List. |
httpclient
This is an example of an HTTP client using ech.Transport to send an HTTP request using Encrypted Client Hello (ECH).
|
This is an example of an HTTP client using ech.Transport to send an HTTP request using Encrypted Client Hello (ECH). |
resolve
This is an example showing how to use ech.Resolve to securely and privately find the Encrypted Client Hello (ECH) Config List for a DNS name.
|
This is an example showing how to use ech.Resolve to securely and privately find the Encrypted Client Hello (ECH) Config List for a DNS name. |
server
This is an example of a Client-Facing Server using ech.Conn to handle the TLS Encrypted Client Hello (ECH) message and routing the connection using the encrypting Server Name Indication (SNI).
|
This is an example of a Client-Facing Server using ech.Conn to handle the TLS Encrypted Client Hello (ECH) message and routing the connection using the encrypting Server Name Indication (SNI). |
internal
|
|
hpke/internal/byteorder
Package byteorder provides functions for decoding and encoding little and big endian integer types from/to byte slices.
|
Package byteorder provides functions for decoding and encoding little and big endian integer types from/to byte slices. |
publish
module
|
|
quic
module
|
|