Bläddra i källkod

Merge remote-tracking branch 'origin/master' into stable

tags/v2.2.0^2
9seconds 1 månad sedan
förälder
incheckning
30aa9d3a44
96 ändrade filer med 3920 tillägg och 1427 borttagningar
  1. 33
    11
      .github/workflows/ci.yaml
  2. 1
    0
      .github/workflows/govulncheck.yml
  3. 2
    0
      .goreleaser.yml
  4. 3
    3
      .mise.toml
  5. 55
    0
      BEST_PRACTICES.md
  6. 11
    6
      Dockerfile
  7. 78
    0
      README.md
  8. 60
    0
      example.config.toml
  9. 7
    7
      go.mod
  10. 14
    14
      go.sum
  11. 6
    1
      internal/cli/access.go
  12. 12
    2
      internal/cli/run_proxy.go
  13. 8
    2
      internal/config/config.go
  14. 6
    0
      internal/config/parse.go
  15. 53
    0
      internal/config/type_https_url.go
  16. 100
    0
      internal/config/type_https_url_test.go
  17. 5
    0
      internal/testlib/mtglib_network_mock.go
  18. 18
    18
      mise.lock
  19. 12
    1
      mtglib/init.go
  20. 35
    0
      mtglib/internal/doppel/clock.go
  21. 80
    0
      mtglib/internal/doppel/clock_test.go
  22. 130
    0
      mtglib/internal/doppel/conn.go
  23. 293
    0
      mtglib/internal/doppel/conn_test.go
  24. 184
    0
      mtglib/internal/doppel/ganger.go
  25. 107
    0
      mtglib/internal/doppel/ganger_test.go
  26. 43
    0
      mtglib/internal/doppel/init.go
  27. 107
    0
      mtglib/internal/doppel/init_test.go
  28. 6
    0
      mtglib/internal/doppel/logger.go
  29. 105
    0
      mtglib/internal/doppel/scout.go
  30. 57
    0
      mtglib/internal/doppel/scout_conn.go
  31. 29
    0
      mtglib/internal/doppel/scout_conn_collected.go
  32. 42
    0
      mtglib/internal/doppel/scout_conn_collected_test.go
  33. 39
    0
      mtglib/internal/doppel/scout_test.go
  34. 170
    0
      mtglib/internal/doppel/stats.go
  35. 219
    0
      mtglib/internal/doppel/stats_test.go
  36. 0
    134
      mtglib/internal/faketls/client_hello.go
  37. 0
    21
      mtglib/internal/faketls/client_hello_fuzz_test.go
  38. 0
    191
      mtglib/internal/faketls/client_hello_test.go
  39. 0
    72
      mtglib/internal/faketls/conn.go
  40. 0
    153
      mtglib/internal/faketls/conn_test.go
  41. 0
    59
      mtglib/internal/faketls/init.go
  42. 0
    84
      mtglib/internal/faketls/record/init.go
  43. 0
    79
      mtglib/internal/faketls/record/init_test.go
  44. 0
    20
      mtglib/internal/faketls/record/pools.go
  45. 0
    86
      mtglib/internal/faketls/record/record.go
  46. 0
    110
      mtglib/internal/faketls/record/record_test.go
  47. 0
    6
      mtglib/internal/faketls/record/testdata/05eb6b71f87b6802.json
  48. 0
    6
      mtglib/internal/faketls/record/testdata/4eef4abc15b206b6.json
  49. 0
    6
      mtglib/internal/faketls/record/testdata/736f358216afe91f.json
  50. 0
    6
      mtglib/internal/faketls/record/testdata/8405d94222bd0b6a.json
  51. 0
    6
      mtglib/internal/faketls/record/testdata/9036f76e517f0cd1.json
  52. 0
    6
      mtglib/internal/faketls/record/testdata/9244766a0fe4a02a.json
  53. 0
    6
      mtglib/internal/faketls/record/testdata/9255c73d3de76e7b.json
  54. 0
    6
      mtglib/internal/faketls/record/testdata/aeb65b9924315cf8.json
  55. 0
    6
      mtglib/internal/faketls/record/testdata/b0acd44296056b54.json
  56. 0
    6
      mtglib/internal/faketls/record/testdata/c0545a13fd9a3fa3.json
  57. 0
    6
      mtglib/internal/faketls/record/testdata/f083f4501668b759.json
  58. 0
    6
      mtglib/internal/faketls/record/testdata/f5696bcdffd11706.json
  59. 0
    8
      mtglib/internal/faketls/testdata/client-hello-bad-fa2e46cdb33e2a1b.json
  60. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-19dfe38384b9884b.json
  61. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-48f8a72a56f3174a.json
  62. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-651054256093c6cd.json
  63. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-79d01ef18a9d2621.json
  64. 0
    8
      mtglib/internal/faketls/testdata/client-hello-ok-7a5569f05b118145.json
  65. 0
    91
      mtglib/internal/faketls/welcome.go
  66. 0
    82
      mtglib/internal/faketls/welcome_test.go
  67. 0
    4
      mtglib/internal/relay/init.go
  68. 2
    1
      mtglib/internal/relay/relay.go
  69. 86
    0
      mtglib/internal/tls/conn.go
  70. 160
    0
      mtglib/internal/tls/conn_test.go
  71. 21
    0
      mtglib/internal/tls/fake/bytes_pool.go
  72. 309
    0
      mtglib/internal/tls/fake/client_side.go
  73. 48
    0
      mtglib/internal/tls/fake/client_side_fuzz_test.go
  74. 153
    0
      mtglib/internal/tls/fake/client_side_snapshot_test.go
  75. 395
    0
      mtglib/internal/tls/fake/client_side_test.go
  76. 16
    0
      mtglib/internal/tls/fake/init.go
  77. 135
    0
      mtglib/internal/tls/fake/server_side.go
  78. 130
    0
      mtglib/internal/tls/fake/server_side_test.go
  79. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-bad-fa2e46cdb33e2a1b.json
  80. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-19dfe38384b9884b.json
  81. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-48f8a72a56f3174a.json
  82. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-651054256093c6cd.json
  83. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-79d01ef18a9d2621.json
  84. 8
    0
      mtglib/internal/tls/fake/testdata/client-hello-ok-7a5569f05b118145.json
  85. 30
    0
      mtglib/internal/tls/init_test.go
  86. 48
    0
      mtglib/internal/tls/utils.go
  87. 125
    0
      mtglib/internal/tls/utils_test.go
  88. 41
    37
      mtglib/proxy.go
  89. 18
    0
      mtglib/proxy_opts.go
  90. 4
    0
      network/network.go
  91. 2
    1
      network/v2/base_network_test.go
  92. 3
    9
      network/v2/init.go
  93. 4
    3
      network/v2/multi_network.go
  94. 6
    1
      network/v2/network.go
  95. 3
    2
      network/v2/proxy_network.go
  96. 3
    2
      network/v2/socks_proxy_test.go

+ 33
- 11
.github/workflows/ci.yaml Visa fil

48
       - uses: jdx/mise-action@v3
48
       - uses: jdx/mise-action@v3
49
         name: Install mise
49
         name: Install mise
50
 
50
 
51
+      - name: Cache Go modules and build
52
+        uses: actions/cache@v5
53
+        with:
54
+          path: |
55
+            ~/go/pkg/mod
56
+            ~/.cache/go-build
57
+          key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
58
+          restore-keys: |
59
+            ${{ runner.os }}-go-
60
+
51
       - name: Run tests
61
       - name: Run tests
52
         run: mise tasks run covtest
62
         run: mise tasks run covtest
53
 
63
 
69
       - uses: jdx/mise-action@v3
79
       - uses: jdx/mise-action@v3
70
         name: Install mise
80
         name: Install mise
71
 
81
 
82
+      - name: Cache Go modules and build
83
+        uses: actions/cache@v5
84
+        with:
85
+          path: |
86
+            ~/go/pkg/mod
87
+            ~/.cache/go-build
88
+          key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
89
+          restore-keys: |
90
+            ${{ runner.os }}-go-
91
+
72
       - name: Run fuzzing
92
       - name: Run fuzzing
73
         run: mise tasks run 'test:fuzz:*'
93
         run: mise tasks run 'test:fuzz:*'
74
 
94
 
86
       - uses: jdx/mise-action@v3
106
       - uses: jdx/mise-action@v3
87
         name: Install mise
107
         name: Install mise
88
 
108
 
109
+      - name: Cache Go modules and build
110
+        uses: actions/cache@v5
111
+        with:
112
+          path: |
113
+            ~/go/pkg/mod
114
+            ~/.cache/go-build
115
+          key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
116
+          restore-keys: |
117
+            ${{ runner.os }}-go-
118
+
89
       - name: Run linter
119
       - name: Run linter
90
         run: mise tasks run lint
120
         run: mise tasks run lint
91
 
121
 
123
       - name: Setup BuildX
153
       - name: Setup BuildX
124
         uses: docker/setup-buildx-action@v3
154
         uses: docker/setup-buildx-action@v3
125
 
155
 
126
-      - name: Setup cache
127
-        uses: actions/cache@v5
128
-        with:
129
-          path: /tmp/buildx-cache
130
-          key: ${{ runner.os }}-buildx-${{ github.sha }}
131
-          restore-keys: |
132
-            ${{ runner.os }}-buildx-
133
-
134
       - name: Login to DockerHub
156
       - name: Login to DockerHub
135
         if: github.event_name != 'pull_request'
157
         if: github.event_name != 'pull_request'
136
         uses: docker/login-action@v3
158
         uses: docker/login-action@v3
147
           password: ${{ secrets.GITHUB_TOKEN }}
169
           password: ${{ secrets.GITHUB_TOKEN }}
148
 
170
 
149
       - name: Build and push
171
       - name: Build and push
150
-        uses: docker/build-push-action@v2
172
+        uses: docker/build-push-action@v6
151
         with:
173
         with:
152
           pull: true
174
           pull: true
153
           context: .
175
           context: .
155
           push: ${{ github.event_name != 'pull_request' }}
177
           push: ${{ github.event_name != 'pull_request' }}
156
           tags: ${{ steps.meta.outputs.tags }}
178
           tags: ${{ steps.meta.outputs.tags }}
157
           labels: ${{ steps.meta.outputs.labels }}
179
           labels: ${{ steps.meta.outputs.labels }}
158
-          cache-from: type=local,src=/tmp/buildx-cache
159
-          cache-to: type=local,dest=/tmp/buildx-cache
180
+          cache-from: type=gha
181
+          cache-to: type=gha,mode=max

+ 1
- 0
.github/workflows/govulncheck.yml Visa fil

35
       uses: actions/setup-go@v6
35
       uses: actions/setup-go@v6
36
       with:
36
       with:
37
         go-version-file: go.mod
37
         go-version-file: go.mod
38
+        cache: true
38
 
39
 
39
     - name: Check for vulnerabilities
40
     - name: Check for vulnerabilities
40
       run: |
41
       run: |

+ 2
- 0
.goreleaser.yml Visa fil

54
       - LICENSE
54
       - LICENSE
55
       - README.md
55
       - README.md
56
       - SECURITY.md
56
       - SECURITY.md
57
+      - BEST_PRACTICES.md
58
+      - example.config.toml
57
 
59
 
58
 gomod:
60
 gomod:
59
   proxy: true
61
   proxy: true

+ 3
- 3
.mise.toml Visa fil

33
 
33
 
34
 [tasks.test]
34
 [tasks.test]
35
 description = "Run tests"
35
 description = "Run tests"
36
-run = "go test -v ./..."
36
+run = "go test -v -race ./..."
37
 
37
 
38
 [tasks.covtest]
38
 [tasks.covtest]
39
 description = "Run tests with code coverage"
39
 description = "Run tests with code coverage"
40
-run = "go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./..."
40
+run = "go test -coverprofile=coverage.txt -covermode=atomic -count=2 -race -v ./..."
41
 
41
 
42
 [tasks.test-all]
42
 [tasks.test-all]
43
 description = "Run all tests"
43
 description = "Run all tests"
48
 
48
 
49
 [tasks."test:fuzz:client-hello"]
49
 [tasks."test:fuzz:client-hello"]
50
 description = "Run fuzzy test for ClientHello"
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
 [tasks."test:fuzz:client-handshake"]
53
 [tasks."test:fuzz:client-handshake"]
54
 description = "Run fuzzy test for ClientHandshake"
54
 description = "Run fuzzy test for ClientHandshake"

+ 55
- 0
BEST_PRACTICES.md Visa fil

1
+# Best practices
2
+
3
+This is unfortunate, but since 2018 many things were changed. Most of them
4
+became way worse. Previous iterations of censorship systems were very dumb,
5
+DPI were primitive and filtered very obvious things. Nowadays they are
6
+way more intelligent and it is very naive to treat them frivolously.
7
+
8
+In 2026 is not enough to pretend that your mtg installation is a Microsoft
9
+website that sits in Amsterdam Digital Ocean location. Now your installation
10
+has to be a website that is mtg in disguise. Yes, it requires a bit more effort
11
+but this effort is probably less than rotating proxies each other day.
12
+
13
+mtproto traffic, even with FakeTLS, has its specifics that are probably
14
+very well known by DPI systems. These specifics are not something unique but
15
+could mark an IP address as suspicious. Now let's think:
16
+
17
+1. You have a proxy in Amsterdam Digital Ocean that tells it is microsoft.com
18
+   how hard could it be to find out that this is probably fake? 1 or probably 2
19
+   DNS queries for `microsoft.com`? In case of some CDN, there are ECS-powered
20
+   resolvers that are very capable to return results from POV of some subnets.
21
+   If censor sees no relevant results, will they be afraid to block IP?
22
+2. You have a proxy in Amsterdam Digital Ocean that tells it is a website from
23
+   the same public subnet. But not the same. Would it be hard to make these DNS
24
+   queries and ban IP?
25
+
26
+The correct way of having this proxy is following:
27
+
28
+1. Register a domain name
29
+2. Get some VPS, probably in your domestic location
30
+3. Set that domain name from a step 1 to IP address of that VPS
31
+4. Generate a couple of HTML pages by LLMs or even copy them from elsewhere
32
+5. Set some webserver and issue TLS certificates with Let's Encrypt or any other
33
+   name
34
+6. Set mtg before this webserver.
35
+7. Use sing-box or anything like that to provide local socks5 interface and
36
+   have VPNized uplinks
37
+8. Set up mtg to use socks5 from a 7 step.
38
+
39
+In that case you will get a match of DNS and SNI in requests. As a side effect,
40
+your proxy will work with XTLS and its friends: XTLS in sniff mode ignores
41
+IP address a client wants to connect to. Instead, it reads SNI and connect
42
+to resolved address: a clever idea if user does not have a trustworthy DNS
43
+set up.
44
+
45
+Yes, this is much longer that usual technique, and requires more effort. But
46
+this is could probably be very well automated to some reasonable extent.
47
+
48
+Unfortunately, this is a best practice right now.
49
+
50
+Do not also forget about other implementation, like
51
+[telemt](https://github.com/telemt/telemt). Try everything. Use VPNs. It does
52
+not really matter which project you are going to use as long it helps you to
53
+stay connected.
54
+
55
+_March 2026._

+ 11
- 6
Dockerfile Visa fil

5
 
5
 
6
 ENV CGO_ENABLED=0
6
 ENV CGO_ENABLED=0
7
 
7
 
8
-RUN set -x \
9
-  && apk --no-cache --update add \
10
-    bash \
11
-    ca-certificates \
12
-    git
8
+RUN --mount=type=cache,target=/var/cache/apk \
9
+    set -x \
10
+    && apk --update add \
11
+      bash \
12
+      ca-certificates \
13
+      git
14
+
15
+COPY go.mod go.sum /app/
16
+WORKDIR /app
17
+
18
+RUN go mod download
13
 
19
 
14
 COPY . /app
20
 COPY . /app
15
-WORKDIR /app
16
 
21
 
17
 RUN set -x \
22
 RUN set -x \
18
   && version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \
23
   && version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \

+ 78
- 0
README.md Visa fil

38
 censored environment. But it does it slightly differently in details
38
 censored environment. But it does it slightly differently in details
39
 that probably matter.
39
 that probably matter.
40
 
40
 
41
+* **Domain fronting**
42
+
43
+  For years mtg supports domain fronting. This technique means that it fallbacks
44
+  to accessing a real website in case if request fails. It could fail by many
45
+  reasons: anti-replay protection, accidental access to the webserver or
46
+  stale request. Anyway, if mtg rejects this request, it does not break a
47
+  connection. It connects to the websites and replicates everything that client
48
+  has sent, and simply proxies it back as is. Users will see a response from
49
+  the real website, _byte-to-byte identical_ to the response of the real netloc.
50
+
51
+* **Doppelganger**
52
+
53
+  mtg also is a doppelganger of the website it fronts. Sure, with domain fronting
54
+  users will see replies of the real website in case if something will go wrong.
55
+  But what about such cases when _everything is fine_?
56
+
57
+  In that case mtg mimics TLS connection statistical characteristics as close as
58
+  possible. Different application have different statistics of their patterns.
59
+  Big CDN steadily pumping the data, small websites burst with short easily
60
+  compressiable chunks of traffic.
61
+
62
+  mtg artificially emulates those delays to be statistically indistinguishable
63
+  from the real website even if it covers connection of the very specific app.
64
+  It also follows 2 most common patterns of traffic chunking, so censors
65
+  will have to put more resources to find out that we have Telegram here
66
+  but not a hookah webshop served by nginx.
67
+
41
 * **Resource-efficient**
68
 * **Resource-efficient**
42
 
69
 
43
   It has to be resource-efficient. It does not mean that you will see
70
   It has to be resource-efficient. It does not mean that you will see
93
   software (written in Golang) with a minimum effort + you can replace
120
   software (written in Golang) with a minimum effort + you can replace
94
   some parts with those you want.
121
   some parts with those you want.
95
 
122
 
123
+Please also to read about [best practices](https://github.com/9seconds/mtg/blob/master/BEST_PRACTICES.md).
124
+
96
 ### Version 2
125
 ### Version 2
97
 
126
 
98
 If you use version 1.x before, you are probably noticed some major
127
 If you use version 1.x before, you are probably noticed some major
398
 $ docker exec mtg-proxy /mtg access /config.toml
427
 $ docker exec mtg-proxy /mtg access /config.toml
399
 ```
428
 ```
400
 
429
 
430
+## Doppelganger
431
+
432
+mtg can mimic real websites, please take a look at relevant section in example
433
+config file.
434
+
435
+mtg comes with some very good precollected statistics coming from
436
+[ok.ru](https://ok.ru/). It does not mean that you have to cover yourself
437
+by pretending that mtg is _ok.ru_. **Do not do that: ok.ru comes from very specific
438
+ASNs, but not from VPS providers you are going to use.** What I want to say
439
+is that defaults are very good enough to use as is because ok.ru for public
440
+pages has a very generic profile of TLS packets delay.
441
+
442
+But for better results it is recommended to teach mtg about the website you
443
+will use as a domain front. In order to do that, you need to specify URLs
444
+from this website. Just go to it, open WebDeveloper console and pick up
445
+random URLs. For better results they have to be **from the same domain name
446
+you are going to use as a disguise** but serve light and heavy content: pages,
447
+images etc. Do not use many, 2-3 will probably work.
448
+
449
+mtg will crawl these pages periodically, accumulating statistics and
450
+using it as you go.
451
+
452
+```toml
453
+[defense.doppelganger]
454
+urls = [
455
+  "https://lalala.com/index.html",
456
+  "https://lalala.com/contacts.html",
457
+]
458
+```
459
+
460
+This is not very necessary. Keep in mind these rules:
461
+
462
+1. If you are not sure what is this all about, do nothing. Defaults are good.
463
+2. All URLs must be HTTPS
464
+3. All URLs should be from the same domain name (but this is not a rule)
465
+4. Do not use a lot of pages. Use _different_ pages. mtg will start using this
466
+   statistics when it will accumulate enough anyway.
467
+5. These URLs should be directly accessible from mtg without proxies whatsoever
468
+6. Do not create huge raids. mtg will repeatedly crawl in raids, making N repeats.
469
+   Do not use high N, you do not want to be noticeable.
470
+7. It makes no sense to have small delay between raids. Usually webservers
471
+   do not update their TLS settings each hour.
472
+8. If you have some specific knowledge if webserver is using
473
+   [TLS Dynamic Record Sizing](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/), you
474
+   can use a very specific setting. This are Cloudflare, Go standard webservers,
475
+   [caddy](https://caddyserver.com/) and [H2O](https://h2o.examp1e.net/). If so,
476
+   you can enable `drs` setting.
477
+9. **If you are not sure, touch nothing!**
478
+
401
 ## Metrics
479
 ## Metrics
402
 
480
 
403
 Out of the box, mtg works with
481
 Out of the box, mtg works with

+ 60
- 0
example.config.toml Visa fil

206
 http = "10s"
206
 http = "10s"
207
 idle = "1m"
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
+# 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
+# Delays between TLS packets are not constant. There are many factors
222
+# that come in play. Application should generate some response, it could
223
+# send some headers first and stream content with chunked encoding. So
224
+# some first packets could come as soon as possible, with some delays
225
+# after first ones. Such phenomenon is described by different statistic
226
+# distribution. There are 2 distribution that describe it: lognormal
227
+# distribution and Weibul distribution. Lognormal is all about steady streams
228
+# of heavy content like a video. Weibul is great about short bursts like
229
+# user who requested a static page an a couple of images.
230
+[defense.doppelganger]
231
+# This is a list of URLs that would be crawled by mtg to approximate delay
232
+# statistics. They MUST be HTTPS urls.
233
+#
234
+# You can come to the website and collect different URLs, with light and
235
+# heavy content. We recommend to search for CDNs.
236
+urls = [
237
+    # "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
238
+]
239
+# A collection is done in raids. Each raid makes this number of requests to
240
+# each URL in this list. Do not use a huge number, 10 is probably ok.
241
+repeats-per-raid = 10
242
+# This is a duration between each raid. It makes no sense to have a small number
243
+# here as you would start to make a noticeable activity. Usually traffic patterns
244
+# do not change a lot, so do not expect different results if you request
245
+# each 10 minutes.
246
+raid-each = "6h"
247
+# This enables dynamic tls record sizing.
248
+#
249
+# Some modern stacks and platforms start to use the technique that is called
250
+# DRS. They start with small TLS packets and ramp up eventually. First packets
251
+# are usually about MTU size, after that we get 4k and eventually max size.
252
+# This is done with a good intention: to minimize a time to the first byte,
253
+# so application could start doing something with the data right after first
254
+# RTT.
255
+#
256
+# Apparently, about 90% of application do not employ this technique, they use
257
+# max size always: nginx, apache, java stuff. But Golang tools, angie and
258
+# some specific patches activate this technique.
259
+#
260
+# In order to mimic a real website we need to know something about software
261
+# it uses. Usually nobody cares: openssl does 16384, Python does it, nginx
262
+# does it. So this setting is disabled by default.
263
+#
264
+#      https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
265
+#      https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
266
+#      https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
267
+drs = false
268
+
209
 # Some countries do active probing on Telegram connections. This technique
269
 # Some countries do active probing on Telegram connections. This technique
210
 # allows to protect from such effort.
270
 # allows to protect from such effort.
211
 #
271
 #

+ 7
- 7
go.mod Visa fil

11
 	github.com/d4l3k/messagediff v1.2.1 // indirect
11
 	github.com/d4l3k/messagediff v1.2.1 // indirect
12
 	github.com/jarcoal/httpmock v1.0.8
12
 	github.com/jarcoal/httpmock v1.0.8
13
 	github.com/mccutchen/go-httpbin v1.1.1
13
 	github.com/mccutchen/go-httpbin v1.1.1
14
-	github.com/panjf2000/ants/v2 v2.11.5
14
+	github.com/panjf2000/ants/v2 v2.11.6
15
 	github.com/prometheus/client_golang v1.23.2
15
 	github.com/prometheus/client_golang v1.23.2
16
 	github.com/prometheus/common v0.67.5 // indirect
16
 	github.com/prometheus/common v0.67.5 // indirect
17
-	github.com/prometheus/procfs v0.20.0 // indirect
17
+	github.com/prometheus/procfs v0.20.1 // indirect
18
 	github.com/rs/zerolog v1.34.0
18
 	github.com/rs/zerolog v1.34.0
19
 	github.com/smira/go-statsd v1.3.4
19
 	github.com/smira/go-statsd v1.3.4
20
 	github.com/stretchr/objx v0.5.2 // indirect
20
 	github.com/stretchr/objx v0.5.2 // indirect
21
 	github.com/stretchr/testify v1.11.1
21
 	github.com/stretchr/testify v1.11.1
22
 	github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
22
 	github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
23
-	golang.org/x/crypto v0.48.0
24
-	golang.org/x/net v0.51.0
25
-	golang.org/x/sys v0.41.0
23
+	golang.org/x/crypto v0.49.0
24
+	golang.org/x/net v0.52.0
25
+	golang.org/x/sys v0.42.0
26
 	google.golang.org/protobuf v1.36.11 // indirect
26
 	google.golang.org/protobuf v1.36.11 // indirect
27
 )
27
 )
28
 
28
 
49
 	github.com/prometheus/client_model v0.6.2 // indirect
49
 	github.com/prometheus/client_model v0.6.2 // indirect
50
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
50
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
51
 	github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
51
 	github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
52
-	go.yaml.in/yaml/v2 v2.4.3 // indirect
53
-	golang.org/x/sync v0.19.0 // indirect
52
+	go.yaml.in/yaml/v2 v2.4.4 // indirect
53
+	golang.org/x/sync v0.20.0 // indirect
54
 	golang.org/x/tools v0.41.0 // indirect
54
 	golang.org/x/tools v0.41.0 // indirect
55
 	gopkg.in/yaml.v3 v3.0.1 // indirect
55
 	gopkg.in/yaml.v3 v3.0.1 // indirect
56
 )
56
 )

+ 14
- 14
go.sum Visa fil

53
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
53
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
54
 github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
54
 github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
55
 github.com/ncruces/go-dns v1.3.2/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
55
 github.com/ncruces/go-dns v1.3.2/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
56
-github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl6Rg=
57
-github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
56
+github.com/panjf2000/ants/v2 v2.11.6 h1:JKsoIUukIoCO0sP0gcOqdyoXmpyKXuU6fC57rODtpug=
57
+github.com/panjf2000/ants/v2 v2.11.6/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
58
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
58
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
59
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
59
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
60
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
60
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
70
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
70
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
71
 github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
71
 github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
72
 github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
72
 github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
73
-github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
74
-github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
73
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
74
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
75
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
75
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
76
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
76
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
77
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
77
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
105
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
105
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
106
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
106
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
107
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
107
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
108
-go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
109
-go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
108
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
109
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
110
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
110
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
111
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
111
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
112
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
113
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
112
+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
113
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
114
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
114
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
115
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
115
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
116
 golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
116
 golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
119
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
119
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
120
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
120
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
121
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
121
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
122
-golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
123
-golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
122
+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
123
+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
124
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
124
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
125
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
125
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
126
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
126
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
127
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
128
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
127
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
128
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
129
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
129
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
130
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
130
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
131
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
131
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
135
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
135
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
137
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
137
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
138
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
139
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
138
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
139
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
140
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
140
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
141
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
141
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
142
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
142
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

+ 6
- 1
internal/cli/access.go Visa fil

101
 }
101
 }
102
 
102
 
103
 func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
103
 func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
104
+	dialer := ntw.NativeDialer()
104
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
105
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
105
-		return ntw.DialContext(ctx, protocol, address) //nolint: wrapcheck
106
+		conn, err := dialer.DialContext(ctx, protocol, address)
107
+		if err != nil {
108
+			return nil, err
109
+		}
110
+		return essentials.WrapNetConn(conn), err
106
 	})
111
 	})
107
 
112
 
108
 	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
113
 	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx

+ 12
- 2
internal/cli/run_proxy.go Visa fil

46
 
46
 
47
 	base := network.New(
47
 	base := network.New(
48
 		resolver,
48
 		resolver,
49
-		"mtg/"+version,
49
+		"",
50
 		conf.Network.Timeout.TCP.Get(0),
50
 		conf.Network.Timeout.TCP.Get(0),
51
 		conf.Network.Timeout.HTTP.Get(0),
51
 		conf.Network.Timeout.HTTP.Get(0),
52
 		conf.Network.Timeout.Idle.Get(0),
52
 		conf.Network.Timeout.Idle.Get(0),
53
 	)
53
 	)
54
 
54
 
55
-	proxyDialers := make([]network.Network, len(conf.Network.Proxies))
55
+	proxyDialers := make([]mtglib.Network, len(conf.Network.Proxies))
56
 	for idx, v := range conf.Network.Proxies {
56
 	for idx, v := range conf.Network.Proxies {
57
 		value, err := network.NewProxyNetwork(base, v.Get(nil))
57
 		value, err := network.NewProxyNetwork(base, v.Get(nil))
58
 		if err != nil {
58
 		if err != nil {
239
 		return fmt.Errorf("cannot build ip allowlist: %w", err)
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
 	opts := mtglib.ProxyOpts{
247
 	opts := mtglib.ProxyOpts{
243
 		Logger:          logger,
248
 		Logger:          logger,
244
 		Network:         ntw,
249
 		Network:         ntw,
256
 
261
 
257
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
262
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
258
 		TolerateTimeSkewness:     conf.TolerateTimeSkewness.Value,
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),
268
+		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
259
 	}
269
 	}
260
 
270
 
261
 	proxy, err := mtglib.NewProxy(opts)
271
 	proxy, err := mtglib.NewProxy(opts)

+ 8
- 2
internal/config/config.go Visa fil

47
 			MaxSize   TypeBytes     `json:"maxSize"`
47
 			MaxSize   TypeBytes     `json:"maxSize"`
48
 			ErrorRate TypeErrorRate `json:"errorRate"`
48
 			ErrorRate TypeErrorRate `json:"errorRate"`
49
 		} `json:"antiReplay"`
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
+			DRS        TypeBool        `json:"drs"`
57
+		} `json:"doppelganger"`
52
 	} `json:"defense"`
58
 	} `json:"defense"`
53
 	Network struct {
59
 	Network struct {
54
 		Timeout struct {
60
 		Timeout struct {

+ 6
- 0
internal/config/parse.go Visa fil

44
 			URLs                []string `toml:"urls" json:"urls,omitempty"`
44
 			URLs                []string `toml:"urls" json:"urls,omitempty"`
45
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
45
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
46
 		} `toml:"allowlist" json:"allowlist,omitempty"`
46
 		} `toml:"allowlist" json:"allowlist,omitempty"`
47
+		Doppelganger struct {
48
+			URLs       []string `toml:"urls" json:"urls,omitempty"`
49
+			Repeats    uint     `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
50
+			UpdateEach string   `toml:"raid-each" json:"raid_each,omitempty"`
51
+			DRS        bool     `toml:"drs" json:"drs,omitempty"`
52
+		} `toml:"doppelganger" json:"doppelganger,omitempty"`
47
 	} `toml:"defense" json:"defense,omitempty"`
53
 	} `toml:"defense" json:"defense,omitempty"`
48
 	Network struct {
54
 	Network struct {
49
 		Timeout struct {
55
 		Timeout struct {

+ 53
- 0
internal/config/type_https_url.go Visa fil

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 Visa fil

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
+}

+ 5
- 0
internal/testlib/mtglib_network_mock.go Visa fil

2
 
2
 
3
 import (
3
 import (
4
 	"context"
4
 	"context"
5
+	"net"
5
 	"net/http"
6
 	"net/http"
6
 
7
 
7
 	"github.com/9seconds/mtg/v2/essentials"
8
 	"github.com/9seconds/mtg/v2/essentials"
24
 	return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert
25
 	return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert
25
 }
26
 }
26
 
27
 
28
+func (m *MtglibNetworkMock) NativeDialer() *net.Dialer {
29
+	return m.Called().Get(0).(*net.Dialer)
30
+}
31
+
27
 func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
32
 func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
28
 	network, address string) (essentials.Conn, error),
33
 	network, address string) (essentials.Conn, error),
29
 ) *http.Client {
34
 ) *http.Client {

+ 18
- 18
mise.lock Visa fil

1
 [[tools.go]]
1
 [[tools.go]]
2
-version = "1.26.0"
2
+version = "1.26.1"
3
 backend = "core:go"
3
 backend = "core:go"
4
-"platforms.linux-arm64" = { checksum = "sha256:bd03b743eb6eb4193ea3c3fd3956546bf0e3ca5b7076c8226334afe6b75704cd", url = "https://dl.google.com/go/go1.26.0.linux-arm64.tar.gz"}
5
-"platforms.linux-x64" = { checksum = "sha256:aac1b08a0fb0c4e0a7c1555beb7b59180b05dfc5a3d62e40e9de90cd42f88235", url = "https://dl.google.com/go/go1.26.0.linux-amd64.tar.gz"}
6
-"platforms.macos-arm64" = { checksum = "sha256:b1640525dfe68f066d56f200bef7bf4dce955a1a893bd061de6754c211431023", url = "https://dl.google.com/go/go1.26.0.darwin-arm64.tar.gz"}
7
-"platforms.macos-x64" = { checksum = "sha256:1ca28b7703cbea05a65b2a1d92d6b308610ef92f8824578a0874f2e60c9d5a22", url = "https://dl.google.com/go/go1.26.0.darwin-amd64.tar.gz"}
8
-"platforms.windows-x64" = { checksum = "sha256:9bbe0fc64236b2b51f6255c05c4232532b8ecc0e6d2e00950bd3021d8a4d07d4", url = "https://dl.google.com/go/go1.26.0.windows-amd64.zip"}
4
+"platforms.linux-arm64" = { checksum = "sha256:a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7", url = "https://dl.google.com/go/go1.26.1.linux-arm64.tar.gz"}
5
+"platforms.linux-x64" = { checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a", url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"}
6
+"platforms.macos-arm64" = { checksum = "sha256:353df43a7811ce284c8938b5f3c7df40b7bfb6f56cb165b150bc40b5e2dd541f", url = "https://dl.google.com/go/go1.26.1.darwin-arm64.tar.gz"}
7
+"platforms.macos-x64" = { checksum = "sha256:65773dab2f8cc4cd23d93ba6d0a805de150ca0b78378879292be0b903b8cdd08", url = "https://dl.google.com/go/go1.26.1.darwin-amd64.tar.gz"}
8
+"platforms.windows-x64" = { checksum = "sha256:9b68112c913f45b7aebbf13c036721264bbba7e03a642f8f7490c561eebd1ecc", url = "https://dl.google.com/go/go1.26.1.windows-amd64.zip"}
9
 
9
 
10
 [[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]]
10
 [[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]]
11
 version = "latest"
11
 version = "latest"
24
 backend = "go:mvdan.cc/gofumpt"
24
 backend = "go:mvdan.cc/gofumpt"
25
 
25
 
26
 [[tools.golangci-lint]]
26
 [[tools.golangci-lint]]
27
-version = "2.10.1"
27
+version = "2.11.3"
28
 backend = "aqua:golangci/golangci-lint"
28
 backend = "aqua:golangci/golangci-lint"
29
-"platforms.linux-arm64" = { checksum = "sha256:6652b42ae02915eb2f9cb2a2e0cac99514c8eded8388d88ae3e06e1a52c00de8", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-arm64.tar.gz"}
30
-"platforms.linux-x64" = { checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz"}
31
-"platforms.macos-arm64" = { checksum = "sha256:03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-arm64.tar.gz"}
32
-"platforms.macos-x64" = { checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz"}
33
-"platforms.windows-x64" = { checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip"}
29
+"platforms.linux-arm64" = { checksum = "sha256:ee3d95f301359e7d578e6d99c8ad5aeadbabc5a13009a30b2b0df11c8058afe9", url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.3/golangci-lint-2.11.3-linux-arm64.tar.gz"}
30
+"platforms.linux-x64" = { checksum = "sha256:87bb8cddbcc825d5778b64e8a91b46c0526b247f4e2f2904dea74ec7450475d1", url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.3/golangci-lint-2.11.3-linux-amd64.tar.gz"}
31
+"platforms.macos-arm64" = { checksum = "sha256:30ee39979c516b9d1adca289a3f93429d130c4c0fda5e57d637850894221f6cc", url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.3/golangci-lint-2.11.3-darwin-arm64.tar.gz"}
32
+"platforms.macos-x64" = { checksum = "sha256:f93bda1f2cc981fd1326464020494be62f387bbf262706e1b3b644e5afacc440", url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.3/golangci-lint-2.11.3-darwin-amd64.tar.gz"}
33
+"platforms.windows-x64" = { checksum = "sha256:cd42e890176bc5cfeb36225a77e66b9410ddd3a59a03551e23f6b210d29e1f67", url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.3/golangci-lint-2.11.3-windows-amd64.zip"}
34
 
34
 
35
 [[tools.goreleaser]]
35
 [[tools.goreleaser]]
36
-version = "2.14.1"
36
+version = "2.14.3"
37
 backend = "aqua:goreleaser/goreleaser"
37
 backend = "aqua:goreleaser/goreleaser"
38
-"platforms.linux-arm64" = { checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz"}
39
-"platforms.linux-x64" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"}
40
-"platforms.macos-arm64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
41
-"platforms.macos-x64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
42
-"platforms.windows-x64" = { checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip"}
38
+"platforms.linux-arm64" = { checksum = "sha256:581a10e53c1176b3e81ee45cf531e02dbf899db0bc7b795669347df4276ce948", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.3/goreleaser_Linux_arm64.tar.gz"}
39
+"platforms.linux-x64" = { checksum = "sha256:dc7faeeeb6da8bdfda788626263a4ae725892a8c7504b975c3234127d4a44579", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.3/goreleaser_Linux_x86_64.tar.gz"}
40
+"platforms.macos-arm64" = { checksum = "sha256:3507798489e107a78aff36b169de48148a335ac26eb3161608d905f3f3a957bd", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.3/goreleaser_Darwin_all.tar.gz"}
41
+"platforms.macos-x64" = { checksum = "sha256:3507798489e107a78aff36b169de48148a335ac26eb3161608d905f3f3a957bd", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.3/goreleaser_Darwin_all.tar.gz"}
42
+"platforms.windows-x64" = { checksum = "sha256:3deea8ff471aa258a2d99f3e5302971d7028647ae8ddaf103257a8113e485a31", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.3/goreleaser_Windows_x86_64.zip"}

+ 12
- 1
mtglib/init.go Visa fil

99
 	// reads from Telegram after which connection will be terminated. This is
99
 	// reads from Telegram after which connection will be terminated. This is
100
 	// required to abort stale connections.
100
 	// required to abort stale connections.
101
 	TCPRelayReadTimeout = 20 * time.Second
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
 // Network defines a knowledge how to work with a network. It may sound fun but
111
 // Network defines a knowledge how to work with a network. It may sound fun but
117
 	// Dial establishes context-free TCP connections.
124
 	// Dial establishes context-free TCP connections.
118
 	Dial(network, address string) (essentials.Conn, error)
125
 	Dial(network, address string) (essentials.Conn, error)
119
 
126
 
120
-	// DialContext dials using a context. This is a preferrable way of
127
+	// DialContext dials using a context. This is a preferable way of
121
 	// establishing TCP connections.
128
 	// establishing TCP connections.
122
 	DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
129
 	DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
123
 
130
 
124
 	// MakeHTTPClient build an HTTP client with given dial function. If nothing is
131
 	// MakeHTTPClient build an HTTP client with given dial function. If nothing is
125
 	// provided, then DialContext of this interface is going to be used.
132
 	// provided, then DialContext of this interface is going to be used.
126
 	MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
133
 	MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
134
+
135
+	// NativeDialer returns a configured instance of native dialer that
136
+	// skips proxy connections or any other irrelevant settings.
137
+	NativeDialer() *net.Dialer
127
 }
138
 }
128
 
139
 
129
 // AntiReplayCache is an interface that is used to detect replay attacks based
140
 // AntiReplayCache is an interface that is used to detect replay attacks based

+ 35
- 0
mtglib/internal/doppel/clock.go Visa fil

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 Visa fil

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
+}

+ 130
- 0
mtglib/internal/doppel/conn.go Visa fil

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
+	syncWriteLock sync.RWMutex
24
+	writeStream   bytes.Buffer
25
+	writeCond     *sync.Cond
26
+}
27
+
28
+func (c Conn) Write(p []byte) (int, error) {
29
+	c.p.syncWriteLock.RLock()
30
+	defer c.p.syncWriteLock.RUnlock()
31
+
32
+	c.p.writeCond.L.Lock()
33
+	c.p.writeStream.Write(p)
34
+	c.p.writeCond.L.Unlock()
35
+
36
+	return len(p), context.Cause(c.p.ctx)
37
+}
38
+
39
+func (c Conn) SyncWrite(p []byte) (int, error) {
40
+	c.p.syncWriteLock.Lock()
41
+	defer c.p.syncWriteLock.Unlock()
42
+
43
+	c.p.writeCond.L.Lock()
44
+	// wait until buffer is exhausted
45
+	for c.p.writeStream.Len() != 0 && context.Cause(c.p.ctx) == nil {
46
+		c.p.writeCond.Wait()
47
+	}
48
+	c.p.writeStream.Write(p)
49
+	c.p.writeCond.L.Unlock()
50
+
51
+	if err := context.Cause(c.p.ctx); err != nil {
52
+		return len(p), err
53
+	}
54
+
55
+	c.p.writeCond.L.Lock()
56
+	// wait until data will be sent
57
+	for c.p.writeStream.Len() != 0 && context.Cause(c.p.ctx) == nil {
58
+		c.p.writeCond.Wait()
59
+	}
60
+	c.p.writeCond.L.Unlock()
61
+
62
+	return len(p), context.Cause(c.p.ctx)
63
+}
64
+
65
+func (c Conn) Start() {
66
+	c.p.wg.Go(func() {
67
+		c.start()
68
+	})
69
+}
70
+
71
+func (c Conn) start() {
72
+	defer c.p.writeCond.Broadcast()
73
+
74
+	buf := [tls.MaxRecordSize]byte{}
75
+
76
+	for {
77
+		select {
78
+		case <-c.p.ctx.Done():
79
+			return
80
+		case <-c.p.clock.tick:
81
+		}
82
+
83
+		c.p.writeCond.L.Lock()
84
+		n, err := c.p.writeStream.Read(buf[:c.p.clock.stats.Size()])
85
+		c.p.writeCond.L.Unlock()
86
+
87
+		if n == 0 || err != nil {
88
+			continue
89
+		}
90
+
91
+		if err := tls.WriteRecord(c.Conn, buf[:n]); err != nil {
92
+			c.p.ctxCancel(err)
93
+			return
94
+		}
95
+
96
+		c.p.writeCond.Signal()
97
+	}
98
+}
99
+
100
+func (c Conn) Stop() {
101
+	c.p.ctxCancel(nil)
102
+	c.p.wg.Wait()
103
+}
104
+
105
+func NewConn(ctx context.Context, conn essentials.Conn, stats *Stats) Conn {
106
+	ctx, cancel := context.WithCancelCause(ctx)
107
+	rv := Conn{
108
+		Conn: conn,
109
+		p: &connPayload{
110
+			ctx:       ctx,
111
+			ctxCancel: cancel,
112
+			writeCond: sync.NewCond(&sync.Mutex{}),
113
+			clock: Clock{
114
+				stats: stats,
115
+				tick:  make(chan struct{}),
116
+			},
117
+		},
118
+	}
119
+
120
+	rv.p.writeStream.Grow(tls.DefaultBufferSize)
121
+
122
+	rv.p.wg.Go(func() {
123
+		rv.p.clock.Start(ctx)
124
+	})
125
+	rv.p.wg.Go(func() {
126
+		rv.start()
127
+	})
128
+
129
+	return rv
130
+}

+ 293
- 0
mtglib/internal/doppel/conn_test.go Visa fil

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 (suite *ConnTestSuite) TestSyncWriteDataSent() {
161
+	suite.connMock.
162
+		On("Write", mock.AnythingOfType("[]uint8")).
163
+		Return(0, nil).
164
+		Maybe()
165
+
166
+	c := suite.makeConn()
167
+	defer c.Stop()
168
+
169
+	payload := []byte("sync hello")
170
+	n, err := c.SyncWrite(payload)
171
+	suite.NoError(err)
172
+	suite.Equal(len(payload), n)
173
+
174
+	// SyncWrite returns only after data is flushed to the wire.
175
+	assembled := &bytes.Buffer{}
176
+	reader := bytes.NewReader(suite.connMock.Written())
177
+
178
+	for {
179
+		header := make([]byte, tls.SizeHeader)
180
+		if _, err := io.ReadFull(reader, header); err != nil {
181
+			break
182
+		}
183
+
184
+		suite.Equal(byte(tls.TypeApplicationData), header[0])
185
+
186
+		length := binary.BigEndian.Uint16(header[tls.SizeRecordType+tls.SizeVersion:])
187
+		rec := make([]byte, length)
188
+		_, err := io.ReadFull(reader, rec)
189
+		suite.NoError(err)
190
+
191
+		assembled.Write(rec)
192
+	}
193
+
194
+	suite.Equal(payload, assembled.Bytes())
195
+}
196
+
197
+func (suite *ConnTestSuite) TestSyncWriteDrainsBufferFirst() {
198
+	suite.connMock.
199
+		On("Write", mock.AnythingOfType("[]uint8")).
200
+		Return(0, nil).
201
+		Maybe()
202
+
203
+	c := suite.makeConn()
204
+	defer c.Stop()
205
+
206
+	// Buffer some data via async Write.
207
+	_, err := c.Write([]byte("first"))
208
+	suite.NoError(err)
209
+
210
+	// SyncWrite must drain "first" before sending "second".
211
+	n, err := c.SyncWrite([]byte("second"))
212
+	suite.NoError(err)
213
+	suite.Equal(6, n)
214
+
215
+	// All data should be on the wire now.
216
+	assembled := &bytes.Buffer{}
217
+	reader := bytes.NewReader(suite.connMock.Written())
218
+
219
+	for {
220
+		header := make([]byte, tls.SizeHeader)
221
+		if _, err := io.ReadFull(reader, header); err != nil {
222
+			break
223
+		}
224
+
225
+		length := binary.BigEndian.Uint16(header[tls.SizeRecordType+tls.SizeVersion:])
226
+		rec := make([]byte, length)
227
+		_, err := io.ReadFull(reader, rec)
228
+		suite.NoError(err)
229
+
230
+		assembled.Write(rec)
231
+	}
232
+
233
+	suite.Equal([]byte("firstsecond"), assembled.Bytes())
234
+}
235
+
236
+func (suite *ConnTestSuite) TestSyncWriteBlocksAsyncWrite() {
237
+	suite.connMock.
238
+		On("Write", mock.AnythingOfType("[]uint8")).
239
+		Return(0, nil).
240
+		Maybe()
241
+
242
+	c := suite.makeConn()
243
+	defer c.Stop()
244
+
245
+	// Start SyncWrite — it holds exclusive lock.
246
+	syncDone := make(chan struct{})
247
+
248
+	go func() {
249
+		defer close(syncDone)
250
+		c.SyncWrite([]byte("exclusive")) //nolint: errcheck
251
+	}()
252
+
253
+	// Give SyncWrite time to acquire the lock.
254
+	time.Sleep(10 * time.Millisecond)
255
+
256
+	// Async Write should block until SyncWrite completes.
257
+	writeDone := make(chan struct{})
258
+
259
+	go func() {
260
+		defer close(writeDone)
261
+		c.Write([]byte("blocked")) //nolint: errcheck
262
+	}()
263
+
264
+	// SyncWrite should finish first.
265
+	<-syncDone
266
+
267
+	select {
268
+	case <-writeDone:
269
+		// Write completed after SyncWrite — correct.
270
+	case <-time.After(2 * time.Second):
271
+		suite.Fail("async Write did not unblock after SyncWrite completed")
272
+	}
273
+}
274
+
275
+func (suite *ConnTestSuite) TestSyncWriteReturnsErrorAfterStop() {
276
+	suite.connMock.
277
+		On("Write", mock.AnythingOfType("[]uint8")).
278
+		Return(0, nil).
279
+		Maybe()
280
+
281
+	c := suite.makeConn()
282
+	c.Stop()
283
+
284
+	time.Sleep(10 * time.Millisecond)
285
+
286
+	_, err := c.SyncWrite([]byte("too late"))
287
+	suite.Error(err)
288
+}
289
+
290
+func TestConn(t *testing.T) {
291
+	t.Parallel()
292
+	suite.Run(t, &ConnTestSuite{})
293
+}

+ 184
- 0
mtglib/internal/doppel/ganger.go Visa fil

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

+ 107
- 0
mtglib/internal/doppel/ganger_test.go Visa fil

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, true)
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
+}

+ 43
- 0
mtglib/internal/doppel/init.go Visa fil

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

+ 107
- 0
mtglib/internal/doppel/init_test.go Visa fil

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) NativeDialer() *net.Dialer {
34
+	return &net.Dialer{}
35
+}
36
+
37
+func (s SimpleNetwork) MakeHTTPClient(dialFunc func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client {
38
+	if dialFunc == nil {
39
+		dialFunc = s.DialContext
40
+	}
41
+
42
+	return &http.Client{
43
+		Transport: &http.Transport{
44
+			TLSClientConfig: &tls.Config{
45
+				InsecureSkipVerify: true, //nolint: gosec
46
+			},
47
+			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
48
+				return dialFunc(ctx, network, address)
49
+			},
50
+		},
51
+	}
52
+}
53
+
54
+type TLSServerTestSuite struct {
55
+	suite.Suite
56
+
57
+	tlsServer *httptest.Server
58
+	ctx       context.Context
59
+	ctxCancel context.CancelFunc
60
+	network   SimpleNetwork
61
+	urls      []string
62
+}
63
+
64
+func (suite *TLSServerTestSuite) SetupSuite() {
65
+	suite.tlsServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
66
+		w.WriteHeader(http.StatusOK)
67
+		w.Header().Add("Hello", "how long")
68
+
69
+		if _, err := w.Write([]byte{1, 2, 3}); err != nil {
70
+			panic(err)
71
+		}
72
+
73
+		time.Sleep(5 * time.Millisecond)
74
+
75
+		if _, err := w.Write([]byte{1, 2, 3}); err != nil {
76
+			panic(err)
77
+		}
78
+	}))
79
+	suite.urls = []string{suite.tlsServer.URL}
80
+}
81
+
82
+func (suite *TLSServerTestSuite) SetupTest() {
83
+	ctx, cancel := context.WithCancel(context.Background())
84
+	suite.ctx = ctx
85
+	suite.ctxCancel = cancel
86
+}
87
+
88
+func (suite *TLSServerTestSuite) TearDownTest() {
89
+	suite.ctxCancel()
90
+	suite.tlsServer.CloseClientConnections()
91
+}
92
+
93
+func (suite *TLSServerTestSuite) TearDownSuite() {
94
+	suite.tlsServer.Close()
95
+}
96
+
97
+type LoggerMock struct {
98
+	mock.Mock
99
+}
100
+
101
+func (l *LoggerMock) Info(msg string) {
102
+	l.Called(msg)
103
+}
104
+
105
+func (l *LoggerMock) WarningError(msg string, err error) {
106
+	l.Called(msg, err)
107
+}

+ 6
- 0
mtglib/internal/doppel/logger.go Visa fil

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

+ 105
- 0
mtglib/internal/doppel/scout.go Visa fil

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
+	dialer := s.network.NativeDialer()
83
+	collected := NewScoutConnCollected()
84
+	client := s.network.MakeHTTPClient(func(
85
+		ctx context.Context,
86
+		network string,
87
+		address string,
88
+	) (essentials.Conn, error) {
89
+		conn, err := dialer.DialContext(ctx, network, address)
90
+		if err != nil {
91
+			return nil, err
92
+		}
93
+
94
+		return NewScoutConn(essentials.WrapNetConn(conn), collected), nil
95
+	})
96
+
97
+	return client, collected
98
+}
99
+
100
+func NewScout(network Network, urls []string) Scout {
101
+	return Scout{
102
+		network: network,
103
+		urls:    urls,
104
+	}
105
+}

+ 57
- 0
mtglib/internal/doppel/scout_conn.go Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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
+}

+ 170
- 0
mtglib/internal/doppel/stats.go Visa fil

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

+ 219
- 0
mtglib/internal/doppel/stats_test.go Visa fil

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, true)
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, true)
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, true)
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, true)
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, true)
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, drs: true}
142
+
143
+	for range TLSCounterAccelAfter {
144
+		size := stats.Size()
145
+		suite.GreaterOrEqual(size, TLSRecordSizeStart-DRSNoise)
146
+		suite.LessOrEqual(size, TLSRecordSizeStart)
147
+	}
148
+}
149
+
150
+func (suite *StatsTestSuite) TestSizeAccelPhase() {
151
+	stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
152
+
153
+	for range TLSCounterAccelAfter {
154
+		stats.Size()
155
+	}
156
+
157
+	for range TLSCounterMaxAfter - TLSCounterAccelAfter {
158
+		size := stats.Size()
159
+		suite.GreaterOrEqual(size, TLSRecordSizeAccel-DRSNoise)
160
+		suite.LessOrEqual(size, TLSRecordSizeAccel)
161
+	}
162
+}
163
+
164
+func (suite *StatsTestSuite) TestSizeMaxPhase() {
165
+	stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
166
+
167
+	for range TLSCounterMaxAfter {
168
+		stats.Size()
169
+	}
170
+
171
+	for range 20 {
172
+		size := stats.Size()
173
+		suite.Equal(TLSRecordSizeMax, size)
174
+	}
175
+}
176
+
177
+func (suite *StatsTestSuite) TestSizeResetsAfterInactivity() {
178
+	stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
179
+
180
+	// Advance past start phase.
181
+	for range TLSCounterMaxAfter {
182
+		stats.Size()
183
+	}
184
+
185
+	suite.Equal(TLSRecordSizeMax, stats.Size())
186
+
187
+	// Simulate inactivity by backdating sizeLastRequested.
188
+	stats.sizeLastRequested = time.Now().Add(-TLSRecordSizeResetAfter - time.Millisecond)
189
+
190
+	size := stats.Size()
191
+	suite.GreaterOrEqual(size, TLSRecordSizeStart-DRSNoise)
192
+	suite.LessOrEqual(size, TLSRecordSizeStart)
193
+}
194
+
195
+func (suite *StatsTestSuite) TestSizeNoDRSAlwaysMax() {
196
+	stats := &Stats{k: 1.0, lambda: 1.0, drs: false}
197
+
198
+	for range TLSCounterMaxAfter + 20 {
199
+		suite.Equal(TLSRecordSizeMax, stats.Size())
200
+	}
201
+}
202
+
203
+func (suite *StatsTestSuite) TestSizeNoDRSIgnoresCounter() {
204
+	stats := &Stats{k: 1.0, lambda: 1.0, drs: false}
205
+
206
+	// Even after many calls, always returns max.
207
+	for range 200 {
208
+		suite.Equal(TLSRecordSizeMax, stats.Size())
209
+	}
210
+
211
+	// Inactivity has no effect either.
212
+	stats.sizeLastRequested = time.Now().Add(-TLSRecordSizeResetAfter - time.Millisecond)
213
+	suite.Equal(TLSRecordSizeMax, stats.Size())
214
+}
215
+
216
+func TestStats(t *testing.T) {
217
+	t.Parallel()
218
+	suite.Run(t, &StatsTestSuite{})
219
+}

+ 0
- 134
mtglib/internal/faketls/client_hello.go Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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

+ 0
- 4
mtglib/internal/relay/init.go Visa fil

1
 package relay
1
 package relay
2
 
2
 
3
-const (
4
-	copyBufferSize = 64 * 1024
5
-)
6
-
7
 type Logger interface {
3
 type Logger interface {
8
 	Printf(msg string, args ...any)
4
 	Printf(msg string, args ...any)
9
 }
5
 }

+ 2
- 1
mtglib/internal/relay/relay.go Visa fil

6
 	"io"
6
 	"io"
7
 
7
 
8
 	"github.com/9seconds/mtg/v2/essentials"
8
 	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
9
 )
10
 )
10
 
11
 
11
 func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) {
12
 func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) {
35
 }
36
 }
36
 
37
 
37
 func pump(log Logger, src, dst essentials.Conn, direction string) {
38
 func pump(log Logger, src, dst essentials.Conn, direction string) {
38
-	var buf [copyBufferSize]byte
39
+	var buf [tls.MaxRecordPayloadSize]byte
39
 
40
 
40
 	defer src.CloseRead()  //nolint: errcheck
41
 	defer src.CloseRead()  //nolint: errcheck
41
 	defer dst.CloseWrite() //nolint: errcheck
42
 	defer dst.CloseWrite() //nolint: errcheck

+ 86
- 0
mtglib/internal/tls/conn.go Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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
+)

+ 135
- 0
mtglib/internal/tls/fake/server_side.go Visa fil

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

+ 130
- 0
mtglib/internal/tls/fake/server_side_test.go Visa fil

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

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

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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 Visa fil

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
+}

+ 41
- 37
mtglib/proxy.go Visa fil

11
 
11
 
12
 	"github.com/9seconds/mtg/v2/essentials"
12
 	"github.com/9seconds/mtg/v2/essentials"
13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
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
 	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
15
 	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
17
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
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
 	"github.com/panjf2000/ants/v2"
19
 	"github.com/panjf2000/ants/v2"
19
 )
20
 )
20
 
21
 
32
 	workerPool                  *ants.PoolWithFunc
33
 	workerPool                  *ants.PoolWithFunc
33
 	telegram                    *dc.Telegram
34
 	telegram                    *dc.Telegram
34
 	configUpdater               *dc.PublicConfigUpdater
35
 	configUpdater               *dc.PublicConfigUpdater
36
+	doppelGanger                *doppel.Ganger
35
 	clientObfuscatror           obfuscation.Obfuscator
37
 	clientObfuscatror           obfuscation.Obfuscator
36
 
38
 
37
 	secret          Secret
39
 	secret          Secret
80
 		return
82
 		return
81
 	}
83
 	}
82
 
84
 
83
-	if err := p.doObfuscatedHandshake(ctx); err != nil {
84
-		p.logger.InfoError("obfuscated handshake is failed", err)
85
+	clientConn, err := p.doppelGanger.NewConn(ctx.clientConn)
86
+	if err != nil {
87
+		ctx.logger.InfoError("cannot wrap into doppelganger connection", err)
88
+		return
89
+	}
90
+	defer clientConn.Stop()
85
 
91
 
92
+	ctx.clientConn = clientConn
93
+
94
+	if err := p.doObfuscatedHandshake(ctx); err != nil {
95
+		ctx.logger.InfoError("obfuscated handshake is failed", err)
86
 		return
96
 		return
87
 	}
97
 	}
88
 
98
 
89
 	if err := p.doTelegramCall(ctx); err != nil {
99
 	if err := p.doTelegramCall(ctx); err != nil {
90
-		p.logger.WarningError("cannot dial to telegram", err)
91
-
100
+		ctx.logger.WarningError("cannot dial to telegram", err)
92
 		return
101
 		return
93
 	}
102
 	}
94
 
103
 
155
 	p.streamWaitGroup.Wait()
164
 	p.streamWaitGroup.Wait()
156
 	p.workerPool.Release()
165
 	p.workerPool.Release()
157
 	p.configUpdater.Wait()
166
 	p.configUpdater.Wait()
167
+	p.doppelGanger.Shutdown()
158
 
168
 
159
 	p.allowlist.Shutdown()
169
 	p.allowlist.Shutdown()
160
 	p.blocklist.Shutdown()
170
 	p.blocklist.Shutdown()
161
 }
171
 }
162
 
172
 
163
 func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
173
 func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
164
-	rec := record.AcquireRecord()
165
-	defer record.ReleaseRecord(rec)
166
-
167
 	rewind := newConnRewind(ctx.clientConn)
174
 	rewind := newConnRewind(ctx.clientConn)
168
 
175
 
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())
176
+	clientHello, err := fake.ReadClientHello(
177
+		rewind,
178
+		p.secret.Key[:],
179
+		p.secret.Host,
180
+		p.tolerateTimeSkewness,
181
+	)
177
 	if err != nil {
182
 	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)
183
+		p.logger.InfoError("cannot read client hello", err)
189
 		p.doDomainFronting(ctx, rewind)
184
 		p.doDomainFronting(ctx, rewind)
190
-
191
 		return false
185
 		return false
192
 	}
186
 	}
193
 
187
 
194
-	if p.antiReplayCache.SeenBefore(hello.SessionID) {
188
+	if p.antiReplayCache.SeenBefore(clientHello.SessionID) {
195
 		p.logger.Warning("replay attack has been detected!")
189
 		p.logger.Warning("replay attack has been detected!")
196
 		p.eventStream.Send(p.ctx, NewEventReplayAttack(ctx.streamID))
190
 		p.eventStream.Send(p.ctx, NewEventReplayAttack(ctx.streamID))
197
 		p.doDomainFronting(ctx, rewind)
191
 		p.doDomainFronting(ctx, rewind)
198
-
199
 		return false
192
 		return false
200
 	}
193
 	}
201
 
194
 
202
-	if err := faketls.SendWelcomePacket(rewind, p.secret.Key[:], hello); err != nil {
195
+	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello); err != nil {
203
 		p.logger.InfoError("cannot send welcome packet", err)
196
 		p.logger.InfoError("cannot send welcome packet", err)
204
-
205
 		return false
197
 		return false
206
 	}
198
 	}
207
 
199
 
208
-	ctx.clientConn = &faketls.Conn{
209
-		Conn: ctx.clientConn,
210
-	}
200
+	ctx.clientConn = tls.New(ctx.clientConn, true, false)
211
 
201
 
212
 	return true
202
 	return true
213
 }
203
 }
282
 	p.eventStream.Send(p.ctx, NewEventDomainFronting(ctx.streamID))
272
 	p.eventStream.Send(p.ctx, NewEventDomainFronting(ctx.streamID))
283
 	conn.Rewind()
273
 	conn.Rewind()
284
 
274
 
285
-	frontConn, err := p.network.DialContext(ctx, "tcp", p.DomainFrontingAddress())
275
+	nativeDialer := p.network.NativeDialer()
276
+	fConn, err := nativeDialer.DialContext(ctx, "tcp", p.DomainFrontingAddress())
286
 	if err != nil {
277
 	if err != nil {
287
 		p.logger.WarningError("cannot dial to the fronting domain", err)
278
 		p.logger.WarningError("cannot dial to the fronting domain", err)
288
 
279
 
289
 		return
280
 		return
290
 	}
281
 	}
291
 
282
 
283
+	frontConn := essentials.WrapNetConn(fConn)
284
+
292
 	if p.domainFrontingProxyProtocol {
285
 	if p.domainFrontingProxyProtocol {
293
 		frontConn = newConnProxyProtocol(ctx.clientConn, frontConn)
286
 		frontConn = newConnProxyProtocol(ctx.clientConn, frontConn)
294
 	}
287
 	}
338
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
331
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
339
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
332
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
340
 		telegram:                 tg,
333
 		telegram:                 tg,
334
+		doppelGanger: doppel.NewGanger(
335
+			ctx,
336
+			opts.Network,
337
+			logger.Named("doppelganger"),
338
+			opts.DoppelGangerEach,
339
+			int(opts.DoppelGangerPerRaid),
340
+			opts.DoppelGangerURLs,
341
+			opts.DoppelGangerDRS,
342
+		),
341
 		configUpdater: dc.NewPublicConfigUpdater(
343
 		configUpdater: dc.NewPublicConfigUpdater(
342
 			tg,
344
 			tg,
343
 			updatersLogger.Named("public-config"),
345
 			updatersLogger.Named("public-config"),
349
 		domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol,
351
 		domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol,
350
 	}
352
 	}
351
 
353
 
354
+	proxy.doppelGanger.Run()
355
+
352
 	if opts.AutoUpdate {
356
 	if opts.AutoUpdate {
353
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
357
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
354
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")
358
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")

+ 18
- 0
mtglib/proxy_opts.go Visa fil

142
 	//
142
 	//
143
 	// OBSOLETE and DEPRECATED. Ignored.
143
 	// OBSOLETE and DEPRECATED. Ignored.
144
 	DCOverrides map[int][]string
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
160
+
161
+	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
162
+	DoppelGangerDRS bool
145
 }
163
 }
146
 
164
 
147
 func (p ProxyOpts) valid() error {
165
 func (p ProxyOpts) valid() error {

+ 4
- 0
network/network.go Visa fil

60
 	return nil, fmt.Errorf("cannot dial to %s:%s: %w", protocol, address, err)
60
 	return nil, fmt.Errorf("cannot dial to %s:%s: %w", protocol, address, err)
61
 }
61
 }
62
 
62
 
63
+func (n *network) NativeDialer() *net.Dialer {
64
+	return &net.Dialer{}
65
+}
66
+
63
 func (n *network) MakeHTTPClient(dialFunc func(ctx context.Context,
67
 func (n *network) MakeHTTPClient(dialFunc func(ctx context.Context,
64
 	network, address string) (essentials.Conn, error),
68
 	network, address string) (essentials.Conn, error),
65
 ) *http.Client {
69
 ) *http.Client {

+ 2
- 1
network/v2/base_network_test.go Visa fil

4
 	"context"
4
 	"context"
5
 	"testing"
5
 	"testing"
6
 
6
 
7
+	"github.com/9seconds/mtg/v2/mtglib"
7
 	"github.com/9seconds/mtg/v2/network/v2"
8
 	"github.com/9seconds/mtg/v2/network/v2"
8
 	"github.com/stretchr/testify/assert"
9
 	"github.com/stretchr/testify/assert"
9
 	"github.com/stretchr/testify/suite"
10
 	"github.com/stretchr/testify/suite"
12
 type BaseNetworkTestSuite struct {
13
 type BaseNetworkTestSuite struct {
13
 	EchoServerTestSuite
14
 	EchoServerTestSuite
14
 
15
 
15
-	net network.Network
16
+	net mtglib.Network
16
 }
17
 }
17
 
18
 
18
 func (suite *BaseNetworkTestSuite) SetupSuite() {
19
 func (suite *BaseNetworkTestSuite) SetupSuite() {

+ 3
- 9
network/v2/init.go Visa fil

11
 
11
 
12
 import (
12
 import (
13
 	"errors"
13
 	"errors"
14
-	"net"
15
 	"time"
14
 	"time"
16
-
17
-	"github.com/9seconds/mtg/v2/mtglib"
18
 )
15
 )
19
 
16
 
20
 const (
17
 const (
31
 	// probes.
28
 	// probes.
32
 	DefaultTCPKeepAlivePeriod = 10 * time.Second
29
 	DefaultTCPKeepAlivePeriod = 10 * time.Second
33
 
30
 
31
+	// User Agent to use in HTTP client.
32
+	UserAgent = "curl/8.5.0"
33
+
34
 	// tcpLingerTimeout defines a number of seconds to wait for sending
34
 	// tcpLingerTimeout defines a number of seconds to wait for sending
35
 	// unacknowledged data.
35
 	// unacknowledged data.
36
 	tcpLingerTimeout = 1
36
 	tcpLingerTimeout = 1
37
 )
37
 )
38
 
38
 
39
 var ErrCannotDial = errors.New("cannot dial to any address")
39
 var ErrCannotDial = errors.New("cannot dial to any address")
40
-
41
-type Network interface {
42
-	mtglib.Network
43
-
44
-	NativeDialer() *net.Dialer
45
-}

+ 4
- 3
network/v2/multi_network.go Visa fil

8
 	"net/http"
8
 	"net/http"
9
 
9
 
10
 	"github.com/9seconds/mtg/v2/essentials"
10
 	"github.com/9seconds/mtg/v2/essentials"
11
+	"github.com/9seconds/mtg/v2/mtglib"
11
 )
12
 )
12
 
13
 
13
 type multiNetwork struct {
14
 type multiNetwork struct {
14
-	networks []Network
15
+	networks []mtglib.Network
15
 }
16
 }
16
 
17
 
17
 func (m multiNetwork) Dial(network, address string) (essentials.Conn, error) {
18
 func (m multiNetwork) Dial(network, address string) (essentials.Conn, error) {
22
 	networks := m.networks
23
 	networks := m.networks
23
 
24
 
24
 	if len(networks) > 1 {
25
 	if len(networks) > 1 {
25
-		networks = make([]Network, len(m.networks))
26
+		networks = make([]mtglib.Network, len(m.networks))
26
 		copy(networks, m.networks)
27
 		copy(networks, m.networks)
27
 
28
 
28
 		rand.Shuffle(len(m.networks), func(i, j int) {
29
 		rand.Shuffle(len(m.networks), func(i, j int) {
59
 	return m.networks[0].MakeHTTPClient(dialFunc)
60
 	return m.networks[0].MakeHTTPClient(dialFunc)
60
 }
61
 }
61
 
62
 
62
-func Join(networks ...Network) (Network, error) {
63
+func Join(networks ...mtglib.Network) (mtglib.Network, error) {
63
 	if len(networks) == 0 {
64
 	if len(networks) == 0 {
64
 		return nil, errors.New("cannot join no networks")
65
 		return nil, errors.New("cannot join no networks")
65
 	}
66
 	}

+ 6
- 1
network/v2/network.go Visa fil

8
 	"time"
8
 	"time"
9
 
9
 
10
 	"github.com/9seconds/mtg/v2/essentials"
10
 	"github.com/9seconds/mtg/v2/essentials"
11
+	"github.com/9seconds/mtg/v2/mtglib"
11
 )
12
 )
12
 
13
 
13
 type network struct {
14
 type network struct {
70
 	tcpTimeout,
71
 	tcpTimeout,
71
 	httpTimeout,
72
 	httpTimeout,
72
 	idleTimeout time.Duration,
73
 	idleTimeout time.Duration,
73
-) Network {
74
+) mtglib.Network {
74
 	if dnsResolver == nil {
75
 	if dnsResolver == nil {
75
 		dnsResolver = net.DefaultResolver
76
 		dnsResolver = net.DefaultResolver
76
 	}
77
 	}
77
 
78
 
79
+	if userAgent == "" {
80
+		userAgent = UserAgent
81
+	}
82
+
78
 	return &network{
83
 	return &network{
79
 		Dialer: net.Dialer{
84
 		Dialer: net.Dialer{
80
 			Timeout:       tcpTimeout,
85
 			Timeout:       tcpTimeout,

+ 3
- 2
network/v2/proxy_network.go Visa fil

6
 	"net/url"
6
 	"net/url"
7
 
7
 
8
 	"github.com/9seconds/mtg/v2/essentials"
8
 	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib"
9
 	"golang.org/x/net/proxy"
10
 	"golang.org/x/net/proxy"
10
 )
11
 )
11
 
12
 
12
 type proxyNetwork struct {
13
 type proxyNetwork struct {
13
-	Network
14
+	mtglib.Network
14
 	client proxy.ContextDialer
15
 	client proxy.ContextDialer
15
 }
16
 }
16
 
17
 
23
 	return essentials.WrapNetConn(conn), nil
24
 	return essentials.WrapNetConn(conn), nil
24
 }
25
 }
25
 
26
 
26
-func NewProxyNetwork(base Network, proxyURL *url.URL) (*proxyNetwork, error) {
27
+func NewProxyNetwork(base mtglib.Network, proxyURL *url.URL) (*proxyNetwork, error) {
27
 	socks, err := proxy.FromURL(proxyURL, base.NativeDialer())
28
 	socks, err := proxy.FromURL(proxyURL, base.NativeDialer())
28
 	if err != nil {
29
 	if err != nil {
29
 		return nil, fmt.Errorf("cannot build proxy dialer: %w", err)
30
 		return nil, fmt.Errorf("cannot build proxy dialer: %w", err)

+ 3
- 2
network/v2/socks_proxy_test.go Visa fil

6
 	"sync"
6
 	"sync"
7
 	"testing"
7
 	"testing"
8
 
8
 
9
+	"github.com/9seconds/mtg/v2/mtglib"
9
 	"github.com/9seconds/mtg/v2/network/v2"
10
 	"github.com/9seconds/mtg/v2/network/v2"
10
 	"github.com/stretchr/testify/assert"
11
 	"github.com/stretchr/testify/assert"
11
 	"github.com/stretchr/testify/require"
12
 	"github.com/stretchr/testify/require"
17
 	EchoServerTestSuite
18
 	EchoServerTestSuite
18
 
19
 
19
 	wg          sync.WaitGroup
20
 	wg          sync.WaitGroup
20
-	baseNetwork network.Network
21
+	baseNetwork mtglib.Network
21
 
22
 
22
 	noAuthURL *url.URL
23
 	noAuthURL *url.URL
23
 	authURL   *url.URL
24
 	authURL   *url.URL
85
 
86
 
86
 	for name, proxies := range testData {
87
 	for name, proxies := range testData {
87
 		suite.T().Run(name, func(t *testing.T) {
88
 		suite.T().Run(name, func(t *testing.T) {
88
-			proxyNetworks := []network.Network{}
89
+			proxyNetworks := []mtglib.Network{}
89
 
90
 
90
 			for _, u := range proxies {
91
 			for _, u := range proxies {
91
 				value, err := network.NewProxyNetwork(suite.baseNetwork, u)
92
 				value, err := network.NewProxyNetwork(suite.baseNetwork, u)

Laddar…
Avbryt
Spara