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 }