//go:build with_utls package tls import ( "context" "crypto/tls" "crypto/x509" "math/rand" "net" "net/netip" "os" "strings" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" utls "github.com/sagernet/utls" "golang.org/x/net/http2" ) type UTLSClientConfig struct { config *utls.Config id utls.ClientHelloID } func (e *UTLSClientConfig) ServerName() string { return e.config.ServerName } func (e *UTLSClientConfig) SetServerName(serverName string) { e.config.ServerName = serverName } func (e *UTLSClientConfig) NextProtos() []string { return e.config.NextProtos } func (e *UTLSClientConfig) SetNextProtos(nextProto []string) { if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { nextProto = append(nextProto, "http/1.1") } e.config.NextProtos = nextProto } func (e *UTLSClientConfig) Config() (*STDConfig, error) { return nil, E.New("unsupported usage for uTLS") } func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil } func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { e.config.SessionIDGenerator = generator } func (e *UTLSClientConfig) Clone() Config { return &UTLSClientConfig{ config: e.config.Clone(), id: e.id, } } type utlsConnWrapper struct { *utls.UConn } func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState { state := c.Conn.ConnectionState() return tls.ConnectionState{ Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, NegotiatedProtocol: state.NegotiatedProtocol, NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains, SignedCertificateTimestamps: state.SignedCertificateTimestamps, OCSPResponse: state.OCSPResponse, TLSUnique: state.TLSUnique, } } func (c *utlsConnWrapper) Upstream() any { return c.UConn } type utlsALPNWrapper struct { utlsConnWrapper nextProtocols []string } func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error { if len(c.nextProtocols) > 0 { err := c.BuildHandshakeState() if err != nil { return err } for _, extension := range c.Extensions { if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN { alpnExtension.AlpnProtocols = c.nextProtocols err = c.BuildHandshakeState() if err != nil { return err } break } } } return c.UConn.HandshakeContext(ctx) } func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, 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 utls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) if options.DisableSNI { tlsConfig.ServerName = "127.0.0.1" } else { tlsConfig.ServerName = serverName } if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { return nil, E.New("disable_sni is unsupported in uTLS") } 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 len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } 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 } id, err := uTLSClientHelloID(options.UTLS.Fingerprint) if err != nil { return nil, err } return &UTLSClientConfig{&tlsConfig, id}, nil } var ( randomFingerprint utls.ClientHelloID randomizedFingerprint utls.ClientHelloID ) func init() { modernFingerprints := []utls.ClientHelloID{ utls.HelloChrome_Auto, utls.HelloFirefox_Auto, utls.HelloEdge_Auto, utls.HelloSafari_Auto, utls.HelloIOS_Auto, } randomFingerprint = modernFingerprints[rand.Intn(len(modernFingerprints))] weights := utls.DefaultWeights weights.TLSVersMax_Set_VersionTLS13 = 1 weights.FirstKeyShare_Set_CurveP256 = 0 randomizedFingerprint = utls.HelloRandomized randomizedFingerprint.Seed, _ = utls.NewPRNGSeed() randomizedFingerprint.Weights = &weights } func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { switch name { case "chrome", "": return utls.HelloChrome_Auto, nil case "firefox": return utls.HelloFirefox_Auto, nil case "edge": return utls.HelloEdge_Auto, nil case "safari": return utls.HelloSafari_Auto, nil case "360": return utls.Hello360_Auto, nil case "qq": return utls.HelloQQ_Auto, nil case "ios": return utls.HelloIOS_Auto, nil case "android": return utls.HelloAndroid_11_OkHttp, nil case "random": return randomFingerprint, nil case "randomized": return randomizedFingerprint, nil default: return utls.ClientHelloID{}, E.New("unknown uTLS fingerprint: ", name) } }