瀏覽代碼

Merge pull request #409 from dolonet/cert-noise-calibration

Add dynamic cert noise calibration for FakeTLS handshake
tags/v2.2.5^2^2
Sergei Arkhipov 1 月之前
父節點
當前提交
cc4b6ce2f4
沒有連結到貢獻者的電子郵件帳戶。

+ 4
- 4
internal/config/config.go 查看文件

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
 		} `json:"doppelganger"`
57
 		} `json:"doppelganger"`
58
 	} `json:"defense"`
58
 	} `json:"defense"`
59
 	Network struct {
59
 	Network struct {

+ 4
- 4
internal/config/parse.go 查看文件

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
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
52
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
53
 	} `toml:"defense" json:"defense,omitempty"`
53
 	} `toml:"defense" json:"defense,omitempty"`
54
 	Network struct {
54
 	Network struct {

+ 89
- 7
mtglib/internal/doppel/ganger.go 查看文件

2
 
2
 
3
 import (
3
 import (
4
 	"context"
4
 	"context"
5
+	"fmt"
5
 	"sync"
6
 	"sync"
7
+	"sync/atomic"
6
 	"time"
8
 	"time"
7
 
9
 
8
 	"github.com/9seconds/mtg/v2/essentials"
10
 	"github.com/9seconds/mtg/v2/essentials"
12
 	DoppelGangerMaxDurations  = 4096
14
 	DoppelGangerMaxDurations  = 4096
13
 	DoppelGangerScoutRaidEach = 6 * time.Hour
15
 	DoppelGangerScoutRaidEach = 6 * time.Hour
14
 	DoppelGangerScoutRepeats  = 10
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
 type gangerConnRequest struct {
33
 type gangerConnRequest struct {
18
 	ret     chan<- Conn
34
 	ret     chan<- Conn
19
 	payload essentials.Conn
35
 	payload essentials.Conn
33
 
49
 
34
 	stats     *Stats
50
 	stats     *Stats
35
 	durations []time.Duration
51
 	durations []time.Duration
52
+	certSizes []int
53
+
54
+	noiseParams atomic.Pointer[NoiseParams]
36
 
55
 
37
 	connRequests chan gangerConnRequest
56
 	connRequests chan gangerConnRequest
38
 }
57
 }
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
 func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
80
 func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
52
 	rvChan := make(chan Conn)
81
 	rvChan := make(chan Conn)
53
 	req := gangerConnRequest{
82
 	req := gangerConnRequest{
81
 		}
110
 		}
82
 	}()
111
 	}()
83
 
112
 
84
-	scoutCollectedChan := make(chan []time.Duration)
113
+	scoutCollectedChan := make(chan scoutRaidResult)
85
 	currentScoutCollectedChan := scoutCollectedChan
114
 	currentScoutCollectedChan := scoutCollectedChan
86
 
115
 
87
 	updatedStatsChan := make(chan *Stats)
116
 	updatedStatsChan := make(chan *Stats)
94
 		select {
123
 		select {
95
 		case <-g.ctx.Done():
124
 		case <-g.ctx.Done():
96
 			return
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
 			if len(g.durations) > DoppelGangerMaxDurations {
129
 			if len(g.durations) > DoppelGangerMaxDurations {
101
 				copy(g.durations, g.durations[len(g.durations)-DoppelGangerMaxDurations:])
130
 				copy(g.durations, g.durations[len(g.durations)-DoppelGangerMaxDurations:])
102
 				g.durations = g.durations[:DoppelGangerMaxDurations]
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
 			if len(g.durations) < MinDurationsToCalculate {
144
 			if len(g.durations) < MinDurationsToCalculate {
106
 				continue
145
 				continue
107
 			}
146
 			}
108
 
147
 
148
+			durations := g.durations
109
 			currentScoutCollectedChan = nil
149
 			currentScoutCollectedChan = nil
110
 			g.wg.Go(func() {
150
 			g.wg.Go(func() {
111
 				select {
151
 				select {
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
 	for range g.scoutRaidRepeats {
212
 	for range g.scoutRaidRepeats {
136
 		learned, err := g.scout.Learn(g.ctx)
213
 		learned, err := g.scout.Learn(g.ctx)
138
 			g.logger.WarningError("cannot learn", err)
215
 			g.logger.WarningError("cannot learn", err)
139
 			continue
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
 	select {
226
 	select {
145
 	case <-g.ctx.Done():
227
 	case <-g.ctx.Done():
146
 		return
228
 		return
147
-	case rvChan <- durations:
229
+	case rvChan <- result:
148
 	}
230
 	}
149
 }
231
 }
150
 
232
 

+ 47
- 12
mtglib/internal/doppel/scout.go 查看文件

12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
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
 type Scout struct {
21
 type Scout struct {
16
 	network Network
22
 	network Network
17
 	urls    []string
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
 	for _, url := range s.urls {
29
 	for _, url := range s.urls {
24
 		learned, err := s.learn(ctx, url)
30
 		learned, err := s.learn(ctx, url)
25
 		if err != nil {
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
 	client, results := s.makeClient()
46
 	client, results := s.makeClient()
37
 
47
 
38
 	if !strings.HasPrefix(url, "https://") {
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
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
52
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
43
 	if err != nil {
53
 	if err != nil {
44
-		return nil, err
54
+		return ScoutResult{}, err
45
 	}
55
 	}
46
 
56
 
47
 	resp, err := client.Do(req)
57
 	resp, err := client.Do(req)
52
 	}
62
 	}
53
 
63
 
54
 	if err != nil || len(results.data) == 0 {
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
 	lastTimestamp := time.Time{}
71
 	lastTimestamp := time.Time{}
60
 
72
 
61
 	for i, v := range results.data {
73
 	for i, v := range results.data {
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
 		lastTimestamp = v.timestamp
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
 func (s Scout) makeClient() (*http.Client, *ScoutConnCollected) {
116
 func (s Scout) makeClient() (*http.Client, *ScoutConnCollected) {

+ 17
- 4
mtglib/internal/doppel/scout_conn.go 查看文件

14
 
14
 
15
 	results *ScoutConnCollected
15
 	results *ScoutConnCollected
16
 	rawBuf  *bytes.Buffer
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
 	buf := &bytes.Buffer{}
21
 	buf := &bytes.Buffer{}
21
 
22
 
22
 	for {
23
 	for {
31
 			return 0, err
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
 		s.rawBuf.Write([]byte{recordType})
40
 		s.rawBuf.Write([]byte{recordType})
36
 		s.rawBuf.Write(tls.TLSVersion[:])
41
 		s.rawBuf.Write(tls.TLSVersion[:])
37
 
42
 
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
 	rawBuf := &bytes.Buffer{}
62
 	rawBuf := &bytes.Buffer{}
50
 	rawBuf.Grow(tls.MaxRecordSize)
63
 	rawBuf.Grow(tls.MaxRecordSize)
51
 
64
 
52
-	return ScoutConn{
65
+	return &ScoutConn{
53
 		Conn:    tls.New(conn, false, false),
66
 		Conn:    tls.New(conn, false, false),
54
 		results: results,
67
 		results: results,
55
 		rawBuf:  rawBuf,
68
 		rawBuf:  rawBuf,

+ 14
- 3
mtglib/internal/doppel/scout_conn_collected.go 查看文件

9
 type ScoutConnResult struct {
9
 type ScoutConnResult struct {
10
 	timestamp  time.Time
10
 	timestamp  time.Time
11
 	recordType byte
11
 	recordType byte
12
+	payloadLen int
12
 }
13
 }
13
 
14
 
14
 type ScoutConnCollected struct {
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
 	s.data = append(s.data, ScoutConnResult{
21
 	s.data = append(s.data, ScoutConnResult{
20
 		timestamp:  time.Now(),
22
 		timestamp:  time.Now(),
21
 		recordType: record,
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
 func NewScoutConnCollected() *ScoutConnCollected {
35
 func NewScoutConnCollected() *ScoutConnCollected {
26
 	return &ScoutConnCollected{
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
 
14
 
15
 func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
15
 func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
16
 	collected := NewScoutConnCollected()
16
 	collected := NewScoutConnCollected()
17
-	collected.Add(tls.TypeApplicationData)
17
+	collected.Add(tls.TypeApplicationData, 100)
18
 
18
 
19
 	suite.Len(collected.data, 1)
19
 	suite.Len(collected.data, 1)
20
 	suite.Equal(byte(tls.TypeApplicationData), collected.data[0].recordType)
20
 	suite.Equal(byte(tls.TypeApplicationData), collected.data[0].recordType)
23
 func (suite *ScoutConnCollectedTestSuite) TestAddTimestampsAreMonotonic() {
23
 func (suite *ScoutConnCollectedTestSuite) TestAddTimestampsAreMonotonic() {
24
 	collected := NewScoutConnCollected()
24
 	collected := NewScoutConnCollected()
25
 
25
 
26
-	collected.Add(tls.TypeApplicationData)
26
+	collected.Add(tls.TypeApplicationData, 100)
27
 
27
 
28
 	time.Sleep(time.Microsecond)
28
 	time.Sleep(time.Microsecond)
29
-	collected.Add(tls.TypeApplicationData)
29
+	collected.Add(tls.TypeApplicationData, 100)
30
 
30
 
31
 	time.Sleep(time.Microsecond)
31
 	time.Sleep(time.Microsecond)
32
-	collected.Add(tls.TypeApplicationData)
32
+	collected.Add(tls.TypeApplicationData, 100)
33
 
33
 
34
 	for i := 1; i < len(collected.data); i++ {
34
 	for i := 1; i < len(collected.data); i++ {
35
 		suite.True(collected.data[i].timestamp.After(collected.data[i-1].timestamp))
35
 		suite.True(collected.data[i].timestamp.After(collected.data[i-1].timestamp))

+ 2
- 2
mtglib/internal/doppel/scout_test.go 查看文件

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

+ 34
- 15
mtglib/internal/tls/fake/server_side.go 查看文件

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 查看文件

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{})

+ 4
- 1
mtglib/proxy.go 查看文件

192
 		return false
192
 		return false
193
 	}
193
 	}
194
 
194
 
195
-	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello); 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 {
196
 		p.logger.InfoError("cannot send welcome packet", err)
199
 		p.logger.InfoError("cannot send welcome packet", err)
197
 		return false
200
 		return false
198
 	}
201
 	}

+ 1
- 0
mtglib/proxy_opts.go 查看文件

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
+
163
 }
164
 }
164
 
165
 
165
 func (p ProxyOpts) valid() error {
166
 func (p ProxyOpts) valid() error {

Loading…
取消
儲存