mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-09-01 16:01:55 +03:00
Add dynamic cert noise calibration for FakeTLS handshake
The hardcoded noise range (2500-4700 bytes) in the FakeTLS ServerHello does not match the real certificate chain sizes of many popular fronting domains (e.g., dl.google.com ≈ 6480 bytes, microsoft.com ≈ 13004 bytes). This makes the proxy detectable by DPI systems that compare the ApplicationData size with the real cert chain size for the SNI domain. On startup, probe the fronting domain's actual TLS handshake size and use the measured value ± jitter instead of the static range. Falls back to the legacy 2500-4700 range if the probe fails. Also adds optional caching of probe results between restarts (noise-cache-path, noise-cache-ttl) and a configurable probe count (noise-probe-count) under [defense.doppelganger]. Closes #408
This commit is contained in:
@@ -9,11 +9,18 @@ import (
|
||||
"io"
|
||||
rnd "math/rand/v2"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
|
||||
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// NoiseParams controls the size of the fake ApplicationData record
|
||||
// in ServerHello. If Mean is 0, the legacy random range (2500-4700)
|
||||
// is used.
|
||||
type NoiseParams struct {
|
||||
Mean int
|
||||
Jitter int
|
||||
}
|
||||
|
||||
const (
|
||||
TypeHandshakeServer = 0x02
|
||||
ChangeCipherValue = 0x01
|
||||
@@ -33,13 +40,13 @@ var serverHelloSuffix = []byte{
|
||||
0x00, 0x20, // 32 bytes of key
|
||||
}
|
||||
|
||||
func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello) error {
|
||||
func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello, noise NoiseParams) error {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(tls.MaxRecordSize)
|
||||
|
||||
generateServerHello(buf, clientHello)
|
||||
generateChangeCipherValue(buf)
|
||||
generateNoise(buf)
|
||||
generateNoise(buf, noise)
|
||||
|
||||
packet := buf.Bytes()
|
||||
digest := hmac.New(sha256.New, secret)
|
||||
@@ -125,19 +132,31 @@ func generateChangeCipherValue(buf *bytes.Buffer) {
|
||||
buf.WriteByte(ChangeCipherValue)
|
||||
}
|
||||
|
||||
func generateNoise(buf *bytes.Buffer) {
|
||||
data := make(
|
||||
[]byte,
|
||||
int64(
|
||||
doppel.TLSRecordSizeStart+rnd.IntN(
|
||||
doppel.TLSRecordSizeAccel-doppel.TLSRecordSizeStart,
|
||||
),
|
||||
),
|
||||
)
|
||||
// generateNoise writes a single ApplicationData record mimicking the combined
|
||||
// size of a real TLS 1.3 encrypted server handshake (EncryptedExtensions +
|
||||
// Certificate chain + CertificateVerify + Finished).
|
||||
//
|
||||
// NOTE: Must be exactly ONE ApplicationData record — the Telegram client reads
|
||||
// ServerHello + CCS + 1 ApplicationData and computes HMAC over all three.
|
||||
// Multiple records would cause HMAC mismatch and connection failure.
|
||||
func generateNoise(buf *bytes.Buffer, noise NoiseParams) {
|
||||
var size int
|
||||
|
||||
if _, err := rand.Read(data[:]); err != nil {
|
||||
if noise.Mean > 0 && noise.Jitter > 0 {
|
||||
// Calibrated: use measured cert chain size ± jitter.
|
||||
size = noise.Mean - noise.Jitter + rnd.IntN(2*noise.Jitter)
|
||||
if size < 1000 {
|
||||
size = 1000
|
||||
}
|
||||
} else {
|
||||
// Legacy fallback: random in 2500-4700 range.
|
||||
size = 2500 + rnd.IntN(2200)
|
||||
}
|
||||
|
||||
data := make([]byte, size)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tls.WriteRecord(buf, data[:]) //nolint: errcheck
|
||||
tls.WriteRecord(buf, data) //nolint: errcheck
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user