mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 21:14:01 +03:00
Fix DPI detection: skip GREASE cipher suite in ClientHello parsing
Instead of echoing the first cipher suite from ClientHello (which is often a GREASE value like 0x5a5a), iterate the list and pick the first real cipher suite. This is what real TLS servers do per RFC 8701. Production data shows two client profiles: - 87% send GREASE first, then 0x1301 (TLS_AES_128_GCM_SHA256) - 13% send 0xc02b first (TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) The fix correctly selects 0x1301 or 0xc02b respectively, matching real server behavior. Fallback to 0x1301 if all suites are GREASE. Add snapshot test with GREASE as first cipher suite.
This commit is contained in:
@@ -130,18 +130,25 @@ func parseHandshake(r io.Reader) (*ClientHello, error) {
|
|||||||
|
|
||||||
cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
|
cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
|
||||||
|
|
||||||
// we do not care about picking up any cipher. we pick the first one,
|
// Pick the first non-GREASE cipher suite from the list.
|
||||||
// so it is always should be present.
|
// Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a),
|
||||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
// so echoing them back is a trivial DPI fingerprint.
|
||||||
return nil, fmt.Errorf("cannot read first cipher suite: %w", err)
|
for remaining := cipherSuiteLen; remaining >= 2; remaining -= 2 {
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read cipher suite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := binary.BigEndian.Uint16(header[:])
|
||||||
|
if hello.CipherSuite == 0 && cs&0x0f0f != 0x0a0a {
|
||||||
|
hello.CipherSuite = cs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hello.CipherSuite = binary.BigEndian.Uint16(header[:])
|
if hello.CipherSuite == 0 {
|
||||||
|
hello.CipherSuite = 0x1301 // fallback: TLS_AES_128_GCM_SHA256
|
||||||
if _, err := io.CopyN(io.Discard, r, cipherSuiteLen-2); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot skip remaining cipher suites: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(r, header[:1]); err != nil {
|
if _, err := io.ReadFull(r, header[:1]); err != nil {
|
||||||
return nil, fmt.Errorf("cannot read compression methods length: %w", err)
|
return nil, fmt.Errorf("cannot read compression methods length: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,12 +234,13 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCipherSuiteLe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() {
|
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() {
|
||||||
body := make([]byte, 2+fake.RandomLen+1+2)
|
body := make([]byte, 2+fake.RandomLen+1+2+1) // cipherSuiteLen=2 but only 1 byte available
|
||||||
|
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
|
||||||
|
|
||||||
suite.writeBody(body)
|
suite.writeBody(body)
|
||||||
|
|
||||||
_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
|
_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
|
||||||
suite.ErrorContains(err, "cannot read first cipher suite")
|
suite.ErrorContains(err, "cannot read cipher suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() {
|
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() {
|
||||||
@@ -249,7 +250,7 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCiph
|
|||||||
suite.writeBody(body)
|
suite.writeBody(body)
|
||||||
|
|
||||||
_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
|
_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
|
||||||
suite.ErrorContains(err, "cannot skip remaining cipher suites")
|
suite.ErrorContains(err, "cannot read cipher suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
|
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (suite *SendServerHelloTestSuite) TestRecordStructure() {
|
|||||||
recordType, length, err := tls.ReadRecord(suite.buf, &rec)
|
recordType, length, err := tls.ReadRecord(suite.buf, &rec)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(byte(tls.TypeApplicationData), recordType)
|
suite.Equal(byte(tls.TypeApplicationData), recordType)
|
||||||
suite.Greater(length, int64(2500))
|
suite.GreaterOrEqual(length, int64(2500))
|
||||||
|
|
||||||
suite.Empty(suite.buf.Bytes())
|
suite.Empty(suite.buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"time": 1617181365,
|
||||||
|
"random": "w4TaDfYg/aUKdx1oi68vxMKvHJczRNvtRRppLETzeNE=",
|
||||||
|
"sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
|
||||||
|
"host": "storage.googleapis.com",
|
||||||
|
"cipherSuite": 4867,
|
||||||
|
"full": "FgMBAgIBAAH+AwPDhNoN9iD9pQp3HWiLry/Ewq8clzNE2+1FGmksRPN40SBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIwKrgA2WloTAxMBEwLALMArwCTAI8AKwAnMqcAwwC/AKMAnwBTAE8yoAJ0AnAA9ADwANQAvwAjAEgAKAQABf/8BAAEAAAAAGwAZAAAWc3RvcmFnZS5nb29nbGVhcGlzLmNvbQAXAAAADQAYABYEAwgEBAEFAwIDCAUIBQUBCAYGAQIBAAUABQEAAAAAM3QAAAASAAAAEAAwAC4CaDIFaDItMTYFaDItMTUFaDItMTQIc3BkeS8zLjEGc3BkeS8zCGh0dHAvMS4xAAsAAgEAADMAJgAkAB0AIAf+6C8fSRJSAC7CyUvdR9kDclNR9KLCsCFHpVZ3bC8iAC0AAgEBACsACQgDBAMDAwIDAQAKAAoACAAdABcAGAAZABUAoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user