mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 18:44:02 +03:00
REPOSITORY / ScuroNeko/mtg
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d748077e9 | ||
|
|
f61a63a59b | ||
|
|
282896be09 | ||
|
|
3809f034ad | ||
|
|
6feef02d08 | ||
|
|
5549a82d19 | ||
|
|
189dce07c6 | ||
|
|
449f84133a | ||
|
|
9c122f942f | ||
|
|
136eea551f | ||
|
|
e6fa5906c9 | ||
|
|
42f612f49e | ||
|
|
d7db8ca98b | ||
|
|
1cb225f52c | ||
|
|
af72b2a574 | ||
|
|
2cbee5d453 | ||
|
|
cde313b359 | ||
|
|
58cb0b2caf | ||
|
|
bb320e9d89 | ||
|
|
f6d2f2ffd8 | ||
|
|
5fe3fdd73c | ||
|
|
5b91edf5c4 | ||
|
|
8b34c1b104 | ||
|
|
36c766b331 | ||
|
|
e4a9a96309 | ||
|
|
94d46d2c65 | ||
|
|
908842063a | ||
|
|
e50cee5748 | ||
|
|
ee524abdb5 | ||
|
|
3e75e4fa63 | ||
|
|
140e9dfc2e | ||
|
|
d0065d35c2 | ||
|
|
45ce5c2f61 | ||
|
|
21129b6e00 | ||
|
|
bf38f9f8af | ||
|
|
b854b16e1a | ||
|
|
f4b296d1e0 | ||
|
|
432e530f68 | ||
|
|
a2bf9a269a | ||
|
|
cf3437bb63 | ||
|
|
d0e99dda2b | ||
|
|
ac3bd16e83 | ||
|
|
222cef8c41 | ||
|
|
ed5da2864a | ||
|
|
08393e426a | ||
|
|
0440ccf4ff | ||
|
|
a0aabf2391 | ||
|
|
3b03c4a90a | ||
|
|
80b9159ce9 | ||
|
|
bdabb0e59a | ||
|
|
543f5cde9c | ||
|
|
1a247d18b1 | ||
|
|
5a63c7d5b4 | ||
|
|
e7fdb02a29 |
@@ -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 ./...
|
||||
+14
-17
@@ -1,7 +1,11 @@
|
||||
[tools]
|
||||
go = "latest"
|
||||
"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"
|
||||
@@ -16,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 ./..."
|
||||
@@ -45,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"
|
||||
@@ -77,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",
|
||||
@@ -92,7 +90,6 @@ 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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
+56
-16
@@ -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,14 +99,26 @@ tolerate-time-skewness = "5s"
|
||||
# Otherwise, chose a new DC.
|
||||
allow-fallback-on-unknown-dc = false
|
||||
|
||||
# Telegram uses different DCs for different purposes. Unfortunately, most of
|
||||
# DCs are not public, and dependent on a location of the current user, so
|
||||
# mtg cannot know upfront about all of them, and how to access them. It has
|
||||
# a default list of DCs, including some CDN IPs, but it is possible that some
|
||||
# of them are not working for you. In this case, you can override them here.
|
||||
[[dc-overrides]]
|
||||
dc = 101
|
||||
ips = ["127.0.0.1:443"]
|
||||
# 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]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/9seconds/mtg/v2
|
||||
|
||||
go 1.25
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/panjf2000/ants/v2 v2.11.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 +28,7 @@ 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
|
||||
)
|
||||
|
||||
@@ -57,6 +57,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
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,8 +68,8 @@ 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/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=
|
||||
|
||||
+4
-10
@@ -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()
|
||||
|
||||
|
||||
+16
-12
@@ -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"
|
||||
)
|
||||
@@ -240,14 +242,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
|
||||
return fmt.Errorf("cannot build ip allowlist: %w", err)
|
||||
}
|
||||
|
||||
dcOverrides := map[int][]string{}
|
||||
for _, override := range conf.DCOverrides {
|
||||
dcid := override.DC.Get()
|
||||
for _, addr := range override.IPs {
|
||||
dcOverrides[dcid] = append(dcOverrides[dcid], addr.Get(""))
|
||||
}
|
||||
}
|
||||
|
||||
opts := mtglib.ProxyOpts{
|
||||
Logger: logger,
|
||||
Network: ntw,
|
||||
@@ -256,13 +250,15 @@ 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,
|
||||
DCOverrides: dcOverrides,
|
||||
}
|
||||
|
||||
proxy, err := mtglib.NewProxy(opts)
|
||||
@@ -275,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
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
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
|
||||
@@ -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
-13
@@ -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
|
||||
|
||||
@@ -64,10 +74,27 @@ type Config struct {
|
||||
MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
|
||||
} `json:"prometheus"`
|
||||
} `json:"stats"`
|
||||
DCOverrides []struct {
|
||||
DC TypeDC `json:"dc"`
|
||||
IPs []TypeHostPort `json:"ips"`
|
||||
} `json:"dcOverrides"`
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+18
-13
@@ -9,15 +9,24 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
@@ -59,10 +68,6 @@ type tomlConfig struct {
|
||||
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
|
||||
} `toml:"prometheus" json:"prometheus,omitempty"`
|
||||
} `toml:"stats" json:"stats,omitempty"`
|
||||
DCOverrides []struct {
|
||||
DC uint `toml:"dc" json:"dc"`
|
||||
IPs []string `toml:"ips" json:"ips"`
|
||||
} `toml:"dc-overrides" json:"dcOverrides,omitempty"`
|
||||
}
|
||||
|
||||
func Parse(rawData []byte) (*Config, error) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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...))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,36 @@ backend = "core:go"
|
||||
"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.0"
|
||||
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
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package dc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
|
||||
)
|
||||
|
||||
type Addr struct {
|
||||
Network string
|
||||
Address string
|
||||
Network string
|
||||
Address string
|
||||
Obfuscator obfuscation.Obfuscator
|
||||
}
|
||||
|
||||
func (d Addr) String() string {
|
||||
return d.Address
|
||||
return fmt.Sprintf("addr=%s, secret=%v", d.Address, d.Obfuscator.Secret)
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package dc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/9seconds/mtg/v2/mtglib/internal/dc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
addr := dc.Addr{Network: "tcp4", Address: "127.0.0.1:443"}
|
||||
|
||||
assert.Equal(t, "127.0.0.1:443", addr.String())
|
||||
}
|
||||
+61
-50
@@ -1,5 +1,10 @@
|
||||
package dc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type preferIP uint8
|
||||
|
||||
const (
|
||||
@@ -10,7 +15,18 @@ const (
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -18,56 +34,51 @@ type Logger interface {
|
||||
WarningError(msg string, err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
|
||||
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"},
|
||||
},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
type Updater interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
defaultDCOverridesAddrSet = dcAddrSet{
|
||||
v4: map[int][]Addr{
|
||||
203: {
|
||||
{Network: "tcp4", Address: "91.105.192.100:443"},
|
||||
},
|
||||
// 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"},
|
||||
},
|
||||
v6: map[int][]Addr{
|
||||
203: {
|
||||
{Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]: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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -2,16 +2,20 @@ package dc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"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)
|
||||
@@ -24,7 +28,7 @@ func (t *Telegram) GetAddresses(dc int) []Addr {
|
||||
return append(t.view.getV6(dc), t.view.getV4(dc)...)
|
||||
}
|
||||
|
||||
func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error) {
|
||||
func New(ipPreference string) (*Telegram, error) {
|
||||
var pref preferIP
|
||||
|
||||
switch strings.ToLower(ipPreference) {
|
||||
@@ -40,40 +44,7 @@ func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error)
|
||||
return nil, fmt.Errorf("unknown ip preference %s", ipPreference)
|
||||
}
|
||||
|
||||
overrides := dcAddrSet{
|
||||
v4: map[int][]Addr{},
|
||||
v6: map[int][]Addr{},
|
||||
}
|
||||
for dc, addrs := range userOverrides {
|
||||
for _, addr := range addrs {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect host %s: %w", addr, err)
|
||||
}
|
||||
|
||||
parsed := net.ParseIP(host)
|
||||
if parsed == nil {
|
||||
return nil, fmt.Errorf("incorrect host %s", addr)
|
||||
}
|
||||
|
||||
if parsed.To4() != nil {
|
||||
overrides.v4[dc] = append(overrides.v4[dc], Addr{
|
||||
Network: "tcp4",
|
||||
Address: addr,
|
||||
})
|
||||
} else {
|
||||
overrides.v6[dc] = append(overrides.v6[dc], Addr{
|
||||
Network: "tcp6",
|
||||
Address: addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Telegram{
|
||||
view: dcView{
|
||||
overrides: overrides,
|
||||
},
|
||||
preferIP: pref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
package dc
|
||||
|
||||
type dcView struct {
|
||||
overrides dcAddrSet
|
||||
publicConfigs dcAddrSet
|
||||
}
|
||||
|
||||
func (d dcView) getV4(dc int) []Addr {
|
||||
addrs := d.overrides.getV4(dc)
|
||||
addrs = append(addrs, defaultDCOverridesAddrSet.getV4(dc)...)
|
||||
addrs := d.publicConfigs.getV4(dc)
|
||||
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (d dcView) getV6(dc int) []Addr {
|
||||
addrs := d.overrides.getV6(dc)
|
||||
addrs = append(addrs, defaultDCOverridesAddrSet.getV6(dc)...)
|
||||
addrs := d.publicConfigs.getV6(dc)
|
||||
addrs = append(addrs, defaultDCAddrSet.getV6(dc)...)
|
||||
|
||||
return addrs
|
||||
|
||||
@@ -16,7 +16,7 @@ type ViewTestSuite struct {
|
||||
|
||||
func (suite *ViewTestSuite) SetupSuite() {
|
||||
suite.view = dcView{
|
||||
overrides: dcAddrSet{
|
||||
publicConfigs: dcAddrSet{
|
||||
v4: map[int][]Addr{
|
||||
111: {
|
||||
{Network: "tcp4", Address: "127.0.0.1:443"},
|
||||
@@ -37,15 +37,15 @@ func (suite *ViewTestSuite) SetupSuite() {
|
||||
func (suite *ViewTestSuite) TestGetV4() {
|
||||
testData := map[int][]Addr{
|
||||
111: {
|
||||
{"tcp4", "127.0.0.1:443"},
|
||||
{Network: "tcp4", Address: "127.0.0.1:443"},
|
||||
},
|
||||
203: {
|
||||
{"tcp4", "127.0.0.2:443"},
|
||||
{"tcp4", "91.105.192.100:443"},
|
||||
{Network: "tcp4", Address: "127.0.0.2:443"},
|
||||
{Network: "tcp4", Address: "91.105.192.100:443"},
|
||||
},
|
||||
2: {
|
||||
{"tcp4", "149.154.167.51:443"},
|
||||
{"tcp4", "95.161.76.100:443"},
|
||||
{Network: "tcp4", Address: "149.154.167.51:443"},
|
||||
{Network: "tcp4", Address: "95.161.76.100:443"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ func (suite *ViewTestSuite) TestGetV6() {
|
||||
testData := map[int][]Addr{
|
||||
111: {},
|
||||
203: {
|
||||
{"tcp6", "xxx"},
|
||||
{"tcp6", "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
|
||||
{Network: "tcp6", Address: "xxx"},
|
||||
{Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
|
||||
},
|
||||
1: {
|
||||
{"tcp6", "[2001:b28:f23d:f001::a]:443"},
|
||||
{Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
var recordPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return &Record{}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -5,5 +5,5 @@ const (
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Printf(msg string, args ...interface{})
|
||||
Printf(msg string, args ...any)
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
+69
-37
@@ -13,7 +13,7 @@ import (
|
||||
"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/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 *dc.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,19 +212,15 @@ 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
|
||||
}
|
||||
@@ -223,40 +230,43 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error {
|
||||
|
||||
addresses := p.telegram.GetAddresses(dcid)
|
||||
if len(addresses) == 0 && p.allowFallbackOnUnknownDC {
|
||||
ctx.logger = ctx.logger.BindInt("fallback_dc", dc.DefaultDC)
|
||||
ctx.logger = ctx.logger.BindInt("original_dc", dcid)
|
||||
ctx.logger.Warning("unknown DC, fallbacks")
|
||||
ctx.dc = dc.DefaultDC
|
||||
addresses = p.telegram.GetAddresses(dc.DefaultDC)
|
||||
}
|
||||
|
||||
var conn essentials.Conn
|
||||
var err error
|
||||
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("no addresses to call: %w", err)
|
||||
}
|
||||
|
||||
encryptor, decryptor, err := obfuscated2.ServerHandshake(conn)
|
||||
if err != nil {
|
||||
conn.Close() //nolint: errcheck
|
||||
|
||||
return fmt.Errorf("cannot perform obfuscated2 handshake: %w", err)
|
||||
if conn == nil {
|
||||
return fmt.Errorf("no available addresses for DC %d", ctx.dc)
|
||||
}
|
||||
|
||||
ctx.telegramConn = obfuscated2.Conn{
|
||||
Conn: connTraffic{
|
||||
Conn: conn,
|
||||
streamID: ctx.streamID,
|
||||
stream: p.eventStream,
|
||||
ctx: ctx,
|
||||
},
|
||||
Encryptor: encryptor,
|
||||
Decryptor: decryptor,
|
||||
tgConn, err := foundAddr.Obfuscator.SendHandshake(conn, ctx.dc)
|
||||
if err != nil {
|
||||
conn.Close() // nolint: errcheck
|
||||
return fmt.Errorf("cannot perform server handshake: %w", err)
|
||||
}
|
||||
|
||||
ctx.telegramConn = connTraffic{
|
||||
Conn: tgConn,
|
||||
streamID: ctx.streamID,
|
||||
stream: p.eventStream,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
p.eventStream.Send(ctx,
|
||||
@@ -279,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,
|
||||
@@ -300,12 +314,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
|
||||
return nil, fmt.Errorf("invalid settings: %w", err)
|
||||
}
|
||||
|
||||
tg, err := dc.New(opts.getPreferIP(), opts.DCOverrides)
|
||||
tg, err := dc.New(opts.getPreferIP())
|
||||
if err != nil {
|
||||
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,
|
||||
@@ -315,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")),
|
||||
|
||||
+24
-1
@@ -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.
|
||||
@@ -117,7 +140,7 @@ type ProxyOpts struct {
|
||||
// DCOverrides defines a set of IP addresses that should be used
|
||||
// with a higher priority to those that are calculated somehow by mtg.
|
||||
//
|
||||
// This is an optional setting
|
||||
// OBSOLETE and DEPRECATED. Ignored.
|
||||
DCOverrides map[int][]string
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user