Просмотр исходного кода

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

tags/v2.2.5
9seconds 1 месяц назад
Родитель
Сommit
8b3c622ea6
39 измененных файлов: 484 добавлений и 241 удалений
  1. 33
    0
      .github/workflows/ci.yaml
  2. 7
    5
      .github/workflows/codeql-analysis.yml
  3. 6
    0
      .mise.toml
  4. 3
    0
      README.md
  5. Двоичные данные
      default.pgo
  6. 1
    1
      events/event_stream.go
  7. 7
    0
      example.config.toml
  8. 2
    2
      go.mod
  9. 4
    13
      go.sum
  10. 6
    0
      internal/cli/access.go
  11. 10
    3
      internal/cli/doctor.go
  12. 2
    0
      internal/cli/run_proxy.go
  13. 6
    4
      internal/config/config.go
  14. 26
    0
      internal/config/config_test.go
  15. 6
    4
      internal/config/parse.go
  16. 4
    0
      internal/config/testdata/public_ip.toml
  17. 3
    0
      internal/config/testdata/public_ip_invalid.toml
  18. 3
    0
      internal/config/testdata/public_ip_v4_only.toml
  19. 19
    0
      mtglib/conns.go
  20. 0
    35
      mtglib/internal/doppel/clock.go
  21. 0
    80
      mtglib/internal/doppel/clock_test.go
  22. 21
    19
      mtglib/internal/doppel/conn.go
  23. 2
    2
      mtglib/internal/doppel/conn_test.go
  24. 92
    10
      mtglib/internal/doppel/ganger.go
  25. 47
    12
      mtglib/internal/doppel/scout.go
  26. 17
    4
      mtglib/internal/doppel/scout_conn.go
  27. 14
    3
      mtglib/internal/doppel/scout_conn_collected.go
  28. 4
    4
      mtglib/internal/doppel/scout_conn_collected_test.go
  29. 2
    2
      mtglib/internal/doppel/scout_test.go
  30. 2
    2
      mtglib/internal/doppel/stats.go
  31. 13
    0
      mtglib/internal/relay/pool_settings_constrained.go
  32. 9
    0
      mtglib/internal/relay/pool_settings_other.go
  33. 18
    0
      mtglib/internal/relay/pools.go
  34. 6
    6
      mtglib/internal/relay/relay.go
  35. 34
    15
      mtglib/internal/tls/fake/server_side.go
  36. 32
    6
      mtglib/internal/tls/fake/server_side_test.go
  37. 14
    8
      mtglib/proxy.go
  38. 9
    0
      mtglib/proxy_opts.go
  39. 0
    1
      run_profile_tag_prof.go

+ 33
- 0
.github/workflows/ci.yaml Просмотреть файл

@@ -119,6 +119,39 @@ jobs:
119 119
       - name: Run linter
120 120
         run: mise tasks run lint
121 121
 
122
+  artifacts:
123
+    name: Build release artifacts
124
+    runs-on: ubuntu-latest
125
+    timeout-minutes: 10
126
+    steps:
127
+      - name: Checkout
128
+        uses: actions/checkout@v6
129
+        with:
130
+          submodules: recursive
131
+
132
+      - uses: jdx/mise-action@v3
133
+        name: Install mise
134
+
135
+      - name: Cache Go modules
136
+        uses: actions/cache@v5
137
+        with:
138
+          path: ~/go/pkg/mod
139
+          key: ${{ runner.os }}-gomod-${{ hashFiles('go.sum') }}
140
+          restore-keys: |
141
+            ${{ runner.os }}-gomod-
142
+
143
+      - name: Cache cross-compilation build
144
+        uses: actions/cache@v5
145
+        with:
146
+          path: ~/.cache/go-build
147
+          key: ${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-${{ hashFiles('**/*.go') }}
148
+          restore-keys: |
149
+            ${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-
150
+            ${{ runner.os }}-goreleaser-
151
+
152
+      - name: Run release
153
+        run: mise tasks run release
154
+
122 155
   docker:
123 156
     name: Docker
124 157
     runs-on: ubuntu-latest

+ 7
- 5
.github/workflows/codeql-analysis.yml Просмотреть файл

@@ -21,7 +21,7 @@ permissions:
21 21
 
22 22
 on:
23 23
   push:
24
-    branches: 
24
+    branches:
25 25
       - master
26 26
       - stable
27 27
   pull_request:
@@ -45,11 +45,13 @@ jobs:
45 45
 
46 46
     steps:
47 47
     - name: Checkout repository
48
-      uses: actions/checkout@v2
48
+      uses: actions/checkout@v6
49
+      with:
50
+        submodules: recursive
49 51
 
50 52
     # Initializes the CodeQL tools for scanning.
51 53
     - name: Initialize CodeQL
52
-      uses: github/codeql-action/init@v1
54
+      uses: github/codeql-action/init@v4
53 55
       with:
54 56
         languages: ${{ matrix.language }}
55 57
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -60,7 +62,7 @@ jobs:
60 62
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
61 63
     # If this step fails, then you should remove it and run the build manually (see below)
62 64
     - name: Autobuild
63
-      uses: github/codeql-action/autobuild@v1
65
+      uses: github/codeql-action/autobuild@v4
64 66
 
65 67
     # ℹ️ Command-line programs to run using the OS shell.
66 68
     # 📚 https://git.io/JvXDl
@@ -74,4 +76,4 @@ jobs:
74 76
     #   make release
75 77
 
76 78
     - name: Perform CodeQL Analysis
77
-      uses: github/codeql-action/analyze@v1
79
+      uses: github/codeql-action/analyze@v4

+ 6
- 0
.mise.toml Просмотреть файл

@@ -16,6 +16,12 @@ sources = ["**/*.go", "go.mod", "go.sum"]
16 16
 outputs = ["mtg"]
17 17
 run = "go build"
18 18
 
19
+[tasks."build:prof"]
20
+description = "Build binary with profiling enabled"
21
+sources = ["**/*.go", "go.mod", "go.sum"]
22
+outputs = ["mtg"]
23
+run = "go build -tags prof"
24
+
19 25
 [tasks.update]
20 26
 description = "Update dependencies"
21 27
 run = [

+ 3
- 0
README.md Просмотреть файл

@@ -91,6 +91,9 @@ that probably matter.
91 91
   software. I also believe that in the case of throwout proxies, this
92 92
   the feature is a useless luxury.
93 93
 
94
+  This is very controversial topic. Please read [rationale (in russian)](https://github.com/9seconds/mtg/issues/376#issuecomment-4118726699)
95
+  and use [mtg-multi](https://github.com/dolonet/mtg-multi) fork if you are disagree with.
96
+
94 97
 * **No adtag support**
95 98
 
96 99
   Please read [Version 2](#version-2) chapter.

Двоичные данные
default.pgo Просмотреть файл


+ 1
- 1
events/event_stream.go Просмотреть файл

@@ -38,7 +38,7 @@ func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
38 38
 	select {
39 39
 	case <-ctx.Done():
40 40
 	case <-e.ctx.Done():
41
-	case e.chans[int(chanNo)%len(e.chans)] <- evt:
41
+	case e.chans[chanNo%uint32(len(e.chans))] <- evt:
42 42
 	}
43 43
 }
44 44
 

+ 7
- 0
example.config.toml Просмотреть файл

@@ -48,6 +48,13 @@ concurrency = 8192
48 48
 #     Only ipv4 connectivity is used
49 49
 prefer-ip = "prefer-ipv6"
50 50
 
51
+# Public IP addresses of this server. Used by 'mtg access' to generate
52
+# proxy links and by 'mtg doctor' to validate SNI-DNS match.
53
+# If not set, mtg tries to detect them automatically via ifconfig.co.
54
+# Set these if ifconfig.co is unreachable from your server.
55
+# public-ipv4 = "1.2.3.4"
56
+# public-ipv6 = "2001:db8::1"
57
+
51 58
 # If this setting is set, then mtg will try to get proxy updates from Telegram
52 59
 # Usually this is completely fine to have it disabled, because mtg has a list
53 60
 # of some core proxies hardcoded.

+ 2
- 2
go.mod Просмотреть файл

@@ -15,7 +15,7 @@ require (
15 15
 	github.com/prometheus/client_golang v1.23.2
16 16
 	github.com/prometheus/common v0.67.5 // indirect
17 17
 	github.com/prometheus/procfs v0.20.1 // indirect
18
-	github.com/rs/zerolog v1.34.0
18
+	github.com/rs/zerolog v1.35.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
@@ -29,7 +29,7 @@ require (
29 29
 require (
30 30
 	github.com/beevik/ntp v1.5.0
31 31
 	github.com/ncruces/go-dns v1.3.2
32
-	github.com/pelletier/go-toml/v2 v2.2.4
32
+	github.com/pelletier/go-toml/v2 v2.3.0
33 33
 	github.com/pires/go-proxyproto v0.11.0
34 34
 	github.com/things-go/go-socks5 v0.1.0
35 35
 	github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e

+ 4
- 13
go.sum Просмотреть файл

@@ -18,14 +18,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
18 18
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
19 19
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
20 20
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
21
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
22 21
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
23 22
 github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
24 23
 github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
25 24
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 25
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
27 26
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
28
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
29 27
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
30 28
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
31 29
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -40,11 +38,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
40 38
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
41 39
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
42 40
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
43
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
44 41
 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
45 42
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
46
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
47
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
48 43
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
49 44
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
50 45
 github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
@@ -59,11 +54,10 @@ github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4w
59 54
 github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
60 55
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
61 56
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
62
-github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
63
-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
57
+github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
58
+github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
64 59
 github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
65 60
 github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
66
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
67 61
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
68 62
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
69 63
 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -76,9 +70,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
76 70
 github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
77 71
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
78 72
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
79
-github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
80
-github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
81
-github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
73
+github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
74
+github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
82 75
 github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c=
83 76
 github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
84 77
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -133,10 +126,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
133 126
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
134 127
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
135 128
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
137 129
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
138 130
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
139
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
140 131
 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
141 132
 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
142 133
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

+ 6
- 0
internal/cli/access.go Просмотреть файл

@@ -58,6 +58,9 @@ func (a *Access) Run(cli *CLI, version string) error {
58 58
 
59 59
 	wg.Go(func() {
60 60
 		ip := a.PublicIPv4
61
+		if ip == nil {
62
+			ip = conf.PublicIPv4.Get(nil)
63
+		}
61 64
 		if ip == nil {
62 65
 			ip = getIP(ntw, "tcp4")
63 66
 		}
@@ -70,6 +73,9 @@ func (a *Access) Run(cli *CLI, version string) error {
70 73
 	})
71 74
 	wg.Go(func() {
72 75
 		ip := a.PublicIPv6
76
+		if ip == nil {
77
+			ip = conf.PublicIPv6.Get(nil)
78
+		}
73 79
 		if ip == nil {
74 80
 			ip = getIP(ntw, "tcp6")
75 81
 		}

+ 10
- 3
internal/cli/doctor.go Просмотреть файл

@@ -332,13 +332,20 @@ func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) boo
332 332
 		return false
333 333
 	}
334 334
 
335
-	ourIP4 := getIP(ntw, "tcp4")
336
-	ourIP6 := getIP(ntw, "tcp6")
335
+	ourIP4 := d.conf.PublicIPv4.Get(nil)
336
+	if ourIP4 == nil {
337
+		ourIP4 = getIP(ntw, "tcp4")
338
+	}
339
+
340
+	ourIP6 := d.conf.PublicIPv6.Get(nil)
341
+	if ourIP6 == nil {
342
+		ourIP6 = getIP(ntw, "tcp6")
343
+	}
337 344
 
338 345
 	if ourIP4 == nil && ourIP6 == nil {
339 346
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
340 347
 			"description": "cannot detect public IP address",
341
-			"error":       errors.New("ifconfig.co is unreachable for both IPv4 and IPv6"),
348
+			"error":       errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
342 349
 		})
343 350
 		return false
344 351
 	}

+ 2
- 0
internal/cli/run_proxy.go Просмотреть файл

@@ -5,6 +5,7 @@ import (
5 5
 	"fmt"
6 6
 	"net"
7 7
 	"os"
8
+	"time"
8 9
 
9 10
 	"github.com/9seconds/mtg/v2/antireplay"
10 11
 	"github.com/9seconds/mtg/v2/events"
@@ -262,6 +263,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
262 263
 
263 264
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
264 265
 		TolerateTimeSkewness:     conf.TolerateTimeSkewness.Value,
266
+		IdleTimeout:              conf.Network.Timeout.Idle.Get(time.Minute),
265 267
 
266 268
 		DoppelGangerURLs:    doppelGangerURLs,
267 269
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),

+ 6
- 4
internal/config/config.go Просмотреть файл

@@ -35,6 +35,8 @@ type Config struct {
35 35
 	DomainFrontingProxyProtocol TypeBool        `json:"domainFrontingProxyProtocol"`
36 36
 	TolerateTimeSkewness        TypeDuration    `json:"tolerateTimeSkewness"`
37 37
 	Concurrency                 TypeConcurrency `json:"concurrency"`
38
+	PublicIPv4                  TypeIP          `json:"publicIpv4"`
39
+	PublicIPv6                  TypeIP          `json:"publicIpv6"`
38 40
 	DomainFronting              struct {
39 41
 		IP            TypeIP   `json:"ip"`
40 42
 		Port          TypePort `json:"port"`
@@ -50,10 +52,10 @@ type Config struct {
50 52
 		Blocklist    ListConfig `json:"blocklist"`
51 53
 		Allowlist    ListConfig `json:"allowlist"`
52 54
 		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"`
55
+			URLs            []TypeHttpsURL  `json:"urls"`
56
+			Repeats         TypeConcurrency `json:"repeats_per_raid"`
57
+			UpdateEach      TypeDuration    `json:"raid_each"`
58
+			DRS             TypeBool        `json:"drs"`
57 59
 		} `json:"doppelganger"`
58 60
 	} `json:"defense"`
59 61
 	Network struct {

+ 26
- 0
internal/config/config_test.go Просмотреть файл

@@ -42,6 +42,32 @@ func (suite *ConfigTestSuite) TestParseMinimalConfig() {
42 42
 	suite.Equal("0.0.0.0:3128", conf.BindTo.String())
43 43
 }
44 44
 
45
+func (suite *ConfigTestSuite) TestParsePublicIP() {
46
+	conf, err := config.Parse(suite.ReadConfig("public_ip.toml"))
47
+	suite.NoError(err)
48
+	suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
49
+	suite.Equal("2001:db8::1", conf.PublicIPv6.Get(nil).String())
50
+}
51
+
52
+func (suite *ConfigTestSuite) TestParsePublicIPv4Only() {
53
+	conf, err := config.Parse(suite.ReadConfig("public_ip_v4_only.toml"))
54
+	suite.NoError(err)
55
+	suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
56
+	suite.Nil(conf.PublicIPv6.Get(nil))
57
+}
58
+
59
+func (suite *ConfigTestSuite) TestParsePublicIPInvalid() {
60
+	_, err := config.Parse(suite.ReadConfig("public_ip_invalid.toml"))
61
+	suite.Error(err)
62
+}
63
+
64
+func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
65
+	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
66
+	suite.NoError(err)
67
+	suite.Nil(conf.PublicIPv4.Get(nil))
68
+	suite.Nil(conf.PublicIPv6.Get(nil))
69
+}
70
+
45 71
 func (suite *ConfigTestSuite) TestString() {
46 72
 	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
47 73
 	suite.NoError(err)

+ 6
- 4
internal/config/parse.go Просмотреть файл

@@ -21,6 +21,8 @@ type tomlConfig struct {
21 21
 	DomainFrontingProxyProtocol bool   `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"`
22 22
 	TolerateTimeSkewness        string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
23 23
 	Concurrency                 uint   `toml:"concurrency" json:"concurrency,omitempty"`
24
+	PublicIPv4                  string `toml:"public-ipv4" json:"publicIpv4,omitempty"`
25
+	PublicIPv6                  string `toml:"public-ipv6" json:"publicIpv6,omitempty"`
24 26
 	DomainFronting              struct {
25 27
 		IP            string `toml:"ip" json:"ip,omitempty"`
26 28
 		Port          uint   `toml:"port" json:"port,omitempty"`
@@ -45,10 +47,10 @@ type tomlConfig struct {
45 47
 			UpdateEach          string   `toml:"update-each" json:"updateEach,omitempty"`
46 48
 		} `toml:"allowlist" json:"allowlist,omitempty"`
47 49
 		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"`
50
+			URLs            []string `toml:"urls" json:"urls,omitempty"`
51
+			Repeats         uint     `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
52
+			UpdateEach      string   `toml:"raid-each" json:"raid_each,omitempty"`
53
+			DRS             bool     `toml:"drs" json:"drs,omitempty"`
52 54
 		} `toml:"doppelganger" json:"doppelganger,omitempty"`
53 55
 	} `toml:"defense" json:"defense,omitempty"`
54 56
 	Network struct {

+ 4
- 0
internal/config/testdata/public_ip.toml Просмотреть файл

@@ -0,0 +1,4 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+public-ipv4 = "203.0.113.1"
4
+public-ipv6 = "2001:db8::1"

+ 3
- 0
internal/config/testdata/public_ip_invalid.toml Просмотреть файл

@@ -0,0 +1,3 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+public-ipv4 = "not-an-ip"

+ 3
- 0
internal/config/testdata/public_ip_v4_only.toml Просмотреть файл

@@ -0,0 +1,3 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+public-ipv4 = "203.0.113.1"

+ 19
- 0
mtglib/conns.go Просмотреть файл

@@ -6,6 +6,7 @@ import (
6 6
 	"fmt"
7 7
 	"io"
8 8
 	"net"
9
+	"time"
9 10
 
10 11
 	"github.com/9seconds/mtg/v2/essentials"
11 12
 	"github.com/pires/go-proxyproto"
@@ -95,3 +96,21 @@ func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol {
95 96
 		sourceAddr: source.RemoteAddr(),
96 97
 	}
97 98
 }
99
+
100
+type connIdleTimeout struct {
101
+	essentials.Conn
102
+
103
+	timeout time.Duration
104
+}
105
+
106
+func (c connIdleTimeout) Read(b []byte) (int, error) {
107
+	c.SetReadDeadline(time.Now().Add(c.timeout)) //nolint: errcheck
108
+
109
+	return c.Conn.Read(b) //nolint: wrapcheck
110
+}
111
+
112
+func (c connIdleTimeout) Write(b []byte) (int, error) {
113
+	c.SetWriteDeadline(time.Now().Add(c.timeout)) //nolint: errcheck
114
+
115
+	return c.Conn.Write(b) //nolint: wrapcheck
116
+}

+ 0
- 35
mtglib/internal/doppel/clock.go Просмотреть файл

@@ -1,35 +0,0 @@
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
-}

+ 0
- 80
mtglib/internal/doppel/clock_test.go Просмотреть файл

@@ -1,80 +0,0 @@
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
-}

+ 21
- 19
mtglib/internal/doppel/conn.go Просмотреть файл

@@ -4,11 +4,19 @@ import (
4 4
 	"bytes"
5 5
 	"context"
6 6
 	"sync"
7
+	"time"
7 8
 
8 9
 	"github.com/9seconds/mtg/v2/essentials"
9 10
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
10 11
 )
11 12
 
13
+var doppelBufPool = sync.Pool{
14
+	New: func() any {
15
+		b := make([]byte, tls.MaxRecordSize)
16
+		return &b
17
+	},
18
+}
19
+
12 20
 type Conn struct {
13 21
 	essentials.Conn
14 22
 
@@ -18,7 +26,7 @@ type Conn struct {
18 26
 type connPayload struct {
19 27
 	ctx         context.Context
20 28
 	ctxCancel   context.CancelCauseFunc
21
-	clock       Clock
29
+	stats       Stats
22 30
 	wg          sync.WaitGroup
23 31
 	writeStream bytes.Buffer
24 32
 	writtenCond sync.Cond
@@ -39,23 +47,23 @@ func (c Conn) Write(p []byte) (int, error) {
39 47
 	return len(p), context.Cause(c.p.ctx)
40 48
 }
41 49
 
42
-func (c Conn) Start() {
43
-	c.p.wg.Go(func() {
44
-		c.start()
45
-	})
46
-}
47
-
48 50
 func (c Conn) start() {
49
-	buf := [tls.MaxRecordSize]byte{}
51
+	bp := doppelBufPool.Get().(*[]byte)
52
+	buf := *bp
53
+	defer doppelBufPool.Put(bp)
54
+
55
+	timer := time.NewTimer(c.p.stats.Delay())
56
+	defer timer.Stop()
50 57
 
51 58
 	for {
52 59
 		select {
53 60
 		case <-c.p.ctx.Done():
54 61
 			return
55
-		case <-c.p.clock.tick:
62
+		case <-timer.C:
63
+			timer.Reset(c.p.stats.Delay())
56 64
 		}
57 65
 
58
-		size := c.p.clock.stats.Size()
66
+		size := c.p.stats.Size()
59 67
 
60 68
 		c.p.writtenCond.L.Lock()
61 69
 		for c.p.writeStream.Len() == 0 && !c.p.done {
@@ -68,7 +76,7 @@ func (c Conn) start() {
68 76
 			continue
69 77
 		}
70 78
 
71
-		if err := tls.WriteRecordInPlace(c.Conn, buf[:], n); err != nil {
79
+		if err := tls.WriteRecordInPlace(c.Conn, buf, n); err != nil {
72 80
 			c.p.ctxCancel(err)
73 81
 			return
74 82
 		}
@@ -86,28 +94,22 @@ func (c Conn) Stop() {
86 94
 	c.p.wg.Wait()
87 95
 }
88 96
 
89
-func NewConn(ctx context.Context, conn essentials.Conn, stats *Stats) Conn {
97
+func NewConn(ctx context.Context, conn essentials.Conn, stats Stats) Conn {
90 98
 	ctx, cancel := context.WithCancelCause(ctx)
91 99
 	rv := Conn{
92 100
 		Conn: conn,
93 101
 		p: &connPayload{
94 102
 			ctx:       ctx,
95 103
 			ctxCancel: cancel,
104
+			stats:     stats,
96 105
 			writtenCond: sync.Cond{
97 106
 				L: &sync.Mutex{},
98 107
 			},
99
-			clock: Clock{
100
-				stats: stats,
101
-				tick:  make(chan struct{}),
102
-			},
103 108
 		},
104 109
 	}
105 110
 
106 111
 	rv.p.writeStream.Grow(tls.DefaultBufferSize)
107 112
 
108
-	rv.p.wg.Go(func() {
109
-		rv.p.clock.Start(ctx)
110
-	})
111 113
 	rv.p.wg.Go(func() {
112 114
 		rv.start()
113 115
 	})

+ 2
- 2
mtglib/internal/doppel/conn_test.go Просмотреть файл

@@ -63,7 +63,7 @@ func (suite *ConnTestSuite) TearDownTest() {
63 63
 }
64 64
 
65 65
 func (suite *ConnTestSuite) makeConn() Conn {
66
-	return NewConn(suite.ctx, suite.connMock, &Stats{
66
+	return NewConn(suite.ctx, suite.connMock, Stats{
67 67
 		k:      2.0,
68 68
 		lambda: 0.01,
69 69
 	})
@@ -152,7 +152,7 @@ func (suite *ConnTestSuite) TestStopDoesNotDeadlockWhenStartIsWaiting() {
152 152
 			ctx, cancel := context.WithCancel(suite.ctx)
153 153
 			defer cancel()
154 154
 
155
-			c := NewConn(ctx, suite.connMock, &Stats{
155
+			c := NewConn(ctx, suite.connMock, Stats{
156 156
 				k:      2.0,
157 157
 				lambda: 0.01,
158 158
 			})

+ 92
- 10
mtglib/internal/doppel/ganger.go Просмотреть файл

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

+ 47
- 12
mtglib/internal/doppel/scout.go Просмотреть файл

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

+ 17
- 4
mtglib/internal/doppel/scout_conn.go Просмотреть файл

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

+ 14
- 3
mtglib/internal/doppel/scout_conn_collected.go Просмотреть файл

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

+ 4
- 4
mtglib/internal/doppel/scout_conn_collected_test.go Просмотреть файл

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

+ 2
- 2
mtglib/internal/doppel/scout_test.go Просмотреть файл

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

+ 2
- 2
mtglib/internal/doppel/stats.go Просмотреть файл

@@ -112,7 +112,7 @@ func (d *Stats) Size() int {
112 112
 	return TLSRecordSizeMax
113 113
 }
114 114
 
115
-func NewStats(durations []time.Duration, drs bool) *Stats {
115
+func NewStats(durations []time.Duration, drs bool) Stats {
116 116
 	n := float64(len(durations))
117 117
 
118 118
 	// in milliseconds
@@ -162,7 +162,7 @@ func NewStats(durations []time.Duration, drs bool) *Stats {
162 162
 	// λ = (Σxᵢᵏ / n)^(1/k)
163 163
 	lambda := math.Pow(sumXK/n, 1.0/k)
164 164
 
165
-	return &Stats{
165
+	return Stats{
166 166
 		k:      k,
167 167
 		lambda: lambda,
168 168
 		drs:    drs,

+ 13
- 0
mtglib/internal/relay/pool_settings_constrained.go Просмотреть файл

@@ -0,0 +1,13 @@
1
+//go:build mips || mipsle
2
+
3
+package relay
4
+
5
+import "github.com/9seconds/mtg/v2/mtglib/internal/tls"
6
+
7
+const (
8
+	// MIPS is quite short in resources, and usually it means that it will run
9
+	// on Microtiks, OpenWRT-based routers or similar hardware. I think it worth
10
+	// to sacrifice a number of read syscalls (read, CPU load) to shrink
11
+	// limited RAM resources.
12
+	bufPoolSize = tls.MaxRecordPayloadSize / 2
13
+)

+ 9
- 0
mtglib/internal/relay/pool_settings_other.go Просмотреть файл

@@ -0,0 +1,9 @@
1
+//go:build !mips && !mipsle
2
+
3
+package relay
4
+
5
+import "github.com/9seconds/mtg/v2/mtglib/internal/tls"
6
+
7
+const (
8
+	bufPoolSize = tls.MaxRecordPayloadSize
9
+)

+ 18
- 0
mtglib/internal/relay/pools.go Просмотреть файл

@@ -0,0 +1,18 @@
1
+package relay
2
+
3
+import "sync"
4
+
5
+var bufPool = sync.Pool{
6
+	New: func() any {
7
+		b := make([]byte, bufPoolSize)
8
+		return &b
9
+	},
10
+}
11
+
12
+func acquireBuffer() *[]byte {
13
+	return bufPool.Get().(*[]byte)
14
+}
15
+
16
+func releaseBuffer(p *[]byte) {
17
+	bufPool.Put(p)
18
+}

+ 6
- 6
mtglib/internal/relay/relay.go Просмотреть файл

@@ -6,7 +6,6 @@ import (
6 6
 	"io"
7 7
 
8 8
 	"github.com/9seconds/mtg/v2/essentials"
9
-	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
10 9
 )
11 10
 
12 11
 func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) {
@@ -16,11 +15,11 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
16 15
 	ctx, cancel := context.WithCancel(ctx)
17 16
 	defer cancel()
18 17
 
19
-	go func() {
20
-		<-ctx.Done()
18
+	stop := context.AfterFunc(ctx, func() {
21 19
 		telegramConn.Close() //nolint: errcheck
22 20
 		clientConn.Close()   //nolint: errcheck
23
-	}()
21
+	})
22
+	defer stop()
24 23
 
25 24
 	closeChan := make(chan struct{})
26 25
 
@@ -36,12 +35,13 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
36 35
 }
37 36
 
38 37
 func pump(log Logger, src, dst essentials.Conn, direction string) {
39
-	var buf [tls.MaxRecordPayloadSize]byte
38
+	buf := acquireBuffer()
39
+	defer releaseBuffer(buf)
40 40
 
41 41
 	defer src.CloseRead()  //nolint: errcheck
42 42
 	defer dst.CloseWrite() //nolint: errcheck
43 43
 
44
-	n, err := io.CopyBuffer(src, dst, buf[:])
44
+	n, err := io.CopyBuffer(src, dst, *buf)
45 45
 
46 46
 	switch {
47 47
 	case err == nil:

+ 34
- 15
mtglib/internal/tls/fake/server_side.go Просмотреть файл

@@ -9,11 +9,18 @@ import (
9 9
 	"io"
10 10
 	rnd "math/rand/v2"
11 11
 
12
-	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
13 12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
14 13
 	"golang.org/x/crypto/curve25519"
15 14
 )
16 15
 
16
+// NoiseParams controls the size of the fake ApplicationData record
17
+// in ServerHello. If Mean is 0, the legacy random range (2500-4700)
18
+// is used.
19
+type NoiseParams struct {
20
+	Mean   int
21
+	Jitter int
22
+}
23
+
17 24
 const (
18 25
 	TypeHandshakeServer = 0x02
19 26
 	ChangeCipherValue   = 0x01
@@ -33,13 +40,13 @@ var serverHelloSuffix = []byte{
33 40
 	0x00, 0x20, // 32 bytes of key
34 41
 }
35 42
 
36
-func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello) error {
43
+func SendServerHello(w io.Writer, secret []byte, clientHello *ClientHello, noise NoiseParams) error {
37 44
 	buf := &bytes.Buffer{}
38 45
 	buf.Grow(tls.MaxRecordSize)
39 46
 
40 47
 	generateServerHello(buf, clientHello)
41 48
 	generateChangeCipherValue(buf)
42
-	generateNoise(buf)
49
+	generateNoise(buf, noise)
43 50
 
44 51
 	packet := buf.Bytes()
45 52
 	digest := hmac.New(sha256.New, secret)
@@ -125,19 +132,31 @@ func generateChangeCipherValue(buf *bytes.Buffer) {
125 132
 	buf.WriteByte(ChangeCipherValue)
126 133
 }
127 134
 
128
-func generateNoise(buf *bytes.Buffer) {
129
-	data := make(
130
-		[]byte,
131
-		int64(
132
-			doppel.TLSRecordSizeStart+rnd.IntN(
133
-				doppel.TLSRecordSizeAccel-doppel.TLSRecordSizeStart,
134
-			),
135
-		),
136
-	)
137
-
138
-	if _, err := rand.Read(data[:]); err != nil {
135
+// generateNoise writes a single ApplicationData record mimicking the combined
136
+// size of a real TLS 1.3 encrypted server handshake (EncryptedExtensions +
137
+// Certificate chain + CertificateVerify + Finished).
138
+//
139
+// NOTE: Must be exactly ONE ApplicationData record — the Telegram client reads
140
+// ServerHello + CCS + 1 ApplicationData and computes HMAC over all three.
141
+// Multiple records would cause HMAC mismatch and connection failure.
142
+func generateNoise(buf *bytes.Buffer, noise NoiseParams) {
143
+	var size int
144
+
145
+	if noise.Mean > 0 && noise.Jitter > 0 {
146
+		// Calibrated: use measured cert chain size ± jitter.
147
+		size = noise.Mean - noise.Jitter + rnd.IntN(2*noise.Jitter)
148
+		if size < 1000 {
149
+			size = 1000
150
+		}
151
+	} else {
152
+		// Legacy fallback: random in 2500-4700 range.
153
+		size = 2500 + rnd.IntN(2200)
154
+	}
155
+
156
+	data := make([]byte, size)
157
+	if _, err := rand.Read(data); err != nil {
139 158
 		panic(err)
140 159
 	}
141 160
 
142
-	tls.WriteRecord(buf, data[:]) //nolint: errcheck
161
+	tls.WriteRecord(buf, data) //nolint: errcheck
143 162
 }

+ 32
- 6
mtglib/internal/tls/fake/server_side_test.go Просмотреть файл

@@ -8,7 +8,6 @@ import (
8 8
 	"testing"
9 9
 
10 10
 	"github.com/9seconds/mtg/v2/mtglib"
11
-	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
12 11
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
13 12
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
14 13
 	"github.com/stretchr/testify/suite"
@@ -39,7 +38,7 @@ func (suite *SendServerHelloTestSuite) SetupTest() {
39 38
 }
40 39
 
41 40
 func (suite *SendServerHelloTestSuite) TestRecordStructure() {
42
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
41
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
43 42
 	suite.NoError(err)
44 43
 
45 44
 	var rec bytes.Buffer
@@ -59,13 +58,13 @@ func (suite *SendServerHelloTestSuite) TestRecordStructure() {
59 58
 	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
60 59
 	suite.NoError(err)
61 60
 	suite.Equal(byte(tls.TypeApplicationData), recordType)
62
-	suite.Greater(length, int64(doppel.TLSRecordSizeStart))
61
+	suite.Greater(length, int64(2500))
63 62
 
64 63
 	suite.Empty(suite.buf.Bytes())
65 64
 }
66 65
 
67 66
 func (suite *SendServerHelloTestSuite) TestHMAC() {
68
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
67
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
69 68
 	suite.NoError(err)
70 69
 
71 70
 	packet := make([]byte, suite.buf.Len())
@@ -83,7 +82,7 @@ func (suite *SendServerHelloTestSuite) TestHMAC() {
83 82
 }
84 83
 
85 84
 func (suite *SendServerHelloTestSuite) TestHandshakePayload() {
86
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
85
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
87 86
 	suite.NoError(err)
88 87
 
89 88
 	packet := suite.buf.Bytes()
@@ -105,7 +104,7 @@ func (suite *SendServerHelloTestSuite) TestHandshakePayload() {
105 104
 }
106 105
 
107 106
 func (suite *SendServerHelloTestSuite) TestChangeCipherSpec() {
108
-	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello)
107
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, fake.NoiseParams{})
109 108
 	suite.NoError(err)
110 109
 
111 110
 	// Skip first record
@@ -124,6 +123,33 @@ func (suite *SendServerHelloTestSuite) TestChangeCipherSpec() {
124 123
 	suite.Equal([]byte{fake.ChangeCipherValue}, rec.Bytes())
125 124
 }
126 125
 
126
+func (suite *SendServerHelloTestSuite) TestCalibratedNoiseSize() {
127
+	noise := fake.NoiseParams{Mean: 6480, Jitter: 100}
128
+	err := fake.SendServerHello(suite.buf, suite.secret.Key[:], suite.hello, noise)
129
+	suite.NoError(err)
130
+
131
+	var rec bytes.Buffer
132
+
133
+	// Skip ServerHello
134
+	_, _, err = tls.ReadRecord(suite.buf, &rec)
135
+	suite.NoError(err)
136
+
137
+	// Skip ChangeCipherSpec
138
+	rec.Reset()
139
+	_, _, err = tls.ReadRecord(suite.buf, &rec)
140
+	suite.NoError(err)
141
+
142
+	// Read noise ApplicationData
143
+	rec.Reset()
144
+	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
145
+	suite.NoError(err)
146
+	suite.Equal(byte(tls.TypeApplicationData), recordType)
147
+
148
+	// Should be within mean ± jitter range.
149
+	suite.GreaterOrEqual(length, int64(noise.Mean-noise.Jitter))
150
+	suite.LessOrEqual(length, int64(noise.Mean+noise.Jitter))
151
+}
152
+
127 153
 func TestSendServerHello(t *testing.T) {
128 154
 	t.Parallel()
129 155
 	suite.Run(t, &SendServerHelloTestSuite{})

+ 14
- 8
mtglib/proxy.go Просмотреть файл

@@ -27,6 +27,7 @@ type Proxy struct {
27 27
 
28 28
 	allowFallbackOnUnknownDC    bool
29 29
 	tolerateTimeSkewness        time.Duration
30
+	idleTimeout                 time.Duration
30 31
 	domainFrontingPort          int
31 32
 	domainFrontingIP            string
32 33
 	domainFrontingProxyProtocol bool
@@ -65,10 +66,10 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
65 66
 	ctx := newStreamContext(p.ctx, p.logger, conn)
66 67
 	defer ctx.Close()
67 68
 
68
-	go func() {
69
-		<-ctx.Done()
69
+	stop := context.AfterFunc(ctx, func() {
70 70
 		ctx.Close()
71
-	}()
71
+	})
72
+	defer stop()
72 73
 
73 74
 	p.eventStream.Send(ctx, NewEventStart(ctx.streamID, ctx.ClientIP()))
74 75
 	ctx.logger.Info("Stream has been started")
@@ -104,8 +105,8 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
104 105
 	relay.Relay(
105 106
 		ctx,
106 107
 		ctx.logger.Named("relay"),
107
-		ctx.telegramConn,
108
-		ctx.clientConn,
108
+		connIdleTimeout{Conn: ctx.telegramConn, timeout: p.idleTimeout},
109
+		connIdleTimeout{Conn: ctx.clientConn, timeout: p.idleTimeout},
109 110
 	)
110 111
 }
111 112
 
@@ -151,6 +152,7 @@ func (p *Proxy) Serve(listener net.Listener) error {
151 152
 		case errors.Is(err, ants.ErrPoolClosed):
152 153
 			return nil
153 154
 		case errors.Is(err, ants.ErrPoolOverload):
155
+			conn.Close() //nolint: errcheck
154 156
 			logger.Info("connection was concurrency limited")
155 157
 			p.eventStream.Send(p.ctx, NewEventConcurrencyLimited())
156 158
 		}
@@ -192,7 +194,10 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
192 194
 		return false
193 195
 	}
194 196
 
195
-	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello); err != nil {
197
+	gangerNoise := p.doppelGanger.NoiseParams()
198
+	noiseParams := fake.NoiseParams{Mean: gangerNoise.Mean, Jitter: gangerNoise.Jitter}
199
+
200
+	if err := fake.SendServerHello(ctx.clientConn, p.secret.Key[:], clientHello, noiseParams); err != nil {
196 201
 		p.logger.InfoError("cannot send welcome packet", err)
197 202
 		return false
198 203
 	}
@@ -303,8 +308,8 @@ func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) {
303 308
 	relay.Relay(
304 309
 		ctx,
305 310
 		ctx.logger.Named("domain-fronting"),
306
-		frontConn,
307
-		conn,
311
+		connIdleTimeout{Conn: frontConn, timeout: p.idleTimeout},
312
+		connIdleTimeout{Conn: conn, timeout: p.idleTimeout},
308 313
 	)
309 314
 }
310 315
 
@@ -336,6 +341,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
336 341
 		domainFrontingPort:       opts.getDomainFrontingPort(),
337 342
 		domainFrontingIP:         opts.DomainFrontingIP,
338 343
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
344
+		idleTimeout:              opts.getIdleTimeout(),
339 345
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
340 346
 		telegram:                 tg,
341 347
 		doppelGanger: doppel.NewGanger(

+ 9
- 0
mtglib/proxy_opts.go Просмотреть файл

@@ -160,6 +160,7 @@ type ProxyOpts struct {
160 160
 
161 161
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
162 162
 	DoppelGangerDRS bool
163
+
163 164
 }
164 165
 
165 166
 func (p ProxyOpts) valid() error {
@@ -215,6 +216,14 @@ func (p ProxyOpts) getPreferIP() string {
215 216
 	return p.PreferIP
216 217
 }
217 218
 
219
+func (p ProxyOpts) getIdleTimeout() time.Duration {
220
+	if p.IdleTimeout == 0 {
221
+		return time.Minute
222
+	}
223
+
224
+	return p.IdleTimeout
225
+}
226
+
218 227
 func (p ProxyOpts) getLogger(name string) Logger {
219 228
 	return p.Logger.Named(name)
220 229
 }

+ 0
- 1
run_profile_tag_prof.go Просмотреть файл

@@ -3,7 +3,6 @@
3 3
 package main
4 4
 
5 5
 import (
6
-	"fmt"
7 6
 	"net"
8 7
 	"net/http"
9 8
 	_ "net/http/pprof" //nolint: gosec

Загрузка…
Отмена
Сохранить