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

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

tags/v2.2.0^2
9seconds пре 1 месец
родитељ
комит
30aa9d3a44
96 измењених фајлова са 3920 додато и 1427 уклоњено
  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 Прегледај датотеку

@@ -48,6 +48,16 @@ jobs:
48 48
       - uses: jdx/mise-action@v3
49 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 61
       - name: Run tests
52 62
         run: mise tasks run covtest
53 63
 
@@ -69,6 +79,16 @@ jobs:
69 79
       - uses: jdx/mise-action@v3
70 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 92
       - name: Run fuzzing
73 93
         run: mise tasks run 'test:fuzz:*'
74 94
 
@@ -86,6 +106,16 @@ jobs:
86 106
       - uses: jdx/mise-action@v3
87 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 119
       - name: Run linter
90 120
         run: mise tasks run lint
91 121
 
@@ -123,14 +153,6 @@ jobs:
123 153
       - name: Setup BuildX
124 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 156
       - name: Login to DockerHub
135 157
         if: github.event_name != 'pull_request'
136 158
         uses: docker/login-action@v3
@@ -147,7 +169,7 @@ jobs:
147 169
           password: ${{ secrets.GITHUB_TOKEN }}
148 170
 
149 171
       - name: Build and push
150
-        uses: docker/build-push-action@v2
172
+        uses: docker/build-push-action@v6
151 173
         with:
152 174
           pull: true
153 175
           context: .
@@ -155,5 +177,5 @@ jobs:
155 177
           push: ${{ github.event_name != 'pull_request' }}
156 178
           tags: ${{ steps.meta.outputs.tags }}
157 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 Прегледај датотеку

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

+ 2
- 0
.goreleaser.yml Прегледај датотеку

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

+ 3
- 3
.mise.toml Прегледај датотеку

@@ -33,11 +33,11 @@ run = "govulncheck ./..."
33 33
 
34 34
 [tasks.test]
35 35
 description = "Run tests"
36
-run = "go test -v ./..."
36
+run = "go test -v -race ./..."
37 37
 
38 38
 [tasks.covtest]
39 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 42
 [tasks.test-all]
43 43
 description = "Run all tests"
@@ -48,7 +48,7 @@ depends = [
48 48
 
49 49
 [tasks."test:fuzz:client-hello"]
50 50
 description = "Run fuzzy test for ClientHello"
51
-run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls"
51
+run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
52 52
 
53 53
 [tasks."test:fuzz:client-handshake"]
54 54
 description = "Run fuzzy test for ClientHandshake"

+ 55
- 0
BEST_PRACTICES.md Прегледај датотеку

@@ -0,0 +1,55 @@
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 Прегледај датотеку

@@ -5,14 +5,19 @@ FROM golang:1.26-alpine AS build
5 5
 
6 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 20
 COPY . /app
15
-WORKDIR /app
16 21
 
17 22
 RUN set -x \
18 23
   && version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \

+ 78
- 0
README.md Прегледај датотеку

@@ -38,6 +38,33 @@ goal: to give a possibility to connect to Telegram in a restricted,
38 38
 censored environment. But it does it slightly differently in details
39 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 68
 * **Resource-efficient**
42 69
 
43 70
   It has to be resource-efficient. It does not mean that you will see
@@ -93,6 +120,8 @@ that probably matter.
93 120
   software (written in Golang) with a minimum effort + you can replace
94 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 125
 ### Version 2
97 126
 
98 127
 If you use version 1.x before, you are probably noticed some major
@@ -398,6 +427,55 @@ or if you are using docker:
398 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 479
 ## Metrics
402 480
 
403 481
 Out of the box, mtg works with

+ 60
- 0
example.config.toml Прегледај датотеку

@@ -206,6 +206,66 @@ tcp = "5s"
206 206
 http = "10s"
207 207
 idle = "1m"
208 208
 
209
+# mtg has to mimic real websites. It does not mean domain fronting, it also
210
+# means that traffic characteristics should be similar to real world traffic.
211
+# websites and applications behave differently, their traffic patterns are also
212
+# different. Applications do bursts of RPC-style messages (or JSON communication,
213
+# does not really matter), while websites pump heavy content in HTTP2 streams
214
+#
215
+# It means that statistically there is a different between traffic shape:
216
+# 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 269
 # Some countries do active probing on Telegram connections. This technique
210 270
 # allows to protect from such effort.
211 271
 #

+ 7
- 7
go.mod Прегледај датотеку

@@ -11,18 +11,18 @@ require (
11 11
 	github.com/d4l3k/messagediff v1.2.1 // indirect
12 12
 	github.com/jarcoal/httpmock v1.0.8
13 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 15
 	github.com/prometheus/client_golang v1.23.2
16 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 18
 	github.com/rs/zerolog v1.34.0
19 19
 	github.com/smira/go-statsd v1.3.4
20 20
 	github.com/stretchr/objx v0.5.2 // indirect
21 21
 	github.com/stretchr/testify v1.11.1
22 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 26
 	google.golang.org/protobuf v1.36.11 // indirect
27 27
 )
28 28
 
@@ -49,8 +49,8 @@ require (
49 49
 	github.com/prometheus/client_model v0.6.2 // indirect
50 50
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
51 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 54
 	golang.org/x/tools v0.41.0 // indirect
55 55
 	gopkg.in/yaml.v3 v3.0.1 // indirect
56 56
 )

+ 14
- 14
go.sum Прегледај датотеку

@@ -53,8 +53,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
53 53
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
54 54
 github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
55 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 58
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
59 59
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
60 60
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -70,8 +70,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
70 70
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
71 71
 github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
72 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 75
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
76 76
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
77 77
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -105,12 +105,12 @@ github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/
105 105
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
106 106
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
107 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 110
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
111 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 114
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
115 115
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
116 116
 golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
@@ -119,13 +119,13 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
119 119
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
120 120
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
121 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 124
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
125 125
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
126 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 129
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
130 130
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
131 131
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -135,8 +135,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
135 135
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136 136
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
137 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 140
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
141 141
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
142 142
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

+ 6
- 1
internal/cli/access.go Прегледај датотеку

@@ -101,8 +101,13 @@ func (a *Access) Run(cli *CLI, version string) error {
101 101
 }
102 102
 
103 103
 func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
104
+	dialer := ntw.NativeDialer()
104 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 113
 	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx

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

@@ -46,13 +46,13 @@ func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
46 46
 
47 47
 	base := network.New(
48 48
 		resolver,
49
-		"mtg/"+version,
49
+		"",
50 50
 		conf.Network.Timeout.TCP.Get(0),
51 51
 		conf.Network.Timeout.HTTP.Get(0),
52 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 56
 	for idx, v := range conf.Network.Proxies {
57 57
 		value, err := network.NewProxyNetwork(base, v.Get(nil))
58 58
 		if err != nil {
@@ -239,6 +239,11 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
239 239
 		return fmt.Errorf("cannot build ip allowlist: %w", err)
240 240
 	}
241 241
 
242
+	doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
243
+	for i, v := range conf.Defense.Doppelganger.URLs {
244
+		doppelGangerURLs[i] = v.String()
245
+	}
246
+
242 247
 	opts := mtglib.ProxyOpts{
243 248
 		Logger:          logger,
244 249
 		Network:         ntw,
@@ -256,6 +261,11 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
256 261
 
257 262
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
258 263
 		TolerateTimeSkewness:     conf.TolerateTimeSkewness.Value,
264
+
265
+		DoppelGangerURLs:    doppelGangerURLs,
266
+		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
267
+		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
268
+		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
259 269
 	}
260 270
 
261 271
 	proxy, err := mtglib.NewProxy(opts)

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

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

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

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

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

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

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

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

+ 5
- 0
internal/testlib/mtglib_network_mock.go Прегледај датотеку

@@ -2,6 +2,7 @@ package testlib
2 2
 
3 3
 import (
4 4
 	"context"
5
+	"net"
5 6
 	"net/http"
6 7
 
7 8
 	"github.com/9seconds/mtg/v2/essentials"
@@ -24,6 +25,10 @@ func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address st
24 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 32
 func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
28 33
 	network, address string) (essentials.Conn, error),
29 34
 ) *http.Client {

+ 18
- 18
mise.lock Прегледај датотеку

@@ -1,11 +1,11 @@
1 1
 [[tools.go]]
2
-version = "1.26.0"
2
+version = "1.26.1"
3 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 10
 [[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]]
11 11
 version = "latest"
@@ -24,19 +24,19 @@ version = "0.9.2"
24 24
 backend = "go:mvdan.cc/gofumpt"
25 25
 
26 26
 [[tools.golangci-lint]]
27
-version = "2.10.1"
27
+version = "2.11.3"
28 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 35
 [[tools.goreleaser]]
36
-version = "2.14.1"
36
+version = "2.14.3"
37 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 Прегледај датотеку

@@ -99,6 +99,13 @@ const (
99 99
 	// reads from Telegram after which connection will be terminated. This is
100 100
 	// required to abort stale connections.
101 101
 	TCPRelayReadTimeout = 20 * time.Second
102
+
103
+	// DoppelGangerPerRaid defines a number of requests to each URL
104
+	// per raid.
105
+	DoppelGangerPerRaid = 10
106
+
107
+	// DoppelGangerEach defines a time period between each crawl attempt.
108
+	DoppelGangerEach = 6 * time.Hour
102 109
 )
103 110
 
104 111
 // Network defines a knowledge how to work with a network. It may sound fun but
@@ -117,13 +124,17 @@ type Network interface {
117 124
 	// Dial establishes context-free TCP connections.
118 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 128
 	// establishing TCP connections.
122 129
 	DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
123 130
 
124 131
 	// MakeHTTPClient build an HTTP client with given dial function. If nothing is
125 132
 	// provided, then DialContext of this interface is going to be used.
126 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 140
 // AntiReplayCache is an interface that is used to detect replay attacks based

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

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

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

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

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

@@ -0,0 +1,130 @@
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 Прегледај датотеку

@@ -0,0 +1,293 @@
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 Прегледај датотеку

@@ -0,0 +1,184 @@
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 Прегледај датотеку

@@ -0,0 +1,107 @@
1
+package doppel
2
+
3
+import (
4
+	"bytes"
5
+	"sync"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/9seconds/mtg/v2/internal/testlib"
10
+	"github.com/stretchr/testify/mock"
11
+	"github.com/stretchr/testify/suite"
12
+)
13
+
14
+type GangerTestSuite struct {
15
+	TLSServerTestSuite
16
+
17
+	log *LoggerMock
18
+	g   *Ganger
19
+}
20
+
21
+func (suite *GangerTestSuite) SetupTest() {
22
+	suite.TLSServerTestSuite.SetupTest()
23
+
24
+	suite.log = &LoggerMock{}
25
+	suite.log.
26
+		On("Info", mock.AnythingOfType("string")).
27
+		Maybe()
28
+	suite.log.
29
+		On("WarningError", mock.AnythingOfType("string"), mock.Anything).
30
+		Maybe()
31
+
32
+	suite.g = NewGanger(suite.ctx, suite.network, suite.log, time.Hour, 1, suite.urls, 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 Прегледај датотеку

@@ -0,0 +1,43 @@
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 Прегледај датотеку

@@ -0,0 +1,107 @@
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 Прегледај датотеку

@@ -0,0 +1,6 @@
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 Прегледај датотеку

@@ -0,0 +1,105 @@
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 Прегледај датотеку

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

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

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

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

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

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

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

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

@@ -0,0 +1,170 @@
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 Прегледај датотеку

@@ -0,0 +1,219 @@
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 Прегледај датотеку

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

+ 0
- 21
mtglib/internal/faketls/client_hello_fuzz_test.go Прегледај датотеку

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

+ 0
- 191
mtglib/internal/faketls/client_hello_test.go Прегледај датотеку

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

+ 0
- 72
mtglib/internal/faketls/conn.go Прегледај датотеку

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

+ 0
- 153
mtglib/internal/faketls/conn_test.go Прегледај датотеку

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

+ 0
- 59
mtglib/internal/faketls/init.go Прегледај датотеку

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

+ 0
- 84
mtglib/internal/faketls/record/init.go Прегледај датотеку

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

+ 0
- 79
mtglib/internal/faketls/record/init_test.go Прегледај датотеку

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

+ 0
- 20
mtglib/internal/faketls/record/pools.go Прегледај датотеку

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

+ 0
- 86
mtglib/internal/faketls/record/record.go Прегледај датотеку

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

+ 0
- 110
mtglib/internal/faketls/record/record_test.go Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/05eb6b71f87b6802.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/4eef4abc15b206b6.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/736f358216afe91f.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/8405d94222bd0b6a.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/9036f76e517f0cd1.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/9244766a0fe4a02a.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/9255c73d3de76e7b.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/aeb65b9924315cf8.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/b0acd44296056b54.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/c0545a13fd9a3fa3.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/f083f4501668b759.json Прегледај датотеку

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

+ 0
- 6
mtglib/internal/faketls/record/testdata/f5696bcdffd11706.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-bad-fa2e46cdb33e2a1b.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-19dfe38384b9884b.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-48f8a72a56f3174a.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-651054256093c6cd.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-79d01ef18a9d2621.json Прегледај датотеку

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

+ 0
- 8
mtglib/internal/faketls/testdata/client-hello-ok-7a5569f05b118145.json Прегледај датотеку

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

+ 0
- 91
mtglib/internal/faketls/welcome.go Прегледај датотеку

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

+ 0
- 82
mtglib/internal/faketls/welcome_test.go Прегледај датотеку

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

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

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

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

@@ -6,6 +6,7 @@ import (
6 6
 	"io"
7 7
 
8 8
 	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
9 10
 )
10 11
 
11 12
 func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) {
@@ -35,7 +36,7 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
35 36
 }
36 37
 
37 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 41
 	defer src.CloseRead()  //nolint: errcheck
41 42
 	defer dst.CloseWrite() //nolint: errcheck

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,135 @@
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 Прегледај датотеку

@@ -0,0 +1,130 @@
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 Прегледај датотеку

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

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-19dfe38384b9884b.json Прегледај датотеку

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

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-48f8a72a56f3174a.json Прегледај датотеку

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

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-651054256093c6cd.json Прегледај датотеку

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

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-79d01ef18a9d2621.json Прегледај датотеку

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

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-7a5569f05b118145.json Прегледај датотеку

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

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

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

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

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

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

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

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

@@ -11,10 +11,11 @@ import (
11 11
 
12 12
 	"github.com/9seconds/mtg/v2/essentials"
13 13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
14
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
15
-	"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
14
+	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
16 15
 	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
17 16
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
17
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
18
+	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
18 19
 	"github.com/panjf2000/ants/v2"
19 20
 )
20 21
 
@@ -32,6 +33,7 @@ type Proxy struct {
32 33
 	workerPool                  *ants.PoolWithFunc
33 34
 	telegram                    *dc.Telegram
34 35
 	configUpdater               *dc.PublicConfigUpdater
36
+	doppelGanger                *doppel.Ganger
35 37
 	clientObfuscatror           obfuscation.Obfuscator
36 38
 
37 39
 	secret          Secret
@@ -80,15 +82,22 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
80 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 96
 		return
87 97
 	}
88 98
 
89 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 101
 		return
93 102
 	}
94 103
 
@@ -155,59 +164,40 @@ func (p *Proxy) Shutdown() {
155 164
 	p.streamWaitGroup.Wait()
156 165
 	p.workerPool.Release()
157 166
 	p.configUpdater.Wait()
167
+	p.doppelGanger.Shutdown()
158 168
 
159 169
 	p.allowlist.Shutdown()
160 170
 	p.blocklist.Shutdown()
161 171
 }
162 172
 
163 173
 func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
164
-	rec := record.AcquireRecord()
165
-	defer record.ReleaseRecord(rec)
166
-
167 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 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 184
 		p.doDomainFronting(ctx, rewind)
190
-
191 185
 		return false
192 186
 	}
193 187
 
194
-	if p.antiReplayCache.SeenBefore(hello.SessionID) {
188
+	if p.antiReplayCache.SeenBefore(clientHello.SessionID) {
195 189
 		p.logger.Warning("replay attack has been detected!")
196 190
 		p.eventStream.Send(p.ctx, NewEventReplayAttack(ctx.streamID))
197 191
 		p.doDomainFronting(ctx, rewind)
198
-
199 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 196
 		p.logger.InfoError("cannot send welcome packet", err)
204
-
205 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 202
 	return true
213 203
 }
@@ -282,13 +272,16 @@ func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) {
282 272
 	p.eventStream.Send(p.ctx, NewEventDomainFronting(ctx.streamID))
283 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 277
 	if err != nil {
287 278
 		p.logger.WarningError("cannot dial to the fronting domain", err)
288 279
 
289 280
 		return
290 281
 	}
291 282
 
283
+	frontConn := essentials.WrapNetConn(fConn)
284
+
292 285
 	if p.domainFrontingProxyProtocol {
293 286
 		frontConn = newConnProxyProtocol(ctx.clientConn, frontConn)
294 287
 	}
@@ -338,6 +331,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
338 331
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
339 332
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
340 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 343
 		configUpdater: dc.NewPublicConfigUpdater(
342 344
 			tg,
343 345
 			updatersLogger.Named("public-config"),
@@ -349,6 +351,8 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
349 351
 		domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol,
350 352
 	}
351 353
 
354
+	proxy.doppelGanger.Run()
355
+
352 356
 	if opts.AutoUpdate {
353 357
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
354 358
 		proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")

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

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

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

@@ -60,6 +60,10 @@ func (n *network) DialContext(ctx context.Context, protocol, address string) (es
60 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 67
 func (n *network) MakeHTTPClient(dialFunc func(ctx context.Context,
64 68
 	network, address string) (essentials.Conn, error),
65 69
 ) *http.Client {

+ 2
- 1
network/v2/base_network_test.go Прегледај датотеку

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

+ 3
- 9
network/v2/init.go Прегледај датотеку

@@ -11,10 +11,7 @@ package network
11 11
 
12 12
 import (
13 13
 	"errors"
14
-	"net"
15 14
 	"time"
16
-
17
-	"github.com/9seconds/mtg/v2/mtglib"
18 15
 )
19 16
 
20 17
 const (
@@ -31,15 +28,12 @@ const (
31 28
 	// probes.
32 29
 	DefaultTCPKeepAlivePeriod = 10 * time.Second
33 30
 
31
+	// User Agent to use in HTTP client.
32
+	UserAgent = "curl/8.5.0"
33
+
34 34
 	// tcpLingerTimeout defines a number of seconds to wait for sending
35 35
 	// unacknowledged data.
36 36
 	tcpLingerTimeout = 1
37 37
 )
38 38
 
39 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 Прегледај датотеку

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

+ 6
- 1
network/v2/network.go Прегледај датотеку

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

+ 3
- 2
network/v2/proxy_network.go Прегледај датотеку

@@ -6,11 +6,12 @@ import (
6 6
 	"net/url"
7 7
 
8 8
 	"github.com/9seconds/mtg/v2/essentials"
9
+	"github.com/9seconds/mtg/v2/mtglib"
9 10
 	"golang.org/x/net/proxy"
10 11
 )
11 12
 
12 13
 type proxyNetwork struct {
13
-	Network
14
+	mtglib.Network
14 15
 	client proxy.ContextDialer
15 16
 }
16 17
 
@@ -23,7 +24,7 @@ func (p proxyNetwork) DialContext(ctx context.Context, network, address string)
23 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 28
 	socks, err := proxy.FromURL(proxyURL, base.NativeDialer())
28 29
 	if err != nil {
29 30
 		return nil, fmt.Errorf("cannot build proxy dialer: %w", err)

+ 3
- 2
network/v2/socks_proxy_test.go Прегледај датотеку

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

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