From ba1a82a348da0b74f5d093bcfaababa37e157e89 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Mon, 16 Feb 2026 20:13:05 +0800 Subject: [PATCH 1/3] Fix max CCS fingerprint --- common.go | 2 +- conn.go | 20 ++++++---- record_detect.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++-- tls.go | 6 ++- 4 files changed, 112 insertions(+), 14 deletions(-) diff --git a/common.go b/common.go index 8b19ccf..28f4af0 100644 --- a/common.go +++ b/common.go @@ -67,7 +67,7 @@ const ( recordHeaderLen = 5 // record header length maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB) maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB) - maxUselessRecords = 16 // maximum number of consecutive non-advancing records + maxUselessRecords = 32 // maximum number of consecutive non-advancing records ) // TLS record types. diff --git a/conn.go b/conn.go index a408e76..d99b2e6 100644 --- a/conn.go +++ b/conn.go @@ -25,16 +25,17 @@ import ( // A Conn represents a secured connection. // It implements the net.Conn interface. type Conn struct { - AuthKey []byte - ClientVer [3]byte - ClientTime time.Time - ClientShortId [8]byte + AuthKey []byte + ClientVer [3]byte + ClientTime time.Time + ClientShortId [8]byte + MaxUselessRecords int // constant conn net.Conn isClient bool handshakeFn func(context.Context) error // (*Conn).clientHandshake or serverHandshake - quic *quicState // nil for non-QUIC connections + quic *quicState // nil for non-QUIC connections // isHandshakeComplete is true if the connection is currently transferring // application data (i.e. is not currently processing a handshake). @@ -827,7 +828,10 @@ func (c *Conn) readRecordOrCCS(expectChangeCipherSpec bool) error { // a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3. func (c *Conn) retryReadRecord(expectChangeCipherSpec bool) error { c.retryCount++ - if c.retryCount > maxUselessRecords { + if c.MaxUselessRecords <= 0 { + c.MaxUselessRecords = maxUselessRecords + } + if c.retryCount > c.MaxUselessRecords { c.sendAlert(alertUnexpectedMessage) return c.in.setErrorLocked(errors.New("tls: too many ignored records")) } @@ -1248,7 +1252,7 @@ func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript transcriptHash) if transcript != nil { transcript.Write(data) } - + return m, nil } @@ -1375,7 +1379,7 @@ func (c *Conn) handlePostHandshakeMessage() error { return err } c.retryCount++ - if c.retryCount > maxUselessRecords { + if c.retryCount > c.MaxUselessRecords { c.sendAlert(alertUnexpectedMessage) return c.in.setErrorLocked(errors.New("tls: too many non-advancing records")) } diff --git a/record_detect.go b/record_detect.go index 71b29bc..31b240e 100644 --- a/record_detect.go +++ b/record_detect.go @@ -7,6 +7,7 @@ import ( "net" "strconv" "sync" + "sync/atomic" "time" "github.com/pires/go-proxyproto" @@ -14,6 +15,7 @@ import ( ) var GlobalPostHandshakeRecordsLens sync.Map +var GlobalMaxCSSMsgCount sync.Map func DetectPostHandshakeRecordsLens(config *Config) { for sni := range config.ServerNames { @@ -36,7 +38,7 @@ func DetectPostHandshakeRecordsLens(config *Config) { return } } - detectConn := &DetectConn{ + detectConn := &PostHandshakeRecordDetectConn{ Conn: target, Key: key, } @@ -60,25 +62,61 @@ func DetectPostHandshakeRecordsLens(config *Config) { } io.Copy(io.Discard, uConn) }() + go func() { + now := time.Now() + target, err := net.Dial("tcp", config.Dest) + rtt := time.Since(now) + if err != nil { + return + } + if config.Xver == 1 || config.Xver == 2 { + if _, err = proxyproto.HeaderProxyFromAddrs(config.Xver, target.LocalAddr(), target.RemoteAddr()).WriteTo(target); err != nil { + return + } + } + fingerprint := utls.HelloChrome_Auto + nextProtos := []string{"h2", "http/1.1"} + if alpn != 2 { + fingerprint = utls.HelloGolang + } + if alpn == 1 { + nextProtos = []string{"http/1.1"} + } + if alpn == 0 { + nextProtos = nil + } + conn := &CCSDetectConn{ + Conn: target, + Key: key, + rtt: rtt, + } + uConn := utls.UClient(conn, &utls.Config{ + ServerName: sni, // needs new loopvar behaviour + NextProtos: nextProtos, + }, fingerprint) + if err = uConn.Handshake(); err != nil { + return + } + }() } } } } -type DetectConn struct { +type PostHandshakeRecordDetectConn struct { net.Conn Key string CcsSent bool } -func (c *DetectConn) Write(b []byte) (n int, err error) { +func (c *PostHandshakeRecordDetectConn) Write(b []byte) (n int, err error) { if len(b) >= 3 && bytes.Equal(b[:3], []byte{20, 3, 3}) { c.CcsSent = true } return c.Conn.Write(b) } -func (c *DetectConn) Read(b []byte) (n int, err error) { +func (c *PostHandshakeRecordDetectConn) Read(b []byte) (n int, err error) { if !c.CcsSent { return c.Conn.Read(b) } @@ -97,3 +135,55 @@ func (c *DetectConn) Read(b []byte) (n int, err error) { GlobalPostHandshakeRecordsLens.Store(c.Key, postHandshakeRecordsLens) return 0, io.EOF } + +var CCSMsg = []byte{0x14, 0x3, 0x3, 0x0, 0x1, 0x1} + +type CCSDetectConn struct { + net.Conn + rtt time.Duration + Key string +} + +func (c *CCSDetectConn) Write(b []byte) (n int, err error) { + if len(b) >= 3 && bytes.Equal(b[:3], []byte{20, 3, 3}) { + var hasAlert atomic.Bool + go func() { + defer hasAlert.Store(true) + buf := make([]byte, 512) + for { + _, err = c.Conn.Read(buf) + if err != nil { + return + } + if buf[0] == 0x15 { + return + } + } + }() + sendProbePayload := func(count int) bool { + msg := bytes.Repeat(CCSMsg, count) + rtt := max(100*time.Millisecond, c.rtt) + c.Conn.Write(msg) + time.Sleep(rtt) + if hasAlert.Load() { + return true + } + return false + } + if sendProbePayload(2) { + GlobalMaxCSSMsgCount.Store(c.Key, 1) + return c.Conn.Write(b) + } + if sendProbePayload(15) { + GlobalMaxCSSMsgCount.Store(c.Key, 16) + return c.Conn.Write(b) + } + if sendProbePayload(16) { + GlobalMaxCSSMsgCount.Store(c.Key, 32) + return c.Conn.Write(b) + } + GlobalMaxCSSMsgCount.Store(c.Key, 1145141919810) + return c.Conn.Write(b) + } + return c.Conn.Write(b) +} diff --git a/tls.go b/tls.go index add597a..de9dc75 100644 --- a/tls.go +++ b/tls.go @@ -372,7 +372,7 @@ func Server(ctx context.Context, conn net.Conn, config *Config) (*Conn, error) { if err != nil { break } - go func() { // TODO: Probe target's maxUselessRecords and some time-outs in advance. + go func() { // TODO: Probe some time-outs in advance. if handshakeLen-len(s2cSaved) > 0 { io.ReadFull(target, buf[:handshakeLen-len(s2cSaved)]) } @@ -422,6 +422,10 @@ func Server(ctx context.Context, conn net.Conn, config *Config) (*Conn, error) { } } time.Sleep(5 * time.Second) + if maxUseless, ok := GlobalMaxCSSMsgCount.Load(key); ok { + hs.c.MaxUselessRecords = maxUseless.(int) + } + } hs.c.isHandshakeComplete.Store(true) break From 50d2db8eb2150cec248b0d0143e3bcf730b70951 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sun, 22 Mar 2026 15:42:47 +0800 Subject: [PATCH 2/3] format --- record_detect.go | 9 ++------- tls.go | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/record_detect.go b/record_detect.go index 31b240e..cc3acb3 100644 --- a/record_detect.go +++ b/record_detect.go @@ -63,9 +63,7 @@ func DetectPostHandshakeRecordsLens(config *Config) { io.Copy(io.Discard, uConn) }() go func() { - now := time.Now() - target, err := net.Dial("tcp", config.Dest) - rtt := time.Since(now) + target, err := net.Dial(config.Type, config.Dest) if err != nil { return } @@ -88,7 +86,6 @@ func DetectPostHandshakeRecordsLens(config *Config) { conn := &CCSDetectConn{ Conn: target, Key: key, - rtt: rtt, } uConn := utls.UClient(conn, &utls.Config{ ServerName: sni, // needs new loopvar behaviour @@ -140,7 +137,6 @@ var CCSMsg = []byte{0x14, 0x3, 0x3, 0x0, 0x1, 0x1} type CCSDetectConn struct { net.Conn - rtt time.Duration Key string } @@ -162,9 +158,8 @@ func (c *CCSDetectConn) Write(b []byte) (n int, err error) { }() sendProbePayload := func(count int) bool { msg := bytes.Repeat(CCSMsg, count) - rtt := max(100*time.Millisecond, c.rtt) c.Conn.Write(msg) - time.Sleep(rtt) + time.Sleep(1 * time.Second) if hasAlert.Load() { return true } diff --git a/tls.go b/tls.go index de9dc75..4c8ef86 100644 --- a/tls.go +++ b/tls.go @@ -425,7 +425,6 @@ func Server(ctx context.Context, conn net.Conn, config *Config) (*Conn, error) { if maxUseless, ok := GlobalMaxCSSMsgCount.Load(key); ok { hs.c.MaxUselessRecords = maxUseless.(int) } - } hs.c.isHandshakeComplete.Store(true) break From dcc87c6006f65a3d4a5f7cf3ca11f19b392d9d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 22 Mar 2026 09:37:12 +0000 Subject: [PATCH 3/3] bump version --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index cae710d..21223d1 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/xtls/reality go 1.24 require ( - github.com/cloudflare/circl v1.6.1 + github.com/cloudflare/circl v1.6.3 github.com/juju/ratelimit v1.0.2 - github.com/pires/go-proxyproto v0.8.1 - github.com/refraction-networking/utls v1.8.1 - golang.org/x/crypto v0.43.0 - golang.org/x/sys v0.37.0 + github.com/pires/go-proxyproto v0.11.0 + github.com/refraction-networking/utls v1.8.2 + golang.org/x/crypto v0.36.0 + golang.org/x/sys v0.31.0 ) require ( diff --git a/go.sum b/go.sum index b06cacf..b140616 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= @@ -11,13 +11,13 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= -github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= -github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= -github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +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= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=