瀏覽代碼

Add ECH TLS client

世界 3 年之前
父節點
當前提交
3ad4370fa5
共有 8 個文件被更改,包括 242 次插入18 次删除
  1. 5 1
      common/tls/client.go
  2. 188 0
      common/tls/ech_client.go
  3. 1 0
      go.mod
  4. 6 0
      go.sum
  5. 18 10
      option/tls.go
  6. 2 0
      transport/cloudflaretls/common.go
  7. 21 6
      transport/cloudflaretls/ech.go
  8. 1 1
      transport/cloudflaretls/handshake_client.go

+ 5 - 1
common/tls/client.go

@@ -21,7 +21,11 @@ func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress
 }
 }
 
 
 func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
 func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
-	return newStdClient(serverAddress, options)
+	if options.ECH != nil && options.ECH.Enabled {
+		return newECHClient(router, serverAddress, options)
+	} else {
+		return newStdClient(serverAddress, options)
+	}
 }
 }
 
 
 func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (net.Conn, error) {
 func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (net.Conn, error) {

+ 188 - 0
common/tls/ech_client.go

@@ -0,0 +1,188 @@
+//go:build with_ech
+
+package tls
+
+import (
+	"context"
+	"crypto/x509"
+	"encoding/base64"
+	"net"
+	"net/netip"
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/cloudflaretls"
+	"github.com/sagernet/sing-dns"
+	E "github.com/sagernet/sing/common/exceptions"
+
+	mDNS "github.com/miekg/dns"
+	"golang.org/x/net/dns/dnsmessage"
+)
+
+type echClientConfig struct {
+	config *tls.Config
+}
+
+func (e *echClientConfig) Config() (*STDConfig, error) {
+	return nil, E.New("unsupported usage for ECH")
+}
+
+func (e *echClientConfig) Client(conn net.Conn) Conn {
+	return tls.Client(conn, e.config)
+}
+
+func newECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
+	var serverName string
+	if options.ServerName != "" {
+		serverName = options.ServerName
+	} else if serverAddress != "" {
+		if _, err := netip.ParseAddr(serverName); err != nil {
+			serverName = serverAddress
+		}
+	}
+	if serverName == "" && !options.Insecure {
+		return nil, E.New("missing server_name or insecure=true")
+	}
+
+	var tlsConfig tls.Config
+	if options.DisableSNI {
+		tlsConfig.ServerName = "127.0.0.1"
+	} else {
+		tlsConfig.ServerName = serverName
+	}
+	if options.Insecure {
+		tlsConfig.InsecureSkipVerify = options.Insecure
+	} else if options.DisableSNI {
+		tlsConfig.InsecureSkipVerify = true
+		tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
+			verifyOptions := x509.VerifyOptions{
+				DNSName:       serverName,
+				Intermediates: x509.NewCertPool(),
+			}
+			for _, cert := range state.PeerCertificates[1:] {
+				verifyOptions.Intermediates.AddCert(cert)
+			}
+			_, err := state.PeerCertificates[0].Verify(verifyOptions)
+			return err
+		}
+	}
+	if len(options.ALPN) > 0 {
+		tlsConfig.NextProtos = options.ALPN
+	}
+	if options.MinVersion != "" {
+		minVersion, err := ParseTLSVersion(options.MinVersion)
+		if err != nil {
+			return nil, E.Cause(err, "parse min_version")
+		}
+		tlsConfig.MinVersion = minVersion
+	}
+	if options.MaxVersion != "" {
+		maxVersion, err := ParseTLSVersion(options.MaxVersion)
+		if err != nil {
+			return nil, E.Cause(err, "parse max_version")
+		}
+		tlsConfig.MaxVersion = maxVersion
+	}
+	if options.CipherSuites != nil {
+	find:
+		for _, cipherSuite := range options.CipherSuites {
+			for _, tlsCipherSuite := range tls.CipherSuites() {
+				if cipherSuite == tlsCipherSuite.Name {
+					tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
+					continue find
+				}
+			}
+			return nil, E.New("unknown cipher_suite: ", cipherSuite)
+		}
+	}
+	var certificate []byte
+	if options.Certificate != "" {
+		certificate = []byte(options.Certificate)
+	} else if options.CertificatePath != "" {
+		content, err := os.ReadFile(options.CertificatePath)
+		if err != nil {
+			return nil, E.Cause(err, "read certificate")
+		}
+		certificate = content
+	}
+	if len(certificate) > 0 {
+		certPool := x509.NewCertPool()
+		if !certPool.AppendCertsFromPEM(certificate) {
+			return nil, E.New("failed to parse certificate:\n\n", certificate)
+		}
+		tlsConfig.RootCAs = certPool
+	}
+
+	// ECH Config
+
+	tlsConfig.ECHEnabled = true
+	tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
+	tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
+	if options.ECH.Config != "" {
+		clientConfigContent, err := base64.StdEncoding.DecodeString(options.ECH.Config)
+		if err != nil {
+			return nil, err
+		}
+		clientConfig, err := tls.UnmarshalECHConfigs(clientConfigContent)
+		if err != nil {
+			return nil, err
+		}
+		tlsConfig.ClientECHConfigs = clientConfig
+	} else {
+		tlsConfig.GetClientECHConfigs = fetchECHClientConfig(router)
+	}
+	return &echClientConfig{&tlsConfig}, nil
+}
+
+const typeHTTPS = 65
+
+func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serverName string) ([]tls.ECHConfig, error) {
+	return func(ctx context.Context, serverName string) ([]tls.ECHConfig, error) {
+		message := &dnsmessage.Message{
+			Header: dnsmessage.Header{
+				RecursionDesired: true,
+			},
+			Questions: []dnsmessage.Question{
+				{
+					Name:  dnsmessage.MustNewName(serverName + "."),
+					Type:  typeHTTPS,
+					Class: dnsmessage.ClassINET,
+				},
+			},
+		}
+		response, err := router.Exchange(ctx, message)
+		if err != nil {
+			return nil, err
+		}
+		if response.RCode != dnsmessage.RCodeSuccess {
+			return nil, dns.RCodeError(response.RCode)
+		}
+		content, err := response.Pack()
+		if err != nil {
+			return nil, err
+		}
+		var mMsg mDNS.Msg
+		err = mMsg.Unpack(content)
+		if err != nil {
+			return nil, err
+		}
+		for _, rr := range mMsg.Answer {
+			switch resource := rr.(type) {
+			case *mDNS.HTTPS:
+				for _, value := range resource.Value {
+					if value.Key().String() == "ech" {
+						echConfig, err := base64.StdEncoding.DecodeString(value.String())
+						if err != nil {
+							return nil, E.Cause(err, "decode ECH config")
+						}
+						return tls.UnmarshalECHConfigs(echConfig)
+					}
+				}
+			default:
+				return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
+			}
+		}
+		return nil, E.New("no ECH config found")
+	}
+}

+ 1 - 0
go.mod

@@ -17,6 +17,7 @@ require (
 	github.com/hashicorp/yamux v0.1.1
 	github.com/hashicorp/yamux v0.1.1
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/mholt/acmez v1.0.4
 	github.com/mholt/acmez v1.0.4
+	github.com/miekg/dns v1.1.50
 	github.com/oschwald/maxminddb-golang v1.10.0
 	github.com/oschwald/maxminddb-golang v1.10.0
 	github.com/pires/go-proxyproto v0.6.2
 	github.com/pires/go-proxyproto v0.6.2
 	github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a
 	github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a

+ 6 - 0
go.sum

@@ -103,6 +103,8 @@ github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK
 github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
 github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
 github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
 github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
 github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
 github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
+github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -215,6 +217,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
@@ -225,6 +228,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -245,6 +249,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -273,6 +278,7 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg=
 golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg=
 golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 18 - 10
option/tls.go

@@ -15,14 +15,22 @@ type InboundTLSOptions struct {
 }
 }
 
 
 type OutboundTLSOptions struct {
 type OutboundTLSOptions struct {
-	Enabled         bool             `json:"enabled,omitempty"`
-	DisableSNI      bool             `json:"disable_sni,omitempty"`
-	ServerName      string           `json:"server_name,omitempty"`
-	Insecure        bool             `json:"insecure,omitempty"`
-	ALPN            Listable[string] `json:"alpn,omitempty"`
-	MinVersion      string           `json:"min_version,omitempty"`
-	MaxVersion      string           `json:"max_version,omitempty"`
-	CipherSuites    Listable[string] `json:"cipher_suites,omitempty"`
-	Certificate     string           `json:"certificate,omitempty"`
-	CertificatePath string           `json:"certificate_path,omitempty"`
+	Enabled         bool                `json:"enabled,omitempty"`
+	DisableSNI      bool                `json:"disable_sni,omitempty"`
+	ServerName      string              `json:"server_name,omitempty"`
+	Insecure        bool                `json:"insecure,omitempty"`
+	ALPN            Listable[string]    `json:"alpn,omitempty"`
+	MinVersion      string              `json:"min_version,omitempty"`
+	MaxVersion      string              `json:"max_version,omitempty"`
+	CipherSuites    Listable[string]    `json:"cipher_suites,omitempty"`
+	Certificate     string              `json:"certificate,omitempty"`
+	CertificatePath string              `json:"certificate_path,omitempty"`
+	ECH             *OutboundECHOptions `json:"ech,omitempty"`
+}
+
+type OutboundECHOptions struct {
+	Enabled                     bool   `json:"enabled,omitempty"`
+	PQSignatureSchemesEnabled   bool   `json:"pq_signature_schemes_enabled,omitempty"`
+	DynamicRecordSizingDisabled bool   `json:"dynamic_record_sizing_disabled,omitempty"`
+	Config                      string `json:"config,omitempty"`
 }
 }

+ 2 - 0
transport/cloudflaretls/common.go

@@ -834,6 +834,8 @@ type Config struct {
 	// Otherwise, if ECH is enabled, it will send a dummy ECH extension.
 	// Otherwise, if ECH is enabled, it will send a dummy ECH extension.
 	ClientECHConfigs []ECHConfig
 	ClientECHConfigs []ECHConfig
 
 
+	GetClientECHConfigs func(ctx context.Context, serverName string) ([]ECHConfig, error)
+
 	// ServerECHProvider is the ECH provider used by the client-facing server
 	// ServerECHProvider is the ECH provider used by the client-facing server
 	// for the ECH extension. If the client offers ECH and TLS 1.3 is
 	// for the ECH extension. If the client offers ECH and TLS 1.3 is
 	// negotiated, then the provider is used to compute the HPKE context
 	// negotiated, then the provider is used to compute the HPKE context

+ 21 - 6
transport/cloudflaretls/ech.go

@@ -4,6 +4,7 @@
 package tls
 package tls
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
@@ -37,7 +38,7 @@ var zeros = [8]byte{}
 //
 //
 // TODO(cjpatton): "[When offering ECH, the client] MUST NOT offer to resume any
 // TODO(cjpatton): "[When offering ECH, the client] MUST NOT offer to resume any
 // session for TLS 1.2 and below [in ClientHelloInner]."
 // session for TLS 1.2 and below [in ClientHelloInner]."
-func (c *Conn) echOfferOrGrease(helloBase *clientHelloMsg) (hello, helloInner *clientHelloMsg, err error) {
+func (c *Conn) echOfferOrGrease(ctx context.Context, helloBase *clientHelloMsg) (hello, helloInner *clientHelloMsg, err error) {
 	config := c.config
 	config := c.config
 
 
 	if !config.ECHEnabled || testingECHTriggerBypassBeforeHRR {
 	if !config.ECHEnabled || testingECHTriggerBypassBeforeHRR {
@@ -47,7 +48,10 @@ func (c *Conn) echOfferOrGrease(helloBase *clientHelloMsg) (hello, helloInner *c
 
 
 	// Choose the ECHConfig to use for this connection. If none is available, or
 	// Choose the ECHConfig to use for this connection. If none is available, or
 	// if we're not offering TLS 1.3 or above, then GREASE.
 	// if we're not offering TLS 1.3 or above, then GREASE.
-	echConfig := config.echSelectConfig()
+	echConfig, err := config.echSelectConfig(ctx, helloBase.serverName)
+	if err != nil {
+		return nil, nil, fmt.Errorf("tls: ech: fetch ech config: %s", err)
+	}
 	if echConfig == nil || config.maxSupportedVersion(roleClient) < VersionTLS13 {
 	if echConfig == nil || config.maxSupportedVersion(roleClient) < VersionTLS13 {
 		var err error
 		var err error
 
 
@@ -1008,14 +1012,26 @@ func splitClientHelloExtensions(data []byte) ([]byte, []byte) {
 //
 //
 // TODO(cjpatton): Implement ECH config extensions as described in
 // TODO(cjpatton): Implement ECH config extensions as described in
 // draft-ietf-tls-esni-13, Section 4.1.
 // draft-ietf-tls-esni-13, Section 4.1.
-func (c *Config) echSelectConfig() *ECHConfig {
+func (c *Config) echSelectConfig(ctx context.Context, serverName string) (*ECHConfig, error) {
 	for _, echConfig := range c.ClientECHConfigs {
 	for _, echConfig := range c.ClientECHConfigs {
 		if _, err := echConfig.selectSuite(); err == nil &&
 		if _, err := echConfig.selectSuite(); err == nil &&
 			echConfig.version == extensionECH {
 			echConfig.version == extensionECH {
-			return &echConfig
+			return &echConfig, nil
 		}
 		}
 	}
 	}
-	return nil
+	if c.GetClientECHConfigs != nil {
+		echConfigs, err := c.GetClientECHConfigs(ctx, serverName)
+		if err != nil {
+			return nil, err
+		}
+		for _, echConfig := range echConfigs {
+			if _, err = echConfig.selectSuite(); err == nil &&
+				echConfig.version == extensionECH {
+				return &echConfig, nil
+			}
+		}
+	}
+	return nil, nil
 }
 }
 
 
 func (c *Config) echCanOffer() bool {
 func (c *Config) echCanOffer() bool {
@@ -1023,7 +1039,6 @@ func (c *Config) echCanOffer() bool {
 		return false
 		return false
 	}
 	}
 	return c.ECHEnabled &&
 	return c.ECHEnabled &&
-		c.echSelectConfig() != nil &&
 		c.maxSupportedVersion(roleClient) >= VersionTLS13
 		c.maxSupportedVersion(roleClient) >= VersionTLS13
 }
 }
 
 

+ 1 - 1
transport/cloudflaretls/handshake_client.go

@@ -187,7 +187,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
 		return err
 		return err
 	}
 	}
 
 
-	hello, helloInner, err := c.echOfferOrGrease(helloBase)
+	hello, helloInner, err := c.echOfferOrGrease(ctx, helloBase)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}