github.com/sagernet/sing-box@v1.9.0-rc.20/common/tls/ech_client.go (about)

     1  //go:build with_ech
     2  
     3  package tls
     4  
     5  import (
     6  	"context"
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"encoding/base64"
    10  	"encoding/pem"
    11  	"net"
    12  	"net/netip"
    13  	"os"
    14  	"strings"
    15  
    16  	cftls "github.com/sagernet/cloudflare-tls"
    17  	"github.com/sagernet/sing-box/adapter"
    18  	"github.com/sagernet/sing-box/option"
    19  	"github.com/sagernet/sing-dns"
    20  	E "github.com/sagernet/sing/common/exceptions"
    21  	"github.com/sagernet/sing/common/ntp"
    22  
    23  	mDNS "github.com/miekg/dns"
    24  )
    25  
    26  type echClientConfig struct {
    27  	config *cftls.Config
    28  }
    29  
    30  func (c *echClientConfig) ServerName() string {
    31  	return c.config.ServerName
    32  }
    33  
    34  func (c *echClientConfig) SetServerName(serverName string) {
    35  	c.config.ServerName = serverName
    36  }
    37  
    38  func (c *echClientConfig) NextProtos() []string {
    39  	return c.config.NextProtos
    40  }
    41  
    42  func (c *echClientConfig) SetNextProtos(nextProto []string) {
    43  	c.config.NextProtos = nextProto
    44  }
    45  
    46  func (c *echClientConfig) Config() (*STDConfig, error) {
    47  	return nil, E.New("unsupported usage for ECH")
    48  }
    49  
    50  func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
    51  	return &echConnWrapper{cftls.Client(conn, c.config)}, nil
    52  }
    53  
    54  func (c *echClientConfig) Clone() Config {
    55  	return &echClientConfig{
    56  		config: c.config.Clone(),
    57  	}
    58  }
    59  
    60  type echConnWrapper struct {
    61  	*cftls.Conn
    62  }
    63  
    64  func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
    65  	state := c.Conn.ConnectionState()
    66  	return tls.ConnectionState{
    67  		Version:                     state.Version,
    68  		HandshakeComplete:           state.HandshakeComplete,
    69  		DidResume:                   state.DidResume,
    70  		CipherSuite:                 state.CipherSuite,
    71  		NegotiatedProtocol:          state.NegotiatedProtocol,
    72  		NegotiatedProtocolIsMutual:  state.NegotiatedProtocolIsMutual,
    73  		ServerName:                  state.ServerName,
    74  		PeerCertificates:            state.PeerCertificates,
    75  		VerifiedChains:              state.VerifiedChains,
    76  		SignedCertificateTimestamps: state.SignedCertificateTimestamps,
    77  		OCSPResponse:                state.OCSPResponse,
    78  		TLSUnique:                   state.TLSUnique,
    79  	}
    80  }
    81  
    82  func (c *echConnWrapper) Upstream() any {
    83  	return c.Conn
    84  }
    85  
    86  func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
    87  	var serverName string
    88  	if options.ServerName != "" {
    89  		serverName = options.ServerName
    90  	} else if serverAddress != "" {
    91  		if _, err := netip.ParseAddr(serverName); err != nil {
    92  			serverName = serverAddress
    93  		}
    94  	}
    95  	if serverName == "" && !options.Insecure {
    96  		return nil, E.New("missing server_name or insecure=true")
    97  	}
    98  
    99  	var tlsConfig cftls.Config
   100  	tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
   101  	if options.DisableSNI {
   102  		tlsConfig.ServerName = "127.0.0.1"
   103  	} else {
   104  		tlsConfig.ServerName = serverName
   105  	}
   106  	if options.Insecure {
   107  		tlsConfig.InsecureSkipVerify = options.Insecure
   108  	} else if options.DisableSNI {
   109  		tlsConfig.InsecureSkipVerify = true
   110  		tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
   111  			verifyOptions := x509.VerifyOptions{
   112  				DNSName:       serverName,
   113  				Intermediates: x509.NewCertPool(),
   114  			}
   115  			for _, cert := range state.PeerCertificates[1:] {
   116  				verifyOptions.Intermediates.AddCert(cert)
   117  			}
   118  			_, err := state.PeerCertificates[0].Verify(verifyOptions)
   119  			return err
   120  		}
   121  	}
   122  	if len(options.ALPN) > 0 {
   123  		tlsConfig.NextProtos = options.ALPN
   124  	}
   125  	if options.MinVersion != "" {
   126  		minVersion, err := ParseTLSVersion(options.MinVersion)
   127  		if err != nil {
   128  			return nil, E.Cause(err, "parse min_version")
   129  		}
   130  		tlsConfig.MinVersion = minVersion
   131  	}
   132  	if options.MaxVersion != "" {
   133  		maxVersion, err := ParseTLSVersion(options.MaxVersion)
   134  		if err != nil {
   135  			return nil, E.Cause(err, "parse max_version")
   136  		}
   137  		tlsConfig.MaxVersion = maxVersion
   138  	}
   139  	if options.CipherSuites != nil {
   140  	find:
   141  		for _, cipherSuite := range options.CipherSuites {
   142  			for _, tlsCipherSuite := range cftls.CipherSuites() {
   143  				if cipherSuite == tlsCipherSuite.Name {
   144  					tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
   145  					continue find
   146  				}
   147  			}
   148  			return nil, E.New("unknown cipher_suite: ", cipherSuite)
   149  		}
   150  	}
   151  	var certificate []byte
   152  	if len(options.Certificate) > 0 {
   153  		certificate = []byte(strings.Join(options.Certificate, "\n"))
   154  	} else if options.CertificatePath != "" {
   155  		content, err := os.ReadFile(options.CertificatePath)
   156  		if err != nil {
   157  			return nil, E.Cause(err, "read certificate")
   158  		}
   159  		certificate = content
   160  	}
   161  	if len(certificate) > 0 {
   162  		certPool := x509.NewCertPool()
   163  		if !certPool.AppendCertsFromPEM(certificate) {
   164  			return nil, E.New("failed to parse certificate:\n\n", certificate)
   165  		}
   166  		tlsConfig.RootCAs = certPool
   167  	}
   168  
   169  	// ECH Config
   170  
   171  	tlsConfig.ECHEnabled = true
   172  	tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
   173  	tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
   174  
   175  	var echConfig []byte
   176  	if len(options.ECH.Config) > 0 {
   177  		echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
   178  	} else if options.ECH.ConfigPath != "" {
   179  		content, err := os.ReadFile(options.ECH.ConfigPath)
   180  		if err != nil {
   181  			return nil, E.Cause(err, "read ECH config")
   182  		}
   183  		echConfig = content
   184  	}
   185  
   186  	if len(echConfig) > 0 {
   187  		block, rest := pem.Decode(echConfig)
   188  		if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
   189  			return nil, E.New("invalid ECH configs pem")
   190  		}
   191  		echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
   192  		if err != nil {
   193  			return nil, E.Cause(err, "parse ECH configs")
   194  		}
   195  		tlsConfig.ClientECHConfigs = echConfigs
   196  	} else {
   197  		tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
   198  	}
   199  	return &echClientConfig{&tlsConfig}, nil
   200  }
   201  
   202  func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
   203  	return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
   204  		message := &mDNS.Msg{
   205  			MsgHdr: mDNS.MsgHdr{
   206  				RecursionDesired: true,
   207  			},
   208  			Question: []mDNS.Question{
   209  				{
   210  					Name:   serverName + ".",
   211  					Qtype:  mDNS.TypeHTTPS,
   212  					Qclass: mDNS.ClassINET,
   213  				},
   214  			},
   215  		}
   216  		response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		if response.Rcode != mDNS.RcodeSuccess {
   221  			return nil, dns.RCodeError(response.Rcode)
   222  		}
   223  		for _, rr := range response.Answer {
   224  			switch resource := rr.(type) {
   225  			case *mDNS.HTTPS:
   226  				for _, value := range resource.Value {
   227  					if value.Key().String() == "ech" {
   228  						echConfig, err := base64.StdEncoding.DecodeString(value.String())
   229  						if err != nil {
   230  							return nil, E.Cause(err, "decode ECH config")
   231  						}
   232  						return cftls.UnmarshalECHConfigs(echConfig)
   233  					}
   234  				}
   235  			default:
   236  				return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
   237  			}
   238  		}
   239  		return nil, E.New("no ECH config found")
   240  	}
   241  }