Add maxUselessRecords (ChangeCipherSpec, etc.) detection (#29)

https://github.com/XTLS/BBS/issues/16
https://github.com/XTLS/BBS/issues/19
This commit is contained in:
风扇滑翔翼
2026-03-22 12:59:25 +00:00
committed by GitHub
parent ad4fbafc4b
commit 9234c772ba
6 changed files with 123 additions and 30 deletions
+1 -1
View File
@@ -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.
+12 -8
View File
@@ -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"))
}
+6 -6
View File
@@ -1,14 +1,14 @@
module github.com/xtls/reality
go 1.24
go 1.24.0
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.48.0
golang.org/x/sys v0.41.0
)
require (
+10 -10
View File
@@ -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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
+90 -4
View File
@@ -4,9 +4,11 @@ import (
"bytes"
"encoding/binary"
"io"
"math"
"net"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/pires/go-proxyproto"
@@ -14,6 +16,7 @@ import (
)
var GlobalPostHandshakeRecordsLens sync.Map
var GlobalMaxCSSMsgCount sync.Map
func DetectPostHandshakeRecordsLens(config *Config) {
for sni := range config.ServerNames {
@@ -36,7 +39,7 @@ func DetectPostHandshakeRecordsLens(config *Config) {
return
}
}
detectConn := &DetectConn{
detectConn := &PostHandshakeRecordDetectConn{
Conn: target,
Key: key,
}
@@ -60,25 +63,58 @@ func DetectPostHandshakeRecordsLens(config *Config) {
}
io.Copy(io.Discard, uConn)
}()
go func() {
target, err := net.Dial(config.Type, config.Dest)
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,
}
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 +133,53 @@ 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
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)
c.Conn.Write(msg)
time.Sleep(1 * time.Second)
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, math.MaxInt)
return c.Conn.Write(b)
}
return c.Conn.Write(b)
}
+4 -1
View File
@@ -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,9 @@ 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