ech

package module
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2025 License: MIT Imports: 26 Imported by: 6

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

View Source
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")
)
View Source
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")
)
View Source
var DefaultResolver = CloudflareResolver()

DefaultResolver is used by Resolve, Dial and github.com/c2FmZQ/ech/quic.Dial.

Functions

func ConfigList

func ConfigList(configs []Config) ([]byte, error)

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

func Dial(ctx context.Context, network, addr string, tc *tls.Config) (*tls.Conn, error)

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 CipherSuite struct {
	KDF  uint16
	AEAD uint16
}

type Config

type Config []byte

Config is a serialized Encrypted Client Hello (ECH) Config.

func NewConfig

func NewConfig(id uint8, publicName []byte) (*ecdh.PrivateKey, Config, error)

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

func NewConn(ctx context.Context, conn net.Conn, options ...Option) (outConn *Conn, err error)

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

func (c *Conn) ALPNProtos() []string

ALPNProtos returns the ALPN protocol values extracted from the ClientHello.

func (*Conn) ECHAccepted

func (c *Conn) ECHAccepted() bool

ECHAccepted indicates whether the client's Encrypted Client Hello was successfully decrypted and validated.

func (*Conn) ECHPresented

func (c *Conn) ECHPresented() bool

ECHPresented indicates whether the client presented an Encrypted Client Hello.

func (*Conn) Read

func (c *Conn) Read(b []byte) (int, error)

func (*Conn) ServerName

func (c *Conn) ServerName() string

ServerName returns the SNI value extracted from the ClientHello.

func (*Conn) Write

func (c *Conn) Write(b []byte) (int, error)

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

func NewDialer() *Dialer[*tls.Conn]

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

func (d *Dialer[T]) Dial(ctx context.Context, network, addr string, tc *tls.Config) (T, error)

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 Option

type Option func(*Conn)

Option is a argument passed to NewConn.

func WithDebug

func WithDebug(f func(format string, arg ...any)) Option

WithDebug enables debugging.

func WithKeys

func WithKeys(keys []Key) Option

WithKeys enables the decryption of Encrypted Client Hello messages.

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.

func (ResolveResult) Targets added in v0.1.6

func (r ResolveResult) Targets(network string) iter.Seq[Target]

Targets computes the target addresses to attempt in preferred order.

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

func NewResolver(URL string) (*Resolver, error)

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

func (r *Resolver) Resolve(ctx context.Context, name string) (ResolveResult, error)

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

func (r *Resolver) SetCacheSize(n int)

SetCacheSize sets the size of the DNS cache. The default size is 32. A zero or negative value disables caching.

type Target added in v0.1.6

type Target struct {
	Address netip.AddrPort
	ECH     []byte
	ALPN    []string
}

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.

func (*Transport) RoundTrip added in v0.3.2

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements the http.RoundTripper interface.

Directories

Path Synopsis
dns
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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL