REPOSITORY / ScuroNeko/mtg

Compare commits

DIFF REPOSITORY

Compare commits

..
348 Commits
Author SHA1 Message Date
9seconds 1a81efcb6e Merge remote-tracking branch 'origin/stable' into v2 2026-04-01 17:17:22 +02:00
9seconds 2544c521ed Merge remote-tracking branch 'origin/master' into stable 2026-04-01 17:16:55 +02:00
9seconds 3a68ea5f2d Update goreleaser 2026-04-01 17:05:30 +02:00
Sergei ArkhipovandGitHub dbced77566 Merge pull request #433 from 9seconds/refactor-tls-fragmentation
Refactor TLS fragmenting
2026-04-01 14:30:21 +02:00
9seconds f4f969e702 Refactor TLS fragmenting 2026-04-01 14:01:24 +02:00
Sergei ArkhipovandGitHub e8368f7645 Merge pull request #431 from appolimp/tls-record-reassembly-pr
Support fragmented TLS handshake records
2026-04-01 09:33:13 +02:00
appolimp 38abee7d7f Support fragmented TLS handshake records
DPI bypass tools like ByeDPI fragment a single TLS record into multiple
records to evade censorship. This broke ReadClientHello because it
assumed the entire ClientHello arrives in one TLS record.

Add reassembleTLSHandshake that reads continuation records and
reconstructs a single TLS record before parsing and HMAC verification.
Per RFC 5246 Section 6.2.1, handshake messages may be fragmented
across multiple records — this is valid TLS behavior.
2026-04-01 09:05:24 +03:00
9seconds a3663fe8b5 Increase timeout for CI artifacts build 2026-03-31 22:13:42 +02:00
9seconds 2aa3321bd4 Add more forks 2026-03-31 19:03:48 +02:00
Sergei ArkhipovandGitHub 3793558c4c Merge pull request #430 from 9seconds/golang-idiomatic
More idiomatic Golang
2026-03-31 17:23:11 +02:00
9seconds b6427ee321 More idiomatic Golang 2026-03-31 15:07:01 +02:00
Sergei ArkhipovandGitHub 0c9fa5e710 Merge pull request #428 from 9seconds/auto-update-prio{
Change IP address set priority
2026-03-31 12:56:58 +02:00
9seconds 1fcec38aea Change IP address set priority
For a couple of releases we use collected IPs as a prioritized source
for connecting to Telegram. But apparently, they work way worse than it
should, and having connectivity to core ip ALWAYS gives better results.
Thus, this PR flips priorities, so users could have auto-update enabled
as a source of secondary addresses, not primary ones
2026-03-31 11:05:49 +02:00
Sergei ArkhipovandGitHub 89930631cf Merge pull request #426 from dolonet/fix/flaky-ci-race-and-bloom 2026-03-31 07:10:00 +02:00
dolonet eedee63143 Address review: use slices.Clone, simplify concurrent test
- Replace manual make+copy with slices.Clone in Snapshot()
- Remove redundant _ = len(data); Snapshot() call alone is
  sufficient to exercise the lock under -race
2026-03-30 16:17:51 +00:00
Alexey Dolotov 73c6a3aa37 fix: tighten ScoutConnCollected encapsulation and add concurrency test
- Move error check before Snapshot() to avoid unnecessary allocation
- Update existing tests to use Snapshot() instead of direct field access
- Add TestConcurrentAddSnapshot to explicitly exercise the mutex
2026-03-30 15:05:50 +03:00
Alexey Dolotov e54d9d60d3 fix: stabilize flaky CI tests
1. Add sync.Mutex to ScoutConnCollected to eliminate data race between
   Add()/MarkWrite() in readLoop and learn() iterating results.
   Introduce Snapshot() for safe read access.

2. Increase bloom filter test size from 500 to 100000 to prevent
   false negatives from random eviction in the stable bloom filter.

3. Use Require().NoError() in TestHTTPSRequest to prevent nil-pointer
   panic on resp.Body.Close() when the request fails.

Fixes #425
2026-03-30 14:50:32 +03:00
9seconds db2e6031a3 Merge remote-tracking branch 'origin/stable' into v2 2026-03-30 13:09:05 +02:00
9seconds 4b8da719ae Merge remote-tracking branch 'origin/master' into stable 2026-03-30 13:08:51 +02:00
9seconds a2de52f071 Update PGO 2026-03-30 13:08:32 +02:00
Sergei ArkhipovandGitHub b926a0590c Merge pull request #424 from dolonet/fix/relay-idle-timeout-shared-tracker
fix: use shared idle tracker for relay connections
2026-03-30 12:54:51 +02:00
Alexey Dolotov 58e8c8f982 ci: trigger tests 2026-03-30 13:51:17 +03:00
Alexey Dolotov 4642546b35 test: add idleTracker and connIdleTimeout tests
Cover shared idle tracker behavior:
- tracker lifecycle (new, idle after timeout, touch resets)
- read/write with data touches tracker
- read retries on timeout when tracker is not idle
- read closes on timeout when tracker is idle
- shared tracker prevents false timeout across directions
2026-03-30 13:21:51 +03:00
Alexey Dolotov 4627910238 fix: use shared idle tracker for relay connections
connIdleTimeout previously set per-direction deadlines independently.
During media downloads the client→telegram direction can be idle at the
application level while telegram→client is actively streaming data.
After IdleTimeout (default 1 min) the idle direction's ReadDeadline
fires, tearing down the entire relay and breaking media transfers.

Replace the per-direction timeout with a shared atomic timestamp that
both pump goroutines update on any successful Read or Write. When a
ReadDeadline fires on the idle direction, we check the shared tracker:
if the other direction was recently active, we retry instead of closing.
The connection is only torn down when both directions are idle for the
full timeout period.

This matches the documented IdleTimeout contract: "if we have any
message which will pass to either direction, a timer is reset."

Overhead: one atomic.Int64 (8 bytes) per connection pair, one
atomic.Store (~1 ns) per Read/Write with data, zero extra goroutines.

Fixes #423
2026-03-30 10:34:16 +03:00
9seconds 8b3c622ea6 Merge remote-tracking branch 'origin/stable' into v2 2026-03-29 23:33:24 +02:00
9seconds 9d43c2d759 Merge remote-tracking branch 'origin/master' into stable 2026-03-29 23:33:06 +02:00
9seconds 0840c7e3e5 Update PGO 2026-03-29 23:31:41 +02:00
9seconds de48e177b1 Update depndencies 2026-03-29 23:16:06 +02:00
Sergei ArkhipovandGitHub 3ed09146b9 Merge pull request #422 from 9seconds/release-ci
Build release artifacts in CI
2026-03-29 23:15:28 +02:00
9seconds 018bd2fdc1 Run release build 2026-03-29 22:54:08 +02:00
Sergei ArkhipovandGitHub 0ad3a06863 Merge pull request #421 from 9seconds/mips-save-mem
Decrease a relay buffer size for MIPS devices
2026-03-29 22:09:25 +02:00
9seconds d3a090d6b4 Decrease a relay buffer size for MIPS devices 2026-03-29 21:42:26 +02:00
Sergei ArkhipovandGitHub 1725a0d721 Merge pull request #420 from dolonet/fix/telegram-relay-idle-timeout
fix: apply idle timeout to Telegram relay
2026-03-29 17:14:58 +02:00
Alexey Dolotov 87988326ab retry CI 2026-03-29 17:02:53 +03:00
Alexey Dolotov ec271baab0 fix: apply idle timeout to Telegram relay
Wrap both sides of the Telegram relay in connIdleTimeout,
same as already done for domain fronting in #416.

Without this, if a client disappears (network drop, battery dies),
the TCP connection stays formally alive and the goroutine in the
worker pool blocks on io.CopyBuffer indefinitely. Under mass client
disconnects this accumulates zombie goroutines.

Fixes #417
2026-03-29 16:59:30 +03:00
Sergei ArkhipovandGitHub 139db15e83 Merge pull request #419 from 9seconds/timers
Remove clock goroutine
2026-03-29 15:46:55 +02:00
9seconds 0c030646f9 Remove clock goroutine
This is a followup for https://github.com/9seconds/mtg/issues/412 it
makes sense to manage timers inplace instead of creating for new
goroutines: saves memory
2026-03-29 15:39:33 +02:00
Sergei ArkhipovandGitHub 822560bede Merge pull request #418 from dolonet/public-ip-config
Add public-ipv4/public-ipv6 config options
2026-03-29 13:52:07 +02:00
Sergei ArkhipovandGitHub 9917f61bc8 Merge pull request #415 from dolonet/fix/event-stream-32bit-index-panic
fix: prevent index out of range panic on 32-bit platforms
2026-03-29 13:51:12 +02:00
Sergei ArkhipovandGitHub 735466b90d Merge pull request #416 from dolonet/fix/domain-fronting-idle-timeout
fix: apply idle timeout to domain fronting relay
2026-03-29 13:50:45 +02:00
Sergei ArkhipovandGitHub 6b51de8305 Merge pull request #414 from dolonet/optimize-per-connection-overhead
Reduce per-connection memory overhead
2026-03-29 13:49:08 +02:00
Alexey Dolotov 7b62e06e36 Retry CI: flaky antireplay bloom filter test 2026-03-29 00:53:20 +03:00
Alexey Dolotov 2b07c0037e Add public-ipv4/public-ipv6 config options for manual IP override
On some servers ifconfig.co is unreachable (e.g. Hetzner, AdGuard DNS
blocklists), causing 'mtg doctor' SNI-DNS check and 'mtg access' link
generation to fail. New config options allow specifying public IPs
manually, with automatic detection as fallback.

Fixes #405
2026-03-29 00:47:49 +03:00
Alexey Dolotov 450381ee16 ci: retrigger (flaky antireplay test) 2026-03-28 23:13:35 +03:00
Alexey Dolotov 46c33f1532 ci: retrigger (flaky antireplay test) 2026-03-28 23:04:33 +03:00
Alexey Dolotov f355512aa6 fix: address staticcheck lint issues
- avoid deprecated DefaultIdleTimeout, use time.Minute directly
- simplify embedded field selectors (QF1008)
2026-03-28 22:59:32 +03:00
Alexey Dolotov 289bb283b1 fix: close connection on worker pool overflow
When the worker pool rejected a connection (ErrPoolOverload), the
accepted net.Conn was never closed — leaking a file descriptor and
TCP socket per rejected connection. Under sustained traffic spikes this
compounds the problem: leaked descriptors reduce the capacity for new
dials (including to the fronting domain), accelerating the failure
cascade described in #378.
2026-03-28 22:52:39 +03:00
Alexey Dolotov 836090ebdf fix: apply idle timeout to domain fronting relay connections
Domain fronting relay (for non-Telegram traffic) had no idle timeout,
causing worker pool exhaustion under traffic spikes.

The ProxyOpts.IdleTimeout field existed but was never wired into the
proxy. Now domain fronting connections are wrapped with per-read/write
deadlines reset to the configured idle timeout (default 1m), so stale
or slowloris-style connections are reaped promptly.

Fixes #378
2026-03-28 22:47:39 +03:00
Alexey Dolotov 01402bdba2 fix: prevent index out of range panic on 32-bit platforms
On 32-bit architectures (e.g. ARM7), int is 32 bits wide.
Casting a uint32 hash value to int can overflow, producing a
negative number. Go's modulo operator preserves the sign, so
the channel index can become -1, causing a panic.

Perform the modulo in uint32 space before indexing to ensure
the result is always non-negative.

Fixes #413
2026-03-28 22:31:47 +03:00
Alexey Dolotov 026ec74dfd Reduce per-connection memory overhead
- Use sync.Pool for relay buffers instead of stack-allocated arrays.
  A [16379]byte on the goroutine stack forces Go to grow it to 32KB
  (next power of two). Pooled buffers keep goroutine stacks small.

- Same fix for doppelganger write buffer ([16384]byte in conn.start).

- Replace idle goroutines with context.AfterFunc in proxy.ServeConn
  and relay.Relay. These goroutines existed only to wait on ctx.Done()
  and close connections. AfterFunc achieves the same without allocating
  a goroutine until the context is actually cancelled.

Net effect: at 3000 concurrent connections on a 1-vCPU/961MB VPS,
the unmodified binary drops 246 connections and falls to 10 MB/s.
With these changes: zero failures, 63 MB/s, 31% lower RSS.

Closes #412
2026-03-28 13:24:39 +03:00
Sergei ArkhipovandGitHub cc4b6ce2f4 Merge pull request #409 from dolonet/cert-noise-calibration
Add dynamic cert noise calibration for FakeTLS handshake
2026-03-28 09:04:18 +01:00
Alexey Dolotov 9dfd992c1d Move cert noise calibration into doppelganger scout
Instead of a separate cert_probe.go that duplicates the scout's TLS
connection logic, measure the cert chain size directly from the same
HTTPS connections the scout already makes.

Changes:
- Extend ScoutConnResult with payloadLen field
- Add Write interception to ScoutConn for handshake boundary detection
- Scout.learn() now computes cert size (sum of ApplicationData between
  CCS and first client Write) alongside inter-record durations
- Ganger aggregates cert sizes across raids and exposes NoiseParams()
  via atomic pointer for lock-free reads from proxy goroutines
- Proxy reads NoiseParams from Ganger on each handshake instead of
  probing at startup
- Remove cert_probe.go, disk cache, and related config options
  (noise-cache-path, noise-cache-ttl, noise-probe-count)

Falls back to legacy 2500-4700 range until the first scout raid
completes (typically within 1-2 seconds of startup).
2026-03-27 16:34:42 +03:00
Alexey Dolotov 80213ad35d Add dynamic cert noise calibration for FakeTLS handshake
The hardcoded noise range (2500-4700 bytes) in the FakeTLS ServerHello
does not match the real certificate chain sizes of many popular fronting
domains (e.g., dl.google.com ≈ 6480 bytes, microsoft.com ≈ 13004 bytes).
This makes the proxy detectable by DPI systems that compare the
ApplicationData size with the real cert chain size for the SNI domain.

On startup, probe the fronting domain's actual TLS handshake size and
use the measured value ± jitter instead of the static range. Falls back
to the legacy 2500-4700 range if the probe fails.

Also adds optional caching of probe results between restarts
(noise-cache-path, noise-cache-ttl) and a configurable probe count
(noise-probe-count) under [defense.doppelganger].

Closes #408
2026-03-26 23:38:58 +03:00
Sergei ArkhipovandGitHub d32e8e8b97 Merge pull request #404 from 9seconds/codeql
Update stale codeql configuration
2026-03-25 10:19:15 +01:00
9seconds 60c57c2306 Update stale codeql configuration 2026-03-25 10:17:30 +01:00
Sergei ArkhipovandGitHub 0edd5e6f92 Merge pull request #402 from 9seconds/PGO
Update PGO
2026-03-24 21:07:10 +01:00
9seconds a8e4acb6f8 Update PGO 2026-03-24 21:04:30 +01:00
Sergei ArkhipovandGitHub 27d10e6820 Merge pull request #401 from 9seconds/fix-prof
Fix build with profiling
2026-03-24 20:50:41 +01:00
9seconds b47e13556e Fix build with profiling 2026-03-24 20:46:41 +01:00
9seconds de81ed565d Add mention of fork 2026-03-24 15:28:33 +01:00
9seconds 006fba1046 Merge remote-tracking branch 'origin/stable' into v2 2026-03-24 09:59:03 +01:00
9seconds 7b333ed833 Merge remote-tracking branch 'origin/master' into stable 2026-03-24 09:58:48 +01:00
9seconds 5adfee5dd4 Remove wrong binary 2026-03-24 09:58:13 +01:00
9seconds de89de2ad6 Merge remote-tracking branch 'origin/master' into stable 2026-03-24 09:57:43 +01:00
9seconds b0d37de0ec Update linter 2026-03-24 09:57:20 +01:00
9seconds 0cb25ba7ff Update go dependencies 2026-03-24 09:55:53 +01:00
9seconds 614acd7303 Mention doctor in README 2026-03-24 09:55:30 +01:00
Sergei ArkhipovandGitHub 4f5368aa2a Merge pull request #398 from 9seconds/docker-directory
Allow using directory bind mounts for a docker container
2026-03-24 09:00:59 +01:00
9seconds cfb5fe66be Allow using directory bind mounts for a docker container
This helps with a situation when some applications do not allow mounting
individual files, but whole directories. In that case users could mount
`/config` directory with a single file, `config.toml`: `-v
/path/to/dir:/config`. Also, there is a backward compatibility to using
a single `/config.toml`
2026-03-24 08:48:42 +01:00
Sergei ArkhipovandGitHub fb390d3417 Merge pull request #397 from 9seconds/doctor 2026-03-23 19:35:49 +01:00
9seconds f0ae4ce290 Validate domain fronting availability 2026-03-23 19:22:21 +01:00
9seconds b6b900e430 Refactoring 2026-03-23 19:12:14 +01:00
9seconds 8154f65e0e Add validation of telegram connectivity 2026-03-23 18:34:38 +01:00
9seconds a60523fed0 Add verification of time skewness 2026-03-23 15:28:48 +01:00
9seconds 63b147c287 Add doctor command for deprecated config values 2026-03-23 14:45:10 +01:00
Sergei ArkhipovandGitHub 21c0d18c7c Merge pull request #395 from roman901/master 2026-03-21 23:31:24 +01:00
Roman Shishkin 8f0bf47d56 Add Config.GetConcurrency with default fallback 2026-03-21 21:53:38 +03:00
9seconds 7fec30908a Merge remote-tracking branch 'origin/stable' into v2 2026-03-20 11:29:39 +01:00
9seconds 2eb0828f72 Merge remote-tracking branch 'origin/master' into stable 2026-03-20 11:29:24 +01:00
Sergei ArkhipovandGitHub d01e089f54 Merge pull request #386 from 9seconds/architectures
Add more architectures for mtg
2026-03-20 11:22:58 +01:00
Sergei ArkhipovandGitHub c736881792 Merge pull request #388 from 9seconds/doc-limits
Document a necessety of increasing limits for systemd unit
2026-03-20 11:16:06 +01:00
9seconds d5a118f125 Remove explicit pgo 2026-03-20 11:15:01 +01:00
9seconds d79a8f8406 Fix failed builds 2026-03-20 11:14:25 +01:00
9seconds 97932758d1 Add mips support 2026-03-20 11:14:25 +01:00
9seconds 1f7d1c0eea Add windows builds 2026-03-20 11:14:25 +01:00
9seconds 8c73dde928 Add build for AMD64v3 2026-03-20 11:14:25 +01:00
9seconds ded3fe26b9 Build for ARMv9 2026-03-20 11:14:25 +01:00
Sergei ArkhipovandGitHub 2f00adfe91 Merge pull request #385 from 9seconds/pgo
Add PGO
2026-03-20 11:14:00 +01:00
9seconds 049bee3d84 Document a necessety of increasing limits for systemd unit
It seems that default DynamicUser limits are very low. We have to
increase them anyway.
2026-03-20 11:13:03 +01:00
9seconds 4fbabfda2a Add PGO 2026-03-20 10:54:30 +01:00
9seconds fc72de9e39 Merge remote-tracking branch 'origin/stable' into v2 2026-03-19 18:52:35 +01:00
9seconds cb627f2a66 Merge remote-tracking branch 'origin/master' into stable 2026-03-19 18:52:11 +01:00
Sergei ArkhipovandGitHub 9ba6df0d1c Merge pull request #383 from 9seconds/avoid-double-buffering
Avoid double buffering in TLS hot path
2026-03-19 17:46:36 +01:00
9seconds 4a8d099aca Remove unused buffer 2026-03-19 17:39:57 +01:00
9seconds feb57004e1 Fix reslicing 2026-03-19 17:39:48 +01:00
9seconds cb436efd87 Avoid double buffering in TLS hot path 2026-03-19 17:37:51 +01:00
Sergei ArkhipovandGitHub 24148ea95c Merge pull request #382 from 9seconds/write-cond
Optimize waiting time for TLS chunker
2026-03-19 15:51:11 +01:00
9seconds 724904f50d Wait in doppel.Conn if there is anything to write 2026-03-19 15:42:00 +01:00
9seconds a23ae05f3b Remove SyncWrite 2026-03-19 13:47:08 +01:00
Sergei ArkhipovandGitHub b153a55149 Merge pull request #379 from 9seconds/fix-telegram-ips
Show ip of telegram endpoints in event stream
2026-03-18 22:46:47 +01:00
9seconds 913a38d13a Show real IP of the telegram endpoint in event stream 2026-03-18 22:05:34 +01:00
9seconds dc81f7981c Merge remote-tracking branch 'origin/stable' into v2 2026-03-16 23:56:10 +01:00
9seconds 9d5fd989e5 Merge remote-tracking branch 'origin/master' into stable 2026-03-16 23:55:56 +01:00
Sergei ArkhipovandGitHub 81703233b0 Merge pull request #368 from 9seconds/flake-tests
Fix flaky test
2026-03-16 23:55:01 +01:00
9seconds eb7720b11e Fix flaky test 2026-03-16 23:44:06 +01:00
Sergei ArkhipovandGitHub df7ddc3d6a Merge pull request #367 from saleacy/patch-1
fix: ensure network.Dial and MakeHTTPClient use socks5 proxy
2026-03-16 23:38:43 +01:00
saleacyandGitHub 3bc1e415f9 fix: ensure network.Dial and MakeHTTPClient use socks5 proxy
The package `network/v2/proxy_network.go` does not wrap `network.Dial`
and `network.MakeHTTPClient`, which causes them to bypass the SOCKS5
proxy and initiate TCP connections directly from the local machine.
2026-03-17 01:35:18 +08:00
Sergei ArkhipovandGitHub 306fa19ad6 Merge pull request #366 from Maks-2012/patch-1
Fix preferIPOnlyIPv6
2026-03-16 15:31:09 +01:00
Maks-2012andGitHub 079252d810 Fix preferIPOnlyIPv6 2026-03-16 16:10:38 +03:00
9seconds d0502e7083 Merge remote-tracking branch 'origin/stable' into v2 2026-03-15 22:04:35 +01:00
9seconds 30aa9d3a44 Merge remote-tracking branch 'origin/master' into stable 2026-03-15 22:04:09 +01:00
9seconds d98d5be3a7 Update go dependencies 2026-03-15 22:01:04 +01:00
9seconds 94ac9d6ffa Update tooling 2026-03-15 22:00:08 +01:00
9seconds c036558df4 Update docs 2026-03-15 21:58:04 +01:00
Sergei ArkhipovandGitHub 4167dec5e1 Merge pull request #365 from 9seconds/best-practices
Add document about best practices
2026-03-15 21:52:16 +01:00
9seconds 57529904e3 Add document about best practices 2026-03-15 21:50:43 +01:00
Sergei ArkhipovandGitHub ed4e511560 Merge pull request #363 from 9seconds/relay-buffer
Decrease relay buffer size
2026-03-14 14:15:52 +01:00
9seconds bb90bcc127 Decrease relay buffer size
Even if it makes sense to have a huge buffers, we do artificial delays
now. In that case we could achieve the same results with a lower buffer.
If not, then we won't send a packet bigger that this value
2026-03-14 14:06:48 +01:00
9seconds 879ebd132d Formatting of the README 2026-03-14 09:44:07 +01:00
Sergei ArkhipovandGitHub 58e6dfd603 Merge pull request #361 from 9seconds/docs
Add docs about doppelganger
2026-03-14 09:43:19 +01:00
9seconds e675baa860 Add docs about doppelganger 2026-03-14 09:42:28 +01:00
Sergei ArkhipovandGitHub 4a17849763 Merge pull request #360 from 9seconds/fix-android
Have a noise of variable size
2026-03-13 17:11:46 +01:00
9seconds 7adfc0352e Have a noise of variable size 2026-03-13 17:08:44 +01:00
Sergei ArkhipovandGitHub a5d602b538 Merge pull request #359 from 9seconds/fix-android
Fix android ping
2026-03-13 16:46:18 +01:00
9seconds b35c212d65 Have a noise of variable size 2026-03-13 16:39:16 +01:00
9seconds 45b0964afd Fix android ping 2026-03-13 16:20:18 +01:00
Sergei ArkhipovandGitHub 6d8d2961e8 Merge pull request #358 from 9seconds/doppel-drs
Make DRS optional
2026-03-13 14:51:15 +01:00
9seconds 4138cc6494 Use SyncWrite call to send a packet to proxy 2026-03-13 14:44:56 +01:00
9seconds 33c0fa9bf7 Add SyncWrite method to doppel.Conn 2026-03-13 11:30:52 +01:00
9seconds 21d7522356 Make DRS optional 2026-03-13 11:04:01 +01:00
9seconds ea71fe81b2 Update stats defaults 2026-03-13 08:59:59 +01:00
Sergei ArkhipovandGitHub 0871c2d790 Merge pull request #357 from 9seconds/access-native-dialer
Access command should use native dialer instead of network one
2026-03-13 08:47:24 +01:00
9seconds 8df1629882 Access command should use native dialer instead of network one 2026-03-13 08:42:15 +01:00
9seconds ad945c77a9 Enable caching for govulncheck 2026-03-12 23:01:26 +01:00
Sergei ArkhipovandGitHub a051518def Merge pull request #356 from 9seconds/gh-cache
Speed up Github actions
2026-03-12 22:51:59 +01:00
9seconds 4dca1d2b07 Add cache mount for apk downloads 2026-03-12 22:46:19 +01:00
9seconds 287a794772 Always run tests with race detector 2026-03-12 22:22:09 +01:00
9seconds d4822989ec Add count number to covtest 2026-03-12 22:22:00 +01:00
Sergei ArkhipovandGitHub d6a17c502f Merge pull request #355 from 9seconds/scout-native
Use native dialer for scout
2026-03-12 22:14:52 +01:00
9seconds 3db1be0687 Use cache to speed up jobs 2026-03-12 22:13:17 +01:00
9seconds 991346621c Speedup docker builds by using cache 2026-03-12 22:13:00 +01:00
9seconds cc63e762e3 Use native dialer for scout 2026-03-12 21:47:03 +01:00
9seconds a85348d6be Use curl as user agent 2026-03-12 21:43:04 +01:00
Sergei ArkhipovandGitHub 7762cf3a55 Merge pull request #353 from 9seconds/domain-fronting-native
Use native dialer to communicate with fronting domain
2026-03-12 21:40:38 +01:00
9seconds 62cba24071 Use native dialer to communicate with fronting domain 2026-03-12 20:51:02 +01:00
9seconds 94e4179fb7 Add NativeDialer method to mtglib.Network 2026-03-12 19:25:53 +01:00
Sergei ArkhipovandGitHub 6493688282 Merge pull request #352 from 9seconds/doppleganger
Doppleganger
2026-03-12 19:18:44 +01:00
9seconds ffd3ab03cc Formatting with gofumpt 2026-03-12 19:13:33 +01:00
9seconds 00403e3a94 Remove old faketls package 2026-03-12 19:11:39 +01:00
9seconds 0bfc1ef2d4 Fix double TLS wrapping for noise 2026-03-12 19:07:11 +01:00
9seconds 7aa01dcebe Limit min number of collected stats 2026-03-12 19:07:11 +01:00
9seconds 1c0e847247 Set statistics measured from real life 2026-03-12 19:07:11 +01:00
9seconds 83ff4ee266 Align scout defaults with config defaults 2026-03-12 19:07:11 +01:00
9seconds 7a58c74cfe Correctly process doppel conns 2026-03-12 19:07:11 +01:00
9seconds d43d6692d7 Align naming about scout raids 2026-03-12 19:07:11 +01:00
9seconds 75392941da Propagate settings to Proxy 2026-03-12 19:07:11 +01:00
9seconds 25ad776b6f Propagate doppelganger to config 2026-03-12 19:07:11 +01:00
9seconds 5557393b38 Add config section 2026-03-12 19:07:11 +01:00
9seconds bb49c6a55d Integrate new fake package and doppel into proxy 2026-03-12 19:07:11 +01:00
9seconds 37f8d18be5 Add server side of things 2026-03-12 19:07:11 +01:00
9seconds 59557059df ReadClientHello function 2026-03-12 19:07:10 +01:00
9seconds 1182b9ef6f Add doppel and tls packages 2026-03-12 19:07:10 +01:00
9seconds c886ffdd81 Add TrustTLS method to networkHTTPTransport 2026-03-11 23:21:41 +01:00
Sergei ArkhipovandGitHub e9b38a5b74 Merge pull request #348 from 9seconds/bundle-config 2026-03-05 19:09:48 +01:00
9seconds 036b10be67 Include example config into distribution bundle 2026-03-05 17:14:52 +01:00
9seconds fefc479f94 Merge remote-tracking branch 'origin/stable' into v2 2026-02-28 14:52:05 +01:00
9seconds 9f12620dba Merge remote-tracking branch 'origin/master' into stable 2026-02-28 14:51:26 +01:00
9seconds 23aa2eefad More details on DOH-IP option 2026-02-28 14:50:48 +01:00
9seconds ddc34bf918 Comment out doh-ip 2026-02-28 14:48:37 +01:00
9seconds f5244c2bfd Merge remote-tracking branch 'origin/stable' into v2 2026-02-28 14:47:43 +01:00
9seconds 9946cdfeb5 Merge remote-tracking branch 'origin/master' into stable 2026-02-28 14:47:25 +01:00
Sergei ArkhipovandGitHub ffe052617b Merge pull request #346 from 9seconds/simple-network
Simplify network implementation
2026-02-27 16:25:23 +01:00
9seconds 700417b1d0 Integrate DNS resolver in our codebase 2026-02-27 16:23:24 +01:00
9seconds 897e6bf505 Propagate DNS setting to configuration 2026-02-27 16:23:24 +01:00
9seconds 317d7380cb Add support for custom DNS resolvers 2026-02-27 16:08:28 +01:00
9seconds 1151291535 Use network v2 instead of network v1 2026-02-27 15:52:29 +01:00
9seconds 42927c8bdc Add v2 network package 2026-02-27 15:50:21 +01:00
9seconds 7d748077e9 Merge remote-tracking branch 'origin/stable' into v2 2026-02-26 13:53:32 +01:00
9seconds f61a63a59b Merge remote-tracking branch 'origin/master' into stable 2026-02-26 13:53:17 +01:00
9seconds 282896be09 Update own dependencies 2026-02-26 13:52:26 +01:00
9seconds 3809f034ad Update go packages 2026-02-26 13:51:29 +01:00
Sergei ArkhipovandGitHub 6feef02d08 Merge pull request #344 from 9seconds/blocked-telegram
Make auto updating optional
2026-02-26 13:46:01 +01:00
Sergei ArkhipovandGitHub 5549a82d19 Merge pull request #345 from 9seconds/fix-no-addresses
Do not panic if no addresses were found
2026-02-26 13:35:47 +01:00
9seconds 189dce07c6 Make auto updating optional 2026-02-26 13:35:05 +01:00
9seconds 449f84133a Do not panic if no addresses were found 2026-02-26 12:31:30 +01:00
9seconds 9c122f942f Always use latest govulncheck to check vulnerabilities 2026-02-24 19:06:16 +01:00
9seconds 136eea551f Merge remote-tracking branch 'origin/stable' into v2 2026-02-24 18:46:18 +01:00
9seconds e6fa5906c9 Merge remote-tracking branch 'origin/master' into stable 2026-02-24 18:45:46 +01:00
9seconds 42f612f49e Use go tag for 1.26 2026-02-24 18:08:45 +01:00
Sergei ArkhipovandGitHub d7db8ca98b Merge pull request #339 from 9seconds/domain-fronting-config-grouping
Domain fronting config grouping
2026-02-24 18:07:02 +01:00
9seconds 1cb225f52c Introduce [domain-fronting] config 2026-02-24 18:05:12 +01:00
9seconds af72b2a574 Delete obsoleted setting 2026-02-24 16:56:42 +01:00
Sergei ArkhipovandGitHub 2cbee5d453 Merge pull request #338 from 9seconds/proxy-proto-front
Add support for domain fronting proxy protocol
2026-02-24 16:53:17 +01:00
9seconds cde313b359 Add support for domain fronting proxy protocol 2026-02-24 16:44:35 +01:00
Sergei ArkhipovandGitHub 58cb0b2caf Merge pull request #336 from 9seconds/obfuscated2
Fetch DC203 from Telegram
2026-02-24 16:41:06 +01:00
9seconds bb320e9d89 Update fuzz tests 2026-02-24 15:27:19 +01:00
Sergei ArkhipovandGitHub f6d2f2ffd8 Merge pull request #337 from 9seconds/govulncheck
Check for vulnerabilities
2026-02-24 15:25:06 +01:00
9seconds 5fe3fdd73c Check for vulnerabilities 2026-02-24 14:21:16 +01:00
9seconds 5b91edf5c4 Fix tests 2026-02-24 13:58:16 +01:00
9seconds 8b34c1b104 Merge remote-tracking branch 'origin/master' into obfuscated2 2026-02-24 13:37:10 +01:00
9seconds 36c766b331 Fix lint issues 2026-02-24 13:35:06 +01:00
9seconds e4a9a96309 Remove mentioning of DC overrides 2026-02-24 13:32:06 +01:00
9seconds 94d46d2c65 Add fetching of addresses from proxyGetConfig endpoint 2026-02-24 12:55:16 +01:00
9seconds 908842063a Do not use additional bytes buffer for faketls 2026-02-23 10:27:01 +01:00
9seconds e50cee5748 Do not use unnecessary lock in connRewind 2026-02-23 10:12:25 +01:00
9seconds ee524abdb5 Remove redundant copyBufferPool from relay 2026-02-23 10:12:25 +01:00
9seconds 3e75e4fa63 Delete old obfuscated2 package 2026-02-23 10:12:25 +01:00
9seconds 140e9dfc2e Integrate obfuscation package 2026-02-23 10:12:25 +01:00
9seconds d0065d35c2 Add new obfuscation package 2026-02-23 10:12:25 +01:00
Sergei ArkhipovandGitHub 45ce5c2f61 Merge pull request #334 from ivulit/master 2026-02-20 20:33:10 +01:00
ivulit 21129b6e00 Add domain-fronting-ip to example config 2026-02-20 12:34:22 +03:00
ivulit bf38f9f8af Add domain-fronting-ip option
Allow specifying an explicit IP address for the domain fronting host
instead of relying on DNS resolution. Useful when DNS resolution of
the fronting hostname is blocked.

The hostname from the secret is still used for SNI in TLS handshake.
2026-02-20 12:34:17 +03:00
9seconds b854b16e1a Merge branch 'stable' into v2 2026-02-19 14:37:52 +01:00
9seconds f4b296d1e0 Merge branch 'master' into stable 2026-02-19 14:37:36 +01:00
Sergei ArkhipovandGitHub 432e530f68 Adjust formatting in readme 2026-02-19 14:35:04 +01:00
Sergei ArkhipovandGitHub a2bf9a269a Merge pull request #333 from 9seconds/proxyproto
Add support of proxy protocol
2026-02-19 14:27:43 +01:00
9seconds cf3437bb63 Add support of proxy protocol 2026-02-19 14:22:00 +01:00
Sergei ArkhipovandGitHub d0e99dda2b Update README.md 2026-02-18 11:16:04 +01:00
Sergei ArkhipovandGitHub ac3bd16e83 Merge pull request #332 from 9seconds/telemt-recommend
Recommend to use telemt for adtag carvings
2026-02-18 11:14:22 +01:00
9seconds 222cef8c41 Recommend to use telemt for adtag carvings 2026-02-18 11:13:55 +01:00
Sergei ArkhipovandGitHub ed5da2864a Merge pull request #331 from 9seconds/go1.26
Chore updates for golang 1.26
2026-02-18 11:04:23 +01:00
9seconds 08393e426a Update mise dependencies 2026-02-18 10:37:41 +01:00
9seconds 0440ccf4ff Rewrite to WaitGroup.Go 2026-02-18 10:34:31 +01:00
9seconds a0aabf2391 Switch to rand/v2 2026-02-18 10:34:31 +01:00
9seconds 3b03c4a90a More idioms related to go 1.26 2026-02-17 23:38:12 +01:00
Sergei ArkhipovandGitHub 80b9159ce9 Merge pull request #330 from 9seconds/telemt
Mention telemt project in README
2026-02-17 22:19:50 +01:00
Sergei ArkhipovandGitHub bdabb0e59a Merge pull request #329 from 9seconds/tools
Tell about mise in readme
2026-02-17 22:19:32 +01:00
9seconds 543f5cde9c Mention telemt project in README 2026-02-17 21:46:15 +01:00
9seconds 1a247d18b1 Tell about mise in README 2026-02-17 21:43:16 +01:00
9seconds 5a63c7d5b4 Lock all mise dependencies 2026-02-17 21:39:12 +01:00
9seconds e7fdb02a29 Merge branch 'v2' into stable 2026-02-16 19:23:45 +01:00
9seconds 7a6ba6d8c6 Merge branch 'master' into v2 2026-02-16 19:23:28 +01:00
9seconds be398f9c79 Use mise lock 2026-02-16 19:22:37 +01:00
9seconds 4c029fc683 Use gofumpt from github 2026-02-16 19:22:37 +01:00
9seconds 3066672353 Add permissions for Docker job to write into packages 2026-02-16 18:01:27 +00:00
9seconds ae88c0cab0 More tags for relevant branches 2026-02-16 17:17:52 +00:00
9seconds 816b7f72b6 Change secret to Github token 2026-02-16 17:17:47 +00:00
Sergei ArkhipovandGitHub 1f7584c108 Merge pull request #326 from 9seconds/dc203
Correctly connect to DC203/CDN
2026-02-16 18:01:26 +01:00
Sergei ArkhipovandGitHub 2fb025bbfd Merge pull request #327 from 9seconds/workflow
Correctly build and push images into registry
2026-02-16 18:01:09 +01:00
Sergey Arkhipov c7ab53134a Correctly build and push images into registry 2026-02-16 16:39:29 +00:00
9seconds f83ee17361 Fix test 2026-02-16 17:30:11 +01:00
9seconds 36546cec2f Change default DOH to cloudflarte 2026-02-16 17:18:31 +01:00
9seconds 074c4017f5 Formatting by gofumpt 2026-02-16 17:10:06 +01:00
9seconds 8e87405d3e Remove integration with gotd 2026-02-16 16:30:10 +01:00
9seconds 82679ec20f Update config 2026-02-16 15:22:03 +01:00
9seconds 308e372a5d Propagate DcUpdateEach setting 2026-02-16 15:07:56 +01:00
9seconds 836a481026 Propagate DCOverrides 2026-02-16 14:58:41 +01:00
9seconds 0a5a45b32d Create internal DC package 2026-02-16 14:15:38 +01:00
9seconds 852ca713c8 Fetch DC ips from Telegram 2026-02-15 23:56:53 +01:00
9seconds 301bde88ac Add gopls to mise 2026-02-11 15:09:43 +01:00
9seconds 25bca76da5 Merge remote-tracking branch 'origin/stable' into v2 2026-02-11 14:26:31 +01:00
9seconds 76109f6204 Merge remote-tracking branch 'origin/master' into stable 2026-02-11 14:25:54 +01:00
9seconds 6db6415798 Enable tests back 2026-02-11 14:25:00 +01:00
9seconds 0faf482e52 Update dockerfile 2026-02-11 13:55:11 +01:00
9seconds 3e4faf6ba6 Test docker 2026-02-11 12:13:55 +01:00
9seconds f7f6a7637c Update for tag semver 2026-02-11 12:09:57 +01:00
9seconds 130b02013b More CI updates 2026-02-11 12:07:45 +01:00
9seconds 1b4c777ed3 Use mise for fuzzing 2026-02-11 11:58:30 +01:00
9seconds 60b4b5ad8f Fix lint issues 2026-02-11 11:54:47 +01:00
9seconds a4930a596a Update linter job 2026-02-11 11:49:51 +01:00
9seconds fb3e921bc6 Update linter job 2026-02-11 11:48:09 +01:00
9seconds afbf4f6f74 Disable push to docker hub 2026-02-11 11:44:59 +01:00
9seconds 592b7c5b87 Update setup-go actions 2026-02-11 11:44:52 +01:00
9seconds c170887499 Update checkouts 2026-02-11 11:44:44 +01:00
9seconds ad76304b43 Correct argument for coverage 2026-02-11 11:43:01 +01:00
9seconds 5e38819506 Update codecov action 2026-02-11 11:42:01 +01:00
9seconds f06fd5ca77 Update CI 2026-02-11 11:21:27 +01:00
9seconds ad88afeecd Remove makefile 2026-02-11 11:13:36 +01:00
9seconds c74c0c92c6 Update CI 2026-02-11 11:13:25 +01:00
9seconds e3dc03b4db Update dependencies 2026-02-11 10:46:07 +01:00
9seconds ecba88d2e3 Update to the latest golangci-lint 2026-02-11 10:20:04 +01:00
9seconds 37de052feb Disable failing real request test for a while 2026-02-11 10:02:39 +01:00
9seconds 2041f3154b Add tests into mise 2026-02-11 09:58:26 +01:00
9seconds b1728c3474 Add task for building image 2026-02-11 09:47:11 +01:00
9seconds b72af2b953 Add task for formatting source code 2026-02-11 09:45:40 +01:00
9seconds 75c317f35e Add task for static build 2026-02-11 09:45:40 +01:00
9seconds 795dabb80b Add docs task 2026-02-11 09:38:36 +01:00
9seconds 01b0166995 Add release mise task 2026-02-11 09:38:36 +01:00
9seconds e9a147c836 Add build task 2026-02-11 09:26:46 +01:00
9seconds de66835657 Update goreleaser 2026-02-11 08:59:17 +01:00
9seconds 75f11965d8 Switch to mise 2026-02-11 08:52:45 +01:00
Sergey ArkhipovandGitHub e68d0c7da5 Merge pull request #289 from arashnm80/arashnm80-fix-docker-command
edit docker command in README.md
2022-11-21 17:53:10 +01:00
Arash Nemat ZadehandGitHub 051180a170 edit docker command in README.md
added the missing slash mentioned in issue #287
2022-11-17 03:42:09 +03:30
9seconds b4e0143cb7 Merge remote-tracking branch 'origin/stable' 2022-08-09 17:57:09 +03:00
9seconds 269852a4f1 Merge remote-tracking branch 'origin/master' into stable 2022-08-09 17:26:42 +03:00
Sergey ArkhipovandGitHub bd8a7ed8c9 Merge pull request #278 from 9seconds/socks5-proxies-for-simple-run
Add parameter for a simple run that sets socks5 proxies up
2022-08-09 16:24:37 +02:00
9seconds 7e3e4a611d Add parameter for a simple run that sets socks5 proxies up 2022-08-09 17:02:49 +03:00
Sergey ArkhipovandGitHub db2be4001b Merge pull request #277 from 9seconds/go1.19 2022-08-08 22:12:13 +02:00
9seconds 05c99aee4a Update github workflow 2022-08-08 16:52:58 +03:00
9seconds 53dde2aafa Update dependencies 2022-08-08 16:51:47 +03:00
9seconds 36dad5a2f6 Update golangci-lint 2022-08-08 15:54:38 +03:00
9seconds 6a19ded78e Update docs 2022-08-04 18:39:00 +03:00
9seconds 008e17cdff Update golangci-lint to 1.47.3 2022-08-04 11:25:10 +03:00
9seconds 5ad64390d6 There is no need to set rlimit in go 1.19 2022-08-04 10:58:31 +03:00
9seconds f9ad93dd06 More comprehensive build info collection 2022-08-04 10:57:28 +03:00
9seconds 77edea0e18 Update Go in Dockerfile to 1.19 2022-08-04 10:18:32 +03:00
Sergey ArkhipovandGitHub 79f921fde9 Merge pull request #271 from Dank-del/patch-1 2022-07-02 21:41:17 +02:00
Sayan BiswasandGitHub 1cdaaa16b0 change usage of $PWD in docker command 2022-07-02 17:36:03 +05:30
Sayan BiswasandGitHub 5852f48ca5 Fix instructions in readme 2022-06-30 14:59:36 +05:30
9seconds 0e80222990 Merge remote-tracking branch 'origin/stable' 2022-03-21 16:14:15 +03:00
9seconds c07e3d573d Merge remote-tracking branch 'origin/master' into stable 2022-03-21 16:08:55 +03:00
9seconds f192a718f4 Fix test 2022-03-21 15:58:15 +03:00
Sergey ArkhipovandGitHub 0c4523d2c8 Merge pull request #263 from 9seconds/tags-for-ipblocklisted
Tags for ipblocklisted
2022-03-21 14:21:31 +03:00
9seconds b0063a4a25 Increase test time in CI 2022-03-21 14:21:07 +03:00
9seconds 6dc494e7d0 Update README 2022-03-21 13:42:13 +03:00
9seconds 534d5b755e Add tags for ip blocklisted metric 2022-03-21 13:42:13 +03:00
9seconds aa7e488a3a Update dependencies 2022-03-21 12:42:23 +03:00
Sergey ArkhipovandGitHub eba3673e27 Merge pull request #262 from 9seconds/better-whitelist
More elegant management of ip allowlists
2022-03-21 12:13:25 +03:00
9seconds a27facaa16 More elegant management of ip allowlists 2022-03-21 12:04:31 +03:00
Sergey ArkhipovandGitHub 78a9ff18a5 Merge pull request #261 from arch-btw/patch-1
Add AmbientCapabilities
2022-03-21 11:11:12 +03:00
9seconds c0899d0743 Add in-memory file 2022-03-21 10:18:50 +03:00
arch-btwandGitHub 12563219e6 Add AmbientCapabilities
AmbientCapabilities=CAP_NET_BIND_SERVICE
2022-03-20 08:35:20 -07:00
Sergey ArkhipovandGitHub 59b5ff4080 Merge pull request #259 from 9seconds/go118
Support of Go 1.18
2022-03-19 15:18:21 +03:00
9seconds d7e554540d Use recommended way of running golangci-lint install 2022-03-19 14:45:58 +03:00
9seconds 1575b82688 Update golangci-lint 2022-03-19 14:45:58 +03:00
9seconds 5df1f594cc Use embedded buildinfo 2022-03-18 18:06:56 +03:00
9seconds 02ad052c49 Update update-deps make command 2022-03-18 18:06:56 +03:00
9seconds 2b8c7825ca Run fuzzing in GitHub actions 2022-03-18 18:06:56 +03:00
9seconds 75357b3e3c Add fuzz to makefile 2022-03-18 17:26:57 +03:00
9seconds d8b14dc765 Add fuzzing for client hello 2022-03-18 17:24:36 +03:00
9seconds e0850869ba Linting 2022-03-18 14:58:08 +03:00
9seconds 9375552180 Add fuzz tests for obfuscated2 2022-03-18 14:58:03 +03:00
9seconds cbe5b8c94e Update go.mod to 1.18 2022-03-16 09:39:58 +03:00
9seconds 63b425f245 Use Go 1.18 only for CI
This is required due to fuzzing
2022-03-16 09:37:52 +03:00
9seconds b53ead4372 Build image on go 1.18 2022-03-16 09:37:30 +03:00
Sergey ArkhipovandGitHub 2cdd66c722 Merge pull request #258 from AHOHNMYC/readme-service-edit
More detail and secure service example
2022-03-14 09:37:45 +03:00
AHOHNMYCandGitHub 2ae0101d18 More detail and secure service example 2022-03-13 04:52:56 +03:00
9seconds 6da9c2e58d Merge remote-tracking branch 'origin/stable' 2022-03-11 18:17:18 +03:00
9seconds 6d89f14c9b Merge remote-tracking branch 'origin/master' into stable 2022-03-11 18:03:35 +03:00
9seconds 442e2da330 Revert "Consider test modules for update-deps"
This reverts commit 880dd04728.
2022-03-11 17:55:12 +03:00
9seconds a9b3560b25 Remove obsolete Golang 1.16 2022-03-11 17:27:01 +03:00
9seconds 880dd04728 Consider test modules for update-deps 2022-03-11 17:23:53 +03:00
9seconds 8ea1aa3f5e Update dependencies 2022-03-11 17:19:15 +03:00
Sergey ArkhipovandGitHub 4e5a83cfe9 Merge pull request #256 from 9seconds/golangcilint-1.44.2
Update golangci-lint to 1.44.2
2022-03-11 17:13:58 +03:00
9seconds 5282ca26f3 Update golangci-lint to 1.44.2 2022-03-11 17:08:33 +03:00
Sergey ArkhipovandGitHub 2077db1f1e Merge pull request #255 from 9seconds/iplistsize
Add iplist_size metric
2022-03-11 16:42:40 +03:00
9seconds 1a9bc80091 Unexport an error that is not required 2022-03-11 16:22:19 +03:00
9seconds 30170b9413 Add iplist_size metric 2022-03-11 16:20:11 +03:00
Sergey ArkhipovandGitHub 4687a7c899 Merge pull request #254 from 9seconds/cidranger
Use cidrranger instead of patricia
2022-03-11 16:16:52 +03:00
9seconds d467fba674 Use cidrranger instead of patricia 2022-03-11 16:12:45 +03:00
Sergey ArkhipovandGitHub 6e447b1d59 Merge pull request #253 from 9seconds/cannot-dial-err
More correct error if no addresses are found
2022-03-11 11:33:36 +03:00
9seconds b15a8ec4a5 More correct error if no addresses are found 2022-03-11 11:19:15 +03:00
Sergey ArkhipovandGitHub 6bef4df091 Merge pull request #252 from 9seconds/broken-whitelist
Fix broken ip lists
2022-03-11 11:05:11 +03:00
9seconds 0ce0c668b9 Fix broken whitelists 2022-03-11 10:50:15 +03:00
Sergey ArkhipovandGitHub 01e201365b Merge pull request #249 from themegabyte/master
modified docker commands for ease of use
2022-03-09 09:49:07 +03:00
Shayan 0b52367a82 modified docker commands for ease of use 2022-03-07 13:52:57 +05:00
Sergey ArkhipovandGitHub 25c842daf1 Fix typo 2022-03-02 10:25:46 +03:00
9seconds 4c543aaea2 Merge remote-tracking branch 'origin/stable' 2021-12-03 09:36:47 +03:00
240 changed files with 10030 additions and 4070 deletions
+3
View File
@@ -0,0 +1,3 @@
# git config merge.theirs.name "Always accept theirs"
# git config merge.theirs.driver "cp %B %A"
default.pgo binary merge=theirs
+118 -48
View File
@@ -38,37 +38,60 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
matrix:
go_version:
- ~1.16
- ^1.17
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go_version }}
- uses: jdx/mise-action@v3
name: Install mise
- name: Cache dependencies
uses: actions/cache@v2
- name: Cache Go modules and build
uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: go test -coverprofile=./coverage.txt -covermode=atomic -v ./...
run: mise tasks run covtest
- name: Collect coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v5
with:
file: ./coverage.txt
files: ./coverage.txt
fuzz:
name: Fuzzing
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- uses: jdx/mise-action@v3
name: Install mise
- name: Cache Go modules and build
uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run fuzzing
run: mise tasks run 'test:fuzz:*'
lint:
name: Lint
@@ -76,69 +99,116 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: recursive
- name: Run linter
uses: golangci/golangci-lint-action@v2
- uses: jdx/mise-action@v3
name: Install mise
- name: Cache Go modules and build
uses: actions/cache@v5
with:
version: v1.43.0
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run linter
run: mise tasks run lint
artifacts:
name: Build release artifacts
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- uses: jdx/mise-action@v3
name: Install mise
- name: Cache Go modules
uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-gomod-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-gomod-
- name: Cache cross-compilation build
uses: actions/cache@v5
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-${{ hashFiles('**/*.go') }}
restore-keys: |
${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-
${{ runner.os }}-goreleaser-
- name: Run release
run: mise tasks run release
docker:
name: Docker
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
id: meta
uses: docker/metadata-action@v5
with:
images: nineseconds/mtg,ghcr.io/9seconds/mtg
tag-semver: "{{version}},{{major}},{{major}}.{{minor}}"
images: |
nineseconds/mtg
ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Setup BuildX
uses: docker/setup-buildx-action@v1
- name: Setup cache
uses: actions/cache@v2
with:
path: /tmp/buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GHCR.io
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_PAT }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
pull: true
context: .
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=local,src=/tmp/buildx-cache
cache-to: type=local,dest=/tmp/buildx-cache
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+6 -4
View File
@@ -45,11 +45,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: recursive
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -60,7 +62,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -74,4 +76,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v4
+42
View File
@@ -0,0 +1,42 @@
---
name: Vulnerability checks
permissions:
actions: read
checks: read
contents: read
deployments: read
issues: read
discussions: read
pull-requests: read
repository-projects: read
security-events: read
statuses: read
on:
push:
pull_request:
schedule: # daily at 10:22 UTC
- cron: '22 10 * * *'
workflow_dispatch:
jobs:
vuln:
name: Test vulnerabilities
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Check for vulnerabilities
run: |
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
-3
View File
@@ -6,8 +6,5 @@
*.test
*.out
mtg
vendor/
ccbuilds/
.bin/
coverage.txt
dist/
+17 -8
View File
@@ -1,12 +1,21 @@
# https://golangci-lint.run/docs/configuration/file/
version = '2'
[run]
concurrency = 4
deadline = "2m"
tests = true
skip-dirs = ["vendor"]
[output]
format = "colored-line-number"
[linters]
enable-all = true
disable = ["ireturn", "varnamelen", "gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]
# [linters]
# enable-all = true
# disable = [
# "containedctx",
# "exhaustivestruct",
# "exhaustruct",
# "gas",
# "gochecknoglobals",
# "goerr113",
# "ireturn",
# "thelper",
# "varnamelen",
# ]
+90 -5
View File
@@ -1,5 +1,7 @@
---
version: 2
project_name: mtg
before:
@@ -8,13 +10,15 @@ before:
- go generate ./...
builds:
- binary: '{{ .ProjectName }}'
- id: default
binary: '{{ .ProjectName }}'
goos:
- darwin
- freebsd
- linux
- netbsd
- openbsd
- windows
goarch:
- 386
- amd64
@@ -32,30 +36,111 @@ builds:
ignore:
- goos: darwin
goarch: 386
- goos: darwin
goarch: arm
- goos: freebsd
goarch: arm64
- goos: netbsd
goarch: arm64
- goos: openbsd
goarch: arm64
- goos: windows
goarch: 386
- goos: windows
goarch: arm
- id: mips
binary: '{{ .ProjectName }}'
goos:
- linux
goarch:
- mips
- mipsle
gomips:
- softfloat
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
- id: arm64-v9
binary: '{{ .ProjectName }}'
goos:
- darwin
- linux
goarch:
- arm64
goarm64:
- v9.0
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
- id: amd64-v3
binary: '{{ .ProjectName }}'
goos:
- darwin
- freebsd
- linux
- netbsd
- openbsd
- windows
goarch:
- amd64
goamd64:
- v3
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
archives:
- name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
format: tar.gz
- id: default
ids:
- default
- mips
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
formats:
- tar.gz
wrap_in_directory: true
format_overrides:
- goos: windows
format: zip
formats:
- zip
files:
- LICENSE
- README.md
- SECURITY.md
- BEST_PRACTICES.md
- example.config.toml
- id: optimized
ids:
- arm64-v9
- amd64-v3
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm64 }}-{{ .Arm64 }}{{ end }}{{ if .Amd64 }}-{{ .Amd64 }}{{ end }}'
formats:
- tar.gz
wrap_in_directory: true
format_overrides:
- goos: windows
formats:
- zip
files:
- LICENSE
- README.md
- SECURITY.md
- BEST_PRACTICES.md
- example.config.toml
gomod:
proxy: true
snapshot:
name_template: '{{ .Version }}'
version_template: '{{ .Version }}'
checksum:
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
+104
View File
@@ -0,0 +1,104 @@
[tools]
"go:golang.org/x/pkgsite/cmd/pkgsite" = "latest"
"go:golang.org/x/tools/gopls" = "latest"
"go:golang.org/x/vuln/cmd/govulncheck" = "latest"
"go:mvdan.cc/gofumpt" = "latest"
go = "latest"
golangci-lint = "latest"
goreleaser = "latest"
[vars]
fuzzflags = "-fuzztime=120s"
[tasks.build]
description = "Build binary"
sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["mtg"]
run = "go build"
[tasks."build:prof"]
description = "Build binary with profiling enabled"
sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["mtg"]
run = "go build -tags prof"
[tasks.update]
description = "Update dependencies"
run = [
"go get -u",
"go mod tidy -go=1.26"
]
[tasks.lint]
description = "Run linter"
run = "golangci-lint run"
[tasks.vuln]
description = "Test for vulnerabilities"
run = "govulncheck ./..."
[tasks.test]
description = "Run tests"
run = "go test -v -race ./..."
[tasks.covtest]
description = "Run tests with code coverage"
run = "go test -coverprofile=coverage.txt -covermode=atomic -count=2 -race -v ./..."
[tasks.test-all]
description = "Run all tests"
depends = [
"test",
"test:fuzz:*"
]
[tasks."test:fuzz:client-hello"]
description = "Run fuzzy test for ClientHello"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
[tasks."test:fuzz:client-handshake"]
description = "Run fuzzy test for ClientHandshake"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/internal/obfuscation"
[tasks."test:fuzz:server-handshake-frame"]
description = "Run fuzzy test for GenerateHandshakeFrame"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/internal/obfuscation"
[tasks.static]
description = "Build static binary"
sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["mtg"]
run = """
#!/bin/bash
version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)"
go build \
-trimpath \
-mod=readonly \
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
-a \
-tags netgo
"""
[tasks.release]
description = "Create release tarballs"
sources = ["**/*.go", "go.mod", "go.sum", ".goreleaser.yml"]
run = [
"goreleaser --snapshot --clean",
"find dist -depth 1 -type d | xargs -r rm -r",
"rm ./dist/config.yaml"
]
[tasks.image]
description = "Build docker image"
sources = ["**/*.go", "go.mod", "go.sum", "Dockerfile"]
run = "docker buildx build --pull -t mtg ."
[tasks.docs]
description = "Run doc server"
run = "pkgsite -http 0.0.0.0:10000"
[tasks.fmt]
description = "Reformat source code"
sources = ["**/*.go"]
run = "gofumpt -w --extra ."
+55
View File
@@ -0,0 +1,55 @@
# Best practices
This is unfortunate, but since 2018 many things were changed. Most of them
became way worse. Previous iterations of censorship systems were very dumb,
DPI were primitive and filtered very obvious things. Nowadays they are
way more intelligent and it is very naive to treat them frivolously.
In 2026 is not enough to pretend that your mtg installation is a Microsoft
website that sits in Amsterdam Digital Ocean location. Now your installation
has to be a website that is mtg in disguise. Yes, it requires a bit more effort
but this effort is probably less than rotating proxies each other day.
mtproto traffic, even with FakeTLS, has its specifics that are probably
very well known by DPI systems. These specifics are not something unique but
could mark an IP address as suspicious. Now let's think:
1. You have a proxy in Amsterdam Digital Ocean that tells it is microsoft.com
how hard could it be to find out that this is probably fake? 1 or probably 2
DNS queries for `microsoft.com`? In case of some CDN, there are ECS-powered
resolvers that are very capable to return results from POV of some subnets.
If censor sees no relevant results, will they be afraid to block IP?
2. You have a proxy in Amsterdam Digital Ocean that tells it is a website from
the same public subnet. But not the same. Would it be hard to make these DNS
queries and ban IP?
The correct way of having this proxy is following:
1. Register a domain name
2. Get some VPS, probably in your domestic location
3. Set that domain name from a step 1 to IP address of that VPS
4. Generate a couple of HTML pages by LLMs or even copy them from elsewhere
5. Set some webserver and issue TLS certificates with Let's Encrypt or any other
name
6. Set mtg before this webserver.
7. Use sing-box or anything like that to provide local socks5 interface and
have VPNized uplinks
8. Set up mtg to use socks5 from a 7 step.
In that case you will get a match of DNS and SNI in requests. As a side effect,
your proxy will work with XTLS and its friends: XTLS in sniff mode ignores
IP address a client wants to connect to. Instead, it reads SNI and connect
to resolved address: a clever idea if user does not have a trustworthy DNS
set up.
Yes, this is much longer that usual technique, and requires more effort. But
this is could probably be very well automated to some reasonable extent.
Unfortunately, this is a best practice right now.
Do not also forget about other implementation, like
[telemt](https://github.com/telemt/telemt). Try everything. Use VPNs. It does
not really matter which project you are going to use as long it helps you to
stay connected.
_March 2026._
+35 -10
View File
@@ -1,21 +1,45 @@
###############################################################################
# BUILD STAGE
FROM golang:1.17-alpine AS build
FROM golang:1.26-alpine AS build
ENV CGO_ENABLED=0
# this is done for backward compatibility: before that we mounted a config
# into /config.toml. Some application allow mounting directories only,
# so it makes problems. So, instead we are going to do 2 steps:
# 1. Create /config/config.toml as a symlink to /config.toml
# 2. Force /mtg to use /config/config.toml
#
# it helps in both ways: users with directories could use /config directory
# and overlap a symlink by their bind mount. Old users could continue using
# /config.toml as a real config.
RUN set -x \
&& apk --no-cache --update add \
bash \
ca-certificates \
curl \
git \
make
&& mkdir -p /config \
&& ln -sv /config.toml /config/config.toml
COPY . /app
RUN --mount=type=cache,target=/var/cache/apk \
set -x \
&& apk --update add \
bash \
ca-certificates \
git
COPY go.mod go.sum /app/
WORKDIR /app
RUN go mod download
COPY . /app
RUN set -x \
&& make -j 4 static
&& version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always 2>/dev/null || echo dev)" \
&& go build \
-trimpath \
-mod=readonly \
-ldflags="-extldflags '-static' -s -w -X 'main.version=$version'" \
-a \
-tags netgo
###############################################################################
@@ -24,8 +48,9 @@ RUN set -x \
FROM scratch
ENTRYPOINT ["/mtg"]
CMD ["run", "/config.toml"]
CMD ["run", "/config/config.toml"]
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /app/mtg /mtg
COPY --from=build /app/example.config.toml /config.toml
COPY --from=build /config /config
-98
View File
@@ -1,98 +0,0 @@
ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
IMAGE_NAME := mtg
APP_NAME := $(IMAGE_NAME)
GOLANGCI_LINT_VERSION := v1.43.0
VERSION_GO := $(shell go version)
VERSION_DATE := $(shell date -Ru)
VERSION_TAG := $(shell git describe --tags --always)
COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
GOBIN := $(ROOT_DIR)/.bin
GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
# -----------------------------------------------------------------------------
.PHONY: all
all: build
.PHONY: build
build:
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
$(APP_NAME): build
.PHONY: static
static:
@env CGO_ENABLED=0 GOOS=linux go build \
$(COMMON_BUILD_FLAGS) \
-tags netgo \
-a \
-o "$(APP_NAME)"
vendor: go.mod go.sum
@$(MOD_ON) go mod vendor
.bin:
@mkdir -p "$(GOBIN)" || true
.PHONY: fmt
fmt:
@$(GOTOOL) gofumpt -w -extra "$(ROOT_DIR)"
.PHONY: test
test:
@go test -v ./...
.PHONY: citest
citest:
@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
.PHONY: clean
clean:
@git clean -xfd && \
git reset --hard >/dev/null && \
git submodule foreach --recursive sh -c 'git clean -xfd && git reset --hard' >/dev/null
.PHONY: lint
lint:
@$(GOTOOL) golangci-lint run
.PHONY: release
release:
@$(GOTOOL) goreleaser release --snapshot --rm-dist && \
find "$(ROOT_DIR)/dist" -type d | grep -vP "dist$$" | xargs -r rm -rf && \
rm -f "$(ROOT_DIR)/dist/config.yaml"
.PHONY: docker
docker:
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
.PHONY: doc
doc:
@$(GOTOOL) godoc -http 0.0.0.0:10000
.PHONY: install-tools
install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt install-tools-goreleaser
.PHONY: install-tools-lint
install-tools-lint: .bin
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
| bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)"
.PHONY: install-tools-godoc
install-tools-godoc: .bin
@$(GOTOOL) go install golang.org/x/tools/cmd/godoc@latest
.PHONY: install-tools-gofumpt
install-tools-gofumpt: .bin
@$(GOTOOL) go install mvdan.cc/gofumpt@latest
.PHONY: goreleaser
install-tools-goreleaser: .bin
@$(GOTOOL) go install github.com/goreleaser/goreleaser@latest
.PHONY: update-deps
update-deps:
@go get -u && go mod tidy -go=1.17
+171 -8
View File
@@ -1,6 +1,6 @@
# mtg
Highly-opionated (ex-bullshit-free) MTPROTO proxy for
Highly-opinionated (ex-bullshit-free) MTPROTO proxy for
[Telegram](https://telegram.org/).
[![CI](https://github.com/9seconds/mtg/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
@@ -10,6 +10,17 @@ Highly-opionated (ex-bullshit-free) MTPROTO proxy for
**If you use v1.0 or upgrade broke you proxy, please read the chapter
[Version 2](#version-2)**
If you want to have a proxy that _supports adtag_ (possibility to promote a
channel with a special Telegram bot), I recommend to use
[telemt](https://github.com/telemt/telemt) project. v1 of mtg supports it
but I do not see any reasonable point of using it: adtag requires communication
via a fragile set of middle proxies, requires complex setup that must expose
a public IPs, has lower bandwidth and latency.
mtg idea is simple: minimal unbloated proxy that can handle a reasonable scale
~10-20k simultaneous connections, has no user management, but ticks all
checkboxes related to its main intent: provide a way to use Telegram.
## Rationale
There are several available proxies for Telegram MTPROTO available. Here
@@ -18,6 +29,9 @@ are the most notable:
* [Official](https://github.com/TelegramMessenger/MTProxy)
* [Python](https://github.com/alexbers/mtprotoproxy)
* [Erlang](https://github.com/seriyps/mtproto_proxy)
* [Teleproxy (C)](https://github.com/teleproxy/teleproxy)
* [mtproto.zig (Zig)](https://github.com/sleep3r/mtproto.zig)
* [Telemt (Rust)](https://github.com/telemt/telemt)
You can use any of these. They work great and all implementations have
feature parity now. This includes support of adtag, replay attack
@@ -26,6 +40,33 @@ goal: to give a possibility to connect to Telegram in a restricted,
censored environment. But it does it slightly differently in details
that probably matter.
* **Domain fronting**
For years mtg supports domain fronting. This technique means that it fallbacks
to accessing a real website in case if request fails. It could fail by many
reasons: anti-replay protection, accidental access to the webserver or
stale request. Anyway, if mtg rejects this request, it does not break a
connection. It connects to the websites and replicates everything that client
has sent, and simply proxies it back as is. Users will see a response from
the real website, _byte-to-byte identical_ to the response of the real netloc.
* **Doppelganger**
mtg also is a doppelganger of the website it fronts. Sure, with domain fronting
users will see replies of the real website in case if something will go wrong.
But what about such cases when _everything is fine_?
In that case mtg mimics TLS connection statistical characteristics as close as
possible. Different application have different statistics of their patterns.
Big CDN steadily pumping the data, small websites burst with short easily
compressiable chunks of traffic.
mtg artificially emulates those delays to be statistically indistinguishable
from the real website even if it covers connection of the very specific app.
It also follows 2 most common patterns of traffic chunking, so censors
will have to put more resources to find out that we have Telegram here
but not a hookah webshop served by nginx.
* **Resource-efficient**
It has to be resource-efficient. It does not mean that you will see
@@ -40,12 +81,21 @@ that probably matter.
way of doing business I suppose. I think the only viable way is to
have a proxy that can be restored anywhere easily.
* **Supports proxy protocol v1/v2**
This makes integration with loadbalancers like HAProxy and ELB a first class
citizen by supporting their
[commuication protocols](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt).
* **A single secret**
I think that multiple secrets solve no problems and just complex
software. I also believe that in the case of throwout proxies, this
the feature is a useless luxury.
This is very controversial topic. Please read [rationale (in russian)](https://github.com/9seconds/mtg/issues/376#issuecomment-4118726699)
and use [mtg-multi](https://github.com/dolonet/mtg-multi) fork if you are disagree with.
* **No adtag support**
Please read [Version 2](#version-2) chapter.
@@ -75,6 +125,8 @@ that probably matter.
software (written in Golang) with a minimum effort + you can replace
some parts with those you want.
Please also to read about [best practices](https://github.com/9seconds/mtg/blob/master/BEST_PRACTICES.md).
### Version 2
If you use version 1.x before, you are probably noticed some major
@@ -162,6 +214,12 @@ This project has several main branches
## Getting started
### Download mise
mtg uses [mise](https://mise.jdx.dev/) to maintain its development
dependencies + replaces a make for building things. Please
[install](https://mise.jdx.dev/getting-started.html) it first.
### Download a tool
#### Download binaries
@@ -190,7 +248,7 @@ surprises. Always choose some version tag.
Also, if you have `go` installed, you can always download this tool with `go get`:
```console
go get github.com/9seconds/mtg/v2
go install github.com/9seconds/mtg/v2@latest
```
#### Build from sources
@@ -198,13 +256,14 @@ go get github.com/9seconds/mtg/v2
```console
git clone https://github.com/9seconds/mtg.git
cd mtg
make static
mise install
mise tasks run build
```
or for the docker image:
```console
make docker
mise tasks run image
```
### Generate secret
@@ -224,6 +283,16 @@ $ mtg generate-secret --hex google.com
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
```
equivalent commands with docker:
```console
$ docker run --rm nineseconds/mtg:2 generate-secret google.com
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
$ docker run --rm nineseconds/mtg:2 generate-secret --hex google.com
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
```
This secret is a keystone for a proxy and your password for a client.
You need to keep it secured.
@@ -237,6 +306,38 @@ For example, you've bought a VPS from [Digital
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
generate a secret for _digitalocean.com_ then.
### Check configuration
There is a special command for secret verification:
```
$ mtg doctor /path/to/my/config.toml
Deprecated options
✅ All good
Time skewness
✅ Time drift is -607.048µs, but tolerate-time-skewness is 5s
Validate native network connectivity
✅ DC 1
✅ DC 2
✅ DC 3
✅ DC 4
✅ DC 5
✅ DC 203
Validate network connectivity with proxy socks5://127.0.0.1:1080
✅ DC 1
✅ DC 2
✅ DC 3
✅ DC 4
✅ DC 5
✅ DC 203
Validate fronting domain connectivity
✅ xx.xx.xx.xx:yyy is reachable
Validate SNI-DNS match
✅ IP address xx.xx.xx.xx matches secret hostname <REDACTED>
```
It aims to find out possible inconsistencies and problems with your
configuration. It makes sense to run it before executing any relevant commands.
### Simple run mode
@@ -265,7 +366,7 @@ Flags:
-b, --tcp-buffer="4KB" Size of TCP buffer to use.
-i, --prefer-ip="prefer-ipv6" IP preference. By default we prefer IPv6 with fallback to IPv4.
-p, --domain-fronting-port=443 A port to access for domain fronting.
-n, --doh-ip=9.9.9.9 IP address of DNS-over-HTTP to use.
-n, --doh-ip=1.1.1.1 IP address of DNS-over-HTTP to use.
-t, --timeout=10s Network timeout to use
-a, --antireplay-cache-size="1MB" A size of anti-replay cache to use.
```
@@ -307,12 +408,17 @@ Now you can create a systemd unit:
```console
$ cat /etc/systemd/system/mtg.service
[Unit]
Description=mtg
Description=mtg - MTProto proxy server
Documentation=https://github.com/9seconds/mtg
After=network.target
[Service]
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
Restart=always
RestartSec=3
DynamicUser=true
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
@@ -324,7 +430,7 @@ $ sudo systemctl start mtg
or you can run a docker image
```console
docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
docker run -d -v $PWD/config.toml:/config.toml -p 443:3128 --name mtg-proxy --restart=unless-stopped nineseconds/mtg:2
```
where _443_ is a host port (a port you want to connect to from a
@@ -353,6 +459,61 @@ $ mtg access /etc/mtg.toml
}
```
or if you are using docker:
```console
$ docker exec mtg-proxy /mtg access /config.toml
```
## Doppelganger
mtg can mimic real websites, please take a look at relevant section in example
config file.
mtg comes with some very good precollected statistics coming from
[ok.ru](https://ok.ru/). It does not mean that you have to cover yourself
by pretending that mtg is _ok.ru_. **Do not do that: ok.ru comes from very specific
ASNs, but not from VPS providers you are going to use.** What I want to say
is that defaults are very good enough to use as is because ok.ru for public
pages has a very generic profile of TLS packets delay.
But for better results it is recommended to teach mtg about the website you
will use as a domain front. In order to do that, you need to specify URLs
from this website. Just go to it, open WebDeveloper console and pick up
random URLs. For better results they have to be **from the same domain name
you are going to use as a disguise** but serve light and heavy content: pages,
images etc. Do not use many, 2-3 will probably work.
mtg will crawl these pages periodically, accumulating statistics and
using it as you go.
```toml
[defense.doppelganger]
urls = [
"https://lalala.com/index.html",
"https://lalala.com/contacts.html",
]
```
This is not very necessary. Keep in mind these rules:
1. If you are not sure what is this all about, do nothing. Defaults are good.
2. All URLs must be HTTPS
3. All URLs should be from the same domain name (but this is not a rule)
4. Do not use a lot of pages. Use _different_ pages. mtg will start using this
statistics when it will accumulate enough anyway.
5. These URLs should be directly accessible from mtg without proxies whatsoever
6. Do not create huge raids. mtg will repeatedly crawl in raids, making N repeats.
Do not use high N, you do not want to be noticeable.
7. It makes no sense to have small delay between raids. Usually webservers
do not update their TLS settings each hour.
8. If you have some specific knowledge if webserver is using
[TLS Dynamic Record Sizing](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/), you
can use a very specific setting. This are Cloudflare, Go standard webservers,
[caddy](https://caddyserver.com/) and [H2O](https://h2o.examp1e.net/). If so,
you can enable `drs` setting.
9. **If you are not sure, touch nothing!**
## Metrics
Out of the box, mtg works with
@@ -367,11 +528,12 @@ Here goes a list of metrics with their types but without a prefix.
| client_connections | gauge | `ip_family` | Count of processing client connections. |
| telegram_connections | gauge | `telegram_ip`, `dc` | Count of connections to Telegram servers. |
| domain_fronting_connections | gauge | `ip_family` | Count of connections to fronting domain. |
| iplist_size | gauge | `ip_list` | A size of either allowlist or blocklist in use. |
| telegram_traffic | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram. |
| domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. |
| domain_fronting | counter | | Count of domain fronting events. |
| concurrency_limited | counter | | Count of events, when client connection was rejected due to concurrency limit. |
| ip_blocklisted | counter | | Count of events when client connection was rejected because IP was found in the blacklist. |
| ip_blocklisted | counter | `ip_list` | Count of events when client connection was rejected because IP was found in the blocklist. |
| replay_attacks | counter | | Count of detected replay attacks. |
Tag meaning:
@@ -382,3 +544,4 @@ Tag meaning:
| dc | | A number of the Telegram DC for a connection. |
| telegram_ip | | IP address of the Telegram server. |
| direction | `to_client`, `from_client` | A direction of the traffic flow. |
| ip_list | `allowlist`, `blocklist` | A type of the IP list. |
+8 -8
View File
@@ -1,17 +1,17 @@
// Antireplay package has cache implementations that are effective
// against replay attacks.
// Antireplay package has cache implementations that are effective against
// replay attacks.
//
// To understand more about replay attacks, please read documentation
// for mtglib.AntiReplayCache interface. This package has a list of some
// To understand more about replay attacks, please read documentation for
// [mtglib.AntiReplayCache] interface. This package has a list of some
// implementations of this interface.
package antireplay
const (
// DefaultStableBloomFilterMaxSize is a recommended byte size for a
// stable bloom filter.
// DefaultStableBloomFilterMaxSize is a recommended byte size for a stable
// bloom filter.
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
// DefaultStableBloomFilterErrorRate is a recommended default error
// rate for a stable bloom filter.
// DefaultStableBloomFilterErrorRate is a recommended default error rate for a
// stable bloom filter.
DefaultStableBloomFilterErrorRate = 0.001
)
+2 -3
View File
@@ -6,9 +6,8 @@ type noop struct{}
func (n noop) SeenBefore(_ []byte) bool { return false }
// NewNoop returns an implementation that does nothing. A corresponding
// method always returns false, so this cache accepts everything you
// pass to it.
// NewNoop returns an implementation that does nothing. A corresponding method
// always returns false, so this cache accepts everything you pass to it.
func NewNoop() mtglib.AntiReplayCache {
return noop{}
}
+9 -9
View File
@@ -20,19 +20,19 @@ func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
return s.filter.TestAndAdd(digest)
}
// NewStableBloomFilter returns an implementation of AntiReplayCache
// based on stable bloom filter.
// NewStableBloomFilter returns an implementation of AntiReplayCache based on
// stable bloom filter.
//
// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
//
// The basic idea of a stable bloom filter is quite simple: each time
// when you set a new element, you randomly reset P elements. There is a
// hardcore math which proves that if you choose this P correctly, you
// can maintain the same error rate for a stream of elements.
// The basic idea of a stable bloom filter is quite simple: each time when you
// set a new element, you randomly reset P elements. There is a hardcore math
// which proves that if you choose this P correctly, you can maintain the same
// error rate for a stream of elements.
//
// byteSize is the number of bytes you want to give to a bloom filter.
// errorRate is desired false-positive error rate. If you want to use
// default values, please pass 0 for byteSize and <0 for errorRate.
// errorRate is desired false-positive error rate. If you want to use default
// values, please pass 0 for byteSize and <0 for errorRate.
func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
if byteSize == 0 {
byteSize = DefaultStableBloomFilterMaxSize
@@ -42,7 +42,7 @@ func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCac
errorRate = DefaultStableBloomFilterErrorRate
}
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) // nolint: gomnd
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate)
sf.SetHash(xxhash.New64())
return &stableBloomFilter{
+1 -1
View File
@@ -12,7 +12,7 @@ type StableBloomFilterTestSuite struct {
}
func (suite *StableBloomFilterTestSuite) TestOp() {
filter := antireplay.NewStableBloomFilter(500, 0.001)
filter := antireplay.NewStableBloomFilter(100000, 0.001)
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"runtime/debug"
"sort"
"strconv"
"time"
)
var version = "dev" // has to be set by ldflags
const (
buildInfoModuleStart byte = iota
buildInfoModuleFinish
buildInfoModuleDelimeter
)
func getVersion() string {
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return version
}
date := time.Now()
commit := ""
goVersion := buildInfo.GoVersion
dirtySuffix := ""
for _, setting := range buildInfo.Settings {
switch setting.Key {
case "vcs.time":
date, _ = time.Parse(time.RFC3339, setting.Value)
case "vcs.revision":
commit = setting.Value
case "vcs.modified":
if dirty, _ := strconv.ParseBool(setting.Value); dirty {
dirtySuffix = " [dirty]"
}
}
}
hasher := sha256.New()
checksumModule := func(mod *debug.Module) {
hasher.Write([]byte{buildInfoModuleStart})
io.WriteString(hasher, mod.Path) //nolint: errcheck
hasher.Write([]byte{buildInfoModuleDelimeter})
io.WriteString(hasher, mod.Version) //nolint: errcheck
hasher.Write([]byte{buildInfoModuleDelimeter})
io.WriteString(hasher, mod.Sum) //nolint: errcheck
hasher.Write([]byte{buildInfoModuleFinish})
}
io.WriteString(hasher, buildInfo.Path) //nolint: errcheck
binary.Write(hasher, binary.LittleEndian, uint64(1+len(buildInfo.Deps))) //nolint: errcheck
sort.Slice(buildInfo.Deps, func(i, j int) bool {
return buildInfo.Deps[i].Path > buildInfo.Deps[j].Path
})
checksumModule(&buildInfo.Main)
for _, module := range buildInfo.Deps {
checksumModule(module)
}
return fmt.Sprintf("%s (%s: %s on %s%s, modules checksum %s)",
version,
goVersion,
date.Format(time.RFC3339),
commit,
dirtySuffix,
base64.StdEncoding.EncodeToString(hasher.Sum(nil)))
}
BIN
View File
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
package essentials
// TelegramCoreAddresses are publicly known addresses of Telegram core network.
var TelegramCoreAddresses = map[int][]string{
1: {
"149.154.175.50:443",
"[2001:b28:f23d:f001::a]:443",
},
2: {
"149.154.167.51:443",
"95.161.76.100:443",
"[2001:67c:04e8:f002::a]:443",
},
3: {
"149.154.175.100:443",
"[2001:b28:f23d:f003::a]:443",
},
4: {
"149.154.167.91:443",
"[2001:67c:04e8:f004::a]:443",
},
5: {
"149.154.171.5:443",
"[2001:b28:f23f:f005::a]:443",
},
203: {
"91.105.192.100:443",
"[2a0a:f280:0203:000a:5000:0000:0000:0100]:443",
},
}
+28 -3
View File
@@ -5,22 +5,47 @@ import (
"net"
)
// CloseableReader is a reader interface that can close its reading end.
// CloseableReader is an [io.Reader] interface that can close its reading end.
type CloseableReader interface {
io.Reader
CloseRead() error
}
// CloseableWriter is a writer that can close its writing end.
// CloseableWriter is an [io.Writer] that can close its writing end.
type CloseableWriter interface {
io.Writer
CloseWrite() error
}
// Conn is an extension of net.Conn that can close its ends. This mostly
// Conn is an extension of [net.Conn] that can close its ends. This mostly
// implies TCP connections.
type Conn interface {
net.Conn
CloseableReader
CloseableWriter
}
type netConnWrapper struct {
net.Conn
}
func (n netConnWrapper) CloseRead() error {
if conn, ok := n.Conn.(CloseableReader); ok {
return conn.CloseRead()
}
return n.Close()
}
func (n netConnWrapper) CloseWrite() error {
if conn, ok := n.Conn.(CloseableWriter); ok {
return conn.CloseWrite()
}
return n.Close()
}
// WrapConn wraps a generic [net.Conn] into Conn.
func WrapNetConn(conn net.Conn) Conn {
return netConnWrapper{conn}
}
+7 -5
View File
@@ -2,14 +2,14 @@ package events
import (
"context"
"math/rand"
"math/rand/v2"
"runtime"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/OneOfOne/xxhash"
)
// EventStream is a default implementation of the mtglib.EventStream
// EventStream is a default implementation of the [mtglib.EventStream]
// interface.
//
// EventStream manages a set of goroutines, observers. Main
@@ -38,7 +38,7 @@ func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
select {
case <-ctx.Done():
case <-e.ctx.Done():
case e.chans[int(chanNo)%len(e.chans)] <- evt:
case e.chans[chanNo%uint32(len(e.chans))] <- evt:
}
}
@@ -64,7 +64,7 @@ func NewEventStream(observerFactories []ObserverFactory) EventStream {
chans: make([]chan mtglib.Event, runtime.NumCPU()),
}
for i := 0; i < runtime.NumCPU(); i++ {
for i := range runtime.NumCPU() {
rv.chans[i] = make(chan mtglib.Event, 1)
if len(observerFactories) == 1 {
@@ -77,7 +77,7 @@ func NewEventStream(observerFactories []ObserverFactory) EventStream {
return rv
}
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { // nolint: cyclop
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { //nolint: cyclop
defer observer.Shutdown()
for {
@@ -102,6 +102,8 @@ func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, ob
observer.EventConcurrencyLimited(typedEvt)
case mtglib.EventReplayAttack:
observer.EventReplayAttack(typedEvt)
case mtglib.EventIPListSize:
observer.EventIPListSize(typedEvt)
}
}
}
+21
View File
@@ -204,6 +204,27 @@ func (suite *EventStreamTestSuite) TestEventReplayAttack() {
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventIPListSize() {
evt := mtglib.NewEventIPListSize(10, true)
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventIPListSize", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventIPListSize)
suite.True(ok)
suite.Equal(evt.Timestamp(), caught.Timestamp())
suite.Equal(evt.Size, caught.Size)
suite.Equal(evt.IsBlockList, caught.IsBlockList)
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TearDownTest() {
suite.stream.Shutdown()
suite.ctxCancel()
+23 -20
View File
@@ -1,19 +1,19 @@
// Events has a default implementations of EventStream for mtglib.
//
// Please see documentation for mtglib.EventStream interface to get an
// idea of such an abstraction. This package has implementations for the
// default event stream.
// Please see documentation for [mtglib.EventStream] interface to get an idea
// of such an abstraction. This package has implementations for the default
// event stream.
//
// Default event stream has a list of its own concepts. First, all it
// does is a routing of messages to known observers. It takes an event,
// defines its type and pass this message to a method of the observer.
// Default event stream has a list of its own concepts. First, all it does is a
// routing of messages to known observers. It takes an event, defines its type
// and pass this message to a method of the observer.
//
// There might be many observers, but default event stream has a
// guarantee though. It uses StreamID as a sharding key and guarantees
// that a message with the same StreamID will be devlivered to the same
// observer instance. So, each producer is guarateed to get all relevant
// messages related to the same session. It is not possible that it will
// get EventFinish if it has not seen EventStart for that session yet.
// There might be many observers, but default event stream has a guarantee
// though. It uses StreamID as a sharding key and guarantees that a message
// with the same StreamID will be devlivered to the same observer instance. So,
// each producer is guarateed to get all relevant messages related to the same
// session. It is not possible that it will get EventFinish if it has not seen
// EventStart for that session yet.
package events
import "github.com/9seconds/mtg/v2/mtglib"
@@ -21,10 +21,10 @@ import "github.com/9seconds/mtg/v2/mtglib"
// Observer is an instance that listens for the incoming events.
//
// As it is said in the package description, the default event stream
// guarantees that all events with the same StreamID are going to be
// routed to the same instance of the observer. So, there is no need
// to synchronize information about streams between many observers
// instances, they can have their local storage.
// guarantees that all events with the same StreamID are going to be routed to
// the same instance of the observer. So, there is no need to synchronize
// information about streams between many observers instances, they can have
// their local storage.
type Observer interface {
// EventStart reacts on incoming mtglib.EventStart event.
EventStart(mtglib.EventStart)
@@ -53,6 +53,9 @@ type Observer interface {
// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
EventReplayAttack(mtglib.EventReplayAttack)
// EventIPListSize reacts on incoming mtglib.EventIPListSize
EventIPListSize(mtglib.EventIPListSize)
// Shutdown stop observer. Default event stream guarantees:
// 1. If shutdown is executed, it is executed only once
// 2. Observer won't receieve any new message after this
@@ -62,8 +65,8 @@ type Observer interface {
// ObserverFactory creates a new instance of the observer.
//
// Default event stream creates a small set of goroutines to manage
// incoming messages. Each message is routed to an appropriate observer
// based on a sharding key, stream id. So, it is possible that an
// instance of mtg will have many observer instances, not a single one.
// Default event stream creates a small set of goroutines to manage incoming
// messages. Each message is routed to an appropriate observer based on a
// sharding key, stream id. So, it is possible that an instance of mtg will
// have many observer instances, not a single one.
type ObserverFactory func() Observer
+4
View File
@@ -41,6 +41,10 @@ func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
o.Called(evt)
}
func (o *ObserverMock) EventIPListSize(evt mtglib.EventIPListSize) {
o.Called(evt)
}
func (o *ObserverMock) Shutdown() {
o.Called()
}
+35 -47
View File
@@ -12,14 +12,11 @@ type multiObserver struct {
func (m multiObserver) EventStart(evt mtglib.EventStart) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventStart(evt)
}(v)
wg.Go(func() {
v.EventStart(evt)
})
}
wg.Wait()
@@ -27,14 +24,11 @@ func (m multiObserver) EventStart(evt mtglib.EventStart) {
func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventConnectedToDC(evt)
}(v)
wg.Go(func() {
v.EventConnectedToDC(evt)
})
}
wg.Wait()
@@ -42,14 +36,11 @@ func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventDomainFronting(evt)
}(v)
wg.Go(func() {
v.EventDomainFronting(evt)
})
}
wg.Wait()
@@ -57,14 +48,11 @@ func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventTraffic(evt)
}(v)
wg.Go(func() {
v.EventTraffic(evt)
})
}
wg.Wait()
@@ -72,14 +60,11 @@ func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventFinish(evt)
}(v)
wg.Go(func() {
v.EventFinish(evt)
})
}
wg.Wait()
@@ -87,14 +72,11 @@ func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventConcurrencyLimited(evt)
}(v)
wg.Go(func() {
v.EventConcurrencyLimited(evt)
})
}
wg.Wait()
@@ -102,14 +84,11 @@ func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimite
func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventIPBlocklisted(evt)
}(v)
wg.Go(func() {
v.EventIPBlocklisted(evt)
})
}
wg.Wait()
@@ -117,14 +96,23 @@ func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
wg.Go(func() {
v.EventReplayAttack(evt)
})
}
obs.EventReplayAttack(evt)
}(v)
wg.Wait()
}
func (m multiObserver) EventIPListSize(evt mtglib.EventIPListSize) {
wg := &sync.WaitGroup{}
for _, v := range m.observers {
wg.Go(func() {
v.EventIPListSize(evt)
})
}
wg.Wait()
+1
View File
@@ -25,6 +25,7 @@ func (n noopObserver) EventFinish(_ mtglib.EventFinish)
func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {}
func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {}
func (n noopObserver) EventIPListSize(_ mtglib.EventIPListSize) {}
func (n noopObserver) Shutdown() {}
// NewNoopObserver creates an observer which discards each message.
+3
View File
@@ -27,6 +27,7 @@ func (suite *NoopTestSuite) SetupSuite() {
"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
"ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
"replay-attack": mtglib.NewEventReplayAttack("connID"),
"ip-list-size": mtglib.NewEventIPListSize(10, true),
}
suite.ctx = context.Background()
}
@@ -65,6 +66,8 @@ func (suite *NoopTestSuite) TestObserver() {
observer.EventIPBlocklisted(typedEvt)
case mtglib.EventReplayAttack:
observer.EventReplayAttack(typedEvt)
case mtglib.EventIPListSize:
observer.EventIPListSize(typedEvt)
}
})
}
+164 -25
View File
@@ -23,17 +23,19 @@ secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c6561706973
# Host:port pair to run proxy on.
bind-to = "0.0.0.0:3128"
# This defines what types of traffic mtg listens to. If you are not sure,
# then definitely keep it disable. Enable it only and only if incoming traffic
# is coming from some sort of load-balancer like HAProxy or ELB.
# https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt
#
# mtg uses a library that supports v1 and v2 versions of ProxyProtocol.
# default value is false.
# proxy-protocol-listener = false
# Defines how many concurrent connections are allowed to this proxy.
# All other incoming connections are going to be dropped.
concurrency = 8192
# A size of user-space buffer for TCP to use. Since we do 2 connections,
# then we have tcp-buffer * (4 + 2) per each connection: read/write for
# each connection + 2 copy buffers to pump the data between sockets.
#
# Deprecated: this setting is no longer makes any effect.
# tcp-buffer = "4kb"
# Sometimes you want to enforce mtg to use some types of
# IP connectivity to Telegram. We have 4 modes:
# - prefer-ipv6:
@@ -46,9 +48,42 @@ concurrency = 8192
# Only ipv4 connectivity is used
prefer-ip = "prefer-ipv6"
# Public IP addresses of this server. Used by 'mtg access' to generate
# proxy links and by 'mtg doctor' to validate SNI-DNS match.
# If not set, mtg tries to detect them automatically via ifconfig.co.
# Set these if ifconfig.co is unreachable from your server.
# public-ipv4 = "1.2.3.4"
# public-ipv6 = "2001:db8::1"
# If this setting is set, then mtg will try to get proxy updates from Telegram
# Usually this is completely fine to have it disabled, because mtg has a list
# of some core proxies hardcoded.
auto-update = false
# FakeTLS uses domain fronting protection. So it needs to know a port to
# access.
domain-fronting-port = 443
#
# Deprecated: use [domain-fronting] configuration block. If relevant option
# is defined there, this one would be ignored.
# domain-fronting-port = 443
# By default, mtg resolves the fronting hostname (from the secret) via DNS
# to establish a TCP connection. If DNS resolution of that hostname is blocked,
# you can specify an IP address to connect to directly. The hostname is still
# used for SNI in the TLS handshake.
#
# default value is not set (DNS resolution is used).
#
# Deprecated: use [domain-fronting] configuration block. If relevant option
# is defined there, this one would be ignored.
# domain-fronting-ip = "10.0.0.10"
# This makes a communication between both fronting website and mtg to use
# proxy protocol.
#
# Deprecated: use [domain-fronting] configuration block. If relevant option
# is defined there, this one would be ignored.
# domain-fronting-proxy-protocol = false
# FakeTLS can compare timestamps to prevent probes. Each message has
# encrypted timestamp. So, mtg can compare this timestamp and decide if
@@ -71,6 +106,27 @@ tolerate-time-skewness = "5s"
# Otherwise, chose a new DC.
allow-fallback-on-unknown-dc = false
# This section is relevant to communication with fronting domain. Usually
# you do not need to setup anything here but there are plenty of cases, especially
# if you put mtg behind load balancer, when some specific configuration is
# required.
[domain-fronting]
# By default, mtg resolves the fronting hostname (from the secret) via DNS
# to establish a TCP connection. If DNS resolution of that hostname is blocked,
# you can specify an IP address to connect to directly. The hostname is still
# used for SNI in the TLS handshake.
#
# default value is not set (DNS resolution is used).
# ip = "10.10.10.11"
# FakeTLS uses domain fronting protection. So it needs to know a port to
# access. Default value is 443
# port = 443
# This makes a communication between both fronting website and mtg to use
# proxy protocol.
# proxy-protocol = false
# network defines different network-related settings
[network]
# please be aware that mtg needs to do some external requests. For
@@ -84,8 +140,43 @@ allow-fallback-on-unknown-dc = false
# resolver of the operating system and uses DOH instead. This is a host
# it has to access.
#
# By default we use Quad9.
doh-ip = "9.9.9.9"
# By default we use Cloudflare.
#
# DEPRECATED option:
# If dns option is specified, it will be used instead. No default is defined
# anymore. But if you set this option, this is literally the same providing
# https:// URL to `dns`.
#
# In other words:
#
# doh-ip = "1.1.1.1"
#
# is literally the same as
#
# dns = "https://1.1.1.1"
# doh-ip = "1.1.1.1"
# Starting from mtg v2.1.12 we have changed a configuration for DNS. Now it
# supports DNS-over-HTTPS, DNS-over-TLS, custom UDP resolver and system
# resolver.
#
# Here is how to define DNS-over-HTTPS:
# - https://1.1.1.1
# - https://1.1.1.1/dns-query
# - https://cloudflare-dns.com/dns-query
# - https://cloudflare-dns.com
#
# Here is how to define DNS-over-TLS:
# - tls://1.1.1.1
# - tls://cloudflare-dns.com
#
# Here is how to define a custom UDP resolver (we support only IPs here)
# - 1.1.1.1
# - udp://1.1.1.1
#
# If you set it to empty string, default resolver will be used.
# But please comment out doh-ip
dns = "https://1.1.1.1"
# mtg can work via proxies (for now, we support only socks5). Proxy
# configuration is done via list. So, you can specify many proxies
@@ -100,25 +191,13 @@ doh-ip = "9.9.9.9"
#
# Proxy configuration is done via ordinary URI schema:
#
# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
# socks5://user:password@host:port
#
# Only socks5 proxy is used. user/password is optional. As you can
# see, you can specify some parameters in GET query. These parameters
# configure circuit breaker.
#
# open_threshold means a number of errors which should happen so we stop
# use a proxy.
#
# half_open_timeout means a time period (in Golang duration notation)
# after which we can retry with this proxy
#
# reset_failures_timeout means a time period when we flush out errors
# when circuit breaker in closed state.
#
# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
# on details about circuit breakers.
proxies = [
# "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
# "socks5://user:password@host:port"
]
# network timeouts define different settings for timeouts. tcp timeout
@@ -134,6 +213,66 @@ tcp = "5s"
http = "10s"
idle = "1m"
# mtg has to mimic real websites. It does not mean domain fronting, it also
# means that traffic characteristics should be similar to real world traffic.
# websites and applications behave differently, their traffic patterns are also
# different. Applications do bursts of RPC-style messages (or JSON communication,
# does not really matter), while websites pump heavy content in HTTP2 streams
#
# It means that statistically there is a different between traffic shape:
# delays between packets are also different.
# In order to avoid censorship detection based on these patterns, there is a
# mtg subsystem called "Doppelganger" that aims to mimic website statistics
# as close as it could.
#
# Delays between TLS packets are not constant. There are many factors
# that come in play. Application should generate some response, it could
# send some headers first and stream content with chunked encoding. So
# some first packets could come as soon as possible, with some delays
# after first ones. Such phenomenon is described by different statistic
# distribution. There are 2 distribution that describe it: lognormal
# distribution and Weibul distribution. Lognormal is all about steady streams
# of heavy content like a video. Weibul is great about short bursts like
# user who requested a static page an a couple of images.
[defense.doppelganger]
# This is a list of URLs that would be crawled by mtg to approximate delay
# statistics. They MUST be HTTPS urls.
#
# You can come to the website and collect different URLs, with light and
# heavy content. We recommend to search for CDNs.
urls = [
# "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
]
# A collection is done in raids. Each raid makes this number of requests to
# each URL in this list. Do not use a huge number, 10 is probably ok.
repeats-per-raid = 10
# This is a duration between each raid. It makes no sense to have a small number
# here as you would start to make a noticeable activity. Usually traffic patterns
# do not change a lot, so do not expect different results if you request
# each 10 minutes.
raid-each = "6h"
# This enables dynamic tls record sizing.
#
# Some modern stacks and platforms start to use the technique that is called
# DRS. They start with small TLS packets and ramp up eventually. First packets
# are usually about MTU size, after that we get 4k and eventually max size.
# This is done with a good intention: to minimize a time to the first byte,
# so application could start doing something with the data right after first
# RTT.
#
# Apparently, about 90% of application do not employ this technique, they use
# max size always: nginx, apache, java stuff. But Golang tools, angie and
# some specific patches activate this technique.
#
# In order to mimic a real website we need to know something about software
# it uses. Usually nobody cares: openssl does 16384, Python does it, nginx
# does it. So this setting is disabled by default.
#
# https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
# https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
# https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
drs = false
# Some countries do active probing on Telegram connections. This technique
# allows to protect from such effort.
#
@@ -170,7 +309,7 @@ download-concurrency = 2
# You can provider links here (starts with https:// or http://) or
# path to a local file, but in this case it should be absolute.
urls = [
# "https://iplists.firehol.org/files/firehol_level1.netset",
"https://iplists.firehol.org/files/firehol_level1.netset",
# "/local.file"
]
# How often do we need to update a blocklist set.
+38 -36
View File
@@ -1,55 +1,57 @@
module github.com/9seconds/mtg/v2
go 1.17
go 1.26
require (
github.com/OneOfOne/xxhash v1.2.8
github.com/alecthomas/kong v0.2.19
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
github.com/alecthomas/kong v1.14.0
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
github.com/d4l3k/messagediff v1.2.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gotd/td v0.34.0
github.com/jarcoal/httpmock v1.0.8
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e
github.com/mccutchen/go-httpbin v1.1.1
github.com/panjf2000/ants/v2 v2.4.7
github.com/pelletier/go-toml v1.9.4
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/zerolog v1.26.0
github.com/smira/go-statsd v1.3.2
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881
google.golang.org/protobuf v1.27.1 // indirect
github.com/panjf2000/ants/v2 v2.12.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rs/zerolog v1.35.0
github.com/smira/go-statsd v1.3.4
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/sys v0.42.0
google.golang.org/protobuf v1.36.11 // indirect
)
require github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b
require (
github.com/beevik/ntp v1.5.0
github.com/ncruces/go-dns v1.3.2
github.com/pelletier/go-toml/v2 v2.3.0
github.com/pires/go-proxyproto v0.11.0
github.com/things-go/go-socks5 v0.1.0
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
github.com/yl2chen/cidranger v1.0.2
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gotd/ige v0.1.5 // indirect
github.com/gotd/xor v0.1.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+116 -541
View File
@@ -1,580 +1,155 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/alecthomas/kong v0.2.19 h1:qBDfByO5XgWUXyNB4D6OOhGh5Z1eNOwWayDPQJFNWdc=
github.com/alecthomas/kong v0.2.19/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc=
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gotd/getdoc v0.6.2/go.mod h1:6Ul8cyaq+Aw0gVyuU+TKZPCmNjdlylzSIWVxjzI5t/U=
github.com/gotd/ige v0.1.5 h1:qITql4hZpqPM/DSeO5IVlxzXJxrs4ZZiKRwPif2K76U=
github.com/gotd/ige v0.1.5/go.mod h1:LbvqjUuGELVuHcKjfJZ2NPNzegICQU4eqrPAkXy0nTs=
github.com/gotd/neo v0.1.2 h1:hayFfKNSH5RP/L+KKKnsKMdDNCdhSfzVuo5TF4wvpa8=
github.com/gotd/neo v0.1.2/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.34.0 h1:BLIdpv2sxbCuWI2fyXCECRQuLJdnYk3VMZDRKNog/E8=
github.com/gotd/td v0.34.0/go.mod h1:4s/7cuEscdvZIecM/pVi5L3nTK+djSoVLAhgfk40OE4=
github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c=
github.com/gotd/xor v0.1.0/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
github.com/gotd/xor v0.1.1 h1:LSPEeuf7noTo4fi4PrEsAaWXOSwjsY2e+IINPiR+c7s=
github.com/gotd/xor v0.1.1/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e h1:1wAVuGu1c+lsdaOPQN+9xoP9+gaIMJV6H0ehGc+K5iA=
github.com/kentik/patricia v0.0.0-20210909164817-21603333b70e/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/panjf2000/ants/v2 v2.4.7 h1:MZnw2JRyTJxFwtaMtUJcwE618wKD04POWk2gwwP4E2M=
github.com/panjf2000/ants/v2 v2.4.7/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
github.com/ncruces/go-dns v1.3.2/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/quasilyte/go-ruleguard/dsl v0.3.2/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smira/go-statsd v1.3.2 h1:1EeuzxNZ/TD9apbTOFSM9nulqfcsQFmT4u1A2DREabI=
github.com/smira/go-statsd v1.3.2/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c=
github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b h1:6J/38A0Xmdnjacfie0Udams7OP/GdoExyTipKwuQWjY=
github.com/txthinking/socks5 v0.0.0-20211121111206-e03c1217a50b/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae h1:ArVM1jICfm7g4E4dBet+KHUFMLuxmj1Nxdp/tr3ByCU=
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae/go.mod h1:cldYm15/XHcGt7ndItnEWHwFZo7dinU+2QoyjfErhsI=
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e h1:xA7GVlbz6teIF4FdvuqwbX6C4tiqNk2PH7FRPIDerao=
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b h1:p+bJ3v5uUdEVMCoeFUs+BNJPsqt+Y6BLbDaPfTcbcH8=
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+20 -60
View File
@@ -1,22 +1,16 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/mtglib"
)
type accessResponse struct {
@@ -31,17 +25,17 @@ type accessResponse struct {
type accessResponseURLs struct {
IP net.IP `json:"ip"`
Port uint `json:"port"`
TgURL string `json:"tg_url"` // nolint: tagliatelle
TgQrCode string `json:"tg_qrcode"` // nolint: tagliatelle
TmeURL string `json:"tme_url"` // nolint: tagliatelle
TmeQrCode string `json:"tme_qrcode"` // nolint: tagliatelle
TgURL string `json:"tg_url"` //nolint: tagliatelle
TgQrCode string `json:"tg_qrcode"` //nolint: tagliatelle
TmeURL string `json:"tme_url"` //nolint: tagliatelle
TmeQrCode string `json:"tme_qrcode"` //nolint: tagliatelle
}
type Access struct {
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` // nolint: lll
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` // nolint: lll
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` //nolint: lll
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` //nolint: lll
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` //nolint: lll
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
}
@@ -61,14 +55,14 @@ func (a *Access) Run(cli *CLI, version string) error {
}
wg := &sync.WaitGroup{}
wg.Add(2) // nolint: gomnd
go func() {
defer wg.Done()
wg.Go(func() {
ip := a.PublicIPv4
if ip == nil {
ip = a.getIP(ntw, "tcp4")
ip = conf.PublicIPv4.Get(nil)
}
if ip == nil {
ip = getIP(ntw, "tcp4")
}
if ip != nil {
@@ -76,14 +70,14 @@ func (a *Access) Run(cli *CLI, version string) error {
}
resp.IPv4 = a.makeURLs(conf, ip)
}()
go func() {
defer wg.Done()
})
wg.Go(func() {
ip := a.PublicIPv6
if ip == nil {
ip = a.getIP(ntw, "tcp6")
ip = conf.PublicIPv6.Get(nil)
}
if ip == nil {
ip = getIP(ntw, "tcp6")
}
if ip != nil {
@@ -91,7 +85,7 @@ func (a *Access) Run(cli *CLI, version string) error {
}
resp.IPv6 = a.makeURLs(conf, ip)
}()
})
wg.Wait()
@@ -106,40 +100,6 @@ func (a *Access) Run(cli *CLI, version string) error {
return nil
}
func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
return ntw.DialContext(ctx, protocol, address) // nolint: wrapcheck
})
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
if err != nil {
panic(err)
}
req.Header.Add("Accept", "text/plain")
resp, err := client.Do(req)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusOK {
return nil
}
defer func() {
io.Copy(io.Discard, resp.Body) // nolint: errcheck
resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return net.ParseIP(strings.TrimSpace(string(data)))
}
func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs {
if ip == nil {
return nil
+1
View File
@@ -4,6 +4,7 @@ import "github.com/alecthomas/kong"
type CLI struct {
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
Doctor Doctor `kong:"cmd,help='Check that proxy can run correctly'"`
Access Access `kong:"cmd,help='Print access information.'"`
Run Run `kong:"cmd,help='Run proxy.'"`
SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"`
+375
View File
@@ -0,0 +1,375 @@
package cli
import (
"context"
"errors"
"fmt"
"maps"
"net"
"os"
"slices"
"strconv"
"strings"
"text/template"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network/v2"
"github.com/beevik/ntp"
)
var (
tplError = template.Must(
template.New("").Parse(" ‼️ {{ .description }}: {{ .error }}\n"),
)
tplWDeprecatedConfig = template.Must(
template.New("").
Parse(` ⚠️ Option {{ .old | printf "%q" }}{{ if .old_section }} from section [{{ .old_section }}]{{ end }} is deprecated and will be removed in v{{ .when }}. Please use {{ .new | printf "%q" }}{{ if .new_section }} in [{{ .new_section }}] section{{ end }} instead.` + "\n"),
)
tplOTimeSkewness = template.Must(
template.New("").
Parse(" ✅ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}\n"),
)
tplWTimeSkewness = template.Must(
template.New("").
Parse(" ⚠️ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. Please check ntp.\n"),
)
tplETimeSkewness = template.Must(
template.New("").
Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"),
)
tplODCConnect = template.Must(
template.New("").Parse(" ✅ DC {{ .dc }}\n"),
)
tplEDCConnect = template.Must(
template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"),
)
tplODNSSNIMatch = template.Must(
template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
)
tplEDNSSNIMatch = template.Must(
template.New("").Parse(" ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"),
)
tplOFrontingDomain = template.Must(
template.New("").Parse(" ✅ {{ .address }} is reachable\n"),
)
tplEFrontingDomain = template.Must(
template.New("").Parse(" ❌ {{ .address }}: {{ .error }}\n"),
)
)
type Doctor struct {
conf *config.Config
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
}
func (d *Doctor) Run(cli *CLI, version string) error {
conf, err := utils.ReadConfig(d.ConfigPath)
if err != nil {
return fmt.Errorf("cannot init config: %w", err)
}
d.conf = conf
fmt.Println("Deprecated options")
everythingOK := d.checkDeprecatedConfig()
fmt.Println("Time skewness")
everythingOK = d.checkTimeSkewness() && everythingOK
resolver, err := network.GetDNS(conf.GetDNS())
if err != nil {
return fmt.Errorf("cannot create DNS resolver: %w", err)
}
base := network.New(
resolver,
"",
conf.Network.Timeout.TCP.Get(10*time.Second),
conf.Network.Timeout.HTTP.Get(0),
conf.Network.Timeout.Idle.Get(0),
)
fmt.Println("Validate native network connectivity")
everythingOK = d.checkNetwork(base) && everythingOK
for _, url := range conf.Network.Proxies {
value, err := network.NewProxyNetwork(base, url.Get(nil))
if err != nil {
return err
}
fmt.Printf("Validate network connectivity with proxy %s\n", url.Get(nil))
everythingOK = d.checkNetwork(value) && everythingOK
}
fmt.Println("Validate fronting domain connectivity")
everythingOK = d.checkFrontingDomain(base) && everythingOK
fmt.Println("Validate SNI-DNS match")
everythingOK = d.checkSecretHost(resolver, base) && everythingOK
if !everythingOK {
os.Exit(1)
}
return nil
}
func (d *Doctor) checkDeprecatedConfig() bool {
ok := true
if d.conf.DomainFrontingIP.Value != nil {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-ip",
"old_section": "",
"new": "ip",
"new_section": "domain-fronting",
})
}
if d.conf.DomainFrontingPort.Value != 0 {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-port",
"old_section": "",
"new": "port",
"new_section": "domain-fronting",
})
}
if d.conf.DomainFrontingProxyProtocol.Value {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-proxy-protocol",
"old_section": "",
"new": "proxy-protocol",
"new_section": "domain-fronting",
})
}
if d.conf.Network.DOHIP.Value != nil {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "doh-ip",
"old_section": "network",
"new": "dns",
"new_section": "network",
})
}
if ok {
fmt.Println(" ✅ All good")
}
return ok
}
func (d *Doctor) checkTimeSkewness() bool {
response, err := ntp.Query("0.pool.ntp.org")
if err != nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": "cannot access ntp pool",
"error": err,
})
return false
}
skewness := response.ClockOffset.Abs()
confValue := d.conf.TolerateTimeSkewness.Get(mtglib.DefaultTolerateTimeSkewness)
diff := float64(skewness) / float64(confValue)
tplData := map[string]any{
"drift": response.ClockOffset,
"value": confValue,
}
switch {
case diff < 0.3:
tplOTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
return true
case diff < 0.7:
tplWTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
default:
tplETimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
}
return false
}
func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses))
slices.Sort(dcs)
ok := true
for _, dc := range dcs {
err := d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc])
if err == nil {
tplODCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"dc": dc,
})
} else {
tplEDCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"dc": dc,
"error": err,
})
ok = false
}
}
return ok
}
func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error {
checkAddresses := []string{}
switch d.conf.PreferIP.Get("prefer-ip4") {
case "only-ipv4":
for _, addr := range addresses {
host, _, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
checkAddresses = append(checkAddresses, addr)
}
}
case "only-ipv6":
for _, addr := range addresses {
host, _, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
checkAddresses = append(checkAddresses, addr)
}
}
default:
checkAddresses = addresses
}
if len(checkAddresses) == 0 {
return fmt.Errorf("no suitable addresses after IP version filtering")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var (
conn net.Conn
err error
)
for _, addr := range checkAddresses {
conn, err = ntw.DialContext(ctx, "tcp", addr)
if err != nil {
continue
}
conn.Close() //nolint: errcheck
return nil
}
return err
}
func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
host := d.conf.Secret.Host
if ip := d.conf.GetDomainFrontingIP(nil); ip != "" {
host = ip
}
port := d.conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort)
address := net.JoinHostPort(host, strconv.Itoa(int(port)))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dialer := ntw.NativeDialer()
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
tplEFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"address": address,
"error": err,
})
return false
}
conn.Close() //nolint: errcheck
tplOFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"address": address,
})
return true
}
func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
if err != nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
"error": err,
})
return false
}
ourIP4 := d.conf.PublicIPv4.Get(nil)
if ourIP4 == nil {
ourIP4 = getIP(ntw, "tcp4")
}
ourIP6 := d.conf.PublicIPv6.Get(nil)
if ourIP6 == nil {
ourIP6 = getIP(ntw, "tcp6")
}
if ourIP4 == nil && ourIP6 == nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": "cannot detect public IP address",
"error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
})
return false
}
strAddresses := []string{}
for _, value := range addresses {
if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"ip": value.IP,
"hostname": d.conf.Secret.Host,
})
return true
}
strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
}
tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"hostname": d.conf.Secret.Host,
"resolved": strings.Join(strAddresses, ", "),
"ip4": ourIP4,
"ip6": ourIP6,
})
return false
}
+2 -2
View File
@@ -15,9 +15,9 @@ func (g *GenerateSecret) Run(cli *CLI, _ string) error {
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
if g.Hex {
fmt.Println(secret.Hex()) // nolint: forbidigo
fmt.Println(secret.Hex()) //nolint: forbidigo
} else {
fmt.Println(secret.Base64()) // nolint: forbidigo
fmt.Println(secret.Base64()) //nolint: forbidigo
}
return nil
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
type Run struct {
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
}
func (r *Run) Run(cli *CLI, version string) error {
+134 -54
View File
@@ -1,21 +1,26 @@
package cli
import (
"context"
"fmt"
"net"
"net/url"
"os"
"time"
"github.com/9seconds/mtg/v2/antireplay"
"github.com/9seconds/mtg/v2/events"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/proxyprotocol"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/ipblocklist"
"github.com/9seconds/mtg/v2/ipblocklist/files"
"github.com/9seconds/mtg/v2/logger"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network"
"github.com/9seconds/mtg/v2/network/v2"
"github.com/9seconds/mtg/v2/stats"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog"
"github.com/yl2chen/cidranger"
)
func makeLogger(conf *config.Config) mtglib.Logger {
@@ -35,43 +40,41 @@ func makeLogger(conf *config.Config) mtglib.Logger {
}
func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
tcpTimeout := conf.Network.Timeout.TCP.Get(network.DefaultTimeout)
httpTimeout := conf.Network.Timeout.HTTP.Get(network.DefaultHTTPTimeout)
dohIP := conf.Network.DOHIP.Get(net.ParseIP(network.DefaultDOHHostname)).String()
userAgent := "mtg/" + version
baseDialer, err := network.NewDefaultDialer(tcpTimeout, 0)
resolver, err := network.GetDNS(conf.GetDNS())
if err != nil {
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
return nil, fmt.Errorf("cannot create DNS resolver: %w", err)
}
if len(conf.Network.Proxies) == 0 {
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
}
base := network.New(
resolver,
"",
conf.Network.Timeout.TCP.Get(0),
conf.Network.Timeout.HTTP.Get(0),
conf.Network.Timeout.Idle.Get(0),
)
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
for _, v := range conf.Network.Proxies {
if value := v.Get(nil); value != nil {
proxyURLs = append(proxyURLs, value)
}
}
if len(proxyURLs) == 1 {
socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
proxyDialers := make([]mtglib.Network, len(conf.Network.Proxies))
for idx, v := range conf.Network.Proxies {
value, err := network.NewProxyNetwork(base, v.Get(nil))
if err != nil {
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
return nil, fmt.Errorf("cannot use %v for proxy url: %w", v.Get(nil), err)
}
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
proxyDialers[idx] = value
}
socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
switch len(proxyDialers) {
case 0:
return base, nil
case 1:
return proxyDialers[0], nil
}
value, err := network.Join(proxyDialers...)
if err != nil {
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
panic(err)
}
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
return value, nil
}
func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
@@ -85,7 +88,11 @@ func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache {
)
}
func makeIPBlocklist(conf config.ListConfig, logger mtglib.Logger, ntw mtglib.Network) (mtglib.IPBlocklist, error) {
func makeIPBlocklist(conf config.ListConfig,
logger mtglib.Logger,
ntw mtglib.Network,
updateCallback ipblocklist.FireholUpdateCallback,
) (mtglib.IPBlocklist, error) {
if !conf.Enabled.Get(false) {
return ipblocklist.NewNoop(), nil
}
@@ -101,20 +108,63 @@ func makeIPBlocklist(conf config.ListConfig, logger mtglib.Logger, ntw mtglib.Ne
}
}
firehol, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
blocklist, err := ipblocklist.NewFirehol(logger.Named("ipblockist"),
ntw,
conf.DownloadConcurrency.Get(1),
remoteURLs,
localFiles)
localFiles,
updateCallback)
if err != nil {
return nil, fmt.Errorf("incorrect parameters for firehol: %w", err)
}
return firehol, nil
go blocklist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
return blocklist, nil
}
func makeIPAllowlist(conf config.ListConfig,
logger mtglib.Logger,
ntw mtglib.Network,
updateCallback ipblocklist.FireholUpdateCallback,
) (mtglib.IPBlocklist, error) {
var (
allowlist mtglib.IPBlocklist
err error
)
if !conf.Enabled.Get(false) {
allowlist, err = ipblocklist.NewFireholFromFiles(
logger.Named("ipblocklist"),
1,
[]files.File{
files.NewMem([]*net.IPNet{
cidranger.AllIPv4,
cidranger.AllIPv6,
}),
},
updateCallback,
)
go allowlist.Run(conf.UpdateEach.Get(ipblocklist.DefaultFireholUpdateEach))
} else {
allowlist, err = makeIPBlocklist(
conf,
logger,
ntw,
updateCallback,
)
}
if err != nil {
return nil, fmt.Errorf("cannot build allowlist: %w", err)
}
return allowlist, nil
}
func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) {
factories := make([]events.ObserverFactory, 0, 2) // nolint: gomnd
factories := make([]events.ObserverFactory, 0, 2)
if conf.Stats.StatsD.Enabled.Get(false) {
statsdFactory, err := stats.NewStatsd(
@@ -140,7 +190,7 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
return nil, fmt.Errorf("cannot start a listener for prometheus: %w", err)
}
go prometheus.Serve(listener) // nolint: errcheck
go prometheus.Serve(listener) //nolint: errcheck
factories = append(factories, prometheus.Make)
}
@@ -152,35 +202,47 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
return events.NewNoopStream(), nil
}
func runProxy(conf *config.Config, version string) error { // nolint: funlen
func runProxy(conf *config.Config, version string) error { //nolint: funlen
logger := makeLogger(conf)
logger.BindJSON("configuration", conf.String()).Debug("configuration")
eventStream, err := makeEventStream(conf, logger)
if err != nil {
return fmt.Errorf("cannot build event stream: %w", err)
}
ntw, err := makeNetwork(conf, version)
if err != nil {
return fmt.Errorf("cannot build network: %w", err)
}
blocklist, err := makeIPBlocklist(conf.Defense.Blocklist, logger, ntw)
blocklist, err := makeIPBlocklist(
conf.Defense.Blocklist,
logger.Named("blocklist"),
ntw,
func(ctx context.Context, size int) {
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
})
if err != nil {
return fmt.Errorf("cannot build ip blocklist: %w", err)
}
var whitelist mtglib.IPBlocklist
if conf.Defense.Allowlist.Enabled.Get(false) {
whlist, err := makeIPBlocklist(conf.Defense.Allowlist, logger, ntw)
if err != nil {
return fmt.Errorf("cannot build ip blocklist: %w", err)
}
whitelist = whlist
allowlist, err := makeIPAllowlist(
conf.Defense.Allowlist,
logger.Named("allowlist"),
ntw,
func(ctx context.Context, size int) {
eventStream.Send(ctx, mtglib.NewEventIPListSize(size, false))
},
)
if err != nil {
return fmt.Errorf("cannot build ip allowlist: %w", err)
}
eventStream, err := makeEventStream(conf, logger)
if err != nil {
return fmt.Errorf("cannot build event stream: %w", err)
doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
for i, v := range conf.Defense.Doppelganger.URLs {
doppelGangerURLs[i] = v.String()
}
opts := mtglib.ProxyOpts{
@@ -188,15 +250,25 @@ func runProxy(conf *config.Config, version string) error { // nolint: funlen
Network: ntw,
AntiReplayCache: makeAntiReplayCache(conf),
IPBlocklist: blocklist,
IPWhitelist: whitelist,
IPAllowlist: allowlist,
EventStream: eventStream,
Secret: conf.Secret,
DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort),
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
Secret: conf.Secret,
Concurrency: conf.GetConcurrency(mtglib.DefaultConcurrency),
DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort),
DomainFrontingIP: conf.GetDomainFrontingIP(nil),
DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false),
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
AutoUpdate: conf.AutoUpdate.Get(false),
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
IdleTimeout: conf.Network.Timeout.Idle.Get(time.Minute),
DoppelGangerURLs: doppelGangerURLs,
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
DoppelGangerEach: conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
DoppelGangerDRS: conf.Defense.Doppelganger.DRS.Get(false),
}
proxy, err := mtglib.NewProxy(opts)
@@ -209,12 +281,20 @@ func runProxy(conf *config.Config, version string) error { // nolint: funlen
return fmt.Errorf("cannot start proxy: %w", err)
}
if conf.ProxyProtocolListener.Get(false) {
listener = &proxyprotocol.ListenerAdapter{
Listener: proxyproto.Listener{
Listener: listener,
},
}
}
ctx := utils.RootContext()
go proxy.Serve(listener) // nolint: errcheck
go proxy.Serve(listener) //nolint: errcheck
<-ctx.Done()
listener.Close()
listener.Close() //nolint: errcheck
proxy.Shutdown()
return nil
+29 -11
View File
@@ -13,17 +13,19 @@ type SimpleRun struct {
BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` // nolint: lll
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` // nolint: lll
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` // nolint: lll
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` // nolint: lll
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` // nolint: lll
DOHIP net.IP `kong:"name='doh-ip',short='n',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` // nolint: lll
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` // nolint: lll
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` // nolint: lll
Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` //nolint: lll
Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` //nolint: lll
TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll
PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll
DomainFrontingIP string `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"` //nolint: lll
DOHIP net.IP `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"` //nolint: lll
Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll
Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll
AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` //nolint: lll
}
func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funlen
conf := &config.Config{}
if err := conf.BindTo.Set(s.BindTo); err != nil {
@@ -34,7 +36,7 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
return fmt.Errorf("incorrect secret: %w", err)
}
if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil { // nolint: gomnd
if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil {
return fmt.Errorf("incorrect concurrency: %w", err)
}
@@ -42,10 +44,16 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
return fmt.Errorf("incorrect prefer-ip: %w", err)
}
if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil { // nolint: gomnd
if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil {
return fmt.Errorf("incorrect domain-fronting-port: %w", err)
}
if s.DomainFrontingIP != "" {
if err := conf.DomainFrontingIP.Set(s.DomainFrontingIP); err != nil {
return fmt.Errorf("incorrect domain-fronting-ip: %w", err)
}
}
if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil {
return fmt.Errorf("incorrect doh-ip: %w", err)
}
@@ -66,6 +74,16 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop
return fmt.Errorf("incorrect antireplay-cache-size: %w", err)
}
for _, v := range s.Socks5Proxies {
proxyURL := config.TypeProxyURL{}
if err := proxyURL.Set(v); err != nil {
return fmt.Errorf("incorrect socks5 proxy URL: %w", err)
}
conf.Network.Proxies = append(conf.Network.Proxies, proxyURL)
}
conf.Debug.Value = s.Debug
conf.AllowFallbackOnUnknownDC.Value = true
conf.Defense.AntiReplay.Enabled.Value = true
+51
View File
@@ -0,0 +1,51 @@
package cli
import (
"context"
"io"
"net"
"net/http"
"strings"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib"
)
func getIP(ntw mtglib.Network, protocol string) net.IP {
dialer := ntw.NativeDialer()
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
conn, err := dialer.DialContext(ctx, protocol, address)
if err != nil {
return nil, err
}
return essentials.WrapNetConn(conn), err
})
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
if err != nil {
panic(err)
}
req.Header.Add("Accept", "text/plain")
resp, err := client.Do(req)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusOK {
return nil
}
defer func() {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return net.ParseIP(strings.TrimSpace(string(data)))
}
+69 -11
View File
@@ -4,6 +4,8 @@ import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/url"
"github.com/9seconds/mtg/v2/mtglib"
)
@@ -21,23 +23,40 @@ type ListConfig struct {
}
type Config struct {
Debug TypeBool `json:"debug"`
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
Secret mtglib.Secret `json:"secret"`
BindTo TypeHostPort `json:"bindTo"`
PreferIP TypePreferIP `json:"preferIp"`
DomainFrontingPort TypePort `json:"domainFrontingPort"`
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
Concurrency TypeConcurrency `json:"concurrency"`
Defense struct {
Debug TypeBool `json:"debug"`
AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"`
Secret mtglib.Secret `json:"secret"`
BindTo TypeHostPort `json:"bindTo"`
ProxyProtocolListener TypeBool `json:"proxyProtocolListener"`
PreferIP TypePreferIP `json:"preferIp"`
AutoUpdate TypeBool `json:"autoUpdate"`
DomainFrontingPort TypePort `json:"domainFrontingPort"`
DomainFrontingIP TypeIP `json:"domainFrontingIp"`
DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"`
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
Concurrency TypeConcurrency `json:"concurrency"`
PublicIPv4 TypeIP `json:"publicIpv4"`
PublicIPv6 TypeIP `json:"publicIpv6"`
DomainFronting struct {
IP TypeIP `json:"ip"`
Port TypePort `json:"port"`
ProxyProtocol TypeBool `json:"proxyProtocol"`
} `json:"domainFronting"`
Defense struct {
AntiReplay struct {
Optional
MaxSize TypeBytes `json:"maxSize"`
ErrorRate TypeErrorRate `json:"errorRate"`
} `json:"antiReplay"`
Blocklist ListConfig `json:"blocklist"`
Allowlist ListConfig `json:"allowlist"`
Blocklist ListConfig `json:"blocklist"`
Allowlist ListConfig `json:"allowlist"`
Doppelganger struct {
URLs []TypeHttpsURL `json:"urls"`
Repeats TypeConcurrency `json:"repeats_per_raid"`
UpdateEach TypeDuration `json:"raid_each"`
DRS TypeBool `json:"drs"`
} `json:"doppelganger"`
} `json:"defense"`
Network struct {
Timeout struct {
@@ -46,6 +65,7 @@ type Config struct {
Idle TypeDuration `json:"idle"`
} `json:"timeout"`
DOHIP TypeIP `json:"dohIp"`
DNS TypeDNSURI `json:"dns"`
Proxies []TypeProxyURL `json:"proxies"`
} `json:"network"`
Stats struct {
@@ -66,6 +86,44 @@ type Config struct {
} `json:"stats"`
}
func (c *Config) GetConcurrency(defaultValue uint) uint {
if concurrency := c.Concurrency.Get(0); concurrency != 0 {
return concurrency
}
return c.Concurrency.Get(defaultValue)
}
func (c *Config) GetDNS() *url.URL {
var dohURL *url.URL
if dohIP := c.Network.DOHIP.Get(nil); dohIP != nil {
dohURL, _ = url.Parse("https://" + dohIP.String())
}
return c.Network.DNS.Get(dohURL)
}
func (c *Config) GetDomainFrontingPort(defaultValue uint) uint {
if port := c.DomainFronting.Port.Get(0); port != 0 {
return port
}
return c.DomainFrontingPort.Get(defaultValue)
}
func (c *Config) GetDomainFrontingIP(defaultValue net.IP) string {
if ip := c.DomainFronting.IP.Get(nil); ip != nil {
return ip.String()
}
if ip := c.DomainFrontingIP.Get(defaultValue); ip != nil {
return ip.String()
}
return ""
}
func (c *Config) GetDomainFrontingProxyProtocol(defaultValue bool) bool {
return c.DomainFronting.ProxyProtocol.Get(false) || c.DomainFrontingProxyProtocol.Get(defaultValue)
}
func (c *Config) Validate() error {
if !c.Secret.Valid() {
return fmt.Errorf("invalid secret %s", c.Secret.String())
+26
View File
@@ -42,6 +42,32 @@ func (suite *ConfigTestSuite) TestParseMinimalConfig() {
suite.Equal("0.0.0.0:3128", conf.BindTo.String())
}
func (suite *ConfigTestSuite) TestParsePublicIP() {
conf, err := config.Parse(suite.ReadConfig("public_ip.toml"))
suite.NoError(err)
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
suite.Equal("2001:db8::1", conf.PublicIPv6.Get(nil).String())
}
func (suite *ConfigTestSuite) TestParsePublicIPv4Only() {
conf, err := config.Parse(suite.ReadConfig("public_ip_v4_only.toml"))
suite.NoError(err)
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
suite.Nil(conf.PublicIPv6.Get(nil))
}
func (suite *ConfigTestSuite) TestParsePublicIPInvalid() {
_, err := config.Parse(suite.ReadConfig("public_ip_invalid.toml"))
suite.Error(err)
}
func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
suite.Nil(conf.PublicIPv4.Get(nil))
suite.Nil(conf.PublicIPv6.Get(nil))
}
func (suite *ConfigTestSuite) TestString() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
+28 -10
View File
@@ -5,19 +5,30 @@ import (
"encoding/json"
"fmt"
"github.com/pelletier/go-toml"
"github.com/pelletier/go-toml/v2"
)
type tomlConfig struct {
Debug bool `toml:"debug" json:"debug,omitempty"`
AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"`
Secret string `toml:"secret" json:"secret"`
BindTo string `toml:"bind-to" json:"bindTo"`
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
Defense struct {
Debug bool `toml:"debug" json:"debug,omitempty"`
AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"`
Secret string `toml:"secret" json:"secret"`
BindTo string `toml:"bind-to" json:"bindTo"`
ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"`
PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"`
AutoUpdate bool `toml:"auto-update" json:"autoUpdate,omitempty"`
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`
DomainFrontingIP string `toml:"domain-fronting-ip" json:"domainFrontingIp,omitempty"`
DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"`
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
PublicIPv4 string `toml:"public-ipv4" json:"publicIpv4,omitempty"`
PublicIPv6 string `toml:"public-ipv6" json:"publicIpv6,omitempty"`
DomainFronting struct {
IP string `toml:"ip" json:"ip,omitempty"`
Port uint `toml:"port" json:"port,omitempty"`
ProxyProtocol bool `toml:"proxy-protocol" json:"proxyProtocol,omitempty"`
} `toml:"domain-fronting" json:"domainFronting,omitempty"`
Defense struct {
AntiReplay struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
MaxSize string `toml:"max-size" json:"maxSize,omitempty"`
@@ -35,6 +46,12 @@ type tomlConfig struct {
URLs []string `toml:"urls" json:"urls,omitempty"`
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
} `toml:"allowlist" json:"allowlist,omitempty"`
Doppelganger struct {
URLs []string `toml:"urls" json:"urls,omitempty"`
Repeats uint `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
UpdateEach string `toml:"raid-each" json:"raid_each,omitempty"`
DRS bool `toml:"drs" json:"drs,omitempty"`
} `toml:"doppelganger" json:"doppelganger,omitempty"`
} `toml:"defense" json:"defense,omitempty"`
Network struct {
Timeout struct {
@@ -43,6 +60,7 @@ type tomlConfig struct {
Idle string `toml:"idle" json:"idle,omitempty"`
} `toml:"timeout" json:"timeout,omitempty"`
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
DNS string `toml:"dns" json:"dns,omitempty"`
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
} `toml:"network" json:"network,omitempty"`
Stats struct {
+4
View File
@@ -0,0 +1,4 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "203.0.113.1"
public-ipv6 = "2001:db8::1"
+3
View File
@@ -0,0 +1,3 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "not-an-ip"
+3
View File
@@ -0,0 +1,3 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "203.0.113.1"
+2 -2
View File
@@ -20,7 +20,7 @@ type TypeBoolTestSuite struct {
}
func (suite *TypeBoolTestSuite) TestUnmarshalFail() {
testData := []interface{}{
testData := []any{
"",
"np",
"нет",
@@ -29,7 +29,7 @@ func (suite *TypeBoolTestSuite) TestUnmarshalFail() {
}
for _, v := range testData {
data, err := json.Marshal(map[string]interface{}{
data, err := json.Marshal(map[string]any{
"value": v,
})
suite.NoError(err)
+2 -2
View File
@@ -10,7 +10,7 @@ type TypeConcurrency struct {
}
func (t *TypeConcurrency) Set(value string) error {
concurrencyValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
concurrencyValue, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return fmt.Errorf("value is not uint (%s): %w", value, err)
}
@@ -41,5 +41,5 @@ func (t TypeConcurrency) MarshalJSON() ([]byte, error) {
}
func (t TypeConcurrency) String() string {
return strconv.FormatUint(uint64(t.Value), 10) // nolint: gomnd
return strconv.FormatUint(uint64(t.Value), 10)
}
+41
View File
@@ -0,0 +1,41 @@
package config
import (
"fmt"
"strconv"
)
type TypeDC struct {
Value int
}
func (t *TypeDC) Set(value string) error {
parsed, err := strconv.ParseInt(value, 10, 16)
if err != nil {
return fmt.Errorf("cannot parse dc: %w", err)
}
if parsed < 0 {
parsed = -parsed
}
t.Value = int(parsed)
return nil
}
func (t *TypeDC) UnmarshalJSON(data []byte) error {
return t.Set(string(data))
}
func (t TypeDC) MarshalJSON() ([]byte, error) {
return []byte(t.String()), nil
}
func (t TypeDC) String() string {
return strconv.Itoa(t.Value)
}
func (t TypeDC) Get() int {
return t.Value
}
+96
View File
@@ -0,0 +1,96 @@
package config_test
import (
"encoding/json"
"strconv"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeDCTestStruct struct {
Value config.TypeDC `json:"value"`
}
type TypeDCTestSuite struct {
suite.Suite
}
func (suite *TypeDCTestSuite) TestUnmarshalFail() {
testData := []string{
"-1s",
"1202002020202",
"xxx",
"-11111111111111",
"",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeDCTestStruct{}))
})
}
}
func (suite *TypeDCTestSuite) TestUnmarshalOk() {
testData := map[int]int{
1: 1,
-1: 1,
203: 203,
}
for value, expected := range testData {
data, err := json.Marshal(map[string]int{
"value": value,
})
suite.NoError(err)
suite.T().Run(strconv.Itoa(value), func(t *testing.T) {
testStruct := &typeDCTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, expected, testStruct.Value.Value)
assert.Equal(t, expected, testStruct.Value.Get())
})
}
}
func (suite *TypeDCTestSuite) TestMarshalOk() {
testData := map[string]int{
"1": 1,
"203": 203,
}
for k, v := range testData {
value := k
expected := v
suite.T().Run(value, func(t *testing.T) {
testStruct := &typeDCTestStruct{}
assert.NoError(t, testStruct.Value.Set(value))
data, err := json.Marshal(testStruct)
assert.NoError(t, err)
expectedJSON, err := json.Marshal(map[string]int{
"value": expected,
})
assert.NoError(t, err)
assert.JSONEq(t, string(expectedJSON), string(data))
})
}
}
func TestTypeDC(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeDCTestSuite{})
}
+69
View File
@@ -0,0 +1,69 @@
package config
import (
"fmt"
"net"
"net/url"
)
type TypeDNSURI struct {
Value *url.URL
}
func (t *TypeDNSURI) Set(value string) error {
parsed, err := url.Parse(value)
if err != nil {
return fmt.Errorf("value is not URI: %w", err)
}
if parsed.Host == "" {
parsed.Host = parsed.Path
parsed.Path = ""
parsed.Scheme = "udp"
}
switch parsed.Scheme {
case "https", "tls":
case "udp":
if ip := net.ParseIP(parsed.Hostname()); ip == nil {
return fmt.Errorf("simple DNS must IP address: %s", parsed.Hostname())
}
default:
return fmt.Errorf("unsupported DNS type %s", parsed.Scheme)
}
if parsed.Scheme != "https" && parsed.Path != "" {
return fmt.Errorf("path is supported only for DoH: %s", parsed)
}
if parsed.User != nil {
return fmt.Errorf("used info is not supported: %s", parsed.User.String())
}
t.Value = parsed
return nil
}
func (t *TypeDNSURI) Get(defaultValue *url.URL) *url.URL {
if t.Value != nil {
return t.Value
}
return defaultValue
}
func (t *TypeDNSURI) UnmarshalText(data []byte) error {
return t.Set(string(data))
}
func (t TypeDNSURI) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
func (t TypeDNSURI) String() string {
if t.Value == nil {
return ""
}
return t.Value.String()
}
+117
View File
@@ -0,0 +1,117 @@
package config_test
import (
"encoding/json"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeDNSURITestStruct struct {
Value config.TypeDNSURI `json:"value"`
}
type TypeDNSURITestSuite struct {
suite.Suite
}
func (suite *TypeDNSURITestSuite) TestUnmarshalFail() {
testData := []string{
"xx",
"ppar",
"",
"dns://hahaha",
"udp://xcxxcv",
"udp://1.1.1.1/xcv",
"1.1.1.1/xxx",
"tls://dns/xx",
"tls://1.1.1.1/xx",
"https://user:password@1.1.1.1",
"tls://user:password@1.1.1.1",
"udp://user:password@1.1.1.1",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeDNSURITestStruct{}))
})
}
}
func (suite *TypeDNSURITestSuite) TestUnmarshalOk() {
testData := []string{
"1.1.1.1",
"tls://1.1.1.1",
"tls://dns.google",
"https://1.1.1.1",
"https://1.1.1.1/dns-query",
"https://dns.google",
"https://dns.google/dns-query",
"udp://1.1.1.1",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeDNSURITestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
if v == "1.1.1.1" {
v = "udp://" + v
}
assert.Equal(t, v, testStruct.Value.String())
})
}
}
func (suite *TypeDNSURITestSuite) TestMarshalOk() {
testData := []string{
"tls://1.1.1.1",
"tls://dns.google",
"https://1.1.1.1",
"https://1.1.1.1/dns-query",
}
for _, v := range testData {
suite.T().Run(v, func(t *testing.T) {
testStruct := &typePreferIPTestStruct{
Value: config.TypePreferIP{
Value: v,
},
}
encodedJSON, err := json.Marshal(testStruct)
assert.NoError(t, err)
expectedJSON, err := json.Marshal(map[string]string{
"value": v,
})
assert.NoError(t, err)
assert.JSONEq(t, string(expectedJSON), string(encodedJSON))
})
}
}
func (suite *TypeDNSURITestSuite) TestGet() {
value := config.TypeDNSURI{}
suite.Nil(value.Get(nil))
suite.NoError(value.Set("tls://1.1.1.1"))
suite.NotNil(value.Get(nil))
}
func TestDNSURI(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeDNSURITestSuite{})
}
+2 -2
View File
@@ -12,7 +12,7 @@ type TypeErrorRate struct {
}
func (t *TypeErrorRate) Set(value string) error {
parsedValue, err := strconv.ParseFloat(value, 64) // nolint: gomnd
parsedValue, err := strconv.ParseFloat(value, 64)
if err != nil {
return fmt.Errorf("value is not a float (%s): %w", value, err)
}
@@ -43,5 +43,5 @@ func (t TypeErrorRate) MarshalJSON() ([]byte, error) {
}
func (t TypeErrorRate) String() string {
return strconv.FormatFloat(t.Value, 'f', -1, 64) // nolint: gomnd
return strconv.FormatFloat(t.Value, 'f', -1, 64)
}
+1 -1
View File
@@ -18,7 +18,7 @@ func (t *TypeHostPort) Set(value string) error {
return fmt.Errorf("incorrect host:port value (%v): %w", value, err)
}
portValue, err := strconv.ParseUint(port, 10, 16) // nolint: gomnd
portValue, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return fmt.Errorf("incorrect port number (%v): %w", value, err)
}
+53
View File
@@ -0,0 +1,53 @@
package config
import (
"fmt"
"net/url"
)
type TypeHttpsURL struct {
Value *url.URL
}
func (t *TypeHttpsURL) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return fmt.Errorf("value is not correct URL (%s): %w", value, err)
}
if parsedURL.Host == "" {
return fmt.Errorf("url has to have a schema: %s", value)
}
if parsedURL.Scheme != "https" {
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
}
t.Value = parsedURL
return nil
}
func (t *TypeHttpsURL) Get(defaultValue *url.URL) *url.URL {
if t.Value == nil {
return defaultValue
}
return t.Value
}
func (t *TypeHttpsURL) UnmarshalText(data []byte) error {
return t.Set(string(data))
}
func (t TypeHttpsURL) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
func (t TypeHttpsURL) String() string {
if t.Value == nil {
return ""
}
return t.Value.String()
}
+100
View File
@@ -0,0 +1,100 @@
package config_test
import (
"encoding/json"
"net/url"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeHttpsURLTestStruct struct {
Value config.TypeHttpsURL `json:"value"`
}
type HttpsURLTestSuite struct {
suite.Suite
}
func (suite *HttpsURLTestSuite) TestUnmarshalFail() {
testData := []string{
"",
"https://",
"://lala",
"/path",
"http://example.com",
"socks5://example.com",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeHttpsURLTestStruct{}))
})
}
}
func (suite *HttpsURLTestSuite) TestUnmarshalOk() {
testData := map[string]string{
"https://example.com": "https://example.com",
"https://example.com:8443": "https://example.com:8443",
"https://example.com/path?q=1": "https://example.com/path?q=1",
"https://user:pass@example.com": "https://user:pass@example.com",
}
for k, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": k,
})
suite.NoError(err)
suite.T().Run(k, func(t *testing.T) {
testStruct := &typeHttpsURLTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
parsed, _ := url.Parse(value)
assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme)
assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host)
assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery)
assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path)
})
}
}
func (suite *HttpsURLTestSuite) TestMarshalOk() {
parsed, _ := url.Parse("https://example.com/path?q=1")
testStruct := &typeHttpsURLTestStruct{
Value: config.TypeHttpsURL{
Value: parsed,
},
}
encodedJSON, err := json.Marshal(testStruct)
suite.NoError(err)
suite.JSONEq(`{"value": "https://example.com/path?q=1"}`,
string(encodedJSON))
}
func (suite *HttpsURLTestSuite) TestGet() {
emptyURL := &url.URL{}
value := config.TypeHttpsURL{}
suite.Equal(emptyURL, value.Get(emptyURL))
value.Value = &url.URL{}
suite.Equal(value.Value, value.Get(emptyURL))
}
func TestTypeHttpsURL(t *testing.T) {
t.Parallel()
suite.Run(t, &HttpsURLTestSuite{})
}
+1 -1
View File
@@ -10,7 +10,7 @@ type TypePort struct {
}
func (t *TypePort) Set(value string) error {
portValue, err := strconv.ParseUint(value, 10, 16) // nolint: gomnd
portValue, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return fmt.Errorf("incorrect port number (%v): %w", value, err)
}
+8 -4
View File
@@ -15,20 +15,24 @@ type TypeProxyURL struct {
func (t *TypeProxyURL) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return fmt.Errorf("value is not corect URL (%s): %w", value, err)
return fmt.Errorf("value is not correct URL (%s): %w", value, err)
}
if parsedURL.Host == "" {
return fmt.Errorf("url has to have a schema: %s", value)
}
if parsedURL.Scheme != "socks5" {
switch parsedURL.Scheme {
case "socks5", "socks5h":
default:
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
}
if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil {
parsedURL.Host = net.JoinHostPort(parsedURL.Host,
typeProxyURLDefaultSOCKS5Port)
parsedURL.Host = net.JoinHostPort(
parsedURL.Host,
typeProxyURLDefaultSOCKS5Port,
)
}
t.Value = parsedURL
+20
View File
@@ -0,0 +1,20 @@
package proxyprotocol
import (
"net"
"github.com/pires/go-proxyproto"
)
type ListenerAdapter struct {
proxyproto.Listener
}
func (l *ListenerAdapter) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return connWrapper{conn.(*proxyproto.Conn)}, nil
}
+25
View File
@@ -0,0 +1,25 @@
package proxyprotocol
import "github.com/pires/go-proxyproto"
type connWrapper struct {
*proxyproto.Conn
}
func (c connWrapper) CloseRead() error {
tcpConn, ok := c.TCPConn()
if !ok {
panic("we support only tcp connections")
}
return tcpConn.CloseRead()
}
func (c connWrapper) CloseWrite() error {
tcpConn, ok := c.TCPConn()
if !ok {
panic("we support only tcp connections")
}
return tcpConn.CloseWrite()
}
+2 -2
View File
@@ -27,7 +27,7 @@ func captureOutput(filefp **os.File, callback func()) string {
closeChan := make(chan bool)
go func() {
io.Copy(buf, reader) // nolint: errcheck
io.Copy(buf, reader) //nolint: errcheck
close(closeChan)
}()
@@ -35,7 +35,7 @@ func captureOutput(filefp **os.File, callback func()) string {
callback()
writer.Close()
writer.Close() //nolint: errcheck
<-closeChan
return strings.TrimSpace(buf.String())
+10 -4
View File
@@ -2,6 +2,7 @@ package testlib
import (
"context"
"net"
"net/http"
"github.com/9seconds/mtg/v2/essentials"
@@ -15,16 +16,21 @@ type MtglibNetworkMock struct {
func (m *MtglibNetworkMock) Dial(network, address string) (essentials.Conn, error) {
args := m.Called(network, address)
return args.Get(0).(essentials.Conn), args.Error(1) // nolint: wrapcheck
return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert
}
func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
args := m.Called(ctx, network, address)
return args.Get(0).(essentials.Conn), args.Error(1) // nolint: wrapcheck
return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert
}
func (m *MtglibNetworkMock) NativeDialer() *net.Dialer {
return m.Called().Get(0).(*net.Dialer)
}
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
network, address string) (essentials.Conn, error)) *http.Client {
return m.Called(dialFunc).Get(0).(*http.Client)
network, address string) (essentials.Conn, error),
) *http.Client {
return m.Called(dialFunc).Get(0).(*http.Client) //nolint: forcetypeassert
}
+8 -8
View File
@@ -24,33 +24,33 @@ func (n *EssentialsConnMock) Write(b []byte) (int, error) {
}
func (n *EssentialsConnMock) Close() error {
return n.Called().Error(0) // nolint: wrapcheck
return n.Called().Error(0) //nolint: wrapcheck
}
func (n *EssentialsConnMock) CloseRead() error {
return n.Called().Error(0) // nolint: wrapcheck
return n.Called().Error(0) //nolint: wrapcheck
}
func (n *EssentialsConnMock) CloseWrite() error {
return n.Called().Error(0) // nolint: wrapcheck
return n.Called().Error(0) //nolint: wrapcheck
}
func (n *EssentialsConnMock) LocalAddr() net.Addr {
return n.Called().Get(0).(net.Addr)
return n.Called().Get(0).(net.Addr) //nolint: forcetypeassert
}
func (n *EssentialsConnMock) RemoteAddr() net.Addr {
return n.Called().Get(0).(net.Addr)
return n.Called().Get(0).(net.Addr) //nolint: forcetypeassert
}
func (n *EssentialsConnMock) SetDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
return n.Called(t).Error(0) //nolint: wrapcheck
}
func (n *EssentialsConnMock) SetReadDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
return n.Called(t).Error(0) //nolint: wrapcheck
}
func (n *EssentialsConnMock) SetWriteDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
return n.Called(t).Error(0) //nolint: wrapcheck
}
+2 -2
View File
@@ -14,11 +14,11 @@ type Listener struct {
func (l Listener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err // nolint: wrapcheck
return nil, err //nolint: wrapcheck
}
if err := network.SetClientSocketOptions(conn, 0); err != nil {
conn.Close()
conn.Close() //nolint: errcheck
return nil, fmt.Errorf("cannot set TCP options: %w", err)
}
-25
View File
@@ -1,25 +0,0 @@
//go:build !windows
// +build !windows
package utils
import (
"fmt"
"golang.org/x/sys/unix"
)
func SetLimits() error {
rLimit := unix.Rlimit{}
if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
return fmt.Errorf("cannot get rlimit: %w", err)
}
rLimit.Cur = rLimit.Max
if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil {
return fmt.Errorf("cannot set rlimit: %w", err)
}
return nil
}
-8
View File
@@ -1,8 +0,0 @@
//go:build windows
// +build windows
package utils
func SetLimits() error {
return nil
}
+8
View File
@@ -0,0 +1,8 @@
// files defines a set of abstraction for 'files': an openable entities that
// could be read after.
//
// This is not a file on a filesystem of your local machine, it also can
// include "in memory" files or even remote ones, like HTTP endpoints. If you
// make a GET request to HTTP endpoint, then a body is readable and you can
// consider it as an openable file.
package files
+4 -2
View File
@@ -22,8 +22,8 @@ func (h httpFile) Open(ctx context.Context) (io.ReadCloser, error) {
response, err := h.http.Do(request)
if err != nil {
if response != nil {
io.Copy(io.Discard, response.Body) // nolint: errcheck
response.Body.Close()
io.Copy(io.Discard, response.Body) //nolint: errcheck
response.Body.Close() //nolint: errcheck
}
return nil, fmt.Errorf("cannot get url %s: %w", h.url, err)
@@ -40,6 +40,8 @@ func (h httpFile) String() string {
return h.url
}
// NewHTTP returns a file abstraction for HTTP/HTTPS endpoint. You also need to
// provide a valid instance of [http.Client] to access it.
func NewHTTP(client *http.Client, endpoint string) (File, error) {
if client == nil {
return nil, ErrBadHTTPClient
+2 -2
View File
@@ -22,7 +22,7 @@ type HTTPTestSuite struct {
}
func (suite *HTTPTestSuite) makeFile(path string) (files.File, error) {
return files.NewHTTP(suite.httpClient, suite.httpServer.URL+"/"+path) // nolint: wrapcheck
return files.NewHTTP(suite.httpClient, suite.httpServer.URL+"/"+path) //nolint: wrapcheck
}
func (suite *HTTPTestSuite) SetupSuite() {
@@ -77,7 +77,7 @@ func (suite *HTTPTestSuite) TestOk() {
readCloser, err := file.Open(suite.ctx)
suite.NoError(err)
defer readCloser.Close()
defer readCloser.Close() //nolint: errcheck
data, err := io.ReadAll(readCloser)
suite.NoError(err)
+7
View File
@@ -6,9 +6,16 @@ import (
"io"
)
// ErrBadHTTPClient is returned if given HTTP client is initialized
// incorrectly.
var ErrBadHTTPClient = errors.New("incorrect http client")
// File is an abstraction for a entity that can be opened in some context.
type File interface {
// Open returns an readable entity for a file. It is important to not forget
// to close it after the usage.
Open(context.Context) (io.ReadCloser, error)
// String returns a short text description for the file
String() string
}
+2 -1
View File
@@ -12,13 +12,14 @@ type localFile struct {
}
func (l localFile) Open(ctx context.Context) (io.ReadCloser, error) {
return os.Open(l.path) // nolint: wrapcheck
return os.Open(l.path) //nolint: wrapcheck
}
func (l localFile) String() string {
return l.path
}
// NewLocal returns an openable File for a path on a local file system.
func NewLocal(path string) (File, error) {
if stat, err := os.Stat(path); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 {
return nil, fmt.Errorf("%s is not a readable file", path)
+38
View File
@@ -0,0 +1,38 @@
package files
import (
"context"
"io"
"net"
"strings"
)
type memFile struct {
data string
}
func (m memFile) Open(ctx context.Context) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(m.data)), nil
}
func (m memFile) String() string {
return "mem"
}
// NewMem returns an openable file that is kept in RAM.
func NewMem(networks []*net.IPNet) File {
builder := strings.Builder{}
if len(networks) > 0 {
builder.WriteString(networks[0].String())
}
for i := 1; i < len(networks); i++ {
builder.WriteString("\n")
builder.WriteString(networks[i].String())
}
return memFile{
data: builder.String(),
}
}
+42
View File
@@ -0,0 +1,42 @@
package files_test
import (
"context"
"io"
"net"
"strings"
"testing"
"github.com/9seconds/mtg/v2/ipblocklist/files"
"github.com/stretchr/testify/suite"
)
type MemTestSuite struct {
suite.Suite
}
func (suite *MemTestSuite) TestOk() {
_, network1, _ := net.ParseCIDR("192.168.0.1/24")
_, network2, _ := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/36")
file := files.NewMem([]*net.IPNet{
network1,
network2,
})
reader, err := file.Open(context.Background())
suite.NoError(err)
data, err := io.ReadAll(reader)
suite.NoError(err)
strData := strings.TrimSpace(string(data))
suite.Contains(strData, "192.168.0.0/24")
suite.Contains(strData, "2001:db8:8000::/36")
}
func TestMem(t *testing.T) {
t.Parallel()
suite.Run(t, &MemTestSuite{})
}
+86 -102
View File
@@ -12,42 +12,46 @@ import (
"github.com/9seconds/mtg/v2/ipblocklist/files"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/kentik/patricia"
"github.com/kentik/patricia/bool_tree"
"github.com/panjf2000/ants/v2"
"github.com/yl2chen/cidranger"
)
const (
fireholIPv4DefaultCIDR = 32
fireholIPv6DefaultCIDR = 128
var (
fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
fireholIPv4DefaultCIDR = net.CIDRMask(32, 32)
fireholIPv6DefaultCIDR = net.CIDRMask(128, 128)
)
var fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
// FireholUpdateCallback defines a signature of the callback that has to be
// execute when ip list is updated.
type FireholUpdateCallback func(context.Context, int)
// Firehol is IPBlocklist which uses lists from FireHOL:
// Firehol is [mtglib.IPBlocklist] which uses lists from FireHOL:
// https://iplists.firehol.org/
//
// It can use both local files and remote URLs. This is not necessary
// that blocklists should be taken from this website, we expect only
// compatible formats here.
// It can use both local files and remote URLs. This is not necessary that
// blocklists should be taken from this website, we expect only compatible
// formats here.
//
// Example of the format:
//
// # this is a comment
// # to ignore
// 127.0.0.1 # you can specify an IP
// 10.0.0.0/8 # or cidr
// # this is a comment
// # to ignore
// 127.0.0.1 # you can specify an IP
// 10.0.0.0/8 # or cidr
type Firehol struct {
ctx context.Context
ctxCancel context.CancelFunc
logger mtglib.Logger
updateMutex sync.RWMutex
updateCallback FireholUpdateCallback
ranger cidranger.Ranger
blocklists []files.File
workerPool *ants.Pool
treeV4 *bool_tree.TreeV4
treeV6 *bool_tree.TreeV6
}
// Shutdown stop a background update process.
@@ -64,17 +68,17 @@ func (f *Firehol) Contains(ip net.IP) bool {
f.updateMutex.RLock()
defer f.updateMutex.RUnlock()
if ip4 := ip.To4(); ip4 != nil {
return f.containsIPv4(ip4)
ok, err := f.ranger.Contains(ip)
if err != nil {
f.logger.BindStr("ip", ip.String()).DebugError("Cannot check if ip is present", err)
}
return f.containsIPv6(ip.To16())
return ok && err == nil
}
// Run starts a background update process.
//
// This is a blocking method so you probably want to run it in a
// goroutine.
// This is a blocking method so you probably want to run it in a goroutine.
func (f *Firehol) Run(updateEach time.Duration) {
if updateEach == 0 {
updateEach = DefaultFireholUpdateEach
@@ -103,56 +107,32 @@ func (f *Firehol) Run(updateEach time.Duration) {
}
}
func (f *Firehol) containsIPv4(addr net.IP) bool {
ip := patricia.NewIPv4AddressFromBytes(addr, 32) // nolint: gomnd
if ok, _ := f.treeV4.FindDeepestTag(ip); ok {
return true
}
return false
}
func (f *Firehol) containsIPv6(addr net.IP) bool {
ip := patricia.NewIPv6Address(addr, 128) // nolint: gomnd
if ok, _ := f.treeV6.FindDeepestTag(ip); ok {
return true
}
return false
}
func (f *Firehol) update() {
ctx, cancel := context.WithCancel(f.ctx)
defer cancel()
wg := &sync.WaitGroup{}
wg.Add(len(f.blocklists))
treeMutex := &sync.Mutex{}
v4tree := bool_tree.NewTreeV4()
v6tree := bool_tree.NewTreeV6()
mutex := &sync.Mutex{}
ranger := cidranger.NewPCTrieRanger()
for _, v := range f.blocklists {
go func(file files.File) {
defer wg.Done()
wg.Go(func() {
logger := f.logger.BindStr("filename", v.String())
logger := f.logger.BindStr("filename", file.String())
fileContent, err := file.Open(ctx)
fileContent, err := v.Open(ctx)
if err != nil {
logger.WarningError("update has failed", err)
return
}
defer fileContent.Close()
defer fileContent.Close() //nolint: errcheck
if err := f.updateFromFile(treeMutex, v4tree, v6tree, bufio.NewScanner(fileContent)); err != nil {
if err := f.updateFromFile(mutex, ranger, bufio.NewScanner(fileContent)); err != nil {
logger.WarningError("update has failed", err)
}
}(v)
})
}
wg.Wait()
@@ -160,16 +140,19 @@ func (f *Firehol) update() {
f.updateMutex.Lock()
defer f.updateMutex.Unlock()
f.treeV4 = v4tree
f.treeV6 = v6tree
f.ranger = ranger
f.logger.Info("blocklist was updated")
if f.updateCallback != nil {
f.updateCallback(ctx, ranger.Len())
}
f.logger.Info("ip list was updated")
}
func (f *Firehol) updateFromFile(mutex sync.Locker,
v4tree *bool_tree.TreeV4,
v6tree *bool_tree.TreeV6,
scanner *bufio.Scanner) error {
ranger cidranger.Ranger,
scanner *bufio.Scanner,
) error {
for scanner.Scan() {
text := scanner.Text()
text = fireholRegexpComment.ReplaceAllLiteralString(text, "")
@@ -179,12 +162,18 @@ func (f *Firehol) updateFromFile(mutex sync.Locker,
continue
}
ip, cidr, err := f.updateParseLine(text)
ipnet, err := f.updateParseLine(text)
if err != nil {
return fmt.Errorf("cannot parse a line: %w", err)
}
f.updateAddToTrees(ip, cidr, mutex, v4tree, v6tree)
mutex.Lock()
err = ranger.Insert(cidranger.NewBasicRangerEntry(*ipnet))
mutex.Unlock()
if err != nil {
return fmt.Errorf("cannot insert %v into ranger: %w", ipnet, err)
}
}
if scanner.Err() != nil {
@@ -194,48 +183,38 @@ func (f *Firehol) updateFromFile(mutex sync.Locker,
return nil
}
func (f *Firehol) updateParseLine(text string) (net.IP, uint, error) {
_, ipnet, err := net.ParseCIDR(text)
if err != nil {
ipaddr := net.ParseIP(text)
if ipaddr == nil {
return nil, 0, fmt.Errorf("incorrect ip address %s", text)
}
ip4 := ipaddr.To4()
if ip4 != nil {
return ip4, fireholIPv4DefaultCIDR, nil
}
return ipaddr.To16(), fireholIPv6DefaultCIDR, nil
func (f *Firehol) updateParseLine(text string) (*net.IPNet, error) {
if _, ipnet, err := net.ParseCIDR(text); err == nil {
return ipnet, nil
}
ones, _ := ipnet.Mask.Size()
return ipnet.IP, uint(ones), nil
}
func (f *Firehol) updateAddToTrees(ip net.IP, cidr uint,
mutex sync.Locker,
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) {
mutex.Lock()
defer mutex.Unlock()
if ip.To4() != nil {
v4tree.Set(patricia.NewIPv4AddressFromBytes(ip, cidr), true)
} else {
v6tree.Set(patricia.NewIPv6Address(ip, cidr), true)
ipaddr := net.ParseIP(text)
if ipaddr == nil {
return nil, fmt.Errorf("incorrect ip address %s", text)
}
mask := fireholIPv4DefaultCIDR
if ipaddr.To4() == nil {
mask = fireholIPv6DefaultCIDR
}
return &net.IPNet{
IP: ipaddr,
Mask: mask,
}, nil
}
// NewFirehol creates a new instance of FireHOL IP blocklist.
//
// This method does not start an update process so please execute Run
// when it is necessary.
// This method does not start an update process so please execute Run when it
// is necessary.
func NewFirehol(logger mtglib.Logger, network mtglib.Network,
downloadConcurrency uint,
urls []string,
localFiles []string) (*Firehol, error) {
localFiles []string,
updateCallback FireholUpdateCallback,
) (*Firehol, error) {
blocklists := []files.File{}
for _, v := range localFiles {
@@ -258,12 +237,17 @@ func NewFirehol(logger mtglib.Logger, network mtglib.Network,
blocklists = append(blocklists, file)
}
return NewFireholFromFiles(logger, downloadConcurrency, blocklists)
return NewFireholFromFiles(logger, downloadConcurrency, blocklists, updateCallback)
}
// NewFirehol creates a new instance of FireHOL IP blocklist.
//
// This method creates this instances from a given list of files.
func NewFireholFromFiles(logger mtglib.Logger,
downloadConcurrency uint,
blocklists []files.File) (*Firehol, error) {
blocklists []files.File,
updateCallback FireholUpdateCallback,
) (*Firehol, error) {
if downloadConcurrency == 0 {
downloadConcurrency = DefaultFireholDownloadConcurrency
}
@@ -272,12 +256,12 @@ func NewFireholFromFiles(logger mtglib.Logger,
ctx, cancel := context.WithCancel(context.Background())
return &Firehol{
ctx: ctx,
ctxCancel: cancel,
logger: logger.Named("firehol"),
treeV4: bool_tree.NewTreeV4(),
treeV6: bool_tree.NewTreeV6(),
workerPool: workerPool,
blocklists: blocklists,
ctx: ctx,
ctxCancel: cancel,
logger: logger.Named("firehol"),
ranger: cidranger.NewPCTrieRanger(),
workerPool: workerPool,
blocklists: blocklists,
updateCallback: updateCallback,
}, nil
}
+8 -6
View File
@@ -35,9 +35,9 @@ func (suite *FireholTestSuite) SetupSuite() {
panic(err)
}
defer filefp.Close()
defer filefp.Close() //nolint: errcheck
io.Copy(w, filefp) // nolint: errcheck
io.Copy(w, filefp) //nolint: errcheck
})
suite.httpServer = httptest.NewServer(mux)
@@ -67,7 +67,8 @@ func (suite *FireholTestSuite) TearDownSuite() {
func (suite *FireholTestSuite) TestLocalFail() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
nil, []string{filepath.Join("testdata", "broken_ipset.ipset")})
nil, []string{filepath.Join("testdata", "broken_ipset.ipset")},
nil)
suite.NoError(err)
@@ -85,7 +86,8 @@ func (suite *FireholTestSuite) TestLocalFail() {
func (suite *FireholTestSuite) TestLocalOk() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
nil, []string{filepath.Join("testdata", "good_ipset.ipset")})
nil, []string{filepath.Join("testdata", "good_ipset.ipset")},
nil)
suite.NoError(err)
@@ -103,7 +105,7 @@ func (suite *FireholTestSuite) TestLocalOk() {
func (suite *FireholTestSuite) TestRemoteFail() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
[]string{"https://google.com"}, nil)
[]string{"https://google.com"}, nil, nil)
suite.NoError(err)
@@ -127,7 +129,7 @@ func (suite *FireholTestSuite) TestMixed() {
suite.httpServer.URL,
}, []string{
filepath.Join("testdata", "good_ipset.ipset"),
})
}, nil)
suite.NoError(err)
+5 -5
View File
@@ -1,8 +1,8 @@
// Package ipblocklist contains default implementation of the
// IPBlocklist for mtg.
// [mtglib.IPBlocklist] for mtg.
//
// Please check documentation for mtglib.IPBlocklist interface to get an
// idea of this abstraction.
// Please check documentation for [mtglib.IPBlocklist] interface to get an idea
// of this abstraction.
package ipblocklist
import "time"
@@ -12,7 +12,7 @@ const (
// concurrent downloads of ip blocklists for Firehol.
DefaultFireholDownloadConcurrency = 1
// DefaultFireholUpdateEach defines a default time period when
// Firehol requests updates of the blocklists.
// DefaultFireholUpdateEach defines a default time period when Firehol
// requests updates of the blocklists.
DefaultFireholUpdateEach = 6 * time.Hour
)
+5 -3
View File
@@ -2,16 +2,18 @@ package ipblocklist
import (
"net"
"time"
"github.com/9seconds/mtg/v2/mtglib"
)
type noop struct{}
func (n noop) Contains(ip net.IP) bool { return false }
func (n noop) Contains(ip net.IP) bool { return false }
func (n noop) Run(updateEach time.Duration) {}
func (n noop) Shutdown() {}
// NewNoop returns a dummy ipblocklist which allows all incoming
// connections.
// NewNoop returns a dummy ipblocklist which allows all incoming connections.
func NewNoop() mtglib.IPBlocklist {
return noop{}
}
+7
View File
@@ -17,6 +17,13 @@ func (suite *NoopTestSuite) TestOp() {
suite.False(ipblocklist.NewNoop().Contains(net.ParseIP("10.0.0.10")))
}
func (suite *NoopTestSuite) TestRun() {
blocklist := ipblocklist.NewNoop()
blocklist.Run(0)
blocklist.Shutdown()
}
func TestNoop(t *testing.T) {
t.Parallel()
suite.Run(t, &NoopTestSuite{})
+7 -9
View File
@@ -1,14 +1,12 @@
// Package logger has implementation of loggers for mtglib.Logger
// interface.
// Package logger has implementation of loggers for [mtglib.Logger] interface.
//
// Please see a description of that interface to get some agreements
// which are used by mtglib.
// Please see a description of that interface to get some agreements which are
// used by mtglib.
package logger
// StdLikeLogger is an interface which is close to log.Logger. This is
// commonly used by many 3pp tools. While mtglib itself does not need
// it, it is always a good idea to support it and have a transient end
// to end logging.
// StdLikeLogger is an interface which is close to [log.Logger]. This is
// commonly used by many 3pp tools. While mtglib itself does not need it, it is
// always a good idea to support it and have a transient end to end logging.
type StdLikeLogger interface {
Printf(format string, args ...interface{})
Printf(format string, args ...any)
}
+1 -1
View File
@@ -8,7 +8,7 @@ func (n noopLogger) Named(_ string) mtglib.Logger { return n }
func (n noopLogger) BindInt(_ string, _ int) mtglib.Logger { return n }
func (n noopLogger) BindStr(_, _ string) mtglib.Logger { return n }
func (n noopLogger) BindJSON(_, _ string) mtglib.Logger { return n }
func (n noopLogger) Printf(_ string, _ ...interface{}) {}
func (n noopLogger) Printf(_ string, _ ...any) {}
func (n noopLogger) Info(_ string) {}
func (n noopLogger) Warning(_ string) {}
func (n noopLogger) Debug(_ string) {}
+1 -1
View File
@@ -78,7 +78,7 @@ func (z *zeroLogContext) BindJSON(name, value string) mtglib.Logger {
}
}
func (z *zeroLogContext) Printf(format string, args ...interface{}) {
func (z *zeroLogContext) Printf(format string, args ...any) {
z.Debug(fmt.Sprintf(format, args...))
}
+1 -1
View File
@@ -116,6 +116,6 @@ func (suite *ZeroLoggerTestSuite) TestIndependence() {
suite.NotContains("lalala", log12Output)
}
func TestZeroLogger(t *testing.T) { // nolint: paralleltest
func TestZeroLogger(t *testing.T) { //nolint: paralleltest
suite.Run(t, &ZeroLoggerTestSuite{})
}
+17 -12
View File
@@ -9,26 +9,31 @@
package main
import (
"math/rand"
"time"
"github.com/9seconds/mtg/v2/internal/cli"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/alecthomas/kong"
)
var version = "dev" // has to be set by ldflags
func main() {
rand.Seed(time.Now().UTC().UnixNano())
if err := utils.SetLimits(); err != nil {
panic(err)
}
// this runs profiling server. To enable it, build with prof tag
// $ go build -tags prof
//
// Then you can pass a port using MTG_PROF_PORT environment variable.
// Default is 6000
// $ MTG_PROF_PORT=6000 mtg run config.toml
//
// It will run a webserver with profiling data on
// localhost:${MTG_PROF_PORT:-6000}.
//
// To collect PGO do following:
// $ curl -o default.pgo 'http://localhost:6000/debug/pprof/profile?seconds=300'
//
// See also https://pkg.go.dev/net/http/pprof
// https://go.dev/blog/pprof
runProfile()
cli := &cli.CLI{}
ctx := kong.Parse(cli, kong.Vars{
"version": version,
"version": getVersion(),
})
ctx.FatalIfErrorf(ctx.Run(cli, version))
+121
View File
@@ -0,0 +1,121 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
[[tools.go]]
version = "1.26.1"
backend = "core:go"
[tools.go."platforms.linux-arm64"]
checksum = "sha256:a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7"
url = "https://dl.google.com/go/go1.26.1.linux-arm64.tar.gz"
[tools.go."platforms.linux-arm64-musl"]
checksum = "sha256:a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7"
url = "https://dl.google.com/go/go1.26.1.linux-arm64.tar.gz"
[tools.go."platforms.linux-x64"]
checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"
[tools.go."platforms.linux-x64-musl"]
checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"
[tools.go."platforms.macos-arm64"]
checksum = "sha256:353df43a7811ce284c8938b5f3c7df40b7bfb6f56cb165b150bc40b5e2dd541f"
url = "https://dl.google.com/go/go1.26.1.darwin-arm64.tar.gz"
[tools.go."platforms.macos-x64"]
checksum = "sha256:65773dab2f8cc4cd23d93ba6d0a805de150ca0b78378879292be0b903b8cdd08"
url = "https://dl.google.com/go/go1.26.1.darwin-amd64.tar.gz"
[tools.go."platforms.windows-x64"]
checksum = "sha256:9b68112c913f45b7aebbf13c036721264bbba7e03a642f8f7490c561eebd1ecc"
url = "https://dl.google.com/go/go1.26.1.windows-amd64.zip"
[[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]]
version = "latest"
backend = "go:golang.org/x/pkgsite/cmd/pkgsite"
[[tools."go:golang.org/x/tools/gopls"]]
version = "0.21.1"
backend = "go:golang.org/x/tools/gopls"
[[tools."go:golang.org/x/vuln/cmd/govulncheck"]]
version = "1.1.4"
backend = "go:golang.org/x/vuln/cmd/govulncheck"
[[tools."go:mvdan.cc/gofumpt"]]
version = "0.9.2"
backend = "go:mvdan.cc/gofumpt"
[[tools.golangci-lint]]
version = "2.11.4"
backend = "aqua:golangci/golangci-lint"
[tools.golangci-lint."platforms.linux-arm64"]
checksum = "sha256:3bcfa2e6f3d32b2bf5cd75eaa876447507025e0303698633f722a05331988db4"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-arm64.tar.gz"
[tools.golangci-lint."platforms.linux-arm64-musl"]
checksum = "sha256:3bcfa2e6f3d32b2bf5cd75eaa876447507025e0303698633f722a05331988db4"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-arm64.tar.gz"
[tools.golangci-lint."platforms.linux-x64"]
checksum = "sha256:200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-amd64.tar.gz"
[tools.golangci-lint."platforms.linux-x64-musl"]
checksum = "sha256:200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-amd64.tar.gz"
[tools.golangci-lint."platforms.macos-arm64"]
checksum = "sha256:02db2a2dae8b26812e53b0688a6f617e3ef1f489790e829ea22862cf76945675"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-darwin-arm64.tar.gz"
provenance = "github-attestations"
[tools.golangci-lint."platforms.macos-x64"]
checksum = "sha256:c900d4048db75d1edfd550fd11cf6a9b3008e7caa8e119fcddbc700412d63e60"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-darwin-amd64.tar.gz"
[tools.golangci-lint."platforms.windows-x64"]
checksum = "sha256:4932cfca5e75bf60fe1c576edf459e5e809e6644664a068185d64b84af3fad9e"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-windows-amd64.zip"
[[tools.goreleaser]]
version = "2.15.2"
backend = "aqua:goreleaser/goreleaser"
[tools.goreleaser."platforms.linux-arm64"]
checksum = "sha256:5db66761a98f6693161e49e1a95d28d2673a892ba60cb4a5e16736cafd41c4c9"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_arm64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.linux-arm64-musl"]
checksum = "sha256:5db66761a98f6693161e49e1a95d28d2673a892ba60cb4a5e16736cafd41c4c9"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_arm64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.linux-x64"]
checksum = "sha256:0ebdbf0353aba566b969dde746cc4e4806f96c27aa2f3971b229a9df7611fedc"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_x86_64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.linux-x64-musl"]
checksum = "sha256:0ebdbf0353aba566b969dde746cc4e4806f96c27aa2f3971b229a9df7611fedc"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_x86_64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.macos-arm64"]
checksum = "sha256:0e6bd67688ac949780bf1166813a91f89856898ef4c40d7d46c2c74ebaa4b9ee"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Darwin_all.tar.gz"
provenance = "github-attestations"
[tools.goreleaser."platforms.macos-x64"]
checksum = "sha256:0e6bd67688ac949780bf1166813a91f89856898ef4c40d7d46c2c74ebaa4b9ee"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Darwin_all.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.windows-x64"]
checksum = "sha256:7459832946dbe122c144f8d7f87484d8572ca005b779310aa6bb03346e8de17a"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Windows_x86_64.zip"
provenance = "cosign"
+104 -12
View File
@@ -3,10 +3,15 @@ package mtglib
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"sync"
"net"
"sync/atomic"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/pires/go-proxyproto"
)
type connTraffic struct {
@@ -24,7 +29,7 @@ func (c connTraffic) Read(b []byte) (int, error) {
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), true))
}
return n, err // nolint: wrapcheck
return n, err //nolint: wrapcheck
}
func (c connTraffic) Write(b []byte) (int, error) {
@@ -34,28 +39,21 @@ func (c connTraffic) Write(b []byte) (int, error) {
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), false))
}
return n, err // nolint: wrapcheck
return n, err //nolint: wrapcheck
}
type connRewind struct {
essentials.Conn
active io.Reader
buf bytes.Buffer
mutex sync.RWMutex
active io.Reader
}
func (c *connRewind) Read(p []byte) (int, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.active.Read(p) // nolint: wrapcheck
return c.active.Read(p)
}
func (c *connRewind) Rewind() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.active = io.MultiReader(&c.buf, c.Conn)
}
@@ -67,3 +65,97 @@ func newConnRewind(conn essentials.Conn) *connRewind {
return rv
}
type connProxyProtocol struct {
essentials.Conn
sourceAddr net.Addr
headersWritten bool
}
func (c *connProxyProtocol) Write(p []byte) (int, error) {
if !c.headersWritten {
headers := proxyproto.HeaderProxyFromAddrs(2, c.sourceAddr, c.RemoteAddr())
toSend, err := headers.Format()
if err != nil {
panic(err)
}
if _, err := c.Conn.Write(toSend); err != nil {
return 0, fmt.Errorf("cannot send proxy protocol header: %w", err)
}
c.headersWritten = true
}
return c.Conn.Write(p)
}
func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol {
return &connProxyProtocol{
Conn: target,
sourceAddr: source.RemoteAddr(),
}
}
// idleTracker is a shared idle tracker for a pair of relay connections.
// Both directions update the same timestamp so that activity in one direction
// prevents the other (idle) direction from timing out.
type idleTracker struct {
lastActive atomic.Pointer[time.Time]
timeout time.Duration
}
func newIdleTracker(timeout time.Duration) *idleTracker {
t := &idleTracker{timeout: timeout}
t.touch()
return t
}
func (t *idleTracker) touch() {
stamp := time.Now()
t.lastActive.Store(&stamp)
}
func (t *idleTracker) isIdle() bool {
return time.Since(*t.lastActive.Load()) >= t.timeout
}
type connIdleTimeout struct {
essentials.Conn
tracker *idleTracker
}
func (c connIdleTimeout) Read(b []byte) (int, error) {
var netErr net.Error
for {
c.SetReadDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Read(b)
switch {
case err == nil:
c.tracker.touch()
return n, nil
case errors.As(err, &netErr) && netErr.Timeout() && !c.tracker.isIdle():
continue
}
return n, err
}
}
func (c connIdleTimeout) Write(b []byte) (int, error) {
c.SetWriteDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Write(b)
if n > 0 {
c.tracker.touch()
}
return n, err //nolint: wrapcheck
}
+250 -3
View File
@@ -1,18 +1,27 @@
package mtglib
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"net"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/pires/go-proxyproto"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type netTimeoutError struct{}
func (e netTimeoutError) Error() string { return "i/o timeout" }
func (e netTimeoutError) Timeout() bool { return true }
func (e netTimeoutError) Temporary() bool { return true }
type ConnRewindBaseConn struct {
testlib.EssentialsConnMock
@@ -22,7 +31,7 @@ type ConnRewindBaseConn struct {
func (c *ConnRewindBaseConn) Read(p []byte) (int, error) {
c.Called(p)
return c.readBuffer.Read(p) // nolint: wrapcheck
return c.readBuffer.Read(p) //nolint: wrapcheck
}
type ConnTrafficTestSuite struct {
@@ -69,7 +78,7 @@ func (suite *ConnTrafficTestSuite) TestReadOk() {
suite.Equal(10, n)
}
func (suite *ConnTrafficTestSuite) TestReadErr() { // nolint: dupl
func (suite *ConnTrafficTestSuite) TestReadErr() { //nolint: dupl
suite.eventStreamMock.
On("Send", mock.Anything, mock.Anything).
Once().
@@ -125,7 +134,7 @@ func (suite *ConnTrafficTestSuite) TestWriteOk() {
suite.Equal(10, n)
}
func (suite *ConnTrafficTestSuite) TestWriteErr() { // nolint: dupl
func (suite *ConnTrafficTestSuite) TestWriteErr() { //nolint: dupl
suite.eventStreamMock.
On("Send", mock.Anything, mock.Anything).
Once().
@@ -200,6 +209,229 @@ func (suite *ConnRewindTestSuite) TestRead() {
suite.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, data)
}
type ConnProxyProtocolTestSuite struct {
suite.Suite
sourceConnMock *testlib.EssentialsConnMock
targetConnMock *testlib.EssentialsConnMock
conn *connProxyProtocol
}
func (suite *ConnProxyProtocolTestSuite) SetupTest() {
suite.sourceConnMock = &testlib.EssentialsConnMock{}
suite.targetConnMock = &testlib.EssentialsConnMock{}
localAddr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.1").To4(),
}
remoteAddr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.2").To4(),
}
suite.sourceConnMock.
On("RemoteAddr").
Return(localAddr)
suite.targetConnMock.
On("RemoteAddr").
Maybe().
Return(remoteAddr)
suite.conn = newConnProxyProtocol(suite.sourceConnMock, suite.targetConnMock)
}
func (suite *ConnProxyProtocolTestSuite) TestRead() {
value := []byte{1, 2, 3, 4, 5}
toRead := make([]byte, len(value))
suite.targetConnMock.
On("Read", mock.AnythingOfType("[]uint8")).
Once().
Return(len(toRead), nil).
Run(func(args mock.Arguments) {
arr := args.Get(0).([]byte)
copy(arr, value)
})
n, err := suite.conn.Read(toRead)
suite.Equal(len(value), n)
suite.NoError(err)
suite.Equal(value, toRead)
}
func (suite *ConnProxyProtocolTestSuite) TestWrite() {
value := []byte{1, 2, 3, 4, 5}
buf := &bytes.Buffer{}
bufReader := bufio.NewReader(buf)
suite.targetConnMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(28, nil).
Run(func(args mock.Arguments) {
arr := args.Get(0).([]byte)
buf.Write(arr)
})
_, err := suite.conn.Write(value)
suite.NoError(err)
header, err := proxyproto.Read(bufReader)
suite.NoError(err)
sourceAddr, destAddr, ok := header.TCPAddrs()
suite.True(ok)
suite.Equal(suite.sourceConnMock.RemoteAddr(), sourceAddr)
suite.Equal(suite.targetConnMock.RemoteAddr(), destAddr)
read, _ := io.ReadAll(bufReader)
suite.Equal(value, read)
_, err = suite.conn.Write(value)
suite.NoError(err)
read, _ = io.ReadAll(bufReader)
suite.Equal(value, read)
}
func (suite *ConnProxyProtocolTestSuite) TearDownTest() {
suite.sourceConnMock.AssertExpectations(suite.T())
suite.targetConnMock.AssertExpectations(suite.T())
}
type IdleTrackerTestSuite struct {
suite.Suite
}
func (suite *IdleTrackerTestSuite) TestNewNotIdle() {
tracker := newIdleTracker(time.Second)
suite.False(tracker.isIdle())
}
func (suite *IdleTrackerTestSuite) TestIdleAfterTimeout() {
tracker := newIdleTracker(10 * time.Millisecond)
time.Sleep(20 * time.Millisecond)
suite.True(tracker.isIdle())
}
func (suite *IdleTrackerTestSuite) TestTouchResetsIdle() {
tracker := newIdleTracker(50 * time.Millisecond)
time.Sleep(30 * time.Millisecond)
tracker.touch()
suite.False(tracker.isIdle())
}
type ConnIdleTimeoutTestSuite struct {
suite.Suite
connMock *testlib.EssentialsConnMock
tracker *idleTracker
conn connIdleTimeout
}
func (suite *ConnIdleTimeoutTestSuite) SetupTest() {
suite.connMock = &testlib.EssentialsConnMock{}
suite.tracker = newIdleTracker(time.Second)
suite.conn = connIdleTimeout{
Conn: suite.connMock,
tracker: suite.tracker,
}
}
func (suite *ConnIdleTimeoutTestSuite) TearDownTest() {
suite.connMock.AssertExpectations(suite.T())
}
func (suite *ConnIdleTimeoutTestSuite) TestReadOk() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadNonTimeoutErr() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, io.EOF)
n, err := suite.conn.Read(make([]byte, 10))
suite.True(errors.Is(err, io.EOF))
suite.Equal(0, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadTimeoutRetriesWhenNotIdle() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
suite.connMock.On("Read", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadTimeoutClosesWhenIdle() {
suite.tracker = newIdleTracker(time.Millisecond)
suite.conn = connIdleTimeout{
Conn: suite.connMock,
tracker: suite.tracker,
}
time.Sleep(5 * time.Millisecond)
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
n, err := suite.conn.Read(make([]byte, 10))
suite.Equal(0, n)
netErr, ok := err.(net.Error) //nolint: errorlint
suite.True(ok)
suite.True(netErr.Timeout())
}
func (suite *ConnIdleTimeoutTestSuite) TestSharedTrackerPreventsFalseTimeout() {
connMock2 := &testlib.EssentialsConnMock{}
conn2 := connIdleTimeout{
Conn: connMock2,
tracker: suite.tracker,
}
connMock2.On("SetWriteDeadline", mock.Anything).Return(nil)
connMock2.On("Write", mock.Anything).Once().Return(5, nil)
_, _ = conn2.Write(make([]byte, 5))
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
suite.connMock.On("Read", mock.Anything).Once().Return(3, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(3, n)
connMock2.AssertExpectations(suite.T())
}
func (suite *ConnIdleTimeoutTestSuite) TestWriteOk() {
suite.connMock.On("SetWriteDeadline", mock.Anything).Return(nil)
suite.connMock.On("Write", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Write(make([]byte, 5))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestWriteErr() {
suite.connMock.On("SetWriteDeadline", mock.Anything).Return(nil)
suite.connMock.On("Write", mock.Anything).Once().Return(0, io.EOF)
n, err := suite.conn.Write(make([]byte, 5))
suite.True(errors.Is(err, io.EOF))
suite.Equal(0, n)
}
func TestConnTraffic(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTrafficTestSuite{})
@@ -209,3 +441,18 @@ func TestConnRewind(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnRewindTestSuite{})
}
func TestConnProxyProtocol(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnProxyProtocolTestSuite{})
}
func TestIdleTracker(t *testing.T) {
t.Parallel()
suite.Run(t, &IdleTrackerTestSuite{})
}
func TestConnIdleTimeout(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnIdleTimeoutTestSuite{})
}
+53 -19
View File
@@ -29,13 +29,13 @@ type EventStart struct {
RemoteIP net.IP
}
// EventConnectedToDC is emitted when mtg proxy has connected to a
// Telegram server.
// EventConnectedToDC is emitted when mtg proxy has connected to a Telegram
// server.
type EventConnectedToDC struct {
eventBase
// RemoteIP is an IP address of the Telegram server proxy has been
// connected to.
// RemoteIP is an IP address of the Telegram server proxy has been connected
// to.
RemoteIP net.IP
// DC is an index of the datacenter proxy has been connected to.
@@ -49,15 +49,15 @@ type EventTraffic struct {
// Traffic is a count of bytes which were transmitted.
Traffic uint
// IsRead defines if we _read_ or _write_ to connection. A rule of
// thumb is simple: EventTraffic is bound to a remote connection. Not
// to a client one, but either to Telegram or front domain one.
// IsRead defines if we _read_ or _write_ to connection. A rule of thumb is
// simple: EventTraffic is bound to a remote connection. Not to a client one,
// but either to Telegram or front domain one.
//
// In the case of Telegram, isRead means that we've fetched some bytes
// from Telegram to send it to a client.
// In the case of Telegram, isRead means that we've fetched some bytes from
// Telegram to send it to a client.
//
// In the case of the front domain, it means that we've fetched some
// bytes from this domain to send it to a client.
// In the case of the front domain, it means that we've fetched some bytes
// from this domain to send it to a client.
IsRead bool
}
@@ -66,24 +66,25 @@ type EventFinish struct {
eventBase
}
// EventDomainFronting is emitted when we connect to a front domain
// instead of Telegram server.
// EventDomainFronting is emitted when we connect to a front domain instead of
// Telegram server.
type EventDomainFronting struct {
eventBase
}
// EventConcurrencyLimited is emitted when connection was declined
// because of the concurrency limit of the worker pool.
// EventConcurrencyLimited is emitted when connection was declined because of
// the concurrency limit of the worker pool.
type EventConcurrencyLimited struct {
eventBase
}
// EventIPBlocklisted is emitted when connection was declined because
// IP address was found in IP blocklist.
// EventIPBlocklisted is emitted when connection was declined because IP
// address was found in IP blocklist.
type EventIPBlocklisted struct {
eventBase
RemoteIP net.IP
RemoteIP net.IP
IsBlockList bool
}
// EventReplayAttack is emitted when mtg detects a replay attack on a
@@ -92,6 +93,15 @@ type EventReplayAttack struct {
eventBase
}
// EventIPListSize is emitted when mtg updates a contents of the ip lists:
// allowlist or blocklist.
type EventIPListSize struct {
eventBase
Size int
IsBlockList bool
}
// NewEventStart creates a new EventStart event.
func NewEventStart(streamID string, remoteIP net.IP) EventStart {
return EventStart{
@@ -163,7 +173,20 @@ func NewEventIPBlocklisted(remoteIP net.IP) EventIPBlocklisted {
eventBase: eventBase{
timestamp: time.Now(),
},
RemoteIP: remoteIP,
RemoteIP: remoteIP,
IsBlockList: true,
}
}
// NewEventIPAllowlisted creates a NewEventIPBlocklisted event with a mark that
// it is supposed to be for allow list.
func NewEventIPAllowlisted(remoteIP net.IP) EventIPBlocklisted {
return EventIPBlocklisted{
eventBase: eventBase{
timestamp: time.Now(),
},
RemoteIP: remoteIP,
IsBlockList: false,
}
}
@@ -176,3 +199,14 @@ func NewEventReplayAttack(streamID string) EventReplayAttack {
},
}
}
// NewEventIPListSize creates a new EventIPListSize event.
func NewEventIPListSize(size int, isBlockList bool) EventIPListSize {
return EventIPListSize{
eventBase: eventBase{
timestamp: time.Now(),
},
Size: size,
IsBlockList: isBlockList,
}
}
+18
View File
@@ -60,6 +60,15 @@ func (suite *EventsTestSuite) TestEventIPBlocklisted() {
suite.Empty(evt.StreamID())
suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond)
suite.True(evt.IsBlockList)
}
func (suite *EventsTestSuite) TestEventIPAllowlisted() {
evt := mtglib.NewEventIPAllowlisted(net.ParseIP("10.0.0.10"))
suite.Empty(evt.StreamID())
suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond)
suite.False(evt.IsBlockList)
}
func (suite *EventsTestSuite) TestEventReplayAttack() {
@@ -69,6 +78,15 @@ func (suite *EventsTestSuite) TestEventReplayAttack() {
suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond)
}
func (suite *EventsTestSuite) TestEventIPListSize() {
evt := mtglib.NewEventIPListSize(10, false)
suite.Empty(evt.StreamID())
suite.WithinDuration(time.Now(), evt.Timestamp(), 10*time.Millisecond)
suite.Equal(10, evt.Size)
suite.False(evt.IsBlockList)
}
func TestEvents(t *testing.T) {
t.Parallel()
suite.Run(t, &EventsTestSuite{})
+144 -138
View File
@@ -1,20 +1,19 @@
// mtglib defines a package with MTPROTO proxy.
//
// Since mtg itself is build as an example of how to work with mtglib,
// it worth to telling a couple of words about a project organization.
// Since mtg itself is build as an example of how to work with mtglib, it worth
// to telling a couple of words about a project organization.
//
// A core object of the project is mtglib.Proxy. This is a proxy you
// expect: that one which you configure, set to serve on a listener
// and/or shutdown on application termination.
// A core object of the project is [mtglib.Proxy]. This is a proxy you expect:
// that one which you configure, set to serve on a listener and/or shutdown on
// application termination.
//
// But it also has a core logic unrelated to Telegram per se: anti
// replay cache, network connectivity (who knows, maybe you want to have
// a native VMESS integration) and so on.
// But it also has a core logic unrelated to Telegram per se: anti replay
// cache, network connectivity (who knows, maybe you want to have a native
// VMESS integration) and so on.
//
// You can supply such parts to a proxy with interfaces. The rest of
// the packages in mtg define some default implementations of these
// interfaces. But if you want to integrate it with, let say, influxdb,
// you can do it easily.
// You can supply such parts to a proxy with interfaces. The rest of the
// packages in mtg define some default implementations of these interfaces. But
// if you want to integrate it with, let say, influxdb, you can do it easily.
package mtglib
import (
@@ -28,38 +27,42 @@ import (
)
var (
// ErrSecretEmpty is returned if you are trying to create a proxy
// but do not provide a secret.
// ErrSecretEmpty is returned if you are trying to create a proxy but do not
// provide a secret.
ErrSecretEmpty = errors.New("secret is empty")
// ErrSecretInvalid is returned if you are trying to create a proxy
// but secret value is invalid (no host or payload are zeroes).
// ErrSecretInvalid is returned if you are trying to create a proxy but secret
// value is invalid (no host or payload are zeroes).
ErrSecretInvalid = errors.New("secret is invalid")
// ErrNetworkIsNotDefined is returned if you are trying to create a
// proxy but network value is undefined.
// ErrNetworkIsNotDefined is returned if you are trying to create a proxy but
// network value is undefined.
ErrNetworkIsNotDefined = errors.New("network is not defined")
// ErrAntiReplayCacheIsNotDefined is returned if you are trying to
// create a proxy but anti replay cache value is undefined.
// ErrAntiReplayCacheIsNotDefined is returned if you are trying to create a
// proxy but anti replay cache value is undefined.
ErrAntiReplayCacheIsNotDefined = errors.New("anti-replay cache is not defined")
// ErrIPBlocklistIsNotDefined is returned if you are trying to
// create a proxy but ip blocklist instance is not defined.
// ErrIPBlocklistIsNotDefined is returned if you are trying to create a proxy
// but ip blocklist instance is not defined.
ErrIPBlocklistIsNotDefined = errors.New("ip blocklist is not defined")
// ErrEventStreamIsNotDefined is returned if you are trying to create a
// proxy but event stream instance is not defined.
// ErrIPAllowlistIsNotDefined is returned if you are trying to create a proxy
// but ip allowlist instance is not defined.
ErrIPAllowlistIsNotDefined = errors.New("ip allowlist is not defined")
// ErrEventStreamIsNotDefined is returned if you are trying to create a proxy
// but event stream instance is not defined.
ErrEventStreamIsNotDefined = errors.New("event stream is not defined")
// ErrLoggerIsNotDefined is returned if you are trying to
// create a proxy but logger is not defined.
// ErrLoggerIsNotDefined is returned if you are trying to create a proxy but
// logger is not defined.
ErrLoggerIsNotDefined = errors.New("logger is not defined")
)
const (
// DefaultConcurrency is a default max count of simultaneously
// connected clients.
// DefaultConcurrency is a default max count of simultaneously connected
// clients.
DefaultConcurrency = 4096
// DefaultBufferSize is a default size of a copy buffer.
@@ -67,151 +70,156 @@ const (
// Deprecated: this setting no longer makes any effect.
DefaultBufferSize = 16 * 1024 // 16 kib
// DefaultDomainFrontingPort is a default port (HTTPS) to connect to in
// case of probe-resistance activity.
// DefaultDomainFrontingPort is a default port (HTTPS) to connect to in case
// of probe-resistance activity.
DefaultDomainFrontingPort = 443
// DefaultIdleTimeout is a default timeout for closing a connection
// in case of idling.
// DefaultIdleTimeout is a default timeout for closing a connection in case of
// idling.
//
// Deprecated: no longer in use because of changed TCP relay
// algorithm.
// Deprecated: no longer in use because of changed TCP relay algorithm.
DefaultIdleTimeout = time.Minute
// DefaultTolerateTimeSkewness is a default timeout for time
// skewness on a faketls timeout verification.
// DefaultTolerateTimeSkewness is a default timeout for time skewness on a
// faketls timeout verification.
DefaultTolerateTimeSkewness = 3 * time.Second
// DefaultPreferIP is a default value for Telegram IP connectivity
// preference.
// DefaultPreferIP is a default value for Telegram IP connectivity preference.
DefaultPreferIP = "prefer-ipv6"
// SecretKeyLength defines a length of the secret bytes used
// by Telegram and a proxy.
// SecretKeyLength defines a length of the secret bytes used by Telegram and a
// proxy.
SecretKeyLength = 16
// ConnectionIDBytesLength defines a count of random bytes used to generate
// a stream/connection ids.
// ConnectionIDBytesLength defines a count of random bytes used to generate a
// stream/connection ids.
ConnectionIDBytesLength = 16
// TCPRelayReadTimeout defines a max time period between two consecuitive
// reads from Telegram after which connection will be terminated. This is
// required to abort stale connections.
TCPRelayReadTimeout = 20 * time.Second
// DoppelGangerPerRaid defines a number of requests to each URL
// per raid.
DoppelGangerPerRaid = 10
// DoppelGangerEach defines a time period between each crawl attempt.
DoppelGangerEach = 6 * time.Hour
)
// Network defines a knowledge how to work with a network. It may sound
// fun but it encapsulates all the knowledge how to properly establish
// connections to remote hosts and configure HTTP clients.
// Network defines a knowledge how to work with a network. It may sound fun but
// it encapsulates all the knowledge how to properly establish connections to
// remote hosts and configure HTTP clients.
//
// For example, if you want to use SOCKS5 proxy, you probably want to
// have all traffic routed to this proxy: telegram connections, http
// requests and so on. This knowledge is encapsulated into instances of
// such interface.
// For example, if you want to use SOCKS5 proxy, you probably want to have all
// traffic routed to this proxy: telegram connections, http requests and so on.
// This knowledge is encapsulated into instances of such interface.
//
// mtglib uses Network for:
//
// 1. Dialing to Telegram
//
// 2. Dialing to front domain
//
// 3. Doing HTTP requests (for example, for FireHOL ipblocklist).
// 1. Dialing to Telegram
// 2. Dialing to front domain
// 3. Doing HTTP requests (for example, for FireHOL ipblocklist).
type Network interface {
// Dial establishes context-free TCP connections.
Dial(network, address string) (essentials.Conn, error)
// DialContext dials using a context. This is a preferrable
// way of establishing TCP connections.
// DialContext dials using a context. This is a preferable way of
// establishing TCP connections.
DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
// MakeHTTPClient build an HTTP client with given dial function. If
// nothing is provided, then DialContext of this interface is going
// to be used.
// MakeHTTPClient build an HTTP client with given dial function. If nothing is
// provided, then DialContext of this interface is going to be used.
MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
// NativeDialer returns a configured instance of native dialer that
// skips proxy connections or any other irrelevant settings.
NativeDialer() *net.Dialer
}
// AntiReplayCache is an interface that is used to detect replay attacks
// based on some traffic fingerprints.
// AntiReplayCache is an interface that is used to detect replay attacks based
// on some traffic fingerprints.
//
// Replay attacks are probe attacks whose main goal is to identify if
// server software can be classified in some way. For example, if you
// send some HTTP request to a web server, then you can expect that this
// server will respond with HTTP response back.
// Replay attacks are probe attacks whose main goal is to identify if server
// software can be classified in some way. For example, if you send some HTTP
// request to a web server, then you can expect that this server will respond
// with HTTP response back.
//
// There is a problem though. Let's imagine, that connection is
// encrypted. Let's imagine, that it is encrypted with some static key
// like ShadowSocks (https://shadowsocks.org/assets/whitepaper.pdf).
// In that case, in theory, if you repeat the same bytes, you can get
// the same responses. Let's imagine, that you've cracked the key. then
// if you send the same bytes, you can decrypt a response and see its
// structure. Based on its structure you can identify if this server is
// SOCKS5, MTPROTO proxy etc.
// There is a problem though. Let's imagine, that connection is encrypted.
// Let's imagine, that it is encrypted with some static key like [ShadowSocks].
// In that case, in theory, if you repeat the same bytes, you can get the same
// responses. Let's imagine, that you've cracked the key. then if you send the
// same bytes, you can decrypt a response and see its structure. Based on its
// structure you can identify if this server is SOCKS5, MTPROTO proxy etc.
//
// This is just one example, maybe not the best or not the most
// relevant. In real life, different organizations use such replay
// attacks to perform some reverse engineering of the proxy, do some
// statical analysis to identify server software.
// This is just one example, maybe not the best or not the most relevant. In
// real life, different organizations use such replay attacks to perform some
// reverse engineering of the proxy, do some statical analysis to identify
// server software.
//
// There are many ways how to protect your proxy against them. One
// is domain fronting which is a core part of mtg. Another one is to
// collect some 'handshake fingerprints' and forbid duplication.
// There are many ways how to protect your proxy against them. One is domain
// fronting which is a core part of mtg. Another one is to collect some
// 'handshake fingerprints' and forbid duplication.
//
// So, it one is sending the same byte flow right after you (or a couple
// of hours after), mtg should detect that and reject this connection
// (or redirect to fronting domain).
// So, it one is sending the same byte flow right after you (or a couple of
// hours after), mtg should detect that and reject this connection (or redirect
// to fronting domain).
//
// [ShadowSocks]: https://shadowsocks.org/assets/whitepaper.pdf
type AntiReplayCache interface {
// Seen before checks if this set of bytes was observed before or
// not. If it is required to store this information somewhere else,
// then it has to do that.
// Seen before checks if this set of bytes was observed before or not. If it
// is required to store this information somewhere else, then it has to do
// that.
SeenBefore(data []byte) bool
}
// IPBlocklist filters requests based on IP address.
//
// If this filter has an IP address, then mtg closes a request without
// reading anything from a socket. It also does not give such request to
// a worker pool, so in worst cases you can expect that you invoke this
// object more frequent than defined proxy concurrency.
// If this filter has an IP address, then mtg closes a request without reading
// anything from a socket. It also does not give such request to a worker pool,
// so in worst cases you can expect that you invoke this object more frequent
// than defined proxy concurrency.
type IPBlocklist interface {
// Contains checks if given IP address belongs to this blocklist If.
// it is, a connection is terminated .
// Contains checks if given IP address belongs to this blocklist If. it is, a
// connection is terminated .
Contains(net.IP) bool
// Run starts a background update procedure for a blocklist
Run(time.Duration)
// Shutdown stops a blocklist. It is assumed that none will access it after.
Shutdown()
}
// Event is a data structure which is populated during mtg request
// processing lifecycle. Each request popluates many events:
// Event is a data structure which is populated during mtg request processing
// lifecycle. Each request popluates many events:
// 1. Client connected
// 2. Request is finished
// 3. Connection to Telegram server is established
//
// 1. Client connected
//
// 2. Request is finished
//
// 3. Connection to Telegram server is established
//
// and so on. All these events are data structures but all of them
// must conform the same interface.
// and so on. All these events are data structures but all of them must conform
// the same interface.
type Event interface {
// StreamID returns an identifier of the stream, connection,
// request, you name it. All events within the same stream returns
// the same stream id.
// StreamID returns an identifier of the stream, connection, request, you name
// it. All events within the same stream returns the same stream id.
StreamID() string
// Timestamp returns a timestamp when this event was generated.
Timestamp() time.Time
}
// EventStream is an abstraction that accepts a set of events produced
// by mtg. Its main goal is to inject your logging or monitoring system.
// EventStream is an abstraction that accepts a set of events produced by mtg.
// Its main goal is to inject your logging or monitoring system.
//
// The idea is simple. When mtg works, it emits a set of events during
// a lifecycle of the requestor: EventStart, EventFinish etc. mtg is a
// producer which puts these events into a stream. Responsibility of
// the stream is to deliver this event to consumers/observers. There
// might be many different observers (for example, you want to have both
// statsd and prometheus), mtg should know nothing about them.
// The idea is simple. When mtg works, it emits a set of events during a
// lifecycle of the requestor: EventStart, EventFinish etc. mtg is a producer
// which puts these events into a stream. Responsibility of the stream is to
// deliver this event to consumers/observers. There might be many different
// observers (for example, you want to have both statsd and prometheus), mtg
// should know nothing about them.
type EventStream interface {
// Send delivers an event to observers. Given context has to be
// respected. If the context is closed, all blocking operations should
// be released ASAP.
// Send delivers an event to observers. Given context has to be respected. If
// the context is closed, all blocking operations should be released ASAP.
//
// It is possible that context is closed but the message is delivered.
// EventStream implementations should solve this issue somehow.
@@ -220,27 +228,26 @@ type EventStream interface {
// Logger defines an interface of the logger used by mtglib.
//
// Each logger has a name. It is possible to stack names to organize
// poor-man namespaces. Also, each logger must be able to bind
// parameters to avoid pushing them all the time.
// Each logger has a name. It is possible to stack names to organize poor-man
// namespaces. Also, each logger must be able to bind parameters to avoid
// pushing them all the time.
//
// Example
//
// logger := SomeLogger{}
// logger = logger.BindStr("ip", net.IP{127, 0, 0, 1})
// logger.Info("Hello")
// logger := SomeLogger{} logger = logger.BindStr("ip", net.IP{127, 0, 0, 1})
// logger.Info("Hello")
//
// In that case, ip is bound as a parameter. It is a great idea to
// put this parameter somewhere in a log message.
// In that case, ip is bound as a parameter. It is a great idea to put this
// parameter somewhere in a log message.
//
// logger1 = logger.BindStr("param1", "11")
// logger2 = logger.BindInt("param2", 11)
// logger1 = logger.BindStr("param1", "11") logger2 = logger.BindInt("param2",
// 11)
//
// logger1 should see no param2 and vice versa, logger2 should not see param1
// If you attach a parameter to a logger, parents should not know about that.
type Logger interface {
// Named returns a new logger with a bound name. Name chaining is
// allowed and appreciated.
// Named returns a new logger with a bound name. Name chaining is allowed and
// appreciated.
Named(name string) Logger
// BindInt binds new integer parameter to a new logger instance.
@@ -253,27 +260,26 @@ type Logger interface {
BindJSON(name, value string) Logger
// Printf is to support log.Logger behavior.
Printf(format string, args ...interface{})
Printf(format string, args ...any)
// Info puts a message about some normal situation.
Info(msg string)
// InfoError puts a message about some normal situation but this
// situation is related to a given error.
// InfoError puts a message about some normal situation but this situation is
// related to a given error.
InfoError(msg string, err error)
// Warning puts a message about some extraordinary situation
// worth to look at.
// Warning puts a message about some extraordinary situation worth to look at.
Warning(msg string)
// WarningError puts a message about some extraordinary situation
// worth to look at. This situation is related to a given error.
// WarningError puts a message about some extraordinary situation worth to
// look at. This situation is related to a given error.
WarningError(msg string, err error)
// Debug puts a message useful for debugging only.
Debug(msg string)
// Debug puts a message useful for debugging only. This message is
// related to a given error.
// Debug puts a message useful for debugging only. This message is related to
// a given error.
DebugError(msg string, err error)
}
+11 -11
View File
@@ -8,17 +8,17 @@ import (
type NoopLogger struct{}
func (n NoopLogger) Named(_ string) Logger { return n }
func (n NoopLogger) BindInt(_ string, _ int) Logger { return n }
func (n NoopLogger) BindStr(_, _ string) Logger { return n }
func (n NoopLogger) BindJSON(_, _ string) Logger { return n }
func (n NoopLogger) Printf(_ string, _ ...interface{}) {}
func (n NoopLogger) Info(_ string) {}
func (n NoopLogger) Warning(_ string) {}
func (n NoopLogger) Debug(_ string) {}
func (n NoopLogger) InfoError(_ string, _ error) {}
func (n NoopLogger) WarningError(_ string, _ error) {}
func (n NoopLogger) DebugError(_ string, _ error) {}
func (n NoopLogger) Named(_ string) Logger { return n }
func (n NoopLogger) BindInt(_ string, _ int) Logger { return n }
func (n NoopLogger) BindStr(_, _ string) Logger { return n }
func (n NoopLogger) BindJSON(_, _ string) Logger { return n }
func (n NoopLogger) Printf(_ string, _ ...any) {}
func (n NoopLogger) Info(_ string) {}
func (n NoopLogger) Warning(_ string) {}
func (n NoopLogger) Debug(_ string) {}
func (n NoopLogger) InfoError(_ string, _ error) {}
func (n NoopLogger) WarningError(_ string, _ error) {}
func (n NoopLogger) DebugError(_ string, _ error) {}
type EventStreamMock struct {
mock.Mock
+17
View File
@@ -0,0 +1,17 @@
package dc
import (
"fmt"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
)
type Addr struct {
Network string
Address string
Obfuscator obfuscation.Obfuscator
}
func (d Addr) String() string {
return fmt.Sprintf("addr=%s, secret=%v", d.Address, d.Obfuscator.Secret)
}
+33
View File
@@ -0,0 +1,33 @@
package dc
import "math/rand/v2"
type dcAddrSet struct {
v4 map[int][]Addr
v6 map[int][]Addr
}
func (d dcAddrSet) getV4(dc int) []Addr {
if d.v4 == nil {
return nil
}
return d.get(d.v4[dc])
}
func (d dcAddrSet) getV6(dc int) []Addr {
if d.v6 == nil {
return nil
}
return d.get(d.v6[dc])
}
func (d dcAddrSet) get(addrs []Addr) []Addr {
otherSet := make([]Addr, 0, len(addrs))
otherSet = append(otherSet, addrs...)
rand.Shuffle(len(otherSet), func(i, j int) {
otherSet[i], otherSet[j] = otherSet[j], otherSet[i]
})
return otherSet
}
+77
View File
@@ -0,0 +1,77 @@
package dc
import (
"context"
"net"
"time"
"github.com/9seconds/mtg/v2/essentials"
)
type preferIP uint8
const (
preferIPOnlyIPv4 preferIP = iota
preferIPOnlyIPv6
preferIPPreferIPv4
preferIPPreferIPv6
)
const (
// Default DC to connect to if not sure.
DefaultDC = 2
// How often should we request updates from
// https://core.telegram.org/getProxyConfig
PublicConfigUpdateEach = time.Hour
PublicConfigUpdateURLv4 = "https://core.telegram.org/getProxyConfig"
PublicConfigUpdateURLv6 = "https://core.telegram.org/getProxyConfigV6"
// How often should we extract hosts from Telegram using help.getConfig
// method.
OwnConfigUpdateEach = time.Hour
)
type Logger interface {
Info(msg string)
WarningError(msg string, err error)
}
type Updater interface {
Run(ctx context.Context)
}
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
var defaultDCAddrSet = (func() dcAddrSet {
addrSet := dcAddrSet{
v4: make(map[int][]Addr),
v6: make(map[int][]Addr),
}
for dcid, ips := range essentials.TelegramCoreAddresses {
for _, addr := range ips {
host, _, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
ip := net.ParseIP(host)
if ip == nil {
panic(addr)
}
if ip.To4() == nil {
addrSet.v6[dcid] = append(addrSet.v6[dcid], Addr{
Network: "tcp6",
Address: addr,
})
} else {
addrSet.v4[dcid] = append(addrSet.v4[dcid], Addr{
Network: "tcp4",
Address: addr,
})
}
}
}
return addrSet
})()
+43
View File
@@ -0,0 +1,43 @@
package dc
import (
"context"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type LoggerMock struct {
mock.Mock
}
func (m *LoggerMock) Info(msg string) {
m.Called(msg)
}
func (m *LoggerMock) WarningError(msg string, err error) {
m.Called(msg, err)
}
type UpdaterTestSuiteBase struct {
suite.Suite
ctx context.Context
ctxCancel context.CancelFunc
loggerMock *LoggerMock
}
func (s *UpdaterTestSuiteBase) SetupTest() {
ctx, cancel := context.WithCancel(context.Background())
s.loggerMock = &LoggerMock{}
s.loggerMock.On("Info", mock.AnythingOfType("string"))
s.loggerMock.On("WarningError", mock.AnythingOfType("string"), mock.Anything)
s.ctx = ctx
s.ctxCancel = cancel
}
func (s *UpdaterTestSuiteBase) TearDownTest() {
s.ctxCancel()
}
@@ -0,0 +1,93 @@
package dc
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
)
var publicConfigRe = regexp.MustCompile(`^\s*proxy_for\s+(\d+)\s+(\S+?)?;\s*$`)
type PublicConfigUpdater struct {
updater
http *http.Client
tg *Telegram
}
func (p *PublicConfigUpdater) Run(ctx context.Context, url, network string) {
p.run(ctx, func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
panic(err)
}
resp, err := p.http.Do(req)
if err != nil {
if resp != nil {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}
return fmt.Errorf("cannot fetch url %s: %w", url, err)
}
if resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("unexpected status code from %s: %d", url, resp.StatusCode)
}
scanner := bufio.NewScanner(resp.Body)
addrs := map[int][]Addr{}
for scanner.Scan() {
matches := publicConfigRe.FindStringSubmatch(scanner.Text())
if len(matches) != 3 {
continue
}
dc, err := strconv.Atoi(matches[1])
if err != nil {
continue
}
switch dc {
// this is a list of DC we currently support. Other are ignored.
case 203: // CDN DC
p.logger.Info(fmt.Sprintf("found %s address for DC %d", matches[2], dc))
addrs[dc] = append(addrs[dc], Addr{
Network: network,
Address: matches[2],
})
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("cannot read response body from %s: %w", url, err)
}
p.tg.lock.Lock()
defer p.tg.lock.Unlock()
if network == "tcp4" {
p.tg.view.publicConfigs.v4 = addrs
} else {
p.tg.view.publicConfigs.v6 = addrs
}
return nil
})
}
func NewPublicConfigUpdater(tg *Telegram, logger Logger, client *http.Client) *PublicConfigUpdater {
return &PublicConfigUpdater{
updater: updater{
logger: logger,
period: PublicConfigUpdateEach,
},
http: client,
tg: tg,
}
}
@@ -0,0 +1,113 @@
package dc
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type PublicConfigUpdaterTestSuite struct {
UpdaterTestSuiteBase
u *PublicConfigUpdater
lock sync.Mutex
srv *httptest.Server
responseHandler func(w http.ResponseWriter)
}
func (s *PublicConfigUpdaterTestSuite) SetupSuite() {
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.lock.Lock()
s.responseHandler(w)
s.lock.Unlock()
}))
}
func (s *PublicConfigUpdaterTestSuite) TearDownSuite() {
s.srv.Close()
}
func (s *PublicConfigUpdaterTestSuite) SetupTest() {
s.UpdaterTestSuiteBase.SetupTest()
tg, err := New("prefer-ipv4")
require.NoError(s.T(), err)
s.u = NewPublicConfigUpdater(tg, s.loggerMock, s.srv.Client())
}
func (s *PublicConfigUpdaterTestSuite) Test502StatusCode() {
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusBadGateway)
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestEmptyFile() {
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestGarbage() {
result := `
proxy_for -1 -1;
proxy_for 100 100.10.0.0:3333;
lala 0 0
`
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(result)) //nolint: errcheck
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestOk() {
result := `
proxy_for 203 100.10.0.0:3333;
proxy_for -100 101.10.0.0:3333;
`
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(result)) //nolint: errcheck
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
s.Len(s.u.tg.view.publicConfigs.v4, 1)
s.Len(s.u.tg.view.publicConfigs.v4[203], 1)
s.Equal("100.10.0.0:3333", s.u.tg.view.publicConfigs.v4[203][0].Address)
}
func TestPublicConfigUpdater(t *testing.T) {
suite.Run(t, &PublicConfigUpdaterTestSuite{})
}
+50
View File
@@ -0,0 +1,50 @@
package dc
import (
"fmt"
"strings"
"sync"
)
type Telegram struct {
lock sync.RWMutex
view dcView
preferIP preferIP
}
func (t *Telegram) GetAddresses(dc int) []Addr {
t.lock.RLock()
defer t.lock.RUnlock()
switch t.preferIP {
case preferIPOnlyIPv4:
return t.view.getV4(dc)
case preferIPOnlyIPv6:
return t.view.getV6(dc)
case preferIPPreferIPv4:
return append(t.view.getV4(dc), t.view.getV6(dc)...)
}
return append(t.view.getV6(dc), t.view.getV4(dc)...)
}
func New(ipPreference string) (*Telegram, error) {
var pref preferIP
switch strings.ToLower(ipPreference) {
case "prefer-ipv4":
pref = preferIPPreferIPv4
case "prefer-ipv6":
pref = preferIPPreferIPv6
case "only-ipv4":
pref = preferIPOnlyIPv4
case "only-ipv6":
pref = preferIPOnlyIPv6
default:
return nil, fmt.Errorf("unknown ip preference %s", ipPreference)
}
return &Telegram{
preferIP: pref,
}, nil
}
+47
View File
@@ -0,0 +1,47 @@
package dc
import (
"context"
"sync"
"time"
)
type updater struct {
wg sync.WaitGroup
logger Logger
period time.Duration
}
func (u *updater) Wait() {
u.wg.Wait()
}
func (u *updater) run(ctx context.Context, callback func() error) {
u.wg.Go(func() {
ticker := time.NewTicker(u.period)
defer func() {
ticker.Stop()
select {
case <-ticker.C:
default:
}
}()
for {
u.logger.Info("start update")
if err := callback(); err != nil {
u.logger.WarningError("cannot update", err)
}
u.logger.Info("updated")
select {
case <-ctx.Done():
u.logger.Info("stop updating")
return
case <-ticker.C:
}
}
})
}
+55
View File
@@ -0,0 +1,55 @@
package dc
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type UpdaterTestSuite struct {
UpdaterTestSuiteBase
u updater
}
func (s *UpdaterTestSuite) SetupTest() {
s.UpdaterTestSuiteBase.SetupTest()
s.u = updater{
logger: s.loggerMock,
period: 100 * time.Millisecond,
}
}
func (s *UpdaterTestSuite) TestPeriodicUpdates() {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
lock := &sync.Mutex{}
collected := []time.Time{}
go s.u.run(s.ctx, func() error {
select {
case <-s.ctx.Done():
case value := <-ticker.C:
lock.Lock()
collected = append(collected, value)
lock.Unlock()
}
return nil
})
s.Eventually(func() bool {
lock.Lock()
defer lock.Unlock()
return len(collected) == 3
}, time.Second, 10*time.Millisecond)
}
func TestUpdater(t *testing.T) {
t.Parallel()
suite.Run(t, &UpdaterTestSuite{})
}
+23
View File
@@ -0,0 +1,23 @@
package dc
type dcView struct {
publicConfigs dcAddrSet
}
func (d dcView) getV4(dc int) []Addr {
var addrs []Addr
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
addrs = append(addrs, d.publicConfigs.getV4(dc)...)
return addrs
}
func (d dcView) getV6(dc int) []Addr {
var addrs []Addr
addrs = append(addrs, defaultDCAddrSet.getV6(dc)...)
addrs = append(addrs, d.publicConfigs.getV6(dc)...)
return addrs
}
+81
View File
@@ -0,0 +1,81 @@
package dc
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ViewTestSuite struct {
suite.Suite
view dcView
}
func (suite *ViewTestSuite) SetupSuite() {
suite.view = dcView{
publicConfigs: dcAddrSet{
v4: map[int][]Addr{
111: {
{Network: "tcp4", Address: "127.0.0.1:443"},
},
203: {
{Network: "tcp4", Address: "127.0.0.2:443"},
},
},
v6: map[int][]Addr{
203: {
{Network: "tcp6", Address: "xxx"},
},
},
},
}
}
func (suite *ViewTestSuite) TestGetV4() {
testData := map[int][]Addr{
111: {
{Network: "tcp4", Address: "127.0.0.1:443"},
},
203: {
{Network: "tcp4", Address: "127.0.0.2:443"},
{Network: "tcp4", Address: "91.105.192.100:443"},
},
2: {
{Network: "tcp4", Address: "149.154.167.51:443"},
{Network: "tcp4", Address: "95.161.76.100:443"},
},
}
for dc, addresses := range testData {
suite.T().Run(fmt.Sprintf("dc%d", dc), func(t *testing.T) {
assert.ElementsMatch(t, addresses, suite.view.getV4(dc))
})
}
}
func (suite *ViewTestSuite) TestGetV6() {
testData := map[int][]Addr{
111: {},
203: {
{Network: "tcp6", Address: "xxx"},
{Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
},
1: {
{Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
},
}
for dc, addresses := range testData {
suite.T().Run(fmt.Sprintf("dc%d", dc), func(t *testing.T) {
assert.ElementsMatch(t, addresses, suite.view.getV6(dc))
})
}
}
func TestView(t *testing.T) {
t.Parallel()
suite.Run(t, &ViewTestSuite{})
}
+118
View File
@@ -0,0 +1,118 @@
package doppel
import (
"bytes"
"context"
"sync"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
var doppelBufPool = sync.Pool{
New: func() any {
b := make([]byte, tls.MaxRecordSize)
return &b
},
}
type Conn struct {
essentials.Conn
p *connPayload
}
type connPayload struct {
ctx context.Context
ctxCancel context.CancelCauseFunc
stats Stats
wg sync.WaitGroup
writeStream bytes.Buffer
writtenCond sync.Cond
done bool
}
func (c Conn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, context.Cause(c.p.ctx)
}
c.p.writtenCond.L.Lock()
c.p.writeStream.Write(p)
c.p.writtenCond.L.Unlock()
c.p.writtenCond.Signal()
return len(p), context.Cause(c.p.ctx)
}
func (c Conn) start() {
bp := doppelBufPool.Get().(*[]byte)
buf := *bp
defer doppelBufPool.Put(bp)
timer := time.NewTimer(c.p.stats.Delay())
defer timer.Stop()
for {
select {
case <-c.p.ctx.Done():
return
case <-timer.C:
timer.Reset(c.p.stats.Delay())
}
size := c.p.stats.Size()
c.p.writtenCond.L.Lock()
for c.p.writeStream.Len() == 0 && !c.p.done {
c.p.writtenCond.Wait()
}
n, _ := c.p.writeStream.Read(buf[tls.SizeHeader : tls.SizeHeader+size])
c.p.writtenCond.L.Unlock()
if n == 0 {
continue
}
if err := tls.WriteRecordInPlace(c.Conn, buf, n); err != nil {
c.p.ctxCancel(err)
return
}
}
}
func (c Conn) Stop() {
c.p.ctxCancel(nil)
c.p.writtenCond.L.Lock()
c.p.done = true
c.p.writtenCond.L.Unlock()
c.p.writtenCond.Broadcast()
c.p.wg.Wait()
}
func NewConn(ctx context.Context, conn essentials.Conn, stats Stats) Conn {
ctx, cancel := context.WithCancelCause(ctx)
rv := Conn{
Conn: conn,
p: &connPayload{
ctx: ctx,
ctxCancel: cancel,
stats: stats,
writtenCond: sync.Cond{
L: &sync.Mutex{},
},
},
}
rv.p.writeStream.Grow(tls.DefaultBufferSize)
rv.p.wg.Go(func() {
rv.start()
})
return rv
}

Some files were not shown because too many files have changed in this diff Show More