diff --git a/common/tls/client.go b/common/tls/client.go index 4d6b0c54..d645778b 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -30,14 +30,15 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun return nil, nil } if options.ECH != nil && options.ECH.Enabled { - return NewECHClient(ctx, serverAddress, options) + if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { + return NewECHClient(ctx, serverAddress, options) + } } else if options.Reality != nil && options.Reality.Enabled { return NewRealityClient(ctx, serverAddress, options) } else if options.UTLS != nil && options.UTLS.Enabled { return NewUTLSClient(ctx, serverAddress, options) - } else { - return NewSTDClient(ctx, serverAddress, options) } + return NewSTDClient(ctx, serverAddress, options) } func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { diff --git a/common/tls/ech_keygen.go b/common/tls/ech_keygen.go index 1fea131c..e889f021 100644 --- a/common/tls/ech_keygen.go +++ b/common/tls/ech_keygen.go @@ -7,7 +7,6 @@ import ( "encoding/binary" "encoding/pem" - cftls "github.com/sagernet/cloudflare-tls" E "github.com/sagernet/sing/common/exceptions" "github.com/cloudflare/circl/hpke" @@ -59,7 +58,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config type echKeyConfigPair struct { id uint8 - key cftls.EXP_ECHKey rawKey []byte conf myECHKeyConfig rawConf []byte @@ -153,14 +151,13 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [ sk = be.AppendUint16(sk, uint16(len(b))) sk = append(sk, b...) - cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk) + cfECHKeys, err := UnmarshalECHKeys(sk) if err != nil { return nil, E.Cause(err, "bug: can't parse generated ECH server key") } if len(cfECHKeys) != 1 { return nil, E.New("bug: unexpected server key count") } - pair.key = cfECHKeys[0] pair.rawKey = sk pairs = append(pairs, pair) diff --git a/common/tls/server.go b/common/tls/server.go index 6afd89d6..3033711f 100644 --- a/common/tls/server.go +++ b/common/tls/server.go @@ -17,12 +17,13 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS return nil, nil } if options.ECH != nil && options.ECH.Enabled { - return NewECHServer(ctx, logger, options) + if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { + return NewECHServer(ctx, logger, options) + } } else if options.Reality != nil && options.Reality.Enabled { return NewRealityServer(ctx, logger, options) - } else { - return NewSTDServer(ctx, logger, options) } + return NewSTDServer(ctx, logger, options) } func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 90f51821..b89ef5e5 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -4,16 +4,25 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/base64" "net" "net/netip" "os" "strings" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" ) +var _ ConfigCompat = (*STDClientConfig)(nil) + type STDClientConfig struct { config *tls.Config } @@ -46,6 +55,63 @@ func (s *STDClientConfig) Clone() Config { return &STDClientConfig{s.config.Clone()} } +type STDECHClientConfig struct { + STDClientConfig +} + +func (s *STDClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { + if len(s.config.EncryptedClientHelloConfigList) == 0 { + message := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{ + { + Name: mDNS.Fqdn(s.config.ServerName), + Qtype: mDNS.TypeHTTPS, + Qclass: mDNS.ClassINET, + }, + }, + } + dnsRouter := service.FromContext[adapter.Router](ctx) + response, err := dnsRouter.Exchange(ctx, message) + if err != nil { + return nil, E.Cause(err, "fetch ECH config list") + } + if response.Rcode != mDNS.RcodeSuccess { + return nil, E.Cause(dns.RCodeError(response.Rcode), "fetch ECH config list") + } + for _, rr := range response.Answer { + switch resource := rr.(type) { + case *mDNS.HTTPS: + for _, value := range resource.Value { + if value.Key().String() == "ech" { + echConfigList, err := base64.StdEncoding.DecodeString(value.String()) + if err != nil { + return nil, E.Cause(err, "decode ECH config") + } + s.config.EncryptedClientHelloConfigList = echConfigList + } + } + } + } + return nil, E.New("no ECH config found in DNS records") + } + tlsConn, err := s.Client(conn) + if err != nil { + return nil, err + } + err = tlsConn.HandshakeContext(ctx) + if err != nil { + return nil, err + } + return tlsConn, nil +} + +func (s *STDECHClientConfig) Clone() Config { + return &STDECHClientConfig{STDClientConfig{s.config.Clone()}} +} + func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { var serverName string if options.ServerName != "" { @@ -128,5 +194,21 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb } tlsConfig.RootCAs = certPool } + if options.ECH != nil && options.ECH.Enabled { + var echConfig []byte + if len(options.ECH.Config) > 0 { + echConfig = []byte(strings.Join(options.ECH.Config, "\n")) + } else if options.ECH.ConfigPath != "" { + content, err := os.ReadFile(options.ECH.ConfigPath) + if err != nil { + return nil, E.Cause(err, "read ECH config") + } + echConfig = content + } + if echConfig != nil { + tlsConfig.EncryptedClientHelloConfigList = echConfig + } + return &STDECHClientConfig{STDClientConfig{&tlsConfig}}, nil + } return &STDClientConfig{&tlsConfig}, nil } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 8eab87da..5bd3ccb2 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -3,6 +3,7 @@ package tls import ( "context" "crypto/tls" + "encoding/pem" "net" "os" "strings" @@ -14,6 +15,8 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" + + "golang.org/x/crypto/cryptobyte" ) var errInsecureUnused = E.New("tls: insecure unused") @@ -238,6 +241,31 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound tlsConfig.Certificates = []tls.Certificate{keyPair} } } + if options.ECH != nil && options.ECH.Enabled { + var echKey []byte + if len(options.ECH.Key) > 0 { + echKey = []byte(strings.Join(options.ECH.Key, "\n")) + } else if options.ECH.KeyPath != "" { + content, err := os.ReadFile(options.ECH.KeyPath) + if err != nil { + return nil, E.Cause(err, "read ECH key") + } + echKey = content + } else { + return nil, E.New("missing ECH key") + } + + block, rest := pem.Decode(echKey) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + return nil, E.New("invalid ECH keys pem") + } + + echKeys, err := UnmarshalECHKeys(block.Bytes) + if err != nil { + return nil, E.Cause(err, "parse ECH keys") + } + tlsConfig.EncryptedClientHelloKeys = echKeys + } return &STDServerConfig{ config: tlsConfig, logger: logger, @@ -248,3 +276,22 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound keyPath: options.KeyPath, }, nil } + +func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { + var keys []tls.EncryptedClientHelloKey + rawString := cryptobyte.String(raw) + for !rawString.Empty() { + var key tls.EncryptedClientHelloKey + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) { + return nil, E.New("error parsing private key") + } + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) { + return nil, E.New("error parsing config") + } + keys = append(keys, key) + } + if len(keys) == 0 { + return nil, E.New("empty ECH keys") + } + return keys, nil +}