Browse Source

TLS ECH: Avoid outer ALPN http/1.1 for WSS & HUS; Change `echForceQuery`'s default value to "full"; Update github.com/refraction-networking/utls to 20260301010127; Add irrelevant tests for uTLS-REALITY (#5725)

https://github.com/XTLS/Xray-core/pull/5725#issuecomment-3982680111
风扇滑翔翼 3 months ago
parent
commit
e86c365572
5 changed files with 167 additions and 6 deletions
  1. 1 1
      go.mod
  2. 2 2
      go.sum
  3. 150 0
      testing/scenarios/vless_test.go
  4. 2 2
      transport/internet/tls/ech.go
  5. 12 1
      transport/internet/tls/tls.go

+ 1 - 1
go.mod

@@ -13,7 +13,7 @@ require (
 	github.com/miekg/dns v1.1.72
 	github.com/miekg/dns v1.1.72
 	github.com/pelletier/go-toml v1.9.5
 	github.com/pelletier/go-toml v1.9.5
 	github.com/pires/go-proxyproto v0.11.0
 	github.com/pires/go-proxyproto v0.11.0
-	github.com/refraction-networking/utls v1.8.2
+	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
 	github.com/sagernet/sing v0.5.1
 	github.com/sagernet/sing v0.5.1
 	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/stretchr/testify v1.11.1
 	github.com/stretchr/testify v1.11.1

+ 2 - 2
go.sum

@@ -51,8 +51,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
 github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
 github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
 github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
-github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
-github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
+github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
+github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
 github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=

+ 150 - 0
testing/scenarios/vless_test.go

@@ -3,6 +3,7 @@ package scenarios
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/hex"
+	"sync"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -497,3 +498,152 @@ func TestVlessXtlsVisionReality(t *testing.T) {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 }
 }
+
+// This testing test all known utls fingerprint in tls.PresetFingerprints that support reality (expect unsafe and random*)
+// Beacuse figerprint support may be broken after utls/reality update
+// Known broken fingerprint: android, 360
+func TestVlessRealityFingerprints(t *testing.T) {
+	TestFingerprint := func(fingerprint string) error {
+		tcpServer := tcp.Server{
+			MsgProcessor: xor,
+		}
+		dest, err := tcpServer.Start()
+		common.Must(err)
+		defer tcpServer.Close()
+
+		userID := protocol.NewID(uuid.New())
+		serverPort := tcp.PickPort()
+		privateKey, _ := base64.RawURLEncoding.DecodeString("aGSYystUbf59_9_6LKRxD27rmSW_-2_nyd9YG_Gwbks")
+		publicKey, _ := base64.RawURLEncoding.DecodeString("E59WjnvZcQMu7tR7_BgyhycuEdBS-CtKxfImRCdAvFM")
+		shortIds := make([][]byte, 1)
+		shortIds[0] = make([]byte, 8)
+		hex.Decode(shortIds[0], []byte("0123456789abcdef"))
+		serverConfig := &core.Config{
+			App: []*serial.TypedMessage{
+				serial.ToTypedMessage(&log.Config{
+					ErrorLogType: log.LogType_None,
+				}),
+			},
+			Inbound: []*core.InboundHandlerConfig{
+				{
+					ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+						PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
+						Listen:   net.NewIPOrDomain(net.LocalHostIP),
+						StreamSettings: &internet.StreamConfig{
+							ProtocolName: "tcp",
+							SecurityType: serial.GetMessageType(&reality.Config{}),
+							SecuritySettings: []*serial.TypedMessage{
+								serial.ToTypedMessage(&reality.Config{
+									Show:        false,
+									Dest:        "www.google.com:443", // use google for now, may fail in some region
+									ServerNames: []string{"www.google.com"},
+									PrivateKey:  privateKey,
+									ShortIds:    shortIds,
+									Type:        "tcp",
+								}),
+							},
+						},
+					}),
+					ProxySettings: serial.ToTypedMessage(&inbound.Config{
+						Clients: []*protocol.User{
+							{
+								Account: serial.ToTypedMessage(&vless.Account{
+									Id: userID.String(),
+								}),
+							},
+						},
+					}),
+				},
+			},
+			Outbound: []*core.OutboundHandlerConfig{
+				{
+					ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
+				},
+			},
+		}
+
+		clientPort := tcp.PickPort()
+		clientConfig := &core.Config{
+			App: []*serial.TypedMessage{
+				serial.ToTypedMessage(&log.Config{
+					ErrorLogType: log.LogType_None,
+				}),
+			},
+			Inbound: []*core.InboundHandlerConfig{
+				{
+					ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+						PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
+						Listen:   net.NewIPOrDomain(net.LocalHostIP),
+					}),
+					ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
+						Address:  net.NewIPOrDomain(dest.Address),
+						Port:     uint32(dest.Port),
+						Networks: []net.Network{net.Network_TCP},
+					}),
+				},
+			},
+			Outbound: []*core.OutboundHandlerConfig{
+				{
+					ProxySettings: serial.ToTypedMessage(&outbound.Config{
+						Vnext: &protocol.ServerEndpoint{
+							Address: net.NewIPOrDomain(net.LocalHostIP),
+							Port:    uint32(serverPort),
+							User: &protocol.User{
+								Account: serial.ToTypedMessage(&vless.Account{
+									Id: userID.String(),
+								}),
+							},
+						},
+					}),
+					SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
+						StreamSettings: &internet.StreamConfig{
+							ProtocolName: "tcp",
+							TransportSettings: []*internet.TransportConfig{
+								{
+									ProtocolName: "tcp",
+									Settings:     serial.ToTypedMessage(&transtcp.Config{}),
+								},
+							},
+							SecurityType: serial.GetMessageType(&reality.Config{}),
+							SecuritySettings: []*serial.TypedMessage{
+								serial.ToTypedMessage(&reality.Config{
+									Show:        false,
+									Fingerprint: fingerprint,
+									ServerName:  "www.google.com",
+									PublicKey:   publicKey,
+									ShortId:     shortIds[0],
+									SpiderX:     "/",
+								}),
+							},
+						},
+					}),
+				},
+			},
+		}
+
+		servers, err := InitializeServerConfigs(serverConfig, clientConfig)
+		common.Must(err)
+		defer CloseAllServers(servers)
+
+		err = testTCPConn(clientPort, 1024*1024, time.Second*15)()
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+	fingerPrints := []string{"chrome", "firefox", "safari", "ios", "edge", "qq"}
+	wg := sync.WaitGroup{}
+	wg.Add(len(fingerPrints))
+	for _, fp := range fingerPrints {
+		go func() {
+			err := TestFingerprint(fp)
+			if err != nil {
+				t.Errorf("Fingerprint %s test failed: %v", fp, err)
+			} else {
+				t.Logf("Fingerprint %s test passed", fp)
+			}
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+}

+ 2 - 2
transport/internet/tls/ech.go

@@ -53,7 +53,7 @@ func ApplyECH(c *Config, config *tls.Config) error {
 		switch ECHForceQuery {
 		switch ECHForceQuery {
 		case "none", "half", "full":
 		case "none", "half", "full":
 		case "":
 		case "":
-			ECHForceQuery = "none" // default to none
+			ECHForceQuery = "full" // default to full
 		default:
 		default:
 			panic("Invalid ECHForceQuery: " + c.EchForceQuery)
 			panic("Invalid ECHForceQuery: " + c.EchForceQuery)
 		}
 		}
@@ -174,7 +174,7 @@ func QueryRecord(domain string, server string, forceQuery string, sockopt *inter
 	// If expire is zero value, it means we are in initial state, wait for the query to finish
 	// If expire is zero value, it means we are in initial state, wait for the query to finish
 	// otherwise return old value immediately and update in a goroutine
 	// otherwise return old value immediately and update in a goroutine
 	// but if the cache is too old, wait for update
 	// but if the cache is too old, wait for update
-	if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*6).Before(time.Now()) {
+	if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*4).Before(time.Now()) {
 		return echConfigCache.Update(domain, server, false, forceQuery, sockopt)
 		return echConfigCache.Update(domain, server, false, forceQuery, sockopt)
 	} else {
 	} else {
 		// If someone already acquired the lock, it means it is updating, do not start another update goroutine
 		// If someone already acquired the lock, it means it is updating, do not start another update goroutine

+ 12 - 1
transport/internet/tls/tls.go

@@ -10,6 +10,7 @@ import (
 	utls "github.com/refraction-networking/utls"
 	utls "github.com/refraction-networking/utls"
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/utils"
 )
 )
 
 
 type Interface interface {
 type Interface interface {
@@ -97,6 +98,12 @@ func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error {
 	if err := c.BuildHandshakeState(); err != nil {
 	if err := c.BuildHandshakeState(); err != nil {
 		return err
 		return err
 	}
 	}
+	config := *utils.AccessField[*utls.Config](c, "config")
+	// Do not modify outer ALPN to http/1.1 if ECH is used
+	// Outer ALPN will be h2,http/1.1, and real ALPN in config will be hidden in ECH
+	if config.EncryptedClientHelloConfigList != nil {
+		return c.HandshakeContext(ctx)
+	}
 	// Iterate over extensions and check for utls.ALPNExtension
 	// Iterate over extensions and check for utls.ALPNExtension
 	hasALPNExtension := false
 	hasALPNExtension := false
 	for _, extension := range c.Extensions {
 	for _, extension := range c.Extensions {
@@ -131,7 +138,7 @@ func GeneraticUClient(c net.Conn, config *tls.Config) *utls.UConn {
 }
 }
 
 
 func copyConfig(c *tls.Config) *utls.Config {
 func copyConfig(c *tls.Config) *utls.Config {
-	return &utls.Config{
+	config := &utls.Config{
 		Rand:                           c.Rand,
 		Rand:                           c.Rand,
 		RootCAs:                        c.RootCAs,
 		RootCAs:                        c.RootCAs,
 		ServerName:                     c.ServerName,
 		ServerName:                     c.ServerName,
@@ -140,6 +147,10 @@ func copyConfig(c *tls.Config) *utls.Config {
 		KeyLogWriter:                   c.KeyLogWriter,
 		KeyLogWriter:                   c.KeyLogWriter,
 		EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
 		EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
 	}
 	}
+	if config.EncryptedClientHelloConfigList != nil {
+		config.NextProtos = c.NextProtos
+	}
+	return config
 }
 }
 
 
 func init() {
 func init() {