Add ECH support for uTLS

This commit is contained in:
Restia-Ashbell 2025-06-12 09:13:23 +08:00 committed by 世界
parent 0a5f09f147
commit d8e1cd0d51
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
6 changed files with 87 additions and 66 deletions

View File

@ -25,7 +25,7 @@ import (
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
) )
func parseECHClientConfig(ctx context.Context, stdConfig *STDClientConfig, options option.OutboundTLSOptions) (Config, error) { func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
var echConfig []byte var echConfig []byte
if len(options.ECH.Config) > 0 { if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n")) echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
@ -45,12 +45,12 @@ func parseECHClientConfig(ctx context.Context, stdConfig *STDClientConfig, optio
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem") return nil, E.New("invalid ECH configs pem")
} }
stdConfig.config.EncryptedClientHelloConfigList = block.Bytes clientConfig.SetECHConfigList(block.Bytes)
return stdConfig, nil return clientConfig, nil
} else { } else {
return &STDECHClientConfig{ return &ECHClientConfig{
STDClientConfig: stdConfig, ECHCapableConfig: clientConfig,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
}, nil }, nil
} }
} }
@ -102,15 +102,15 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return nil return nil
} }
type STDECHClientConfig struct { type ECHClientConfig struct {
*STDClientConfig ECHCapableConfig
access sync.Mutex access sync.Mutex
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
lastTTL time.Duration lastTTL time.Duration
lastUpdate time.Time lastUpdate time.Time
} }
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn) tlsConn, err := s.fetchAndHandshake(ctx, conn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -122,17 +122,17 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
return tlsConn, nil return tlsConn, nil
} }
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL { if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []mDNS.Question{ Question: []mDNS.Question{
{ {
Name: mDNS.Fqdn(s.config.ServerName), Name: mDNS.Fqdn(s.ServerName()),
Qtype: mDNS.TypeHTTPS, Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET, Qclass: mDNS.ClassINET,
}, },
@ -157,21 +157,21 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
} }
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now() s.lastUpdate = time.Now()
s.config.EncryptedClientHelloConfigList = echConfigList s.SetECHConfigList(echConfigList)
break match break match
} }
} }
} }
} }
if len(s.config.EncryptedClientHelloConfigList) == 0 { if len(s.ECHConfigList()) == 0 {
return nil, E.New("no ECH config found in DNS records") return nil, E.New("no ECH config found in DNS records")
} }
} }
return s.Client(conn) return s.Client(conn)
} }
func (s *STDECHClientConfig) Clone() Config { func (s *ECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig: s.STDClientConfig.Clone().(*STDClientConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate} return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@ -11,6 +11,12 @@ import (
"github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem"
) )
type ECHCapableConfig interface {
Config
ECHConfigList() []byte
SetECHConfigList([]byte)
}
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) { func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{ cipherSuites := []echCipherSuite{
{ {

View File

@ -10,7 +10,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) { func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.") return nil, E.New("ECH requires go1.24, please recompile your binary.")
} }

View File

@ -74,7 +74,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
if decodedLen > 8 { if decodedLen > 8 {
return nil, E.New("invalid short_id") return nil, E.New("invalid short_id")
} }
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
} }
func (e *RealityClientConfig) ServerName() string { func (e *RealityClientConfig) ServerName() string {

View File

@ -24,35 +24,43 @@ type STDClientConfig struct {
recordFragment bool recordFragment bool
} }
func (s *STDClientConfig) ServerName() string { func (c *STDClientConfig) ServerName() string {
return s.config.ServerName return c.config.ServerName
} }
func (s *STDClientConfig) SetServerName(serverName string) { func (c *STDClientConfig) SetServerName(serverName string) {
s.config.ServerName = serverName c.config.ServerName = serverName
} }
func (s *STDClientConfig) NextProtos() []string { func (c *STDClientConfig) NextProtos() []string {
return s.config.NextProtos return c.config.NextProtos
} }
func (s *STDClientConfig) SetNextProtos(nextProto []string) { func (c *STDClientConfig) SetNextProtos(nextProto []string) {
s.config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
func (s *STDClientConfig) Config() (*STDConfig, error) { func (c *STDClientConfig) Config() (*STDConfig, error) {
return s.config, nil return c.config, nil
} }
func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) { func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
if s.recordFragment { if c.recordFragment {
conn = tf.NewConn(conn, s.ctx, s.fragment, s.recordFragment, s.fragmentFallbackDelay) conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
} }
return tls.Client(conn, s.config), nil return tls.Client(conn, c.config), nil
} }
func (s *STDClientConfig) Clone() Config { func (c *STDClientConfig) Clone() Config {
return &STDClientConfig{s.ctx, s.config.Clone(), s.fragment, s.fragmentFallbackDelay, s.recordFragment} return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment}
}
func (c *STDClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
} }
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@ -69,9 +77,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig tls.Config var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI { if !options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName tlsConfig.ServerName = serverName
} }
if options.Insecure { if options.Insecure {

View File

@ -8,7 +8,6 @@ import (
"crypto/x509" "crypto/x509"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"os" "os"
"strings" "strings"
"time" "time"
@ -32,46 +31,54 @@ type UTLSClientConfig struct {
recordFragment bool recordFragment bool
} }
func (e *UTLSClientConfig) ServerName() string { func (c *UTLSClientConfig) ServerName() string {
return e.config.ServerName return c.config.ServerName
} }
func (e *UTLSClientConfig) SetServerName(serverName string) { func (c *UTLSClientConfig) SetServerName(serverName string) {
e.config.ServerName = serverName c.config.ServerName = serverName
} }
func (e *UTLSClientConfig) NextProtos() []string { func (c *UTLSClientConfig) NextProtos() []string {
return e.config.NextProtos return c.config.NextProtos
} }
func (e *UTLSClientConfig) SetNextProtos(nextProto []string) { func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS {
nextProto = append(nextProto, "http/1.1") nextProto = append(nextProto, "http/1.1")
} }
e.config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
func (e *UTLSClientConfig) Config() (*STDConfig, error) { func (c *UTLSClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS") return nil, E.New("unsupported usage for uTLS")
} }
func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
if e.recordFragment { if c.recordFragment {
conn = tf.NewConn(conn, e.ctx, e.fragment, e.recordFragment, e.fragmentFallbackDelay) conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
} }
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil
} }
func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
e.config.SessionIDGenerator = generator c.config.SessionIDGenerator = generator
} }
func (e *UTLSClientConfig) Clone() Config { func (c *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{ return &UTLSClientConfig{
e.ctx, e.config.Clone(), e.id, e.fragment, e.fragmentFallbackDelay, e.recordFragment, c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
} }
} }
func (c *UTLSClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
}
type utlsConnWrapper struct { type utlsConnWrapper struct {
*utls.UConn *utls.UConn
} }
@ -124,14 +131,12 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
return c.UConn.HandshakeContext(ctx) return c.UConn.HandshakeContext(ctx)
} }
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) { func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
serverName = options.ServerName serverName = options.ServerName
} else if serverAddress != "" { } else if serverAddress != "" {
if _, err := netip.ParseAddr(serverName); err != nil { serverName = serverAddress
serverName = serverAddress
}
} }
if serverName == "" && !options.Insecure { if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true") return nil, E.New("missing server_name or insecure=true")
@ -140,11 +145,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
var tlsConfig utls.Config var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI { tlsConfig.ServerName = serverName
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName
}
if options.Insecure { if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI { } else if options.DisableSNI {
@ -200,7 +201,15 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}, nil uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")
}
return parseECHClientConfig(ctx, uConfig, options)
} else {
return uConfig, nil
}
} }
var ( var (
@ -228,7 +237,7 @@ func init() {
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
switch name { switch name {
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq": case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk":
fallthrough fallthrough
case "chrome", "": case "chrome", "":
return utls.HelloChrome_Auto, nil return utls.HelloChrome_Auto, nil