diff --git a/go.mod b/go.mod index f921af82..2f67af70 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 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-shadowsocks v0.2.7 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 2ebcddc5..93554409 100644 --- a/go.sum +++ b/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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= diff --git a/testing/scenarios/vless_test.go b/testing/scenarios/vless_test.go index 446ce7b9..cdc75c59 100644 --- a/testing/scenarios/vless_test.go +++ b/testing/scenarios/vless_test.go @@ -3,6 +3,7 @@ package scenarios import ( "encoding/base64" "encoding/hex" + "sync" "testing" "time" @@ -497,3 +498,152 @@ func TestVlessXtlsVisionReality(t *testing.T) { 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() +} diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index ba175fd8..26721bc1 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -53,7 +53,7 @@ func ApplyECH(c *Config, config *tls.Config) error { switch ECHForceQuery { case "none", "half", "full": case "": - ECHForceQuery = "none" // default to none + ECHForceQuery = "full" // default to full default: 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 // otherwise return old value immediately and update in a goroutine // 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) } else { // If someone already acquired the lock, it means it is updating, do not start another update goroutine diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index cbc80bf5..7bd64a7e 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -10,6 +10,7 @@ import ( utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" ) type Interface interface { @@ -97,6 +98,12 @@ func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error { if err := c.BuildHandshakeState(); err != nil { 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 hasALPNExtension := false 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 { - return &utls.Config{ + config := &utls.Config{ Rand: c.Rand, RootCAs: c.RootCAs, ServerName: c.ServerName, @@ -140,6 +147,10 @@ func copyConfig(c *tls.Config) *utls.Config { KeyLogWriter: c.KeyLogWriter, EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList, } + if config.EncryptedClientHelloConfigList != nil { + config.NextProtos = c.NextProtos + } + return config } func init() {