Explorar el Código

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
tags/v2.2.5^2^2
Alexey Dolotov hace 1 mes
padre
commit
80213ad35d

+ 4
- 0
internal/cli/run_proxy.go Ver fichero

267
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
267
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
268
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
268
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
269
 		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
269
 		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
270
+
271
+		NoiseProbeCount: conf.Defense.Doppelganger.NoiseProbeCount.Get(0),
272
+		NoiseCacheTTL:   conf.Defense.Doppelganger.NoiseCacheTTL.Get(0),
273
+		NoiseCachePath:  conf.Defense.Doppelganger.NoiseCachePath,
270
 	}
274
 	}
271
 
275
 
272
 	proxy, err := mtglib.NewProxy(opts)
276
 	proxy, err := mtglib.NewProxy(opts)

+ 7
- 4
internal/config/config.go Ver fichero

50
 		Blocklist    ListConfig `json:"blocklist"`
50
 		Blocklist    ListConfig `json:"blocklist"`
51
 		Allowlist    ListConfig `json:"allowlist"`
51
 		Allowlist    ListConfig `json:"allowlist"`
52
 		Doppelganger struct {
52
 		Doppelganger struct {
53
-			URLs       []TypeHttpsURL  `json:"urls"`
54
-			Repeats    TypeConcurrency `json:"repeats_per_raid"`
55
-			UpdateEach TypeDuration    `json:"raid_each"`
56
-			DRS        TypeBool        `json:"drs"`
53
+			URLs            []TypeHttpsURL  `json:"urls"`
54
+			Repeats         TypeConcurrency `json:"repeats_per_raid"`
55
+			UpdateEach      TypeDuration    `json:"raid_each"`
56
+			DRS             TypeBool        `json:"drs"`
57
+			NoiseProbeCount TypeConcurrency `json:"noise_probe_count"`
58
+			NoiseCacheTTL   TypeDuration    `json:"noise_cache_ttl"`
59
+			NoiseCachePath  string          `json:"noise_cache_path"`
57
 		} `json:"doppelganger"`
60
 		} `json:"doppelganger"`
58
 	} `json:"defense"`
61
 	} `json:"defense"`
59
 	Network struct {
62
 	Network struct {

+ 7
- 4
internal/config/parse.go Ver fichero

45
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
45
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
46
 		} `toml:"allowlist" json:"allowlist,omitempty"`
46
 		} `toml:"allowlist" json:"allowlist,omitempty"`
47
 		Doppelganger struct {
47
 		Doppelganger struct {
48
-			URLs       []string `toml:"urls" json:"urls,omitempty"`
49
-			Repeats    uint     `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
50
-			UpdateEach string   `toml:"raid-each" json:"raid_each,omitempty"`
51
-			DRS        bool     `toml:"drs" json:"drs,omitempty"`
48
+			URLs            []string `toml:"urls" json:"urls,omitempty"`
49
+			Repeats         uint     `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
50
+			UpdateEach      string   `toml:"raid-each" json:"raid_each,omitempty"`
51
+			DRS             bool     `toml:"drs" json:"drs,omitempty"`
52
+			NoiseProbeCount uint     `toml:"noise-probe-count" json:"noise_probe_count,omitempty"`
53
+			NoiseCacheTTL   string   `toml:"noise-cache-ttl" json:"noise_cache_ttl,omitempty"`
54
+			NoiseCachePath  string   `toml:"noise-cache-path" json:"noise_cache_path,omitempty"`
52
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
55
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
53
 	} `toml:"defense" json:"defense,omitempty"`
56
 	} `toml:"defense" json:"defense,omitempty"`
54
 	Network struct {
57
 	Network struct {

+ 261
- 0
mtglib/internal/tls/fake/cert_probe.go Ver fichero

1
+package fake
2
+
3
+import (
4
+	"crypto/tls"
5
+	"encoding/binary"
6
+	"encoding/json"
7
+	"fmt"
8
+	"net"
9
+	"os"
10
+	"sync"
11
+	"time"
12
+)
13
+
14
+const (
15
+	probeDialTimeout      = 10 * time.Second
16
+	probeHandshakeTimeout = 10 * time.Second
17
+	defaultProbeCount     = 15
18
+	defaultCacheTTL       = 24 * time.Hour
19
+
20
+	tlsTypeChangeCipherSpec = 0x14
21
+	tlsTypeApplicationData  = 0x17
22
+)
23
+
24
+// CertProbeResult holds the measured encrypted handshake size.
25
+type CertProbeResult struct {
26
+	Mean   int `json:"mean"`
27
+	Jitter int `json:"jitter"`
28
+}
29
+
30
+// CertProbeCache is the on-disk format for cached probe results.
31
+type CertProbeCache struct {
32
+	Hostname string    `json:"hostname"`
33
+	Port     int       `json:"port"`
34
+	Mean     int       `json:"mean"`
35
+	Jitter   int       `json:"jitter"`
36
+	ProbedAt time.Time `json:"probed_at"`
37
+}
38
+
39
+// LoadCachedProbe reads a cached probe result from path. Returns the result
40
+// and true if the cache exists, matches hostname:port, and is younger than ttl.
41
+// Otherwise returns zero value and false.
42
+func LoadCachedProbe(path, hostname string, port int, ttl time.Duration) (CertProbeResult, bool) {
43
+	if ttl <= 0 {
44
+		ttl = defaultCacheTTL
45
+	}
46
+
47
+	data, err := os.ReadFile(path)
48
+	if err != nil {
49
+		return CertProbeResult{}, false
50
+	}
51
+
52
+	var cache CertProbeCache
53
+	if err := json.Unmarshal(data, &cache); err != nil {
54
+		return CertProbeResult{}, false
55
+	}
56
+
57
+	if cache.Hostname != hostname || cache.Port != port {
58
+		return CertProbeResult{}, false
59
+	}
60
+
61
+	if time.Since(cache.ProbedAt) > ttl {
62
+		return CertProbeResult{}, false
63
+	}
64
+
65
+	if cache.Mean <= 0 {
66
+		return CertProbeResult{}, false
67
+	}
68
+
69
+	return CertProbeResult{Mean: cache.Mean, Jitter: cache.Jitter}, true
70
+}
71
+
72
+// SaveCachedProbe writes a probe result to path as JSON.
73
+func SaveCachedProbe(path, hostname string, port int, result CertProbeResult) error {
74
+	cache := CertProbeCache{
75
+		Hostname: hostname,
76
+		Port:     port,
77
+		Mean:     result.Mean,
78
+		Jitter:   result.Jitter,
79
+		ProbedAt: time.Now(),
80
+	}
81
+
82
+	data, err := json.MarshalIndent(cache, "", "  ")
83
+	if err != nil {
84
+		return err
85
+	}
86
+
87
+	return os.WriteFile(path, data, 0o644) //nolint: gosec
88
+}
89
+
90
+// ProbeCertSize connects to hostname:port via TLS multiple times and measures
91
+// the total ApplicationData payload bytes sent by the server during the
92
+// handshake (between ChangeCipherSpec and the first application-level data).
93
+// This corresponds to EncryptedExtensions + Certificate + CertificateVerify +
94
+// Finished in TLS 1.3, which is what the FakeTLS noise must mimic.
95
+func ProbeCertSize(hostname string, port int, count int) (CertProbeResult, error) {
96
+	if count <= 0 {
97
+		count = defaultProbeCount
98
+	}
99
+
100
+	addr := net.JoinHostPort(hostname, fmt.Sprintf("%d", port))
101
+	sizes := make([]int, 0, count)
102
+
103
+	for i := 0; i < count; i++ {
104
+		size, err := probeSingle(addr, hostname)
105
+		if err != nil {
106
+			if len(sizes) > 0 {
107
+				break // use what we have
108
+			}
109
+
110
+			return CertProbeResult{}, fmt.Errorf("probe %d failed: %w", i, err)
111
+		}
112
+
113
+		sizes = append(sizes, size)
114
+	}
115
+
116
+	if len(sizes) == 0 {
117
+		return CertProbeResult{}, fmt.Errorf("no successful probes")
118
+	}
119
+
120
+	// Calculate mean and jitter (max deviation from mean).
121
+	sum := 0
122
+	for _, s := range sizes {
123
+		sum += s
124
+	}
125
+
126
+	mean := sum / len(sizes)
127
+
128
+	maxDev := 0
129
+	for _, s := range sizes {
130
+		d := s - mean
131
+		if d < 0 {
132
+			d = -d
133
+		}
134
+
135
+		if d > maxDev {
136
+			maxDev = d
137
+		}
138
+	}
139
+
140
+	// Ensure minimum jitter of 100 bytes for variability.
141
+	if maxDev < 100 {
142
+		maxDev = 100
143
+	}
144
+
145
+	return CertProbeResult{Mean: mean, Jitter: maxDev}, nil
146
+}
147
+
148
+// probeSingle does one TLS handshake and measures ApplicationData bytes
149
+// received during the handshake.
150
+func probeSingle(addr, hostname string) (int, error) {
151
+	rawConn, err := net.DialTimeout("tcp", addr, probeDialTimeout)
152
+	if err != nil {
153
+		return 0, err
154
+	}
155
+	defer rawConn.Close() //nolint: errcheck
156
+
157
+	capture := &recordCapture{conn: rawConn}
158
+
159
+	tlsConn := tls.Client(capture, &tls.Config{
160
+		ServerName: hostname,
161
+		MinVersion: tls.VersionTLS12,
162
+	})
163
+	tlsConn.SetDeadline(time.Now().Add(probeHandshakeTimeout)) //nolint: errcheck
164
+
165
+	if err := tlsConn.Handshake(); err != nil {
166
+		return 0, err
167
+	}
168
+
169
+	tlsConn.Close() //nolint: errcheck
170
+
171
+	return capture.appDataBytes, nil
172
+}
173
+
174
+// recordCapture wraps a net.Conn and parses the raw TLS record stream to
175
+// measure ApplicationData payload sizes sent by the server during handshake.
176
+// It tracks record boundaries by maintaining a state machine over Read calls.
177
+type recordCapture struct {
178
+	conn         net.Conn
179
+	mu           sync.Mutex
180
+	appDataBytes int
181
+	seenCCS      bool
182
+	done         bool
183
+
184
+	// Record boundary tracking for the read side.
185
+	readRemaining int // bytes left in current record payload
186
+	readHeaderBuf [5]byte
187
+	readHeaderPos int
188
+}
189
+
190
+func (rc *recordCapture) Read(p []byte) (int, error) {
191
+	n, err := rc.conn.Read(p)
192
+	if n > 0 && !rc.done {
193
+		rc.mu.Lock()
194
+		rc.parseReadBytes(p[:n])
195
+		rc.mu.Unlock()
196
+	}
197
+
198
+	return n, err
199
+}
200
+
201
+func (rc *recordCapture) parseReadBytes(data []byte) {
202
+	for len(data) > 0 {
203
+		if rc.readRemaining > 0 {
204
+			// Consuming payload of current record.
205
+			consume := rc.readRemaining
206
+			if consume > len(data) {
207
+				consume = len(data)
208
+			}
209
+
210
+			rc.readRemaining -= consume
211
+			data = data[consume:]
212
+
213
+			continue
214
+		}
215
+
216
+		// Accumulate header bytes (5 bytes per record).
217
+		need := 5 - rc.readHeaderPos
218
+		if need > len(data) {
219
+			need = len(data)
220
+		}
221
+
222
+		copy(rc.readHeaderBuf[rc.readHeaderPos:], data[:need])
223
+		rc.readHeaderPos += need
224
+		data = data[need:]
225
+
226
+		if rc.readHeaderPos < 5 {
227
+			return // incomplete header
228
+		}
229
+
230
+		// Full header available.
231
+		recordType := rc.readHeaderBuf[0]
232
+		payloadLen := int(binary.BigEndian.Uint16(rc.readHeaderBuf[3:5]))
233
+		rc.readHeaderPos = 0
234
+		rc.readRemaining = payloadLen
235
+
236
+		if recordType == tlsTypeChangeCipherSpec {
237
+			rc.seenCCS = true
238
+		} else if recordType == tlsTypeApplicationData && rc.seenCCS {
239
+			rc.appDataBytes += payloadLen
240
+		}
241
+	}
242
+}
243
+
244
+func (rc *recordCapture) Write(p []byte) (int, error) {
245
+	// After client writes post-CCS data, server handshake records are done.
246
+	if rc.seenCCS && rc.appDataBytes > 0 {
247
+		rc.done = true
248
+	}
249
+
250
+	return rc.conn.Write(p)
251
+}
252
+
253
+func (rc *recordCapture) Close() error                       { return rc.conn.Close() }
254
+func (rc *recordCapture) LocalAddr() net.Addr                { return rc.conn.LocalAddr() }
255
+func (rc *recordCapture) RemoteAddr() net.Addr               { return rc.conn.RemoteAddr() }
256
+func (rc *recordCapture) SetDeadline(t time.Time) error      { return rc.conn.SetDeadline(t) }
257
+func (rc *recordCapture) SetReadDeadline(t time.Time) error  { return rc.conn.SetReadDeadline(t) }
258
+func (rc *recordCapture) SetWriteDeadline(t time.Time) error { return rc.conn.SetWriteDeadline(t) }
259
+
260
+// Ensure recordCapture implements net.Conn.
261
+var _ net.Conn = (*recordCapture)(nil)

+ 34
- 15
mtglib/internal/tls/fake/server_side.go Ver fichero

9
 	"io"
9
 	"io"
10
 	rnd "math/rand/v2"
10
 	rnd "math/rand/v2"
11
 
11
 
12
-	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
13
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
14
 	"golang.org/x/crypto/curve25519"
13
 	"golang.org/x/crypto/curve25519"
15
 )
14
 )
16
 
15
 
16
+// NoiseParams controls the size of the fake ApplicationData record
17
+// in ServerHello. If Mean is 0, the legacy random range (2500-4700)
18
+// is used.
19
+type NoiseParams struct {
20
+	Mean   int
21
+	Jitter int
22
+}
23
+
17
 const (
24
 const (
18
 	TypeHandshakeServer = 0x02
25
 	TypeHandshakeServer = 0x02
19
 	ChangeCipherValue   = 0x01
26
 	ChangeCipherValue   = 0x01
33
 	0x00, 0x20, // 32 bytes of key
40
 	0x00, 0x20, // 32 bytes of key
34
 }
41
 }
35
 
42
 
36
-func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello) error {
43
+func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello, noise NoiseParams) error {
37
 	buf := &bytes.Buffer{}
44
 	buf := &bytes.Buffer{}
38
 	buf.Grow(tls.MaxRecordSize)
45
 	buf.Grow(tls.MaxRecordSize)
39
 
46
 
40
 	generateServerHello(buf, clientHello)
47
 	generateServerHello(buf, clientHello)
41
 	generateChangeCipherValue(buf)
48
 	generateChangeCipherValue(buf)
42
-	generateNoise(buf)
49
+	generateNoise(buf, noise)
43
 
50
 
44
 	packet := buf.Bytes()
51
 	packet := buf.Bytes()
45
 	digest := hmac.New(sha256.New, secret)
52
 	digest := hmac.New(sha256.New, secret)
125
 	buf.WriteByte(ChangeCipherValue)
132
 	buf.WriteByte(ChangeCipherValue)
126
 }
133
 }
127
 
134
 
128
-func generateNoise(buf *bytes.Buffer) {
129
-	data := make(
130
-		[]byte,
131
-		int64(
132
-			doppel.TLSRecordSizeStart+rnd.IntN(
133
-				doppel.TLSRecordSizeAccel-doppel.TLSRecordSizeStart,
134
-			),
135
-		),
136
-	)
137
-
138
-	if _, err := rand.Read(data[:]); err != nil {
135
+// generateNoise writes a single ApplicationData record mimicking the combined
136
+// size of a real TLS 1.3 encrypted server handshake (EncryptedExtensions +
137
+// Certificate chain + CertificateVerify + Finished).
138
+//
139
+// NOTE: Must be exactly ONE ApplicationData record — the Telegram client reads
140
+// ServerHello + CCS + 1 ApplicationData and computes HMAC over all three.
141
+// Multiple records would cause HMAC mismatch and connection failure.
142
+func generateNoise(buf *bytes.Buffer, noise NoiseParams) {
143
+	var size int
144
+
145
+	if noise.Mean > 0 && noise.Jitter > 0 {
146
+		// Calibrated: use measured cert chain size ± jitter.
147
+		size = noise.Mean - noise.Jitter + rnd.IntN(2*noise.Jitter)
148
+		if size < 1000 {
149
+			size = 1000
150
+		}
151
+	} else {
152
+		// Legacy fallback: random in 2500-4700 range.
153
+		size = 2500 + rnd.IntN(2200)
154
+	}
155
+
156
+	data := make([]byte, size)
157
+	if _, err := rand.Read(data); err != nil {
139
 		panic(err)
158
 		panic(err)
140
 	}
159
 	}
141
 
160
 
142
-	tls.WriteRecord(buf, data[:]) //nolint: errcheck
161
+	tls.WriteRecord(buf, data) //nolint: errcheck
143
 }
162
 }

+ 32
- 6
mtglib/internal/tls/fake/server_side_test.go Ver fichero

8
 	"testing"
8
 	"testing"
9
 
9
 
10
 	"github.com/9seconds/mtg/v2/mtglib"
10
 	"github.com/9seconds/mtg/v2/mtglib"
11
-	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
11
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
13
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
14
 	"github.com/stretchr/testify/suite"
13
 	"github.com/stretchr/testify/suite"
39
 }
38
 }
40
 
39
 
41
 func (suite *SendServerHelloTestSuite) TestRecordStructure() {
40
 func (suite *SendServerHelloTestSuite) TestRecordStructure() {
42
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
41
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
43
 	suite.NoError(err)
42
 	suite.NoError(err)
44
 
43
 
45
 	var rec bytes.Buffer
44
 	var rec bytes.Buffer
59
 	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
58
 	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
60
 	suite.NoError(err)
59
 	suite.NoError(err)
61
 	suite.Equal(byte(tls.TypeApplicationData), recordType)
60
 	suite.Equal(byte(tls.TypeApplicationData), recordType)
62
-	suite.Greater(length, int64(doppel.TLSRecordSizeStart))
61
+	suite.Greater(length, int64(2500))
63
 
62
 
64
 	suite.Empty(suite.buf.Bytes())
63
 	suite.Empty(suite.buf.Bytes())
65
 }
64
 }
66
 
65
 
67
 func (suite *SendServerHelloTestSuite) TestHMAC() {
66
 func (suite *SendServerHelloTestSuite) TestHMAC() {
68
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
67
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
69
 	suite.NoError(err)
68
 	suite.NoError(err)
70
 
69
 
71
 	packet := make([]byte, suite.buf.Len())
70
 	packet := make([]byte, suite.buf.Len())
83
 }
82
 }
84
 
83
 
85
 func (suite *SendServerHelloTestSuite) TestHandshakePayload() {
84
 func (suite *SendServerHelloTestSuite) TestHandshakePayload() {
86
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
85
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
87
 	suite.NoError(err)
86
 	suite.NoError(err)
88
 
87
 
89
 	packet := suite.buf.Bytes()
88
 	packet := suite.buf.Bytes()
105
 }
104
 }
106
 
105
 
107
 func (suite *SendServerHelloTestSuite) TestChangeCipherSpec() {
106
 func (suite *SendServerHelloTestSuite) TestChangeCipherSpec() {
108
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
107
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
109
 	suite.NoError(err)
108
 	suite.NoError(err)
110
 
109
 
111
 	// Skip first record
110
 	// Skip first record
124
 	suite.Equal([]byte{fake.ChangeCipherValue}, rec.Bytes())
123
 	suite.Equal([]byte{fake.ChangeCipherValue}, rec.Bytes())
125
 }
124
 }
126
 
125
 
126
+func (suite *SendServerHelloTestSuite) TestCalibratedNoiseSize() {
127
+	noise := fake.NoiseParams{Mean: 6480, Jitter: 100}
128
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, noise)
129
+	suite.NoError(err)
130
+
131
+	var rec bytes.Buffer
132
+
133
+	// Skip ServerHello
134
+	_, _, err = tls.ReadRecord(suite.buf, &rec)
135
+	suite.NoError(err)
136
+
137
+	// Skip ChangeCipherSpec
138
+	rec.Reset()
139
+	_, _, err = tls.ReadRecord(suite.buf, &rec)
140
+	suite.NoError(err)
141
+
142
+	// Read noise ApplicationData
143
+	rec.Reset()
144
+	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
145
+	suite.NoError(err)
146
+	suite.Equal(byte(tls.TypeApplicationData), recordType)
147
+
148
+	// Should be within mean ± jitter range.
149
+	suite.GreaterOrEqual(length, int64(noise.Mean-noise.Jitter))
150
+	suite.LessOrEqual(length, int64(noise.Mean+noise.Jitter))
151
+}
152
+
127
 func TestSendServerHello(t *testing.T) {
153
 func TestSendServerHello(t *testing.T) {
128
 	t.Parallel()
154
 	t.Parallel()
129
 	suite.Run(t, &SendServerHelloTestSuite{})
155
 	suite.Run(t, &SendServerHelloTestSuite{})

+ 42
- 1
mtglib/proxy.go Ver fichero

36
 	doppelGanger                *doppel.Ganger
36
 	doppelGanger                *doppel.Ganger
37
 	clientObfuscatror           obfuscation.Obfuscator
37
 	clientObfuscatror           obfuscation.Obfuscator
38
 
38
 
39
+	noiseParams     fake.NoiseParams
39
 	secret          Secret
40
 	secret          Secret
40
 	network         Network
41
 	network         Network
41
 	antiReplayCache AntiReplayCache
42
 	antiReplayCache AntiReplayCache
192
 		return false
193
 		return false
193
 	}
194
 	}
194
 
195
 
195
-	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello); err != nil {
196
+	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello, p.noiseParams); err != nil {
196
 		p.logger.InfoError("cannot send welcome packet", err)
197
 		p.logger.InfoError("cannot send welcome packet", err)
197
 		return false
198
 		return false
198
 	}
199
 	}
323
 	logger := opts.getLogger("proxy")
324
 	logger := opts.getLogger("proxy")
324
 	updatersLogger := logger.Named("telegram-updaters")
325
 	updatersLogger := logger.Named("telegram-updaters")
325
 
326
 
327
+	// Probe the fronting domain's cert chain size for noise calibration.
328
+	probeHost := opts.Secret.Host
329
+	probePort := opts.getDomainFrontingPort()
330
+	noiseParams := fake.NoiseParams{}
331
+
332
+	probeCount := int(opts.NoiseProbeCount)
333
+	if probeCount <= 0 {
334
+		probeCount = 15
335
+	}
336
+
337
+	cacheTTL := opts.NoiseCacheTTL
338
+
339
+	// Try loading from cache first.
340
+	if opts.NoiseCachePath != "" {
341
+		if cached, ok := fake.LoadCachedProbe(opts.NoiseCachePath, probeHost, probePort, cacheTTL); ok {
342
+			noiseParams = fake.NoiseParams(cached)
343
+			logger.Info(fmt.Sprintf("cert probe: loaded from cache, host=%s mean=%d jitter=%d",
344
+				probeHost, cached.Mean, cached.Jitter))
345
+		}
346
+	}
347
+
348
+	// If no cached result, probe live.
349
+	if noiseParams.Mean == 0 {
350
+		probeResult, probeErr := fake.ProbeCertSize(probeHost, probePort, probeCount)
351
+		if probeErr != nil {
352
+			logger.WarningError("cert probe failed, using default noise size", probeErr)
353
+		} else {
354
+			noiseParams = fake.NoiseParams(probeResult)
355
+			logger.Info(fmt.Sprintf("cert probe: host=%s mean=%d jitter=%d",
356
+				probeHost, probeResult.Mean, probeResult.Jitter))
357
+
358
+			if opts.NoiseCachePath != "" {
359
+				if saveErr := fake.SaveCachedProbe(opts.NoiseCachePath, probeHost, probePort, probeResult); saveErr != nil {
360
+					logger.WarningError("failed to save cert probe cache", saveErr)
361
+				}
362
+			}
363
+		}
364
+	}
365
+
326
 	proxy := &Proxy{
366
 	proxy := &Proxy{
327
 		ctx:                      ctx,
367
 		ctx:                      ctx,
328
 		ctxCancel:                cancel,
368
 		ctxCancel:                cancel,
369
+		noiseParams:              noiseParams,
329
 		secret:                   opts.Secret,
370
 		secret:                   opts.Secret,
330
 		network:                  opts.Network,
371
 		network:                  opts.Network,
331
 		antiReplayCache:          opts.AntiReplayCache,
372
 		antiReplayCache:          opts.AntiReplayCache,

+ 19
- 0
mtglib/proxy_opts.go Ver fichero

160
 
160
 
161
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
161
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
162
 	DoppelGangerDRS bool
162
 	DoppelGangerDRS bool
163
+
164
+	// NoiseProbeCount is the number of TLS connections to make when probing
165
+	// the fronting domain's cert chain size for noise calibration.
166
+	// Default is 15.
167
+	//
168
+	// This is an optional setting.
169
+	NoiseProbeCount uint
170
+
171
+	// NoiseCacheTTL is how long a cached cert probe result is considered
172
+	// valid. Default is 24 hours.
173
+	//
174
+	// This is an optional setting.
175
+	NoiseCacheTTL time.Duration
176
+
177
+	// NoiseCachePath is the file path for caching cert probe results
178
+	// between restarts. If empty, no caching is performed.
179
+	//
180
+	// This is an optional setting.
181
+	NoiseCachePath string
163
 }
182
 }
164
 
183
 
165
 func (p ProxyOpts) valid() error {
184
 func (p ProxyOpts) valid() error {

Loading…
Cancelar
Guardar