feat(tcp): add Minecraft analyzer for handshake detection

feat(tcp): implement FETAnalyzer with segment buffering
feat(tcp): enhance TCP engine with window scale detection
test(tcp): add tests for Minecraft and FET analyzers
docs: add documentation for Minecraft analyzer and TCP evasion
This commit is contained in:
2026-07-27 01:09:41 +08:00
parent 278d731b6f
commit f6342c4bcf
11 changed files with 914 additions and 8 deletions
+51 -6
View File
@@ -1,6 +1,21 @@
package tcp
import "github.com/apernet/OpenGFW/analyzer"
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
const (
// fetMinDataLen is how many bytes we want before classifying a stream.
// The metrics below are meaningless on a handful of bytes - a peer can
// force a tiny first segment by advertising a small TCP receive window in
// its SYN-ACK, which used to be enough to make us exempt the connection.
fetMinDataLen = 16
// fetMaxSegments bounds how long we wait for fetMinDataLen bytes. Without
// it, a peer that sends one byte and then goes quiet would keep the stream
// active - and therefore out of the connmark bypass - until the TCP timeout.
fetMaxSegments = 4
)
var _ analyzer.TCPAnalyzer = (*FETAnalyzer)(nil)
@@ -25,6 +40,8 @@ func (a *FETAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) anal
type fetStream struct {
logger analyzer.Logger
buf utils.ByteBuffer
segs int
}
func newFETStream(logger analyzer.Logger) *fetStream {
@@ -38,6 +55,38 @@ func (s *fetStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyz
if len(data) == 0 {
return nil, false
}
if rev {
// We classify what the client sends first. If the server speaks before
// the client does, this isn't a handshake we can say anything about.
if s.buf.Len() == 0 {
return nil, true
}
return nil, false
}
s.segs++
if s.buf.Len() == 0 && len(data) >= fetMinDataLen {
// Enough to decide on, and nothing buffered yet - classify in place.
return fetResult(data), true
}
s.buf.Append(data)
if s.buf.Len() < fetMinDataLen && s.segs < fetMaxSegments {
return nil, false
}
u = fetResult(s.buf.Buf)
s.buf.Reset()
return u, true
}
func (s *fetStream) Close(limited bool) *analyzer.PropUpdate {
if s.buf.Len() == 0 {
return nil
}
u := fetResult(s.buf.Buf)
s.buf.Reset()
return u
}
func fetResult(data []byte) *analyzer.PropUpdate {
ex1 := averagePopCount(data)
ex2 := isFirstSixPrintable(data)
ex3 := printablePercentage(data)
@@ -54,11 +103,7 @@ func (s *fetStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyz
"ex5": ex5,
"yes": !exempt,
},
}, true
}
func (s *fetStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
}
func popCount(b byte) int {