REPOSITORY / ScuroNeko/mtg

Compare commits

DIFF REPOSITORY

Compare commits

...
74 Commits
Author SHA1 Message Date
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
88 changed files with 2126 additions and 1427 deletions
+19 -1
View File
@@ -93,6 +93,9 @@ jobs:
name: Docker
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -103,9 +106,16 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: nineseconds/mtg
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@v3
@@ -128,6 +138,14 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
+41
View File
@@ -0,0 +1,41 @@
---
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
- name: Check for vulnerabilities
run: |
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
+15 -17
View File
@@ -1,5 +1,11 @@
[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"
@@ -14,14 +20,17 @@ run = "go build"
description = "Update dependencies"
run = [
"go get -u",
"go mod tidy -go=1.25"
"go mod tidy -go=1.26"
]
[tasks.lint]
description = "Run linter"
tools.golangci-lint = "latest"
run = "golangci-lint run"
[tasks.vuln]
description = "Test for vulnerabilities"
run = "govulncheck ./..."
[tasks.test]
description = "Run tests"
run = "go test -v ./..."
@@ -43,19 +52,11 @@ run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/f
[tasks."test:fuzz:client-handshake"]
description = "Run fuzzy test for ClientHandshake"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHandshake ./mtglib/internal/obfuscated2"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/internal/obfuscation"
[tasks."test:fuzz:server-generate-handshake-frame"]
description = "Run fuzzy test for ServerGenerateHandshakeFrame"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerGenerateHandshakeFrame ./mtglib/internal/obfuscated2"
[tasks."test:fuzz:server-receive"]
description = "Run fuzzy test for ServerReceive"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerReceive ./mtglib/internal/obfuscated2"
[tasks."test:fuzz:server-send"]
description = "Run fuzzy test for ServerSend"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerSend ./mtglib/internal/obfuscated2"
[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"
@@ -75,7 +76,6 @@ go build \
[tasks.release]
description = "Create release tarballs"
tools.goreleaser = "latest"
sources = ["**/*.go", "go.mod", "go.sum", ".goreleaser.yml"]
run = [
"goreleaser --snapshot --clean",
@@ -90,11 +90,9 @@ run = "docker buildx build --pull -t mtg ."
[tasks.docs]
description = "Run doc server"
tools."go:golang.org/x/pkgsite/cmd/pkgsite" = "latest"
run = "pkgsite -http 0.0.0.0:10000"
[tasks.fmt]
description = "Reformat source code"
tools.gofumpt = "latest"
sources = ["**/*.go"]
run = "gofumpt -w --extra ."
+28 -3
View File
@@ -10,6 +10,17 @@ Highly-opinionated (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,7 @@ are the most notable:
* [Official](https://github.com/TelegramMessenger/MTProxy)
* [Python](https://github.com/alexbers/mtprotoproxy)
* [Erlang](https://github.com/seriyps/mtproto_proxy)
* [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
@@ -40,6 +52,12 @@ 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
@@ -162,6 +180,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
@@ -198,13 +222,14 @@ go install github.com/9seconds/mtg/v2@latest
```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
@@ -275,7 +300,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.
```
+2 -2
View File
@@ -2,7 +2,7 @@ package events
import (
"context"
"math/rand"
"math/rand/v2"
"runtime"
"github.com/9seconds/mtg/v2/mtglib"
@@ -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 {
+27 -54
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,11 @@ 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()
obs.EventReplayAttack(evt)
}(v)
wg.Go(func() {
v.EventReplayAttack(evt)
})
}
wg.Wait()
@@ -132,14 +108,11 @@ func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
func (m multiObserver) EventIPListSize(evt mtglib.EventIPListSize) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventIPListSize(evt)
}(v)
wg.Go(func() {
v.EventIPListSize(evt)
})
}
wg.Wait()
+59 -10
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,35 @@ concurrency = 8192
# Only ipv4 connectivity is used
prefer-ip = "prefer-ipv6"
# 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 +99,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 +133,8 @@ 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.
doh-ip = "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
+7 -3
View File
@@ -1,6 +1,6 @@
module github.com/9seconds/mtg/v2
go 1.25
go 1.26
require (
github.com/OneOfOne/xxhash v1.2.8
@@ -12,10 +12,9 @@ require (
github.com/jarcoal/httpmock v1.0.8
github.com/mccutchen/go-httpbin v1.1.1
github.com/panjf2000/ants/v2 v2.11.5
github.com/pelletier/go-toml v1.9.5
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/rs/zerolog v1.34.0
github.com/smira/go-statsd v1.3.4
github.com/stretchr/objx v0.5.2 // indirect
@@ -28,6 +27,8 @@ require (
)
require (
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pires/go-proxyproto v0.11.0
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
github.com/yl2chen/cidranger v1.0.2
)
@@ -36,6 +37,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.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
@@ -43,8 +45,10 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // 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.3 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+14 -10
View File
@@ -30,8 +30,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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=
@@ -55,8 +55,10 @@ github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl
github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
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.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -66,10 +68,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
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.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/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/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
@@ -106,8 +108,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -140,8 +143,9 @@ 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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
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=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+5 -11
View File
@@ -61,11 +61,8 @@ func (a *Access) Run(cli *CLI, version string) error {
}
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
wg.Go(func() {
ip := a.PublicIPv4
if ip == nil {
ip = a.getIP(ntw, "tcp4")
@@ -76,11 +73,8 @@ 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")
@@ -91,7 +85,7 @@ func (a *Access) Run(cli *CLI, version string) error {
}
resp.IPv6 = a.makeURLs(conf, ip)
}()
})
wg.Wait()
@@ -129,7 +123,7 @@ func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
defer func() {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}()
data, err := io.ReadAll(resp.Body)
+16 -3
View File
@@ -10,6 +10,7 @@ import (
"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"
@@ -17,6 +18,7 @@ import (
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network"
"github.com/9seconds/mtg/v2/stats"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog"
"github.com/yl2chen/cidranger"
)
@@ -248,9 +250,12 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
IPAllowlist: allowlist,
EventStream: eventStream,
Secret: conf.Secret,
DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort),
PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP),
Secret: conf.Secret,
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,
@@ -266,6 +271,14 @@ 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
+8 -1
View File
@@ -18,7 +18,8 @@ type SimpleRun struct {
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
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
@@ -47,6 +48,12 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funle
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)
}
+40 -9
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"net"
"github.com/9seconds/mtg/v2/mtglib"
)
@@ -21,15 +22,24 @@ 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"`
DomainFronting struct {
IP TypeIP `json:"ip"`
Port TypePort `json:"port"`
ProxyProtocol TypeBool `json:"proxyProtocol"`
} `json:"domainFronting"`
Defense struct {
AntiReplay struct {
Optional
@@ -66,6 +76,27 @@ type Config struct {
} `json:"stats"`
}
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())
+19 -10
View File
@@ -5,19 +5,28 @@ 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"`
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"`
+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)
+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{})
}
+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()
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (h httpFile) Open(ctx context.Context) (io.ReadCloser, error) {
if err != nil {
if response != nil {
io.Copy(io.Discard, response.Body) //nolint: errcheck
response.Body.Close() //nolint: errcheck
response.Body.Close() //nolint: errcheck
}
return nil, fmt.Errorf("cannot get url %s: %w", h.url, err)
+4 -7
View File
@@ -112,18 +112,15 @@ func (f *Firehol) update() {
defer cancel()
wg := &sync.WaitGroup{}
wg.Add(len(f.blocklists))
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)
@@ -135,7 +132,7 @@ func (f *Firehol) update() {
if err := f.updateFromFile(mutex, ranger, bufio.NewScanner(fileContent)); err != nil {
logger.WarningError("update has failed", err)
}
}(v)
})
}
wg.Wait()
+1 -1
View File
@@ -8,5 +8,5 @@ package logger
// 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...))
}
+42
View File
@@ -0,0 +1,42 @@
[[tools.go]]
version = "1.26.0"
backend = "core:go"
"platforms.linux-arm64" = { checksum = "sha256:bd03b743eb6eb4193ea3c3fd3956546bf0e3ca5b7076c8226334afe6b75704cd", url = "https://dl.google.com/go/go1.26.0.linux-arm64.tar.gz"}
"platforms.linux-x64" = { checksum = "sha256:aac1b08a0fb0c4e0a7c1555beb7b59180b05dfc5a3d62e40e9de90cd42f88235", url = "https://dl.google.com/go/go1.26.0.linux-amd64.tar.gz"}
"platforms.macos-arm64" = { checksum = "sha256:b1640525dfe68f066d56f200bef7bf4dce955a1a893bd061de6754c211431023", url = "https://dl.google.com/go/go1.26.0.darwin-arm64.tar.gz"}
"platforms.macos-x64" = { checksum = "sha256:1ca28b7703cbea05a65b2a1d92d6b308610ef92f8824578a0874f2e60c9d5a22", url = "https://dl.google.com/go/go1.26.0.darwin-amd64.tar.gz"}
"platforms.windows-x64" = { checksum = "sha256:9bbe0fc64236b2b51f6255c05c4232532b8ecc0e6d2e00950bd3021d8a4d07d4", url = "https://dl.google.com/go/go1.26.0.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.10.1"
backend = "aqua:golangci/golangci-lint"
"platforms.linux-arm64" = { checksum = "sha256:6652b42ae02915eb2f9cb2a2e0cac99514c8eded8388d88ae3e06e1a52c00de8", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-arm64.tar.gz"}
"platforms.linux-x64" = { checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz"}
"platforms.macos-arm64" = { checksum = "sha256:03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-arm64.tar.gz"}
"platforms.macos-x64" = { checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz"}
"platforms.windows-x64" = { checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3", url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip"}
[[tools.goreleaser]]
version = "2.14.1"
backend = "aqua:goreleaser/goreleaser"
"platforms.linux-arm64" = { checksum = "sha256:a84d3b27f052c12ad5c8342d7caf1450a7174a305730aed21d72db09301e49a5", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_arm64.tar.gz"}
"platforms.linux-x64" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"}
"platforms.macos-arm64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
"platforms.macos-x64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
"platforms.windows-x64" = { checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip"}
+38 -10
View File
@@ -3,10 +3,12 @@ package mtglib
import (
"bytes"
"context"
"fmt"
"io"
"sync"
"net"
"github.com/9seconds/mtg/v2/essentials"
"github.com/pires/go-proxyproto"
)
type connTraffic struct {
@@ -40,22 +42,15 @@ func (c connTraffic) Write(b []byte) (int, error) {
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 +62,36 @@ 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(),
}
}
+96
View File
@@ -1,14 +1,17 @@
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"
)
@@ -200,6 +203,94 @@ 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())
}
func TestConnTraffic(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTrafficTestSuite{})
@@ -209,3 +300,8 @@ func TestConnRewind(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnRewindTestSuite{})
}
func TestConnProxyProtocol(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnProxyProtocolTestSuite{})
}
+1 -1
View File
@@ -249,7 +249,7 @@ 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)
+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
}
+84
View File
@@ -0,0 +1,84 @@
package dc
import (
"context"
"time"
)
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 = dcAddrSet{
v4: map[int][]Addr{
1: {
{Network: "tcp4", Address: "149.154.175.50:443"},
},
2: {
{Network: "tcp4", Address: "149.154.167.51:443"},
{Network: "tcp4", Address: "95.161.76.100:443"},
},
3: {
{Network: "tcp4", Address: "149.154.175.100:443"},
},
4: {
{Network: "tcp4", Address: "149.154.167.91:443"},
},
5: {
{Network: "tcp4", Address: "149.154.171.5:443"},
},
203: {
{Network: "tcp4", Address: "91.105.192.100:443"},
},
},
v6: map[int][]Addr{
1: {
{Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
},
2: {
{Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"},
},
3: {
{Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"},
},
4: {
{Network: "tcp6", Address: "[2001:67c:04e8:f004::a]:443"},
},
5: {
{Network: "tcp6", Address: "[2001:b28:f23f:f005::a]:443"},
},
203: {
{Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
},
},
}
+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.getV4(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{})
}
+19
View File
@@ -0,0 +1,19 @@
package dc
type dcView struct {
publicConfigs dcAddrSet
}
func (d dcView) getV4(dc int) []Addr {
addrs := d.publicConfigs.getV4(dc)
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
return addrs
}
func (d dcView) getV6(dc int) []Addr {
addrs := d.publicConfigs.getV6(dc)
addrs = append(addrs, defaultDCAddrSet.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{})
}
+1 -1
View File
@@ -76,7 +76,7 @@ func ParseClientHello(secret, handshake []byte) (ClientHello, error) {
computedRandom := mac.Sum(nil)
for i := 0; i < RandomLen; i++ {
for i := range RandomLen {
computedRandom[i] ^= hello.Random[i]
}
+11 -12
View File
@@ -3,7 +3,7 @@ package faketls
import (
"bytes"
"fmt"
"math/rand"
"math/rand/v2"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
@@ -47,27 +47,26 @@ func (c *Conn) Write(p []byte) (int, error) {
rec.Type = record.TypeApplicationData
rec.Version = record.Version12
sendBuffer := acquireBytesBuffer()
defer releaseBytesBuffer(sendBuffer)
lenP := len(p)
written := 0
for len(p) > 0 {
chunkSize := rand.Intn(record.TLSMaxRecordSize)
chunkSize := rand.IntN(record.TLSMaxRecordSize)
if chunkSize > len(p) || chunkSize == 0 {
chunkSize = len(p)
}
rec.Payload.Reset()
rec.Payload.Write(p[:chunkSize])
rec.Dump(sendBuffer) //nolint: errcheck
err := rec.Dump(c.Conn)
written += chunkSize
if err != nil {
return written, err
}
p = p[chunkSize:]
}
if _, err := c.Conn.Write(sendBuffer.Bytes()); err != nil {
return 0, err //nolint: wrapcheck
}
return lenP, nil
return written, nil
}
+2 -2
View File
@@ -2,9 +2,9 @@ package faketls_test
import (
"bytes"
"crypto/rand"
"errors"
"io"
"math/rand"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
@@ -123,7 +123,7 @@ func (suite *ConnTestSuite) TestWrite() {
suite.connMock.On("Write", mock.Anything).Return(0, nil)
dataToRec := make([]byte, record.TLSMaxRecordSize*2)
rand.Read(dataToRec) //nolint: staticcheck
rand.Read(dataToRec) //nolint: staticcheck, errcheck
n, err := suite.c.Write(dataToRec)
suite.NoError(err)
-21
View File
@@ -1,21 +0,0 @@
package faketls
import (
"bytes"
"sync"
)
var bytesBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func acquireBytesBuffer() *bytes.Buffer {
return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert
}
func releaseBytesBuffer(b *bytes.Buffer) {
b.Reset()
bytesBufferPool.Put(b)
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
)
var recordPool = sync.Pool{
New: func() interface{} {
New: func() any {
return &Record{}
},
}
+5 -6
View File
@@ -1,20 +1,20 @@
package faketls
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"io"
mrand "math/rand"
mrand "math/rand/v2"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"golang.org/x/crypto/curve25519"
)
func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error {
buf := acquireBytesBuffer()
defer releaseBytesBuffer(buf)
buf := &bytes.Buffer{}
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
@@ -36,7 +36,7 @@ func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello)
rec.Type = record.TypeApplicationData
rec.Version = record.Version12
if _, err := io.CopyN(&rec.Payload, rand.Reader, int64(1024+mrand.Intn(3092))); err != nil {
if _, err := io.CopyN(&rec.Payload, rand.Reader, int64(1024+mrand.IntN(3092))); err != nil {
panic(err)
}
@@ -58,8 +58,7 @@ func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello)
}
func generateServerHello(writer io.Writer, clientHello ClientHello) {
bodyBuf := acquireBytesBuffer()
defer releaseBytesBuffer(bodyBuf)
bodyBuf := &bytes.Buffer{}
sliceBuf := [2]byte{}
digest := [RandomLen]byte{}
+1 -1
View File
@@ -3,8 +3,8 @@ package faketls_test
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"math/rand"
"testing"
"time"
@@ -1,54 +0,0 @@
package obfuscated2
import (
"crypto/cipher"
"crypto/subtle"
"encoding/hex"
"fmt"
"io"
)
type clientHandhakeFrame struct {
handshakeFrame
}
func (c *clientHandhakeFrame) decryptor(secret []byte) cipher.Stream {
hasher := acquireSha256Hasher()
defer releaseSha256Hasher(hasher)
hasher.Write(c.key())
hasher.Write(secret)
return makeAesCtr(hasher.Sum(nil), c.iv())
}
func (c *clientHandhakeFrame) encryptor(secret []byte) cipher.Stream {
invertedHandshake := c.invert()
hasher := acquireSha256Hasher()
defer releaseSha256Hasher(hasher)
hasher.Write(invertedHandshake.key())
hasher.Write(secret)
return makeAesCtr(hasher.Sum(nil), invertedHandshake.iv())
}
func ClientHandshake(secret []byte, reader io.Reader) (int, cipher.Stream, cipher.Stream, error) {
handshake := clientHandhakeFrame{}
if _, err := io.ReadFull(reader, handshake.data[:]); err != nil {
return 0, nil, nil, fmt.Errorf("cannot read frame: %w", err)
}
decryptor := handshake.decryptor(secret)
encryptor := handshake.encryptor(secret)
decryptor.XORKeyStream(handshake.data[:], handshake.data[:])
if val := handshake.connectionType(); subtle.ConstantTimeCompare(handshakeConnectionType, val) != 1 {
return 0, nil, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val))
}
return handshake.dc(), encryptor, decryptor, nil
}
@@ -1,32 +0,0 @@
package obfuscated2
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
var FuzzClientHandshakeSecret = []byte{1, 2, 3}
func FuzzClientHandshake(f *testing.F) {
f.Add([]byte{1, 2, 3})
f.Fuzz(func(t *testing.T, frame []byte) {
data := bytes.NewReader(frame)
if _, _, _, err := ClientHandshake(FuzzClientHandshakeSecret, data); err != nil {
return
}
handshake := clientHandhakeFrame{}
require.Len(t, frame, handshakeFrameLen)
copy(handshake.data[:], frame)
decryptor := handshake.decryptor(FuzzClientHandshakeSecret)
decryptor.XORKeyStream(handshake.data[:], handshake.data[:])
require.Equal(t, handshakeConnectionType, handshake.connectionType())
})
}
@@ -1,89 +0,0 @@
package obfuscated2_test
import (
"bytes"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ClientHandshakeTestSuite struct {
suite.Suite
SnapshotTestSuite
}
func (suite *ClientHandshakeTestSuite) SetupSuite() {
suite.NoError(suite.IngestSnapshots(".", "client-handshake-snapshot-"))
}
func (suite *ClientHandshakeTestSuite) TestCannotRead() {
buf := bytes.NewBuffer([]byte{1, 2, 3})
_, _, _, err := obfuscated2.ClientHandshake([]byte{1, 2, 3}, buf) //nolint: dogsled
suite.Error(err)
}
func (suite *ClientHandshakeTestSuite) TestOk() {
for nameV, snapshotV := range suite.snapshots {
snapshot := snapshotV
suite.T().Run(nameV, func(t *testing.T) {
buf := bytes.NewBuffer(snapshot.Frame.data)
dc, encryptor, decryptor, err := obfuscated2.ClientHandshake(
snapshot.Secret.data, buf)
assert.NoError(t, err)
assert.EqualValues(t, snapshot.DC, dc)
writeData := make([]byte, len(snapshot.Encrypted.Text.data))
readData := make([]byte, len(snapshot.Decrypted.Text.data))
connMock := &testlib.EssentialsConnMock{}
connMock.On("Read", mock.Anything).
Once().
Return(len(snapshot.Decrypted.Text.data), nil).
Run(func(args mock.Arguments) {
arr, ok := args.Get(0).([]byte)
suite.True(ok)
copy(arr, snapshot.Decrypted.Cipher.data)
})
connMock.On("Write", mock.Anything).
Once().
Return(len(snapshot.Encrypted.Text.data), nil).
Run(func(args mock.Arguments) {
arr, ok := args.Get(0).([]byte)
suite.True(ok)
copy(writeData, arr)
})
conn := obfuscated2.Conn{
Conn: connMock,
Encryptor: encryptor,
Decryptor: decryptor,
}
n, err := conn.Read(readData)
assert.Equal(t, len(readData), n)
assert.NoError(t, err)
assert.Equal(t, snapshot.Decrypted.Text.data, readData)
n, err = conn.Write(snapshot.Encrypted.Text.data)
assert.Equal(t, len(writeData), n)
assert.NoError(t, err)
assert.Equal(t, snapshot.Encrypted.Cipher.data, writeData)
connMock.AssertExpectations(t)
})
}
}
func TestClientHandshake(t *testing.T) {
t.Parallel()
suite.Run(t, &ClientHandshakeTestSuite{})
}
-37
View File
@@ -1,37 +0,0 @@
package obfuscated2
import (
"crypto/cipher"
"github.com/9seconds/mtg/v2/essentials"
)
type Conn struct {
essentials.Conn
Encryptor cipher.Stream
Decryptor cipher.Stream
}
func (c Conn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if err != nil {
return n, err //nolint: wrapcheck
}
c.Decryptor.XORKeyStream(p, p[:n])
return n, nil
}
func (c Conn) Write(p []byte) (int, error) {
buf := acquireBytesBuffer()
defer releaseBytesBuffer(buf)
buf.Write(p)
payload := buf.Bytes()
c.Encryptor.XORKeyStream(payload, payload)
return c.Conn.Write(payload) //nolint: wrapcheck
}
@@ -1,71 +0,0 @@
package obfuscated2
const (
// DefaultDC defines a number of the default DC to use. This value used
// only if a value from obfuscated2 handshake frame is 0 (default).
DefaultDC = 2
handshakeFrameLen = 64
handshakeFrameLenKey = 32
handshakeFrameLenIV = 16
handshakeFrameLenConnectionType = 4
handshakeFrameOffsetStart = 8
handshakeFrameOffsetKey = handshakeFrameOffsetStart
handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey
handshakeFrameOffsetConnectionType = handshakeFrameOffsetIV + handshakeFrameLenIV
handshakeFrameOffsetDC = handshakeFrameOffsetConnectionType + handshakeFrameLenConnectionType
)
// Connection-Type: Secure. We support only fake tls.
var handshakeConnectionType = []byte{0xdd, 0xdd, 0xdd, 0xdd}
// A structure of obfuscated2 handshake frame is following:
//
// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd].
//
// - 8 bytes of noise
// - 32 bytes of AES Key
// - 16 bytes of AES IV
// - 4 bytes of 'connection type' - this has some setting like a connection type
// - 2 bytes of 'DC'. DC is little endian int16
// - 2 bytes of noise
type handshakeFrame struct {
data [handshakeFrameLen]byte
}
func (h *handshakeFrame) dc() int {
idx := int16(h.data[handshakeFrameOffsetDC]) | int16(h.data[handshakeFrameOffsetDC+1])<<8 //nolint: lll // little endian for int16 is here
switch {
case idx > 0:
return int(idx)
case idx < 0:
return -int(idx)
default:
return DefaultDC
}
}
func (h *handshakeFrame) key() []byte {
return h.data[handshakeFrameOffsetKey:handshakeFrameOffsetIV]
}
func (h *handshakeFrame) iv() []byte {
return h.data[handshakeFrameOffsetIV:handshakeFrameOffsetConnectionType]
}
func (h *handshakeFrame) connectionType() []byte {
return h.data[handshakeFrameOffsetConnectionType:handshakeFrameOffsetDC]
}
func (h *handshakeFrame) invert() handshakeFrame {
copyFrame := *h
for i := 0; i < handshakeFrameLenKey+handshakeFrameLenIV; i++ {
copyFrame.data[handshakeFrameOffsetKey+i] = h.data[handshakeFrameOffsetConnectionType-1-i]
}
return copyFrame
}
@@ -1,73 +0,0 @@
package obfuscated2
import (
"crypto/rand"
"encoding/base64"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type HandshakeFrameTestSuite struct {
suite.Suite
}
func (suite *HandshakeFrameTestSuite) Decode(value string) []byte {
v, err := base64.RawStdEncoding.DecodeString(value)
suite.NoError(err)
return v
}
func (suite *HandshakeFrameTestSuite) Encode(value []byte) string {
return base64.RawStdEncoding.EncodeToString(value)
}
func (suite *HandshakeFrameTestSuite) TestOk() {
hf := handshakeFrame{}
testFrame := suite.Decode(
"L9TmCzzxl9bPKODBpZeVM/qqNUxQ/axxBup1S2ymbIfUd6f7YSyzzM9EmTFv2/XzGqJGEHuj2zofmUGBLghu5g")
copy(hf.data[:], testFrame)
suite.Equal("zyjgwaWXlTP6qjVMUP2scQbqdUtspmyH1Hen+2Ess8w", suite.Encode(hf.key()))
suite.Equal("z0SZMW/b9fMaokYQe6PbOg", suite.Encode(hf.iv()))
suite.Equal("H5lBgQ", suite.Encode(hf.connectionType()))
suite.EqualValues(2094, hf.dc())
inverted := hf.invert()
suite.Equal("OtujexBGohrz9dtvMZlEz8yzLGH7p3fUh2ymbEt16gY", suite.Encode(inverted.key()))
suite.Equal("caz9UEw1qvozlZelweAozw", suite.Encode(inverted.iv()))
suite.Equal("H5lBgQ", suite.Encode(inverted.connectionType()))
suite.EqualValues(2094, inverted.dc())
}
func (suite *HandshakeFrameTestSuite) TestDC() {
testData := map[int16]int{
1: 1,
-1: 1,
0: DefaultDC,
}
for k, v := range testData {
incoming := k
expected := v
suite.T().Run(strconv.Itoa(int(incoming)), func(t *testing.T) {
frame := handshakeFrame{}
rand.Read(frame.data[:]) //nolint: errcheck
frame.data[handshakeFrameOffsetDC] = byte(incoming)
frame.data[handshakeFrameOffsetDC+1] = byte(incoming >> 8)
assert.Equal(t, expected, frame.dc())
})
}
}
func TestHandshakeFrame(t *testing.T) {
t.Parallel()
suite.Run(t, &HandshakeFrameTestSuite{})
}
-137
View File
@@ -1,137 +0,0 @@
package obfuscated2_test
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2"
"github.com/stretchr/testify/require"
)
type snapshotBytes struct {
data []byte
}
func (s snapshotBytes) MarshalText() ([]byte, error) {
if len(s.data) == 0 {
return nil, nil
}
return []byte(base64.RawStdEncoding.EncodeToString(s.data)), nil
}
func (s *snapshotBytes) UnmarshalText(data []byte) error {
val, err := base64.RawStdEncoding.DecodeString(string(data))
if err != nil {
return fmt.Errorf("cannot unmarshal %v: %w", len(val), err)
}
s.data = val
return nil
}
type Obfuscated2Snapshot struct {
Secret snapshotBytes `json:"secret"`
Frame snapshotBytes `json:"frame"`
DC int16 `json:"dc"`
Encrypted struct {
Text snapshotBytes `json:"text"`
Cipher snapshotBytes `json:"cipher"`
} `json:"encrypted"`
Decrypted struct {
Text snapshotBytes `json:"text"`
Cipher snapshotBytes `json:"cipher"`
} `json:"decrypted"`
}
type SnapshotTestSuite struct {
snapshots map[string]*Obfuscated2Snapshot
}
type ServerHandshakeTestData struct {
connMock *testlib.EssentialsConnMock
proxyConn obfuscated2.Conn
encryptor cipher.Stream
decryptor cipher.Stream
}
func (suite *SnapshotTestSuite) IngestSnapshots(dirname, namePrefix string) error {
suite.snapshots = map[string]*Obfuscated2Snapshot{}
files, err := os.ReadDir(filepath.Join("testdata", dirname))
if err != nil {
return fmt.Errorf("cannot ingest snapshots: %w", err)
}
for _, v := range files {
if !strings.HasPrefix(v.Name(), namePrefix) {
continue
}
filename := filepath.Join("testdata", dirname, v.Name())
contents, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("cannot read %s: %w", filename, err)
}
value := &Obfuscated2Snapshot{}
if err := json.Unmarshal(contents, value); err != nil {
return fmt.Errorf("cannot unmarshal %s: %w", filename, err)
}
suite.snapshots[v.Name()] = value
}
return nil
}
func NewServerHandshakeTestData(t *testing.T) ServerHandshakeTestData {
buf := &bytes.Buffer{}
connMock := &testlib.EssentialsConnMock{}
handshakeEnc, handshakeDec, err := obfuscated2.ServerHandshake(buf)
require.NoError(t, err)
serverEncrypted := buf.Bytes()
decBlock, _ := aes.NewCipher(serverEncrypted[8 : 8+32])
decryptor := cipher.NewCTR(decBlock, serverEncrypted[8+32:8+32+16])
serverDecrypted := make([]byte, len(serverEncrypted))
decryptor.XORKeyStream(serverDecrypted, serverEncrypted)
require.Equal(t, "3d3d3Q",
base64.RawStdEncoding.EncodeToString(serverDecrypted[8+32+16:8+32+16+4]))
serverEncryptedReverted := make([]byte, len(serverEncrypted))
for i := 0; i < 32+16; i++ {
serverEncryptedReverted[8+i] = serverEncrypted[8+32+16-1-i]
}
encBlock, _ := aes.NewCipher(serverEncryptedReverted[8 : 8+32])
encryptor := cipher.NewCTR(encBlock, serverEncryptedReverted[8+32:8+32+16])
return ServerHandshakeTestData{
connMock: connMock,
proxyConn: obfuscated2.Conn{
Conn: connMock,
Encryptor: handshakeEnc,
Decryptor: handshakeDec,
},
encryptor: encryptor,
decryptor: decryptor,
}
}
-39
View File
@@ -1,39 +0,0 @@
package obfuscated2
import (
"bytes"
"crypto/sha256"
"hash"
"sync"
)
var (
sha256HasherPool = sync.Pool{
New: func() interface{} {
return sha256.New()
},
}
bytesBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
)
func acquireSha256Hasher() hash.Hash {
return sha256HasherPool.Get().(hash.Hash) //nolint: forcetypeassert
}
func releaseSha256Hasher(h hash.Hash) {
h.Reset()
sha256HasherPool.Put(h)
}
func acquireBytesBuffer() *bytes.Buffer {
return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert
}
func releaseBytesBuffer(buf *bytes.Buffer) {
buf.Reset()
bytesBufferPool.Put(buf)
}
@@ -1,67 +0,0 @@
package obfuscated2
import (
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
)
type serverHandshakeFrame struct {
handshakeFrame
}
func (s *serverHandshakeFrame) decryptor() cipher.Stream {
invertedHandshake := s.invert()
return makeAesCtr(invertedHandshake.key(), invertedHandshake.iv())
}
func (s *serverHandshakeFrame) encryptor() cipher.Stream {
return makeAesCtr(s.key(), s.iv())
}
func ServerHandshake(writer io.Writer) (cipher.Stream, cipher.Stream, error) {
handshake := generateServerHanshakeFrame()
copyHandshake := handshake
encryptor := handshake.encryptor()
decryptor := handshake.decryptor()
encryptor.XORKeyStream(handshake.data[:], handshake.data[:])
copy(handshake.key(), copyHandshake.key())
copy(handshake.iv(), copyHandshake.iv())
if _, err := writer.Write(handshake.data[:]); err != nil {
return nil, nil, fmt.Errorf("cannot send a handshake frame to telegram: %w", err)
}
return encryptor, decryptor, nil
}
func generateServerHanshakeFrame() serverHandshakeFrame {
frame := serverHandshakeFrame{}
for {
if _, err := rand.Read(frame.data[:]); err != nil {
panic(err)
}
if frame.data[0] == 0xef { // taken from tg sources
continue
}
switch binary.LittleEndian.Uint32(frame.data[:4]) {
case 0x44414548, 0x54534f50, 0x20544547, 0x4954504f, 0xeeeeeeee: // taken from tg sources
continue
}
if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 {
continue
}
copy(frame.connectionType(), handshakeConnectionType)
return frame
}
}
@@ -1,58 +0,0 @@
package obfuscated2_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func FuzzServerSend(f *testing.F) {
f.Add([]byte{1, 2, 3, 4, 5})
f.Fuzz(func(t *testing.T, data []byte) {
handshakeData := NewServerHandshakeTestData(t)
handshakeData.connMock.
On("Write", mock.Anything).
Return(len(data), nil).
Once().
Run(func(args mock.Arguments) {
message := make([]byte, len(data))
handshakeData.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert
assert.Equal(t, message, data)
})
n, err := handshakeData.proxyConn.Write(data)
assert.EqualValues(t, len(data), n)
assert.NoError(t, err)
handshakeData.connMock.AssertExpectations(t)
})
}
func FuzzServerReceive(f *testing.F) {
f.Add([]byte{1, 2, 3, 4, 5})
f.Fuzz(func(t *testing.T, data []byte) {
handshakeData := NewServerHandshakeTestData(t)
buffer := make([]byte, len(data))
handshakeData.connMock.
On("Read", mock.Anything).
Return(len(data), nil).
Once().
Run(func(args mock.Arguments) {
message := make([]byte, len(data))
handshakeData.encryptor.XORKeyStream(message, data)
copy(args.Get(0).([]byte), message) //nolint: forcetypeassert
})
n, err := handshakeData.proxyConn.Read(buffer)
assert.EqualValues(t, len(data), n)
assert.NoError(t, err)
assert.Equal(t, data, buffer)
handshakeData.connMock.AssertExpectations(t)
})
}
@@ -1,65 +0,0 @@
package obfuscated2_test
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ServerHandshakeTestSuite struct {
suite.Suite
data ServerHandshakeTestData
}
func (suite *ServerHandshakeTestSuite) SetupTest() {
suite.data = NewServerHandshakeTestData(suite.T())
}
func (suite *ServerHandshakeTestSuite) TearDownTest() {
suite.data.connMock.AssertExpectations(suite.T())
}
func (suite *ServerHandshakeTestSuite) TestSendToTelegram() {
messageToTelegram := []byte{10, 11, 12, 13, 14, 'a'}
suite.data.connMock.
On("Write", mock.Anything).
Return(len(messageToTelegram), nil).
Once().
Run(func(args mock.Arguments) {
message := make([]byte, len(messageToTelegram))
suite.data.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert
suite.Equal(messageToTelegram, message)
})
n, err := suite.data.proxyConn.Write(messageToTelegram)
suite.EqualValues(len(messageToTelegram), n)
suite.NoError(err)
}
func (suite *ServerHandshakeTestSuite) TestRecieveFromTelegram() {
messageFromTelegram := []byte{10, 11, 12, 13, 14, 'a'}
buffer := make([]byte, len(messageFromTelegram))
suite.data.connMock.
On("Read", mock.Anything).
Return(len(messageFromTelegram), nil).
Once().
Run(func(args mock.Arguments) {
message := make([]byte, len(messageFromTelegram))
suite.data.encryptor.XORKeyStream(message, messageFromTelegram)
copy(args.Get(0).([]byte), message) //nolint: forcetypeassert
})
n, err := suite.data.proxyConn.Read(buffer)
suite.EqualValues(len(messageFromTelegram), n)
suite.NoError(err)
suite.Equal(messageFromTelegram, buffer)
}
func TestServerHandshake(t *testing.T) {
t.Parallel()
suite.Run(t, &ServerHandshakeTestSuite{})
}
-15
View File
@@ -1,15 +0,0 @@
package obfuscated2
import (
"crypto/aes"
"crypto/cipher"
)
func makeAesCtr(key, iv []byte) cipher.Stream {
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
return cipher.NewCTR(block, iv)
}
+34
View File
@@ -0,0 +1,34 @@
package obfuscation
import (
"crypto/cipher"
"github.com/9seconds/mtg/v2/essentials"
)
type conn struct {
essentials.Conn
sendCipher cipher.Stream
recvCipher cipher.Stream
}
func (c conn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if err != nil {
return n, err
}
c.recvCipher.XORKeyStream(p, p[:n])
return n, nil
}
func (c conn) Write(p []byte) (int, error) {
// yes, this is a bit violent and goes against a contract in io.Writer
// but we do it to avoid creating a new buffer just to perform this
// encryption.
c.sendCipher.XORKeyStream(p, p)
return c.Conn.Write(p)
}
+102
View File
@@ -0,0 +1,102 @@
package obfuscation
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"testing"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ConnTestSuite struct {
suite.Suite
secret []byte
}
func (s *ConnTestSuite) SetupSuite() {
secret := [32]byte{}
s.secret = secret[:]
}
func (s *ConnTestSuite) TestRead() {
testData := map[string]string{
"data1": "b8f4b41993",
"": "",
"___": "83ca9f",
}
for incoming, outgoing := range testData {
s.T().Run(incoming, func(t *testing.T) {
connMock := &testlib.EssentialsConnMock{}
testConn := s.makeConn(connMock)
data := make([]byte, len(incoming))
connMock.On("Read", make([]byte, len(incoming))).Return(len(incoming), nil).Run(func(args mock.Arguments) {
arg := args.Get(0).([]byte)
copy(arg, []byte(incoming))
})
n, err := testConn.Read(data)
assert.Equal(t, len(data), n)
assert.NoError(t, err)
assert.Equal(t, outgoing, hex.EncodeToString(data))
connMock.AssertExpectations(t)
})
}
}
func (s *ConnTestSuite) TestWrite() {
testData := map[string]string{
"b8f4b41993": "data1",
"": "",
"83ca9f": "___",
}
for incoming, outgoing := range testData {
s.T().Run(incoming, func(t *testing.T) {
connMock := &testlib.EssentialsConnMock{}
testConn := s.makeConn(connMock)
toWrite, _ := hex.DecodeString(incoming)
data := make([]byte, len(toWrite))
connMock.On("Write", []byte(outgoing)).Return(len(toWrite), nil)
n, err := testConn.Write(toWrite)
assert.Equal(t, len(data), n)
assert.NoError(t, err)
connMock.AssertExpectations(t)
})
}
}
func (s *ConnTestSuite) makeConn(rawConn *testlib.EssentialsConnMock) essentials.Conn {
rblock, err := aes.NewCipher(s.secret)
if err != nil {
panic(err)
}
wblock, err := aes.NewCipher(s.secret)
if err != nil {
panic(err)
}
return conn{
Conn: rawConn,
sendCipher: cipher.NewCTR(wblock, s.secret[:aes.BlockSize]),
recvCipher: cipher.NewCTR(rblock, s.secret[:aes.BlockSize]),
}
}
func TestConn(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTestSuite{})
}
@@ -0,0 +1,111 @@
package obfuscation
import (
"crypto/rand"
"encoding/binary"
"slices"
)
// https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation
const (
// default DC is nothing is selected
defaultDC = 2
// the length of the handshake frame. Always 64 bytes
hfLen = 64
hfLenKey = 32
hfLenIV = 16
hfLenConnectionType = 4
// A structure of obfuscated handshake frame is following:
//
// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd].
//
// - 8 bytes of noise
// - 32 bytes of AES Key
// - 16 bytes of AES IV
// - 4 bytes of 'connection type' - this has some setting like a connection type
// - 2 bytes of 'DC'. DC is little endian int16
// - 2 bytes of noise
hfOffsetKey = 8
hfOffsetIV = hfOffsetKey + hfLenKey
hfOffsetConnectionType = hfOffsetIV + hfLenIV
hfOffsetDC = hfOffsetConnectionType + hfLenConnectionType
)
// Connection-Type: Secure. We support only fake tls.
var hfConnectionType = [hfLenConnectionType]byte{0xdd, 0xdd, 0xdd, 0xdd}
type handshakeFrame struct {
data [hfLen]byte
}
func (h *handshakeFrame) key() []byte {
return h.data[hfOffsetKey : hfOffsetKey+hfLenKey]
}
func (h *handshakeFrame) iv() []byte {
return h.data[hfOffsetIV : hfOffsetIV+hfLenIV]
}
func (h *handshakeFrame) connectionType() []byte {
return h.data[hfOffsetConnectionType : hfOffsetConnectionType+hfLenConnectionType]
}
func (h *handshakeFrame) dcSlice() []byte {
return h.data[hfOffsetDC : hfOffsetDC+2]
}
func (h *handshakeFrame) dc() int {
idx := int16(binary.LittleEndian.Uint16(h.dcSlice()))
switch {
case idx > 0:
return int(idx)
case idx < 0:
return -int(idx)
}
return defaultDC
}
func (h *handshakeFrame) revert() {
slices.Reverse(h.data[hfOffsetKey:hfOffsetConnectionType])
}
func generateHandshake(dc int) handshakeFrame {
frame := handshakeFrame{}
for {
if _, err := rand.Read(frame.data[:]); err != nil {
panic(err)
}
// https://github.com/tdlib/td/blob/master/td/mtproto/TcpTransport.cpp#L157-L158.
if frame.data[0] == 0xef { // abridged header
// https://core.telegram.org/mtproto/mtproto-transports#abridged
continue
}
switch binary.LittleEndian.Uint32(frame.data[:4]) {
case 0x44414548, // HEAD
0x54534f50, // POST
0x20544547, // GET
0x4954504f, // OPTI
0x02010316, // ????
0xdddddddd, // PaddedIntermediate header
0xeeeeeeee: // Intermediate header
continue
}
if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 {
continue
}
copy(frame.connectionType(), hfConnectionType[:])
binary.LittleEndian.PutUint16(frame.dcSlice(), uint16(dc))
return frame
}
}
@@ -1,4 +1,4 @@
package obfuscated2
package obfuscation
import (
"encoding/binary"
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
)
func FuzzServerGenerateHandshakeFrame(f *testing.F) {
f.Fuzz(func(t *testing.T, arg int) {
frame := generateServerHanshakeFrame()
func FuzzGenerateHandshakeFrame(f *testing.F) {
f.Fuzz(func(t *testing.T, arg int16) {
frame := generateHandshake(int(arg))
assert.NotEqualValues(t, 0xef, frame.data[0])
@@ -18,13 +18,23 @@ func FuzzServerGenerateHandshakeFrame(f *testing.F) {
assert.NotEqualValues(t, 0x54534f50, firstBytes)
assert.NotEqualValues(t, 0x20544547, firstBytes)
assert.NotEqualValues(t, 0x4954504f, firstBytes)
assert.NotEqualValues(t, 0x02010316, firstBytes)
assert.NotEqualValues(t, 0xeeeeeeee, firstBytes)
assert.NotEqualValues(t, 0xdddddddd, firstBytes)
assert.NotEqualValues(
t,
0,
frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7])
assert.Equal(t, handshakeConnectionType, frame.connectionType())
assert.Equal(t, hfConnectionType[:], frame.connectionType())
if arg < 0 {
arg = -arg
} else if arg == 0 {
arg = defaultDC
}
assert.EqualValues(t, arg, frame.dc())
})
}
@@ -0,0 +1,66 @@
package obfuscation
import (
"testing"
"github.com/stretchr/testify/suite"
)
type HandshakeFrameTestSuite struct {
suite.Suite
frame handshakeFrame
reverted handshakeFrame
}
func (h *HandshakeFrameTestSuite) SetupSuite() {
for i := range hfLen {
h.frame.data[i] = byte(i + 1)
h.reverted.data[i] = byte(hfLen - i)
}
}
func (h *HandshakeFrameTestSuite) TestKey() {
key := h.frame.key()
h.EqualValues(8+1, key[0])
h.EqualValues(8+hfLenKey, key[len(key)-1])
h.Len(key, hfLenKey)
}
func (h *HandshakeFrameTestSuite) TestIV() {
iv := h.frame.iv()
h.EqualValues(40+1, iv[0])
h.EqualValues(40+hfLenIV, iv[len(iv)-1])
h.Len(iv, hfLenIV)
}
func (h *HandshakeFrameTestSuite) TestConnectionType() {
connectionType := h.frame.connectionType()
h.EqualValues(56+1, connectionType[0])
h.EqualValues(56+hfLenConnectionType, connectionType[len(connectionType)-1])
h.Len(connectionType, hfLenConnectionType)
}
func (h *HandshakeFrameTestSuite) TestDCSlice() {
dcSlice := h.frame.dcSlice()
h.EqualValues(61, dcSlice[0])
h.EqualValues(61+1, dcSlice[1])
h.Len(dcSlice, 2)
}
func (h *HandshakeFrameTestSuite) TestDC() {
h.Equal(15933, h.frame.dc())
}
func (h *HandshakeFrameTestSuite) TestRevert() {
fr := h.frame
fr.revert()
h.Equal(h.reverted.key(), fr.key())
h.Equal(h.reverted.iv(), fr.iv())
}
func TestHandshakeFrame(t *testing.T) {
t.Parallel()
suite.Run(t, &HandshakeFrameTestSuite{})
}
+79
View File
@@ -0,0 +1,79 @@
package obfuscation_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type snapshotBytes struct {
data []byte
}
func (s snapshotBytes) MarshalText() ([]byte, error) {
if len(s.data) == 0 {
return nil, nil
}
return []byte(base64.RawStdEncoding.EncodeToString(s.data)), nil
}
func (s *snapshotBytes) UnmarshalText(data []byte) error {
val, err := base64.RawStdEncoding.DecodeString(string(data))
if err != nil {
return fmt.Errorf("cannot unmarshal %v: %w", len(val), err)
}
s.data = val
return nil
}
type ObfuscatedSnapshot struct {
Secret snapshotBytes `json:"secret"`
Frame snapshotBytes `json:"frame"`
DC int16 `json:"dc"`
Encrypted struct {
Text snapshotBytes `json:"text"`
Cipher snapshotBytes `json:"cipher"`
} `json:"encrypted"`
Decrypted struct {
Text snapshotBytes `json:"text"`
Cipher snapshotBytes `json:"cipher"`
} `json:"decrypted"`
}
type SnapshotTestSuite struct {
suite.Suite
snapshots map[string]*ObfuscatedSnapshot
}
func (s *SnapshotTestSuite) Setup(dirname, namePrefix string) {
s.snapshots = make(map[string]*ObfuscatedSnapshot)
files, err := os.ReadDir("testdata")
require.NoError(s.T(), err)
for _, v := range files {
if !strings.HasPrefix(v.Name(), namePrefix) {
continue
}
filename := filepath.Join("testdata", v.Name())
contents, err := os.ReadFile(filename)
require.NoError(s.T(), err)
value := &ObfuscatedSnapshot{}
require.NoError(s.T(), json.Unmarshal(contents, value))
s.snapshots[v.Name()] = value
}
}
+87
View File
@@ -0,0 +1,87 @@
package obfuscation
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"hash"
"io"
"github.com/9seconds/mtg/v2/essentials"
)
type Obfuscator struct {
Secret []byte
}
func (o Obfuscator) ReadHandshake(r essentials.Conn) (int, essentials.Conn, error) {
frame := handshakeFrame{}
if _, err := io.ReadFull(r, frame.data[:]); err != nil {
return 0, nil, fmt.Errorf("cannot read frame: %w", err)
}
hasher := sha256.New()
recvCipher := o.getCipher(&frame, hasher)
frame.revert()
hasher.Reset()
sendCipher := o.getCipher(&frame, hasher)
recvCipher.XORKeyStream(frame.data[:], frame.data[:])
if val := frame.connectionType(); subtle.ConstantTimeCompare(val, hfConnectionType[:]) != 1 {
return 0, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val))
}
cn := conn{
Conn: r,
recvCipher: recvCipher,
sendCipher: sendCipher,
}
return frame.dc(), cn, nil
}
func (o Obfuscator) SendHandshake(w essentials.Conn, dc int) (essentials.Conn, error) {
frame := generateHandshake(dc)
copyFrame := frame
hasher := sha256.New()
sendCipher := o.getCipher(&frame, hasher)
frame.revert()
hasher.Reset()
recvCipher := o.getCipher(&frame, hasher)
sendCipher.XORKeyStream(frame.data[:], frame.data[:])
copy(frame.key(), copyFrame.key())
copy(frame.iv(), copyFrame.iv())
if _, err := w.Write(frame.data[:]); err != nil {
return nil, fmt.Errorf("cannot send a handshake: %w", err)
}
return conn{
Conn: w,
recvCipher: recvCipher,
sendCipher: sendCipher,
}, nil
}
func (o Obfuscator) getCipher(f *handshakeFrame, hasher hash.Hash) cipher.Stream {
blockKey := f.key()
if o.Secret != nil {
hasher.Write(blockKey)
hasher.Write(o.Secret)
blockKey = hasher.Sum(nil)
}
block, _ := aes.NewCipher(blockKey)
return cipher.NewCTR(block, f.iv())
}
@@ -0,0 +1,63 @@
package obfuscation_test
import (
"bytes"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func FuzzClientServerHandshakes(f *testing.F) {
f.Add(int16(1), make([]byte, mtglib.SecretKeyLength))
f.Fuzz(func(t *testing.T, dc int16, data []byte) {
if dc <= 0 {
dc = 1
}
client := obfuscation.Obfuscator{
Secret: data,
}
server := client
clientToServerBuf := &bytes.Buffer{}
writeConnMock := &testlib.EssentialsConnMock{}
writeConnMock.
On("Write", mock.AnythingOfType("[]uint8")).
Once().
Return(64, nil).
Run(func(args mock.Arguments) {
arg := args.Get(0).([]byte)
n, err := clientToServerBuf.Write(arg)
assert.Equal(t, 64, n)
assert.NoError(t, err)
})
readConnMock := &testlib.EssentialsConnMock{}
readConnMock.
On("Read", mock.AnythingOfType("[]uint8")).
Once().
Return(64, nil).
Run(func(args mock.Arguments) {
arg := args.Get(0).([]byte)
n, err := clientToServerBuf.Read(arg)
assert.Equal(t, 64, n)
assert.NoError(t, err)
})
_, err := client.SendHandshake(writeConnMock, int(dc))
assert.NoError(t, err)
readDc, _, err := server.ReadHandshake(readConnMock)
assert.NoError(t, err)
assert.EqualValues(t, dc, readDc)
writeConnMock.AssertExpectations(t)
readConnMock.AssertExpectations(t)
})
}
@@ -0,0 +1,94 @@
package obfuscation_test
import (
"bytes"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ObfuscatorTestSuite struct {
SnapshotTestSuite
secret *mtglib.Secret
}
func (s *ObfuscatorTestSuite) SetupSuite() {
s.Setup("", "client-handshake")
secret := mtglib.GenerateSecret("hostname.com")
s.secret = &secret
}
func (s *ObfuscatorTestSuite) TestSnapshot() {
for name, snapshot := range s.snapshots {
s.T().Run(name, func(t *testing.T) {
obfs := obfuscation.Obfuscator{
Secret: snapshot.Secret.data,
}
connMock := &testlib.EssentialsConnMock{}
connMockReadBuffer := &bytes.Buffer{}
connMockReadBuffer.Write(snapshot.Frame.data)
connMockReadBuffer.Write(snapshot.Decrypted.Cipher.data)
connMockWriteBuffer := &bytes.Buffer{}
connMock.
On("Read", mock.AnythingOfType("[]uint8")).
Return(64, nil).
Run(func(args mock.Arguments) {
arr := args.Get(0).([]byte)
_, err := connMockReadBuffer.Read(arr)
require.NoError(t, err)
})
dc, cn, err := obfs.ReadHandshake(connMock)
assert.EqualValues(t, 2, dc)
assert.NoError(t, err)
connMock.Calls = []mock.Call{}
connMock.ExpectedCalls = []*mock.Call{}
connMock.
On("Read", mock.AnythingOfType("[]uint8")).
Return(len(snapshot.Decrypted.Cipher.data), nil).
Run(func(args mock.Arguments) {
arr := args.Get(0).([]byte)
_, err := connMockReadBuffer.Read(arr)
require.NoError(t, err)
})
connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(len(snapshot.Encrypted.Cipher.data), nil).
Run(func(args mock.Arguments) {
arr := args.Get(0).([]byte)
_, err := connMockWriteBuffer.Write(arr)
require.NoError(t, err)
})
readBuf := make([]byte, len(snapshot.Decrypted.Text.data))
_, err = cn.Read(readBuf)
assert.NoError(t, err)
assert.Equal(t, readBuf, snapshot.Decrypted.Text.data)
_, err = cn.Write(snapshot.Encrypted.Text.data)
assert.NoError(t, err)
assert.Equal(t, connMockWriteBuffer.Bytes(), snapshot.Encrypted.Cipher.data)
connMock.AssertExpectations(t)
})
}
}
func TestObfuscator(t *testing.T) {
t.Parallel()
suite.Run(t, &ObfuscatorTestSuite{})
}
+1 -1
View File
@@ -5,5 +5,5 @@ const (
)
type Logger interface {
Printf(msg string, args ...interface{})
Printf(msg string, args ...any)
}
+1 -1
View File
@@ -2,4 +2,4 @@ package relay_test
type loggerMock struct{}
func (l loggerMock) Printf(format string, args ...interface{}) {}
func (l loggerMock) Printf(format string, args ...any) {}
-19
View File
@@ -1,19 +0,0 @@
package relay
import "sync"
var copyBufferPool = sync.Pool{
New: func() interface{} {
rv := make([]byte, copyBufferSize)
return &rv
},
}
func acquireCopyBuffer() *[]byte {
return copyBufferPool.Get().(*[]byte) //nolint: forcetypeassert
}
func releaseCopyBuffer(buf *[]byte) {
copyBufferPool.Put(buf)
}
+5 -6
View File
@@ -10,7 +10,7 @@ import (
func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.Conn) {
defer telegramConn.Close() //nolint: errcheck
defer clientConn.Close() //nolint: errcheck
defer clientConn.Close() //nolint: errcheck
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -18,7 +18,7 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
go func() {
<-ctx.Done()
telegramConn.Close() //nolint: errcheck
clientConn.Close() //nolint: errcheck
clientConn.Close() //nolint: errcheck
}()
closeChan := make(chan struct{})
@@ -35,13 +35,12 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
}
func pump(log Logger, src, dst essentials.Conn, direction string) {
var buf [copyBufferSize]byte
defer src.CloseRead() //nolint: errcheck
defer dst.CloseWrite() //nolint: errcheck
copyBuffer := acquireCopyBuffer()
defer releaseCopyBuffer(copyBuffer)
n, err := io.CopyBuffer(src, dst, *copyBuffer)
n, err := io.CopyBuffer(src, dst, buf[:])
switch {
case err == nil:
-41
View File
@@ -1,41 +0,0 @@
package telegram
import "math/rand"
type addressPool struct {
v4 [][]tgAddr
v6 [][]tgAddr
}
func (a addressPool) isValidDC(dc int) bool {
return dc > 0 && dc <= len(a.v4) && dc <= len(a.v6)
}
func (a addressPool) getRandomDC() int {
return 1 + rand.Intn(len(a.v4))
}
func (a addressPool) getV4(dc int) []tgAddr {
return a.get(a.v4, dc-1)
}
func (a addressPool) getV6(dc int) []tgAddr {
return a.get(a.v6, dc-1)
}
func (a addressPool) get(addresses [][]tgAddr, dc int) []tgAddr {
if dc < 0 || dc >= len(addresses) {
return nil
}
rv := make([]tgAddr, len(addresses[dc]))
copy(rv, addresses[dc])
if len(rv) > 1 {
rand.Shuffle(len(rv), func(i, j int) {
rv[i], rv[j] = rv[j], rv[i]
})
}
return rv
}
-90
View File
@@ -1,90 +0,0 @@
package telegram
import (
"context"
"errors"
"github.com/9seconds/mtg/v2/essentials"
)
var errNoAddresses = errors.New("no addresses")
type preferIP uint8
const (
preferIPOnlyIPv4 preferIP = iota
preferIPOnlyIPv6
preferIPPreferIPv4
preferIPPreferIPv6
)
type tgAddr struct {
network string
address string
}
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
var (
productionV4Addresses = [][]tgAddr{
{ // dc1
{network: "tcp4", address: "149.154.175.50:443"},
},
{ // dc2
{network: "tcp4", address: "149.154.167.51:443"},
{network: "tcp4", address: "95.161.76.100:443"},
},
{ // dc3
{network: "tcp4", address: "149.154.175.100:443"},
},
{ // dc4
{network: "tcp4", address: "149.154.167.91:443"},
},
{ // dc5
{network: "tcp4", address: "149.154.171.5:443"},
},
}
productionV6Addresses = [][]tgAddr{
{ // dc1
{network: "tcp6", address: "[2001:b28:f23d:f001::a]:443"},
},
{ // dc2
{network: "tcp6", address: "[2001:67c:04e8:f002::a]:443"},
},
{ // dc3
{network: "tcp6", address: "[2001:b28:f23d:f003::a]:443"},
},
{ // dc4
{network: "tcp6", address: "[2001:67c:04e8:f004::a]:443"},
},
{ // dc5
{network: "tcp6", address: "[2001:b28:f23f:f005::a]:443"},
},
}
testV4Addresses = [][]tgAddr{
{ // dc1
{network: "tcp4", address: "149.154.175.10:443"},
},
{ // dc2
{network: "tcp4", address: "149.154.167.40:443"},
},
{ // dc3
{network: "tcp4", address: "149.154.175.117:443"},
},
}
testV6Addresses = [][]tgAddr{
{ // dc1
{network: "tcp6", address: "[2001:b28:f23d:f001::e]:443"},
},
{ // dc2
{network: "tcp6", address: "[2001:67c:04e8:f002::e]:443"},
},
{ // dc3
{network: "tcp6", address: "[2001:b28:f23d:f003::e]:443"},
},
}
)
type Dialer interface {
DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
}
-83
View File
@@ -1,83 +0,0 @@
package telegram
import (
"context"
"fmt"
"strings"
"github.com/9seconds/mtg/v2/essentials"
)
type Telegram struct {
dialer Dialer
preferIP preferIP
pool addressPool
}
func (t Telegram) Dial(ctx context.Context, dc int) (essentials.Conn, error) {
var addresses []tgAddr
switch t.preferIP {
case preferIPOnlyIPv4:
addresses = t.pool.getV4(dc)
case preferIPOnlyIPv6:
addresses = t.pool.getV6(dc)
case preferIPPreferIPv4:
addresses = append(t.pool.getV4(dc), t.pool.getV6(dc)...)
case preferIPPreferIPv6:
addresses = append(t.pool.getV6(dc), t.pool.getV4(dc)...)
}
var conn essentials.Conn
err := errNoAddresses
for _, v := range addresses {
conn, err = t.dialer.DialContext(ctx, v.network, v.address)
if err == nil {
return conn, nil
}
}
return nil, fmt.Errorf("cannot dial to %d dc: %w", dc, err)
}
func (t Telegram) IsKnownDC(dc int) bool {
return t.pool.isValidDC(dc)
}
func (t Telegram) GetFallbackDC() int {
return t.pool.getRandomDC()
}
func New(dialer Dialer, ipPreference string, useTestDCs bool) (*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)
}
pool := addressPool{
v4: productionV4Addresses,
v6: productionV6Addresses,
}
if useTestDCs {
pool.v4 = testV4Addresses
pool.v6 = testV6Addresses
}
return &Telegram{
dialer: dialer,
preferIP: pref,
pool: pool,
}, nil
}
@@ -1,159 +0,0 @@
package telegram
import (
"context"
"errors"
"io"
"net"
"strconv"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type TelegramTestSuite struct {
suite.Suite
dialerMock *testlib.MtglibNetworkMock
t *Telegram
}
func (suite *TelegramTestSuite) SetupTest() {
suite.dialerMock = &testlib.MtglibNetworkMock{}
suite.t, _ = New(suite.dialerMock, "prefer-ipv4", false)
}
func (suite *TelegramTestSuite) TearDownTest() {
suite.dialerMock.AssertExpectations(suite.T())
}
func (suite *TelegramTestSuite) TestUnknownDC() {
testData := []int{
-1,
0,
6,
100,
}
for _, v := range testData {
value := v
suite.T().Run(strconv.Itoa(value), func(t *testing.T) {
_, err := suite.t.Dial(context.Background(), value)
assert.Error(t, err)
assert.False(t, suite.t.IsKnownDC(value))
})
}
}
func (suite *TelegramTestSuite) TestDialToCorrectIPs() {
testData := map[int][]tgAddr{}
for i := 1; i <= 5; i++ {
testData[i] = []tgAddr{}
testData[i] = append(testData[i], productionV4Addresses[i-1]...)
testData[i] = append(testData[i], productionV6Addresses[i-1]...)
}
for i, v := range testData {
idx := i
addresses := v
suite.T().Run(strconv.Itoa(idx), func(t *testing.T) {
for _, addr := range addresses {
suite.dialerMock.
On("DialContext", mock.Anything, addr.network, addr.address).
Once().
Return((*net.TCPConn)(nil), io.EOF)
}
_, err := suite.t.Dial(context.Background(), idx)
assert.True(t, errors.Is(err, io.EOF))
assert.True(t, suite.t.IsKnownDC(idx))
})
}
}
func (suite *TelegramTestSuite) TestDialPreferIPRange() {
testData := map[string][]tgAddr{
"prefer-ipv4": {testV4Addresses[0][0], testV6Addresses[0][0]},
"prefer-ipv6": {testV6Addresses[0][0], testV4Addresses[0][0]},
"only-ipv4": {testV4Addresses[0][0]},
"only-ipv6": {testV6Addresses[0][0]},
}
for k, v := range testData {
name := k
addresses := v
suite.T().Run(name, func(t *testing.T) {
for _, addr := range addresses {
suite.dialerMock.
On("DialContext", mock.Anything, addr.network, addr.address).
Once().
Return((*net.TCPConn)(nil), io.EOF)
}
tg, _ := New(suite.dialerMock, name, true)
_, err := tg.Dial(context.Background(), 1)
assert.True(t, errors.Is(err, io.EOF))
})
}
}
func (suite *TelegramTestSuite) TestDialPreferIPPriority() {
testData := map[string]tgAddr{
"prefer-ipv4": productionV4Addresses[0][0],
"prefer-ipv6": productionV6Addresses[0][0],
}
for k, v := range testData {
name := k
addr := v
suite.T().Run(name, func(t *testing.T) {
conn := &net.TCPConn{}
suite.dialerMock.
On("DialContext", mock.Anything, addr.network, addr.address).
Once().
Return(conn, nil)
tg, _ := New(suite.dialerMock, name, false)
res, err := tg.Dial(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, conn, res)
})
}
}
func (suite *TelegramTestSuite) TestUnknownPreferIP() {
_, err := New(suite.dialerMock, "xxx", false)
suite.Error(err)
}
func (suite *TelegramTestSuite) TestFallbackDC() {
dcs := make([]int, 10)
for i := 0; i < len(dcs); i++ {
dcs[i] = suite.t.GetFallbackDC()
}
for _, v := range dcs {
value := v
suite.T().Run(strconv.Itoa(value), func(t *testing.T) {
assert.True(t, suite.t.IsKnownDC(value))
})
}
}
func TestTelegram(t *testing.T) {
t.Parallel()
suite.Run(t, &TelegramTestSuite{})
}
+81 -41
View File
@@ -10,11 +10,11 @@ import (
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/dc"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
"github.com/9seconds/mtg/v2/mtglib/internal/relay"
"github.com/9seconds/mtg/v2/mtglib/internal/telegram"
"github.com/panjf2000/ants/v2"
)
@@ -24,11 +24,15 @@ type Proxy struct {
ctxCancel context.CancelFunc
streamWaitGroup sync.WaitGroup
allowFallbackOnUnknownDC bool
tolerateTimeSkewness time.Duration
domainFrontingPort int
workerPool *ants.PoolWithFunc
telegram *telegram.Telegram
allowFallbackOnUnknownDC bool
tolerateTimeSkewness time.Duration
domainFrontingPort int
domainFrontingIP string
domainFrontingProxyProtocol bool
workerPool *ants.PoolWithFunc
telegram *dc.Telegram
configUpdater *dc.PublicConfigUpdater
clientObfuscatror obfuscation.Obfuscator
secret Secret
network Network
@@ -40,8 +44,14 @@ type Proxy struct {
}
// DomainFrontingAddress returns a host:port pair for a fronting domain.
// If DomainFrontingIP is set, it is used instead of resolving the hostname.
func (p *Proxy) DomainFrontingAddress() string {
return net.JoinHostPort(p.secret.Host, strconv.Itoa(p.domainFrontingPort))
host := p.secret.Host
if p.domainFrontingIP != "" {
host = p.domainFrontingIP
}
return net.JoinHostPort(host, strconv.Itoa(p.domainFrontingPort))
}
// ServeConn serves a connection. We do not check IP blocklist and concurrency
@@ -70,8 +80,8 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
return
}
if err := p.doObfuscated2Handshake(ctx); err != nil {
p.logger.InfoError("obfuscated2 handshake is failed", err)
if err := p.doObfuscatedHandshake(ctx); err != nil {
p.logger.InfoError("obfuscated handshake is failed", err)
return
}
@@ -144,6 +154,7 @@ func (p *Proxy) Shutdown() {
p.ctxCancel()
p.streamWaitGroup.Wait()
p.workerPool.Release()
p.configUpdater.Wait()
p.allowlist.Shutdown()
p.blocklist.Shutdown()
@@ -201,54 +212,61 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
return true
}
func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error {
dc, encryptor, decryptor, err := obfuscated2.ClientHandshake(p.secret.Key[:], ctx.clientConn)
func (p *Proxy) doObfuscatedHandshake(ctx *streamContext) error {
dc, conn, err := p.clientObfuscatror.ReadHandshake(ctx.clientConn)
if err != nil {
return fmt.Errorf("cannot process client handshake: %w", err)
}
ctx.dc = dc
ctx.clientConn = conn
ctx.logger = ctx.logger.BindInt("dc", dc)
ctx.clientConn = obfuscated2.Conn{
Conn: ctx.clientConn,
Encryptor: encryptor,
Decryptor: decryptor,
}
return nil
}
func (p *Proxy) doTelegramCall(ctx *streamContext) error {
dc := ctx.dc
if p.allowFallbackOnUnknownDC && !p.telegram.IsKnownDC(dc) {
dc = p.telegram.GetFallbackDC()
ctx.logger = ctx.logger.BindInt("fallback_dc", dc)
dcid := ctx.dc
addresses := p.telegram.GetAddresses(dcid)
if len(addresses) == 0 && p.allowFallbackOnUnknownDC {
ctx.logger = ctx.logger.BindInt("original_dc", dcid)
ctx.logger.Warning("unknown DC, fallbacks")
ctx.dc = dc.DefaultDC
addresses = p.telegram.GetAddresses(dc.DefaultDC)
}
conn, err := p.telegram.Dial(ctx, dc)
var (
conn essentials.Conn
err error
foundAddr dc.Addr
)
for _, addr := range addresses {
conn, err = p.network.Dial(addr.Network, addr.Address)
if err == nil {
foundAddr = addr
break
}
}
if err != nil {
return fmt.Errorf("cannot dial to Telegram: %w", err)
return fmt.Errorf("no addresses to call: %w", err)
}
if conn == nil {
return fmt.Errorf("no available addresses for DC %d", ctx.dc)
}
encryptor, decryptor, err := obfuscated2.ServerHandshake(conn)
tgConn, err := foundAddr.Obfuscator.SendHandshake(conn, ctx.dc)
if err != nil {
conn.Close() //nolint: errcheck
return fmt.Errorf("cannot perform obfuscated2 handshake: %w", err)
conn.Close() // nolint: errcheck
return fmt.Errorf("cannot perform server handshake: %w", err)
}
ctx.telegramConn = obfuscated2.Conn{
Conn: connTraffic{
Conn: conn,
streamID: ctx.streamID,
stream: p.eventStream,
ctx: ctx,
},
Encryptor: encryptor,
Decryptor: decryptor,
ctx.telegramConn = connTraffic{
Conn: tgConn,
streamID: ctx.streamID,
stream: p.eventStream,
ctx: ctx,
}
p.eventStream.Send(ctx,
@@ -271,6 +289,10 @@ func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) {
return
}
if p.domainFrontingProxyProtocol {
frontConn = newConnProxyProtocol(ctx.clientConn, frontConn)
}
frontConn = connTraffic{
Conn: frontConn,
ctx: ctx,
@@ -292,12 +314,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
return nil, fmt.Errorf("invalid settings: %w", err)
}
tg, err := telegram.New(opts.Network, opts.getPreferIP(), opts.UseTestDCs)
tg, err := dc.New(opts.getPreferIP())
if err != nil {
return nil, fmt.Errorf("cannot build telegram dialer: %w", err)
return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
logger := opts.getLogger("proxy")
updatersLogger := logger.Named("telegram-updaters")
proxy := &Proxy{
ctx: ctx,
ctxCancel: cancel,
@@ -307,15 +332,30 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
blocklist: opts.IPBlocklist,
allowlist: opts.IPAllowlist,
eventStream: opts.EventStream,
logger: opts.getLogger("proxy"),
logger: logger,
domainFrontingPort: opts.getDomainFrontingPort(),
domainFrontingIP: opts.DomainFrontingIP,
tolerateTimeSkewness: opts.getTolerateTimeSkewness(),
allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
telegram: tg,
configUpdater: dc.NewPublicConfigUpdater(
tg,
updatersLogger.Named("public-config"),
opts.Network.MakeHTTPClient(nil),
),
clientObfuscatror: obfuscation.Obfuscator{
Secret: opts.Secret.Key[:],
},
domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol,
}
if opts.AutoUpdate {
proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")
}
pool, err := ants.NewPoolWithFunc(opts.getConcurrency(),
func(arg interface{}) {
func(arg any) {
proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert
},
ants.WithLogger(opts.getLogger("ants")),
+31
View File
@@ -85,6 +85,12 @@ type ProxyOpts struct {
// This is an optional setting.
PreferIP string
// AutoUpdate defines if it is required to auto update proxy list from
// Telegram instead of relying on a hardcoded list.
//
// This is an optional setting.
AutoUpdate bool
// DomainFrontingPort is a port we use to connect to a fronting domain.
//
// This is required because secret does not specify a port. It specifies a
@@ -93,6 +99,23 @@ type ProxyOpts struct {
// This is an optional setting.
DomainFrontingPort uint
// DomainFrontingIP is an IP address to use when connecting to the fronting
// domain instead of resolving the hostname from the secret via DNS.
//
// This is useful when DNS resolution of the fronting host is blocked.
// The hostname from the secret is still used for SNI in the TLS handshake.
//
// This is an optional setting.
DomainFrontingIP string
// DomainFrontingProxyProtocol is used if communication between upstream
// endpoint and mtg supports proxy protocol. This is useful in case
// if mtg is also placed behind load balancer, and this will make
// fronting webserver to know about real IP addresses
//
// This is an optional setting.
DomainFrontingProxyProtocol bool
// AllowFallbackOnUnknownDC defines how proxy behaves if unknown DC was
// requested. If this setting is set to false, then such connection will be
// rejected. Otherwise, proxy will chose any DC.
@@ -110,7 +133,15 @@ type ProxyOpts struct {
// Telegram-related projects.
//
// This is an optional setting.
//
// OBSOLETE and DEPRECATED. Ignored.
UseTestDCs bool
// DCOverrides defines a set of IP addresses that should be used
// with a higher priority to those that are calculated somehow by mtg.
//
// OBSOLETE and DEPRECATED. Ignored.
DCOverrides map[int][]string
}
func (p ProxyOpts) valid() error {
+1 -1
View File
@@ -32,7 +32,7 @@ func (s *streamContext) Err() error {
return s.ctx.Err() //nolint: wrapcheck
}
func (s *streamContext) Value(key interface{}) interface{} {
func (s *streamContext) Value(key any) any {
return s.ctx.Value(key)
}
+8 -11
View File
@@ -52,17 +52,9 @@ func (suite *CircuitBreakerTestSuite) TestMultipleRunsOk() {
Return(suite.connMock, nil)
wg := &sync.WaitGroup{}
wg.Add(5)
go func() {
wg.Wait()
suite.ctxCancel()
}()
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
for range 5 {
wg.Go(func() {
conn, err := suite.d.DialContext(suite.ctx, "tcp", "127.0.0.1")
suite.mutex.Lock()
@@ -70,9 +62,14 @@ func (suite *CircuitBreakerTestSuite) TestMultipleRunsOk() {
suite.NoError(err)
suite.Equal("127.0.0.1:3128", conn.RemoteAddr().String())
}()
})
}
go func() {
wg.Wait()
suite.ctxCancel()
}()
suite.Eventually(func() bool {
_, ok := <-suite.ctx.Done()
+1 -1
View File
@@ -63,7 +63,7 @@ const (
// DefaultDOHHostname defines a default IP address for DOH host. Since mtg is
// simple, please pass IP address here. We do not have bootstrap servers here
// embedded.
DefaultDOHHostname = "9.9.9.9"
DefaultDOHHostname = "1.1.1.1"
// DNSTimeout defines a timeout for DNS queries.
DNSTimeout = 5 * time.Second
+2 -2
View File
@@ -3,7 +3,7 @@ package network
import (
"context"
"fmt"
"math/rand"
"math/rand/v2"
"net/url"
"github.com/9seconds/mtg/v2/essentials"
@@ -19,7 +19,7 @@ func (l loadBalancedSocks5Dialer) Dial(network, address string) (essentials.Conn
func (l loadBalancedSocks5Dialer) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
length := len(l.dialers)
start := rand.Intn(length)
start := rand.IntN(length)
moved := false
for i := start; i != start || !moved; i = (i + 1) % length {
+1 -1
View File
@@ -64,7 +64,7 @@ func (suite *LoadBalancedSocks5TestSuite) TestCannotDial() {
})
suite.NoError(err)
for i := 0; i < network.ProxyDialerOpenThreshold*2; i++ {
for range network.ProxyDialerOpenThreshold * 2 {
_, err = lbDialer.Dial("tcp", "127.1.1.1:80")
suite.True(errors.Is(err, network.ErrCannotDialWithAllProxies))
}
+5 -13
View File
@@ -3,7 +3,7 @@ package network
import (
"context"
"fmt"
"math/rand"
"math/rand/v2"
"net"
"net/http"
"sync"
@@ -81,32 +81,24 @@ func (n *network) dnsResolve(protocol, address string) ([]string, error) {
switch protocol {
case "tcp", "tcp4":
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
resolved := n.dns.LookupA(address)
mutex.Lock()
ips = append(ips, resolved...)
mutex.Unlock()
}()
})
}
switch protocol {
case "tcp", "tcp6":
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
resolved := n.dns.LookupAAAA(address)
mutex.Lock()
ips = append(ips, resolved...)
mutex.Unlock()
}()
})
}
wg.Wait()
+1 -1
View File
@@ -3,7 +3,7 @@ package stats
import "sync"
var streamInfoPool = sync.Pool{
New: func() interface{} {
New: func() any {
return &streamInfo{
tags: make(map[string]string),
}
+1 -1
View File
@@ -100,7 +100,7 @@ func (suite *StatsdTestSuite) SetupTest() {
func (suite *StatsdTestSuite) TearDownTest() {
suite.statsd.Shutdown()
suite.factory.Close() //nolint: errcheck
suite.factory.Close() //nolint: errcheck
suite.statsdServer.Close() //nolint: errcheck
}