Quellcode durchsuchen

Merge pull request #352 from 9seconds/doppleganger

Doppleganger
tags/v2.2.0^2^2
Sergei Arkhipov vor 1 Monat
Ursprung
Commit
6493688282
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden
76 geänderte Dateien mit 3441 neuen und 1347 gelöschten Zeilen
  1. 1
    1
      .mise.toml
  2. 61
    0
      example.config.toml
  3. 9
    0
      internal/cli/run_proxy.go
  4. 7
    2
      internal/config/config.go
  5. 5
    0
      internal/config/parse.go
  6. 53
    0
      internal/config/type_https_url.go
  7. 100
    0
      internal/config/type_https_url_test.go
  8. 7
    0
      mtglib/init.go
  9. 35
    0
      mtglib/internal/doppel/clock.go
  10. 80
    0
      mtglib/internal/doppel/clock_test.go
  11. 95
    0
      mtglib/internal/doppel/conn.go
  12. 163
    0
      mtglib/internal/doppel/conn_test.go
  13. 179
    0
      mtglib/internal/doppel/ganger.go
  14. 107
    0
      mtglib/internal/doppel/ganger_test.go
  15. 38
    0
      mtglib/internal/doppel/init.go
  16. 103
    0
      mtglib/internal/doppel/init_test.go
  17. 6
    0
      mtglib/internal/doppel/logger.go
  18. 104
    0
      mtglib/internal/doppel/scout.go
  19. 57
    0
      mtglib/internal/doppel/scout_conn.go
  20. 29
    0
      mtglib/internal/doppel/scout_conn_collected.go
  21. 42
    0
      mtglib/internal/doppel/scout_conn_collected_test.go
  22. 39
    0
      mtglib/internal/doppel/scout_test.go
  23. 154
    0
      mtglib/internal/doppel/stats.go
  24. 194
    0
      mtglib/internal/doppel/stats_test.go
  25. 0
    134
      mtglib/internal/faketls/client_hello.go
  26. 0
    21
      mtglib/internal/faketls/client_hello_fuzz_test.go
  27. 0
    191
      mtglib/internal/faketls/client_hello_test.go
  28. 0
    72
      mtglib/internal/faketls/conn.go
  29. 0
    153
      mtglib/internal/faketls/conn_test.go
  30. 0
    59
      mtglib/internal/faketls/init.go
  31. 0
    84
      mtglib/internal/faketls/record/init.go
  32. 0
    79
      mtglib/internal/faketls/record/init_test.go
  33. 0
    20
      mtglib/internal/faketls/record/pools.go
  34. 0
    86
      mtglib/internal/faketls/record/record.go
  35. 0
    110
      mtglib/internal/faketls/record/record_test.go
  36. 0
    6
      mtglib/internal/faketls/record/testdata/05eb6b71f87b6802.json
  37. 0
    6
      mtglib/internal/faketls/record/testdata/4eef4abc15b206b6.json
  38. 0
    6
      mtglib/internal/faketls/record/testdata/736f358216afe91f.json
  39. 0
    6
      mtglib/internal/faketls/record/testdata/8405d94222bd0b6a.json
  40. 0
    6
      mtglib/internal/faketls/record/testdata/9036f76e517f0cd1.json
  41. 0
    6
      mtglib/internal/faketls/record/testdata/9244766a0fe4a02a.json
  42. 0
    6
      mtglib/internal/faketls/record/testdata/9255c73d3de76e7b.json
  43. 0
    6
      mtglib/internal/faketls/record/testdata/aeb65b9924315cf8.json
  44. 0
    6
      mtglib/internal/faketls/record/testdata/b0acd44296056b54.json
  45. 0
    6
      mtglib/internal/faketls/record/testdata/c0545a13fd9a3fa3.json
  46. 0
    6
      mtglib/internal/faketls/record/testdata/f083f4501668b759.json
  47. 0
    6
      mtglib/internal/faketls/record/testdata/f5696bcdffd11706.json
  48. 0
    8
      mtglib/internal/faketls/testdata/client-hello-bad-fa2e46cdb33e2a1b.json
  49. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-19dfe38384b9884b.json
  50. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-48f8a72a56f3174a.json
  51. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-651054256093c6cd.json
  52. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-79d01ef18a9d2621.json
  53. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-7a5569f05b118145.json
  54. 0
    91
      mtglib/internal/faketls/welcome.go
  55. 0
    82
      mtglib/internal/faketls/welcome_test.go
  56. 86
    0
      mtglib/internal/tls/conn.go
  57. 160
    0
      mtglib/internal/tls/conn_test.go
  58. 21
    0
      mtglib/internal/tls/fake/bytes_pool.go
  59. 309
    0
      mtglib/internal/tls/fake/client_side.go
  60. 48
    0
      mtglib/internal/tls/fake/client_side_fuzz_test.go
  61. 153
    0
      mtglib/internal/tls/fake/client_side_snapshot_test.go
  62. 395
    0
      mtglib/internal/tls/fake/client_side_test.go
  63. 16
    0
      mtglib/internal/tls/fake/init.go
  64. 138
    0
      mtglib/internal/tls/fake/server_side.go
  65. 132
    0
      mtglib/internal/tls/fake/server_side_test.go
  66. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-bad-fa2e46cdb33e2a1b.json
  67. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-19dfe38384b9884b.json
  68. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-48f8a72a56f3174a.json
  69. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-651054256093c6cd.json
  70. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-79d01ef18a9d2621.json
  71. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-7a5569f05b118145.json
  72. 30
    0
      mtglib/internal/tls/init_test.go
  73. 48
    0
      mtglib/internal/tls/utils.go
  74. 125
    0
      mtglib/internal/tls/utils_test.go
  75. 49
    42
      mtglib/proxy.go
  76. 15
    0
      mtglib/proxy_opts.go

+ 1
- 1
.mise.toml Datei anzeigen

@@ -48,7 +48,7 @@ depends = [
48 48
 
49 49
 [tasks."test:fuzz:client-hello"]
50 50
 description = "Run fuzzy test for ClientHello"
51
-run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls"
51
+run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
52 52
 
53 53
 [tasks."test:fuzz:client-handshake"]
54 54
 description = "Run fuzzy test for ClientHandshake"

+ 61
- 0
example.config.toml Datei anzeigen

@@ -206,6 +206,67 @@ tcp = "5s"
206 206
 http = "10s"
207 207
 idle = "1m"
208 208
 
209
+# mtg has to mimic real websites. It does not mean domain fronting, it also
210
+# means that traffic characteristics should be similar to real world traffic.
211
+# websites and applications behave differently, their traffic patterns are also
212
+# different. Applications do bursts of RPC-style messages (or JSON communication,
213
+# does not really matter), while websites pump heavy content in HTTP2 streams
214
+#
215
+# It means that statistically there is a different between traffic shape:
216
+# TLS packet sizes are different, delays between packets are also different.
217
+# In order to avoid censorship detection based on these patterns, there is a
218
+# mtg subsystem called "Doppelganger" that aims to mimic website statistics
219
+# as close as it could.
220
+#
221
+# It does that by 2 ideas:
222
+#   1. Delays between TLS packets are not constant. There are many factors
223
+#      that come in play. Application should generate some response, it could
224
+#      send some headers first and stream content with chunked encoding. So
225
+#      some first packets could come as soon as possible, with some delays
226
+#      after first ones. Such phenomenon is described by different statistic
227
+#      distribution. There are 2 distribution that describe it: lognormal
228
+#      distribution and Weibul distribution. Lognormal is all about steady streams
229
+#      of heavy content like a video. Weibul is great about short bursts like
230
+#      user who requested a static page an a couple of images.
231
+#
232
+#      mtg tries to adapt Weibul distribution. It comes with some sensible
233
+#      defaults that were taken from ok.ru. But when you use domain fronting,
234
+#      it always make sense to take statistics from that website. You can specify
235
+#      some urls here. mtg will crawl them from time to time, accumulate time
236
+#      series and approximates parameters for Weibul.
237
+#   2. TLS record sizes are not random.
238
+#      https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
239
+#      https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
240
+#
241
+#      The idea is that huge TLS records could negatively affect performance.
242
+#      You cannot simply decrypt a part of the packet, you need to wait it
243
+#      whole, and huge packets could involve several RTTs if you do not use
244
+#      any specific software that treat TLS in a very special way. So
245
+#      servers start with small packets, usually around MTU, and ramp up
246
+#      later. This optimizes a time-to-first byte so web browsers start to
247
+#      render early.
248
+#
249
+#      mtg uses the same technique as was introduced by Cloudflare in their
250
+#      patches to nginx 10 years ago:
251
+#      https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
252
+[defense.doppelganger]
253
+# This is a list of URLs that would be crawled by mtg to approximate delay
254
+# statistics. They MUST be HTTPS urls.
255
+#
256
+# You can come to the website and collect different URLs, with light and
257
+# heavy content. We recommend to search for CDNs.
258
+urls = [
259
+    # "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
260
+]
261
+# A collection is done in raids. Each raid makes this number of requests to
262
+# each URL in this list. Do not use a huge number, 10 is probably ok.
263
+repeats-per-raid = 10
264
+# This is a duration between each raid. It makes no sense to have a small number
265
+# here as you would start to make a noticeable activity. Usually traffic patterns
266
+# do not change a lot, so do not expect different results if you request
267
+# each 10 minutes.
268
+raid-each = "6h"
269
+
209 270
 # Some countries do active probing on Telegram connections. This technique
210 271
 # allows to protect from such effort.
211 272
 #

+ 9
- 0
internal/cli/run_proxy.go Datei anzeigen

@@ -239,6 +239,11 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
239 239
 		return fmt.Errorf("cannot build ip allowlist: %w", err)
240 240
 	}
241 241
 
242
+	doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
243
+	for i, v := range conf.Defense.Doppelganger.URLs {
244
+		doppelGangerURLs[i] = v.String()
245
+	}
246
+
242 247
 	opts := mtglib.ProxyOpts{
243 248
 		Logger:          logger,
244 249
 		Network:         ntw,
@@ -256,6 +261,10 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
256 261
 
257 262
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
258 263
 		TolerateTimeSkewness:     conf.TolerateTimeSkewness.Value,
264
+
265
+		DoppelGangerURLs:    doppelGangerURLs,
266
+		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
267
+		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
259 268
 	}
260 269
 
261 270
 	proxy, err := mtglib.NewProxy(opts)

+ 7
- 2
internal/config/config.go Datei anzeigen

@@ -47,8 +47,13 @@ type Config struct {
47 47
 			MaxSize   TypeBytes     `json:"maxSize"`
48 48
 			ErrorRate TypeErrorRate `json:"errorRate"`
49 49
 		} `json:"antiReplay"`
50
-		Blocklist ListConfig `json:"blocklist"`
51
-		Allowlist ListConfig `json:"allowlist"`
50
+		Blocklist    ListConfig `json:"blocklist"`
51
+		Allowlist    ListConfig `json:"allowlist"`
52
+		Doppelganger struct {
53
+			URLs       []TypeHttpsURL  `json:"urls"`
54
+			Repeats    TypeConcurrency `json:"repeats_per_raid"`
55
+			UpdateEach TypeDuration    `json:"raid_each"`
56
+		} `json:"doppelganger"`
52 57
 	} `json:"defense"`
53 58
 	Network struct {
54 59
 		Timeout struct {

+ 5
- 0
internal/config/parse.go Datei anzeigen

@@ -44,6 +44,11 @@ type tomlConfig struct {
44 44
 			URLs                []string `toml:"urls" json:"urls,omitempty"`
45 45
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
46 46
 		} `toml:"allowlist" json:"allowlist,omitempty"`
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
+		} `toml:"doppelganger" json:"doppelganger,omitempty"`
47 52
 	} `toml:"defense" json:"defense,omitempty"`
48 53
 	Network struct {
49 54
 		Timeout struct {

+ 53
- 0
internal/config/type_https_url.go Datei anzeigen

@@ -0,0 +1,53 @@
1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net/url"
6
+)
7
+
8
+type TypeHttpsURL struct {
9
+	Value *url.URL
10
+}
11
+
12
+func (t *TypeHttpsURL) Set(value string) error {
13
+	parsedURL, err := url.Parse(value)
14
+	if err != nil {
15
+		return fmt.Errorf("value is not correct URL (%s): %w", value, err)
16
+	}
17
+
18
+	if parsedURL.Host == "" {
19
+		return fmt.Errorf("url has to have a schema: %s", value)
20
+	}
21
+
22
+	if parsedURL.Scheme != "https" {
23
+		return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
24
+	}
25
+
26
+	t.Value = parsedURL
27
+
28
+	return nil
29
+}
30
+
31
+func (t *TypeHttpsURL) Get(defaultValue *url.URL) *url.URL {
32
+	if t.Value == nil {
33
+		return defaultValue
34
+	}
35
+
36
+	return t.Value
37
+}
38
+
39
+func (t *TypeHttpsURL) UnmarshalText(data []byte) error {
40
+	return t.Set(string(data))
41
+}
42
+
43
+func (t TypeHttpsURL) MarshalText() ([]byte, error) {
44
+	return []byte(t.String()), nil
45
+}
46
+
47
+func (t TypeHttpsURL) String() string {
48
+	if t.Value == nil {
49
+		return ""
50
+	}
51
+
52
+	return t.Value.String()
53
+}

+ 100
- 0
internal/config/type_https_url_test.go Datei anzeigen

@@ -0,0 +1,100 @@
1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"net/url"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeHttpsURLTestStruct struct {
14
+	Value config.TypeHttpsURL `json:"value"`
15
+}
16
+
17
+type HttpsURLTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *HttpsURLTestSuite) TestUnmarshalFail() {
22
+	testData := []string{
23
+		"",
24
+		"https://",
25
+		"://lala",
26
+		"/path",
27
+		"http://example.com",
28
+		"socks5://example.com",
29
+	}
30
+
31
+	for _, v := range testData {
32
+		data, err := json.Marshal(map[string]string{
33
+			"value": v,
34
+		})
35
+		suite.NoError(err)
36
+
37
+		suite.T().Run(v, func(t *testing.T) {
38
+			assert.Error(t, json.Unmarshal(data, &typeHttpsURLTestStruct{}))
39
+		})
40
+	}
41
+}
42
+
43
+func (suite *HttpsURLTestSuite) TestUnmarshalOk() {
44
+	testData := map[string]string{
45
+		"https://example.com":           "https://example.com",
46
+		"https://example.com:8443":      "https://example.com:8443",
47
+		"https://example.com/path?q=1":  "https://example.com/path?q=1",
48
+		"https://user:pass@example.com": "https://user:pass@example.com",
49
+	}
50
+
51
+	for k, v := range testData {
52
+		value := v
53
+
54
+		data, err := json.Marshal(map[string]string{
55
+			"value": k,
56
+		})
57
+		suite.NoError(err)
58
+
59
+		suite.T().Run(k, func(t *testing.T) {
60
+			testStruct := &typeHttpsURLTestStruct{}
61
+			assert.NoError(t, json.Unmarshal(data, testStruct))
62
+
63
+			parsed, _ := url.Parse(value)
64
+
65
+			assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme)
66
+			assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host)
67
+			assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery)
68
+			assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path)
69
+		})
70
+	}
71
+}
72
+
73
+func (suite *HttpsURLTestSuite) TestMarshalOk() {
74
+	parsed, _ := url.Parse("https://example.com/path?q=1")
75
+	testStruct := &typeHttpsURLTestStruct{
76
+		Value: config.TypeHttpsURL{
77
+			Value: parsed,
78
+		},
79
+	}
80
+
81
+	encodedJSON, err := json.Marshal(testStruct)
82
+	suite.NoError(err)
83
+	suite.JSONEq(`{"value": "https://example.com/path?q=1"}`,
84
+		string(encodedJSON))
85
+}
86
+
87
+func (suite *HttpsURLTestSuite) TestGet() {
88
+	emptyURL := &url.URL{}
89
+
90
+	value := config.TypeHttpsURL{}
91
+	suite.Equal(emptyURL, value.Get(emptyURL))
92
+
93
+	value.Value = &url.URL{}
94
+	suite.Equal(value.Value, value.Get(emptyURL))
95
+}
96
+
97
+func TestTypeHttpsURL(t *testing.T) {
98
+	t.Parallel()
99
+	suite.Run(t, &HttpsURLTestSuite{})
100
+}

+ 7
- 0
mtglib/init.go Datei anzeigen

@@ -99,6 +99,13 @@ const (
99 99
 	// reads from Telegram after which connection will be terminated. This is
100 100
 	// required to abort stale connections.
101 101
 	TCPRelayReadTimeout = 20 * time.Second
102
+
103
+	// DoppelGangerPerRaid defines a number of requests to each URL
104
+	// per raid.
105
+	DoppelGangerPerRaid = 10
106
+
107
+	// DoppelGangerEach defines a time period between each crawl attempt.
108
+	DoppelGangerEach = 6 * time.Hour
102 109
 )
103 110
 
104 111
 // Network defines a knowledge how to work with a network. It may sound fun but

+ 35
- 0
mtglib/internal/doppel/clock.go Datei anzeigen

@@ -0,0 +1,35 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"time"
6
+)
7
+
8
+type Clock struct {
9
+	stats *Stats
10
+	tick  chan struct{}
11
+}
12
+
13
+func (c Clock) Start(ctx context.Context) {
14
+	tickTock := time.NewTimer(c.stats.Delay())
15
+	defer func() {
16
+		tickTock.Stop()
17
+		select {
18
+		case <-tickTock.C:
19
+		default:
20
+		}
21
+	}()
22
+
23
+	for {
24
+		select {
25
+		case <-ctx.Done():
26
+			return
27
+		case <-tickTock.C:
28
+			select {
29
+			case <-ctx.Done():
30
+			case c.tick <- struct{}{}:
31
+			}
32
+			tickTock.Reset(c.stats.Delay())
33
+		}
34
+	}
35
+}

+ 80
- 0
mtglib/internal/doppel/clock_test.go Datei anzeigen

@@ -0,0 +1,80 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"sync"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type ClockTestSuite struct {
13
+	suite.Suite
14
+
15
+	clock     Clock
16
+	wg        sync.WaitGroup
17
+	ctx       context.Context
18
+	ctxCancel context.CancelFunc
19
+}
20
+
21
+func (suite *ClockTestSuite) SetupTest() {
22
+	ctx, cancel := context.WithCancel(context.Background())
23
+
24
+	suite.ctx = ctx
25
+	suite.ctxCancel = cancel
26
+	suite.clock = Clock{
27
+		stats: &Stats{
28
+			k:      StatsDefaultK,
29
+			lambda: StatsDefaultLambda,
30
+		},
31
+		tick: make(chan struct{}),
32
+	}
33
+
34
+	suite.wg.Go(func() {
35
+		suite.clock.Start(suite.ctx)
36
+	})
37
+}
38
+
39
+func (suite *ClockTestSuite) TearDownTest() {
40
+	suite.ctxCancel()
41
+	suite.wg.Wait()
42
+}
43
+
44
+func (suite *ClockTestSuite) TestTicks() {
45
+	received := 0
46
+
47
+	for range 3 {
48
+		select {
49
+		case <-suite.clock.tick:
50
+			received++
51
+		case <-time.After(2 * time.Second):
52
+			suite.Fail("timed out waiting for tick")
53
+		}
54
+	}
55
+
56
+	suite.Equal(3, received)
57
+}
58
+
59
+func (suite *ClockTestSuite) TestStopsOnCancel() {
60
+	select {
61
+	case <-suite.clock.tick:
62
+	case <-time.After(2 * time.Second):
63
+		suite.Fail("timed out waiting for first tick")
64
+	}
65
+
66
+	suite.ctxCancel()
67
+
68
+	time.Sleep(50 * time.Millisecond)
69
+
70
+	select {
71
+	case <-suite.clock.tick:
72
+		suite.Fail("received tick after cancel")
73
+	default:
74
+	}
75
+}
76
+
77
+func TestClock(t *testing.T) {
78
+	t.Parallel()
79
+	suite.Run(t, &ClockTestSuite{})
80
+}

+ 95
- 0
mtglib/internal/doppel/conn.go Datei anzeigen

@@ -0,0 +1,95 @@
1
+package doppel
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"sync"
7
+
8
+	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
10
+)
11
+
12
+type Conn struct {
13
+	essentials.Conn
14
+
15
+	p *connPayload
16
+}
17
+
18
+type connPayload struct {
19
+	ctx         context.Context
20
+	ctxCancel   context.CancelCauseFunc
21
+	clock       Clock
22
+	wg          sync.WaitGroup
23
+	writeLock   sync.Mutex
24
+	writeStream bytes.Buffer
25
+}
26
+
27
+func (c Conn) Write(p []byte) (int, error) {
28
+	c.p.writeLock.Lock()
29
+	c.p.writeStream.Write(p)
30
+	c.p.writeLock.Unlock()
31
+
32
+	return len(p), context.Cause(c.p.ctx)
33
+}
34
+
35
+func (c Conn) Start() {
36
+	c.p.wg.Go(func() {
37
+		c.start()
38
+	})
39
+}
40
+
41
+func (c Conn) start() {
42
+	buf := [tls.MaxRecordSize]byte{}
43
+
44
+	for {
45
+		select {
46
+		case <-c.p.ctx.Done():
47
+			return
48
+		case <-c.p.clock.tick:
49
+		}
50
+
51
+		c.p.writeLock.Lock()
52
+		n, err := c.p.writeStream.Read(buf[:c.p.clock.stats.Size()])
53
+		c.p.writeLock.Unlock()
54
+
55
+		if n == 0 || err != nil {
56
+			continue
57
+		}
58
+
59
+		if err := tls.WriteRecord(c.Conn, buf[:n]); err != nil {
60
+			c.p.ctxCancel(err)
61
+			return
62
+		}
63
+	}
64
+}
65
+
66
+func (c Conn) Stop() {
67
+	c.p.ctxCancel(nil)
68
+	c.p.wg.Wait()
69
+}
70
+
71
+func NewConn(ctx context.Context, conn essentials.Conn, stats *Stats) Conn {
72
+	ctx, cancel := context.WithCancelCause(ctx)
73
+	rv := Conn{
74
+		Conn: conn,
75
+		p: &connPayload{
76
+			ctx:       ctx,
77
+			ctxCancel: cancel,
78
+			clock: Clock{
79
+				stats: stats,
80
+				tick:  make(chan struct{}),
81
+			},
82
+		},
83
+	}
84
+
85
+	rv.p.writeStream.Grow(tls.DefaultBufferSize)
86
+
87
+	rv.p.wg.Go(func() {
88
+		rv.p.clock.Start(ctx)
89
+	})
90
+	rv.p.wg.Go(func() {
91
+		rv.start()
92
+	})
93
+
94
+	return rv
95
+}

+ 163
- 0
mtglib/internal/doppel/conn_test.go Datei anzeigen

@@ -0,0 +1,163 @@
1
+package doppel
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/binary"
7
+	"errors"
8
+	"io"
9
+	"sync"
10
+	"testing"
11
+	"time"
12
+
13
+	"github.com/9seconds/mtg/v2/internal/testlib"
14
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
15
+	"github.com/stretchr/testify/mock"
16
+	"github.com/stretchr/testify/suite"
17
+)
18
+
19
+type ConnMock struct {
20
+	testlib.EssentialsConnMock
21
+
22
+	mu          sync.Mutex
23
+	writeBuffer bytes.Buffer
24
+}
25
+
26
+func (m *ConnMock) Write(p []byte) (int, error) {
27
+	args := m.Called(p)
28
+	if err := args.Error(1); err != nil {
29
+		return args.Int(0), err
30
+	}
31
+
32
+	m.mu.Lock()
33
+	defer m.mu.Unlock()
34
+
35
+	return m.writeBuffer.Write(p)
36
+}
37
+
38
+func (m *ConnMock) Written() []byte {
39
+	m.mu.Lock()
40
+	defer m.mu.Unlock()
41
+
42
+	return bytes.Clone(m.writeBuffer.Bytes())
43
+}
44
+
45
+type ConnTestSuite struct {
46
+	suite.Suite
47
+
48
+	connMock  *ConnMock
49
+	ctx       context.Context
50
+	ctxCancel context.CancelFunc
51
+}
52
+
53
+func (suite *ConnTestSuite) SetupTest() {
54
+	ctx, cancel := context.WithCancel(context.Background())
55
+	suite.ctx = ctx
56
+	suite.ctxCancel = cancel
57
+	suite.connMock = &ConnMock{}
58
+}
59
+
60
+func (suite *ConnTestSuite) TearDownTest() {
61
+	suite.ctxCancel()
62
+	suite.connMock.AssertExpectations(suite.T())
63
+}
64
+
65
+func (suite *ConnTestSuite) makeConn() Conn {
66
+	return NewConn(suite.ctx, suite.connMock, &Stats{
67
+		k:      2.0,
68
+		lambda: 0.01,
69
+	})
70
+}
71
+
72
+func (suite *ConnTestSuite) TestWriteBuffersData() {
73
+	suite.connMock.
74
+		On("Write", mock.AnythingOfType("[]uint8")).
75
+		Return(0, nil).
76
+		Maybe()
77
+
78
+	c := suite.makeConn()
79
+	defer c.Stop()
80
+
81
+	n, err := c.Write([]byte{1, 2, 3})
82
+	suite.NoError(err)
83
+	suite.Equal(3, n)
84
+}
85
+
86
+func (suite *ConnTestSuite) TestWriteOutputsTLSRecords() {
87
+	suite.connMock.
88
+		On("Write", mock.AnythingOfType("[]uint8")).
89
+		Return(0, nil).
90
+		Maybe()
91
+
92
+	c := suite.makeConn()
93
+
94
+	payload := []byte("hello doppelganger")
95
+	_, err := c.Write(payload)
96
+	suite.NoError(err)
97
+
98
+	suite.Eventually(func() bool {
99
+		return len(suite.connMock.Written()) > 0
100
+	}, 2*time.Second, time.Millisecond)
101
+
102
+	c.Stop()
103
+
104
+	assembled := &bytes.Buffer{}
105
+	reader := bytes.NewReader(suite.connMock.Written())
106
+
107
+	for {
108
+		header := make([]byte, tls.SizeHeader)
109
+		if _, err := io.ReadFull(reader, header); err != nil {
110
+			break
111
+		}
112
+
113
+		suite.Equal(byte(tls.TypeApplicationData), header[0])
114
+		suite.Equal(tls.TLSVersion[:], header[tls.SizeRecordType:tls.SizeRecordType+tls.SizeVersion])
115
+
116
+		length := binary.BigEndian.Uint16(header[tls.SizeRecordType+tls.SizeVersion:])
117
+		suite.Greater(length, uint16(0))
118
+
119
+		rec := make([]byte, length)
120
+		_, err := io.ReadFull(reader, rec)
121
+		suite.NoError(err)
122
+
123
+		assembled.Write(rec)
124
+	}
125
+
126
+	suite.Equal(payload, assembled.Bytes())
127
+}
128
+
129
+func (suite *ConnTestSuite) TestWriteReturnsErrorAfterStop() {
130
+	suite.connMock.
131
+		On("Write", mock.AnythingOfType("[]uint8")).
132
+		Return(0, nil).
133
+		Maybe()
134
+
135
+	c := suite.makeConn()
136
+	c.Stop()
137
+
138
+	time.Sleep(10 * time.Millisecond)
139
+
140
+	_, err := c.Write([]byte{1})
141
+	suite.Error(err)
142
+}
143
+
144
+func (suite *ConnTestSuite) TestStopOnUnderlyingWriteError() {
145
+	suite.connMock.
146
+		On("Write", mock.AnythingOfType("[]uint8")).
147
+		Return(0, errors.New("connection reset")).
148
+		Maybe()
149
+
150
+	c := suite.makeConn()
151
+
152
+	_, _ = c.Write([]byte("data"))
153
+
154
+	suite.Eventually(func() bool {
155
+		_, err := c.Write([]byte{1})
156
+		return err != nil
157
+	}, 2*time.Second, time.Millisecond)
158
+}
159
+
160
+func TestConn(t *testing.T) {
161
+	t.Parallel()
162
+	suite.Run(t, &ConnTestSuite{})
163
+}

+ 179
- 0
mtglib/internal/doppel/ganger.go Datei anzeigen

@@ -0,0 +1,179 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"sync"
6
+	"time"
7
+
8
+	"github.com/9seconds/mtg/v2/essentials"
9
+)
10
+
11
+const (
12
+	DoppelGangerMaxDurations  = 4096
13
+	DoppelGangerScoutRaidEach = 6 * time.Hour
14
+	DoppelGangerScoutRepeats  = 10
15
+)
16
+
17
+type gangerConnRequest struct {
18
+	ret     chan<- Conn
19
+	payload essentials.Conn
20
+}
21
+
22
+type Ganger struct {
23
+	ctx       context.Context
24
+	ctxCancel context.CancelFunc
25
+	logger    Logger
26
+	wg        sync.WaitGroup
27
+
28
+	scout            Scout
29
+	scoutRaidEach    time.Duration
30
+	scoutRaidRepeats int
31
+
32
+	stats     *Stats
33
+	durations []time.Duration
34
+
35
+	connRequests chan gangerConnRequest
36
+}
37
+
38
+func (g *Ganger) Shutdown() {
39
+	g.ctxCancel()
40
+	g.wg.Wait()
41
+}
42
+
43
+func (g *Ganger) Run() {
44
+	g.wg.Go(func() {
45
+		g.run()
46
+	})
47
+}
48
+
49
+func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
50
+	rvChan := make(chan Conn)
51
+	req := gangerConnRequest{
52
+		ret:     rvChan,
53
+		payload: conn,
54
+	}
55
+	defer close(req.ret)
56
+
57
+	select {
58
+	case <-g.ctx.Done():
59
+		return Conn{}, context.Cause(g.ctx)
60
+	case g.connRequests <- req:
61
+	}
62
+
63
+	select {
64
+	case <-g.ctx.Done():
65
+		return Conn{}, context.Cause(g.ctx)
66
+	case conn := <-rvChan:
67
+		return conn, nil
68
+	}
69
+}
70
+
71
+func (g *Ganger) run() {
72
+	scoutTicker := time.NewTicker(g.scoutRaidEach)
73
+	defer func() {
74
+		scoutTicker.Stop()
75
+
76
+		select {
77
+		case <-scoutTicker.C:
78
+		default:
79
+		}
80
+	}()
81
+
82
+	scoutCollectedChan := make(chan []time.Duration)
83
+	currentScoutCollectedChan := scoutCollectedChan
84
+
85
+	updatedStatsChan := make(chan *Stats)
86
+
87
+	g.wg.Go(func() {
88
+		g.runScoutRaid(scoutCollectedChan)
89
+	})
90
+
91
+	for {
92
+		select {
93
+		case <-g.ctx.Done():
94
+			return
95
+		case durations := <-currentScoutCollectedChan:
96
+			g.durations = append(g.durations, durations...)
97
+
98
+			if len(g.durations) > DoppelGangerMaxDurations {
99
+				g.durations = g.durations[len(g.durations)-DoppelGangerMaxDurations:]
100
+			}
101
+
102
+			if len(g.durations) < MinDurationsToCalculate {
103
+				continue
104
+			}
105
+
106
+			currentScoutCollectedChan = nil
107
+			g.wg.Go(func() {
108
+				select {
109
+				case <-g.ctx.Done():
110
+				case updatedStatsChan <- NewStats(durations):
111
+				}
112
+			})
113
+		case stats := <-updatedStatsChan:
114
+			g.stats = stats
115
+			currentScoutCollectedChan = scoutCollectedChan
116
+		case <-scoutTicker.C:
117
+			g.wg.Go(func() {
118
+				g.runScoutRaid(scoutCollectedChan)
119
+			})
120
+		case req := <-g.connRequests:
121
+			select {
122
+			case <-g.ctx.Done():
123
+			case req.ret <- NewConn(g.ctx, req.payload, g.stats):
124
+			}
125
+		}
126
+	}
127
+}
128
+
129
+func (g *Ganger) runScoutRaid(rvChan chan<- []time.Duration) {
130
+	durations := []time.Duration{}
131
+
132
+	for range g.scoutRaidRepeats {
133
+		learned, err := g.scout.Learn(g.ctx)
134
+		if err != nil {
135
+			g.logger.WarningError("cannot learn", err)
136
+			continue
137
+		}
138
+		durations = append(durations, learned...)
139
+	}
140
+
141
+	select {
142
+	case <-g.ctx.Done():
143
+		return
144
+	case rvChan <- durations:
145
+	}
146
+}
147
+
148
+func NewGanger(
149
+	ctx context.Context,
150
+	network Network,
151
+	logger Logger,
152
+	scoutEach time.Duration,
153
+	scoutRepeats int,
154
+	urls []string,
155
+) *Ganger {
156
+	ctx, cancel := context.WithCancel(ctx)
157
+
158
+	if scoutEach == 0 {
159
+		scoutEach = DoppelGangerScoutRaidEach
160
+	}
161
+
162
+	if scoutRepeats == 0 {
163
+		scoutRepeats = DoppelGangerScoutRepeats
164
+	}
165
+
166
+	return &Ganger{
167
+		ctx:              ctx,
168
+		ctxCancel:        cancel,
169
+		logger:           logger,
170
+		scoutRaidEach:    scoutEach,
171
+		scoutRaidRepeats: scoutRepeats,
172
+		stats: &Stats{
173
+			k:      StatsDefaultK,
174
+			lambda: StatsDefaultLambda,
175
+		},
176
+		scout:        NewScout(network, urls),
177
+		connRequests: make(chan gangerConnRequest),
178
+	}
179
+}

+ 107
- 0
mtglib/internal/doppel/ganger_test.go Datei anzeigen

@@ -0,0 +1,107 @@
1
+package doppel
2
+
3
+import (
4
+	"bytes"
5
+	"sync"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/9seconds/mtg/v2/internal/testlib"
10
+	"github.com/stretchr/testify/mock"
11
+	"github.com/stretchr/testify/suite"
12
+)
13
+
14
+type GangerTestSuite struct {
15
+	TLSServerTestSuite
16
+
17
+	log *LoggerMock
18
+	g   *Ganger
19
+}
20
+
21
+func (suite *GangerTestSuite) SetupTest() {
22
+	suite.TLSServerTestSuite.SetupTest()
23
+
24
+	suite.log = &LoggerMock{}
25
+	suite.log.
26
+		On("Info", mock.AnythingOfType("string")).
27
+		Maybe()
28
+	suite.log.
29
+		On("WarningError", mock.AnythingOfType("string"), mock.Anything).
30
+		Maybe()
31
+
32
+	suite.g = NewGanger(suite.ctx, suite.network, suite.log, time.Hour, 1, suite.urls)
33
+	suite.g.Run()
34
+}
35
+
36
+func (suite *GangerTestSuite) TearDownTest() {
37
+	suite.g.Shutdown()
38
+
39
+	suite.log.AssertExpectations(suite.T())
40
+	suite.TLSServerTestSuite.TearDownTest()
41
+}
42
+
43
+func (suite *GangerTestSuite) TestNewConnAfterShutdown() {
44
+	suite.g.Shutdown()
45
+	connMock := &testlib.EssentialsConnMock{}
46
+
47
+	_, err := suite.g.NewConn(connMock)
48
+	suite.Error(err)
49
+}
50
+
51
+func (suite *GangerTestSuite) TestNewConnWhileRunning() {
52
+	connMock := &testlib.EssentialsConnMock{}
53
+	connMock.
54
+		On("Write", mock.AnythingOfType("[]uint8")).
55
+		Return(0, nil).
56
+		Maybe()
57
+	connMock.On("Close").
58
+		Return(nil).
59
+		Maybe()
60
+
61
+	conn, err := suite.g.NewConn(connMock)
62
+	suite.NoError(err)
63
+
64
+	conn.Stop()
65
+}
66
+
67
+func (suite *GangerTestSuite) TestNewConnWriteProducesTLSRecords() {
68
+	var (
69
+		mu  sync.Mutex
70
+		buf bytes.Buffer
71
+	)
72
+
73
+	connMock := &testlib.EssentialsConnMock{}
74
+	connMock.On("Write", mock.AnythingOfType("[]uint8")).
75
+		Run(func(args mock.Arguments) {
76
+			mu.Lock()
77
+			buf.Write(args.Get(0).([]byte))
78
+			mu.Unlock()
79
+		}).
80
+		Return(0, nil).
81
+		Maybe()
82
+	connMock.On("Close").
83
+		Return(nil).
84
+		Maybe()
85
+
86
+	conn, err := suite.g.NewConn(connMock)
87
+	suite.NoError(err)
88
+
89
+	payload := bytes.Repeat([]byte("x"), 512)
90
+	_, err = conn.Write(payload)
91
+	suite.NoError(err)
92
+
93
+	time.Sleep(500 * time.Millisecond)
94
+	conn.Stop()
95
+
96
+	mu.Lock()
97
+	written := buf.Bytes()
98
+	mu.Unlock()
99
+
100
+	suite.NotEmpty(written)
101
+}
102
+
103
+func TestGanger(t *testing.T) {
104
+	t.Parallel()
105
+
106
+	suite.Run(t, &GangerTestSuite{})
107
+}

+ 38
- 0
mtglib/internal/doppel/init.go Datei anzeigen

@@ -0,0 +1,38 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"net/http"
6
+	"time"
7
+
8
+	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
10
+)
11
+
12
+const (
13
+	// Please see Stats description
14
+	// https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
15
+	// https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
16
+	TLSRecordSizeStart = 1369
17
+	TLSRecordSizeAccel = 4229
18
+	TLSRecordSizeMax   = 16384 - tls.SizeHeader
19
+
20
+	TLSCounterAccelAfter = 40
21
+	TLSCounterMaxAfter   = TLSCounterAccelAfter + 20
22
+
23
+	TLSRecordSizeResetAfter = time.Second
24
+)
25
+
26
+// copypasted from mtglib
27
+type Network interface {
28
+	// Dial establishes context-free TCP connections.
29
+	Dial(network, address string) (essentials.Conn, error)
30
+
31
+	// DialContext dials using a context. This is a preferrable way of
32
+	// establishing TCP connections.
33
+	DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
34
+
35
+	// MakeHTTPClient build an HTTP client with given dial function. If nothing is
36
+	// provided, then DialContext of this interface is going to be used.
37
+	MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
38
+}

+ 103
- 0
mtglib/internal/doppel/init_test.go Datei anzeigen

@@ -0,0 +1,103 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"crypto/tls"
6
+	"net"
7
+	"net/http"
8
+	"net/http/httptest"
9
+	"time"
10
+
11
+	"github.com/9seconds/mtg/v2/essentials"
12
+	"github.com/stretchr/testify/mock"
13
+	"github.com/stretchr/testify/suite"
14
+)
15
+
16
+type SimpleNetwork struct{}
17
+
18
+func (s SimpleNetwork) Dial(network, address string) (essentials.Conn, error) {
19
+	return s.DialContext(context.Background(), network, address)
20
+}
21
+
22
+func (s SimpleNetwork) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
23
+	d := &net.Dialer{}
24
+
25
+	conn, err := d.DialContext(ctx, network, address)
26
+	if err != nil {
27
+		return nil, err
28
+	}
29
+
30
+	return conn.(*net.TCPConn), nil
31
+}
32
+
33
+func (s SimpleNetwork) MakeHTTPClient(dialFunc func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client {
34
+	if dialFunc == nil {
35
+		dialFunc = s.DialContext
36
+	}
37
+
38
+	return &http.Client{
39
+		Transport: &http.Transport{
40
+			TLSClientConfig: &tls.Config{
41
+				InsecureSkipVerify: true, //nolint: gosec
42
+			},
43
+			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
44
+				return dialFunc(ctx, network, address)
45
+			},
46
+		},
47
+	}
48
+}
49
+
50
+type TLSServerTestSuite struct {
51
+	suite.Suite
52
+
53
+	tlsServer *httptest.Server
54
+	ctx       context.Context
55
+	ctxCancel context.CancelFunc
56
+	network   SimpleNetwork
57
+	urls      []string
58
+}
59
+
60
+func (suite *TLSServerTestSuite) SetupSuite() {
61
+	suite.tlsServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
62
+		w.WriteHeader(http.StatusOK)
63
+		w.Header().Add("Hello", "how long")
64
+
65
+		if _, err := w.Write([]byte{1, 2, 3}); err != nil {
66
+			panic(err)
67
+		}
68
+
69
+		time.Sleep(5 * time.Millisecond)
70
+
71
+		if _, err := w.Write([]byte{1, 2, 3}); err != nil {
72
+			panic(err)
73
+		}
74
+	}))
75
+	suite.urls = []string{suite.tlsServer.URL}
76
+}
77
+
78
+func (suite *TLSServerTestSuite) SetupTest() {
79
+	ctx, cancel := context.WithCancel(context.Background())
80
+	suite.ctx = ctx
81
+	suite.ctxCancel = cancel
82
+}
83
+
84
+func (suite *TLSServerTestSuite) TearDownTest() {
85
+	suite.ctxCancel()
86
+	suite.tlsServer.CloseClientConnections()
87
+}
88
+
89
+func (suite *TLSServerTestSuite) TearDownSuite() {
90
+	suite.tlsServer.Close()
91
+}
92
+
93
+type LoggerMock struct {
94
+	mock.Mock
95
+}
96
+
97
+func (l *LoggerMock) Info(msg string) {
98
+	l.Called(msg)
99
+}
100
+
101
+func (l *LoggerMock) WarningError(msg string, err error) {
102
+	l.Called(msg, err)
103
+}

+ 6
- 0
mtglib/internal/doppel/logger.go Datei anzeigen

@@ -0,0 +1,6 @@
1
+package doppel
2
+
3
+type Logger interface {
4
+	Info(msg string)
5
+	WarningError(msg string, err error)
6
+}

+ 104
- 0
mtglib/internal/doppel/scout.go Datei anzeigen

@@ -0,0 +1,104 @@
1
+package doppel
2
+
3
+import (
4
+	"context"
5
+	"fmt"
6
+	"io"
7
+	"net/http"
8
+	"strings"
9
+	"time"
10
+
11
+	"github.com/9seconds/mtg/v2/essentials"
12
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
13
+)
14
+
15
+type Scout struct {
16
+	network Network
17
+	urls    []string
18
+}
19
+
20
+func (s Scout) Learn(ctx context.Context) ([]time.Duration, error) {
21
+	var durations []time.Duration
22
+
23
+	for _, url := range s.urls {
24
+		learned, err := s.learn(ctx, url)
25
+		if err != nil {
26
+			return nil, err
27
+		}
28
+
29
+		durations = append(durations, learned...)
30
+	}
31
+
32
+	return durations, nil
33
+}
34
+
35
+func (s Scout) learn(ctx context.Context, url string) ([]time.Duration, error) {
36
+	client, results := s.makeClient()
37
+
38
+	if !strings.HasPrefix(url, "https://") {
39
+		return nil, fmt.Errorf("url %s must be https", url)
40
+	}
41
+
42
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
43
+	if err != nil {
44
+		return nil, err
45
+	}
46
+
47
+	resp, err := client.Do(req)
48
+	if resp != nil {
49
+		io.Copy(io.Discard, resp.Body) //nolint: errcheck
50
+		resp.Body.Close()              //nolint: errcheck
51
+		client.CloseIdleConnections()
52
+	}
53
+
54
+	if err != nil || len(results.data) == 0 {
55
+		return nil, err
56
+	}
57
+
58
+	durations := []time.Duration{}
59
+	lastTimestamp := time.Time{}
60
+
61
+	for i, v := range results.data {
62
+		if v.recordType != tls.TypeApplicationData {
63
+			continue
64
+		}
65
+
66
+		if lastTimestamp.IsZero() {
67
+			if i > 0 {
68
+				lastTimestamp = results.data[i-1].timestamp
69
+			} else {
70
+				lastTimestamp = v.timestamp
71
+			}
72
+		}
73
+
74
+		durations = append(durations, v.timestamp.Sub(lastTimestamp))
75
+		lastTimestamp = v.timestamp
76
+	}
77
+
78
+	return durations, nil
79
+}
80
+
81
+func (s Scout) makeClient() (*http.Client, *ScoutConnCollected) {
82
+	collected := NewScoutConnCollected()
83
+	client := s.network.MakeHTTPClient(func(
84
+		ctx context.Context,
85
+		network string,
86
+		address string,
87
+	) (essentials.Conn, error) {
88
+		conn, err := s.network.DialContext(ctx, network, address)
89
+		if err != nil {
90
+			return nil, err
91
+		}
92
+
93
+		return NewScoutConn(conn, collected), nil
94
+	})
95
+
96
+	return client, collected
97
+}
98
+
99
+func NewScout(network Network, urls []string) Scout {
100
+	return Scout{
101
+		network: network,
102
+		urls:    urls,
103
+	}
104
+}

+ 57
- 0
mtglib/internal/doppel/scout_conn.go Datei anzeigen

@@ -0,0 +1,57 @@
1
+package doppel
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/binary"
6
+	"io"
7
+
8
+	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
10
+)
11
+
12
+type ScoutConn struct {
13
+	tls.Conn
14
+
15
+	results *ScoutConnCollected
16
+	rawBuf  *bytes.Buffer
17
+}
18
+
19
+func (s ScoutConn) Read(p []byte) (int, error) {
20
+	buf := &bytes.Buffer{}
21
+
22
+	for {
23
+		if n, err := s.rawBuf.Read(p); err == nil {
24
+			return n, nil
25
+		}
26
+
27
+		s.rawBuf.Reset()
28
+
29
+		recordType, length, err := tls.ReadRecord(s.Conn, buf)
30
+		if err != nil {
31
+			return 0, err
32
+		}
33
+
34
+		s.results.Add(recordType)
35
+		s.rawBuf.Write([]byte{recordType})
36
+		s.rawBuf.Write(tls.TLSVersion[:])
37
+
38
+		if err := binary.Write(s.rawBuf, binary.BigEndian, uint16(length)); err != nil {
39
+			return 0, err
40
+		}
41
+
42
+		if _, err := io.Copy(s.rawBuf, buf); err != nil {
43
+			return 0, err
44
+		}
45
+	}
46
+}
47
+
48
+func NewScoutConn(conn essentials.Conn, results *ScoutConnCollected) ScoutConn {
49
+	rawBuf := &bytes.Buffer{}
50
+	rawBuf.Grow(tls.MaxRecordSize)
51
+
52
+	return ScoutConn{
53
+		Conn:    tls.New(conn, false, false),
54
+		results: results,
55
+		rawBuf:  rawBuf,
56
+	}
57
+}

+ 29
- 0
mtglib/internal/doppel/scout_conn_collected.go Datei anzeigen

@@ -0,0 +1,29 @@
1
+package doppel
2
+
3
+import "time"
4
+
5
+const (
6
+	ScoutConnCollectedPreallocSize = 100
7
+)
8
+
9
+type ScoutConnResult struct {
10
+	timestamp  time.Time
11
+	recordType byte
12
+}
13
+
14
+type ScoutConnCollected struct {
15
+	data []ScoutConnResult
16
+}
17
+
18
+func (s *ScoutConnCollected) Add(record byte) {
19
+	s.data = append(s.data, ScoutConnResult{
20
+		timestamp:  time.Now(),
21
+		recordType: record,
22
+	})
23
+}
24
+
25
+func NewScoutConnCollected() *ScoutConnCollected {
26
+	return &ScoutConnCollected{
27
+		data: make([]ScoutConnResult, 0, ScoutConnCollectedPreallocSize),
28
+	}
29
+}

+ 42
- 0
mtglib/internal/doppel/scout_conn_collected_test.go Datei anzeigen

@@ -0,0 +1,42 @@
1
+package doppel
2
+
3
+import (
4
+	"testing"
5
+	"time"
6
+
7
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
8
+	"github.com/stretchr/testify/suite"
9
+)
10
+
11
+type ScoutConnCollectedTestSuite struct {
12
+	suite.Suite
13
+}
14
+
15
+func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
16
+	collected := NewScoutConnCollected()
17
+	collected.Add(tls.TypeApplicationData)
18
+
19
+	suite.Len(collected.data, 1)
20
+	suite.Equal(byte(tls.TypeApplicationData), collected.data[0].recordType)
21
+}
22
+
23
+func (suite *ScoutConnCollectedTestSuite) TestAddTimestampsAreMonotonic() {
24
+	collected := NewScoutConnCollected()
25
+
26
+	collected.Add(tls.TypeApplicationData)
27
+
28
+	time.Sleep(time.Microsecond)
29
+	collected.Add(tls.TypeApplicationData)
30
+
31
+	time.Sleep(time.Microsecond)
32
+	collected.Add(tls.TypeApplicationData)
33
+
34
+	for i := 1; i < len(collected.data); i++ {
35
+		suite.True(collected.data[i].timestamp.After(collected.data[i-1].timestamp))
36
+	}
37
+}
38
+
39
+func TestScoutConnCollected(t *testing.T) {
40
+	t.Parallel()
41
+	suite.Run(t, &ScoutConnCollectedTestSuite{})
42
+}

+ 39
- 0
mtglib/internal/doppel/scout_test.go Datei anzeigen

@@ -0,0 +1,39 @@
1
+package doppel
2
+
3
+import (
4
+	"testing"
5
+
6
+	"github.com/stretchr/testify/suite"
7
+)
8
+
9
+type ScoutTestSuite struct {
10
+	TLSServerTestSuite
11
+
12
+	scout Scout
13
+}
14
+
15
+func (suite *ScoutTestSuite) SetupSuite() {
16
+	suite.TLSServerTestSuite.SetupSuite()
17
+
18
+	suite.scout = Scout{
19
+		network: suite.network,
20
+		urls:    suite.urls,
21
+	}
22
+}
23
+
24
+func (suite *ScoutTestSuite) TestCollectResults() {
25
+	durations, err := suite.scout.Learn(suite.ctx)
26
+	suite.NoError(err)
27
+	suite.Less(3, len(durations))
28
+}
29
+
30
+func (suite *ScoutTestSuite) TestCollectNothing() {
31
+	suite.ctxCancel()
32
+
33
+	_, err := suite.scout.Learn(suite.ctx)
34
+	suite.Error(err)
35
+}
36
+
37
+func TestScout(t *testing.T) {
38
+	suite.Run(t, &ScoutTestSuite{})
39
+}

+ 154
- 0
mtglib/internal/doppel/stats.go Datei anzeigen

@@ -0,0 +1,154 @@
1
+package doppel
2
+
3
+import (
4
+	"math"
5
+	"math/rand/v2"
6
+	"time"
7
+)
8
+
9
+const (
10
+	StatsBisectTimes = 70
11
+	StatsLowK        = 0.01
12
+	StatsHighK       = 10.0
13
+
14
+	// do not calculate statistics if we have < than this number of durations
15
+	MinDurationsToCalculate = 100
16
+
17
+	// these values are taken from ok-cdn. measured from moscow site.
18
+	StatsDefaultK      = 1.2908978568647322
19
+	StatsDefaultLambda = 1.4258402622793287
20
+)
21
+
22
+// Stats is responsible for generating values that are distributed according
23
+// to some statistical distribution.
24
+//
25
+// It follows several ideas:
26
+//  1. Based on nginx and Cloudflare behaviour, even if server is eager
27
+//     to send a lot, they all start with small TLS packets that are
28
+//     approximately MTU-sized. After
29
+//  2. After ~40 TLS records, server considers TCP session as somewhat solid
30
+//     and reliable and ramps up to 4096.
31
+//  3. After ~20 TLS records more it jumps to the max 16384 bytes and keep
32
+//     this size as long as it can
33
+//  4. If there is no any byte within a connection for a longer time period,
34
+//     this counter resets.
35
+//
36
+// This is called Dynamic TLS Record Sizing
37
+//   - https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
38
+//   - https://community.f5.com/kb/technicalarticles/boosting-tls-performance-with-dynamic-record-sizing-on-big-ip/280798
39
+//   - https://www.igvita.com/2013/10/24/optimizing-tls-record-size-and-buffering-latency/
40
+//
41
+// And this optimized for the very first byte, so web browsers could start to
42
+// render as early as possible, showing user some preliminary results, optimizing
43
+// for perceived latency.
44
+//
45
+// Since this is very typical for the website, we also aim for that.
46
+//
47
+// Another important idea is how delays between TLS packets are distributed.
48
+// In case of sending huge heavy content with max sized record, delays have
49
+// lognormal distribution. But a nature of a typical website shows that
50
+// it eagers to deliver as fast as it can in a few very first records and
51
+// could possibly slow down later.
52
+//
53
+// This is perfectly described by Weibull distribution:
54
+//   - https://en.wikipedia.org/wiki/Weibull_distribution
55
+//   - https://ieeexplore.ieee.org/document/6662948
56
+//   - https://www.researchgate.net/publication/224621285_Traffic_modelling_and_cost_optimization_for_transmitting_traffic_messages_over_a_hybrid_broadcast_and_cellular_network
57
+//   - https://ir.uitm.edu.my/id/eprint/105386/1/105386.pdf
58
+//
59
+// In other word, a combination of Dynamic TLS Record Sizing hints us for
60
+// Weibull distribution.
61
+type Stats struct {
62
+	sizeLastRequested time.Time
63
+	sizeCounter       int
64
+
65
+	// https://en.wikipedia.org/wiki/Shape_parameter
66
+	k float64
67
+	// https://en.wikipedia.org/wiki/Scale_parameter
68
+	lambda float64
69
+}
70
+
71
+func (d *Stats) Delay() time.Duration {
72
+	// u ∈ (0, 1], avoids ln(0)
73
+	u := 1.0 - rand.Float64()
74
+
75
+	// X = λ·(-ln U)^(1/k)
76
+	generated := d.lambda * math.Pow(-math.Log(u), 1.0/d.k)
77
+
78
+	// generated is in milliseconds
79
+	return time.Duration(generated * float64(time.Millisecond))
80
+}
81
+
82
+func (d *Stats) Size() int {
83
+	if time.Since(d.sizeLastRequested) > TLSRecordSizeResetAfter {
84
+		d.sizeCounter = 0
85
+	}
86
+
87
+	d.sizeLastRequested = time.Now()
88
+	d.sizeCounter++
89
+
90
+	switch {
91
+	case d.sizeCounter <= TLSCounterAccelAfter:
92
+		return TLSRecordSizeStart
93
+	case d.sizeCounter <= TLSCounterMaxAfter:
94
+		return TLSRecordSizeAccel
95
+	}
96
+
97
+	return TLSRecordSizeMax
98
+}
99
+
100
+func NewStats(durations []time.Duration) *Stats {
101
+	n := float64(len(durations))
102
+
103
+	// in milliseconds
104
+	durFloats := make([]float64, len(durations))
105
+	for i, v := range durations {
106
+		durFloats[i] = float64(v.Microseconds()) / 1000.0
107
+	}
108
+
109
+	// The bisection solves the standard Weibull MLE equation for shape
110
+	// parameter k. There is no any good formula for doing that so we
111
+	// approximate it by several bisections. The number of operations
112
+	// is statically defined by a constant.
113
+
114
+	sumLog := 0.0
115
+	for _, v := range durFloats {
116
+		sumLog += math.Log(v)
117
+	}
118
+
119
+	lowK := StatsLowK
120
+	highK := StatsHighK
121
+
122
+	for range StatsBisectTimes {
123
+		midK := (lowK + highK) / 2.0
124
+		sumXK := 0.0
125
+		sumXKLog := 0.0
126
+
127
+		for _, v := range durFloats {
128
+			xk := math.Pow(v, midK)
129
+			sumXK += xk
130
+			sumXKLog += xk * math.Log(v)
131
+		}
132
+
133
+		if (1.0/midK)+(sumLog/n)-(sumXKLog/sumXK) > 0 {
134
+			lowK = midK
135
+		} else {
136
+			highK = midK
137
+		}
138
+	}
139
+
140
+	k := (lowK + highK) / 2
141
+
142
+	sumXK := 0.0
143
+	for _, v := range durFloats {
144
+		sumXK += math.Pow(v, k)
145
+	}
146
+
147
+	// λ = (Σxᵢᵏ / n)^(1/k)
148
+	lambda := math.Pow(sumXK/n, 1.0/k)
149
+
150
+	return &Stats{
151
+		k:      k,
152
+		lambda: lambda,
153
+	}
154
+}

+ 194
- 0
mtglib/internal/doppel/stats_test.go Datei anzeigen

@@ -0,0 +1,194 @@
1
+package doppel
2
+
3
+import (
4
+	"math"
5
+	"math/rand/v2"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type StatsTestSuite struct {
13
+	suite.Suite
14
+}
15
+
16
+func (suite *StatsTestSuite) GenWeibull(k, lambda float64, n int, seed uint64) []time.Duration {
17
+	rng := rand.New(rand.NewPCG(seed, 0))
18
+	samples := make([]time.Duration, n)
19
+
20
+	for i := range samples {
21
+		u := 1.0 - rng.Float64()
22
+		ms := lambda * math.Pow(-math.Log(u), 1.0/k)
23
+		d := time.Duration(ms * float64(time.Millisecond))
24
+
25
+		if d < time.Microsecond {
26
+			time.Sleep(time.Microsecond)
27
+			d = time.Microsecond
28
+		}
29
+
30
+		samples[i] = d
31
+	}
32
+
33
+	return samples
34
+}
35
+
36
+func (suite *StatsTestSuite) TestNewStatsRecoverParameters() {
37
+	knownK := 1.5
38
+	knownLambda := 100.0
39
+
40
+	samples := suite.GenWeibull(knownK, knownLambda, 5000, 42)
41
+	stats := NewStats(samples)
42
+
43
+	suite.InDelta(knownK, stats.k, 0.1)
44
+	suite.InDelta(knownLambda, stats.lambda, 5.0)
45
+}
46
+
47
+func (suite *StatsTestSuite) TestNewStatsExponentialCase() {
48
+	// When k=1, Weibull reduces to exponential distribution.
49
+	knownK := 1.0
50
+	knownLambda := 50.0
51
+
52
+	samples := suite.GenWeibull(knownK, knownLambda, 5000, 123)
53
+	stats := NewStats(samples)
54
+
55
+	suite.InDelta(knownK, stats.k, 0.1)
56
+	suite.InDelta(knownLambda, stats.lambda, 5.0)
57
+}
58
+
59
+func (suite *StatsTestSuite) TestNewStatsSmallK() {
60
+	// k < 1 produces a heavy-tailed distribution typical for network delays.
61
+	// Lambda must be large enough so samples stay above microsecond precision
62
+	// after time.Duration round-trip.
63
+	knownK := 0.6
64
+	knownLambda := 100.0
65
+
66
+	samples := suite.GenWeibull(knownK, knownLambda, 10000, 99)
67
+	stats := NewStats(samples)
68
+
69
+	suite.InDelta(knownK, stats.k, 0.05)
70
+	suite.InDelta(knownLambda, stats.lambda, 5.0)
71
+}
72
+
73
+func (suite *StatsTestSuite) TestNewStatsLargeK() {
74
+	// k > 1: light tail, concentrated around the mode.
75
+	knownK := 5.0
76
+	knownLambda := 200.0
77
+
78
+	samples := suite.GenWeibull(knownK, knownLambda, 5000, 77)
79
+	stats := NewStats(samples)
80
+
81
+	suite.InDelta(knownK, stats.k, 0.3)
82
+	suite.InDelta(knownLambda, stats.lambda, 5.0)
83
+}
84
+
85
+func (suite *StatsTestSuite) TestDelayNonNegative() {
86
+	stats := &Stats{
87
+		k:      1.5,
88
+		lambda: 100.0,
89
+	}
90
+
91
+	for range 200 {
92
+		dur := stats.Delay()
93
+		suite.GreaterOrEqual(dur, time.Duration(0))
94
+	}
95
+}
96
+
97
+func (suite *StatsTestSuite) TestDelayDistributionMean() {
98
+	// Weibull mean = λ · Γ(1 + 1/k)
99
+	k := 2.0
100
+	lambda := 50.0
101
+	stats := &Stats{k: k, lambda: lambda}
102
+
103
+	n := 50000
104
+	sum := 0.0
105
+
106
+	for range n {
107
+		dur := stats.Delay()
108
+		sum += float64(dur) / float64(time.Millisecond)
109
+	}
110
+
111
+	sampleMean := sum / float64(n)
112
+	expectedMean := lambda * math.Gamma(1.0+1.0/k)
113
+
114
+	suite.InDelta(expectedMean, sampleMean, expectedMean*0.05)
115
+}
116
+
117
+func (suite *StatsTestSuite) TestNewStatsRoundTrip() {
118
+	// Estimate parameters from data, then verify that Delay samples
119
+	// from the fitted distribution have approximately the same mean.
120
+	knownK := 1.2
121
+	knownLambda := 80.0
122
+
123
+	samples := suite.GenWeibull(knownK, knownLambda, 5000, 555)
124
+	stats := NewStats(samples)
125
+
126
+	n := 50000
127
+	sum := 0.0
128
+
129
+	for range n {
130
+		dur := stats.Delay()
131
+		sum += float64(dur) / float64(time.Millisecond)
132
+	}
133
+
134
+	sampleMean := sum / float64(n)
135
+	expectedMean := knownLambda * math.Gamma(1.0+1.0/knownK)
136
+
137
+	suite.InDelta(expectedMean, sampleMean, expectedMean*0.05)
138
+}
139
+
140
+func (suite *StatsTestSuite) TestSizeStartPhase() {
141
+	stats := &Stats{k: 1.0, lambda: 1.0}
142
+
143
+	for range TLSCounterAccelAfter {
144
+		size := stats.Size()
145
+		suite.Equal(TLSRecordSizeStart, size)
146
+	}
147
+}
148
+
149
+func (suite *StatsTestSuite) TestSizeAccelPhase() {
150
+	stats := &Stats{k: 1.0, lambda: 1.0}
151
+
152
+	for range TLSCounterAccelAfter {
153
+		stats.Size()
154
+	}
155
+
156
+	for range TLSCounterMaxAfter - TLSCounterAccelAfter {
157
+		size := stats.Size()
158
+		suite.Equal(TLSRecordSizeAccel, size)
159
+	}
160
+}
161
+
162
+func (suite *StatsTestSuite) TestSizeMaxPhase() {
163
+	stats := &Stats{k: 1.0, lambda: 1.0}
164
+
165
+	for range TLSCounterMaxAfter {
166
+		stats.Size()
167
+	}
168
+
169
+	for range 20 {
170
+		size := stats.Size()
171
+		suite.Equal(TLSRecordSizeMax, size)
172
+	}
173
+}
174
+
175
+func (suite *StatsTestSuite) TestSizeResetsAfterInactivity() {
176
+	stats := &Stats{k: 1.0, lambda: 1.0}
177
+
178
+	// Advance past start phase.
179
+	for range TLSCounterMaxAfter {
180
+		stats.Size()
181
+	}
182
+
183
+	suite.Equal(TLSRecordSizeMax, stats.Size())
184
+
185
+	// Simulate inactivity by backdating sizeLastRequested.
186
+	stats.sizeLastRequested = time.Now().Add(-TLSRecordSizeResetAfter - time.Millisecond)
187
+
188
+	suite.Equal(TLSRecordSizeStart, stats.Size())
189
+}
190
+
191
+func TestStats(t *testing.T) {
192
+	t.Parallel()
193
+	suite.Run(t, &StatsTestSuite{})
194
+}

+ 0
- 134
mtglib/internal/faketls/client_hello.go Datei anzeigen

@@ -1,134 +0,0 @@
1
-package faketls
2
-
3
-import (
4
-	"crypto/hmac"
5
-	"crypto/sha256"
6
-	"crypto/subtle"
7
-	"encoding/binary"
8
-	"fmt"
9
-	"time"
10
-
11
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
12
-)
13
-
14
-type ClientHello struct {
15
-	Time        time.Time
16
-	Random      [RandomLen]byte
17
-	SessionID   []byte
18
-	Host        string
19
-	CipherSuite uint16
20
-}
21
-
22
-func (c ClientHello) Valid(hostname string, tolerateTimeSkewness time.Duration) error {
23
-	if c.Host != "" && c.Host != hostname {
24
-		return fmt.Errorf("incorrect hostname %s", hostname)
25
-	}
26
-
27
-	now := time.Now()
28
-
29
-	timeDiff := now.Sub(c.Time)
30
-	if timeDiff < 0 {
31
-		timeDiff = -timeDiff
32
-	}
33
-
34
-	if timeDiff > tolerateTimeSkewness {
35
-		return fmt.Errorf("incorrect timestamp. got=%d, now=%d, diff=%s",
36
-			c.Time.Unix(), now.Unix(), timeDiff.String())
37
-	}
38
-
39
-	return nil
40
-}
41
-
42
-func ParseClientHello(secret, handshake []byte) (ClientHello, error) {
43
-	hello := ClientHello{}
44
-
45
-	if len(handshake) < ClientHelloMinLen {
46
-		return hello, fmt.Errorf("lengh of handshake is too small: %d", len(handshake))
47
-	}
48
-
49
-	if handshake[0] != HandshakeTypeClient {
50
-		return hello, fmt.Errorf("unknown handshake type %#x", handshake[0])
51
-	}
52
-
53
-	handshakeSizeBytes := [4]byte{0, handshake[1], handshake[2], handshake[3]}
54
-	handshakeLength := binary.BigEndian.Uint32(handshakeSizeBytes[:])
55
-
56
-	if len(handshake)-4 != int(handshakeLength) {
57
-		return hello,
58
-			fmt.Errorf("incorrect handshake size. manifested=%d, real=%d",
59
-				handshakeLength, len(handshake)-4)
60
-	}
61
-
62
-	copy(hello.Random[:], handshake[ClientHelloRandomOffset:])
63
-	copy(handshake[ClientHelloRandomOffset:], clientHelloEmptyRandom)
64
-
65
-	rec := record.AcquireRecord()
66
-	defer record.ReleaseRecord(rec)
67
-
68
-	rec.Type = record.TypeHandshake
69
-	rec.Version = record.Version10
70
-	rec.Payload.Write(handshake)
71
-
72
-	// mac is calculated for the whole record, not only
73
-	// for the payload part
74
-	mac := hmac.New(sha256.New, secret)
75
-	rec.Dump(mac) //nolint: errcheck
76
-
77
-	computedRandom := mac.Sum(nil)
78
-
79
-	for i := range RandomLen {
80
-		computedRandom[i] ^= hello.Random[i]
81
-	}
82
-
83
-	if subtle.ConstantTimeCompare(clientHelloEmptyRandom[:RandomLen-4], computedRandom[:RandomLen-4]) != 1 {
84
-		return hello, ErrBadDigest
85
-	}
86
-
87
-	timestamp := int64(binary.LittleEndian.Uint32(computedRandom[RandomLen-4:]))
88
-	hello.Time = time.Unix(timestamp, 0)
89
-
90
-	parseSessionID(&hello, handshake)
91
-	parseCipherSuite(&hello, handshake)
92
-	parseSNI(&hello, handshake)
93
-
94
-	return hello, nil
95
-}
96
-
97
-func parseSessionID(hello *ClientHello, handshake []byte) {
98
-	hello.SessionID = make([]byte, handshake[ClientHelloSessionIDOffset])
99
-	copy(hello.SessionID, handshake[ClientHelloSessionIDOffset+1:])
100
-}
101
-
102
-func parseCipherSuite(hello *ClientHello, handshake []byte) {
103
-	cipherSuiteOffset := ClientHelloSessionIDOffset + len(hello.SessionID) + 3
104
-	hello.CipherSuite = binary.BigEndian.Uint16(handshake[cipherSuiteOffset : cipherSuiteOffset+2])
105
-}
106
-
107
-func parseSNI(hello *ClientHello, handshake []byte) {
108
-	cipherSuiteOffset := ClientHelloSessionIDOffset + len(hello.SessionID) + 1
109
-	handshake = handshake[cipherSuiteOffset:]
110
-
111
-	cipherSuiteLength := binary.BigEndian.Uint16(handshake[:2])
112
-	handshake = handshake[2+cipherSuiteLength:]
113
-
114
-	compressionMethodsLength := int(handshake[0])
115
-	handshake = handshake[1+compressionMethodsLength:]
116
-
117
-	extensionsLength := binary.BigEndian.Uint16(handshake[:2])
118
-	handshake = handshake[2 : 2+extensionsLength]
119
-
120
-	for len(handshake) > 0 {
121
-		if binary.BigEndian.Uint16(handshake[:2]) != ExtensionSNI {
122
-			extensionsLength := binary.BigEndian.Uint16(handshake[2:4])
123
-			handshake = handshake[4+extensionsLength:]
124
-
125
-			continue
126
-		}
127
-
128
-		hostnameLength := binary.BigEndian.Uint16(handshake[7:9])
129
-		handshake = handshake[9:]
130
-		hello.Host = string(handshake[:int(hostnameLength)])
131
-
132
-		return
133
-	}
134
-}

+ 0
- 21
mtglib/internal/faketls/client_hello_fuzz_test.go Datei anzeigen

@@ -1,21 +0,0 @@
1
-package faketls_test
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
7
-	"github.com/stretchr/testify/require"
8
-)
9
-
10
-var FuzzClientHelloSecret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
11
-
12
-func FuzzClientHello(f *testing.F) {
13
-	f.Add([]byte{1, 2, 3})
14
-
15
-	f.Fuzz(func(t *testing.T, frame []byte) {
16
-		_, err := faketls.ParseClientHello(FuzzClientHelloSecret, frame)
17
-
18
-		// a probability of having != err is almost negligible
19
-		require.Error(t, err)
20
-	})
21
-}

+ 0
- 191
mtglib/internal/faketls/client_hello_test.go Datei anzeigen

@@ -1,191 +0,0 @@
1
-package faketls_test
2
-
3
-import (
4
-	"encoding/base64"
5
-	"encoding/json"
6
-	"os"
7
-	"path/filepath"
8
-	"strings"
9
-	"testing"
10
-	"time"
11
-
12
-	"github.com/9seconds/mtg/v2/mtglib"
13
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
14
-	"github.com/stretchr/testify/assert"
15
-	"github.com/stretchr/testify/suite"
16
-)
17
-
18
-type ClientHelloSnapshot struct {
19
-	Time        int    `json:"time"`
20
-	Random      string `json:"random"`
21
-	SessionID   string `json:"sessionId"`
22
-	Host        string `json:"host"`
23
-	CipherSuite int    `json:"cipherSuite"`
24
-	Full        string `json:"full"`
25
-}
26
-
27
-func (c ClientHelloSnapshot) GetTime() time.Time {
28
-	return time.Unix(int64(c.Time), 0)
29
-}
30
-
31
-func (c ClientHelloSnapshot) GetRandom() []byte {
32
-	data, _ := base64.StdEncoding.DecodeString(c.Random)
33
-
34
-	return data
35
-}
36
-
37
-func (c ClientHelloSnapshot) GetSessionID() []byte {
38
-	data, _ := base64.StdEncoding.DecodeString(c.SessionID)
39
-
40
-	return data
41
-}
42
-
43
-func (c ClientHelloSnapshot) GetHost() string {
44
-	return c.Host
45
-}
46
-
47
-func (c ClientHelloSnapshot) GetCipherSuite() uint16 {
48
-	return uint16(c.CipherSuite)
49
-}
50
-
51
-func (c ClientHelloSnapshot) GetFull() []byte {
52
-	data, _ := base64.StdEncoding.DecodeString(c.Full)
53
-
54
-	return data
55
-}
56
-
57
-type ClientHelloTestSuite struct {
58
-	suite.Suite
59
-
60
-	secret mtglib.Secret
61
-}
62
-
63
-func (suite *ClientHelloTestSuite) SetupSuite() {
64
-	parsed, err := mtglib.ParseSecret("ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d")
65
-	if err != nil {
66
-		panic(err)
67
-	}
68
-
69
-	suite.secret = parsed
70
-}
71
-
72
-func (suite *ClientHelloTestSuite) TestEmptyHandshake() {
73
-	_, err := faketls.ParseClientHello(suite.secret.Key[:], nil)
74
-	suite.Error(err)
75
-}
76
-
77
-func (suite *ClientHelloTestSuite) TestIncorrectHandshakeType() {
78
-	data := make([]byte, 1024)
79
-	data[0] = 0x02
80
-
81
-	_, err := faketls.ParseClientHello(suite.secret.Key[:], data)
82
-	suite.Error(err)
83
-}
84
-
85
-func (suite *ClientHelloTestSuite) TestIncorrectLength() {
86
-	data := make([]byte, 1024)
87
-	data[0] = 0x01
88
-	data[1] = 0xff
89
-	data[2] = 0xff
90
-
91
-	_, err := faketls.ParseClientHello(suite.secret.Key[:], data)
92
-	suite.Error(err)
93
-}
94
-
95
-func (suite *ClientHelloTestSuite) TestSnapshotOk() {
96
-	files, err := os.ReadDir("testdata")
97
-	suite.NoError(err)
98
-
99
-	testData := []string{}
100
-
101
-	for _, v := range files {
102
-		if strings.HasPrefix(v.Name(), "client-hello-ok") {
103
-			testData = append(testData, v.Name())
104
-		}
105
-	}
106
-
107
-	for _, name := range testData {
108
-		path := filepath.Join("testdata", name)
109
-
110
-		suite.T().Run(name, func(t *testing.T) {
111
-			fileData, err := os.ReadFile(path)
112
-			assert.NoError(t, err)
113
-
114
-			snapshot := &ClientHelloSnapshot{}
115
-			assert.NoError(t, json.Unmarshal(fileData, snapshot))
116
-
117
-			hello, err := faketls.ParseClientHello(suite.secret.Key[:], snapshot.GetFull())
118
-			assert.NoError(t, err)
119
-			assert.WithinDuration(t, snapshot.GetTime(), hello.Time, time.Second)
120
-			assert.Equal(t, snapshot.GetRandom(), hello.Random[:])
121
-			assert.Equal(t, snapshot.GetSessionID(), hello.SessionID)
122
-			assert.Equal(t, snapshot.GetHost(), hello.Host)
123
-			assert.Equal(t, snapshot.GetCipherSuite(), hello.CipherSuite)
124
-		})
125
-	}
126
-}
127
-
128
-func (suite *ClientHelloTestSuite) TestSnapshotBad() {
129
-	files, err := os.ReadDir("testdata")
130
-	suite.NoError(err)
131
-
132
-	testData := []string{}
133
-
134
-	for _, v := range files {
135
-		if strings.HasPrefix(v.Name(), "client-hello-bad") {
136
-			testData = append(testData, v.Name())
137
-		}
138
-	}
139
-
140
-	for _, name := range testData {
141
-		path := filepath.Join("testdata", name)
142
-
143
-		suite.T().Run(name, func(t *testing.T) {
144
-			fileData, err := os.ReadFile(path)
145
-			assert.NoError(t, err)
146
-
147
-			snapshot := &ClientHelloSnapshot{}
148
-			assert.NoError(t, json.Unmarshal(fileData, snapshot))
149
-
150
-			_, err = faketls.ParseClientHello(suite.secret.Key[:], snapshot.GetFull())
151
-			assert.Error(t, err)
152
-		})
153
-	}
154
-}
155
-
156
-func (suite *ClientHelloTestSuite) TestValidateHostname() {
157
-	hello := faketls.ClientHello{
158
-		Time: time.Now(),
159
-	}
160
-	suite.NoError(hello.Valid("hostname", time.Second))
161
-
162
-	hello.Host = "hostname"
163
-	suite.Error(hello.Valid("hostname2", time.Second))
164
-	suite.NoError(hello.Valid("hostname", time.Second))
165
-}
166
-
167
-func (suite *ClientHelloTestSuite) TestValidateTime() {
168
-	testData := []time.Duration{
169
-		-2 * time.Second,
170
-		2 * time.Second,
171
-	}
172
-
173
-	for _, v := range testData {
174
-		value := v
175
-
176
-		suite.T().Run(value.String(), func(t *testing.T) {
177
-			hello := faketls.ClientHello{
178
-				Host: "hostname",
179
-				Time: time.Now().Add(value),
180
-			}
181
-			suite.Error(hello.Valid("hostname", 500*time.Millisecond))
182
-			suite.Error(hello.Valid("hostname", time.Second))
183
-			suite.NoError(hello.Valid("hostname", 3*time.Second))
184
-		})
185
-	}
186
-}
187
-
188
-func TestClientHello(t *testing.T) {
189
-	t.Parallel()
190
-	suite.Run(t, &ClientHelloTestSuite{})
191
-}

+ 0
- 72
mtglib/internal/faketls/conn.go Datei anzeigen

@@ -1,72 +0,0 @@
1
-package faketls
2
-
3
-import (
4
-	"bytes"
5
-	"fmt"
6
-	"math/rand/v2"
7
-
8
-	"github.com/9seconds/mtg/v2/essentials"
9
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
10
-)
11
-
12
-type Conn struct {
13
-	essentials.Conn
14
-
15
-	readBuffer bytes.Buffer
16
-}
17
-
18
-func (c *Conn) Read(p []byte) (int, error) {
19
-	if n, _ := c.readBuffer.Read(p); n > 0 {
20
-		return n, nil
21
-	}
22
-
23
-	rec := record.AcquireRecord()
24
-	defer record.ReleaseRecord(rec)
25
-
26
-	for {
27
-		if err := rec.Read(c.Conn); err != nil {
28
-			return 0, err //nolint: wrapcheck
29
-		}
30
-
31
-		switch rec.Type { //nolint: exhaustive
32
-		case record.TypeApplicationData:
33
-			rec.Payload.WriteTo(&c.readBuffer) //nolint: errcheck
34
-
35
-			return c.readBuffer.Read(p) //nolint: wrapcheck
36
-		case record.TypeChangeCipherSpec:
37
-		default:
38
-			return 0, fmt.Errorf("unsupported record type %v", rec.Type)
39
-		}
40
-	}
41
-}
42
-
43
-func (c *Conn) Write(p []byte) (int, error) {
44
-	rec := record.AcquireRecord()
45
-	defer record.ReleaseRecord(rec)
46
-
47
-	rec.Type = record.TypeApplicationData
48
-	rec.Version = record.Version12
49
-
50
-	written := 0
51
-
52
-	for len(p) > 0 {
53
-		chunkSize := rand.IntN(record.TLSMaxRecordSize)
54
-		if chunkSize > len(p) || chunkSize == 0 {
55
-			chunkSize = len(p)
56
-		}
57
-
58
-		rec.Payload.Reset()
59
-		rec.Payload.Write(p[:chunkSize])
60
-
61
-		err := rec.Dump(c.Conn)
62
-		written += chunkSize
63
-
64
-		if err != nil {
65
-			return written, err
66
-		}
67
-
68
-		p = p[chunkSize:]
69
-	}
70
-
71
-	return written, nil
72
-}

+ 0
- 153
mtglib/internal/faketls/conn_test.go Datei anzeigen

@@ -1,153 +0,0 @@
1
-package faketls_test
2
-
3
-import (
4
-	"bytes"
5
-	"crypto/rand"
6
-	"errors"
7
-	"io"
8
-	"testing"
9
-
10
-	"github.com/9seconds/mtg/v2/internal/testlib"
11
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
12
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
13
-	"github.com/stretchr/testify/mock"
14
-	"github.com/stretchr/testify/suite"
15
-)
16
-
17
-type ConnMock struct {
18
-	testlib.EssentialsConnMock
19
-
20
-	readBuffer  bytes.Buffer
21
-	writeBuffer bytes.Buffer
22
-}
23
-
24
-func (m *ConnMock) Read(p []byte) (int, error) {
25
-	m.Called(p)
26
-
27
-	return m.readBuffer.Read(p) //nolint: wrapcheck
28
-}
29
-
30
-func (m *ConnMock) Write(p []byte) (int, error) {
31
-	m.Called(p)
32
-
33
-	return m.writeBuffer.Write(p) //nolint: wrapcheck
34
-}
35
-
36
-type ConnTestSuite struct {
37
-	suite.Suite
38
-
39
-	connMock *ConnMock
40
-	c        *faketls.Conn
41
-}
42
-
43
-func (suite *ConnTestSuite) SetupTest() {
44
-	suite.connMock = &ConnMock{}
45
-	suite.c = &faketls.Conn{
46
-		Conn: suite.connMock,
47
-	}
48
-}
49
-
50
-func (suite *ConnTestSuite) TearDownTest() {
51
-	suite.connMock.AssertExpectations(suite.T())
52
-}
53
-
54
-func (suite *ConnTestSuite) TestRead() {
55
-	suite.connMock.On("Read", mock.Anything).Return(0, nil)
56
-
57
-	rec := record.AcquireRecord()
58
-	defer record.ReleaseRecord(rec)
59
-
60
-	rec.Type = record.TypeChangeCipherSpec
61
-	rec.Version = record.Version12
62
-
63
-	rec.Payload.WriteByte(0x01)
64
-	rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
65
-	rec.Reset()
66
-
67
-	rec.Type = record.TypeApplicationData
68
-	rec.Version = record.Version12
69
-
70
-	rec.Payload.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
71
-	rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
72
-
73
-	resultBuffer := &bytes.Buffer{}
74
-	buf := make([]byte, 2)
75
-
76
-	for {
77
-		n, err := suite.c.Read(buf)
78
-		if errors.Is(err, io.EOF) {
79
-			break
80
-		}
81
-
82
-		resultBuffer.Write(buf[:n])
83
-	}
84
-
85
-	suite.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, resultBuffer.Bytes())
86
-}
87
-
88
-func (suite *ConnTestSuite) TestReadUnexpected() {
89
-	suite.connMock.On("Read", mock.Anything).Return(0, nil)
90
-
91
-	rec := record.AcquireRecord()
92
-	defer record.ReleaseRecord(rec)
93
-
94
-	rec.Type = record.TypeChangeCipherSpec
95
-	rec.Version = record.Version12
96
-
97
-	rec.Payload.WriteByte(0x01)
98
-	rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
99
-	rec.Reset()
100
-
101
-	rec.Type = record.TypeHandshake
102
-	rec.Version = record.Version12
103
-
104
-	rec.Payload.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
105
-	rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
106
-
107
-	buf := make([]byte, 2)
108
-
109
-	for {
110
-		_, err := suite.c.Read(buf)
111
-
112
-		switch {
113
-		case err == nil:
114
-		case errors.Is(err, io.EOF):
115
-			suite.FailNow("unexpected to finish")
116
-		default:
117
-			return
118
-		}
119
-	}
120
-}
121
-
122
-func (suite *ConnTestSuite) TestWrite() {
123
-	suite.connMock.On("Write", mock.Anything).Return(0, nil)
124
-
125
-	dataToRec := make([]byte, record.TLSMaxRecordSize*2)
126
-	rand.Read(dataToRec) //nolint: staticcheck, errcheck
127
-
128
-	n, err := suite.c.Write(dataToRec)
129
-	suite.NoError(err)
130
-	suite.Equal(len(dataToRec), n)
131
-
132
-	rec := record.AcquireRecord()
133
-	defer record.ReleaseRecord(rec)
134
-
135
-	buf := &bytes.Buffer{}
136
-
137
-	for {
138
-		if err := rec.Read(&suite.connMock.writeBuffer); err != nil {
139
-			break
140
-		}
141
-
142
-		suite.Equal(record.TypeApplicationData, rec.Type)
143
-		suite.Equal(record.Version12, rec.Version)
144
-		rec.Payload.WriteTo(buf) //nolint: errcheck
145
-	}
146
-
147
-	suite.Equal(dataToRec, buf.Bytes())
148
-}
149
-
150
-func TestConn(t *testing.T) {
151
-	t.Parallel()
152
-	suite.Run(t, &ConnTestSuite{})
153
-}

+ 0
- 59
mtglib/internal/faketls/init.go Datei anzeigen

@@ -1,59 +0,0 @@
1
-package faketls
2
-
3
-import (
4
-	"bytes"
5
-	"errors"
6
-)
7
-
8
-const (
9
-	// RandomLen defines a size of the random digest in TLS Hellos.
10
-	RandomLen = 32
11
-
12
-	// ClientHelloRandomOffset is an offset in ClientHello record where
13
-	// random digest is started.
14
-	ClientHelloRandomOffset = 6
15
-
16
-	// ClientHelloSessionIDOffset is an offset in ClientHello record where
17
-	// SessionID is started.
18
-	ClientHelloSessionIDOffset = ClientHelloRandomOffset + RandomLen
19
-
20
-	// ClientHelloMinLen is a minimal possible length of
21
-	// ClientHello record.
22
-	ClientHelloMinLen = 6
23
-
24
-	// WelcomePacketRandomOffset is an offset of random in ServerHello
25
-	// packet (including record envelope).
26
-	WelcomePacketRandomOffset = 11
27
-
28
-	// HandshakeTypeClient is a value representing a client handshake.
29
-	HandshakeTypeClient = 0x01
30
-
31
-	// HandshakeTypeServer is a value representing a server handshake.
32
-	HandshakeTypeServer = 0x02
33
-
34
-	// ChangeCipherValue is a value representing a change cipher
35
-	// specification record.
36
-	ChangeCipherValue = 0x01
37
-
38
-	// ExtensionSNI is a value for TLS extension 'SNI'.
39
-	ExtensionSNI = 0x00
40
-)
41
-
42
-var (
43
-	// ErrBadDigest is returned if given TLS Client Hello mismatches with a
44
-	// derived one.
45
-	ErrBadDigest = errors.New("bad digest")
46
-
47
-	serverHelloSuffix = []byte{
48
-		0x00,       // no compression
49
-		0x00, 0x2e, // 46 bytes of data
50
-		0x00, 0x2b, // Extension - Supported Versions
51
-		0x00, 0x02, // 2 bytes are following
52
-		0x03, 0x04, // TLS 1.3
53
-		0x00, 0x33, // Extension - Key Share
54
-		0x00, 0x24, // 36 bytes
55
-		0x00, 0x1d, // x25519 curve
56
-		0x00, 0x20, // 32 bytes of key
57
-	}
58
-	clientHelloEmptyRandom = bytes.Repeat([]byte{0}, RandomLen)
59
-)

+ 0
- 84
mtglib/internal/faketls/record/init.go Datei anzeigen

@@ -1,84 +0,0 @@
1
-package record
2
-
3
-import "fmt"
4
-
5
-const TLSMaxRecordSize = 65535 // max uint16
6
-
7
-type Type uint8
8
-
9
-const (
10
-	// TypeChangeCipherSpec defines a byte value of the TLS record when a
11
-	// peer wants to change a specifications of the chosen cipher.
12
-	TypeChangeCipherSpec Type = 0x14
13
-
14
-	// TypeHandshake defines a byte value of the TLS record when a peer
15
-	// initiates a new TLS connection and wants to make a handshake
16
-	// ceremony.
17
-	TypeHandshake Type = 0x16
18
-
19
-	// TypeApplicationData defines a byte value of the TLS record when a
20
-	// peer sends an user data, not a control frames.
21
-	TypeApplicationData Type = 0x17
22
-)
23
-
24
-func (t Type) String() string {
25
-	switch t {
26
-	case TypeChangeCipherSpec:
27
-		return "changeCipher(0x14)"
28
-	case TypeHandshake:
29
-		return "handshake(0x16)"
30
-	case TypeApplicationData:
31
-		return "applicationData(0x17)"
32
-	}
33
-
34
-	return fmt.Sprintf("unknown(%#x)", byte(t))
35
-}
36
-
37
-func (t Type) Valid() error {
38
-	switch t {
39
-	case TypeChangeCipherSpec, TypeHandshake, TypeApplicationData:
40
-		return nil
41
-	}
42
-
43
-	return fmt.Errorf("unknown type %#x", byte(t))
44
-}
45
-
46
-type Version uint16
47
-
48
-const (
49
-	// Version10 defines a TLS1.0.
50
-	Version10 Version = 769 // 0x03 0x01
51
-
52
-	// Version11 defines a TLS1.1.
53
-	Version11 Version = 770 // 0x03 0x02
54
-
55
-	// Version12 defines a TLS1.2.
56
-	Version12 Version = 771 // 0x03 0x03
57
-
58
-	// Version13 defines a TLS1.3.
59
-	Version13 Version = 772 // 0x03 0x04
60
-)
61
-
62
-func (v Version) String() string {
63
-	switch v {
64
-	case Version10:
65
-		return "tls1.0"
66
-	case Version11:
67
-		return "tls1.1"
68
-	case Version12:
69
-		return "tls1.2"
70
-	case Version13:
71
-		return "tls1.3"
72
-	}
73
-
74
-	return fmt.Sprintf("tls?(%d)", uint16(v))
75
-}
76
-
77
-func (v Version) Valid() error {
78
-	switch v {
79
-	case Version10, Version11, Version12, Version13:
80
-		return nil
81
-	}
82
-
83
-	return fmt.Errorf("unknown version %d", uint16(v))
84
-}

+ 0
- 79
mtglib/internal/faketls/record/init_test.go Datei anzeigen

@@ -1,79 +0,0 @@
1
-package record_test
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
7
-	"github.com/stretchr/testify/suite"
8
-)
9
-
10
-type TypeTestSuite struct {
11
-	suite.Suite
12
-}
13
-
14
-func (suite *TypeTestSuite) TestChangeCipherSpec() {
15
-	suite.Contains(record.TypeChangeCipherSpec.String(), "changeCipher")
16
-	suite.Contains(record.TypeChangeCipherSpec.String(), "0x14")
17
-	suite.NoError(record.TypeChangeCipherSpec.Valid())
18
-}
19
-
20
-func (suite *TypeTestSuite) TestHandshake() {
21
-	suite.Contains(record.TypeHandshake.String(), "handshake")
22
-	suite.Contains(record.TypeHandshake.String(), "0x16")
23
-	suite.NoError(record.TypeHandshake.Valid())
24
-}
25
-
26
-func (suite *TypeTestSuite) TestApplicationData() {
27
-	suite.Contains(record.TypeApplicationData.String(), "applicationData")
28
-	suite.Contains(record.TypeApplicationData.String(), "0x17")
29
-	suite.NoError(record.TypeApplicationData.Valid())
30
-}
31
-
32
-func (suite *TypeTestSuite) TestUnknown() {
33
-	value := record.Type(0x20)
34
-
35
-	suite.Contains(value.String(), "unknown")
36
-	suite.Contains(value.String(), "0x20")
37
-	suite.Error(value.Valid())
38
-}
39
-
40
-type VersionTestSuite struct {
41
-	suite.Suite
42
-}
43
-
44
-func (suite *VersionTestSuite) Test10() {
45
-	suite.Equal("tls1.0", record.Version10.String())
46
-	suite.NoError(record.Version10.Valid())
47
-}
48
-
49
-func (suite *VersionTestSuite) Test11() {
50
-	suite.Equal("tls1.1", record.Version11.String())
51
-	suite.NoError(record.Version11.Valid())
52
-}
53
-
54
-func (suite *VersionTestSuite) Test12() {
55
-	suite.Equal("tls1.2", record.Version12.String())
56
-	suite.NoError(record.Version12.Valid())
57
-}
58
-
59
-func (suite *VersionTestSuite) Test13() {
60
-	suite.Equal("tls1.3", record.Version13.String())
61
-	suite.NoError(record.Version13.Valid())
62
-}
63
-
64
-func (suite *VersionTestSuite) TestUnknown() {
65
-	value := record.Version(900)
66
-
67
-	suite.Equal("tls?(900)", value.String())
68
-	suite.Error(value.Valid())
69
-}
70
-
71
-func TestType(t *testing.T) {
72
-	t.Parallel()
73
-	suite.Run(t, &TypeTestSuite{})
74
-}
75
-
76
-func TestVersion(t *testing.T) {
77
-	t.Parallel()
78
-	suite.Run(t, &VersionTestSuite{})
79
-}

+ 0
- 20
mtglib/internal/faketls/record/pools.go Datei anzeigen

@@ -1,20 +0,0 @@
1
-package record
2
-
3
-import (
4
-	"sync"
5
-)
6
-
7
-var recordPool = sync.Pool{
8
-	New: func() any {
9
-		return &Record{}
10
-	},
11
-}
12
-
13
-func AcquireRecord() *Record {
14
-	return recordPool.Get().(*Record) //nolint: forcetypeassert
15
-}
16
-
17
-func ReleaseRecord(r *Record) {
18
-	r.Reset()
19
-	recordPool.Put(r)
20
-}

+ 0
- 86
mtglib/internal/faketls/record/record.go Datei anzeigen

@@ -1,86 +0,0 @@
1
-package record
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/base64"
6
-	"encoding/binary"
7
-	"fmt"
8
-	"io"
9
-)
10
-
11
-type Record struct {
12
-	Type    Type
13
-	Version Version
14
-	Payload bytes.Buffer
15
-}
16
-
17
-func (r *Record) String() string {
18
-	return fmt.Sprintf("<tlsRecord(type=%v, version=%v, payload=%s)>",
19
-		r.Type,
20
-		r.Version,
21
-		base64.StdEncoding.EncodeToString(r.Payload.Bytes()))
22
-}
23
-
24
-func (r *Record) Reset() {
25
-	r.Payload.Reset()
26
-}
27
-
28
-func (r *Record) Read(reader io.Reader) error {
29
-	r.Reset()
30
-
31
-	buf := [2]byte{}
32
-
33
-	if _, err := io.ReadFull(reader, buf[:1]); err != nil {
34
-		return fmt.Errorf("cannot read type: %w", err)
35
-	}
36
-
37
-	r.Type = Type(buf[0])
38
-	if err := r.Type.Valid(); err != nil {
39
-		return fmt.Errorf("invalid type: %w", err)
40
-	}
41
-
42
-	if _, err := io.ReadFull(reader, buf[:]); err != nil {
43
-		return fmt.Errorf("cannot read version: %w", err)
44
-	}
45
-
46
-	r.Version = Version(binary.BigEndian.Uint16(buf[:]))
47
-	if err := r.Version.Valid(); err != nil {
48
-		return fmt.Errorf("invalid version: %w", err)
49
-	}
50
-
51
-	if _, err := io.ReadFull(reader, buf[:]); err != nil {
52
-		return fmt.Errorf("cannot read payload length: %w", err)
53
-	}
54
-
55
-	length := int64(binary.BigEndian.Uint16(buf[:]))
56
-	if _, err := io.CopyN(&r.Payload, reader, length); err != nil {
57
-		return fmt.Errorf("cannot read payload: %w", err)
58
-	}
59
-
60
-	return nil
61
-}
62
-
63
-func (r *Record) Dump(writer io.Writer) error {
64
-	buf := [2]byte{byte(r.Type), 0}
65
-	if _, err := writer.Write(buf[:1]); err != nil {
66
-		return fmt.Errorf("cannot dump record type: %w", err)
67
-	}
68
-
69
-	binary.BigEndian.PutUint16(buf[:], uint16(r.Version))
70
-
71
-	if _, err := writer.Write(buf[:]); err != nil {
72
-		return fmt.Errorf("cannot dump version: %w", err)
73
-	}
74
-
75
-	binary.BigEndian.PutUint16(buf[:], uint16(r.Payload.Len()))
76
-
77
-	if _, err := writer.Write(buf[:]); err != nil {
78
-		return fmt.Errorf("cannot dump payload length: %w", err)
79
-	}
80
-
81
-	if _, err := writer.Write(r.Payload.Bytes()); err != nil {
82
-		return fmt.Errorf("cannot dump record: %w", err)
83
-	}
84
-
85
-	return nil
86
-}

+ 0
- 110
mtglib/internal/faketls/record/record_test.go Datei anzeigen

@@ -1,110 +0,0 @@
1
-package record_test
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/base64"
6
-	"encoding/json"
7
-	"os"
8
-	"path/filepath"
9
-	"testing"
10
-
11
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
12
-	"github.com/stretchr/testify/assert"
13
-	"github.com/stretchr/testify/suite"
14
-)
15
-
16
-type RecordTestSnapshot struct {
17
-	Type    int    `json:"type"`
18
-	Version int    `json:"version"`
19
-	Payload string `json:"payload"`
20
-	Record  string `json:"record"`
21
-}
22
-
23
-func (r RecordTestSnapshot) RecordBytes() []byte {
24
-	data, _ := base64.StdEncoding.DecodeString(r.Record)
25
-
26
-	return data
27
-}
28
-
29
-func (r RecordTestSnapshot) PayloadBytes() []byte {
30
-	data, _ := base64.StdEncoding.DecodeString(r.Payload)
31
-
32
-	return data
33
-}
34
-
35
-type RecordTestSuite struct {
36
-	suite.Suite
37
-
38
-	r   *record.Record
39
-	buf *bytes.Buffer
40
-}
41
-
42
-func (suite *RecordTestSuite) SetupTest() {
43
-	suite.r = record.AcquireRecord()
44
-	suite.buf = &bytes.Buffer{}
45
-}
46
-
47
-func (suite *RecordTestSuite) TearDownTest() {
48
-	record.ReleaseRecord(suite.r)
49
-	suite.buf.Reset()
50
-}
51
-
52
-func (suite *RecordTestSuite) TestIdempotent() {
53
-	suite.r.Type = record.TypeApplicationData
54
-	suite.r.Version = record.Version13
55
-
56
-	suite.r.Payload.Write([]byte{1, 2, 3})
57
-	suite.NoError(suite.r.Dump(suite.buf))
58
-
59
-	suite.r.Reset()
60
-	suite.NoError(suite.r.Read(suite.buf))
61
-
62
-	suite.Equal(0, suite.buf.Len())
63
-	suite.Equal(record.TypeApplicationData, suite.r.Type)
64
-	suite.Equal(record.Version13, suite.r.Version)
65
-	suite.Equal([]byte{1, 2, 3}, suite.r.Payload.Bytes())
66
-}
67
-
68
-func (suite *RecordTestSuite) TestString() {
69
-	_ = suite.r.String()
70
-}
71
-
72
-func (suite *RecordTestSuite) TestSnapshot() {
73
-	files, err := os.ReadDir("testdata")
74
-	suite.NoError(err)
75
-
76
-	testData := map[string]string{}
77
-
78
-	for _, f := range files {
79
-		testData[f.Name()] = filepath.Join("testdata", f.Name())
80
-	}
81
-
82
-	for name, pathV := range testData {
83
-		path := pathV
84
-
85
-		suite.T().Run(name, func(t *testing.T) {
86
-			data, err := os.ReadFile(path)
87
-			assert.NoError(t, err)
88
-
89
-			snapshot := &RecordTestSnapshot{}
90
-			assert.NoError(t, json.Unmarshal(data, snapshot))
91
-
92
-			rec := record.AcquireRecord()
93
-			defer record.ReleaseRecord(rec)
94
-
95
-			assert.NoError(t, rec.Read(bytes.NewReader(snapshot.RecordBytes())))
96
-			assert.Equal(t, snapshot.Type, int(rec.Type))
97
-			assert.Equal(t, snapshot.Version, int(rec.Version))
98
-			assert.Equal(t, snapshot.PayloadBytes(), rec.Payload.Bytes())
99
-
100
-			buf := &bytes.Buffer{}
101
-			assert.NoError(t, rec.Dump(buf))
102
-			assert.Equal(t, snapshot.RecordBytes(), buf.Bytes())
103
-		})
104
-	}
105
-}
106
-
107
-func TestRecord(t *testing.T) {
108
-	t.Parallel()
109
-	suite.Run(t, &RecordTestSuite{})
110
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/05eb6b71f87b6802.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 20,
3
-  "version": 772,
4
-  "payload": "sxS+0oAyk+NBv0LLVtQOp9WSx4CweyUZPz01tQ0o4oyp8aaBl6/kMFvLq3q52KE8lCiKejLw2NxVBUkE+4izCf2gLx9qfr81opWnqJTChWzcDijvttbq9cmtDFNL+odKsS3v1/TfYEFtPsoRPrJRmOHRAnqnf49Y5Q==",
5
-  "record": "FAMEAHmzFL7SgDKT40G/QstW1A6n1ZLHgLB7JRk/PTW1DSjijKnxpoGXr+QwW8urernYoTyUKIp6MvDY3FUFSQT7iLMJ/aAvH2p+vzWilaeolMKFbNwOKO+21ur1ya0MU0v6h0qxLe/X9N9gQW0+yhE+slGY4dECeqd/j1jl"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/4eef4abc15b206b6.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 22,
3
-  "version": 772,
4
-  "payload": "waNH223htyxCBKAb6hm0u/SK/9mhI8Ck91nfWob7QMOaIREogrDYREJH4Djcp47XrpAlEaUIDiCvoFLVJ/LK1nYs4swzfHSSl/+Aj1eqPA63XqPa8EG4FAbf0DwjwXxV9qVIhvP9b2TafKbzr4Yb5GCygzFRb/zawA==",
5
-  "record": "FgMEAHnBo0fbbeG3LEIEoBvqGbS79Ir/2aEjwKT3Wd9ahvtAw5ohESiCsNhEQkfgONynjteukCURpQgOIK+gUtUn8srWdizizDN8dJKX/4CPV6o8Drdeo9rwQbgUBt/QPCPBfFX2pUiG8/1vZNp8pvOvhhvkYLKDMVFv/NrA"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/736f358216afe91f.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 23,
3
-  "version": 769,
4
-  "payload": "jmJ0o1E5+ehAHHYAbCo4AMV03X7RSivYl250s06nD9CO44fyjaoGELz0N7IeCg1jFKcRVSCRmYYmiIY9wydn2fXOJhKif8B0BlM3qhbethYgyP+l1S8hyyETpIiOtiiiOnAJwl1D1j9OryFiJFSdRRXReIMZ4CPqPg==",
5
-  "record": "FwMBAHmOYnSjUTn56EAcdgBsKjgAxXTdftFKK9iXbnSzTqcP0I7jh/KNqgYQvPQ3sh4KDWMUpxFVIJGZhiaIhj3DJ2fZ9c4mEqJ/wHQGUzeqFt62FiDI/6XVLyHLIROkiI62KKI6cAnCXUPWP06vIWIkVJ1FFdF4gxngI+o+"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/8405d94222bd0b6a.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 22,
3
-  "version": 769,
4
-  "payload": "hBnpBnNUdlqe/rKXa7Judcz79u7AkUgSGOycn8EqvbkZpVxnI31rNOvAsPZqG+GF7DWJ3R7H2ETmFmrpnyyng32MjSs1jptmV1oAs63zTADD7sVipgid9AJHwfl4CrC3FIQr43IPMYd29JPOl5bqu/SfrgI16PBiJw==",
5
-  "record": "FgMBAHmEGekGc1R2Wp7+spdrsm51zPv27sCRSBIY7JyfwSq9uRmlXGcjfWs068Cw9mob4YXsNYndHsfYROYWaumfLKeDfYyNKzWOm2ZXWgCzrfNMAMPuxWKmCJ30AkfB+XgKsLcUhCvjcg8xh3b0k86Xluq79J+uAjXo8GIn"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/9036f76e517f0cd1.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 23,
3
-  "version": 770,
4
-  "payload": "Vm/C+DO56czlbtR915aHzsugSyDtp8CtojF9w1jKY0efyyfcLrNuhNg/pZm3gQ7v2BBbL1UJ97v/RIjST+5gRIfg3bBN1BE9hkf+N2AYY2lHLi0yeInHB0zFWPeHscsDopDFadIi5KtC8HvbEMuK+kK8POVk5tN9UQ==",
5
-  "record": "FwMCAHlWb8L4M7npzOVu1H3XlofOy6BLIO2nwK2iMX3DWMpjR5/LJ9wus26E2D+lmbeBDu/YEFsvVQn3u/9EiNJP7mBEh+DdsE3UET2GR/43YBhjaUcuLTJ4iccHTMVY94exywOikMVp0iLkq0Lwe9sQy4r6Qrw85WTm031R"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/9244766a0fe4a02a.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 22,
3
-  "version": 770,
4
-  "payload": "ajPzpsgk4gwm2stRQKbllvKRLdI7vmyaj1uxEJ/kKoQnQSPumdDNKD618U2Cq6PVd0/b+9YtH67Uzx1QxtpKuby5fUXqw06WUuDAQsmjq7F26EkE5FND6rQUjUPC+e1U0dF4TQzOUSS4IAkFQPAaVehUVTRxVWa/0g==",
5
-  "record": "FgMCAHlqM/OmyCTiDCbay1FApuWW8pEt0ju+bJqPW7EQn+QqhCdBI+6Z0M0oPrXxTYKro9V3T9v71i0frtTPHVDG2kq5vLl9RerDTpZS4MBCyaOrsXboSQTkU0PqtBSNQ8L57VTR0XhNDM5RJLggCQVA8BpV6FRVNHFVZr/S"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/9255c73d3de76e7b.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 20,
3
-  "version": 771,
4
-  "payload": "d1Hiv1NYVgEDR9mtJyv9j8mg3dWqfUpeKfOsL+jzSDfVIxeDiJZFLDT50TjNW44/yEOVEX/Y/pk+wnc7E8aCEiwGwAvB+Insw1UCJ2ejt689VWLo2u4klGVKTHuOpUvdGVTc7Lo4FAt91KQSPLYB5iqxomjEv5e3Vg==",
5
-  "record": "FAMDAHl3UeK/U1hWAQNH2a0nK/2PyaDd1ap9Sl4p86wv6PNIN9UjF4OIlkUsNPnROM1bjj/IQ5URf9j+mT7CdzsTxoISLAbAC8H4iezDVQInZ6O3rz1VYuja7iSUZUpMe46lS90ZVNzsujgUC33UpBI8tgHmKrGiaMS/l7dW"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/aeb65b9924315cf8.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 23,
3
-  "version": 771,
4
-  "payload": "wbdU1CbrzuAJDsh6CFjGyE+AFArJj/Wmsa2wtDyW0kRuE2vUO8gg+nXkg0kkoz0WnvQEOdaswfJIaVrloD78yoyeQVfBB+VUP/63vqn60v5ccaQEn0jLdxgLjiTAxKDQDxCTMRoLnFE2ZZf28zw+HfqpIxiOZs8LhQ==",
5
-  "record": "FwMDAHnBt1TUJuvO4AkOyHoIWMbIT4AUCsmP9aaxrbC0PJbSRG4Ta9Q7yCD6deSDSSSjPRae9AQ51qzB8khpWuWgPvzKjJ5BV8EH5VQ//re+qfrS/lxxpASfSMt3GAuOJMDEoNAPEJMxGgucUTZll/bzPD4d+qkjGI5mzwuF"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/b0acd44296056b54.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 23,
3
-  "version": 772,
4
-  "payload": "qqnBMb1Af3zZt4DPHpVRuIiON9ODGJUNFicFjranORh67L/HI4D6HnHyycZFUSBOw2FjMBF6UialY8snOYaRKrQmQzuUNg1Ztq7yAZ+Lgj3TBarR6OMlYhEAY0Px9Xv1UuJ0YcvQx33gdM1skJ5HBR3yZvEKNJV1LA==",
5
-  "record": "FwMEAHmqqcExvUB/fNm3gM8elVG4iI4304MYlQ0WJwWOtqc5GHrsv8cjgPoecfLJxkVRIE7DYWMwEXpSJqVjyyc5hpEqtCZDO5Q2DVm2rvIBn4uCPdMFqtHo4yViEQBjQ/H1e/VS4nRhy9DHfeB0zWyQnkcFHfJm8Qo0lXUs"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/c0545a13fd9a3fa3.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 20,
3
-  "version": 769,
4
-  "payload": "NEe735TuQFp7bWpFQhASas/e1XaySvus0ovXmkfCbFq334MyFHq2eDMadziXsfu/GfBjoYggvk0LgYUeoAkBNKR0dfSovjSndaqmIUonoWl+6sZObiGZkRIMwuY2q4Eaw4/iuDu/pZhjRW/iAIH+YH7cyk/1tgdJDg==",
5
-  "record": "FAMBAHk0R7vflO5AWnttakVCEBJqz97VdrJK+6zSi9eaR8JsWrffgzIUerZ4Mxp3OJex+78Z8GOhiCC+TQuBhR6gCQE0pHR19Ki+NKd1qqYhSiehaX7qxk5uIZmREgzC5jargRrDj+K4O7+lmGNFb+IAgf5gftzKT/W2B0kO"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/f083f4501668b759.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 22,
3
-  "version": 771,
4
-  "payload": "wrXjZrPm3OSyzO0klv6/G+z2PDloR/colS/RlWwQE31Vb2xm8YkEchDDKwlc/KPLD73qMoz3MQOQLtSLc8LhVYp+l7L9jz49yTaVKtBI5UuGbo09snsKxFCgCyYUBETKabATBQtiaEu/D8dmF4Yk/2ww4sEb8DwKLQ==",
5
-  "record": "FgMDAHnCteNms+bc5LLM7SSW/r8b7PY8OWhH9yiVL9GVbBATfVVvbGbxiQRyEMMrCVz8o8sPveoyjPcxA5Au1ItzwuFVin6Xsv2PPj3JNpUq0EjlS4ZujT2yewrEUKALJhQERMppsBMFC2JoS78Px2YXhiT/bDDiwRvwPAot"
6
-}

+ 0
- 6
mtglib/internal/faketls/record/testdata/f5696bcdffd11706.json Datei anzeigen

@@ -1,6 +0,0 @@
1
-{
2
-  "type": 20,
3
-  "version": 770,
4
-  "payload": "OU5s8Sa11hpXWEarWzFlX55IZt3Eo+F4AMbQ/2RwB4rfHS/JNl8n63OR4oYs9QXw3RfCrYJuU9n6Xn+I/+7ZzAgZ0PbLSXW1PrLtttdfmhTErK90b49YEWdY9na4g++NMkKykwgXvY1hNxZIHX/qawEWJgxXUR3DdQ==",
5
-  "record": "FAMCAHk5TmzxJrXWGldYRqtbMWVfnkhm3cSj4XgAxtD/ZHAHit8dL8k2Xyfrc5Hihiz1BfDdF8Ktgm5T2fpef4j/7tnMCBnQ9stJdbU+su2211+aFMSsr3Rvj1gRZ1j2driD740yQrKTCBe9jWE3Fkgdf+prARYmDFdRHcN1"
6
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-bad-fa2e46cdb33e2a1b.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181365,
3
-  "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
4
-  "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCACq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFANAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-19dfe38384b9884b.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181365,
3
-  "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
4
-  "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-48f8a72a56f3174a.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181352,
3
-  "random": "oYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12os=",
4
-  "sessionId": "FGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8g=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMDoYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12osgFGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8gANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAga6CocpFP8Qd4YCFR9pkaCr97po2ALj0P5nI9Nnb3UWMALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-651054256093c6cd.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181352,
3
-  "random": "5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1ro=",
4
-  "sessionId": "jxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMD5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1rogjxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgrulAaqUdKeVYM0F+pu6on/h6LBpOyzOKG4xFIKcoFk4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-79d01ef18a9d2621.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181365,
3
-  "random": "8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQ=",
4
-  "sessionId": "00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAU=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMD8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQg00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAUANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAg/9P7140NtKzjyDwBf99mOy1+FjRPAPHTNQ9WxHOKpV4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-7a5569f05b118145.json Datei anzeigen

@@ -1,8 +0,0 @@
1
-{
2
-  "time": 1617181352,
3
-  "random": "zja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4=",
4
-  "sessionId": "qPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnA=",
5
-  "host": "storage.googleapis.com",
6
-  "cipherSuite": 4867,
7
-  "full": "AQAB/AMDzja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4gqPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnAANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgXviLRAqAYJ8xOLdlcsUhldI4Xl0g/s9+y2Qrd8raPEgALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
8
-}

+ 0
- 91
mtglib/internal/faketls/welcome.go Datei anzeigen

@@ -1,91 +0,0 @@
1
-package faketls
2
-
3
-import (
4
-	"bytes"
5
-	"crypto/hmac"
6
-	"crypto/rand"
7
-	"crypto/sha256"
8
-	"encoding/binary"
9
-	"io"
10
-	mrand "math/rand/v2"
11
-
12
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
13
-	"golang.org/x/crypto/curve25519"
14
-)
15
-
16
-func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error {
17
-	buf := &bytes.Buffer{}
18
-
19
-	rec := record.AcquireRecord()
20
-	defer record.ReleaseRecord(rec)
21
-
22
-	rec.Type = record.TypeHandshake
23
-	rec.Version = record.Version12
24
-
25
-	generateServerHello(&rec.Payload, clientHello)
26
-	rec.Dump(buf) //nolint: errcheck
27
-	rec.Reset()
28
-
29
-	rec.Type = record.TypeChangeCipherSpec
30
-	rec.Version = record.Version12
31
-	rec.Payload.WriteByte(ChangeCipherValue)
32
-
33
-	rec.Dump(buf) //nolint: errcheck
34
-	rec.Reset()
35
-
36
-	rec.Type = record.TypeApplicationData
37
-	rec.Version = record.Version12
38
-
39
-	if _, err := io.CopyN(&rec.Payload, rand.Reader, int64(1024+mrand.IntN(3092))); err != nil {
40
-		panic(err)
41
-	}
42
-
43
-	rec.Dump(buf) //nolint: errcheck
44
-
45
-	packet := buf.Bytes()
46
-	mac := hmac.New(sha256.New, secret)
47
-
48
-	mac.Write(clientHello.Random[:])
49
-	mac.Write(packet)
50
-
51
-	copy(packet[WelcomePacketRandomOffset:], mac.Sum(nil))
52
-
53
-	if _, err := writer.Write(packet); err != nil {
54
-		return err //nolint: wrapcheck
55
-	}
56
-
57
-	return nil
58
-}
59
-
60
-func generateServerHello(writer io.Writer, clientHello ClientHello) {
61
-	bodyBuf := &bytes.Buffer{}
62
-
63
-	sliceBuf := [2]byte{}
64
-	digest := [RandomLen]byte{}
65
-
66
-	binary.BigEndian.PutUint16(sliceBuf[:], uint16(record.Version12))
67
-	bodyBuf.Write(sliceBuf[:])
68
-	bodyBuf.Write(digest[:])
69
-	bodyBuf.WriteByte(byte(len(clientHello.SessionID)))
70
-	bodyBuf.Write(clientHello.SessionID)
71
-
72
-	binary.BigEndian.PutUint16(sliceBuf[:], clientHello.CipherSuite)
73
-	bodyBuf.Write(sliceBuf[:])
74
-	bodyBuf.Write(serverHelloSuffix)
75
-
76
-	scalar := [32]byte{}
77
-
78
-	if _, err := rand.Read(scalar[:]); err != nil {
79
-		panic(err)
80
-	}
81
-
82
-	curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint)
83
-	bodyBuf.Write(curve)
84
-
85
-	header := [4]byte{0, 0, 0, 0}
86
-	binary.BigEndian.PutUint32(header[:], uint32(bodyBuf.Len()))
87
-	header[0] = HandshakeTypeServer
88
-
89
-	writer.Write(header[:]) //nolint: errcheck
90
-	bodyBuf.WriteTo(writer) //nolint: errcheck
91
-}

+ 0
- 82
mtglib/internal/faketls/welcome_test.go Datei anzeigen

@@ -1,82 +0,0 @@
1
-package faketls_test
2
-
3
-import (
4
-	"bytes"
5
-	"crypto/hmac"
6
-	"crypto/rand"
7
-	"crypto/sha256"
8
-	"testing"
9
-	"time"
10
-
11
-	"github.com/9seconds/mtg/v2/mtglib"
12
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
13
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
14
-	"github.com/stretchr/testify/suite"
15
-)
16
-
17
-type WelcomeTestSuite struct {
18
-	suite.Suite
19
-
20
-	h      *faketls.ClientHello
21
-	buf    *bytes.Buffer
22
-	secret mtglib.Secret
23
-}
24
-
25
-func (suite *WelcomeTestSuite) SetupTest() {
26
-	suite.h = &faketls.ClientHello{
27
-		Time:        time.Now(),
28
-		Host:        "google.com",
29
-		CipherSuite: 4867,
30
-		SessionID:   make([]byte, 32),
31
-	}
32
-
33
-	_, err := rand.Read(suite.h.SessionID) //nolint: staticcheck
34
-	suite.NoError(err)
35
-
36
-	_, err = rand.Read(suite.h.Random[:]) //nolint: staticcheck
37
-	suite.NoError(err)
38
-
39
-	suite.buf = &bytes.Buffer{}
40
-
41
-	suite.secret = mtglib.GenerateSecret("google.com")
42
-}
43
-
44
-func (suite *WelcomeTestSuite) TestOk() {
45
-	suite.NoError(faketls.SendWelcomePacket(suite.buf, suite.secret.Key[:], *suite.h))
46
-
47
-	welcomePacket := []byte{}
48
-	welcomePacket = append(welcomePacket, suite.buf.Bytes()...)
49
-
50
-	rec := record.AcquireRecord()
51
-	defer record.ReleaseRecord(rec)
52
-
53
-	suite.NoError(rec.Read(suite.buf))
54
-	suite.Equal(record.TypeHandshake, rec.Type)
55
-	suite.Equal(record.Version12, rec.Version)
56
-
57
-	suite.NoError(rec.Read(suite.buf))
58
-	suite.Equal(record.TypeChangeCipherSpec, rec.Type)
59
-	suite.Equal(record.Version12, rec.Version)
60
-
61
-	suite.NoError(rec.Read(suite.buf))
62
-	suite.Equal(record.TypeApplicationData, rec.Type)
63
-	suite.Equal(record.Version12, rec.Version)
64
-	suite.Empty(suite.buf.Bytes())
65
-
66
-	random := make([]byte, 32)
67
-	copy(random, welcomePacket[11:])
68
-
69
-	empty := make([]byte, 32)
70
-	copy(welcomePacket[11:], empty)
71
-
72
-	mac := hmac.New(sha256.New, suite.secret.Key[:])
73
-	mac.Write(suite.h.Random[:])
74
-	mac.Write(welcomePacket)
75
-
76
-	suite.Equal(random, mac.Sum(nil))
77
-}
78
-
79
-func TestWelcome(t *testing.T) {
80
-	t.Parallel()
81
-	suite.Run(t, &WelcomeTestSuite{})
82
-}

+ 86
- 0
mtglib/internal/tls/conn.go Datei anzeigen

@@ -0,0 +1,86 @@
1
+package tls
2
+
3
+import (
4
+	"bufio"
5
+	"bytes"
6
+
7
+	"github.com/9seconds/mtg/v2/essentials"
8
+)
9
+
10
+const (
11
+	SizeRecordType = 1
12
+	SizeVersion    = 2
13
+	SizeSize       = 2
14
+	SizeHeader     = SizeRecordType + SizeVersion + SizeSize
15
+
16
+	MaxRecordSize        = 16384
17
+	MaxRecordPayloadSize = MaxRecordSize - SizeHeader
18
+	DefaultBufferSize    = 4096
19
+
20
+	TypeChangeCipherSpec = 0x14
21
+	TypeHandshake        = 0x16
22
+	TypeApplicationData  = 0x17
23
+)
24
+
25
+// TLS 1.2 is used for both TLS 1.2 and 1.3
26
+var TLSVersion = [SizeVersion]byte{3, 3}
27
+
28
+// Conn presents an established TLS 1.3 connection, after handshake
29
+type Conn struct {
30
+	essentials.Conn
31
+
32
+	p *connPayload
33
+}
34
+
35
+type connPayload struct {
36
+	readBuf      bytes.Buffer
37
+	writeBuf     bytes.Buffer
38
+	connBuffered *bufio.Reader
39
+	read         bool
40
+	write        bool
41
+}
42
+
43
+func (c Conn) Write(p []byte) (int, error) {
44
+	if !c.p.write {
45
+		return c.Conn.Write(p)
46
+	}
47
+
48
+	return len(p), WriteRecord(c.Conn, p)
49
+}
50
+
51
+func (c Conn) Read(p []byte) (int, error) {
52
+	if !c.p.read {
53
+		return c.Conn.Read(p)
54
+	}
55
+
56
+	for {
57
+		if n, err := c.p.readBuf.Read(p); err == nil {
58
+			return n, nil
59
+		}
60
+
61
+		recordType, _, err := ReadRecord(c.p.connBuffered, &c.p.readBuf)
62
+		if err != nil {
63
+			return 0, err
64
+		}
65
+
66
+		if recordType != TypeApplicationData {
67
+			c.p.readBuf.Reset()
68
+		}
69
+	}
70
+}
71
+
72
+func New(conn essentials.Conn, read, write bool) Conn {
73
+	newConn := Conn{
74
+		Conn: conn,
75
+		p: &connPayload{
76
+			connBuffered: bufio.NewReaderSize(conn, DefaultBufferSize),
77
+			read:         read,
78
+			write:        write,
79
+		},
80
+	}
81
+
82
+	newConn.p.readBuf.Grow(DefaultBufferSize)
83
+	newConn.p.writeBuf.Grow(DefaultBufferSize)
84
+
85
+	return newConn
86
+}

+ 160
- 0
mtglib/internal/tls/conn_test.go Datei anzeigen

@@ -0,0 +1,160 @@
1
+package tls
2
+
3
+import (
4
+	"io"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/testlib"
8
+	"github.com/stretchr/testify/mock"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type ConnTestSuite struct {
13
+	suite.Suite
14
+
15
+	connMock *testlib.EssentialsConnMock
16
+}
17
+
18
+func (suite *ConnTestSuite) SetupTest() {
19
+	suite.connMock = &testlib.EssentialsConnMock{}
20
+}
21
+
22
+func (suite *ConnTestSuite) TearDownTest() {
23
+	suite.connMock.AssertExpectations(suite.T())
24
+}
25
+
26
+func (suite *ConnTestSuite) feedRead(raw []byte) {
27
+	suite.connMock.
28
+		On("Read", mock.AnythingOfType("[]uint8")).
29
+		Run(func(args mock.Arguments) {
30
+			copy(args.Get(0).([]byte), raw)
31
+		}).
32
+		Return(len(raw), nil).
33
+		Once()
34
+	suite.connMock.
35
+		On("Read", mock.AnythingOfType("[]uint8")).
36
+		Return(0, io.EOF).
37
+		Maybe()
38
+}
39
+
40
+func (suite *ConnTestSuite) TestReadTLSEnabled() {
41
+	payload := []byte("hello world")
42
+	suite.feedRead(MakeTLSRecord(0x17, payload))
43
+
44
+	conn := New(suite.connMock, true, false)
45
+
46
+	buf := make([]byte, 128)
47
+	n, err := conn.Read(buf)
48
+
49
+	suite.NoError(err)
50
+	suite.Equal(payload, buf[:n])
51
+}
52
+
53
+func (suite *ConnTestSuite) TestReadTLSSkipsNonApplicationData() {
54
+	raw := append(
55
+		MakeTLSRecord(0x14, []byte{1}),
56
+		MakeTLSRecord(0x17, []byte("real data"))...,
57
+	)
58
+	suite.feedRead(raw)
59
+
60
+	conn := New(suite.connMock, true, false)
61
+
62
+	buf := make([]byte, 128)
63
+	n, err := conn.Read(buf)
64
+
65
+	suite.NoError(err)
66
+	suite.Equal([]byte("real data"), buf[:n])
67
+}
68
+
69
+func (suite *ConnTestSuite) TestReadTLSMultipleRecords() {
70
+	raw := append(
71
+		MakeTLSRecord(0x17, []byte("first")),
72
+		MakeTLSRecord(0x17, []byte("second"))...,
73
+	)
74
+	suite.feedRead(raw)
75
+
76
+	conn := New(suite.connMock, true, false)
77
+	buf := make([]byte, 128)
78
+
79
+	n, err := conn.Read(buf)
80
+	suite.NoError(err)
81
+	suite.Equal([]byte("first"), buf[:n])
82
+
83
+	n, err = conn.Read(buf)
84
+	suite.NoError(err)
85
+	suite.Equal([]byte("second"), buf[:n])
86
+}
87
+
88
+func (suite *ConnTestSuite) TestReadTLSSmallBuffer() {
89
+	payload := []byte("hello world, this is a longer payload")
90
+	suite.feedRead(MakeTLSRecord(0x17, payload))
91
+
92
+	conn := New(suite.connMock, true, false)
93
+
94
+	small := make([]byte, 5)
95
+	n, err := conn.Read(small)
96
+	suite.NoError(err)
97
+	suite.Equal(payload[:5], small[:n])
98
+
99
+	rest := make([]byte, 128)
100
+	n, err = conn.Read(rest)
101
+	suite.NoError(err)
102
+	suite.Equal(payload[5:], rest[:n])
103
+}
104
+
105
+func (suite *ConnTestSuite) TestReadPassthrough() {
106
+	data := []byte("raw bytes")
107
+
108
+	suite.connMock.
109
+		On("Read", mock.AnythingOfType("[]uint8")).
110
+		Run(func(args mock.Arguments) {
111
+			copy(args.Get(0).([]byte), data)
112
+		}).
113
+		Return(len(data), nil).
114
+		Once()
115
+
116
+	conn := New(suite.connMock, false, false)
117
+
118
+	buf := make([]byte, 128)
119
+	n, err := conn.Read(buf)
120
+
121
+	suite.NoError(err)
122
+	suite.Equal(data, buf[:n])
123
+}
124
+
125
+func (suite *ConnTestSuite) TestWritePassthrough() {
126
+	data := []byte("outgoing data")
127
+
128
+	suite.connMock.
129
+		On("Write", mock.AnythingOfType("[]uint8")).
130
+		Return(len(data), nil).
131
+		Once()
132
+
133
+	conn := New(suite.connMock, false, false)
134
+
135
+	n, err := conn.Write(data)
136
+
137
+	suite.NoError(err)
138
+	suite.Equal(len(data), n)
139
+}
140
+
141
+func (suite *ConnTestSuite) TestWriteTLSEnabled() {
142
+	data := []byte("outgoing data")
143
+
144
+	suite.connMock.
145
+		On("Write", mock.AnythingOfType("[]uint8")).
146
+		Return(len(data), nil).
147
+		Once()
148
+
149
+	conn := New(suite.connMock, false, true)
150
+
151
+	n, err := conn.Write(data)
152
+
153
+	suite.NoError(err)
154
+	suite.Equal(len(data), n)
155
+}
156
+
157
+func TestConn(t *testing.T) {
158
+	t.Parallel()
159
+	suite.Run(t, &ConnTestSuite{})
160
+}

+ 21
- 0
mtglib/internal/tls/fake/bytes_pool.go Datei anzeigen

@@ -0,0 +1,21 @@
1
+package fake
2
+
3
+import (
4
+	"bytes"
5
+	"sync"
6
+)
7
+
8
+var bytesPool = sync.Pool{
9
+	New: func() any {
10
+		return &bytes.Buffer{}
11
+	},
12
+}
13
+
14
+func acquireBuffer() *bytes.Buffer {
15
+	return bytesPool.Get().(*bytes.Buffer)
16
+}
17
+
18
+func releaseBuffer(b *bytes.Buffer) {
19
+	b.Reset()
20
+	bytesPool.Put(b)
21
+}

+ 309
- 0
mtglib/internal/tls/fake/client_side.go Datei anzeigen

@@ -0,0 +1,309 @@
1
+package fake
2
+
3
+import (
4
+	"bytes"
5
+	"crypto/hmac"
6
+	"crypto/sha256"
7
+	"crypto/subtle"
8
+	"encoding/binary"
9
+	"fmt"
10
+	"io"
11
+	"net"
12
+	"slices"
13
+	"time"
14
+
15
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
16
+)
17
+
18
+const (
19
+	TypeHandshakeClient = 0x01
20
+
21
+	RandomLen = 32
22
+	// record_type(1) + version(2) + size(2) + handshake_type(1) + uint24_length(3) + client_version(2)
23
+	RandomOffset = 1 + 2 + 2 + 1 + 3 + 2
24
+
25
+	sniDNSNamesListType = 0
26
+)
27
+
28
+var (
29
+	emptyRandom = [RandomLen]byte{}
30
+	extTypeSNI  = [2]byte{}
31
+)
32
+
33
+type ClientHello struct {
34
+	Random      [RandomLen]byte
35
+	SessionID   []byte
36
+	CipherSuite uint16
37
+}
38
+
39
+func ReadClientHello(
40
+	conn net.Conn,
41
+	secret []byte,
42
+	hostname string,
43
+	tolerateTimeSkewness time.Duration,
44
+) (*ClientHello, error) {
45
+	if err := conn.SetReadDeadline(time.Now().Add(ClientHelloReadTimeout)); err != nil {
46
+		return nil, fmt.Errorf("cannot set read deadline: %w", err)
47
+	}
48
+	defer conn.SetReadDeadline(resetDeadline) //nolint: errcheck
49
+
50
+	// This is how FakeTLS is organized:
51
+	//  1. We create sha256 HMAC with a given secret
52
+	//  2. We dump there a whole TLS frame except of the fact that random
53
+	//     is filled with all zeroes
54
+	//  3. Digest is computed. This digest should be XORed with
55
+	//     original client random
56
+	//  4. New digest should be all 0 except of last 4 bytes
57
+	//  5. Last 4 bytes are little endian uint32 of UNIX timestamp when
58
+	//     this message was created.
59
+	handshakeCopyBuf := &bytes.Buffer{}
60
+	reader := io.TeeReader(conn, handshakeCopyBuf)
61
+
62
+	reader, err := parseTLSHeader(reader)
63
+	if err != nil {
64
+		return nil, fmt.Errorf("cannot parse tls header: %w", err)
65
+	}
66
+
67
+	reader, err = parseHandshakeHeader(reader)
68
+	if err != nil {
69
+		return nil, fmt.Errorf("cannot parse handshake header: %w", err)
70
+	}
71
+
72
+	hello, err := parseHandshake(reader)
73
+	if err != nil {
74
+		return nil, fmt.Errorf("cannot parse handshake: %w", err)
75
+	}
76
+
77
+	sniHostnames, err := parseSNI(reader)
78
+	if err != nil {
79
+		return nil, fmt.Errorf("cannot parse SNI: %w", err)
80
+	}
81
+
82
+	if !slices.Contains(sniHostnames, hostname) {
83
+		return nil, fmt.Errorf("cannot find %s in %v", hostname, sniHostnames)
84
+	}
85
+
86
+	digest := hmac.New(sha256.New, secret)
87
+	// we write a copy of the handshake with client random all nullified.
88
+	digest.Write(handshakeCopyBuf.Next(RandomOffset))
89
+	handshakeCopyBuf.Next(RandomLen)
90
+	digest.Write(emptyRandom[:])
91
+	digest.Write(handshakeCopyBuf.Bytes())
92
+
93
+	computed := digest.Sum(nil)
94
+
95
+	for i := range RandomLen {
96
+		computed[i] ^= hello.Random[i]
97
+	}
98
+
99
+	if subtle.ConstantTimeCompare(emptyRandom[:RandomLen-4], computed[:RandomLen-4]) != 1 {
100
+		return nil, ErrBadDigest
101
+	}
102
+
103
+	timestamp := int64(binary.LittleEndian.Uint32(computed[RandomLen-4:]))
104
+	createdAt := time.Unix(timestamp, 0)
105
+
106
+	if tdiff := time.Since(createdAt).Abs(); tdiff > tolerateTimeSkewness {
107
+		return nil, fmt.Errorf("timestamp %q is too old %s", createdAt, tdiff)
108
+	}
109
+
110
+	return hello, nil
111
+}
112
+
113
+func parseTLSHeader(r io.Reader) (io.Reader, error) {
114
+	// record_type(1) + version(2) + size(2)
115
+	//   16 - type is 0x16 (handshake record)
116
+	//   03 01 - protocol version is "3,1" (also known as TLS 1.0)
117
+	//   00 f8 - 0xF8 (248) bytes of handshake message follows
118
+	header := [1 + 2 + 2]byte{}
119
+
120
+	if _, err := io.ReadFull(r, header[:]); err != nil {
121
+		return nil, fmt.Errorf("cannot read record header: %w", err)
122
+	}
123
+
124
+	if header[0] != tls.TypeHandshake {
125
+		return nil, fmt.Errorf("unexpected record type %#x", header[0])
126
+	}
127
+
128
+	if header[1] != 3 || header[2] != 1 {
129
+		return nil, fmt.Errorf("unexpected protocol version %#x %#x", header[1], header[2])
130
+	}
131
+
132
+	length := int64(binary.BigEndian.Uint16(header[3:]))
133
+	buf := &bytes.Buffer{}
134
+
135
+	_, err := io.CopyN(buf, r, length)
136
+
137
+	return buf, err
138
+}
139
+
140
+func parseHandshakeHeader(r io.Reader) (io.Reader, error) {
141
+	// type(1) + size(3 / uint24)
142
+	// 01 - handshake message type 0x01 (client hello)
143
+	// 00 00 f4 - 0xF4 (244) bytes of client hello data follows
144
+	header := [1 + 3]byte{}
145
+
146
+	if _, err := io.ReadFull(r, header[:]); err != nil {
147
+		return nil, fmt.Errorf("cannot read handshake header: %w", err)
148
+	}
149
+
150
+	if header[0] != TypeHandshakeClient {
151
+		return nil, fmt.Errorf("incorrect handshake type: %#x", header[0])
152
+	}
153
+
154
+	// unfortunately there is not uint24 in golang, so we just reust header
155
+	header[0] = 0
156
+
157
+	length := int64(binary.BigEndian.Uint32(header[:]))
158
+	buf := &bytes.Buffer{}
159
+
160
+	_, err := io.CopyN(buf, r, length)
161
+
162
+	return buf, err
163
+}
164
+
165
+func parseHandshake(r io.Reader) (*ClientHello, error) {
166
+	//  A protocol version of "3,3" (meaning TLS 1.2) is given.
167
+	header := [2]byte{}
168
+
169
+	if _, err := io.ReadFull(r, header[:]); err != nil {
170
+		return nil, fmt.Errorf("cannot read client version: %w", err)
171
+	}
172
+
173
+	hello := &ClientHello{}
174
+
175
+	if _, err := io.ReadFull(r, hello.Random[:]); err != nil {
176
+		return nil, fmt.Errorf("cannot read client random: %w", err)
177
+	}
178
+
179
+	if _, err := io.ReadFull(r, header[:1]); err != nil {
180
+		return nil, fmt.Errorf("cannot read session ID length: %w", err)
181
+	}
182
+
183
+	hello.SessionID = make([]byte, int(header[0]))
184
+
185
+	if _, err := io.ReadFull(r, hello.SessionID); err != nil {
186
+		return nil, fmt.Errorf("cannot read session id: %w", err)
187
+	}
188
+
189
+	if _, err := io.ReadFull(r, header[:]); err != nil {
190
+		return nil, fmt.Errorf("cannot read cipher suite length: %w", err)
191
+	}
192
+
193
+	cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
194
+
195
+	// we do not care about picking up any cipher. we pick the first one,
196
+	// so it is always should be present.
197
+	if _, err := io.ReadFull(r, header[:]); err != nil {
198
+		return nil, fmt.Errorf("cannot read first cipher suite: %w", err)
199
+	}
200
+
201
+	hello.CipherSuite = binary.BigEndian.Uint16(header[:])
202
+
203
+	if _, err := io.CopyN(io.Discard, r, cipherSuiteLen-2); err != nil {
204
+		return nil, fmt.Errorf("cannot skip remaining cipher suites: %w", err)
205
+	}
206
+
207
+	if _, err := io.ReadFull(r, header[:1]); err != nil {
208
+		return nil, fmt.Errorf("cannot read compression methods length: %w", err)
209
+	}
210
+
211
+	if _, err := io.CopyN(io.Discard, r, int64(header[0])); err != nil {
212
+		return nil, fmt.Errorf("cannot skip compression methods: %w", err)
213
+	}
214
+
215
+	return hello, nil
216
+}
217
+
218
+func parseSNI(r io.Reader) ([]string, error) {
219
+	header := [2]byte{}
220
+
221
+	if _, err := io.ReadFull(r, header[:]); err != nil {
222
+		return nil, fmt.Errorf("cannot read length of TLS extensions: %w", err)
223
+	}
224
+
225
+	extensionsLength := int64(binary.BigEndian.Uint16(header[:]))
226
+	buf := &bytes.Buffer{}
227
+	buf.Grow(int(extensionsLength))
228
+
229
+	if _, err := io.CopyN(buf, r, extensionsLength); err != nil {
230
+		return nil, fmt.Errorf("cannot read extensions: %w", err)
231
+	}
232
+
233
+	for buf.Len() > 0 {
234
+		// 00 00 - assigned value for extension "server name"
235
+		// 00 18 - 0x18 (24) bytes of "server name" extension data follows
236
+		// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
237
+		// 00 - list entry is type 0x00 "DNS hostname"
238
+		// 00 13 - 0x13 (19) bytes of hostname follows
239
+		// 65 78 61 ... 6e 65 74 - "example.ulfheim.net"
240
+
241
+		// 00 00 - assigned value for extension "server name"
242
+		extTypeB := buf.Next(2)
243
+		if len(extTypeB) != 2 {
244
+			return nil, fmt.Errorf("cannot read extension type: %v", extTypeB)
245
+		}
246
+
247
+		// 00 18 - 0x18 (24) bytes of "server name" extension data follows
248
+		lengthB := buf.Next(2)
249
+		if len(lengthB) != 2 {
250
+			return nil, fmt.Errorf("cannot read extension %v length: %v", extTypeB, lengthB)
251
+		}
252
+		length := int(binary.BigEndian.Uint16(lengthB))
253
+
254
+		extDataB := buf.Next(length)
255
+		if len(extDataB) != length {
256
+			return nil, fmt.Errorf("cannot read extension %v data: len %d != %d", extTypeB, length, len(extDataB))
257
+		}
258
+
259
+		if !bytes.Equal(extTypeB, extTypeSNI[:]) {
260
+			continue
261
+		}
262
+
263
+		buf.Reset()
264
+		buf.Write(extDataB)
265
+
266
+		// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
267
+		lengthB = buf.Next(2)
268
+		if len(lengthB) != 2 {
269
+			return nil, fmt.Errorf("cannot read the length of the SNI record: %v", lengthB)
270
+		}
271
+
272
+		length = int(binary.BigEndian.Uint16(lengthB))
273
+		if length == 0 {
274
+			return nil, nil
275
+		}
276
+
277
+		listType, err := buf.ReadByte()
278
+		if err != nil {
279
+			return nil, fmt.Errorf("cannot read SNI list type: %w", err)
280
+		}
281
+
282
+		// 00 - list entry is type 0x00 "DNS hostname"
283
+		if listType != sniDNSNamesListType {
284
+			return nil, fmt.Errorf("incorrect SNI list type %#x", listType)
285
+		}
286
+
287
+		names := []string{}
288
+
289
+		for buf.Len() > 0 {
290
+			// 00 13 - 0x13 (19) bytes of hostname follows
291
+			lengthB = buf.Next(2)
292
+			if len(lengthB) != 2 {
293
+				return nil, fmt.Errorf("incorrect length of the hostname: %v", lengthB)
294
+			}
295
+			length = int(binary.BigEndian.Uint16(lengthB))
296
+
297
+			name := buf.Next(length)
298
+			if len(name) != length {
299
+				return nil, fmt.Errorf("incorrect length of SNI hostname: len %d != %d", length, len(name))
300
+			}
301
+
302
+			names = append(names, string(name))
303
+		}
304
+
305
+		return names, nil
306
+	}
307
+
308
+	return nil, nil
309
+}

+ 48
- 0
mtglib/internal/tls/fake/client_side_fuzz_test.go Datei anzeigen

@@ -0,0 +1,48 @@
1
+package fake_test
2
+
3
+import (
4
+	"bytes"
5
+	"testing"
6
+	"time"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/testlib"
9
+	"github.com/9seconds/mtg/v2/mtglib"
10
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
11
+	"github.com/stretchr/testify/assert"
12
+	"github.com/stretchr/testify/mock"
13
+	"github.com/stretchr/testify/require"
14
+)
15
+
16
+type connMock struct {
17
+	testlib.EssentialsConnMock
18
+
19
+	readBuf *bytes.Buffer
20
+}
21
+
22
+func (f *connMock) Read(p []byte) (int, error) {
23
+	return f.readBuf.Read(p)
24
+}
25
+
26
+func FuzzReadClientHello(f *testing.F) {
27
+	seed := [248]byte{}
28
+
29
+	secret, err := mtglib.ParseSecret(
30
+		"ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
31
+	)
32
+	require.NoError(f, err)
33
+
34
+	f.Add(seed[:])
35
+
36
+	f.Fuzz(func(t *testing.T, value []byte) {
37
+		r := &connMock{
38
+			readBuf: bytes.NewBuffer(value),
39
+		}
40
+		r.
41
+			On("SetReadDeadline", mock.AnythingOfType("time.Time")).
42
+			Twice().
43
+			Return(nil)
44
+
45
+		_, err := fake.ReadClientHello(r, secret.Key[:], secret.Host, time.Hour)
46
+		assert.Error(t, err)
47
+	})
48
+}

+ 153
- 0
mtglib/internal/tls/fake/client_side_snapshot_test.go Datei anzeigen

@@ -0,0 +1,153 @@
1
+package fake_test
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/base64"
6
+	"encoding/json"
7
+	"os"
8
+	"path/filepath"
9
+	"strings"
10
+	"testing"
11
+
12
+	"github.com/9seconds/mtg/v2/mtglib"
13
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
14
+	"github.com/stretchr/testify/assert"
15
+	"github.com/stretchr/testify/mock"
16
+	"github.com/stretchr/testify/require"
17
+	"github.com/stretchr/testify/suite"
18
+)
19
+
20
+type clientHelloSnapshot struct {
21
+	Time        int    `json:"time"`
22
+	Random      string `json:"random"`
23
+	SessionID   string `json:"sessionId"`
24
+	Host        string `json:"host"`
25
+	CipherSuite int    `json:"cipherSuite"`
26
+	Full        string `json:"full"`
27
+}
28
+
29
+func (c clientHelloSnapshot) GetRandom() []byte {
30
+	data, _ := base64.StdEncoding.DecodeString(c.Random)
31
+
32
+	return data
33
+}
34
+
35
+func (c clientHelloSnapshot) GetSessionID() []byte {
36
+	data, _ := base64.StdEncoding.DecodeString(c.SessionID)
37
+
38
+	return data
39
+}
40
+
41
+func (c clientHelloSnapshot) GetCipherSuite() uint16 {
42
+	return uint16(c.CipherSuite)
43
+}
44
+
45
+func (c clientHelloSnapshot) GetFull() []byte {
46
+	data, _ := base64.StdEncoding.DecodeString(c.Full)
47
+
48
+	return data
49
+}
50
+
51
+type ParseClientHelloSnapshotTestSuite struct {
52
+	suite.Suite
53
+
54
+	secret mtglib.Secret
55
+}
56
+
57
+func (suite *ParseClientHelloSnapshotTestSuite) SetupSuite() {
58
+	parsed, err := mtglib.ParseSecret(
59
+		"ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
60
+	)
61
+	require.NoError(suite.T(), err)
62
+
63
+	suite.secret = parsed
64
+}
65
+
66
+func (suite *ParseClientHelloSnapshotTestSuite) makeConn(data []byte) *parseClientHelloConnMock {
67
+	readBuf := &bytes.Buffer{}
68
+	readBuf.Write(data)
69
+
70
+	connMock := &parseClientHelloConnMock{
71
+		readBuf: readBuf,
72
+	}
73
+
74
+	connMock.
75
+		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
76
+		Twice().
77
+		Return(nil)
78
+
79
+	return connMock
80
+}
81
+
82
+func (suite *ParseClientHelloSnapshotTestSuite) TestSnapshotOk() {
83
+	files, err := os.ReadDir("testdata")
84
+	require.NoError(suite.T(), err)
85
+
86
+	for _, v := range files {
87
+		if !strings.HasPrefix(v.Name(), "client-hello-ok") {
88
+			continue
89
+		}
90
+
91
+		path := filepath.Join("testdata", v.Name())
92
+
93
+		suite.T().Run(v.Name(), func(t *testing.T) {
94
+			fileData, err := os.ReadFile(path)
95
+			assert.NoError(t, err)
96
+
97
+			snapshot := &clientHelloSnapshot{}
98
+			assert.NoError(t, json.Unmarshal(fileData, snapshot))
99
+
100
+			connMock := suite.makeConn(snapshot.GetFull())
101
+			defer connMock.AssertExpectations(t)
102
+
103
+			hello, err := fake.ReadClientHello(
104
+				connMock,
105
+				suite.secret.Key[:],
106
+				suite.secret.Host,
107
+				TolerateTime,
108
+			)
109
+			require.NoError(t, err)
110
+
111
+			assert.Equal(t, snapshot.GetRandom(), hello.Random[:])
112
+			assert.Equal(t, snapshot.GetSessionID(), hello.SessionID)
113
+			assert.Equal(t, snapshot.GetCipherSuite(), hello.CipherSuite)
114
+		})
115
+	}
116
+}
117
+
118
+func (suite *ParseClientHelloSnapshotTestSuite) TestSnapshotBad() {
119
+	files, err := os.ReadDir("testdata")
120
+	require.NoError(suite.T(), err)
121
+
122
+	for _, v := range files {
123
+		if !strings.HasPrefix(v.Name(), "client-hello-bad") {
124
+			continue
125
+		}
126
+
127
+		path := filepath.Join("testdata", v.Name())
128
+
129
+		suite.T().Run(v.Name(), func(t *testing.T) {
130
+			fileData, err := os.ReadFile(path)
131
+			assert.NoError(t, err)
132
+
133
+			snapshot := &clientHelloSnapshot{}
134
+			assert.NoError(t, json.Unmarshal(fileData, snapshot))
135
+
136
+			connMock := suite.makeConn(snapshot.GetFull())
137
+			defer connMock.AssertExpectations(t)
138
+
139
+			_, err = fake.ReadClientHello(
140
+				connMock,
141
+				suite.secret.Key[:],
142
+				suite.secret.Host,
143
+				TolerateTime,
144
+			)
145
+			assert.ErrorIs(t, err, fake.ErrBadDigest)
146
+		})
147
+	}
148
+}
149
+
150
+func TestParseClientHelloSnapshot(t *testing.T) {
151
+	t.Parallel()
152
+	suite.Run(t, &ParseClientHelloSnapshotTestSuite{})
153
+}

+ 395
- 0
mtglib/internal/tls/fake/client_side_test.go Datei anzeigen

@@ -0,0 +1,395 @@
1
+package fake_test
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/binary"
6
+	"errors"
7
+	"io"
8
+	"testing"
9
+	"time"
10
+
11
+	"github.com/9seconds/mtg/v2/internal/testlib"
12
+	"github.com/9seconds/mtg/v2/mtglib"
13
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
14
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
15
+	"github.com/stretchr/testify/mock"
16
+	"github.com/stretchr/testify/require"
17
+	"github.com/stretchr/testify/suite"
18
+)
19
+
20
+const (
21
+	TolerateTime = 365 * 30 * 24 * time.Hour
22
+)
23
+
24
+type parseClientHelloConnMock struct {
25
+	testlib.EssentialsConnMock
26
+
27
+	readBuf *bytes.Buffer
28
+}
29
+
30
+func (m *parseClientHelloConnMock) Read(p []byte) (int, error) {
31
+	return m.readBuf.Read(p)
32
+}
33
+
34
+type ParseClientHelloTestSuite struct {
35
+	suite.Suite
36
+
37
+	secret   mtglib.Secret
38
+	readBuf  *bytes.Buffer
39
+	connMock *parseClientHelloConnMock
40
+}
41
+
42
+func (suite *ParseClientHelloTestSuite) SetupSuite() {
43
+	parsed, err := mtglib.ParseSecret("ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d")
44
+	require.NoError(suite.T(), err)
45
+
46
+	suite.secret = parsed
47
+}
48
+
49
+func (suite *ParseClientHelloTestSuite) SetupTest() {
50
+	suite.readBuf = &bytes.Buffer{}
51
+	suite.connMock = &parseClientHelloConnMock{
52
+		readBuf: suite.readBuf,
53
+	}
54
+
55
+	suite.connMock.
56
+		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
57
+		Twice().
58
+		Return(nil)
59
+}
60
+
61
+func (suite *ParseClientHelloTestSuite) TearDownTest() {
62
+	suite.connMock.AssertExpectations(suite.T())
63
+}
64
+
65
+type ParseClientHello_TLSHeaderTestSuite struct {
66
+	ParseClientHelloTestSuite
67
+}
68
+
69
+func (suite *ParseClientHello_TLSHeaderTestSuite) TestEmpty() {
70
+	suite.connMock.ExpectedCalls = []*mock.Call{}
71
+	suite.connMock.
72
+		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
73
+		Once().
74
+		Return(errors.New("fail"))
75
+
76
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
77
+	suite.ErrorContains(err, "fail")
78
+}
79
+
80
+func (suite *ParseClientHello_TLSHeaderTestSuite) TestNothing() {
81
+	suite.connMock.ExpectedCalls = []*mock.Call{}
82
+	suite.connMock.
83
+		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
84
+		Twice().
85
+		Return(nil)
86
+
87
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
88
+	suite.ErrorIs(err, io.EOF)
89
+}
90
+
91
+func (suite *ParseClientHello_TLSHeaderTestSuite) TestUnknownRecord() {
92
+	suite.readBuf.Write([]byte{
93
+		10,
94
+		3, 3,
95
+		0, 0,
96
+	})
97
+	suite.readBuf.WriteByte(10)
98
+
99
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
100
+	suite.ErrorContains(err, "unexpected record type 0xa")
101
+}
102
+
103
+func (suite *ParseClientHello_TLSHeaderTestSuite) TestUnknownProtocolVersion() {
104
+	suite.readBuf.Write([]byte{
105
+		tls.TypeHandshake,
106
+		3, 3,
107
+		0, 0,
108
+	})
109
+
110
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
111
+	suite.ErrorContains(err, "unexpected protocol version")
112
+}
113
+
114
+func (suite *ParseClientHello_TLSHeaderTestSuite) TestCannotReadRestOfRecord() {
115
+	suite.readBuf.Write([]byte{
116
+		tls.TypeHandshake,
117
+		3, 1,
118
+		0, 10,
119
+	})
120
+
121
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
122
+	suite.ErrorIs(err, io.EOF)
123
+}
124
+
125
+type ParseClientHelloHandshakeTestSuite struct {
126
+	ParseClientHelloTestSuite
127
+}
128
+
129
+func (suite *ParseClientHelloHandshakeTestSuite) SetupTest() {
130
+	suite.ParseClientHelloTestSuite.SetupTest()
131
+
132
+	suite.readBuf.Write([]byte{
133
+		tls.TypeHandshake,
134
+		3, 1,
135
+		0,
136
+	})
137
+}
138
+
139
+func (suite *ParseClientHelloHandshakeTestSuite) TestCannotReadHeader() {
140
+	suite.readBuf.Write([]byte{
141
+		1,
142
+		10,
143
+	})
144
+
145
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
146
+	suite.ErrorContains(err, "cannot read handshake header")
147
+}
148
+
149
+func (suite *ParseClientHelloHandshakeTestSuite) TestIncorrectHandshakeType() {
150
+	suite.readBuf.Write([]byte{
151
+		4,
152
+		10, 0, 0, 0,
153
+	})
154
+
155
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
156
+	suite.ErrorContains(err, "incorrect handshake type")
157
+}
158
+
159
+func (suite *ParseClientHelloHandshakeTestSuite) TestCannotReadHandshake() {
160
+	suite.readBuf.Write([]byte{
161
+		4 + 3,
162
+		10, 0, 0, 0,
163
+	})
164
+
165
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
166
+	suite.ErrorIs(err, io.EOF)
167
+}
168
+
169
+type ParseClientHelloHandshakeBodyTestSuite struct {
170
+	ParseClientHelloTestSuite
171
+}
172
+
173
+func (suite *ParseClientHelloHandshakeBodyTestSuite) SetupTest() {
174
+	suite.ParseClientHelloTestSuite.SetupTest()
175
+
176
+	suite.readBuf.Write([]byte{
177
+		tls.TypeHandshake,
178
+		3, 1,
179
+		0,
180
+	})
181
+}
182
+
183
+func (suite *ParseClientHelloHandshakeBodyTestSuite) writeBody(body []byte) {
184
+	suite.readBuf.WriteByte(byte(4 + len(body)))
185
+	suite.readBuf.Write([]byte{
186
+		fake.TypeHandshakeClient,
187
+		0, 0, byte(len(body)),
188
+	})
189
+	suite.readBuf.Write(body)
190
+}
191
+
192
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadVersion() {
193
+	suite.writeBody(nil)
194
+
195
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
196
+	suite.ErrorContains(err, "cannot read client version")
197
+}
198
+
199
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadRandom() {
200
+	suite.writeBody([]byte{3, 3})
201
+
202
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
203
+	suite.ErrorContains(err, "cannot read client random")
204
+}
205
+
206
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadSessionIDLength() {
207
+	body := make([]byte, 2+fake.RandomLen)
208
+
209
+	suite.writeBody(body)
210
+
211
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
212
+	suite.ErrorContains(err, "cannot read session ID length")
213
+}
214
+
215
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadSessionID() {
216
+	body := make([]byte, 2+fake.RandomLen+1)
217
+	body[2+fake.RandomLen] = 32
218
+
219
+	suite.writeBody(body)
220
+
221
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
222
+	suite.ErrorContains(err, "cannot read session id")
223
+}
224
+
225
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCipherSuiteLength() {
226
+	body := make([]byte, 2+fake.RandomLen+1)
227
+
228
+	suite.writeBody(body)
229
+
230
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
231
+	suite.ErrorContains(err, "cannot read cipher suite length")
232
+}
233
+
234
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() {
235
+	body := make([]byte, 2+fake.RandomLen+1+2)
236
+
237
+	suite.writeBody(body)
238
+
239
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
240
+	suite.ErrorContains(err, "cannot read first cipher suite")
241
+}
242
+
243
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() {
244
+	body := make([]byte, 2+fake.RandomLen+1+2+2)
245
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 4)
246
+
247
+	suite.writeBody(body)
248
+
249
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
250
+	suite.ErrorContains(err, "cannot skip remaining cipher suites")
251
+}
252
+
253
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
254
+	body := make([]byte, 2+fake.RandomLen+1+2+2)
255
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
256
+
257
+	suite.writeBody(body)
258
+
259
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
260
+	suite.ErrorContains(err, "cannot read compression methods length")
261
+}
262
+
263
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipCompressionMethods() {
264
+	body := make([]byte, 2+fake.RandomLen+1+2+2+1)
265
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
266
+	body[2+fake.RandomLen+1+2+2] = 1
267
+
268
+	suite.writeBody(body)
269
+
270
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
271
+	suite.ErrorContains(err, "cannot skip compression methods")
272
+}
273
+
274
+type ParseClientHelloSNITestSuite struct {
275
+	ParseClientHelloTestSuite
276
+}
277
+
278
+func (suite *ParseClientHelloSNITestSuite) SetupTest() {
279
+	suite.ParseClientHelloTestSuite.SetupTest()
280
+
281
+	suite.readBuf.Write([]byte{
282
+		tls.TypeHandshake,
283
+		3, 1,
284
+		0,
285
+	})
286
+}
287
+
288
+func (suite *ParseClientHelloSNITestSuite) writeExtensions(extensions []byte) {
289
+	handshakeBodyLen := 41 + len(extensions)
290
+
291
+	suite.readBuf.WriteByte(byte(4 + handshakeBodyLen))
292
+	suite.readBuf.Write([]byte{
293
+		fake.TypeHandshakeClient,
294
+		0, 0, byte(handshakeBodyLen),
295
+	})
296
+
297
+	// version(2) + random(32) + sessionIDLen(1) + cipherSuiteLen(2) +
298
+	// cipherSuite(2) + compressionLen(1) + compression(1) = 41
299
+	body := make([]byte, 41)
300
+	binary.BigEndian.PutUint16(body[35:], 2)
301
+	body[39] = 1
302
+
303
+	suite.readBuf.Write(body)
304
+	suite.readBuf.Write(extensions)
305
+}
306
+
307
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionsLength() {
308
+	suite.writeExtensions(nil)
309
+
310
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
311
+	suite.ErrorContains(err, "cannot read length of TLS extensions")
312
+}
313
+
314
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensions() {
315
+	suite.writeExtensions([]byte{0, 10})
316
+
317
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
318
+	suite.ErrorContains(err, "cannot read extensions")
319
+}
320
+
321
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionType() {
322
+	suite.writeExtensions([]byte{0, 1, 0xAB})
323
+
324
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
325
+	suite.ErrorContains(err, "cannot read extension type")
326
+}
327
+
328
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionLength() {
329
+	suite.writeExtensions([]byte{0, 2, 0xFF, 0xFF})
330
+
331
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
332
+	suite.ErrorContains(err, "length:")
333
+}
334
+
335
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionData() {
336
+	suite.writeExtensions([]byte{0, 4, 0xFF, 0xFF, 0, 5})
337
+
338
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
339
+	suite.ErrorContains(err, "data: len")
340
+}
341
+
342
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadSNIRecordLength() {
343
+	suite.writeExtensions([]byte{0, 5, 0, 0, 0, 1, 0xAB})
344
+
345
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
346
+	suite.ErrorContains(err, "cannot read the length of the SNI record")
347
+}
348
+
349
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadSNIListType() {
350
+	suite.writeExtensions([]byte{0, 6, 0, 0, 0, 2, 0, 1})
351
+
352
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
353
+	suite.ErrorContains(err, "cannot read SNI list type")
354
+}
355
+
356
+func (suite *ParseClientHelloSNITestSuite) TestIncorrectSNIListType() {
357
+	suite.writeExtensions([]byte{0, 7, 0, 0, 0, 3, 0, 1, 5})
358
+
359
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
360
+	suite.ErrorContains(err, "incorrect SNI list type")
361
+}
362
+
363
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadHostnameLength() {
364
+	suite.writeExtensions([]byte{0, 8, 0, 0, 0, 4, 0, 2, 0, 0xAB})
365
+
366
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
367
+	suite.ErrorContains(err, "incorrect length of the hostname")
368
+}
369
+
370
+func (suite *ParseClientHelloSNITestSuite) TestCannotReadHostname() {
371
+	suite.writeExtensions([]byte{0, 9, 0, 0, 0, 5, 0, 3, 0, 0, 5})
372
+
373
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
374
+	suite.ErrorContains(err, "incorrect length of SNI hostname")
375
+}
376
+
377
+func TestParseClientHelloTLSHeader(t *testing.T) {
378
+	t.Parallel()
379
+	suite.Run(t, &ParseClientHello_TLSHeaderTestSuite{})
380
+}
381
+
382
+func TestParseClientHelloHandshake(t *testing.T) {
383
+	t.Parallel()
384
+	suite.Run(t, &ParseClientHelloHandshakeTestSuite{})
385
+}
386
+
387
+func TestParseClientHelloHandshakeBody(t *testing.T) {
388
+	t.Parallel()
389
+	suite.Run(t, &ParseClientHelloHandshakeBodyTestSuite{})
390
+}
391
+
392
+func TestParseClientHelloSNI(t *testing.T) {
393
+	t.Parallel()
394
+	suite.Run(t, &ParseClientHelloSNITestSuite{})
395
+}

+ 16
- 0
mtglib/internal/tls/fake/init.go Datei anzeigen

@@ -0,0 +1,16 @@
1
+package fake
2
+
3
+import (
4
+	"errors"
5
+	"time"
6
+)
7
+
8
+const (
9
+	ClientHelloReadTimeout = 5 * time.Second
10
+)
11
+
12
+var (
13
+	resetDeadline time.Time
14
+
15
+	ErrBadDigest = errors.New("incorrect client random")
16
+)

+ 138
- 0
mtglib/internal/tls/fake/server_side.go Datei anzeigen

@@ -0,0 +1,138 @@
1
+package fake
2
+
3
+import (
4
+	"bytes"
5
+	"crypto/hmac"
6
+	"crypto/rand"
7
+	"crypto/sha256"
8
+	"encoding/binary"
9
+	"io"
10
+
11
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
12
+	"golang.org/x/crypto/curve25519"
13
+)
14
+
15
+const (
16
+	TypeHandshakeServer = 0x02
17
+
18
+	ChangeCipherValue = 0x01
19
+
20
+	EllipticCurveLen = 32
21
+)
22
+
23
+var serverHelloSuffix = []byte{
24
+	0x00,       // no compression
25
+	0x00, 0x2e, // 46 bytes of data
26
+	0x00, 0x2b, // Extension - Supported Versions
27
+	0x00, 0x02, // 2 bytes are following
28
+	0x03, 0x04, // TLS 1.3
29
+	0x00, 0x33, // Extension - Key Share
30
+	0x00, 0x24, // 36 bytes
31
+	0x00, 0x1d, // x25519 curve
32
+	0x00, 0x20, // 32 bytes of key
33
+}
34
+
35
+func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello) ([]byte, error) {
36
+	buf := &bytes.Buffer{}
37
+	buf.Grow(tls.MaxRecordSize)
38
+
39
+	generateServerHello(buf, clientHello)
40
+	generateChangeCipherValue(buf)
41
+
42
+	noise := &bytes.Buffer{}
43
+	generateNoise(noise)
44
+
45
+	packet := buf.Bytes()
46
+	digest := hmac.New(sha256.New, secret)
47
+
48
+	digest.Write(clientHello.Random[:])
49
+	digest.Write(packet)
50
+	digest.Write(noise.Bytes())
51
+	copy(packet[RandomOffset:], digest.Sum(nil))
52
+
53
+	_, err := w.Write(packet)
54
+
55
+	return noise.Bytes()[tls.SizeHeader:], err
56
+}
57
+
58
+func generateServerHello(buf *bytes.Buffer, hello *ClientHello) {
59
+	payload := acquireBuffer()
60
+	defer releaseBuffer(payload)
61
+
62
+	generateServerHelloPayload(payload, hello)
63
+
64
+	// 16 - type is 0x16 (handshake record)
65
+	// 03 03 - legacy protocol version of "3,3" (TLS 1.2)
66
+	// 00 7a - 0x7A (122) bytes of handshake message follows
67
+
68
+	// 16 - type is 0x16 (handshake record)
69
+	buf.WriteByte(tls.TypeHandshake)
70
+	// 03 03 - legacy protocol version of "3,3" (TLS 1.2)
71
+	buf.Write(tls.TLSVersion[:])
72
+	// 00 7a - 0x7A (122) bytes of handshake message follows
73
+	binary.Write(buf, binary.BigEndian, uint16(payload.Len())) //nolint: errcheck
74
+
75
+	payload.WriteTo(buf) //nolint: errcheck
76
+}
77
+
78
+func generateServerHelloPayload(buf *bytes.Buffer, hello *ClientHello) {
79
+	data := [4]byte{}
80
+
81
+	payload := acquireBuffer()
82
+	defer releaseBuffer(payload)
83
+
84
+	generateServerHelloHandshakePayload(payload, hello)
85
+
86
+	// 02 - handshake message type 0x02 (server hello)
87
+	// 00 00 76 - 0x76 (118) bytes of server hello data follows
88
+	buf.WriteByte(TypeHandshakeServer)
89
+	// 00 00 76 - 0x76 (118) bytes of server hello data follows
90
+	binary.BigEndian.PutUint32(data[:], uint32(payload.Len()))
91
+	buf.Write(data[1:])
92
+
93
+	payload.WriteTo(buf) //nolint: errcheck
94
+}
95
+
96
+func generateServerHelloHandshakePayload(buf *bytes.Buffer, hello *ClientHello) {
97
+	//  The unusual version number ("3,3" representing TLS 1.2) is due to
98
+	// TLS 1.0 being a minor revision of the SSL 3.0 protocol. Therefore
99
+	// TLS 1.0 is represented by "3,1", TLS 1.1 is "3,2", and so on.
100
+	buf.Write(tls.TLSVersion[:])
101
+
102
+	buf.Write(emptyRandom[:])
103
+
104
+	// 20 - 0x20 (32) bytes of session ID follow
105
+	// e0 e1 ... fe ff - session ID copied from Client Hello
106
+	buf.WriteByte(byte(len(hello.SessionID)))
107
+	buf.Write(hello.SessionID)
108
+
109
+	binary.Write(buf, binary.BigEndian, hello.CipherSuite) //nolint: errcheck
110
+
111
+	buf.Write(serverHelloSuffix)
112
+
113
+	scalar := [EllipticCurveLen]byte{}
114
+
115
+	if _, err := rand.Read(scalar[:]); err != nil {
116
+		panic(err)
117
+	}
118
+
119
+	curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint)
120
+	buf.Write(curve)
121
+}
122
+
123
+func generateChangeCipherValue(buf *bytes.Buffer) {
124
+	buf.WriteByte(tls.TypeChangeCipherSpec)
125
+	buf.Write(tls.TLSVersion[:])
126
+	binary.Write(buf, binary.BigEndian, uint16(1)) //nolint: errcheck
127
+	buf.WriteByte(ChangeCipherValue)
128
+}
129
+
130
+func generateNoise(buf *bytes.Buffer) {
131
+	data := [1369]byte{}
132
+
133
+	if _, err := rand.Read(data[:]); err != nil {
134
+		panic(err)
135
+	}
136
+
137
+	tls.WriteRecord(buf, data[:]) //nolint: errcheck
138
+}

+ 132
- 0
mtglib/internal/tls/fake/server_side_test.go Datei anzeigen

@@ -0,0 +1,132 @@
1
+package fake_test
2
+
3
+import (
4
+	"bytes"
5
+	"crypto/hmac"
6
+	"crypto/rand"
7
+	"crypto/sha256"
8
+	"testing"
9
+
10
+	"github.com/9seconds/mtg/v2/mtglib"
11
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
12
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
13
+	"github.com/stretchr/testify/suite"
14
+)
15
+
16
+type SendServerHelloTestSuite struct {
17
+	suite.Suite
18
+
19
+	hello  *fake.ClientHello
20
+	buf    *bytes.Buffer
21
+	secret mtglib.Secret
22
+}
23
+
24
+func (suite *SendServerHelloTestSuite) SetupTest() {
25
+	suite.hello = &fake.ClientHello{
26
+		CipherSuite: 4867,
27
+		SessionID:   make([]byte, 32),
28
+	}
29
+
30
+	_, err := rand.Read(suite.hello.SessionID)
31
+	suite.NoError(err)
32
+
33
+	_, err = rand.Read(suite.hello.Random[:])
34
+	suite.NoError(err)
35
+
36
+	suite.buf = &bytes.Buffer{}
37
+	suite.secret = mtglib.GenerateSecret("google.com")
38
+}
39
+
40
+func (suite *SendServerHelloTestSuite) TestRecordStructure() {
41
+	noise, err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
42
+	suite.NoError(err)
43
+
44
+	var rec bytes.Buffer
45
+
46
+	recordType, _, err := tls.ReadRecord(suite.buf, &rec)
47
+	suite.NoError(err)
48
+	suite.Equal(byte(tls.TypeHandshake), recordType)
49
+
50
+	rec.Reset()
51
+
52
+	recordType, _, err = tls.ReadRecord(suite.buf, &rec)
53
+	suite.NoError(err)
54
+	suite.Equal(byte(tls.TypeChangeCipherSpec), recordType)
55
+
56
+	suite.Empty(suite.buf.Bytes())
57
+
58
+	// noise is raw payload without TLS record header
59
+	suite.Len(noise, 1369)
60
+}
61
+
62
+func (suite *SendServerHelloTestSuite) TestHMAC() {
63
+	noise, err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
64
+	suite.NoError(err)
65
+
66
+	packet := make([]byte, suite.buf.Len())
67
+	copy(packet, suite.buf.Bytes())
68
+
69
+	random := make([]byte, fake.RandomLen)
70
+	copy(random, packet[fake.RandomOffset:])
71
+	copy(packet[fake.RandomOffset:], make([]byte, fake.RandomLen))
72
+
73
+	mac := hmac.New(sha256.New, suite.secret.Key[:])
74
+	mac.Write(suite.hello.Random[:])
75
+	mac.Write(packet)
76
+
77
+	// HMAC is computed over the full noise TLS record (with header),
78
+	// but SendServerHello returns noise without the header,
79
+	// so we reconstruct the full record.
80
+	var fullNoise bytes.Buffer
81
+	tls.WriteRecord(&fullNoise, noise) //nolint: errcheck
82
+	mac.Write(fullNoise.Bytes())
83
+
84
+	suite.Equal(random, mac.Sum(nil))
85
+}
86
+
87
+func (suite *SendServerHelloTestSuite) TestHandshakePayload() {
88
+	_, err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
89
+	suite.NoError(err)
90
+
91
+	packet := suite.buf.Bytes()
92
+
93
+	// TLS record header: type(1) + version(2) + length(2)
94
+	suite.Equal(byte(tls.TypeHandshake), packet[0])
95
+	suite.Equal([]byte{3, 3}, packet[1:3])
96
+
97
+	// Handshake header: type(1) + uint24_length(3)
98
+	suite.Equal(byte(fake.TypeHandshakeServer), packet[5])
99
+
100
+	// ServerHello version
101
+	suite.Equal([]byte{3, 3}, packet[9:11])
102
+
103
+	// Session ID
104
+	sessionIDOffset := fake.RandomOffset + fake.RandomLen
105
+	suite.Equal(byte(len(suite.hello.SessionID)), packet[sessionIDOffset])
106
+	suite.Equal(suite.hello.SessionID, packet[sessionIDOffset+1:sessionIDOffset+1+len(suite.hello.SessionID)])
107
+}
108
+
109
+func (suite *SendServerHelloTestSuite) TestChangeCipherSpec() {
110
+	_, err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
111
+	suite.NoError(err)
112
+
113
+	// Skip first record
114
+	var rec bytes.Buffer
115
+
116
+	_, _, err = tls.ReadRecord(suite.buf, &rec)
117
+	suite.NoError(err)
118
+
119
+	// Read ChangeCipherSpec record
120
+	rec.Reset()
121
+
122
+	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
123
+	suite.NoError(err)
124
+	suite.Equal(byte(tls.TypeChangeCipherSpec), recordType)
125
+	suite.Equal(int64(1), length)
126
+	suite.Equal([]byte{fake.ChangeCipherValue}, rec.Bytes())
127
+}
128
+
129
+func TestSendServerHello(t *testing.T) {
130
+	t.Parallel()
131
+	suite.Run(t, &SendServerHelloTestSuite{})
132
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-bad-fa2e46cdb33e2a1b.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181365,
3
+  "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
4
+  "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwNe8I9zdoBsduFEu/SRSbLoF89k4a+y6x53n8c2wpcQ+yBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIAKrgA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUA0AAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACAH/ugvH0kSUgAuwslL3UfZA3JTUfSiwrAhR6VWd2wvIgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-19dfe38384b9884b.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181365,
3
+  "random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
4
+  "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwNe8I9zdoBsduFEu/SRSbLoF89k4a+y6x53n8c2wpcQ+yBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIwKrgA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACAH/ugvH0kSUgAuwslL3UfZA3JTUfSiwrAhR6VWd2wvIgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-48f8a72a56f3174a.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181352,
3
+  "random": "oYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12os=",
4
+  "sessionId": "FGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8g=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwOhgS7feOX7NBtQoy1BtXU4cHSBcgzbLZqr2JjRBfXaiyAUaoDdkVitKWP//GXuVqaY2frgr38wraZDcclQa8/7yAA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACBroKhykU/xB3hgIVH2mRoKv3umjYAuPQ/mcj02dvdRYwAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-651054256093c6cd.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181352,
3
+  "random": "5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1ro=",
4
+  "sessionId": "jxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwPlXmxKmuT+0UiDL7HUF4o0aEuUWSp+CAumA3sZg4DWuiCPGvh3o9c8OT4vDHdZSn3C+PxMaU7ER2tEnTJ3In34fwA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACCu6UBqpR0p5VgzQX6m7qif+HosGk7LM4objEUgpygWTgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-79d01ef18a9d2621.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181365,
3
+  "random": "8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQ=",
4
+  "sessionId": "00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAU=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwPzGWOU6GQOWRp8QXm+7d7Wvd9a+HwBfnjzALeEtnezNCDTS68NgqcXJkUrJ/ceUvBYYI7J4ew8WJTlRn4WzmkMBQA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACD/0/vXjQ20rOPIPAF/32Y7LX4WNE8A8dM1D1bEc4qlXgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-7a5569f05b118145.json Datei anzeigen

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181352,
3
+  "random": "zja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4=",
4
+  "sessionId": "qPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnA=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgABAAH8AwPONrcwtnxYZJ+xBG09Xvn61jqBsrfMo+LVLLtIEFp+DiCo+63bIypdr3MYsgj/zvZJDd3hN+qjS6gNvu2mRF8GcAA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACBe+ItECoBgnzE4t2VyxSGV0jheXSD+z37LZCt3yto8SAAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
8
+}

+ 30
- 0
mtglib/internal/tls/init_test.go Datei anzeigen

@@ -0,0 +1,30 @@
1
+package tls
2
+
3
+import (
4
+	"encoding/binary"
5
+
6
+	"github.com/stretchr/testify/mock"
7
+)
8
+
9
+type WriterMock struct {
10
+	mock.Mock
11
+}
12
+
13
+func (m *WriterMock) Write(p []byte) (int, error) {
14
+	args := m.Called(p)
15
+	return args.Int(0), args.Error(1)
16
+}
17
+
18
+// makeTLSRecord builds a raw TLS record from hardcoded offsets:
19
+// type(1) + version(2, {3,3}) + length(2, big-endian) + payload.
20
+func MakeTLSRecord(recordType byte, payload []byte) []byte {
21
+	buf := make([]byte, 5+len(payload))
22
+
23
+	buf[0] = recordType
24
+	buf[1] = 3
25
+	buf[2] = 3
26
+	binary.BigEndian.PutUint16(buf[3:5], uint16(len(payload)))
27
+	copy(buf[5:], payload)
28
+
29
+	return buf
30
+}

+ 48
- 0
mtglib/internal/tls/utils.go Datei anzeigen

@@ -0,0 +1,48 @@
1
+package tls
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/binary"
6
+	"fmt"
7
+	"io"
8
+)
9
+
10
+func ReadRecord(r io.Reader, w io.Writer) (byte, int64, error) {
11
+	buf := [SizeHeader]byte{}
12
+
13
+	if _, err := io.ReadFull(r, buf[:]); err != nil {
14
+		return 0, 0, err
15
+	}
16
+
17
+	pVer := buf[SizeRecordType:]
18
+	pLen := pVer[SizeVersion:]
19
+
20
+	if !bytes.Equal(TLSVersion[:], pVer[:SizeVersion]) {
21
+		return 0, 0, fmt.Errorf("incorrect tls version %v", pVer)
22
+	}
23
+
24
+	length := int64(binary.BigEndian.Uint16(pLen[:SizeSize]))
25
+	_, err := io.CopyN(w, r, length)
26
+
27
+	return buf[0], length, err
28
+}
29
+
30
+func WriteRecord(w io.Writer, payload []byte) error {
31
+	buf := [MaxRecordSize]byte{}
32
+	buf[0] = TypeApplicationData
33
+
34
+	bufV := buf[SizeRecordType:]
35
+	copy(bufV[:SizeVersion], TLSVersion[:])
36
+
37
+	bufS := bufV[SizeVersion:]
38
+	binary.BigEndian.PutUint16(bufS[:SizeSize], uint16(len(payload)))
39
+
40
+	bufP := buf[SizeHeader:]
41
+	if n := copy(bufP, payload); n != len(payload) {
42
+		return fmt.Errorf("copied %d bytes of payload instead of %d", n, len(payload))
43
+	}
44
+
45
+	_, err := w.Write(buf[:SizeHeader+len(payload)])
46
+
47
+	return err
48
+}

+ 125
- 0
mtglib/internal/tls/utils_test.go Datei anzeigen

@@ -0,0 +1,125 @@
1
+package tls
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/binary"
6
+	"errors"
7
+	"testing"
8
+
9
+	"github.com/stretchr/testify/mock"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type UtilsTestSuite struct {
14
+	suite.Suite
15
+
16
+	dst *bytes.Buffer
17
+}
18
+
19
+func (suite *UtilsTestSuite) SetupTest() {
20
+	suite.dst = &bytes.Buffer{}
21
+}
22
+
23
+func (suite *UtilsTestSuite) TestReadRecord() {
24
+	payload := []byte("hello world")
25
+	raw := MakeTLSRecord(0x17, payload)
26
+
27
+	recordType, length, err := ReadRecord(bytes.NewReader(raw), suite.dst)
28
+
29
+	suite.NoError(err)
30
+	suite.Equal(byte(0x17), recordType)
31
+	suite.Equal(int64(len(payload)), length)
32
+	suite.Equal(payload, suite.dst.Bytes())
33
+}
34
+
35
+func (suite *UtilsTestSuite) TestReadRecordChangeCipherSpec() {
36
+	payload := []byte{1}
37
+	raw := MakeTLSRecord(0x14, payload)
38
+
39
+	recordType, length, err := ReadRecord(bytes.NewReader(raw), suite.dst)
40
+
41
+	suite.NoError(err)
42
+	suite.Equal(byte(0x14), recordType)
43
+	suite.Equal(int64(1), length)
44
+}
45
+
46
+func (suite *UtilsTestSuite) TestReadRecordRejectsWrongVersion() {
47
+	record := []byte{0x17, 3, 1, 0, 5, 0, 0, 0, 0, 0}
48
+
49
+	_, _, err := ReadRecord(bytes.NewReader(record), suite.dst)
50
+	suite.ErrorContains(err, "incorrect tls version")
51
+}
52
+
53
+func (suite *UtilsTestSuite) TestReadRecordEmptyReader() {
54
+	_, _, err := ReadRecord(bytes.NewReader(nil), suite.dst)
55
+	suite.Error(err)
56
+}
57
+
58
+func (suite *UtilsTestSuite) TestReadRecordTruncatedHeader() {
59
+	_, _, err := ReadRecord(bytes.NewReader([]byte{0x17, 3}), suite.dst)
60
+	suite.Error(err)
61
+}
62
+
63
+func (suite *UtilsTestSuite) TestReadRecordTruncatedPayload() {
64
+	raw := MakeTLSRecord(0x17, []byte("full payload"))
65
+	truncated := raw[:5+3]
66
+
67
+	_, _, err := ReadRecord(bytes.NewReader(truncated), suite.dst)
68
+	suite.Error(err)
69
+}
70
+
71
+func (suite *UtilsTestSuite) TestWriteRecord() {
72
+	payload := []byte("hello world")
73
+
74
+	err := WriteRecord(suite.dst, payload)
75
+	suite.NoError(err)
76
+
77
+	written := suite.dst.Bytes()
78
+	suite.Equal(byte(0x17), written[0])
79
+	suite.Equal([]byte{3, 3}, written[1:3])
80
+
81
+	length := binary.BigEndian.Uint16(written[3:5])
82
+	suite.Equal(uint16(len(payload)), length)
83
+	suite.Equal(payload, written[5:])
84
+}
85
+
86
+func (suite *UtilsTestSuite) TestWriteRecordRoundTrip() {
87
+	payload := []byte("round trip test")
88
+
89
+	var wire bytes.Buffer
90
+
91
+	err := WriteRecord(&wire, payload)
92
+	suite.NoError(err)
93
+
94
+	var recovered bytes.Buffer
95
+
96
+	recordType, length, err := ReadRecord(&wire, &recovered)
97
+
98
+	suite.NoError(err)
99
+	suite.Equal(byte(0x17), recordType)
100
+	suite.Equal(int64(len(payload)), length)
101
+	suite.Equal(payload, recovered.Bytes())
102
+}
103
+
104
+func (suite *UtilsTestSuite) TestWriteRecordPropagatesError() {
105
+	m := &WriterMock{}
106
+	m.
107
+		On("Write", mock.AnythingOfType("[]uint8")).
108
+		Once().
109
+		Return(0, errors.New("dist full"))
110
+
111
+	err := WriteRecord(m, []byte("data"))
112
+	suite.Error(err)
113
+
114
+	m.AssertExpectations(suite.T())
115
+}
116
+
117
+func (suite *UtilsTestSuite) TestWriteRecordPayloadTooLarge() {
118
+	err := WriteRecord(suite.dst, make([]byte, MaxRecordPayloadSize+1))
119
+	suite.Error(err)
120
+}
121
+
122
+func TestUtils(t *testing.T) {
123
+	t.Parallel()
124
+	suite.Run(t, &UtilsTestSuite{})
125
+}

+ 49
- 42
mtglib/proxy.go Datei anzeigen

@@ -11,10 +11,11 @@ import (
11 11
 
12 12
 	"github.com/9seconds/mtg/v2/essentials"
13 13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
14
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
15
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
14
+	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
16 15
 	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
17 16
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
17
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
18
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
18 19
 	"github.com/panjf2000/ants/v2"
19 20
 )
20 21
 
@@ -32,6 +33,7 @@ type Proxy struct {
32 33
 	workerPool                  *ants.PoolWithFunc
33 34
 	telegram                    *dc.Telegram
34 35
 	configUpdater               *dc.PublicConfigUpdater
36
+	doppelGanger                *doppel.Ganger
35 37
 	clientObfuscatror           obfuscation.Obfuscator
36 38
 
37 39
 	secret          Secret
@@ -76,19 +78,32 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
76 78
 		ctx.logger.Info("Stream has been finished")
77 79
 	}()
78 80
 
79
-	if !p.doFakeTLSHandshake(ctx) {
81
+	noise, ok := p.doFakeTLSHandshake(ctx)
82
+	if !ok {
80 83
 		return
81 84
 	}
82 85
 
83
-	if err := p.doObfuscatedHandshake(ctx); err != nil {
84
-		p.logger.InfoError("obfuscated handshake is failed", err)
86
+	clientConn, err := p.doppelGanger.NewConn(ctx.clientConn)
87
+	if err != nil {
88
+		ctx.logger.InfoError("cannot wrap into doppelganger connection", err)
89
+		return
90
+	}
91
+	defer clientConn.Stop()
85 92
 
93
+	if _, err := clientConn.Write(noise); err != nil {
94
+		ctx.logger.InfoError("cannot send the first packet", err)
86 95
 		return
87 96
 	}
88 97
 
89
-	if err := p.doTelegramCall(ctx); err != nil {
90
-		p.logger.WarningError("cannot dial to telegram", err)
98
+	ctx.clientConn = clientConn
91 99
 
100
+	if err := p.doObfuscatedHandshake(ctx); err != nil {
101
+		ctx.logger.InfoError("obfuscated handshake is failed", err)
102
+		return
103
+	}
104
+
105
+	if err := p.doTelegramCall(ctx); err != nil {
106
+		ctx.logger.WarningError("cannot dial to telegram", err)
92 107
 		return
93 108
 	}
94 109
 
@@ -155,61 +170,43 @@ func (p *Proxy) Shutdown() {
155 170
 	p.streamWaitGroup.Wait()
156 171
 	p.workerPool.Release()
157 172
 	p.configUpdater.Wait()
173
+	p.doppelGanger.Shutdown()
158 174
 
159 175
 	p.allowlist.Shutdown()
160 176
 	p.blocklist.Shutdown()
161 177
 }
162 178
 
163
-func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
164
-	rec := record.AcquireRecord()
165
-	defer record.ReleaseRecord(rec)
166
-
179
+func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) ([]byte, bool) {
167 180
 	rewind := newConnRewind(ctx.clientConn)
168 181
 
169
-	if err := rec.Read(rewind); err != nil {
170
-		p.logger.InfoError("cannot read client hello", err)
171
-		p.doDomainFronting(ctx, rewind)
172
-
173
-		return false
174
-	}
175
-
176
-	hello, err := faketls.ParseClientHello(p.secret.Key[:], rec.Payload.Bytes())
182
+	clientHello, err := fake.ReadClientHello(
183
+		rewind,
184
+		p.secret.Key[:],
185
+		p.secret.Host,
186
+		p.tolerateTimeSkewness,
187
+	)
177 188
 	if err != nil {
178
-		p.logger.InfoError("cannot parse client hello", err)
179
-		p.doDomainFronting(ctx, rewind)
180
-
181
-		return false
182
-	}
183
-
184
-	if err := hello.Valid(p.secret.Host, p.tolerateTimeSkewness); err != nil {
185
-		p.logger.
186
-			BindStr("hostname", hello.Host).
187
-			BindStr("hello-time", hello.Time.String()).
188
-			InfoError("invalid faketls client hello", err)
189
+		p.logger.InfoError("cannot read client hello", err)
189 190
 		p.doDomainFronting(ctx, rewind)
190
-
191
-		return false
191
+		return nil, false
192 192
 	}
193 193
 
194
-	if p.antiReplayCache.SeenBefore(hello.SessionID) {
194
+	if p.antiReplayCache.SeenBefore(clientHello.SessionID) {
195 195
 		p.logger.Warning("replay attack has been detected!")
196 196
 		p.eventStream.Send(p.ctx, NewEventReplayAttack(ctx.streamID))
197 197
 		p.doDomainFronting(ctx, rewind)
198
-
199
-		return false
198
+		return nil, false
200 199
 	}
201 200
 
202
-	if err := faketls.SendWelcomePacket(rewind, p.secret.Key[:], hello); err != nil {
201
+	noise, err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello)
202
+	if err != nil {
203 203
 		p.logger.InfoError("cannot send welcome packet", err)
204
-
205
-		return false
204
+		return nil, false
206 205
 	}
207 206
 
208
-	ctx.clientConn = &faketls.Conn{
209
-		Conn: ctx.clientConn,
210
-	}
207
+	ctx.clientConn = tls.New(ctx.clientConn, true, false)
211 208
 
212
-	return true
209
+	return noise, true
213 210
 }
214 211
 
215 212
 func (p *Proxy) doObfuscatedHandshake(ctx *streamContext) error {
@@ -338,6 +335,14 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
338 335
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
339 336
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
340 337
 		telegram:                 tg,
338
+		doppelGanger: doppel.NewGanger(
339
+			ctx,
340
+			opts.Network,
341
+			logger.Named("doppelganger"),
342
+			opts.DoppelGangerEach,
343
+			int(opts.DoppelGangerPerRaid),
344
+			opts.DoppelGangerURLs,
345
+		),
341 346
 		configUpdater: dc.NewPublicConfigUpdater(
342 347
 			tg,
343 348
 			updatersLogger.Named("public-config"),
@@ -349,6 +354,8 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
349 354
 		domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol,
350 355
 	}
351 356
 
357
+	proxy.doppelGanger.Run()
358
+
352 359
 	if opts.AutoUpdate {
353 360
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
354 361
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")

+ 15
- 0
mtglib/proxy_opts.go Datei anzeigen

@@ -142,6 +142,21 @@ type ProxyOpts struct {
142 142
 	//
143 143
 	// OBSOLETE and DEPRECATED. Ignored.
144 144
 	DCOverrides map[int][]string
145
+
146
+	// DoppelGangerURLs is a list of URLs that should be crawled by
147
+	// mtg to calculate parameters for statistical distribution of a
148
+	// traffic for fronting domains. If nothing is given, then predefined
149
+	// statistics is going to be used.
150
+	DoppelGangerURLs []string
151
+
152
+	// DoppelGangerPerRaid defines how many time each URL from
153
+	// DoppelGangerURLs list should be crawled per raid. We recommend to
154
+	// have this number ~10.
155
+	DoppelGangerPerRaid uint
156
+
157
+	// DoppelGangerEach defines a time period between each raid. We recommend
158
+	// to use hours here.
159
+	DoppelGangerEach time.Duration
145 160
 }
146 161
 
147 162
 func (p ProxyOpts) valid() error {

Laden…
Abbrechen
Speichern