FILE / ScuroNeko/mtg

mtglib/internal/tls/fake/server_side.go

Исходный файл и его история в репозитории.
FILE db2e6031a3aeec840ef2c2b51ae2e3288355f416
Files
mtg/mtglib/internal/tls/fake/server_side.go
T
Alexey Dolotov 80213ad35d 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
2026-03-26 23:38:58 +03:00

163 lines
4.4 KiB
Go

package fake
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"io"
rnd "math/rand/v2"
"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
EllipticCurveLen = 32
)
var serverHelloSuffix = []byte{
0x00, // no compression
0x00, 0x2e, // 46 bytes of data
0x00, 0x2b, // Extension - Supported Versions
0x00, 0x02, // 2 bytes are following
0x03, 0x04, // TLS 1.3
0x00, 0x33, // Extension - Key Share
0x00, 0x24, // 36 bytes
0x00, 0x1d, // x25519 curve
0x00, 0x20, // 32 bytes of key
}
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, noise)
packet := buf.Bytes()
digest := hmac.New(sha256.New, secret)
digest.Write(clientHello.Random[:])
digest.Write(packet)
copy(packet[RandomOffset:], digest.Sum(nil))
_, err := w.Write(packet)
return err
}
func generateServerHello(buf *bytes.Buffer, hello *ClientHello) {
payload := acquireBuffer()
defer releaseBuffer(payload)
generateServerHelloPayload(payload, hello)
// 16 - type is 0x16 (handshake record)
// 03 03 - legacy protocol version of "3,3" (TLS 1.2)
// 00 7a - 0x7A (122) bytes of handshake message follows
// 16 - type is 0x16 (handshake record)
buf.WriteByte(tls.TypeHandshake)
// 03 03 - legacy protocol version of "3,3" (TLS 1.2)
buf.Write(tls.TLSVersion[:])
// 00 7a - 0x7A (122) bytes of handshake message follows
binary.Write(buf, binary.BigEndian, uint16(payload.Len())) //nolint: errcheck
payload.WriteTo(buf) //nolint: errcheck
}
func generateServerHelloPayload(buf *bytes.Buffer, hello *ClientHello) {
data := [4]byte{}
payload := acquireBuffer()
defer releaseBuffer(payload)
generateServerHelloHandshakePayload(payload, hello)
// 02 - handshake message type 0x02 (server hello)
// 00 00 76 - 0x76 (118) bytes of server hello data follows
buf.WriteByte(TypeHandshakeServer)
// 00 00 76 - 0x76 (118) bytes of server hello data follows
binary.BigEndian.PutUint32(data[:], uint32(payload.Len()))
buf.Write(data[1:])
payload.WriteTo(buf) //nolint: errcheck
}
func generateServerHelloHandshakePayload(buf *bytes.Buffer, hello *ClientHello) {
// The unusual version number ("3,3" representing TLS 1.2) is due to
// TLS 1.0 being a minor revision of the SSL 3.0 protocol. Therefore
// TLS 1.0 is represented by "3,1", TLS 1.1 is "3,2", and so on.
buf.Write(tls.TLSVersion[:])
buf.Write(emptyRandom[:])
// 20 - 0x20 (32) bytes of session ID follow
// e0 e1 ... fe ff - session ID copied from Client Hello
buf.WriteByte(byte(len(hello.SessionID)))
buf.Write(hello.SessionID)
binary.Write(buf, binary.BigEndian, hello.CipherSuite) //nolint: errcheck
buf.Write(serverHelloSuffix)
scalar := [EllipticCurveLen]byte{}
if _, err := rand.Read(scalar[:]); err != nil {
panic(err)
}
curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint)
buf.Write(curve)
}
func generateChangeCipherValue(buf *bytes.Buffer) {
buf.WriteByte(tls.TypeChangeCipherSpec)
buf.Write(tls.TLSVersion[:])
binary.Write(buf, binary.BigEndian, uint16(1)) //nolint: errcheck
buf.WriteByte(ChangeCipherValue)
}
// 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 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
}