Преглед изворни кода

Move cert noise calibration into doppelganger scout

Instead of a separate cert_probe.go that duplicates the scout's TLS
connection logic, measure the cert chain size directly from the same
HTTPS connections the scout already makes.

Changes:
- Extend ScoutConnResult with payloadLen field
- Add Write interception to ScoutConn for handshake boundary detection
- Scout.learn() now computes cert size (sum of ApplicationData between
  CCS and first client Write) alongside inter-record durations
- Ganger aggregates cert sizes across raids and exposes NoiseParams()
  via atomic pointer for lock-free reads from proxy goroutines
- Proxy reads NoiseParams from Ganger on each handshake instead of
  probing at startup
- Remove cert_probe.go, disk cache, and related config options
  (noise-cache-path, noise-cache-ttl, noise-probe-count)

Falls back to legacy 2500-4700 range until the first scout raid
completes (typically within 1-2 seconds of startup).
tags/v2.2.5^2^2
Alexey Dolotov пре 1 месец
родитељ
комит
9dfd992c1d

+ 0
- 4
internal/cli/run_proxy.go Прегледај датотеку

@@ -267,10 +267,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
267 267
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
268 268
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
269 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,
274 270
 	}
275 271
 
276 272
 	proxy, err := mtglib.NewProxy(opts)

+ 0
- 3
internal/config/config.go Прегледај датотеку

@@ -54,9 +54,6 @@ type Config struct {
54 54
 			Repeats         TypeConcurrency `json:"repeats_per_raid"`
55 55
 			UpdateEach      TypeDuration    `json:"raid_each"`
56 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"`
60 57
 		} `json:"doppelganger"`
61 58
 	} `json:"defense"`
62 59
 	Network struct {

+ 0
- 3
internal/config/parse.go Прегледај датотеку

@@ -49,9 +49,6 @@ type tomlConfig struct {
49 49
 			Repeats         uint     `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
50 50
 			UpdateEach      string   `toml:"raid-each" json:"raid_each,omitempty"`
51 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"`
55 52
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
56 53
 	} `toml:"defense" json:"defense,omitempty"`
57 54
 	Network struct {

+ 89
- 7
mtglib/internal/doppel/ganger.go Прегледај датотеку

@@ -2,7 +2,9 @@ package doppel
2 2
 
3 3
 import (
4 4
 	"context"
5
+	"fmt"
5 6
 	"sync"
7
+	"sync/atomic"
6 8
 	"time"
7 9
 
8 10
 	"github.com/9seconds/mtg/v2/essentials"
@@ -12,8 +14,22 @@ const (
12 14
 	DoppelGangerMaxDurations  = 4096
13 15
 	DoppelGangerScoutRaidEach = 6 * time.Hour
14 16
 	DoppelGangerScoutRepeats  = 10
17
+
18
+	MinCertSizesToCalculate = 3
15 19
 )
16 20
 
21
+// NoiseParams holds the measured cert chain size for FakeTLS noise calibration.
22
+// If Mean is 0, the caller should use a legacy fallback.
23
+type NoiseParams struct {
24
+	Mean   int
25
+	Jitter int
26
+}
27
+
28
+type scoutRaidResult struct {
29
+	durations []time.Duration
30
+	certSizes []int
31
+}
32
+
17 33
 type gangerConnRequest struct {
18 34
 	ret     chan<- Conn
19 35
 	payload essentials.Conn
@@ -33,6 +49,9 @@ type Ganger struct {
33 49
 
34 50
 	stats     *Stats
35 51
 	durations []time.Duration
52
+	certSizes []int
53
+
54
+	noiseParams atomic.Pointer[NoiseParams]
36 55
 
37 56
 	connRequests chan gangerConnRequest
38 57
 }
@@ -48,6 +67,16 @@ func (g *Ganger) Run() {
48 67
 	})
49 68
 }
50 69
 
70
+// NoiseParams returns the current cert-size-based noise parameters.
71
+// Returns zero-value NoiseParams if not yet measured (caller should use fallback).
72
+func (g *Ganger) NoiseParams() NoiseParams {
73
+	if p := g.noiseParams.Load(); p != nil {
74
+		return *p
75
+	}
76
+
77
+	return NoiseParams{}
78
+}
79
+
51 80
 func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
52 81
 	rvChan := make(chan Conn)
53 82
 	req := gangerConnRequest{
@@ -81,7 +110,7 @@ func (g *Ganger) run() {
81 110
 		}
82 111
 	}()
83 112
 
84
-	scoutCollectedChan := make(chan []time.Duration)
113
+	scoutCollectedChan := make(chan scoutRaidResult)
85 114
 	currentScoutCollectedChan := scoutCollectedChan
86 115
 
87 116
 	updatedStatsChan := make(chan *Stats)
@@ -94,18 +123,29 @@ func (g *Ganger) run() {
94 123
 		select {
95 124
 		case <-g.ctx.Done():
96 125
 			return
97
-		case durations := <-currentScoutCollectedChan:
98
-			g.durations = append(g.durations, durations...)
126
+		case result := <-currentScoutCollectedChan:
127
+			g.durations = append(g.durations, result.durations...)
99 128
 
100 129
 			if len(g.durations) > DoppelGangerMaxDurations {
101 130
 				copy(g.durations, g.durations[len(g.durations)-DoppelGangerMaxDurations:])
102 131
 				g.durations = g.durations[:DoppelGangerMaxDurations]
103 132
 			}
104 133
 
134
+			// Update cert sizes and recompute noise params.
135
+			g.certSizes = append(g.certSizes, result.certSizes...)
136
+			if len(g.certSizes) > DoppelGangerMaxDurations {
137
+				g.certSizes = g.certSizes[len(g.certSizes)-DoppelGangerMaxDurations:]
138
+			}
139
+
140
+			if len(g.certSizes) >= MinCertSizesToCalculate {
141
+				g.updateNoiseParams()
142
+			}
143
+
105 144
 			if len(g.durations) < MinDurationsToCalculate {
106 145
 				continue
107 146
 			}
108 147
 
148
+			durations := g.durations
109 149
 			currentScoutCollectedChan = nil
110 150
 			g.wg.Go(func() {
111 151
 				select {
@@ -129,8 +169,45 @@ func (g *Ganger) run() {
129 169
 	}
130 170
 }
131 171
 
132
-func (g *Ganger) runScoutRaid(rvChan chan<- []time.Duration) {
133
-	durations := []time.Duration{}
172
+func (g *Ganger) updateNoiseParams() {
173
+	if len(g.certSizes) == 0 {
174
+		return
175
+	}
176
+
177
+	sum := 0
178
+	for _, s := range g.certSizes {
179
+		sum += s
180
+	}
181
+
182
+	mean := sum / len(g.certSizes)
183
+
184
+	maxDev := 0
185
+	for _, s := range g.certSizes {
186
+		d := s - mean
187
+		if d < 0 {
188
+			d = -d
189
+		}
190
+
191
+		if d > maxDev {
192
+			maxDev = d
193
+		}
194
+	}
195
+
196
+	if maxDev < 100 {
197
+		maxDev = 100
198
+	}
199
+
200
+	np := &NoiseParams{Mean: mean, Jitter: maxDev}
201
+	g.noiseParams.Store(np)
202
+
203
+	g.logger.Info(fmt.Sprintf(
204
+		"updated noise params: mean=%d jitter=%d samples=%d",
205
+		mean, maxDev, len(g.certSizes),
206
+	))
207
+}
208
+
209
+func (g *Ganger) runScoutRaid(rvChan chan<- scoutRaidResult) {
210
+	var result scoutRaidResult
134 211
 
135 212
 	for range g.scoutRaidRepeats {
136 213
 		learned, err := g.scout.Learn(g.ctx)
@@ -138,13 +215,18 @@ func (g *Ganger) runScoutRaid(rvChan chan<- []time.Duration) {
138 215
 			g.logger.WarningError("cannot learn", err)
139 216
 			continue
140 217
 		}
141
-		durations = append(durations, learned...)
218
+
219
+		result.durations = append(result.durations, learned.Durations...)
220
+
221
+		if learned.CertSize > 0 {
222
+			result.certSizes = append(result.certSizes, learned.CertSize)
223
+		}
142 224
 	}
143 225
 
144 226
 	select {
145 227
 	case <-g.ctx.Done():
146 228
 		return
147
-	case rvChan <- durations:
229
+	case rvChan <- result:
148 230
 	}
149 231
 }
150 232
 

+ 47
- 12
mtglib/internal/doppel/scout.go Прегледај датотеку

@@ -12,36 +12,46 @@ import (
12 12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
13 13
 )
14 14
 
15
+// ScoutResult holds measurements from a single scout HTTP request.
16
+type ScoutResult struct {
17
+	Durations []time.Duration
18
+	CertSize  int // total ApplicationData bytes during TLS handshake; 0 if unknown
19
+}
20
+
15 21
 type Scout struct {
16 22
 	network Network
17 23
 	urls    []string
18 24
 }
19 25
 
20
-func (s Scout) Learn(ctx context.Context) ([]time.Duration, error) {
21
-	var durations []time.Duration
26
+func (s Scout) Learn(ctx context.Context) (ScoutResult, error) {
27
+	var combined ScoutResult
22 28
 
23 29
 	for _, url := range s.urls {
24 30
 		learned, err := s.learn(ctx, url)
25 31
 		if err != nil {
26
-			return nil, err
32
+			return ScoutResult{}, err
27 33
 		}
28 34
 
29
-		durations = append(durations, learned...)
35
+		combined.Durations = append(combined.Durations, learned.Durations...)
36
+
37
+		if learned.CertSize > 0 && combined.CertSize == 0 {
38
+			combined.CertSize = learned.CertSize
39
+		}
30 40
 	}
31 41
 
32
-	return durations, nil
42
+	return combined, nil
33 43
 }
34 44
 
35
-func (s Scout) learn(ctx context.Context, url string) ([]time.Duration, error) {
45
+func (s Scout) learn(ctx context.Context, url string) (ScoutResult, error) {
36 46
 	client, results := s.makeClient()
37 47
 
38 48
 	if !strings.HasPrefix(url, "https://") {
39
-		return nil, fmt.Errorf("url %s must be https", url)
49
+		return ScoutResult{}, fmt.Errorf("url %s must be https", url)
40 50
 	}
41 51
 
42 52
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
43 53
 	if err != nil {
44
-		return nil, err
54
+		return ScoutResult{}, err
45 55
 	}
46 56
 
47 57
 	resp, err := client.Do(req)
@@ -52,10 +62,12 @@ func (s Scout) learn(ctx context.Context, url string) ([]time.Duration, error) {
52 62
 	}
53 63
 
54 64
 	if err != nil || len(results.data) == 0 {
55
-		return nil, err
65
+		return ScoutResult{}, err
56 66
 	}
57 67
 
58
-	durations := []time.Duration{}
68
+	var result ScoutResult
69
+
70
+	// Compute inter-record durations (existing logic).
59 71
 	lastTimestamp := time.Time{}
60 72
 
61 73
 	for i, v := range results.data {
@@ -71,11 +83,34 @@ func (s Scout) learn(ctx context.Context, url string) ([]time.Duration, error) {
71 83
 			}
72 84
 		}
73 85
 
74
-		durations = append(durations, v.timestamp.Sub(lastTimestamp))
86
+		result.Durations = append(result.Durations, v.timestamp.Sub(lastTimestamp))
75 87
 		lastTimestamp = v.timestamp
76 88
 	}
77 89
 
78
-	return durations, nil
90
+	// Compute cert size: sum of ApplicationData payload between CCS and
91
+	// the first client Write (which marks the end of server handshake).
92
+	seenCCS := false
93
+	boundary := results.writeIndex
94
+	if boundary < 0 {
95
+		boundary = len(results.data)
96
+	}
97
+
98
+	for i, v := range results.data {
99
+		if i >= boundary {
100
+			break
101
+		}
102
+
103
+		if v.recordType == tls.TypeChangeCipherSpec {
104
+			seenCCS = true
105
+			continue
106
+		}
107
+
108
+		if seenCCS && v.recordType == tls.TypeApplicationData {
109
+			result.CertSize += v.payloadLen
110
+		}
111
+	}
112
+
113
+	return result, nil
79 114
 }
80 115
 
81 116
 func (s Scout) makeClient() (*http.Client, *ScoutConnCollected) {

+ 17
- 4
mtglib/internal/doppel/scout_conn.go Прегледај датотеку

@@ -14,9 +14,10 @@ type ScoutConn struct {
14 14
 
15 15
 	results *ScoutConnCollected
16 16
 	rawBuf  *bytes.Buffer
17
+	seenCCS bool
17 18
 }
18 19
 
19
-func (s ScoutConn) Read(p []byte) (int, error) {
20
+func (s *ScoutConn) Read(p []byte) (int, error) {
20 21
 	buf := &bytes.Buffer{}
21 22
 
22 23
 	for {
@@ -31,7 +32,11 @@ func (s ScoutConn) Read(p []byte) (int, error) {
31 32
 			return 0, err
32 33
 		}
33 34
 
34
-		s.results.Add(recordType)
35
+		if recordType == tls.TypeChangeCipherSpec {
36
+			s.seenCCS = true
37
+		}
38
+
39
+		s.results.Add(recordType, int(length))
35 40
 		s.rawBuf.Write([]byte{recordType})
36 41
 		s.rawBuf.Write(tls.TLSVersion[:])
37 42
 
@@ -45,11 +50,19 @@ func (s ScoutConn) Read(p []byte) (int, error) {
45 50
 	}
46 51
 }
47 52
 
48
-func NewScoutConn(conn essentials.Conn, results *ScoutConnCollected) ScoutConn {
53
+func (s *ScoutConn) Write(p []byte) (int, error) {
54
+	if s.seenCCS {
55
+		s.results.MarkWrite()
56
+	}
57
+
58
+	return s.Conn.Write(p)
59
+}
60
+
61
+func NewScoutConn(conn essentials.Conn, results *ScoutConnCollected) *ScoutConn {
49 62
 	rawBuf := &bytes.Buffer{}
50 63
 	rawBuf.Grow(tls.MaxRecordSize)
51 64
 
52
-	return ScoutConn{
65
+	return &ScoutConn{
53 66
 		Conn:    tls.New(conn, false, false),
54 67
 		results: results,
55 68
 		rawBuf:  rawBuf,

+ 14
- 3
mtglib/internal/doppel/scout_conn_collected.go Прегледај датотеку

@@ -9,21 +9,32 @@ const (
9 9
 type ScoutConnResult struct {
10 10
 	timestamp  time.Time
11 11
 	recordType byte
12
+	payloadLen int
12 13
 }
13 14
 
14 15
 type ScoutConnCollected struct {
15
-	data []ScoutConnResult
16
+	data       []ScoutConnResult
17
+	writeIndex int // index at which client first wrote post-handshake data; -1 if not set
16 18
 }
17 19
 
18
-func (s *ScoutConnCollected) Add(record byte) {
20
+func (s *ScoutConnCollected) Add(record byte, payloadLen int) {
19 21
 	s.data = append(s.data, ScoutConnResult{
20 22
 		timestamp:  time.Now(),
21 23
 		recordType: record,
24
+		payloadLen: payloadLen,
22 25
 	})
23 26
 }
24 27
 
28
+// MarkWrite records the current data length as the handshake boundary.
29
+func (s *ScoutConnCollected) MarkWrite() {
30
+	if s.writeIndex < 0 {
31
+		s.writeIndex = len(s.data)
32
+	}
33
+}
34
+
25 35
 func NewScoutConnCollected() *ScoutConnCollected {
26 36
 	return &ScoutConnCollected{
27
-		data: make([]ScoutConnResult, 0, ScoutConnCollectedPreallocSize),
37
+		data:       make([]ScoutConnResult, 0, ScoutConnCollectedPreallocSize),
38
+		writeIndex: -1,
28 39
 	}
29 40
 }

+ 4
- 4
mtglib/internal/doppel/scout_conn_collected_test.go Прегледај датотеку

@@ -14,7 +14,7 @@ type ScoutConnCollectedTestSuite struct {
14 14
 
15 15
 func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
16 16
 	collected := NewScoutConnCollected()
17
-	collected.Add(tls.TypeApplicationData)
17
+	collected.Add(tls.TypeApplicationData, 100)
18 18
 
19 19
 	suite.Len(collected.data, 1)
20 20
 	suite.Equal(byte(tls.TypeApplicationData), collected.data[0].recordType)
@@ -23,13 +23,13 @@ func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
23 23
 func (suite *ScoutConnCollectedTestSuite) TestAddTimestampsAreMonotonic() {
24 24
 	collected := NewScoutConnCollected()
25 25
 
26
-	collected.Add(tls.TypeApplicationData)
26
+	collected.Add(tls.TypeApplicationData, 100)
27 27
 
28 28
 	time.Sleep(time.Microsecond)
29
-	collected.Add(tls.TypeApplicationData)
29
+	collected.Add(tls.TypeApplicationData, 100)
30 30
 
31 31
 	time.Sleep(time.Microsecond)
32
-	collected.Add(tls.TypeApplicationData)
32
+	collected.Add(tls.TypeApplicationData, 100)
33 33
 
34 34
 	for i := 1; i < len(collected.data); i++ {
35 35
 		suite.True(collected.data[i].timestamp.After(collected.data[i-1].timestamp))

+ 2
- 2
mtglib/internal/doppel/scout_test.go Прегледај датотеку

@@ -22,9 +22,9 @@ func (suite *ScoutTestSuite) SetupSuite() {
22 22
 }
23 23
 
24 24
 func (suite *ScoutTestSuite) TestCollectResults() {
25
-	durations, err := suite.scout.Learn(suite.ctx)
25
+	result, err := suite.scout.Learn(suite.ctx)
26 26
 	suite.NoError(err)
27
-	suite.Less(3, len(durations))
27
+	suite.Less(3, len(result.Durations))
28 28
 }
29 29
 
30 30
 func (suite *ScoutTestSuite) TestCollectNothing() {

+ 0
- 261
mtglib/internal/tls/fake/cert_probe.go Прегледај датотеку

@@ -1,261 +0,0 @@
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)

+ 4
- 42
mtglib/proxy.go Прегледај датотеку

@@ -36,7 +36,6 @@ type Proxy struct {
36 36
 	doppelGanger                *doppel.Ganger
37 37
 	clientObfuscatror           obfuscation.Obfuscator
38 38
 
39
-	noiseParams     fake.NoiseParams
40 39
 	secret          Secret
41 40
 	network         Network
42 41
 	antiReplayCache AntiReplayCache
@@ -193,7 +192,10 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
193 192
 		return false
194 193
 	}
195 194
 
196
-	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello, p.noiseParams); err != nil {
195
+	gangerNoise := p.doppelGanger.NoiseParams()
196
+	noiseParams := fake.NoiseParams{Mean: gangerNoise.Mean, Jitter: gangerNoise.Jitter}
197
+
198
+	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello, noiseParams); err != nil {
197 199
 		p.logger.InfoError("cannot send welcome packet", err)
198 200
 		return false
199 201
 	}
@@ -324,49 +326,9 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
324 326
 	logger := opts.getLogger("proxy")
325 327
 	updatersLogger := logger.Named("telegram-updaters")
326 328
 
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
-
366 329
 	proxy := &Proxy{
367 330
 		ctx:                      ctx,
368 331
 		ctxCancel:                cancel,
369
-		noiseParams:              noiseParams,
370 332
 		secret:                   opts.Secret,
371 333
 		network:                  opts.Network,
372 334
 		antiReplayCache:          opts.AntiReplayCache,

+ 0
- 18
mtglib/proxy_opts.go Прегледај датотеку

@@ -161,24 +161,6 @@ type ProxyOpts struct {
161 161
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
162 162
 	DoppelGangerDRS bool
163 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
182 164
 }
183 165
 
184 166
 func (p ProxyOpts) valid() error {

Loading…
Откажи
Сачувај