REPOSITORY / ScuroNeko/mtg

Compare commits

DIFF REPOSITORY

Compare commits

..
14 Commits
147 changed files with 1586 additions and 6560 deletions
-3
View File
@@ -1,3 +0,0 @@
# git config merge.theirs.name "Always accept theirs"
# git config merge.theirs.driver "cp %B %A"
default.pgo binary merge=theirs
+11 -66
View File
@@ -48,16 +48,6 @@ jobs:
- uses: jdx/mise-action@v3 - uses: jdx/mise-action@v3
name: Install mise name: Install mise
- name: Cache Go modules and build
uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests - name: Run tests
run: mise tasks run covtest run: mise tasks run covtest
@@ -79,16 +69,6 @@ jobs:
- uses: jdx/mise-action@v3 - uses: jdx/mise-action@v3
name: Install mise name: Install mise
- name: Cache Go modules and build
uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run fuzzing - name: Run fuzzing
run: mise tasks run 'test:fuzz:*' run: mise tasks run 'test:fuzz:*'
@@ -106,52 +86,9 @@ jobs:
- uses: jdx/mise-action@v3 - uses: jdx/mise-action@v3
name: Install mise name: Install mise
- name: Cache Go modules and build
uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run linter - name: Run linter
run: mise tasks run lint run: mise tasks run lint
artifacts:
name: Build release artifacts
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- uses: jdx/mise-action@v3
name: Install mise
- name: Cache Go modules
uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-gomod-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-gomod-
- name: Cache cross-compilation build
uses: actions/cache@v5
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-${{ hashFiles('**/*.go') }}
restore-keys: |
${{ runner.os }}-goreleaser-${{ hashFiles('go.sum') }}-
${{ runner.os }}-goreleaser-
- name: Run release
run: mise tasks run release
docker: docker:
name: Docker name: Docker
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -186,6 +123,14 @@ jobs:
- name: Setup BuildX - name: Setup BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Setup cache
uses: actions/cache@v5
with:
path: /tmp/buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub - name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -202,7 +147,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2
with: with:
pull: true pull: true
context: . context: .
@@ -210,5 +155,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=local,src=/tmp/buildx-cache
cache-to: type=gha,mode=max cache-to: type=local,dest=/tmp/buildx-cache
+5 -7
View File
@@ -21,7 +21,7 @@ permissions:
on: on:
push: push:
branches: branches:
- master - master
- stable - stable
pull_request: pull_request:
@@ -45,13 +45,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v2
with:
submodules: recursive
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -62,7 +60,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v4 uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -76,4 +74,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v1
-1
View File
@@ -35,7 +35,6 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
cache: true
- name: Check for vulnerabilities - name: Check for vulnerabilities
run: | run: |
-2
View File
@@ -8,5 +8,3 @@
mtg mtg
coverage.txt coverage.txt
dist/ dist/
.idea/
.vscode/
+2 -83
View File
@@ -10,15 +10,13 @@ before:
- go generate ./... - go generate ./...
builds: builds:
- id: default - binary: '{{ .ProjectName }}'
binary: '{{ .ProjectName }}'
goos: goos:
- darwin - darwin
- freebsd - freebsd
- linux - linux
- netbsd - netbsd
- openbsd - openbsd
- windows
goarch: goarch:
- 386 - 386
- amd64 - amd64
@@ -36,74 +34,15 @@ builds:
ignore: ignore:
- goos: darwin - goos: darwin
goarch: 386 goarch: 386
- goos: darwin
goarch: arm
- goos: freebsd - goos: freebsd
goarch: arm64 goarch: arm64
- goos: netbsd - goos: netbsd
goarch: arm64 goarch: arm64
- goos: openbsd - goos: openbsd
goarch: arm64 goarch: arm64
- goos: windows
goarch: 386
- goos: windows
goarch: arm
- id: mips
binary: '{{ .ProjectName }}'
goos:
- linux
goarch:
- mips
- mipsle
gomips:
- softfloat
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
- id: arm64-v9
binary: '{{ .ProjectName }}'
goos:
- darwin
- linux
goarch:
- arm64
goarm64:
- v9.0
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
- id: amd64-v3
binary: '{{ .ProjectName }}'
goos:
- darwin
- freebsd
- linux
- netbsd
- openbsd
- windows
goarch:
- amd64
goamd64:
- v3
env:
- CGO_ENABLED=0
flags:
- -trimpath
- -mod=readonly
ldflags: -s -w -X main.version={{ .Version }}
archives: archives:
- id: default - name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
ids:
- default
- mips
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
formats: formats:
- tar.gz - tar.gz
wrap_in_directory: true wrap_in_directory: true
@@ -115,26 +54,6 @@ archives:
- LICENSE - LICENSE
- README.md - README.md
- SECURITY.md - SECURITY.md
- BEST_PRACTICES.md
- example.config.toml
- id: optimized
ids:
- arm64-v9
- amd64-v3
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm64 }}-{{ .Arm64 }}{{ end }}{{ if .Amd64 }}-{{ .Amd64 }}{{ end }}'
formats:
- tar.gz
wrap_in_directory: true
format_overrides:
- goos: windows
formats:
- zip
files:
- LICENSE
- README.md
- SECURITY.md
- BEST_PRACTICES.md
- example.config.toml
gomod: gomod:
proxy: true proxy: true
+3 -9
View File
@@ -16,12 +16,6 @@ sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["mtg"] outputs = ["mtg"]
run = "go build" run = "go build"
[tasks."build:prof"]
description = "Build binary with profiling enabled"
sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["mtg"]
run = "go build -tags prof"
[tasks.update] [tasks.update]
description = "Update dependencies" description = "Update dependencies"
run = [ run = [
@@ -39,11 +33,11 @@ run = "govulncheck ./..."
[tasks.test] [tasks.test]
description = "Run tests" description = "Run tests"
run = "go test -v -race ./..." run = "go test -v ./..."
[tasks.covtest] [tasks.covtest]
description = "Run tests with code coverage" description = "Run tests with code coverage"
run = "go test -coverprofile=coverage.txt -covermode=atomic -count=2 -race -v ./..." run = "go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./..."
[tasks.test-all] [tasks.test-all]
description = "Run all tests" description = "Run all tests"
@@ -54,7 +48,7 @@ depends = [
[tasks."test:fuzz:client-hello"] [tasks."test:fuzz:client-hello"]
description = "Run fuzzy test for ClientHello" description = "Run fuzzy test for ClientHello"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake" run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls"
[tasks."test:fuzz:client-handshake"] [tasks."test:fuzz:client-handshake"]
description = "Run fuzzy test for ClientHandshake" description = "Run fuzzy test for ClientHandshake"
-55
View File
@@ -1,55 +0,0 @@
# Best practices
This is unfortunate, but since 2018 many things were changed. Most of them
became way worse. Previous iterations of censorship systems were very dumb,
DPI were primitive and filtered very obvious things. Nowadays they are
way more intelligent and it is very naive to treat them frivolously.
In 2026 is not enough to pretend that your mtg installation is a Microsoft
website that sits in Amsterdam Digital Ocean location. Now your installation
has to be a website that is mtg in disguise. Yes, it requires a bit more effort
but this effort is probably less than rotating proxies each other day.
mtproto traffic, even with FakeTLS, has its specifics that are probably
very well known by DPI systems. These specifics are not something unique but
could mark an IP address as suspicious. Now let's think:
1. You have a proxy in Amsterdam Digital Ocean that tells it is microsoft.com
how hard could it be to find out that this is probably fake? 1 or probably 2
DNS queries for `microsoft.com`? In case of some CDN, there are ECS-powered
resolvers that are very capable to return results from POV of some subnets.
If censor sees no relevant results, will they be afraid to block IP?
2. You have a proxy in Amsterdam Digital Ocean that tells it is a website from
the same public subnet. But not the same. Would it be hard to make these DNS
queries and ban IP?
The correct way of having this proxy is following:
1. Register a domain name
2. Get some VPS, probably in your domestic location
3. Set that domain name from a step 1 to IP address of that VPS
4. Generate a couple of HTML pages by LLMs or even copy them from elsewhere
5. Set some webserver and issue TLS certificates with Let's Encrypt or any other
name
6. Set mtg before this webserver.
7. Use sing-box or anything like that to provide local socks5 interface and
have VPNized uplinks
8. Set up mtg to use socks5 from a 7 step.
In that case you will get a match of DNS and SNI in requests. As a side effect,
your proxy will work with XTLS and its friends: XTLS in sniff mode ignores
IP address a client wants to connect to. Instead, it reads SNI and connect
to resolved address: a clever idea if user does not have a trustworthy DNS
set up.
Yes, this is much longer that usual technique, and requires more effort. But
this is could probably be very well automated to some reasonable extent.
Unfortunately, this is a best practice right now.
Do not also forget about other implementation, like
[telemt](https://github.com/telemt/telemt). Try everything. Use VPNs. It does
not really matter which project you are going to use as long it helps you to
stay connected.
_March 2026._
+7 -26
View File
@@ -5,35 +5,17 @@ FROM golang:1.26-alpine AS build
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
# this is done for backward compatibility: before that we mounted a config
# into /config.toml. Some application allow mounting directories only,
# so it makes problems. So, instead we are going to do 2 steps:
# 1. Create /config/config.toml as a symlink to /config.toml
# 2. Force /mtg to use /config/config.toml
#
# it helps in both ways: users with directories could use /config directory
# and overlap a symlink by their bind mount. Old users could continue using
# /config.toml as a real config.
RUN set -x \ RUN set -x \
&& mkdir -p /config \ && apk --no-cache --update add \
&& ln -sv /config.toml /config/config.toml bash \
ca-certificates \
RUN --mount=type=cache,target=/var/cache/apk \ git
set -x \
&& apk --update add \
bash \
ca-certificates \
git
COPY go.mod go.sum /app/
WORKDIR /app
RUN go mod download
COPY . /app COPY . /app
WORKDIR /app
RUN set -x \ RUN set -x \
&& version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always 2>/dev/null || echo dev)" \ && version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \
&& go build \ && go build \
-trimpath \ -trimpath \
-mod=readonly \ -mod=readonly \
@@ -48,9 +30,8 @@ RUN set -x \
FROM scratch FROM scratch
ENTRYPOINT ["/mtg"] ENTRYPOINT ["/mtg"]
CMD ["run", "/config/config.toml"] CMD ["run", "/config.toml"]
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /app/mtg /mtg COPY --from=build /app/mtg /mtg
COPY --from=build /app/example.config.toml /config.toml COPY --from=build /app/example.config.toml /config.toml
COPY --from=build /config /config
-160
View File
@@ -29,8 +29,6 @@ are the most notable:
* [Official](https://github.com/TelegramMessenger/MTProxy) * [Official](https://github.com/TelegramMessenger/MTProxy)
* [Python](https://github.com/alexbers/mtprotoproxy) * [Python](https://github.com/alexbers/mtprotoproxy)
* [Erlang](https://github.com/seriyps/mtproto_proxy) * [Erlang](https://github.com/seriyps/mtproto_proxy)
* [Teleproxy (C)](https://github.com/teleproxy/teleproxy)
* [mtproto.zig (Zig)](https://github.com/sleep3r/mtproto.zig)
* [Telemt (Rust)](https://github.com/telemt/telemt) * [Telemt (Rust)](https://github.com/telemt/telemt)
You can use any of these. They work great and all implementations have You can use any of these. They work great and all implementations have
@@ -40,33 +38,6 @@ goal: to give a possibility to connect to Telegram in a restricted,
censored environment. But it does it slightly differently in details censored environment. But it does it slightly differently in details
that probably matter. that probably matter.
* **Domain fronting**
For years mtg supports domain fronting. This technique means that it fallbacks
to accessing a real website in case if request fails. It could fail by many
reasons: anti-replay protection, accidental access to the webserver or
stale request. Anyway, if mtg rejects this request, it does not break a
connection. It connects to the websites and replicates everything that client
has sent, and simply proxies it back as is. Users will see a response from
the real website, _byte-to-byte identical_ to the response of the real netloc.
* **Doppelganger**
mtg also is a doppelganger of the website it fronts. Sure, with domain fronting
users will see replies of the real website in case if something will go wrong.
But what about such cases when _everything is fine_?
In that case mtg mimics TLS connection statistical characteristics as close as
possible. Different application have different statistics of their patterns.
Big CDN steadily pumping the data, small websites burst with short easily
compressiable chunks of traffic.
mtg artificially emulates those delays to be statistically indistinguishable
from the real website even if it covers connection of the very specific app.
It also follows 2 most common patterns of traffic chunking, so censors
will have to put more resources to find out that we have Telegram here
but not a hookah webshop served by nginx.
* **Resource-efficient** * **Resource-efficient**
It has to be resource-efficient. It does not mean that you will see It has to be resource-efficient. It does not mean that you will see
@@ -93,9 +64,6 @@ that probably matter.
software. I also believe that in the case of throwout proxies, this software. I also believe that in the case of throwout proxies, this
the feature is a useless luxury. the feature is a useless luxury.
This is very controversial topic. Please read [rationale (in russian)](https://github.com/9seconds/mtg/issues/376#issuecomment-4118726699)
and use [mtg-multi](https://github.com/dolonet/mtg-multi) fork if you are disagree with.
* **No adtag support** * **No adtag support**
Please read [Version 2](#version-2) chapter. Please read [Version 2](#version-2) chapter.
@@ -125,8 +93,6 @@ that probably matter.
software (written in Golang) with a minimum effort + you can replace software (written in Golang) with a minimum effort + you can replace
some parts with those you want. some parts with those you want.
Please also to read about [best practices](https://github.com/9seconds/mtg/blob/master/BEST_PRACTICES.md).
### Version 2 ### Version 2
If you use version 1.x before, you are probably noticed some major If you use version 1.x before, you are probably noticed some major
@@ -306,38 +272,6 @@ For example, you've bought a VPS from [Digital
Ocean](https://www.digitalocean.com/). Then it might be a good idea to Ocean](https://www.digitalocean.com/). Then it might be a good idea to
generate a secret for _digitalocean.com_ then. generate a secret for _digitalocean.com_ then.
### Check configuration
There is a special command for secret verification:
```
$ mtg doctor /path/to/my/config.toml
Deprecated options
✅ All good
Time skewness
✅ Time drift is -607.048µs, but tolerate-time-skewness is 5s
Validate native network connectivity
✅ DC 1
✅ DC 2
✅ DC 3
✅ DC 4
✅ DC 5
✅ DC 203
Validate network connectivity with proxy socks5://127.0.0.1:1080
✅ DC 1
✅ DC 2
✅ DC 3
✅ DC 4
✅ DC 5
✅ DC 203
Validate fronting domain connectivity
✅ xx.xx.xx.xx:yyy is reachable
Validate SNI-DNS match
✅ IP address xx.xx.xx.xx matches secret hostname <REDACTED>
```
It aims to find out possible inconsistencies and problems with your
configuration. It makes sense to run it before executing any relevant commands.
### Simple run mode ### Simple run mode
@@ -417,7 +351,6 @@ ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
Restart=always Restart=always
RestartSec=3 RestartSec=3
DynamicUser=true DynamicUser=true
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install] [Install]
@@ -465,99 +398,6 @@ or if you are using docker:
$ docker exec mtg-proxy /mtg access /config.toml $ docker exec mtg-proxy /mtg access /config.toml
``` ```
## Doppelganger
mtg can mimic real websites, please take a look at relevant section in example
config file.
mtg comes with some very good precollected statistics coming from
[ok.ru](https://ok.ru/). It does not mean that you have to cover yourself
by pretending that mtg is _ok.ru_. **Do not do that: ok.ru comes from very specific
ASNs, but not from VPS providers you are going to use.** What I want to say
is that defaults are very good enough to use as is because ok.ru for public
pages has a very generic profile of TLS packets delay.
But for better results it is recommended to teach mtg about the website you
will use as a domain front. In order to do that, you need to specify URLs
from this website. Just go to it, open WebDeveloper console and pick up
random URLs. For better results they have to be **from the same domain name
you are going to use as a disguise** but serve light and heavy content: pages,
images etc. Do not use many, 2-3 will probably work.
mtg will crawl these pages periodically, accumulating statistics and
using it as you go.
```toml
[defense.doppelganger]
urls = [
"https://lalala.com/index.html",
"https://lalala.com/contacts.html",
]
```
This is not very necessary. Keep in mind these rules:
1. If you are not sure what is this all about, do nothing. Defaults are good.
2. All URLs must be HTTPS
3. All URLs should be from the same domain name (but this is not a rule)
4. Do not use a lot of pages. Use _different_ pages. mtg will start using this
statistics when it will accumulate enough anyway.
5. These URLs should be directly accessible from mtg without proxies whatsoever
6. Do not create huge raids. mtg will repeatedly crawl in raids, making N repeats.
Do not use high N, you do not want to be noticeable.
7. It makes no sense to have small delay between raids. Usually webservers
do not update their TLS settings each hour.
8. If you have some specific knowledge if webserver is using
[TLS Dynamic Record Sizing](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/), you
can use a very specific setting. This are Cloudflare, Go standard webservers,
[caddy](https://caddyserver.com/) and [H2O](https://h2o.examp1e.net/). If so,
you can enable `drs` setting.
9. **If you are not sure, touch nothing!**
## Troubleshooting
### `ip was blacklisted` for clients on the same LAN
If you run mtg at home and a client on the same LAN (for example, your
phone on the home Wi-Fi) cannot connect, check the proxy logs for a
message like:
```json
{"level":"info","ip":"10.0.1.1","logger":"proxy","message":"ip was blacklisted"}
```
The reason is that the default blocklist (`firehol_level1.netset`)
includes bogon networks, which covers all RFC1918 ranges
(`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). Any client
connecting from such an address is rejected by the blocklist —
the TCP connection is closed immediately with no response, so
from the client's point of view nothing loads at all.
There are three ways to resolve it:
1. Disable the blocklist entirely in `config.toml`:
```toml
[defense.blocklist]
enabled = false
```
Simplest option if the proxy is used only by you and people you trust.
2. Keep the blocklist but swap `firehol_level1` for a narrower list that
does not include bogons, for example `firehol_abusers_1d`:
```toml
[defense.blocklist]
enabled = true
urls = ["https://iplists.firehol.org/files/firehol_abusers_1d.netset"]
```
3. Connect to the proxy through a public IP or domain name with hairpin
NAT (`MASQUERADE`) on your router. mtg will then see the client with
its public address and the blocklist will not match. This is more
work to set up but preserves full blocklist protection.
## Metrics ## Metrics
Out of the box, mtg works with Out of the box, mtg works with
+1 -1
View File
@@ -12,7 +12,7 @@ type StableBloomFilterTestSuite struct {
} }
func (suite *StableBloomFilterTestSuite) TestOp() { func (suite *StableBloomFilterTestSuite) TestOp() {
filter := antireplay.NewStableBloomFilter(100000, 0.001) filter := antireplay.NewStableBloomFilter(500, 0.001)
suite.False(filter.SeenBefore([]byte{1, 2, 3})) suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6})) suite.False(filter.SeenBefore([]byte{4, 5, 6}))
-30
View File
@@ -1,30 +0,0 @@
{
# Caddy sits behind HAProxy which passes raw TLS through on :8443.
# ACME HTTP-01 challenges arrive on :80 via HAProxy's acl passthrough.
http_port 80
https_port 8443
# HAProxy forwards connections to :8443 with a PROXY protocol v2
# header (see haproxy.cfg `send-proxy-v2`). The proxy_protocol
# listener wrapper strips the header and exposes the real client IP
# to Caddy's access log. The `tls` wrapper must follow so that TLS
# is terminated on the unwrapped connection.
#
# `allow` lists the networks permitted to send PROXY headers. These
# ranges cover docker compose's default bridge networks; tighten
# them if you pin a specific subnet in docker-compose.yml.
servers :8443 {
listener_wrappers {
proxy_protocol {
timeout 5s
allow 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
}
tls
}
}
}
{$DOMAIN} {
root * /srv
file_server
}
-89
View File
@@ -1,89 +0,0 @@
# SNI-routing deployment for mtg
A turnkey `docker compose` setup that puts an SNI-aware TCP router
(HAProxy) in front of mtg **and** a real web server (Caddy with
automatic HTTPS).
## Why
Modern DPI systems actively probe suspected proxies. If the server
closes the connection or returns something unexpected, the IP gets
flagged. With this setup:
- **Telegram clients** connect to port 443, HAProxy sees the configured
SNI and routes them to mtg (FakeTLS).
- **Everything else** (browsers, DPI probes, scanners) is routed to
Caddy, which responds with a real Let's Encrypt certificate and serves
genuine web content.
Because your domain's DNS points to this server, the SNI/IP match is
natural and passive DPI has nothing to flag.
## Quick start
```bash
# 1. Point your domain's DNS A/AAAA record to this server's IP.
# 2. Generate an mtg secret:
docker run --rm nineseconds/mtg:2 generate-secret --hex YOUR_DOMAIN
# 3. Edit the config files:
# - mtg-config.toml → paste the secret
# - haproxy.cfg → replace "example.com" in the SNI ACL
# - .env or export → DOMAIN=your.domain
# 4. (Optional) put your site content into www/
# 5. Start:
docker compose up -d
# 6. Verify:
# - Open https://YOUR_DOMAIN in a browser → you should see the web page
# - Configure Telegram with the proxy link from:
docker compose exec mtg mtg access /config/config.toml
```
## Real client IPs (PROXY protocol)
HAProxy forwards TCP connections to mtg and Caddy with a PROXY protocol
v2 header so both backends see the real client IP instead of HAProxy's
container address. The three pieces must stay in sync:
- `haproxy.cfg``send-proxy-v2` on the `mtg` and `web` backend `server` lines
- `mtg-config.toml``proxy-protocol-listener = true`
- `Caddyfile``listener_wrappers { proxy_protocol { ... } tls }` on `:8443`
If you disable one, disable all three, otherwise the backend will fail
to parse the connection.
## ACME (Let's Encrypt) notes
HAProxy passes `/.well-known/acme-challenge/` requests on `:80` to
Caddy so that HTTP-01 validation works out of the box. Make sure your
domain's DNS A/AAAA record points to this server before starting.
## Architecture
```
┌──────────────────┐
:443 ──────>│ HAProxy │
│ (TCP, SNI peek) │
└──┬───────────┬───┘
SNI match │ │ default
v v
┌─────────┐ ┌─────────┐
│ mtg │ │ Caddy │
│ :3128 │ │ :8443 │
│ FakeTLS │ │ real TLS│
└─────────┘ └─────────┘
```
## Files
| File | Purpose |
|---|---|
| `docker-compose.yml` | Service definitions |
| `haproxy.cfg` | SNI routing rules — **edit the domain** |
| `mtg-config.toml` | mtg proxy config — **paste your secret** |
| `Caddyfile` | Web server config (auto-HTTPS) |
| `www/` | Static site content served by Caddy |
-54
View File
@@ -1,54 +0,0 @@
# SNI-routing deployment: HAProxy (443) -> mtg + real web backend
#
# This setup puts an SNI-aware TCP router in front of mtg so that:
# - Telegram clients (FakeTLS with the correct SNI) are routed to mtg
# - All other TLS traffic (including DPI probes) reaches the real web
# server, which responds with a genuine certificate
#
# The result: active probes see a real website; passive DPI sees matching
# SNI/IP because the domain resolves to this server's IP.
#
# Quick start:
# 1. Set YOUR_DOMAIN below (and in mtg-config.toml)
# 2. docker compose up -d
# 3. mtg generate-secret YOUR_DOMAIN -> put it in mtg-config.toml
# 4. docker compose restart mtg
#
# See BEST_PRACTICES.md and the project wiki for background.
services:
haproxy:
image: haproxy:lts-alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on:
- mtg
- web
restart: unless-stopped
mtg:
image: nineseconds/mtg:2
volumes:
- ./mtg-config.toml:/config/config.toml:ro
expose:
- "3128"
restart: unless-stopped
web:
image: caddy:alpine
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- ./www:/srv:ro
expose:
- "80"
- "8443"
environment:
DOMAIN: ${DOMAIN:-example.com}
restart: unless-stopped
volumes:
caddy_data:
-62
View File
@@ -1,62 +0,0 @@
# HAProxy SNI router — Layer 4 (TCP mode)
#
# Inspects the SNI in the TLS ClientHello and routes traffic:
# - SNI matching the mtg secret domain -> mtg (FakeTLS / MTProto)
# - Everything else -> real web backend (Caddy)
#
# Because routing happens before TLS termination, each backend sees the
# raw ClientHello and handles TLS itself. The real web backend therefore
# presents a genuine certificate to any probe or browser.
global
log stdout format raw local0 info
maxconn 4096
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 60s
timeout server 60s
# --- HTTP :80 — ACME challenges + redirect -----------------------------------
frontend http
bind *:80
mode http
# Let Caddy answer ACME HTTP-01 challenges for Let's Encrypt.
acl is_acme path_beg /.well-known/acme-challenge/
use_backend web_acme if is_acme
http-request redirect scheme https code 301
# --- TLS :443 — SNI-based routing -------------------------------------------
frontend tls
bind *:443
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# Route Telegram clients to mtg.
# Replace "example.com" with the domain from your mtg secret.
use_backend mtg if { req_ssl_sni -i example.com }
default_backend web
backend mtg
# send-proxy-v2 prepends a PROXY protocol v2 header so mtg sees the
# real client IP instead of HAProxy's. mtg must have
# `proxy-protocol-listener = true` in its config.
server mtg mtg:3128 send-proxy-v2
backend web
# send-proxy-v2 prepends a PROXY protocol v2 header so Caddy logs the
# real client IP instead of HAProxy's. Caddy must enable the
# proxy_protocol listener wrapper on :8443 (see Caddyfile).
server web web:8443 send-proxy-v2
backend web_acme
mode http
server web web:80
-17
View File
@@ -1,17 +0,0 @@
# Minimal mtg configuration for the SNI-router setup.
#
# 1. Generate a secret: mtg generate-secret --hex example.com
# 2. Paste it below.
# 3. Replace example.com with your actual domain everywhere.
secret = "PASTE_YOUR_SECRET_HERE"
bind-to = "0.0.0.0:3128"
# HAProxy in front sends PROXY protocol v2 headers so mtg can see the
# real client IP. Keep this in sync with haproxy.cfg (`send-proxy-v2`).
proxy-protocol-listener = true
[defense.anti-replay]
enabled = true
max-size = "1mib"
error-rate = 0.001
-5
View File
@@ -1,5 +0,0 @@
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Welcome</title></head>
<body><h1>It works!</h1><p>Replace this with your own content.</p></body>
</html>
BIN
View File
Binary file not shown.
-42
View File
@@ -1,42 +0,0 @@
package essentials
// TelegramCoreAddresses are publicly known addresses of Telegram core network.
var TelegramCoreAddresses = map[int][]string{
1: {
"149.154.175.50:443",
"[2001:b28:f23d:f001::a]:443",
},
2: {
"149.154.167.51:443",
"95.161.76.100:443",
"[2001:67c:04e8:f002::a]:443",
},
3: {
"149.154.175.100:443",
"[2001:b28:f23d:f003::a]:443",
},
4: {
"149.154.167.91:443",
"[2001:67c:04e8:f004::a]:443",
},
5: {
"149.154.171.5:443",
"[2001:b28:f23f:f005::a]:443",
},
203: {
"91.105.192.100:443",
"[2a0a:f280:0203:a:5000::100]:443",
},
10001: {
"149.154.175.10:443",
"[2001:b28:f23d:f001::e]:443",
},
10002: {
"149.154.167.40:443",
"[2001:67c:4e8:f002::e]:443",
},
10003: {
"149.154.175.117:443",
"[2001:b28:f23d:f003::e]:443",
},
}
+1 -1
View File
@@ -38,7 +38,7 @@ func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
case <-e.ctx.Done(): case <-e.ctx.Done():
case e.chans[chanNo%uint32(len(e.chans))] <- evt: case e.chans[int(chanNo)%len(e.chans)] <- evt:
} }
} }
+5 -99
View File
@@ -48,13 +48,6 @@ concurrency = 8192
# Only ipv4 connectivity is used # Only ipv4 connectivity is used
prefer-ip = "prefer-ipv6" prefer-ip = "prefer-ipv6"
# Public IP addresses of this server. Used by 'mtg access' to generate
# proxy links and by 'mtg doctor' to validate SNI-DNS match.
# If not set, mtg tries to detect them automatically via ifconfig.co.
# Set these if ifconfig.co is unreachable from your server.
# public-ipv4 = "1.2.3.4"
# public-ipv6 = "2001:db8::1"
# If this setting is set, then mtg will try to get proxy updates from Telegram # 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 # Usually this is completely fine to have it disabled, because mtg has a list
# of some core proxies hardcoded. # of some core proxies hardcoded.
@@ -204,82 +197,14 @@ proxies = [
# define a global timeout on establishing of network connections. idle # define a global timeout on establishing of network connections. idle
# means a timeout on pumping data between sockset when nothing is # means a timeout on pumping data between sockset when nothing is
# happening. # happening.
#
# please be noticed that handshakes have no timeouts intentionally. You can
# find a reasoning here:
# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
[network.timeout] [network.timeout]
tcp = "5s" tcp = "5s"
http = "10s" http = "10s"
idle = "5m" idle = "1m"
handshake = "10s"
# this defines a configuration for TCP keep alives. Default values are taken
# from Golang default behavior.
[network.keep-alive]
disabled = false
# idle means a time period after which we start sending TCP Keep Alive probes
idle = "15s"
# interval is a period between 2 consecutive probes
interval = "15s"
# if we miss that many probes, a connection will be considered as a dead one.
count = 9
# mtg has to mimic real websites. It does not mean domain fronting, it also
# means that traffic characteristics should be similar to real world traffic.
# websites and applications behave differently, their traffic patterns are also
# different. Applications do bursts of RPC-style messages (or JSON communication,
# does not really matter), while websites pump heavy content in HTTP2 streams
#
# It means that statistically there is a different between traffic shape:
# delays between packets are also different.
# In order to avoid censorship detection based on these patterns, there is a
# mtg subsystem called "Doppelganger" that aims to mimic website statistics
# as close as it could.
#
# Delays between TLS packets are not constant. There are many factors
# that come in play. Application should generate some response, it could
# send some headers first and stream content with chunked encoding. So
# some first packets could come as soon as possible, with some delays
# after first ones. Such phenomenon is described by different statistic
# distribution. There are 2 distribution that describe it: lognormal
# distribution and Weibul distribution. Lognormal is all about steady streams
# of heavy content like a video. Weibul is great about short bursts like
# user who requested a static page an a couple of images.
[defense.doppelganger]
# This is a list of URLs that would be crawled by mtg to approximate delay
# statistics. They MUST be HTTPS urls.
#
# You can come to the website and collect different URLs, with light and
# heavy content. We recommend to search for CDNs.
urls = [
# "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
]
# A collection is done in raids. Each raid makes this number of requests to
# each URL in this list. Do not use a huge number, 10 is probably ok.
repeats-per-raid = 10
# This is a duration between each raid. It makes no sense to have a small number
# here as you would start to make a noticeable activity. Usually traffic patterns
# do not change a lot, so do not expect different results if you request
# each 10 minutes.
raid-each = "6h"
# This enables dynamic tls record sizing.
#
# Some modern stacks and platforms start to use the technique that is called
# DRS. They start with small TLS packets and ramp up eventually. First packets
# are usually about MTU size, after that we get 4k and eventually max size.
# This is done with a good intention: to minimize a time to the first byte,
# so application could start doing something with the data right after first
# RTT.
#
# Apparently, about 90% of application do not employ this technique, they use
# max size always: nginx, apache, java stuff. But Golang tools, angie and
# some specific patches activate this technique.
#
# In order to mimic a real website we need to know something about software
# it uses. Usually nobody cares: openssl does 16384, Python does it, nginx
# does it. So this setting is disabled by default.
#
# https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
# https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
# https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
drs = false
# Some countries do active probing on Telegram connections. This technique # Some countries do active probing on Telegram connections. This technique
# allows to protect from such effort. # allows to protect from such effort.
@@ -316,17 +241,6 @@ download-concurrency = 2
# A list of URLs in FireHOL format (https://iplists.firehol.org/) # A list of URLs in FireHOL format (https://iplists.firehol.org/)
# You can provider links here (starts with https:// or http://) or # You can provider links here (starts with https:// or http://) or
# path to a local file, but in this case it should be absolute. # path to a local file, but in this case it should be absolute.
#
# NOTE: the default list below (firehol_level1.netset) includes bogon
# networks, and therefore RFC1918 ranges as well (10.0.0.0/8,
# 172.16.0.0/12, 192.168.0.0/16). If you run mtg on a home/LAN network
# and connect from a client on the same LAN, that client will be
# rejected with "ip was blacklisted" and the connection dropped (TCP
# close, no response). If you see this, you can either disable this section
# (enabled = false), replace firehol_level1 with a narrower list that
# does not include bogons (e.g. firehol_abusers_1d), or connect via
# a public IP/domain with hairpin NAT on your router. See README for
# details.
urls = [ urls = [
"https://iplists.firehol.org/files/firehol_level1.netset", "https://iplists.firehol.org/files/firehol_level1.netset",
# "/local.file" # "/local.file"
@@ -378,11 +292,3 @@ bind-to = "127.0.0.1:3129"
http-path = "/" http-path = "/"
# prefix for metrics for prometheus # prefix for metrics for prometheus
metric-prefix = "mtg" metric-prefix = "mtg"
[dc]
dc1 = [
"149.154.175.50:443",
"149.154.175.54:443",
"149.154.175.59:443",
"[2001:b28:f23d:f001::a]:443"
]
+11 -12
View File
@@ -4,32 +4,31 @@ go 1.26
require ( require (
github.com/OneOfOne/xxhash v1.2.8 github.com/OneOfOne/xxhash v1.2.8
github.com/alecthomas/kong v1.15.0 github.com/alecthomas/kong v1.14.0
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
github.com/d4l3k/messagediff v1.2.1 // indirect github.com/d4l3k/messagediff v1.2.1 // indirect
github.com/jarcoal/httpmock v1.0.8 github.com/jarcoal/httpmock v1.0.8
github.com/mccutchen/go-httpbin v1.1.1 github.com/mccutchen/go-httpbin v1.1.1
github.com/panjf2000/ants/v2 v2.12.0 github.com/panjf2000/ants/v2 v2.11.5
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.20.0 // indirect
github.com/rs/zerolog v1.35.0 github.com/rs/zerolog v1.34.0
github.com/smira/go-statsd v1.3.4 github.com/smira/go-statsd v1.3.4
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b github.com/tylertreat/BoomFilters v0.0.0-20251117164519-53813c36cc1b
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.48.0
golang.org/x/net v0.52.0 golang.org/x/net v0.51.0
golang.org/x/sys v0.42.0 golang.org/x/sys v0.41.0
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )
require ( require (
github.com/beevik/ntp v1.5.0 github.com/ncruces/go-dns v1.3.2
github.com/ncruces/go-dns v1.3.3 github.com/pelletier/go-toml/v2 v2.2.4
github.com/pelletier/go-toml/v2 v2.3.0
github.com/pires/go-proxyproto v0.11.0 github.com/pires/go-proxyproto v0.11.0
github.com/things-go/go-socks5 v0.1.0 github.com/things-go/go-socks5 v0.1.0
github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e
@@ -50,8 +49,8 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect github.com/txthinking/runnergroup v0.0.0-20250224021307-5864ffeb65ae // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+31 -24
View File
@@ -2,8 +2,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
@@ -12,18 +12,18 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -38,8 +38,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE= github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
@@ -48,16 +51,17 @@ github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-dns v1.3.3 h1:59OV7XoJrTCoUMZjWRVs4GOjtntMTZqiQ5Mn+BT13hk= github.com/ncruces/go-dns v1.3.2 h1:kBLuUZBgkQ4qF4WDXZRQ4rG0Gk6sLVJQ5tESkWrxUa0=
github.com/ncruces/go-dns v1.3.3/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s= github.com/ncruces/go-dns v1.3.2/go.mod h1:tuzixNY8PY/M7yUzcvRbUaeLs3ifIdydpi5H2bfRU+s=
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8= github.com/panjf2000/ants/v2 v2.11.5 h1:a7LMnMEeux/ebqTux140tRiaqcFTV0q2bEHF03nl6Rg=
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY= github.com/panjf2000/ants/v2 v2.11.5/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -66,12 +70,13 @@ 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/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 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c= github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c=
github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A= github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -100,12 +105,12 @@ github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
@@ -114,22 +119,24 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+42 -8
View File
@@ -1,16 +1,22 @@
package cli package cli
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
"strings"
"sync" "sync"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils" "github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/mtglib"
) )
type accessResponse struct { type accessResponse struct {
@@ -59,10 +65,7 @@ func (a *Access) Run(cli *CLI, version string) error {
wg.Go(func() { wg.Go(func() {
ip := a.PublicIPv4 ip := a.PublicIPv4
if ip == nil { if ip == nil {
ip = conf.PublicIPv4.Get(nil) ip = a.getIP(ntw, "tcp4")
}
if ip == nil {
ip = getIP(ntw, "tcp4")
} }
if ip != nil { if ip != nil {
@@ -74,10 +77,7 @@ func (a *Access) Run(cli *CLI, version string) error {
wg.Go(func() { wg.Go(func() {
ip := a.PublicIPv6 ip := a.PublicIPv6
if ip == nil { if ip == nil {
ip = conf.PublicIPv6.Get(nil) ip = a.getIP(ntw, "tcp6")
}
if ip == nil {
ip = getIP(ntw, "tcp6")
} }
if ip != nil { if ip != nil {
@@ -100,6 +100,40 @@ func (a *Access) Run(cli *CLI, version string) error {
return nil return nil
} }
func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP {
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
return ntw.DialContext(ctx, protocol, address) //nolint: wrapcheck
})
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
if err != nil {
panic(err)
}
req.Header.Add("Accept", "text/plain")
resp, err := client.Do(req)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusOK {
return nil
}
defer func() {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return net.ParseIP(strings.TrimSpace(string(data)))
}
func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs {
if ip == nil { if ip == nil {
return nil return nil
-1
View File
@@ -4,7 +4,6 @@ import "github.com/alecthomas/kong"
type CLI struct { type CLI struct {
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"` GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
Doctor Doctor `kong:"cmd,help='Check that proxy can run correctly'"`
Access Access `kong:"cmd,help='Print access information.'"` Access Access `kong:"cmd,help='Print access information.'"`
Run Run `kong:"cmd,help='Run proxy.'"` Run Run `kong:"cmd,help='Run proxy.'"`
SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"` SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"`
-394
View File
@@ -1,394 +0,0 @@
package cli
import (
"context"
"errors"
"fmt"
"maps"
"net"
"os"
"slices"
"strconv"
"strings"
"text/template"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network/v2"
"github.com/beevik/ntp"
)
var (
tplError = template.Must(
template.New("").Parse(" ‼️ {{ .description }}: {{ .error }}\n"),
)
tplWDeprecatedConfig = template.Must(
template.New("").
Parse(` ⚠️ Option {{ .old | printf "%q" }}{{ if .old_section }} from section [{{ .old_section }}]{{ end }} is deprecated and will be removed in v{{ .when }}. Please use {{ .new | printf "%q" }}{{ if .new_section }} in [{{ .new_section }}] section{{ end }} instead.` + "\n"),
)
tplOTimeSkewness = template.Must(
template.New("").
Parse(" ✅ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}\n"),
)
tplWTimeSkewness = template.Must(
template.New("").
Parse(" ⚠️ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. Please check ntp.\n"),
)
tplETimeSkewness = template.Must(
template.New("").
Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"),
)
tplODCConnect = template.Must(
template.New("").Parse(" ✅ DC {{ .dc }}\n"),
)
tplEDCConnect = template.Must(
template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"),
)
tplODCIPConnect = template.Must(
template.New("").Parse(" ✅ {{ .ip }}\n"),
)
tplEDCIPConnect = template.Must(
template.New("").Parse(" ❌ {{ .ip }}: {{ .error }}\n"),
)
tplODNSSNIMatch = template.Must(
template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
)
tplEDNSSNIMatch = template.Must(
template.New("").Parse(" ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"),
)
tplOFrontingDomain = template.Must(
template.New("").Parse(" ✅ {{ .address }} is reachable\n"),
)
tplEFrontingDomain = template.Must(
template.New("").Parse(" ❌ {{ .address }}: {{ .error }}\n"),
)
)
type Doctor struct {
conf *config.Config
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
}
func (d *Doctor) Run(cli *CLI, version string) error {
conf, err := utils.ReadConfig(d.ConfigPath)
if err != nil {
return fmt.Errorf("cannot init config: %w", err)
}
d.conf = conf
fmt.Println("Deprecated options")
everythingOK := d.checkDeprecatedConfig()
fmt.Println("Time skewness")
everythingOK = d.checkTimeSkewness() && everythingOK
resolver, err := network.GetDNS(conf.GetDNS())
if err != nil {
return fmt.Errorf("cannot create DNS resolver: %w", err)
}
base := network.New(
resolver,
"",
conf.Network.Timeout.TCP.Get(10*time.Second),
conf.Network.Timeout.HTTP.Get(0),
conf.Network.Timeout.Idle.Get(0),
net.KeepAliveConfig{
Enable: !conf.Network.KeepAlive.Disabled.Get(false),
Idle: conf.Network.KeepAlive.Idle.Get(0),
Interval: conf.Network.KeepAlive.Interval.Get(0),
Count: int(conf.Network.KeepAlive.Count.Get(0)),
},
)
fmt.Println("Validate native network connectivity")
everythingOK = d.checkNetwork(base) && everythingOK
for _, url := range conf.Network.Proxies {
value, err := network.NewProxyNetwork(base, url.Get(nil))
if err != nil {
return err
}
fmt.Printf("Validate network connectivity with proxy %s\n", url.Get(nil))
everythingOK = d.checkNetwork(value) && everythingOK
}
fmt.Println("Validate fronting domain connectivity")
everythingOK = d.checkFrontingDomain(base) && everythingOK
fmt.Println("Validate SNI-DNS match")
everythingOK = d.checkSecretHost(resolver, base) && everythingOK
if !everythingOK {
os.Exit(1)
}
return nil
}
func (d *Doctor) checkDeprecatedConfig() bool {
ok := true
if d.conf.DomainFrontingIP.Value != nil {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-ip",
"old_section": "",
"new": "ip",
"new_section": "domain-fronting",
})
}
if d.conf.DomainFrontingPort.Value != 0 {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-port",
"old_section": "",
"new": "port",
"new_section": "domain-fronting",
})
}
if d.conf.DomainFrontingProxyProtocol.Value {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "domain-fronting-proxy-protocol",
"old_section": "",
"new": "proxy-protocol",
"new_section": "domain-fronting",
})
}
if d.conf.Network.DOHIP.Value != nil {
ok = false
tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
"when": "2.3.0",
"old": "doh-ip",
"old_section": "network",
"new": "dns",
"new_section": "network",
})
}
if ok {
fmt.Println(" ✅ All good")
}
return ok
}
func (d *Doctor) checkTimeSkewness() bool {
response, err := ntp.Query("0.pool.ntp.org")
if err != nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": "cannot access ntp pool",
"error": err,
})
return false
}
skewness := response.ClockOffset.Abs()
confValue := d.conf.TolerateTimeSkewness.Get(mtglib.DefaultTolerateTimeSkewness)
diff := float64(skewness) / float64(confValue)
tplData := map[string]any{
"drift": response.ClockOffset,
"value": confValue,
}
switch {
case diff < 0.3:
tplOTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
return true
case diff < 0.7:
tplWTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
default:
tplETimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
}
return false
}
func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses))
slices.Sort(dcs)
ok := true
for _, dc := range dcs {
err := d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc])
if err == nil {
tplODCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"dc": dc,
})
} else {
tplEDCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"dc": dc,
"error": err,
})
ok = false
}
}
return ok
}
func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error {
checkAddresses := []string{}
switch d.conf.PreferIP.Get("prefer-ip4") {
case "only-ipv4":
for _, addr := range addresses {
host, _, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
checkAddresses = append(checkAddresses, addr)
}
}
case "only-ipv6":
for _, addr := range addresses {
host, _, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
checkAddresses = append(checkAddresses, addr)
}
}
default:
checkAddresses = addresses
}
if len(checkAddresses) == 0 {
return fmt.Errorf("no suitable addresses after IP version filtering")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var (
conn net.Conn
err error
)
for _, addr := range checkAddresses {
conn, err = ntw.DialContext(ctx, "tcp", addr)
if err != nil {
tplEDCIPConnect.Execute(os.Stdout, map[string]any{
"error": err,
"ip": addr,
})
continue
}
tplODCIPConnect.Execute(os.Stdout, map[string]any{
"ip": addr,
})
conn.Close() //nolint: errcheck
return nil
}
return err
}
func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
host := d.conf.Secret.Host
if ip := d.conf.GetDomainFrontingIP(nil); ip != "" {
host = ip
}
port := d.conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort)
address := net.JoinHostPort(host, strconv.Itoa(int(port)))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dialer := ntw.NativeDialer()
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
tplEFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"address": address,
"error": err,
})
return false
}
conn.Close() //nolint: errcheck
tplOFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"address": address,
})
return true
}
func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
if err != nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
"error": err,
})
return false
}
ourIP4 := d.conf.PublicIPv4.Get(nil)
if ourIP4 == nil {
ourIP4 = getIP(ntw, "tcp4")
}
ourIP6 := d.conf.PublicIPv6.Get(nil)
if ourIP6 == nil {
ourIP6 = getIP(ntw, "tcp6")
}
if ourIP4 == nil && ourIP6 == nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": "cannot detect public IP address",
"error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
})
return false
}
strAddresses := []string{}
for _, value := range addresses {
if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"ip": value.IP,
"hostname": d.conf.Secret.Host,
})
return true
}
strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
}
tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"hostname": d.conf.Secret.Host,
"resolved": strings.Join(strAddresses, ", "),
"ip4": ourIP4,
"ip6": ourIP6,
})
return false
}
+2 -21
View File
@@ -46,19 +46,13 @@ func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
base := network.New( base := network.New(
resolver, resolver,
"", "mtg/"+version,
conf.Network.Timeout.TCP.Get(0), conf.Network.Timeout.TCP.Get(0),
conf.Network.Timeout.HTTP.Get(0), conf.Network.Timeout.HTTP.Get(0),
conf.Network.Timeout.Idle.Get(0), conf.Network.Timeout.Idle.Get(0),
net.KeepAliveConfig{
Enable: !conf.Network.KeepAlive.Disabled.Get(false),
Idle: conf.Network.KeepAlive.Idle.Get(0),
Interval: conf.Network.KeepAlive.Interval.Get(0),
Count: int(conf.Network.KeepAlive.Count.Get(0)),
},
) )
proxyDialers := make([]mtglib.Network, len(conf.Network.Proxies)) proxyDialers := make([]network.Network, len(conf.Network.Proxies))
for idx, v := range conf.Network.Proxies { for idx, v := range conf.Network.Proxies {
value, err := network.NewProxyNetwork(base, v.Get(nil)) value, err := network.NewProxyNetwork(base, v.Get(nil))
if err != nil { if err != nil {
@@ -245,11 +239,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
return fmt.Errorf("cannot build ip allowlist: %w", err) return fmt.Errorf("cannot build ip allowlist: %w", err)
} }
doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
for i, v := range conf.Defense.Doppelganger.URLs {
doppelGangerURLs[i] = v.String()
}
opts := mtglib.ProxyOpts{ opts := mtglib.ProxyOpts{
Logger: logger, Logger: logger,
Network: ntw, Network: ntw,
@@ -259,7 +248,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
EventStream: eventStream, EventStream: eventStream,
Secret: conf.Secret, Secret: conf.Secret,
Concurrency: conf.GetConcurrency(mtglib.DefaultConcurrency),
DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort), DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort),
DomainFrontingIP: conf.GetDomainFrontingIP(nil), DomainFrontingIP: conf.GetDomainFrontingIP(nil),
DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false), DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false),
@@ -268,13 +256,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout),
HandshakeTimeout: conf.Network.Timeout.Handshake.Get(mtglib.DefaultHandshakeTimeout),
DoppelGangerURLs: doppelGangerURLs,
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
DoppelGangerEach: conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
DoppelGangerDRS: conf.Defense.Doppelganger.DRS.Get(false),
} }
proxy, err := mtglib.NewProxy(opts) proxy, err := mtglib.NewProxy(opts)
-51
View File
@@ -1,51 +0,0 @@
package cli
import (
"context"
"io"
"net"
"net/http"
"strings"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib"
)
func getIP(ntw mtglib.Network, protocol string) net.IP {
dialer := ntw.NativeDialer()
client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
conn, err := dialer.DialContext(ctx, protocol, address)
if err != nil {
return nil, err
}
return essentials.WrapNetConn(conn), err
})
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
if err != nil {
panic(err)
}
req.Header.Add("Accept", "text/plain")
resp, err := client.Do(req)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusOK {
return nil
}
defer func() {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return net.ParseIP(strings.TrimSpace(string(data)))
}
+5 -38
View File
@@ -35,8 +35,6 @@ type Config struct {
DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"` DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"`
TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"`
Concurrency TypeConcurrency `json:"concurrency"` Concurrency TypeConcurrency `json:"concurrency"`
PublicIPv4 TypeIP `json:"publicIpv4"`
PublicIPv6 TypeIP `json:"publicIpv6"`
DomainFronting struct { DomainFronting struct {
IP TypeIP `json:"ip"` IP TypeIP `json:"ip"`
Port TypePort `json:"port"` Port TypePort `json:"port"`
@@ -49,28 +47,15 @@ type Config struct {
MaxSize TypeBytes `json:"maxSize"` MaxSize TypeBytes `json:"maxSize"`
ErrorRate TypeErrorRate `json:"errorRate"` ErrorRate TypeErrorRate `json:"errorRate"`
} `json:"antiReplay"` } `json:"antiReplay"`
Blocklist ListConfig `json:"blocklist"` Blocklist ListConfig `json:"blocklist"`
Allowlist ListConfig `json:"allowlist"` Allowlist ListConfig `json:"allowlist"`
Doppelganger struct {
URLs []TypeHttpsURL `json:"urls"`
Repeats TypeConcurrency `json:"repeats_per_raid"`
UpdateEach TypeDuration `json:"raid_each"`
DRS TypeBool `json:"drs"`
} `json:"doppelganger"`
} `json:"defense"` } `json:"defense"`
Network struct { Network struct {
Timeout struct { Timeout struct {
TCP TypeDuration `json:"tcp"` TCP TypeDuration `json:"tcp"`
HTTP TypeDuration `json:"http"` HTTP TypeDuration `json:"http"`
Idle TypeDuration `json:"idle"` Idle TypeDuration `json:"idle"`
Handshake TypeDuration `json:"handshake"`
} `json:"timeout"` } `json:"timeout"`
KeepAlive struct {
Disabled TypeBool `json:"disabled"`
Idle TypeDuration `json:"idle"`
Interval TypeDuration `json:"interval"`
Count TypeConcurrency `json:"count"`
} `json:"keepAlive"`
DOHIP TypeIP `json:"dohIp"` DOHIP TypeIP `json:"dohIp"`
DNS TypeDNSURI `json:"dns"` DNS TypeDNSURI `json:"dns"`
Proxies []TypeProxyURL `json:"proxies"` Proxies []TypeProxyURL `json:"proxies"`
@@ -91,24 +76,6 @@ type Config struct {
MetricPrefix TypeMetricPrefix `json:"metricPrefix"` MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
} `json:"prometheus"` } `json:"prometheus"`
} `json:"stats"` } `json:"stats"`
DCs struct{
DC1 []TypeHostPort `json:"dc1"`
DC2 []TypeHostPort `json:"dc2"`
DC3 []TypeHostPort `json:"dc3"`
DC4 []TypeHostPort `json:"dc4"`
DC5 []TypeHostPort `json:"dc5"`
DC203 []TypeHostPort `json:"dc203"`
DC10001 []TypeHostPort `json:"dc10001"`
DC10002[]TypeHostPort `json:"dc10002"`
DC10003 []TypeHostPort `json:"dc10003"`
} `json:"dc"`
}
func (c *Config) GetConcurrency(defaultValue uint) uint {
if concurrency := c.Concurrency.Get(0); concurrency != 0 {
return concurrency
}
return c.Concurrency.Get(defaultValue)
} }
func (c *Config) GetDNS() *url.URL { func (c *Config) GetDNS() *url.URL {
-26
View File
@@ -42,32 +42,6 @@ func (suite *ConfigTestSuite) TestParseMinimalConfig() {
suite.Equal("0.0.0.0:3128", conf.BindTo.String()) suite.Equal("0.0.0.0:3128", conf.BindTo.String())
} }
func (suite *ConfigTestSuite) TestParsePublicIP() {
conf, err := config.Parse(suite.ReadConfig("public_ip.toml"))
suite.NoError(err)
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
suite.Equal("2001:db8::1", conf.PublicIPv6.Get(nil).String())
}
func (suite *ConfigTestSuite) TestParsePublicIPv4Only() {
conf, err := config.Parse(suite.ReadConfig("public_ip_v4_only.toml"))
suite.NoError(err)
suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String())
suite.Nil(conf.PublicIPv6.Get(nil))
}
func (suite *ConfigTestSuite) TestParsePublicIPInvalid() {
_, err := config.Parse(suite.ReadConfig("public_ip_invalid.toml"))
suite.Error(err)
}
func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
suite.Nil(conf.PublicIPv4.Get(nil))
suite.Nil(conf.PublicIPv6.Get(nil))
}
func (suite *ConfigTestSuite) TestString() { func (suite *ConfigTestSuite) TestString() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml")) conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err) suite.NoError(err)
+3 -29
View File
@@ -21,8 +21,6 @@ type tomlConfig struct {
DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"` DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"`
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"`
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
PublicIPv4 string `toml:"public-ipv4" json:"publicIpv4,omitempty"`
PublicIPv6 string `toml:"public-ipv6" json:"publicIpv6,omitempty"`
DomainFronting struct { DomainFronting struct {
IP string `toml:"ip" json:"ip,omitempty"` IP string `toml:"ip" json:"ip,omitempty"`
Port uint `toml:"port" json:"port,omitempty"` Port uint `toml:"port" json:"port,omitempty"`
@@ -46,26 +44,13 @@ type tomlConfig struct {
URLs []string `toml:"urls" json:"urls,omitempty"` URLs []string `toml:"urls" json:"urls,omitempty"`
UpdateEach string `toml:"update-each" json:"updateEach,omitempty"` UpdateEach string `toml:"update-each" json:"updateEach,omitempty"`
} `toml:"allowlist" json:"allowlist,omitempty"` } `toml:"allowlist" json:"allowlist,omitempty"`
Doppelganger struct {
URLs []string `toml:"urls" json:"urls,omitempty"`
Repeats uint `toml:"repeats-per-raid" json:"repeats_per_raid,omitempty"`
UpdateEach string `toml:"raid-each" json:"raid_each,omitempty"`
DRS bool `toml:"drs" json:"drs,omitempty"`
} `toml:"doppelganger" json:"doppelganger,omitempty"`
} `toml:"defense" json:"defense,omitempty"` } `toml:"defense" json:"defense,omitempty"`
Network struct { Network struct {
Timeout struct { Timeout struct {
TCP string `toml:"tcp" json:"tcp,omitempty"` TCP string `toml:"tcp" json:"tcp,omitempty"`
HTTP string `toml:"http" json:"http,omitempty"` HTTP string `toml:"http" json:"http,omitempty"`
Idle string `toml:"idle" json:"idle,omitempty"` Idle string `toml:"idle" json:"idle,omitempty"`
Handshake string `toml:"handshake" json:"handshake,omitempty"`
} `toml:"timeout" json:"timeout,omitempty"` } `toml:"timeout" json:"timeout,omitempty"`
KeepAlive struct {
Disabled bool `toml:"disabled" json:"disabled,omitempty"`
Idle string `toml:"idle" json:"idle,omitempty"`
Interval string `toml:"interval" json:"interval,omitempty"`
Count uint `toml:"count" json:"count,omitempty"`
} `toml:"keep-alive" json:"keepAlive,omitempty"`
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
DNS string `toml:"dns" json:"dns,omitempty"` DNS string `toml:"dns" json:"dns,omitempty"`
Proxies []string `toml:"proxies" json:"proxies,omitempty"` Proxies []string `toml:"proxies" json:"proxies,omitempty"`
@@ -84,17 +69,6 @@ type tomlConfig struct {
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
} `toml:"prometheus" json:"prometheus,omitempty"` } `toml:"prometheus" json:"prometheus,omitempty"`
} `toml:"stats" json:"stats,omitempty"` } `toml:"stats" json:"stats,omitempty"`
DCs struct{
DC1 []string `toml:"dc1" json:"dc1,omitempty"`
DC2 []string `toml:"dc2" json:"dc2,omitempty"`
DC3 []string `toml:"dc3" json:"dc3,omitempty"`
DC4 []string `toml:"dc4" json:"dc4,omitempty"`
DC5 []string `toml:"dc5" json:"dc5,omitempty"`
DC203 []string `toml:"dc203" json:"dc203,omitempty"`
DC10001 []string `toml:"dc10001" json:"dc10001,omitempty"`
DC10002 []string `toml:"dc10002" json:"dc10002,omitempty"`
DC10003 []string `toml:"dc10003" json:"dc10003,omitempty"`
} `toml:"dc" json:"dc,omitempty"`
} }
func Parse(rawData []byte) (*Config, error) { func Parse(rawData []byte) (*Config, error) {
-4
View File
@@ -1,4 +0,0 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "203.0.113.1"
public-ipv6 = "2001:db8::1"
-3
View File
@@ -1,3 +0,0 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "not-an-ip"
-3
View File
@@ -1,3 +0,0 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
public-ipv4 = "203.0.113.1"
-53
View File
@@ -1,53 +0,0 @@
package config
import (
"fmt"
"net/url"
)
type TypeHttpsURL struct {
Value *url.URL
}
func (t *TypeHttpsURL) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return fmt.Errorf("value is not correct URL (%s): %w", value, err)
}
if parsedURL.Host == "" {
return fmt.Errorf("url has to have a schema: %s", value)
}
if parsedURL.Scheme != "https" {
return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme)
}
t.Value = parsedURL
return nil
}
func (t *TypeHttpsURL) Get(defaultValue *url.URL) *url.URL {
if t.Value == nil {
return defaultValue
}
return t.Value
}
func (t *TypeHttpsURL) UnmarshalText(data []byte) error {
return t.Set(string(data))
}
func (t TypeHttpsURL) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
func (t TypeHttpsURL) String() string {
if t.Value == nil {
return ""
}
return t.Value.String()
}
-100
View File
@@ -1,100 +0,0 @@
package config_test
import (
"encoding/json"
"net/url"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeHttpsURLTestStruct struct {
Value config.TypeHttpsURL `json:"value"`
}
type HttpsURLTestSuite struct {
suite.Suite
}
func (suite *HttpsURLTestSuite) TestUnmarshalFail() {
testData := []string{
"",
"https://",
"://lala",
"/path",
"http://example.com",
"socks5://example.com",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeHttpsURLTestStruct{}))
})
}
}
func (suite *HttpsURLTestSuite) TestUnmarshalOk() {
testData := map[string]string{
"https://example.com": "https://example.com",
"https://example.com:8443": "https://example.com:8443",
"https://example.com/path?q=1": "https://example.com/path?q=1",
"https://user:pass@example.com": "https://user:pass@example.com",
}
for k, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": k,
})
suite.NoError(err)
suite.T().Run(k, func(t *testing.T) {
testStruct := &typeHttpsURLTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
parsed, _ := url.Parse(value)
assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme)
assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host)
assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery)
assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path)
})
}
}
func (suite *HttpsURLTestSuite) TestMarshalOk() {
parsed, _ := url.Parse("https://example.com/path?q=1")
testStruct := &typeHttpsURLTestStruct{
Value: config.TypeHttpsURL{
Value: parsed,
},
}
encodedJSON, err := json.Marshal(testStruct)
suite.NoError(err)
suite.JSONEq(`{"value": "https://example.com/path?q=1"}`,
string(encodedJSON))
}
func (suite *HttpsURLTestSuite) TestGet() {
emptyURL := &url.URL{}
value := config.TypeHttpsURL{}
suite.Equal(emptyURL, value.Get(emptyURL))
value.Value = &url.URL{}
suite.Equal(value.Value, value.Get(emptyURL))
}
func TestTypeHttpsURL(t *testing.T) {
t.Parallel()
suite.Run(t, &HttpsURLTestSuite{})
}
-5
View File
@@ -2,7 +2,6 @@ package testlib
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"github.com/9seconds/mtg/v2/essentials" "github.com/9seconds/mtg/v2/essentials"
@@ -25,10 +24,6 @@ func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address st
return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert return args.Get(0).(essentials.Conn), args.Error(1) //nolint: wrapcheck, forcetypeassert
} }
func (m *MtglibNetworkMock) NativeDialer() *net.Dialer {
return m.Called().Get(0).(*net.Dialer)
}
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context, func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
network, address string) (essentials.Conn, error), network, address string) (essentials.Conn, error),
) *http.Client { ) *http.Client {
-70
View File
@@ -4,77 +4,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/config"
) )
func readCustomDC(cfg *config.Config) {
if len(cfg.DCs.DC1) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC1 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[1] = ips
}
if len(cfg.DCs.DC2) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC2 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[2] = ips
}
if len(cfg.DCs.DC3) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC3 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[3] = ips
}
if len(cfg.DCs.DC4) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC4 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[4] = ips
}
if len(cfg.DCs.DC5) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC5 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[5] = ips
}
if len(cfg.DCs.DC203) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC203 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[203] = ips
}
if len(cfg.DCs.DC10001) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10001 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10001] = ips
}
if len(cfg.DCs.DC10002) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10002 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10002] = ips
}
if len(cfg.DCs.DC10003) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10003 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10003] = ips
}
}
func ReadConfig(path string) (*config.Config, error) { func ReadConfig(path string) (*config.Config, error) {
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
@@ -90,7 +22,5 @@ func ReadConfig(path string) (*config.Config, error) {
return nil, fmt.Errorf("invalid config: %w", err) return nil, fmt.Errorf("invalid config: %w", err)
} }
readCustomDC(conf)
return conf, nil return conf, nil
} }
-17
View File
@@ -14,23 +14,6 @@ import (
) )
func main() { func main() {
// this runs profiling server. To enable it, build with prof tag
// $ go build -tags prof
//
// Then you can pass a port using MTG_PROF_PORT environment variable.
// Default is 6000
// $ MTG_PROF_PORT=6000 mtg run config.toml
//
// It will run a webserver with profiling data on
// localhost:${MTG_PROF_PORT:-6000}.
//
// To collect PGO do following:
// $ curl -o default.pgo 'http://localhost:6000/debug/pprof/profile?seconds=300'
//
// See also https://pkg.go.dev/net/http/pprof
// https://go.dev/blog/pprof
runProfile()
cli := &cli.CLI{} cli := &cli.CLI{}
ctx := kong.Parse(cli, kong.Vars{ ctx := kong.Parse(cli, kong.Vars{
"version": getVersion(), "version": getVersion(),
+18 -97
View File
@@ -1,36 +1,11 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
[[tools.go]] [[tools.go]]
version = "1.26.1" version = "1.26.0"
backend = "core:go" backend = "core:go"
"platforms.linux-arm64" = { checksum = "sha256:bd03b743eb6eb4193ea3c3fd3956546bf0e3ca5b7076c8226334afe6b75704cd", url = "https://dl.google.com/go/go1.26.0.linux-arm64.tar.gz"}
[tools.go."platforms.linux-arm64"] "platforms.linux-x64" = { checksum = "sha256:aac1b08a0fb0c4e0a7c1555beb7b59180b05dfc5a3d62e40e9de90cd42f88235", url = "https://dl.google.com/go/go1.26.0.linux-amd64.tar.gz"}
checksum = "sha256:a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7" "platforms.macos-arm64" = { checksum = "sha256:b1640525dfe68f066d56f200bef7bf4dce955a1a893bd061de6754c211431023", url = "https://dl.google.com/go/go1.26.0.darwin-arm64.tar.gz"}
url = "https://dl.google.com/go/go1.26.1.linux-arm64.tar.gz" "platforms.macos-x64" = { checksum = "sha256:1ca28b7703cbea05a65b2a1d92d6b308610ef92f8824578a0874f2e60c9d5a22", url = "https://dl.google.com/go/go1.26.0.darwin-amd64.tar.gz"}
"platforms.windows-x64" = { checksum = "sha256:9bbe0fc64236b2b51f6255c05c4232532b8ecc0e6d2e00950bd3021d8a4d07d4", url = "https://dl.google.com/go/go1.26.0.windows-amd64.zip"}
[tools.go."platforms.linux-arm64-musl"]
checksum = "sha256:a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7"
url = "https://dl.google.com/go/go1.26.1.linux-arm64.tar.gz"
[tools.go."platforms.linux-x64"]
checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"
[tools.go."platforms.linux-x64-musl"]
checksum = "sha256:031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
url = "https://dl.google.com/go/go1.26.1.linux-amd64.tar.gz"
[tools.go."platforms.macos-arm64"]
checksum = "sha256:353df43a7811ce284c8938b5f3c7df40b7bfb6f56cb165b150bc40b5e2dd541f"
url = "https://dl.google.com/go/go1.26.1.darwin-arm64.tar.gz"
[tools.go."platforms.macos-x64"]
checksum = "sha256:65773dab2f8cc4cd23d93ba6d0a805de150ca0b78378879292be0b903b8cdd08"
url = "https://dl.google.com/go/go1.26.1.darwin-amd64.tar.gz"
[tools.go."platforms.windows-x64"]
checksum = "sha256:9b68112c913f45b7aebbf13c036721264bbba7e03a642f8f7490c561eebd1ecc"
url = "https://dl.google.com/go/go1.26.1.windows-amd64.zip"
[[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]] [[tools."go:golang.org/x/pkgsite/cmd/pkgsite"]]
version = "latest" version = "latest"
@@ -49,73 +24,19 @@ version = "0.9.2"
backend = "go:mvdan.cc/gofumpt" backend = "go:mvdan.cc/gofumpt"
[[tools.golangci-lint]] [[tools.golangci-lint]]
version = "2.11.4" version = "2.10.1"
backend = "aqua:golangci/golangci-lint" 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"}
[tools.golangci-lint."platforms.linux-arm64"] "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"}
checksum = "sha256:3bcfa2e6f3d32b2bf5cd75eaa876447507025e0303698633f722a05331988db4" "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"}
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-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.golangci-lint."platforms.linux-arm64-musl"]
checksum = "sha256:3bcfa2e6f3d32b2bf5cd75eaa876447507025e0303698633f722a05331988db4"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-arm64.tar.gz"
[tools.golangci-lint."platforms.linux-x64"]
checksum = "sha256:200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-amd64.tar.gz"
[tools.golangci-lint."platforms.linux-x64-musl"]
checksum = "sha256:200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-linux-amd64.tar.gz"
[tools.golangci-lint."platforms.macos-arm64"]
checksum = "sha256:02db2a2dae8b26812e53b0688a6f617e3ef1f489790e829ea22862cf76945675"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-darwin-arm64.tar.gz"
provenance = "github-attestations"
[tools.golangci-lint."platforms.macos-x64"]
checksum = "sha256:c900d4048db75d1edfd550fd11cf6a9b3008e7caa8e119fcddbc700412d63e60"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-darwin-amd64.tar.gz"
[tools.golangci-lint."platforms.windows-x64"]
checksum = "sha256:4932cfca5e75bf60fe1c576edf459e5e809e6644664a068185d64b84af3fad9e"
url = "https://github.com/golangci/golangci-lint/releases/download/v2.11.4/golangci-lint-2.11.4-windows-amd64.zip"
[[tools.goreleaser]] [[tools.goreleaser]]
version = "2.15.2" version = "2.14.1"
backend = "aqua:goreleaser/goreleaser" 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"}
[tools.goreleaser."platforms.linux-arm64"] "platforms.linux-x64" = { checksum = "sha256:2df975a7acbfdeaf888d596cab0024d48ec7fb7d747e1d08b90948b791f40a5f", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Linux_x86_64.tar.gz"}
checksum = "sha256:5db66761a98f6693161e49e1a95d28d2673a892ba60cb4a5e16736cafd41c4c9" "platforms.macos-arm64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_arm64.tar.gz" "platforms.macos-x64" = { checksum = "sha256:9f2e47f847b4f4177376fc6aa6914fbc7f673f59720076747e738b578c2e896e", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Darwin_all.tar.gz"}
provenance = "cosign" "platforms.windows-x64" = { checksum = "sha256:d7a3d8ba795e97ab8c4f8003630d300da164adf21fde5a4049440c20f15c3137", url = "https://github.com/goreleaser/goreleaser/releases/download/v2.14.1/goreleaser_Windows_x86_64.zip"}
[tools.goreleaser."platforms.linux-arm64-musl"]
checksum = "sha256:5db66761a98f6693161e49e1a95d28d2673a892ba60cb4a5e16736cafd41c4c9"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_arm64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.linux-x64"]
checksum = "sha256:0ebdbf0353aba566b969dde746cc4e4806f96c27aa2f3971b229a9df7611fedc"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_x86_64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.linux-x64-musl"]
checksum = "sha256:0ebdbf0353aba566b969dde746cc4e4806f96c27aa2f3971b229a9df7611fedc"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Linux_x86_64.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.macos-arm64"]
checksum = "sha256:0e6bd67688ac949780bf1166813a91f89856898ef4c40d7d46c2c74ebaa4b9ee"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Darwin_all.tar.gz"
provenance = "github-attestations"
[tools.goreleaser."platforms.macos-x64"]
checksum = "sha256:0e6bd67688ac949780bf1166813a91f89856898ef4c40d7d46c2c74ebaa4b9ee"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Darwin_all.tar.gz"
provenance = "cosign"
[tools.goreleaser."platforms.windows-x64"]
checksum = "sha256:7459832946dbe122c144f8d7f87484d8572ca005b779310aa6bb03346e8de17a"
url = "https://github.com/goreleaser/goreleaser/releases/download/v2.15.2/goreleaser_Windows_x86_64.zip"
provenance = "cosign"
-64
View File
@@ -3,12 +3,9 @@ package mtglib
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"sync/atomic"
"time"
"github.com/9seconds/mtg/v2/essentials" "github.com/9seconds/mtg/v2/essentials"
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
@@ -98,64 +95,3 @@ func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol {
sourceAddr: source.RemoteAddr(), sourceAddr: source.RemoteAddr(),
} }
} }
// idleTracker is a shared idle tracker for a pair of relay connections.
// Both directions update the same timestamp so that activity in one direction
// prevents the other (idle) direction from timing out.
type idleTracker struct {
lastActive atomic.Pointer[time.Time]
timeout time.Duration
}
func newIdleTracker(timeout time.Duration) *idleTracker {
t := &idleTracker{timeout: timeout}
t.touch()
return t
}
func (t *idleTracker) touch() {
stamp := time.Now()
t.lastActive.Store(&stamp)
}
func (t *idleTracker) isIdle() bool {
return time.Since(*t.lastActive.Load()) >= t.timeout
}
type connIdleTimeout struct {
essentials.Conn
tracker *idleTracker
}
func (c connIdleTimeout) Read(b []byte) (int, error) {
var netErr net.Error
for {
c.SetReadDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Read(b)
switch {
case err == nil:
c.tracker.touch()
return n, nil
case errors.As(err, &netErr) && netErr.Timeout() && !c.tracker.isIdle():
continue
}
return n, err
}
}
func (c connIdleTimeout) Write(b []byte) (int, error) {
c.SetWriteDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Write(b)
if n > 0 {
c.tracker.touch()
}
return n, err //nolint: wrapcheck
}
-151
View File
@@ -16,12 +16,6 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type netTimeoutError struct{}
func (e netTimeoutError) Error() string { return "i/o timeout" }
func (e netTimeoutError) Timeout() bool { return true }
func (e netTimeoutError) Temporary() bool { return true }
type ConnRewindBaseConn struct { type ConnRewindBaseConn struct {
testlib.EssentialsConnMock testlib.EssentialsConnMock
@@ -297,141 +291,6 @@ func (suite *ConnProxyProtocolTestSuite) TearDownTest() {
suite.targetConnMock.AssertExpectations(suite.T()) suite.targetConnMock.AssertExpectations(suite.T())
} }
type IdleTrackerTestSuite struct {
suite.Suite
}
func (suite *IdleTrackerTestSuite) TestNewNotIdle() {
tracker := newIdleTracker(time.Second)
suite.False(tracker.isIdle())
}
func (suite *IdleTrackerTestSuite) TestIdleAfterTimeout() {
tracker := newIdleTracker(10 * time.Millisecond)
time.Sleep(20 * time.Millisecond)
suite.True(tracker.isIdle())
}
func (suite *IdleTrackerTestSuite) TestTouchResetsIdle() {
tracker := newIdleTracker(50 * time.Millisecond)
time.Sleep(30 * time.Millisecond)
tracker.touch()
suite.False(tracker.isIdle())
}
type ConnIdleTimeoutTestSuite struct {
suite.Suite
connMock *testlib.EssentialsConnMock
tracker *idleTracker
conn connIdleTimeout
}
func (suite *ConnIdleTimeoutTestSuite) SetupTest() {
suite.connMock = &testlib.EssentialsConnMock{}
suite.tracker = newIdleTracker(time.Second)
suite.conn = connIdleTimeout{
Conn: suite.connMock,
tracker: suite.tracker,
}
}
func (suite *ConnIdleTimeoutTestSuite) TearDownTest() {
suite.connMock.AssertExpectations(suite.T())
}
func (suite *ConnIdleTimeoutTestSuite) TestReadOk() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadNonTimeoutErr() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, io.EOF)
n, err := suite.conn.Read(make([]byte, 10))
suite.True(errors.Is(err, io.EOF))
suite.Equal(0, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadTimeoutRetriesWhenNotIdle() {
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
suite.connMock.On("Read", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestReadTimeoutClosesWhenIdle() {
suite.tracker = newIdleTracker(time.Millisecond)
suite.conn = connIdleTimeout{
Conn: suite.connMock,
tracker: suite.tracker,
}
time.Sleep(5 * time.Millisecond)
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
n, err := suite.conn.Read(make([]byte, 10))
suite.Equal(0, n)
netErr, ok := err.(net.Error) //nolint: errorlint
suite.True(ok)
suite.True(netErr.Timeout())
}
func (suite *ConnIdleTimeoutTestSuite) TestSharedTrackerPreventsFalseTimeout() {
connMock2 := &testlib.EssentialsConnMock{}
conn2 := connIdleTimeout{
Conn: connMock2,
tracker: suite.tracker,
}
connMock2.On("SetWriteDeadline", mock.Anything).Return(nil)
connMock2.On("Write", mock.Anything).Once().Return(5, nil)
_, _ = conn2.Write(make([]byte, 5))
suite.connMock.On("SetReadDeadline", mock.Anything).Return(nil)
suite.connMock.On("Read", mock.Anything).Once().Return(0, netTimeoutError{})
suite.connMock.On("Read", mock.Anything).Once().Return(3, nil)
n, err := suite.conn.Read(make([]byte, 10))
suite.NoError(err)
suite.Equal(3, n)
connMock2.AssertExpectations(suite.T())
}
func (suite *ConnIdleTimeoutTestSuite) TestWriteOk() {
suite.connMock.On("SetWriteDeadline", mock.Anything).Return(nil)
suite.connMock.On("Write", mock.Anything).Once().Return(5, nil)
n, err := suite.conn.Write(make([]byte, 5))
suite.NoError(err)
suite.Equal(5, n)
}
func (suite *ConnIdleTimeoutTestSuite) TestWriteErr() {
suite.connMock.On("SetWriteDeadline", mock.Anything).Return(nil)
suite.connMock.On("Write", mock.Anything).Once().Return(0, io.EOF)
n, err := suite.conn.Write(make([]byte, 5))
suite.True(errors.Is(err, io.EOF))
suite.Equal(0, n)
}
func TestConnTraffic(t *testing.T) { func TestConnTraffic(t *testing.T) {
t.Parallel() t.Parallel()
suite.Run(t, &ConnTrafficTestSuite{}) suite.Run(t, &ConnTrafficTestSuite{})
@@ -446,13 +305,3 @@ func TestConnProxyProtocol(t *testing.T) {
t.Parallel() t.Parallel()
suite.Run(t, &ConnProxyProtocolTestSuite{}) suite.Run(t, &ConnProxyProtocolTestSuite{})
} }
func TestIdleTracker(t *testing.T) {
t.Parallel()
suite.Run(t, &IdleTrackerTestSuite{})
}
func TestConnIdleTimeout(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnIdleTimeoutTestSuite{})
}
+3 -19
View File
@@ -77,13 +77,8 @@ const (
// DefaultIdleTimeout is a default timeout for closing a connection in case of // DefaultIdleTimeout is a default timeout for closing a connection in case of
// idling. // idling.
// //
// Set to 5 minutes to survive typical mobile sleep periods (2-5 min) and // Deprecated: no longer in use because of changed TCP relay algorithm.
// avoid racing with MTProto ping_delay_disconnect (~60s interval). DefaultIdleTimeout = time.Minute
DefaultIdleTimeout = 5 * time.Minute
// DefaultHandshakeTimeout defines a time period during which the
// all handshake ceremonies must be completed.
DefaultHandshakeTimeout = 10 * time.Second
// DefaultTolerateTimeSkewness is a default timeout for time skewness on a // DefaultTolerateTimeSkewness is a default timeout for time skewness on a
// faketls timeout verification. // faketls timeout verification.
@@ -104,13 +99,6 @@ const (
// reads from Telegram after which connection will be terminated. This is // reads from Telegram after which connection will be terminated. This is
// required to abort stale connections. // required to abort stale connections.
TCPRelayReadTimeout = 20 * time.Second TCPRelayReadTimeout = 20 * time.Second
// DoppelGangerPerRaid defines a number of requests to each URL
// per raid.
DoppelGangerPerRaid = 10
// DoppelGangerEach defines a time period between each crawl attempt.
DoppelGangerEach = 6 * time.Hour
) )
// Network defines a knowledge how to work with a network. It may sound fun but // Network defines a knowledge how to work with a network. It may sound fun but
@@ -129,17 +117,13 @@ type Network interface {
// Dial establishes context-free TCP connections. // Dial establishes context-free TCP connections.
Dial(network, address string) (essentials.Conn, error) Dial(network, address string) (essentials.Conn, error)
// DialContext dials using a context. This is a preferable way of // DialContext dials using a context. This is a preferrable way of
// establishing TCP connections. // establishing TCP connections.
DialContext(ctx context.Context, network, address string) (essentials.Conn, error) DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
// MakeHTTPClient build an HTTP client with given dial function. If nothing is // MakeHTTPClient build an HTTP client with given dial function. If nothing is
// provided, then DialContext of this interface is going to be used. // provided, then DialContext of this interface is going to be used.
MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
// NativeDialer returns a configured instance of native dialer that
// skips proxy connections or any other irrelevant settings.
NativeDialer() *net.Dialer
} }
// AntiReplayCache is an interface that is used to detect replay attacks based // AntiReplayCache is an interface that is used to detect replay attacks based
+43 -36
View File
@@ -2,10 +2,7 @@ package dc
import ( import (
"context" "context"
"net"
"time" "time"
"github.com/9seconds/mtg/v2/essentials"
) )
type preferIP uint8 type preferIP uint8
@@ -42,36 +39,46 @@ type Updater interface {
} }
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
var defaultDCAddrSet = (func() dcAddrSet { var defaultDCAddrSet = dcAddrSet{
addrSet := dcAddrSet{ v4: map[int][]Addr{
v4: make(map[int][]Addr), 1: {
v6: make(map[int][]Addr), {Network: "tcp4", Address: "149.154.175.50:443"},
} },
2: {
for dcid, ips := range essentials.TelegramCoreAddresses { {Network: "tcp4", Address: "149.154.167.51:443"},
for _, addr := range ips { {Network: "tcp4", Address: "95.161.76.100:443"},
host, _, err := net.SplitHostPort(addr) },
if err != nil { 3: {
panic(err) {Network: "tcp4", Address: "149.154.175.100:443"},
} },
4: {
ip := net.ParseIP(host) {Network: "tcp4", Address: "149.154.167.91:443"},
if ip == nil { },
panic(addr) 5: {
} {Network: "tcp4", Address: "149.154.171.5:443"},
if ip.To4() == nil { },
addrSet.v6[dcid] = append(addrSet.v6[dcid], Addr{ 203: {
Network: "tcp6", {Network: "tcp4", Address: "91.105.192.100:443"},
Address: addr, },
}) },
} else { v6: map[int][]Addr{
addrSet.v4[dcid] = append(addrSet.v4[dcid], Addr{ 1: {
Network: "tcp4", {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
Address: addr, },
}) 2: {
} {Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"},
} },
} 3: {
{Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"},
return addrSet },
})() 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"},
},
},
}
+1 -1
View File
@@ -20,7 +20,7 @@ func (t *Telegram) GetAddresses(dc int) []Addr {
case preferIPOnlyIPv4: case preferIPOnlyIPv4:
return t.view.getV4(dc) return t.view.getV4(dc)
case preferIPOnlyIPv6: case preferIPOnlyIPv6:
return t.view.getV6(dc) return t.view.getV4(dc)
case preferIPPreferIPv4: case preferIPPreferIPv4:
return append(t.view.getV4(dc), t.view.getV6(dc)...) return append(t.view.getV4(dc), t.view.getV6(dc)...)
} }
+2 -6
View File
@@ -5,19 +5,15 @@ type dcView struct {
} }
func (d dcView) getV4(dc int) []Addr { func (d dcView) getV4(dc int) []Addr {
var addrs []Addr addrs := d.publicConfigs.getV4(dc)
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...) addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
addrs = append(addrs, d.publicConfigs.getV4(dc)...)
return addrs return addrs
} }
func (d dcView) getV6(dc int) []Addr { func (d dcView) getV6(dc int) []Addr {
var addrs []Addr addrs := d.publicConfigs.getV6(dc)
addrs = append(addrs, defaultDCAddrSet.getV6(dc)...) addrs = append(addrs, defaultDCAddrSet.getV6(dc)...)
addrs = append(addrs, d.publicConfigs.getV6(dc)...)
return addrs return addrs
} }
-118
View File
@@ -1,118 +0,0 @@
package doppel
import (
"bytes"
"context"
"sync"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
var doppelBufPool = sync.Pool{
New: func() any {
b := make([]byte, tls.MaxRecordSize)
return &b
},
}
type Conn struct {
essentials.Conn
p *connPayload
}
type connPayload struct {
ctx context.Context
ctxCancel context.CancelCauseFunc
stats Stats
wg sync.WaitGroup
writeStream bytes.Buffer
writtenCond sync.Cond
done bool
}
func (c Conn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, context.Cause(c.p.ctx)
}
c.p.writtenCond.L.Lock()
c.p.writeStream.Write(p)
c.p.writtenCond.L.Unlock()
c.p.writtenCond.Signal()
return len(p), context.Cause(c.p.ctx)
}
func (c Conn) start() {
bp := doppelBufPool.Get().(*[]byte)
buf := *bp
defer doppelBufPool.Put(bp)
timer := time.NewTimer(c.p.stats.Delay())
defer timer.Stop()
for {
select {
case <-c.p.ctx.Done():
return
case <-timer.C:
timer.Reset(c.p.stats.Delay())
}
size := c.p.stats.Size()
c.p.writtenCond.L.Lock()
for c.p.writeStream.Len() == 0 && !c.p.done {
c.p.writtenCond.Wait()
}
n, _ := c.p.writeStream.Read(buf[tls.SizeHeader : tls.SizeHeader+size])
c.p.writtenCond.L.Unlock()
if n == 0 {
continue
}
if err := tls.WriteRecordInPlace(c.Conn, buf, n); err != nil {
c.p.ctxCancel(err)
return
}
}
}
func (c Conn) Stop() {
c.p.ctxCancel(nil)
c.p.writtenCond.L.Lock()
c.p.done = true
c.p.writtenCond.L.Unlock()
c.p.writtenCond.Broadcast()
c.p.wg.Wait()
}
func NewConn(ctx context.Context, conn essentials.Conn, stats Stats) Conn {
ctx, cancel := context.WithCancelCause(ctx)
rv := Conn{
Conn: conn,
p: &connPayload{
ctx: ctx,
ctxCancel: cancel,
stats: stats,
writtenCond: sync.Cond{
L: &sync.Mutex{},
},
},
}
rv.p.writeStream.Grow(tls.DefaultBufferSize)
rv.p.wg.Go(func() {
rv.start()
})
return rv
}
-194
View File
@@ -1,194 +0,0 @@
package doppel
import (
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ConnMock struct {
testlib.EssentialsConnMock
mu sync.Mutex
writeBuffer bytes.Buffer
}
func (m *ConnMock) Write(p []byte) (int, error) {
args := m.Called(p)
if err := args.Error(1); err != nil {
return args.Int(0), err
}
m.mu.Lock()
defer m.mu.Unlock()
return m.writeBuffer.Write(p)
}
func (m *ConnMock) Written() []byte {
m.mu.Lock()
defer m.mu.Unlock()
return bytes.Clone(m.writeBuffer.Bytes())
}
type ConnTestSuite struct {
suite.Suite
connMock *ConnMock
ctx context.Context
ctxCancel context.CancelFunc
}
func (suite *ConnTestSuite) SetupTest() {
ctx, cancel := context.WithCancel(context.Background())
suite.ctx = ctx
suite.ctxCancel = cancel
suite.connMock = &ConnMock{}
}
func (suite *ConnTestSuite) TearDownTest() {
suite.ctxCancel()
suite.connMock.AssertExpectations(suite.T())
}
func (suite *ConnTestSuite) makeConn() Conn {
return NewConn(suite.ctx, suite.connMock, Stats{
k: 2.0,
lambda: 0.01,
})
}
func (suite *ConnTestSuite) TestWriteBuffersData() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
c := suite.makeConn()
defer c.Stop()
n, err := c.Write([]byte{1, 2, 3})
suite.NoError(err)
suite.Equal(3, n)
}
func (suite *ConnTestSuite) TestWriteOutputsTLSRecords() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
c := suite.makeConn()
payload := []byte("hello doppelganger")
_, err := c.Write(payload)
suite.NoError(err)
suite.Eventually(func() bool {
return len(suite.connMock.Written()) > 0
}, 2*time.Second, time.Millisecond)
c.Stop()
assembled := &bytes.Buffer{}
reader := bytes.NewReader(suite.connMock.Written())
for {
header := make([]byte, tls.SizeHeader)
if _, err := io.ReadFull(reader, header); err != nil {
break
}
suite.Equal(byte(tls.TypeApplicationData), header[0])
suite.Equal(tls.TLSVersion[:], header[tls.SizeRecordType:tls.SizeRecordType+tls.SizeVersion])
length := binary.BigEndian.Uint16(header[tls.SizeRecordType+tls.SizeVersion:])
suite.Greater(length, uint16(0))
rec := make([]byte, length)
_, err := io.ReadFull(reader, rec)
suite.NoError(err)
assembled.Write(rec)
}
suite.Equal(payload, assembled.Bytes())
}
func (suite *ConnTestSuite) TestWriteReturnsErrorAfterStop() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
c := suite.makeConn()
c.Stop()
time.Sleep(10 * time.Millisecond)
_, err := c.Write([]byte{1})
suite.Error(err)
}
func (suite *ConnTestSuite) TestStopDoesNotDeadlockWhenStartIsWaiting() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
for range 100 {
func() {
ctx, cancel := context.WithCancel(suite.ctx)
defer cancel()
c := NewConn(ctx, suite.connMock, Stats{
k: 2.0,
lambda: 0.01,
})
done := make(chan struct{})
go func() {
defer close(done)
c.Stop()
}()
select {
case <-done:
case <-time.After(2 * time.Second):
suite.Fail("Stop() deadlocked: start() likely stuck in writtenCond.Wait()")
}
}()
}
}
func (suite *ConnTestSuite) TestStopOnUnderlyingWriteError() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, errors.New("connection reset")).
Maybe()
c := suite.makeConn()
_, _ = c.Write([]byte("data"))
suite.Eventually(func() bool {
_, err := c.Write([]byte{1})
return err != nil
}, 2*time.Second, time.Millisecond)
}
func TestConn(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTestSuite{})
}
-267
View File
@@ -1,267 +0,0 @@
package doppel
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/9seconds/mtg/v2/essentials"
)
const (
DoppelGangerMaxDurations = 4096
DoppelGangerScoutRaidEach = 6 * time.Hour
DoppelGangerScoutRepeats = 10
MinCertSizesToCalculate = 3
)
// NoiseParams holds the measured cert chain size for FakeTLS noise calibration.
// If Mean is 0, the caller should use a legacy fallback.
type NoiseParams struct {
Mean int
Jitter int
}
type scoutRaidResult struct {
durations []time.Duration
certSizes []int
}
type gangerConnRequest struct {
ret chan<- Conn
payload essentials.Conn
}
type Ganger struct {
ctx context.Context
ctxCancel context.CancelFunc
logger Logger
wg sync.WaitGroup
scout Scout
scoutRaidEach time.Duration
scoutRaidRepeats int
drs bool
stats Stats
durations []time.Duration
certSizes []int
noiseParams atomic.Pointer[NoiseParams]
connRequests chan gangerConnRequest
}
func (g *Ganger) Shutdown() {
g.ctxCancel()
g.wg.Wait()
}
func (g *Ganger) Run() {
g.wg.Go(func() {
g.run()
})
}
// NoiseParams returns the current cert-size-based noise parameters.
// Returns zero-value NoiseParams if not yet measured (caller should use fallback).
func (g *Ganger) NoiseParams() NoiseParams {
if p := g.noiseParams.Load(); p != nil {
return *p
}
return NoiseParams{}
}
func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
rvChan := make(chan Conn)
req := gangerConnRequest{
ret: rvChan,
payload: conn,
}
defer close(req.ret)
select {
case <-g.ctx.Done():
return Conn{}, context.Cause(g.ctx)
case g.connRequests <- req:
}
select {
case <-g.ctx.Done():
return Conn{}, context.Cause(g.ctx)
case conn := <-rvChan:
return conn, nil
}
}
func (g *Ganger) run() {
scoutTicker := time.NewTicker(g.scoutRaidEach)
defer func() {
scoutTicker.Stop()
select {
case <-scoutTicker.C:
default:
}
}()
scoutCollectedChan := make(chan scoutRaidResult)
currentScoutCollectedChan := scoutCollectedChan
updatedStatsChan := make(chan Stats)
g.wg.Go(func() {
g.runScoutRaid(scoutCollectedChan)
})
for {
select {
case <-g.ctx.Done():
return
case result := <-currentScoutCollectedChan:
g.durations = append(g.durations, result.durations...)
if len(g.durations) > DoppelGangerMaxDurations {
copy(g.durations, g.durations[len(g.durations)-DoppelGangerMaxDurations:])
g.durations = g.durations[:DoppelGangerMaxDurations]
}
// Update cert sizes and recompute noise params.
g.certSizes = append(g.certSizes, result.certSizes...)
if len(g.certSizes) > DoppelGangerMaxDurations {
g.certSizes = g.certSizes[len(g.certSizes)-DoppelGangerMaxDurations:]
}
if len(g.certSizes) >= MinCertSizesToCalculate {
g.updateNoiseParams()
}
if len(g.durations) < MinDurationsToCalculate {
continue
}
durations := g.durations
currentScoutCollectedChan = nil
g.wg.Go(func() {
select {
case <-g.ctx.Done():
case updatedStatsChan <- NewStats(durations, g.drs):
}
})
case stats := <-updatedStatsChan:
g.stats = stats
currentScoutCollectedChan = scoutCollectedChan
case <-scoutTicker.C:
g.wg.Go(func() {
g.runScoutRaid(scoutCollectedChan)
})
case req := <-g.connRequests:
select {
case <-g.ctx.Done():
case req.ret <- NewConn(g.ctx, req.payload, g.stats):
}
}
}
}
func (g *Ganger) updateNoiseParams() {
if len(g.certSizes) == 0 {
return
}
sum := 0
for _, s := range g.certSizes {
sum += s
}
mean := sum / len(g.certSizes)
maxDev := 0
for _, s := range g.certSizes {
d := s - mean
if d < 0 {
d = -d
}
if d > maxDev {
maxDev = d
}
}
if maxDev < 100 {
maxDev = 100
}
np := &NoiseParams{Mean: mean, Jitter: maxDev}
g.noiseParams.Store(np)
g.logger.Info(fmt.Sprintf(
"updated noise params: mean=%d jitter=%d samples=%d",
mean, maxDev, len(g.certSizes),
))
}
func (g *Ganger) runScoutRaid(rvChan chan<- scoutRaidResult) {
var result scoutRaidResult
for range g.scoutRaidRepeats {
learned, err := g.scout.Learn(g.ctx)
if err != nil {
g.logger.WarningError("cannot learn", err)
continue
}
result.durations = append(result.durations, learned.Durations...)
if learned.CertSize > 0 {
result.certSizes = append(result.certSizes, learned.CertSize)
}
}
select {
case <-g.ctx.Done():
return
case rvChan <- result:
}
}
func NewGanger(
ctx context.Context,
network Network,
logger Logger,
scoutEach time.Duration,
scoutRepeats int,
urls []string,
drs bool,
) *Ganger {
ctx, cancel := context.WithCancel(ctx)
if scoutEach == 0 {
scoutEach = DoppelGangerScoutRaidEach
}
if scoutRepeats == 0 {
scoutRepeats = DoppelGangerScoutRepeats
}
return &Ganger{
ctx: ctx,
ctxCancel: cancel,
logger: logger,
scoutRaidEach: scoutEach,
scoutRaidRepeats: scoutRepeats,
drs: drs,
stats: Stats{
k: StatsDefaultK,
lambda: StatsDefaultLambda,
drs: drs,
},
scout: NewScout(network, urls),
connRequests: make(chan gangerConnRequest),
}
}
-107
View File
@@ -1,107 +0,0 @@
package doppel
import (
"bytes"
"sync"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type GangerTestSuite struct {
TLSServerTestSuite
log *LoggerMock
g *Ganger
}
func (suite *GangerTestSuite) SetupTest() {
suite.TLSServerTestSuite.SetupTest()
suite.log = &LoggerMock{}
suite.log.
On("Info", mock.AnythingOfType("string")).
Maybe()
suite.log.
On("WarningError", mock.AnythingOfType("string"), mock.Anything).
Maybe()
suite.g = NewGanger(suite.ctx, suite.network, suite.log, time.Hour, 1, suite.urls, true)
suite.g.Run()
}
func (suite *GangerTestSuite) TearDownTest() {
suite.g.Shutdown()
suite.log.AssertExpectations(suite.T())
suite.TLSServerTestSuite.TearDownTest()
}
func (suite *GangerTestSuite) TestNewConnAfterShutdown() {
suite.g.Shutdown()
connMock := &testlib.EssentialsConnMock{}
_, err := suite.g.NewConn(connMock)
suite.Error(err)
}
func (suite *GangerTestSuite) TestNewConnWhileRunning() {
connMock := &testlib.EssentialsConnMock{}
connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
connMock.On("Close").
Return(nil).
Maybe()
conn, err := suite.g.NewConn(connMock)
suite.NoError(err)
conn.Stop()
}
func (suite *GangerTestSuite) TestNewConnWriteProducesTLSRecords() {
var (
mu sync.Mutex
buf bytes.Buffer
)
connMock := &testlib.EssentialsConnMock{}
connMock.On("Write", mock.AnythingOfType("[]uint8")).
Run(func(args mock.Arguments) {
mu.Lock()
buf.Write(args.Get(0).([]byte))
mu.Unlock()
}).
Return(0, nil).
Maybe()
connMock.On("Close").
Return(nil).
Maybe()
conn, err := suite.g.NewConn(connMock)
suite.NoError(err)
payload := bytes.Repeat([]byte("x"), 512)
_, err = conn.Write(payload)
suite.NoError(err)
time.Sleep(500 * time.Millisecond)
conn.Stop()
mu.Lock()
written := buf.Bytes()
mu.Unlock()
suite.NotEmpty(written)
}
func TestGanger(t *testing.T) {
t.Parallel()
suite.Run(t, &GangerTestSuite{})
}
-43
View File
@@ -1,43 +0,0 @@
package doppel
import (
"context"
"net"
"net/http"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
const (
// Please see Stats description
// https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
// https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
TLSRecordSizeStart = 1450
TLSRecordSizeAccel = 4096
TLSRecordSizeMax = 16384 - tls.SizeHeader
TLSCounterAccelAfter = 40
TLSCounterMaxAfter = TLSCounterAccelAfter + 20
TLSRecordSizeResetAfter = time.Second
)
// copypasted from mtglib
type Network interface {
// Dial establishes context-free TCP connections.
Dial(network, address string) (essentials.Conn, error)
// DialContext dials using a context. This is a preferable way of
// establishing TCP connections.
DialContext(ctx context.Context, network, address string) (essentials.Conn, error)
// MakeHTTPClient build an HTTP client with given dial function. If nothing is
// provided, then DialContext of this interface is going to be used.
MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client
// NativeDialer returns a configured instance of native dialer that
// skips proxy connections or any other irrelevant settings.
NativeDialer() *net.Dialer
}
-107
View File
@@ -1,107 +0,0 @@
package doppel
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httptest"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type SimpleNetwork struct{}
func (s SimpleNetwork) Dial(network, address string) (essentials.Conn, error) {
return s.DialContext(context.Background(), network, address)
}
func (s SimpleNetwork) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) {
d := &net.Dialer{}
conn, err := d.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
return conn.(*net.TCPConn), nil
}
func (s SimpleNetwork) NativeDialer() *net.Dialer {
return &net.Dialer{}
}
func (s SimpleNetwork) MakeHTTPClient(dialFunc func(ctx context.Context, network, address string) (essentials.Conn, error)) *http.Client {
if dialFunc == nil {
dialFunc = s.DialContext
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint: gosec
},
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return dialFunc(ctx, network, address)
},
},
}
}
type TLSServerTestSuite struct {
suite.Suite
tlsServer *httptest.Server
ctx context.Context
ctxCancel context.CancelFunc
network SimpleNetwork
urls []string
}
func (suite *TLSServerTestSuite) SetupSuite() {
suite.tlsServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Add("Hello", "how long")
if _, err := w.Write([]byte{1, 2, 3}); err != nil {
panic(err)
}
time.Sleep(5 * time.Millisecond)
if _, err := w.Write([]byte{1, 2, 3}); err != nil {
panic(err)
}
}))
suite.urls = []string{suite.tlsServer.URL}
}
func (suite *TLSServerTestSuite) SetupTest() {
ctx, cancel := context.WithCancel(context.Background())
suite.ctx = ctx
suite.ctxCancel = cancel
}
func (suite *TLSServerTestSuite) TearDownTest() {
suite.ctxCancel()
suite.tlsServer.CloseClientConnections()
}
func (suite *TLSServerTestSuite) TearDownSuite() {
suite.tlsServer.Close()
}
type LoggerMock struct {
mock.Mock
}
func (l *LoggerMock) Info(msg string) {
l.Called(msg)
}
func (l *LoggerMock) WarningError(msg string, err error) {
l.Called(msg, err)
}
-6
View File
@@ -1,6 +0,0 @@
package doppel
type Logger interface {
Info(msg string)
WarningError(msg string, err error)
}
-146
View File
@@ -1,146 +0,0 @@
package doppel
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
// ScoutResult holds measurements from a single scout HTTP request.
type ScoutResult struct {
Durations []time.Duration
CertSize int // total ApplicationData bytes during TLS handshake; 0 if unknown
}
type Scout struct {
network Network
urls []string
}
func (s Scout) Learn(ctx context.Context) (ScoutResult, error) {
var combined ScoutResult
for _, url := range s.urls {
learned, err := s.learn(ctx, url)
if err != nil {
return ScoutResult{}, err
}
combined.Durations = append(combined.Durations, learned.Durations...)
if learned.CertSize > 0 && combined.CertSize == 0 {
combined.CertSize = learned.CertSize
}
}
return combined, nil
}
func (s Scout) learn(ctx context.Context, url string) (ScoutResult, error) {
client, results := s.makeClient()
if !strings.HasPrefix(url, "https://") {
return ScoutResult{}, fmt.Errorf("url %s must be https", url)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return ScoutResult{}, err
}
resp, err := client.Do(req)
if resp != nil {
io.Copy(io.Discard, resp.Body) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
client.CloseIdleConnections()
}
if err != nil {
return ScoutResult{}, err
}
data, writeIndex := results.Snapshot()
if len(data) == 0 {
return ScoutResult{}, nil
}
var result ScoutResult
// Compute inter-record durations (existing logic).
lastTimestamp := time.Time{}
for i, v := range data {
if v.recordType != tls.TypeApplicationData {
continue
}
if lastTimestamp.IsZero() {
if i > 0 {
lastTimestamp = data[i-1].timestamp
} else {
lastTimestamp = v.timestamp
}
}
result.Durations = append(result.Durations, v.timestamp.Sub(lastTimestamp))
lastTimestamp = v.timestamp
}
// Compute cert size: sum of ApplicationData payload between CCS and
// the first client Write (which marks the end of server handshake).
seenCCS := false
boundary := writeIndex
if boundary < 0 {
boundary = len(data)
}
for i, v := range data {
if i >= boundary {
break
}
if v.recordType == tls.TypeChangeCipherSpec {
seenCCS = true
continue
}
if seenCCS && v.recordType == tls.TypeApplicationData {
result.CertSize += v.payloadLen
}
}
return result, nil
}
func (s Scout) makeClient() (*http.Client, *ScoutConnCollected) {
dialer := s.network.NativeDialer()
collected := NewScoutConnCollected()
client := s.network.MakeHTTPClient(func(
ctx context.Context,
network string,
address string,
) (essentials.Conn, error) {
conn, err := dialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
return NewScoutConn(essentials.WrapNetConn(conn), collected), nil
})
return client, collected
}
func NewScout(network Network, urls []string) Scout {
return Scout{
network: network,
urls: urls,
}
}
-70
View File
@@ -1,70 +0,0 @@
package doppel
import (
"bytes"
"encoding/binary"
"io"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
type ScoutConn struct {
tls.Conn
results *ScoutConnCollected
rawBuf *bytes.Buffer
seenCCS bool
}
func (s *ScoutConn) Read(p []byte) (int, error) {
buf := &bytes.Buffer{}
for {
if n, err := s.rawBuf.Read(p); err == nil {
return n, nil
}
s.rawBuf.Reset()
recordType, length, err := tls.ReadRecord(s.Conn, buf)
if err != nil {
return 0, err
}
if recordType == tls.TypeChangeCipherSpec {
s.seenCCS = true
}
s.results.Add(recordType, int(length))
s.rawBuf.Write([]byte{recordType})
s.rawBuf.Write(tls.TLSVersion[:])
if err := binary.Write(s.rawBuf, binary.BigEndian, uint16(length)); err != nil {
return 0, err
}
if _, err := io.Copy(s.rawBuf, buf); err != nil {
return 0, err
}
}
}
func (s *ScoutConn) Write(p []byte) (int, error) {
if s.seenCCS {
s.results.MarkWrite()
}
return s.Conn.Write(p)
}
func NewScoutConn(conn essentials.Conn, results *ScoutConnCollected) *ScoutConn {
rawBuf := &bytes.Buffer{}
rawBuf.Grow(tls.MaxRecordSize)
return &ScoutConn{
Conn: tls.New(conn, false, false),
results: results,
rawBuf: rawBuf,
}
}
@@ -1,59 +0,0 @@
package doppel
import (
"slices"
"sync"
"time"
)
const (
ScoutConnCollectedPreallocSize = 100
)
type ScoutConnResult struct {
timestamp time.Time
recordType byte
payloadLen int
}
type ScoutConnCollected struct {
mu sync.Mutex
data []ScoutConnResult
writeIndex int // index at which client first wrote post-handshake data; -1 if not set
}
func (s *ScoutConnCollected) Add(record byte, payloadLen int) {
s.mu.Lock()
s.data = append(s.data, ScoutConnResult{
timestamp: time.Now(),
recordType: record,
payloadLen: payloadLen,
})
s.mu.Unlock()
}
// MarkWrite records the current data length as the handshake boundary.
func (s *ScoutConnCollected) MarkWrite() {
s.mu.Lock()
if s.writeIndex < 0 {
s.writeIndex = len(s.data)
}
s.mu.Unlock()
}
// Snapshot returns a copy of the collected data and the write index.
func (s *ScoutConnCollected) Snapshot() ([]ScoutConnResult, int) {
s.mu.Lock()
snapshot := slices.Clone(s.data)
writeIndex := s.writeIndex
s.mu.Unlock()
return snapshot, writeIndex
}
func NewScoutConnCollected() *ScoutConnCollected {
return &ScoutConnCollected{
data: make([]ScoutConnResult, 0, ScoutConnCollectedPreallocSize),
writeIndex: -1,
}
}
@@ -1,86 +0,0 @@
package doppel
import (
"sync"
"testing"
"time"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
"github.com/stretchr/testify/suite"
)
type ScoutConnCollectedTestSuite struct {
suite.Suite
}
func (suite *ScoutConnCollectedTestSuite) TestAddSingle() {
collected := NewScoutConnCollected()
collected.Add(tls.TypeApplicationData, 100)
data, _ := collected.Snapshot()
suite.Len(data, 1)
suite.Equal(byte(tls.TypeApplicationData), data[0].recordType)
}
func (suite *ScoutConnCollectedTestSuite) TestAddTimestampsAreMonotonic() {
collected := NewScoutConnCollected()
collected.Add(tls.TypeApplicationData, 100)
time.Sleep(time.Microsecond)
collected.Add(tls.TypeApplicationData, 100)
time.Sleep(time.Microsecond)
collected.Add(tls.TypeApplicationData, 100)
data, _ := collected.Snapshot()
for i := 1; i < len(data); i++ {
suite.True(data[i].timestamp.After(data[i-1].timestamp))
}
}
func (suite *ScoutConnCollectedTestSuite) TestConcurrentAddSnapshot() {
collected := NewScoutConnCollected()
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
collected.Add(tls.TypeApplicationData, i)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
collected.MarkWrite()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
// call Snapshot concurrently to exercise the lock under -race
collected.Snapshot() //nolint:errcheck
}
}()
wg.Wait()
data, writeIndex := collected.Snapshot()
suite.Len(data, 1000)
suite.GreaterOrEqual(writeIndex, 0)
}
func TestScoutConnCollected(t *testing.T) {
t.Parallel()
suite.Run(t, &ScoutConnCollectedTestSuite{})
}
-39
View File
@@ -1,39 +0,0 @@
package doppel
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ScoutTestSuite struct {
TLSServerTestSuite
scout Scout
}
func (suite *ScoutTestSuite) SetupSuite() {
suite.TLSServerTestSuite.SetupSuite()
suite.scout = Scout{
network: suite.network,
urls: suite.urls,
}
}
func (suite *ScoutTestSuite) TestCollectResults() {
result, err := suite.scout.Learn(suite.ctx)
suite.NoError(err)
suite.Less(3, len(result.Durations))
}
func (suite *ScoutTestSuite) TestCollectNothing() {
suite.ctxCancel()
_, err := suite.scout.Learn(suite.ctx)
suite.Error(err)
}
func TestScout(t *testing.T) {
suite.Run(t, &ScoutTestSuite{})
}
-170
View File
@@ -1,170 +0,0 @@
package doppel
import (
"math"
"math/rand/v2"
"time"
)
const (
StatsBisectTimes = 70
StatsLowK = 0.01
StatsHighK = 10.0
// do not calculate statistics if we have < than this number of durations
MinDurationsToCalculate = 100
// these values are taken from ok.ru. measured from moscow site.
StatsDefaultK = 0.37846373895785335
StatsDefaultLambda = 1.73177086015485
// how many bytes should we drift
DRSNoise = 100
)
// Stats is responsible for generating values that are distributed according
// to some statistical distribution.
//
// It follows several ideas:
// 1. Based on nginx and Cloudflare behaviour, even if server is eager
// to send a lot, they all start with small TLS packets that are
// approximately MTU-sized. After
// 2. After ~40 TLS records, server considers TCP session as somewhat solid
// and reliable and ramps up to 4096.
// 3. After ~20 TLS records more it jumps to the max 16384 bytes and keep
// this size as long as it can
// 4. If there is no any byte within a connection for a longer time period,
// this counter resets.
//
// This is called Dynamic TLS Record Sizing
// - https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
// - https://community.f5.com/kb/technicalarticles/boosting-tls-performance-with-dynamic-record-sizing-on-big-ip/280798
// - https://www.igvita.com/2013/10/24/optimizing-tls-record-size-and-buffering-latency/
//
// And this optimized for the very first byte, so web browsers could start to
// render as early as possible, showing user some preliminary results, optimizing
// for perceived latency.
//
// Since this is very typical for the website, we also aim for that.
//
// Another important idea is how delays between TLS packets are distributed.
// In case of sending huge heavy content with max sized record, delays have
// lognormal distribution. But a nature of a typical website shows that
// it eagers to deliver as fast as it can in a few very first records and
// could possibly slow down later.
//
// This is perfectly described by Weibull distribution:
// - https://en.wikipedia.org/wiki/Weibull_distribution
// - https://ieeexplore.ieee.org/document/6662948
// - https://www.researchgate.net/publication/224621285_Traffic_modelling_and_cost_optimization_for_transmitting_traffic_messages_over_a_hybrid_broadcast_and_cellular_network
// - https://ir.uitm.edu.my/id/eprint/105386/1/105386.pdf
//
// In other word, a combination of Dynamic TLS Record Sizing hints us for
// Weibull distribution.
//
// But we also have to keep in mind that DRS is not well spread yet. In most cases
// users still rely on OpenSSL or webserver defaults. OpenSSL chunks with
// biggest packet sizes, nginx relies on static setting that is 16k by default.
// Thus, dynamic sizing has to be present but we cannot oblige users to use that.
type Stats struct {
sizeLastRequested time.Time
sizeCounter int
// https://en.wikipedia.org/wiki/Shape_parameter
k float64
// https://en.wikipedia.org/wiki/Scale_parameter
lambda float64
// Dynamic Record Sizing
drs bool
}
func (d *Stats) Delay() time.Duration {
// u ∈ (0, 1], avoids ln(0)
u := 1.0 - rand.Float64()
// X = λ·(-ln U)^(1/k)
generated := d.lambda * math.Pow(-math.Log(u), 1.0/d.k)
// generated is in milliseconds
return time.Duration(generated * float64(time.Millisecond))
}
func (d *Stats) Size() int {
if time.Since(d.sizeLastRequested) > TLSRecordSizeResetAfter {
d.sizeCounter = 0
}
if !d.drs {
return TLSRecordSizeMax
}
d.sizeLastRequested = time.Now()
d.sizeCounter++
switch {
case d.sizeCounter <= TLSCounterAccelAfter:
return TLSRecordSizeStart - rand.IntN(DRSNoise)
case d.sizeCounter <= TLSCounterMaxAfter:
return TLSRecordSizeAccel - rand.IntN(DRSNoise)
}
return TLSRecordSizeMax
}
func NewStats(durations []time.Duration, drs bool) Stats {
n := float64(len(durations))
// in milliseconds
durFloats := make([]float64, len(durations))
for i, v := range durations {
durFloats[i] = float64(v.Microseconds()) / 1000.0
}
// The bisection solves the standard Weibull MLE equation for shape
// parameter k. There is no any good formula for doing that so we
// approximate it by several bisections. The number of operations
// is statically defined by a constant.
sumLog := 0.0
for _, v := range durFloats {
sumLog += math.Log(v)
}
lowK := StatsLowK
highK := StatsHighK
for range StatsBisectTimes {
midK := (lowK + highK) / 2.0
sumXK := 0.0
sumXKLog := 0.0
for _, v := range durFloats {
xk := math.Pow(v, midK)
sumXK += xk
sumXKLog += xk * math.Log(v)
}
if (1.0/midK)+(sumLog/n)-(sumXKLog/sumXK) > 0 {
lowK = midK
} else {
highK = midK
}
}
k := (lowK + highK) / 2
sumXK := 0.0
for _, v := range durFloats {
sumXK += math.Pow(v, k)
}
// λ = (Σxᵢᵏ / n)^(1/k)
lambda := math.Pow(sumXK/n, 1.0/k)
return Stats{
k: k,
lambda: lambda,
drs: drs,
}
}
-219
View File
@@ -1,219 +0,0 @@
package doppel
import (
"math"
"math/rand/v2"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type StatsTestSuite struct {
suite.Suite
}
func (suite *StatsTestSuite) GenWeibull(k, lambda float64, n int, seed uint64) []time.Duration {
rng := rand.New(rand.NewPCG(seed, 0))
samples := make([]time.Duration, n)
for i := range samples {
u := 1.0 - rng.Float64()
ms := lambda * math.Pow(-math.Log(u), 1.0/k)
d := time.Duration(ms * float64(time.Millisecond))
if d < time.Microsecond {
time.Sleep(time.Microsecond)
d = time.Microsecond
}
samples[i] = d
}
return samples
}
func (suite *StatsTestSuite) TestNewStatsRecoverParameters() {
knownK := 1.5
knownLambda := 100.0
samples := suite.GenWeibull(knownK, knownLambda, 5000, 42)
stats := NewStats(samples, true)
suite.InDelta(knownK, stats.k, 0.1)
suite.InDelta(knownLambda, stats.lambda, 5.0)
}
func (suite *StatsTestSuite) TestNewStatsExponentialCase() {
// When k=1, Weibull reduces to exponential distribution.
knownK := 1.0
knownLambda := 50.0
samples := suite.GenWeibull(knownK, knownLambda, 5000, 123)
stats := NewStats(samples, true)
suite.InDelta(knownK, stats.k, 0.1)
suite.InDelta(knownLambda, stats.lambda, 5.0)
}
func (suite *StatsTestSuite) TestNewStatsSmallK() {
// k < 1 produces a heavy-tailed distribution typical for network delays.
// Lambda must be large enough so samples stay above microsecond precision
// after time.Duration round-trip.
knownK := 0.6
knownLambda := 100.0
samples := suite.GenWeibull(knownK, knownLambda, 10000, 99)
stats := NewStats(samples, true)
suite.InDelta(knownK, stats.k, 0.05)
suite.InDelta(knownLambda, stats.lambda, 5.0)
}
func (suite *StatsTestSuite) TestNewStatsLargeK() {
// k > 1: light tail, concentrated around the mode.
knownK := 5.0
knownLambda := 200.0
samples := suite.GenWeibull(knownK, knownLambda, 5000, 77)
stats := NewStats(samples, true)
suite.InDelta(knownK, stats.k, 0.3)
suite.InDelta(knownLambda, stats.lambda, 5.0)
}
func (suite *StatsTestSuite) TestDelayNonNegative() {
stats := &Stats{
k: 1.5,
lambda: 100.0,
}
for range 200 {
dur := stats.Delay()
suite.GreaterOrEqual(dur, time.Duration(0))
}
}
func (suite *StatsTestSuite) TestDelayDistributionMean() {
// Weibull mean = λ · Γ(1 + 1/k)
k := 2.0
lambda := 50.0
stats := &Stats{k: k, lambda: lambda}
n := 50000
sum := 0.0
for range n {
dur := stats.Delay()
sum += float64(dur) / float64(time.Millisecond)
}
sampleMean := sum / float64(n)
expectedMean := lambda * math.Gamma(1.0+1.0/k)
suite.InDelta(expectedMean, sampleMean, expectedMean*0.05)
}
func (suite *StatsTestSuite) TestNewStatsRoundTrip() {
// Estimate parameters from data, then verify that Delay samples
// from the fitted distribution have approximately the same mean.
knownK := 1.2
knownLambda := 80.0
samples := suite.GenWeibull(knownK, knownLambda, 5000, 555)
stats := NewStats(samples, true)
n := 50000
sum := 0.0
for range n {
dur := stats.Delay()
sum += float64(dur) / float64(time.Millisecond)
}
sampleMean := sum / float64(n)
expectedMean := knownLambda * math.Gamma(1.0+1.0/knownK)
suite.InDelta(expectedMean, sampleMean, expectedMean*0.05)
}
func (suite *StatsTestSuite) TestSizeStartPhase() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
for range TLSCounterAccelAfter {
size := stats.Size()
suite.GreaterOrEqual(size, TLSRecordSizeStart-DRSNoise)
suite.LessOrEqual(size, TLSRecordSizeStart)
}
}
func (suite *StatsTestSuite) TestSizeAccelPhase() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
for range TLSCounterAccelAfter {
stats.Size()
}
for range TLSCounterMaxAfter - TLSCounterAccelAfter {
size := stats.Size()
suite.GreaterOrEqual(size, TLSRecordSizeAccel-DRSNoise)
suite.LessOrEqual(size, TLSRecordSizeAccel)
}
}
func (suite *StatsTestSuite) TestSizeMaxPhase() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
for range TLSCounterMaxAfter {
stats.Size()
}
for range 20 {
size := stats.Size()
suite.Equal(TLSRecordSizeMax, size)
}
}
func (suite *StatsTestSuite) TestSizeResetsAfterInactivity() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: true}
// Advance past start phase.
for range TLSCounterMaxAfter {
stats.Size()
}
suite.Equal(TLSRecordSizeMax, stats.Size())
// Simulate inactivity by backdating sizeLastRequested.
stats.sizeLastRequested = time.Now().Add(-TLSRecordSizeResetAfter - time.Millisecond)
size := stats.Size()
suite.GreaterOrEqual(size, TLSRecordSizeStart-DRSNoise)
suite.LessOrEqual(size, TLSRecordSizeStart)
}
func (suite *StatsTestSuite) TestSizeNoDRSAlwaysMax() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: false}
for range TLSCounterMaxAfter + 20 {
suite.Equal(TLSRecordSizeMax, stats.Size())
}
}
func (suite *StatsTestSuite) TestSizeNoDRSIgnoresCounter() {
stats := &Stats{k: 1.0, lambda: 1.0, drs: false}
// Even after many calls, always returns max.
for range 200 {
suite.Equal(TLSRecordSizeMax, stats.Size())
}
// Inactivity has no effect either.
stats.sizeLastRequested = time.Now().Add(-TLSRecordSizeResetAfter - time.Millisecond)
suite.Equal(TLSRecordSizeMax, stats.Size())
}
func TestStats(t *testing.T) {
t.Parallel()
suite.Run(t, &StatsTestSuite{})
}
+134
View File
@@ -0,0 +1,134 @@
package faketls
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"fmt"
"time"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
)
type ClientHello struct {
Time time.Time
Random [RandomLen]byte
SessionID []byte
Host string
CipherSuite uint16
}
func (c ClientHello) Valid(hostname string, tolerateTimeSkewness time.Duration) error {
if c.Host != "" && c.Host != hostname {
return fmt.Errorf("incorrect hostname %s", hostname)
}
now := time.Now()
timeDiff := now.Sub(c.Time)
if timeDiff < 0 {
timeDiff = -timeDiff
}
if timeDiff > tolerateTimeSkewness {
return fmt.Errorf("incorrect timestamp. got=%d, now=%d, diff=%s",
c.Time.Unix(), now.Unix(), timeDiff.String())
}
return nil
}
func ParseClientHello(secret, handshake []byte) (ClientHello, error) {
hello := ClientHello{}
if len(handshake) < ClientHelloMinLen {
return hello, fmt.Errorf("lengh of handshake is too small: %d", len(handshake))
}
if handshake[0] != HandshakeTypeClient {
return hello, fmt.Errorf("unknown handshake type %#x", handshake[0])
}
handshakeSizeBytes := [4]byte{0, handshake[1], handshake[2], handshake[3]}
handshakeLength := binary.BigEndian.Uint32(handshakeSizeBytes[:])
if len(handshake)-4 != int(handshakeLength) {
return hello,
fmt.Errorf("incorrect handshake size. manifested=%d, real=%d",
handshakeLength, len(handshake)-4)
}
copy(hello.Random[:], handshake[ClientHelloRandomOffset:])
copy(handshake[ClientHelloRandomOffset:], clientHelloEmptyRandom)
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
rec.Type = record.TypeHandshake
rec.Version = record.Version10
rec.Payload.Write(handshake)
// mac is calculated for the whole record, not only
// for the payload part
mac := hmac.New(sha256.New, secret)
rec.Dump(mac) //nolint: errcheck
computedRandom := mac.Sum(nil)
for i := range RandomLen {
computedRandom[i] ^= hello.Random[i]
}
if subtle.ConstantTimeCompare(clientHelloEmptyRandom[:RandomLen-4], computedRandom[:RandomLen-4]) != 1 {
return hello, ErrBadDigest
}
timestamp := int64(binary.LittleEndian.Uint32(computedRandom[RandomLen-4:]))
hello.Time = time.Unix(timestamp, 0)
parseSessionID(&hello, handshake)
parseCipherSuite(&hello, handshake)
parseSNI(&hello, handshake)
return hello, nil
}
func parseSessionID(hello *ClientHello, handshake []byte) {
hello.SessionID = make([]byte, handshake[ClientHelloSessionIDOffset])
copy(hello.SessionID, handshake[ClientHelloSessionIDOffset+1:])
}
func parseCipherSuite(hello *ClientHello, handshake []byte) {
cipherSuiteOffset := ClientHelloSessionIDOffset + len(hello.SessionID) + 3
hello.CipherSuite = binary.BigEndian.Uint16(handshake[cipherSuiteOffset : cipherSuiteOffset+2])
}
func parseSNI(hello *ClientHello, handshake []byte) {
cipherSuiteOffset := ClientHelloSessionIDOffset + len(hello.SessionID) + 1
handshake = handshake[cipherSuiteOffset:]
cipherSuiteLength := binary.BigEndian.Uint16(handshake[:2])
handshake = handshake[2+cipherSuiteLength:]
compressionMethodsLength := int(handshake[0])
handshake = handshake[1+compressionMethodsLength:]
extensionsLength := binary.BigEndian.Uint16(handshake[:2])
handshake = handshake[2 : 2+extensionsLength]
for len(handshake) > 0 {
if binary.BigEndian.Uint16(handshake[:2]) != ExtensionSNI {
extensionsLength := binary.BigEndian.Uint16(handshake[2:4])
handshake = handshake[4+extensionsLength:]
continue
}
hostnameLength := binary.BigEndian.Uint16(handshake[7:9])
handshake = handshake[9:]
hello.Host = string(handshake[:int(hostnameLength)])
return
}
}
@@ -0,0 +1,21 @@
package faketls_test
import (
"testing"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/stretchr/testify/require"
)
var FuzzClientHelloSecret = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
func FuzzClientHello(f *testing.F) {
f.Add([]byte{1, 2, 3})
f.Fuzz(func(t *testing.T, frame []byte) {
_, err := faketls.ParseClientHello(FuzzClientHelloSecret, frame)
// a probability of having != err is almost negligible
require.Error(t, err)
})
}
@@ -0,0 +1,191 @@
package faketls_test
import (
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ClientHelloSnapshot struct {
Time int `json:"time"`
Random string `json:"random"`
SessionID string `json:"sessionId"`
Host string `json:"host"`
CipherSuite int `json:"cipherSuite"`
Full string `json:"full"`
}
func (c ClientHelloSnapshot) GetTime() time.Time {
return time.Unix(int64(c.Time), 0)
}
func (c ClientHelloSnapshot) GetRandom() []byte {
data, _ := base64.StdEncoding.DecodeString(c.Random)
return data
}
func (c ClientHelloSnapshot) GetSessionID() []byte {
data, _ := base64.StdEncoding.DecodeString(c.SessionID)
return data
}
func (c ClientHelloSnapshot) GetHost() string {
return c.Host
}
func (c ClientHelloSnapshot) GetCipherSuite() uint16 {
return uint16(c.CipherSuite)
}
func (c ClientHelloSnapshot) GetFull() []byte {
data, _ := base64.StdEncoding.DecodeString(c.Full)
return data
}
type ClientHelloTestSuite struct {
suite.Suite
secret mtglib.Secret
}
func (suite *ClientHelloTestSuite) SetupSuite() {
parsed, err := mtglib.ParseSecret("ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d")
if err != nil {
panic(err)
}
suite.secret = parsed
}
func (suite *ClientHelloTestSuite) TestEmptyHandshake() {
_, err := faketls.ParseClientHello(suite.secret.Key[:], nil)
suite.Error(err)
}
func (suite *ClientHelloTestSuite) TestIncorrectHandshakeType() {
data := make([]byte, 1024)
data[0] = 0x02
_, err := faketls.ParseClientHello(suite.secret.Key[:], data)
suite.Error(err)
}
func (suite *ClientHelloTestSuite) TestIncorrectLength() {
data := make([]byte, 1024)
data[0] = 0x01
data[1] = 0xff
data[2] = 0xff
_, err := faketls.ParseClientHello(suite.secret.Key[:], data)
suite.Error(err)
}
func (suite *ClientHelloTestSuite) TestSnapshotOk() {
files, err := os.ReadDir("testdata")
suite.NoError(err)
testData := []string{}
for _, v := range files {
if strings.HasPrefix(v.Name(), "client-hello-ok") {
testData = append(testData, v.Name())
}
}
for _, name := range testData {
path := filepath.Join("testdata", name)
suite.T().Run(name, func(t *testing.T) {
fileData, err := os.ReadFile(path)
assert.NoError(t, err)
snapshot := &ClientHelloSnapshot{}
assert.NoError(t, json.Unmarshal(fileData, snapshot))
hello, err := faketls.ParseClientHello(suite.secret.Key[:], snapshot.GetFull())
assert.NoError(t, err)
assert.WithinDuration(t, snapshot.GetTime(), hello.Time, time.Second)
assert.Equal(t, snapshot.GetRandom(), hello.Random[:])
assert.Equal(t, snapshot.GetSessionID(), hello.SessionID)
assert.Equal(t, snapshot.GetHost(), hello.Host)
assert.Equal(t, snapshot.GetCipherSuite(), hello.CipherSuite)
})
}
}
func (suite *ClientHelloTestSuite) TestSnapshotBad() {
files, err := os.ReadDir("testdata")
suite.NoError(err)
testData := []string{}
for _, v := range files {
if strings.HasPrefix(v.Name(), "client-hello-bad") {
testData = append(testData, v.Name())
}
}
for _, name := range testData {
path := filepath.Join("testdata", name)
suite.T().Run(name, func(t *testing.T) {
fileData, err := os.ReadFile(path)
assert.NoError(t, err)
snapshot := &ClientHelloSnapshot{}
assert.NoError(t, json.Unmarshal(fileData, snapshot))
_, err = faketls.ParseClientHello(suite.secret.Key[:], snapshot.GetFull())
assert.Error(t, err)
})
}
}
func (suite *ClientHelloTestSuite) TestValidateHostname() {
hello := faketls.ClientHello{
Time: time.Now(),
}
suite.NoError(hello.Valid("hostname", time.Second))
hello.Host = "hostname"
suite.Error(hello.Valid("hostname2", time.Second))
suite.NoError(hello.Valid("hostname", time.Second))
}
func (suite *ClientHelloTestSuite) TestValidateTime() {
testData := []time.Duration{
-2 * time.Second,
2 * time.Second,
}
for _, v := range testData {
value := v
suite.T().Run(value.String(), func(t *testing.T) {
hello := faketls.ClientHello{
Host: "hostname",
Time: time.Now().Add(value),
}
suite.Error(hello.Valid("hostname", 500*time.Millisecond))
suite.Error(hello.Valid("hostname", time.Second))
suite.NoError(hello.Valid("hostname", 3*time.Second))
})
}
}
func TestClientHello(t *testing.T) {
t.Parallel()
suite.Run(t, &ClientHelloTestSuite{})
}
+72
View File
@@ -0,0 +1,72 @@
package faketls
import (
"bytes"
"fmt"
"math/rand/v2"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
)
type Conn struct {
essentials.Conn
readBuffer bytes.Buffer
}
func (c *Conn) Read(p []byte) (int, error) {
if n, _ := c.readBuffer.Read(p); n > 0 {
return n, nil
}
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
for {
if err := rec.Read(c.Conn); err != nil {
return 0, err //nolint: wrapcheck
}
switch rec.Type { //nolint: exhaustive
case record.TypeApplicationData:
rec.Payload.WriteTo(&c.readBuffer) //nolint: errcheck
return c.readBuffer.Read(p) //nolint: wrapcheck
case record.TypeChangeCipherSpec:
default:
return 0, fmt.Errorf("unsupported record type %v", rec.Type)
}
}
}
func (c *Conn) Write(p []byte) (int, error) {
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
rec.Type = record.TypeApplicationData
rec.Version = record.Version12
written := 0
for len(p) > 0 {
chunkSize := rand.IntN(record.TLSMaxRecordSize)
if chunkSize > len(p) || chunkSize == 0 {
chunkSize = len(p)
}
rec.Payload.Reset()
rec.Payload.Write(p[:chunkSize])
err := rec.Dump(c.Conn)
written += chunkSize
if err != nil {
return written, err
}
p = p[chunkSize:]
}
return written, nil
}
+153
View File
@@ -0,0 +1,153 @@
package faketls_test
import (
"bytes"
"crypto/rand"
"errors"
"io"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ConnMock struct {
testlib.EssentialsConnMock
readBuffer bytes.Buffer
writeBuffer bytes.Buffer
}
func (m *ConnMock) Read(p []byte) (int, error) {
m.Called(p)
return m.readBuffer.Read(p) //nolint: wrapcheck
}
func (m *ConnMock) Write(p []byte) (int, error) {
m.Called(p)
return m.writeBuffer.Write(p) //nolint: wrapcheck
}
type ConnTestSuite struct {
suite.Suite
connMock *ConnMock
c *faketls.Conn
}
func (suite *ConnTestSuite) SetupTest() {
suite.connMock = &ConnMock{}
suite.c = &faketls.Conn{
Conn: suite.connMock,
}
}
func (suite *ConnTestSuite) TearDownTest() {
suite.connMock.AssertExpectations(suite.T())
}
func (suite *ConnTestSuite) TestRead() {
suite.connMock.On("Read", mock.Anything).Return(0, nil)
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
rec.Type = record.TypeChangeCipherSpec
rec.Version = record.Version12
rec.Payload.WriteByte(0x01)
rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
rec.Reset()
rec.Type = record.TypeApplicationData
rec.Version = record.Version12
rec.Payload.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
resultBuffer := &bytes.Buffer{}
buf := make([]byte, 2)
for {
n, err := suite.c.Read(buf)
if errors.Is(err, io.EOF) {
break
}
resultBuffer.Write(buf[:n])
}
suite.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, resultBuffer.Bytes())
}
func (suite *ConnTestSuite) TestReadUnexpected() {
suite.connMock.On("Read", mock.Anything).Return(0, nil)
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
rec.Type = record.TypeChangeCipherSpec
rec.Version = record.Version12
rec.Payload.WriteByte(0x01)
rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
rec.Reset()
rec.Type = record.TypeHandshake
rec.Version = record.Version12
rec.Payload.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
rec.Dump(&suite.connMock.readBuffer) //nolint: errcheck
buf := make([]byte, 2)
for {
_, err := suite.c.Read(buf)
switch {
case err == nil:
case errors.Is(err, io.EOF):
suite.FailNow("unexpected to finish")
default:
return
}
}
}
func (suite *ConnTestSuite) TestWrite() {
suite.connMock.On("Write", mock.Anything).Return(0, nil)
dataToRec := make([]byte, record.TLSMaxRecordSize*2)
rand.Read(dataToRec) //nolint: staticcheck, errcheck
n, err := suite.c.Write(dataToRec)
suite.NoError(err)
suite.Equal(len(dataToRec), n)
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
buf := &bytes.Buffer{}
for {
if err := rec.Read(&suite.connMock.writeBuffer); err != nil {
break
}
suite.Equal(record.TypeApplicationData, rec.Type)
suite.Equal(record.Version12, rec.Version)
rec.Payload.WriteTo(buf) //nolint: errcheck
}
suite.Equal(dataToRec, buf.Bytes())
}
func TestConn(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTestSuite{})
}
+59
View File
@@ -0,0 +1,59 @@
package faketls
import (
"bytes"
"errors"
)
const (
// RandomLen defines a size of the random digest in TLS Hellos.
RandomLen = 32
// ClientHelloRandomOffset is an offset in ClientHello record where
// random digest is started.
ClientHelloRandomOffset = 6
// ClientHelloSessionIDOffset is an offset in ClientHello record where
// SessionID is started.
ClientHelloSessionIDOffset = ClientHelloRandomOffset + RandomLen
// ClientHelloMinLen is a minimal possible length of
// ClientHello record.
ClientHelloMinLen = 6
// WelcomePacketRandomOffset is an offset of random in ServerHello
// packet (including record envelope).
WelcomePacketRandomOffset = 11
// HandshakeTypeClient is a value representing a client handshake.
HandshakeTypeClient = 0x01
// HandshakeTypeServer is a value representing a server handshake.
HandshakeTypeServer = 0x02
// ChangeCipherValue is a value representing a change cipher
// specification record.
ChangeCipherValue = 0x01
// ExtensionSNI is a value for TLS extension 'SNI'.
ExtensionSNI = 0x00
)
var (
// ErrBadDigest is returned if given TLS Client Hello mismatches with a
// derived one.
ErrBadDigest = errors.New("bad digest")
serverHelloSuffix = []byte{
0x00, // no compression
0x00, 0x2e, // 46 bytes of data
0x00, 0x2b, // Extension - Supported Versions
0x00, 0x02, // 2 bytes are following
0x03, 0x04, // TLS 1.3
0x00, 0x33, // Extension - Key Share
0x00, 0x24, // 36 bytes
0x00, 0x1d, // x25519 curve
0x00, 0x20, // 32 bytes of key
}
clientHelloEmptyRandom = bytes.Repeat([]byte{0}, RandomLen)
)
+84
View File
@@ -0,0 +1,84 @@
package record
import "fmt"
const TLSMaxRecordSize = 65535 // max uint16
type Type uint8
const (
// TypeChangeCipherSpec defines a byte value of the TLS record when a
// peer wants to change a specifications of the chosen cipher.
TypeChangeCipherSpec Type = 0x14
// TypeHandshake defines a byte value of the TLS record when a peer
// initiates a new TLS connection and wants to make a handshake
// ceremony.
TypeHandshake Type = 0x16
// TypeApplicationData defines a byte value of the TLS record when a
// peer sends an user data, not a control frames.
TypeApplicationData Type = 0x17
)
func (t Type) String() string {
switch t {
case TypeChangeCipherSpec:
return "changeCipher(0x14)"
case TypeHandshake:
return "handshake(0x16)"
case TypeApplicationData:
return "applicationData(0x17)"
}
return fmt.Sprintf("unknown(%#x)", byte(t))
}
func (t Type) Valid() error {
switch t {
case TypeChangeCipherSpec, TypeHandshake, TypeApplicationData:
return nil
}
return fmt.Errorf("unknown type %#x", byte(t))
}
type Version uint16
const (
// Version10 defines a TLS1.0.
Version10 Version = 769 // 0x03 0x01
// Version11 defines a TLS1.1.
Version11 Version = 770 // 0x03 0x02
// Version12 defines a TLS1.2.
Version12 Version = 771 // 0x03 0x03
// Version13 defines a TLS1.3.
Version13 Version = 772 // 0x03 0x04
)
func (v Version) String() string {
switch v {
case Version10:
return "tls1.0"
case Version11:
return "tls1.1"
case Version12:
return "tls1.2"
case Version13:
return "tls1.3"
}
return fmt.Sprintf("tls?(%d)", uint16(v))
}
func (v Version) Valid() error {
switch v {
case Version10, Version11, Version12, Version13:
return nil
}
return fmt.Errorf("unknown version %d", uint16(v))
}
@@ -0,0 +1,79 @@
package record_test
import (
"testing"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"github.com/stretchr/testify/suite"
)
type TypeTestSuite struct {
suite.Suite
}
func (suite *TypeTestSuite) TestChangeCipherSpec() {
suite.Contains(record.TypeChangeCipherSpec.String(), "changeCipher")
suite.Contains(record.TypeChangeCipherSpec.String(), "0x14")
suite.NoError(record.TypeChangeCipherSpec.Valid())
}
func (suite *TypeTestSuite) TestHandshake() {
suite.Contains(record.TypeHandshake.String(), "handshake")
suite.Contains(record.TypeHandshake.String(), "0x16")
suite.NoError(record.TypeHandshake.Valid())
}
func (suite *TypeTestSuite) TestApplicationData() {
suite.Contains(record.TypeApplicationData.String(), "applicationData")
suite.Contains(record.TypeApplicationData.String(), "0x17")
suite.NoError(record.TypeApplicationData.Valid())
}
func (suite *TypeTestSuite) TestUnknown() {
value := record.Type(0x20)
suite.Contains(value.String(), "unknown")
suite.Contains(value.String(), "0x20")
suite.Error(value.Valid())
}
type VersionTestSuite struct {
suite.Suite
}
func (suite *VersionTestSuite) Test10() {
suite.Equal("tls1.0", record.Version10.String())
suite.NoError(record.Version10.Valid())
}
func (suite *VersionTestSuite) Test11() {
suite.Equal("tls1.1", record.Version11.String())
suite.NoError(record.Version11.Valid())
}
func (suite *VersionTestSuite) Test12() {
suite.Equal("tls1.2", record.Version12.String())
suite.NoError(record.Version12.Valid())
}
func (suite *VersionTestSuite) Test13() {
suite.Equal("tls1.3", record.Version13.String())
suite.NoError(record.Version13.Valid())
}
func (suite *VersionTestSuite) TestUnknown() {
value := record.Version(900)
suite.Equal("tls?(900)", value.String())
suite.Error(value.Valid())
}
func TestType(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeTestSuite{})
}
func TestVersion(t *testing.T) {
t.Parallel()
suite.Run(t, &VersionTestSuite{})
}
+20
View File
@@ -0,0 +1,20 @@
package record
import (
"sync"
)
var recordPool = sync.Pool{
New: func() any {
return &Record{}
},
}
func AcquireRecord() *Record {
return recordPool.Get().(*Record) //nolint: forcetypeassert
}
func ReleaseRecord(r *Record) {
r.Reset()
recordPool.Put(r)
}
+86
View File
@@ -0,0 +1,86 @@
package record
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
)
type Record struct {
Type Type
Version Version
Payload bytes.Buffer
}
func (r *Record) String() string {
return fmt.Sprintf("<tlsRecord(type=%v, version=%v, payload=%s)>",
r.Type,
r.Version,
base64.StdEncoding.EncodeToString(r.Payload.Bytes()))
}
func (r *Record) Reset() {
r.Payload.Reset()
}
func (r *Record) Read(reader io.Reader) error {
r.Reset()
buf := [2]byte{}
if _, err := io.ReadFull(reader, buf[:1]); err != nil {
return fmt.Errorf("cannot read type: %w", err)
}
r.Type = Type(buf[0])
if err := r.Type.Valid(); err != nil {
return fmt.Errorf("invalid type: %w", err)
}
if _, err := io.ReadFull(reader, buf[:]); err != nil {
return fmt.Errorf("cannot read version: %w", err)
}
r.Version = Version(binary.BigEndian.Uint16(buf[:]))
if err := r.Version.Valid(); err != nil {
return fmt.Errorf("invalid version: %w", err)
}
if _, err := io.ReadFull(reader, buf[:]); err != nil {
return fmt.Errorf("cannot read payload length: %w", err)
}
length := int64(binary.BigEndian.Uint16(buf[:]))
if _, err := io.CopyN(&r.Payload, reader, length); err != nil {
return fmt.Errorf("cannot read payload: %w", err)
}
return nil
}
func (r *Record) Dump(writer io.Writer) error {
buf := [2]byte{byte(r.Type), 0}
if _, err := writer.Write(buf[:1]); err != nil {
return fmt.Errorf("cannot dump record type: %w", err)
}
binary.BigEndian.PutUint16(buf[:], uint16(r.Version))
if _, err := writer.Write(buf[:]); err != nil {
return fmt.Errorf("cannot dump version: %w", err)
}
binary.BigEndian.PutUint16(buf[:], uint16(r.Payload.Len()))
if _, err := writer.Write(buf[:]); err != nil {
return fmt.Errorf("cannot dump payload length: %w", err)
}
if _, err := writer.Write(r.Payload.Bytes()); err != nil {
return fmt.Errorf("cannot dump record: %w", err)
}
return nil
}
@@ -0,0 +1,110 @@
package record_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type RecordTestSnapshot struct {
Type int `json:"type"`
Version int `json:"version"`
Payload string `json:"payload"`
Record string `json:"record"`
}
func (r RecordTestSnapshot) RecordBytes() []byte {
data, _ := base64.StdEncoding.DecodeString(r.Record)
return data
}
func (r RecordTestSnapshot) PayloadBytes() []byte {
data, _ := base64.StdEncoding.DecodeString(r.Payload)
return data
}
type RecordTestSuite struct {
suite.Suite
r *record.Record
buf *bytes.Buffer
}
func (suite *RecordTestSuite) SetupTest() {
suite.r = record.AcquireRecord()
suite.buf = &bytes.Buffer{}
}
func (suite *RecordTestSuite) TearDownTest() {
record.ReleaseRecord(suite.r)
suite.buf.Reset()
}
func (suite *RecordTestSuite) TestIdempotent() {
suite.r.Type = record.TypeApplicationData
suite.r.Version = record.Version13
suite.r.Payload.Write([]byte{1, 2, 3})
suite.NoError(suite.r.Dump(suite.buf))
suite.r.Reset()
suite.NoError(suite.r.Read(suite.buf))
suite.Equal(0, suite.buf.Len())
suite.Equal(record.TypeApplicationData, suite.r.Type)
suite.Equal(record.Version13, suite.r.Version)
suite.Equal([]byte{1, 2, 3}, suite.r.Payload.Bytes())
}
func (suite *RecordTestSuite) TestString() {
_ = suite.r.String()
}
func (suite *RecordTestSuite) TestSnapshot() {
files, err := os.ReadDir("testdata")
suite.NoError(err)
testData := map[string]string{}
for _, f := range files {
testData[f.Name()] = filepath.Join("testdata", f.Name())
}
for name, pathV := range testData {
path := pathV
suite.T().Run(name, func(t *testing.T) {
data, err := os.ReadFile(path)
assert.NoError(t, err)
snapshot := &RecordTestSnapshot{}
assert.NoError(t, json.Unmarshal(data, snapshot))
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
assert.NoError(t, rec.Read(bytes.NewReader(snapshot.RecordBytes())))
assert.Equal(t, snapshot.Type, int(rec.Type))
assert.Equal(t, snapshot.Version, int(rec.Version))
assert.Equal(t, snapshot.PayloadBytes(), rec.Payload.Bytes())
buf := &bytes.Buffer{}
assert.NoError(t, rec.Dump(buf))
assert.Equal(t, snapshot.RecordBytes(), buf.Bytes())
})
}
}
func TestRecord(t *testing.T) {
t.Parallel()
suite.Run(t, &RecordTestSuite{})
}
@@ -0,0 +1,6 @@
{
"type": 20,
"version": 772,
"payload": "sxS+0oAyk+NBv0LLVtQOp9WSx4CweyUZPz01tQ0o4oyp8aaBl6/kMFvLq3q52KE8lCiKejLw2NxVBUkE+4izCf2gLx9qfr81opWnqJTChWzcDijvttbq9cmtDFNL+odKsS3v1/TfYEFtPsoRPrJRmOHRAnqnf49Y5Q==",
"record": "FAMEAHmzFL7SgDKT40G/QstW1A6n1ZLHgLB7JRk/PTW1DSjijKnxpoGXr+QwW8urernYoTyUKIp6MvDY3FUFSQT7iLMJ/aAvH2p+vzWilaeolMKFbNwOKO+21ur1ya0MU0v6h0qxLe/X9N9gQW0+yhE+slGY4dECeqd/j1jl"
}
@@ -0,0 +1,6 @@
{
"type": 22,
"version": 772,
"payload": "waNH223htyxCBKAb6hm0u/SK/9mhI8Ck91nfWob7QMOaIREogrDYREJH4Djcp47XrpAlEaUIDiCvoFLVJ/LK1nYs4swzfHSSl/+Aj1eqPA63XqPa8EG4FAbf0DwjwXxV9qVIhvP9b2TafKbzr4Yb5GCygzFRb/zawA==",
"record": "FgMEAHnBo0fbbeG3LEIEoBvqGbS79Ir/2aEjwKT3Wd9ahvtAw5ohESiCsNhEQkfgONynjteukCURpQgOIK+gUtUn8srWdizizDN8dJKX/4CPV6o8Drdeo9rwQbgUBt/QPCPBfFX2pUiG8/1vZNp8pvOvhhvkYLKDMVFv/NrA"
}
@@ -0,0 +1,6 @@
{
"type": 23,
"version": 769,
"payload": "jmJ0o1E5+ehAHHYAbCo4AMV03X7RSivYl250s06nD9CO44fyjaoGELz0N7IeCg1jFKcRVSCRmYYmiIY9wydn2fXOJhKif8B0BlM3qhbethYgyP+l1S8hyyETpIiOtiiiOnAJwl1D1j9OryFiJFSdRRXReIMZ4CPqPg==",
"record": "FwMBAHmOYnSjUTn56EAcdgBsKjgAxXTdftFKK9iXbnSzTqcP0I7jh/KNqgYQvPQ3sh4KDWMUpxFVIJGZhiaIhj3DJ2fZ9c4mEqJ/wHQGUzeqFt62FiDI/6XVLyHLIROkiI62KKI6cAnCXUPWP06vIWIkVJ1FFdF4gxngI+o+"
}
@@ -0,0 +1,6 @@
{
"type": 22,
"version": 769,
"payload": "hBnpBnNUdlqe/rKXa7Judcz79u7AkUgSGOycn8EqvbkZpVxnI31rNOvAsPZqG+GF7DWJ3R7H2ETmFmrpnyyng32MjSs1jptmV1oAs63zTADD7sVipgid9AJHwfl4CrC3FIQr43IPMYd29JPOl5bqu/SfrgI16PBiJw==",
"record": "FgMBAHmEGekGc1R2Wp7+spdrsm51zPv27sCRSBIY7JyfwSq9uRmlXGcjfWs068Cw9mob4YXsNYndHsfYROYWaumfLKeDfYyNKzWOm2ZXWgCzrfNMAMPuxWKmCJ30AkfB+XgKsLcUhCvjcg8xh3b0k86Xluq79J+uAjXo8GIn"
}
@@ -0,0 +1,6 @@
{
"type": 23,
"version": 770,
"payload": "Vm/C+DO56czlbtR915aHzsugSyDtp8CtojF9w1jKY0efyyfcLrNuhNg/pZm3gQ7v2BBbL1UJ97v/RIjST+5gRIfg3bBN1BE9hkf+N2AYY2lHLi0yeInHB0zFWPeHscsDopDFadIi5KtC8HvbEMuK+kK8POVk5tN9UQ==",
"record": "FwMCAHlWb8L4M7npzOVu1H3XlofOy6BLIO2nwK2iMX3DWMpjR5/LJ9wus26E2D+lmbeBDu/YEFsvVQn3u/9EiNJP7mBEh+DdsE3UET2GR/43YBhjaUcuLTJ4iccHTMVY94exywOikMVp0iLkq0Lwe9sQy4r6Qrw85WTm031R"
}
@@ -0,0 +1,6 @@
{
"type": 22,
"version": 770,
"payload": "ajPzpsgk4gwm2stRQKbllvKRLdI7vmyaj1uxEJ/kKoQnQSPumdDNKD618U2Cq6PVd0/b+9YtH67Uzx1QxtpKuby5fUXqw06WUuDAQsmjq7F26EkE5FND6rQUjUPC+e1U0dF4TQzOUSS4IAkFQPAaVehUVTRxVWa/0g==",
"record": "FgMCAHlqM/OmyCTiDCbay1FApuWW8pEt0ju+bJqPW7EQn+QqhCdBI+6Z0M0oPrXxTYKro9V3T9v71i0frtTPHVDG2kq5vLl9RerDTpZS4MBCyaOrsXboSQTkU0PqtBSNQ8L57VTR0XhNDM5RJLggCQVA8BpV6FRVNHFVZr/S"
}
@@ -0,0 +1,6 @@
{
"type": 20,
"version": 771,
"payload": "d1Hiv1NYVgEDR9mtJyv9j8mg3dWqfUpeKfOsL+jzSDfVIxeDiJZFLDT50TjNW44/yEOVEX/Y/pk+wnc7E8aCEiwGwAvB+Insw1UCJ2ejt689VWLo2u4klGVKTHuOpUvdGVTc7Lo4FAt91KQSPLYB5iqxomjEv5e3Vg==",
"record": "FAMDAHl3UeK/U1hWAQNH2a0nK/2PyaDd1ap9Sl4p86wv6PNIN9UjF4OIlkUsNPnROM1bjj/IQ5URf9j+mT7CdzsTxoISLAbAC8H4iezDVQInZ6O3rz1VYuja7iSUZUpMe46lS90ZVNzsujgUC33UpBI8tgHmKrGiaMS/l7dW"
}
@@ -0,0 +1,6 @@
{
"type": 23,
"version": 771,
"payload": "wbdU1CbrzuAJDsh6CFjGyE+AFArJj/Wmsa2wtDyW0kRuE2vUO8gg+nXkg0kkoz0WnvQEOdaswfJIaVrloD78yoyeQVfBB+VUP/63vqn60v5ccaQEn0jLdxgLjiTAxKDQDxCTMRoLnFE2ZZf28zw+HfqpIxiOZs8LhQ==",
"record": "FwMDAHnBt1TUJuvO4AkOyHoIWMbIT4AUCsmP9aaxrbC0PJbSRG4Ta9Q7yCD6deSDSSSjPRae9AQ51qzB8khpWuWgPvzKjJ5BV8EH5VQ//re+qfrS/lxxpASfSMt3GAuOJMDEoNAPEJMxGgucUTZll/bzPD4d+qkjGI5mzwuF"
}
@@ -0,0 +1,6 @@
{
"type": 23,
"version": 772,
"payload": "qqnBMb1Af3zZt4DPHpVRuIiON9ODGJUNFicFjranORh67L/HI4D6HnHyycZFUSBOw2FjMBF6UialY8snOYaRKrQmQzuUNg1Ztq7yAZ+Lgj3TBarR6OMlYhEAY0Px9Xv1UuJ0YcvQx33gdM1skJ5HBR3yZvEKNJV1LA==",
"record": "FwMEAHmqqcExvUB/fNm3gM8elVG4iI4304MYlQ0WJwWOtqc5GHrsv8cjgPoecfLJxkVRIE7DYWMwEXpSJqVjyyc5hpEqtCZDO5Q2DVm2rvIBn4uCPdMFqtHo4yViEQBjQ/H1e/VS4nRhy9DHfeB0zWyQnkcFHfJm8Qo0lXUs"
}
@@ -0,0 +1,6 @@
{
"type": 20,
"version": 769,
"payload": "NEe735TuQFp7bWpFQhASas/e1XaySvus0ovXmkfCbFq334MyFHq2eDMadziXsfu/GfBjoYggvk0LgYUeoAkBNKR0dfSovjSndaqmIUonoWl+6sZObiGZkRIMwuY2q4Eaw4/iuDu/pZhjRW/iAIH+YH7cyk/1tgdJDg==",
"record": "FAMBAHk0R7vflO5AWnttakVCEBJqz97VdrJK+6zSi9eaR8JsWrffgzIUerZ4Mxp3OJex+78Z8GOhiCC+TQuBhR6gCQE0pHR19Ki+NKd1qqYhSiehaX7qxk5uIZmREgzC5jargRrDj+K4O7+lmGNFb+IAgf5gftzKT/W2B0kO"
}
@@ -0,0 +1,6 @@
{
"type": 22,
"version": 771,
"payload": "wrXjZrPm3OSyzO0klv6/G+z2PDloR/colS/RlWwQE31Vb2xm8YkEchDDKwlc/KPLD73qMoz3MQOQLtSLc8LhVYp+l7L9jz49yTaVKtBI5UuGbo09snsKxFCgCyYUBETKabATBQtiaEu/D8dmF4Yk/2ww4sEb8DwKLQ==",
"record": "FgMDAHnCteNms+bc5LLM7SSW/r8b7PY8OWhH9yiVL9GVbBATfVVvbGbxiQRyEMMrCVz8o8sPveoyjPcxA5Au1ItzwuFVin6Xsv2PPj3JNpUq0EjlS4ZujT2yewrEUKALJhQERMppsBMFC2JoS78Px2YXhiT/bDDiwRvwPAot"
}
@@ -0,0 +1,6 @@
{
"type": 20,
"version": 770,
"payload": "OU5s8Sa11hpXWEarWzFlX55IZt3Eo+F4AMbQ/2RwB4rfHS/JNl8n63OR4oYs9QXw3RfCrYJuU9n6Xn+I/+7ZzAgZ0PbLSXW1PrLtttdfmhTErK90b49YEWdY9na4g++NMkKykwgXvY1hNxZIHX/qawEWJgxXUR3DdQ==",
"record": "FAMCAHk5TmzxJrXWGldYRqtbMWVfnkhm3cSj4XgAxtD/ZHAHit8dL8k2Xyfrc5Hihiz1BfDdF8Ktgm5T2fpef4j/7tnMCBnQ9stJdbU+su2211+aFMSsr3Rvj1gRZ1j2driD740yQrKTCBe9jWE3Fkgdf+prARYmDFdRHcN1"
}
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
"sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCACq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFANAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
"sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMDXvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPsgSt2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgB/7oLx9JElIALsLJS91H2QNyU1H0osKwIUelVndsLyIALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "oYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12os=",
"sessionId": "FGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8g=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMDoYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12osgFGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8gANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAga6CocpFP8Qd4YCFR9pkaCr97po2ALj0P5nI9Nnb3UWMALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1ro=",
"sessionId": "jxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMD5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1rogjxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8ANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgrulAaqUdKeVYM0F+pu6on/h6LBpOyzOKG4xFIKcoFk4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQ=",
"sessionId": "00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAU=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMD8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQg00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAUANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAg/9P7140NtKzjyDwBf99mOy1+FjRPAPHTNQ9WxHOKpV4ALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "zja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4=",
"sessionId": "qPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnA=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "AQAB/AMDzja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4gqPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnAANBMDEwETAsAswCvAJMAjwArACcypwDDAL8AowCfAFMATzKgAnQCcAD0APAA1AC/ACMASAAoBAAF//wEAAQAAAAAbABkAABZzdG9yYWdlLmdvb2dsZWFwaXMuY29tABcAAAANABgAFgQDCAQEAQUDAgMIBQgFBQEIBgYBAgEABQAFAQAAAAAzdAAAABIAAAAQADAALgJoMgVoMi0xNgVoMi0xNQVoMi0xNAhzcGR5LzMuMQZzcGR5LzMIaHR0cC8xLjEACwACAQAAMwAmACQAHQAgXviLRAqAYJ8xOLdlcsUhldI4Xl0g/s9+y2Qrd8raPEgALQACAQEAKwAJCAMEAwMDAgMBAAoACgAIAB0AFwAYABkAFQChAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
+91
View File
@@ -0,0 +1,91 @@
package faketls
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"io"
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 := &bytes.Buffer{}
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
rec.Type = record.TypeHandshake
rec.Version = record.Version12
generateServerHello(&rec.Payload, clientHello)
rec.Dump(buf) //nolint: errcheck
rec.Reset()
rec.Type = record.TypeChangeCipherSpec
rec.Version = record.Version12
rec.Payload.WriteByte(ChangeCipherValue)
rec.Dump(buf) //nolint: errcheck
rec.Reset()
rec.Type = record.TypeApplicationData
rec.Version = record.Version12
if _, err := io.CopyN(&rec.Payload, rand.Reader, int64(1024+mrand.IntN(3092))); err != nil {
panic(err)
}
rec.Dump(buf) //nolint: errcheck
packet := buf.Bytes()
mac := hmac.New(sha256.New, secret)
mac.Write(clientHello.Random[:])
mac.Write(packet)
copy(packet[WelcomePacketRandomOffset:], mac.Sum(nil))
if _, err := writer.Write(packet); err != nil {
return err //nolint: wrapcheck
}
return nil
}
func generateServerHello(writer io.Writer, clientHello ClientHello) {
bodyBuf := &bytes.Buffer{}
sliceBuf := [2]byte{}
digest := [RandomLen]byte{}
binary.BigEndian.PutUint16(sliceBuf[:], uint16(record.Version12))
bodyBuf.Write(sliceBuf[:])
bodyBuf.Write(digest[:])
bodyBuf.WriteByte(byte(len(clientHello.SessionID)))
bodyBuf.Write(clientHello.SessionID)
binary.BigEndian.PutUint16(sliceBuf[:], clientHello.CipherSuite)
bodyBuf.Write(sliceBuf[:])
bodyBuf.Write(serverHelloSuffix)
scalar := [32]byte{}
if _, err := rand.Read(scalar[:]); err != nil {
panic(err)
}
curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint)
bodyBuf.Write(curve)
header := [4]byte{0, 0, 0, 0}
binary.BigEndian.PutUint32(header[:], uint32(bodyBuf.Len()))
header[0] = HandshakeTypeServer
writer.Write(header[:]) //nolint: errcheck
bodyBuf.WriteTo(writer) //nolint: errcheck
}
+82
View File
@@ -0,0 +1,82 @@
package faketls_test
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"testing"
"time"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record"
"github.com/stretchr/testify/suite"
)
type WelcomeTestSuite struct {
suite.Suite
h *faketls.ClientHello
buf *bytes.Buffer
secret mtglib.Secret
}
func (suite *WelcomeTestSuite) SetupTest() {
suite.h = &faketls.ClientHello{
Time: time.Now(),
Host: "google.com",
CipherSuite: 4867,
SessionID: make([]byte, 32),
}
_, err := rand.Read(suite.h.SessionID) //nolint: staticcheck
suite.NoError(err)
_, err = rand.Read(suite.h.Random[:]) //nolint: staticcheck
suite.NoError(err)
suite.buf = &bytes.Buffer{}
suite.secret = mtglib.GenerateSecret("google.com")
}
func (suite *WelcomeTestSuite) TestOk() {
suite.NoError(faketls.SendWelcomePacket(suite.buf, suite.secret.Key[:], *suite.h))
welcomePacket := []byte{}
welcomePacket = append(welcomePacket, suite.buf.Bytes()...)
rec := record.AcquireRecord()
defer record.ReleaseRecord(rec)
suite.NoError(rec.Read(suite.buf))
suite.Equal(record.TypeHandshake, rec.Type)
suite.Equal(record.Version12, rec.Version)
suite.NoError(rec.Read(suite.buf))
suite.Equal(record.TypeChangeCipherSpec, rec.Type)
suite.Equal(record.Version12, rec.Version)
suite.NoError(rec.Read(suite.buf))
suite.Equal(record.TypeApplicationData, rec.Type)
suite.Equal(record.Version12, rec.Version)
suite.Empty(suite.buf.Bytes())
random := make([]byte, 32)
copy(random, welcomePacket[11:])
empty := make([]byte, 32)
copy(welcomePacket[11:], empty)
mac := hmac.New(sha256.New, suite.secret.Key[:])
mac.Write(suite.h.Random[:])
mac.Write(welcomePacket)
suite.Equal(random, mac.Sum(nil))
}
func TestWelcome(t *testing.T) {
t.Parallel()
suite.Run(t, &WelcomeTestSuite{})
}
+4
View File
@@ -1,5 +1,9 @@
package relay package relay
const (
copyBufferSize = 64 * 1024
)
type Logger interface { type Logger interface {
Printf(msg string, args ...any) Printf(msg string, args ...any)
} }
@@ -1,13 +0,0 @@
//go:build mips || mipsle
package relay
import "github.com/9seconds/mtg/v2/mtglib/internal/tls"
const (
// MIPS is quite short in resources, and usually it means that it will run
// on Microtiks, OpenWRT-based routers or similar hardware. I think it worth
// to sacrifice a number of read syscalls (read, CPU load) to shrink
// limited RAM resources.
bufPoolSize = tls.MaxRecordPayloadSize / 2
)
@@ -1,9 +0,0 @@
//go:build !mips && !mipsle
package relay
import "github.com/9seconds/mtg/v2/mtglib/internal/tls"
const (
bufPoolSize = tls.MaxRecordPayloadSize
)
-18
View File
@@ -1,18 +0,0 @@
package relay
import "sync"
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, bufPoolSize)
return &b
},
}
func acquireBuffer() *[]byte {
return bufPool.Get().(*[]byte)
}
func releaseBuffer(p *[]byte) {
bufPool.Put(p)
}
+5 -6
View File
@@ -15,11 +15,11 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
stop := context.AfterFunc(ctx, func() { go func() {
<-ctx.Done()
telegramConn.Close() //nolint: errcheck telegramConn.Close() //nolint: errcheck
clientConn.Close() //nolint: errcheck clientConn.Close() //nolint: errcheck
}) }()
defer stop()
closeChan := make(chan struct{}) closeChan := make(chan struct{})
@@ -35,13 +35,12 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
} }
func pump(log Logger, src, dst essentials.Conn, direction string) { func pump(log Logger, src, dst essentials.Conn, direction string) {
buf := acquireBuffer() var buf [copyBufferSize]byte
defer releaseBuffer(buf)
defer src.CloseRead() //nolint: errcheck defer src.CloseRead() //nolint: errcheck
defer dst.CloseWrite() //nolint: errcheck defer dst.CloseWrite() //nolint: errcheck
n, err := io.CopyBuffer(src, dst, *buf) n, err := io.CopyBuffer(src, dst, buf[:])
switch { switch {
case err == nil: case err == nil:
-84
View File
@@ -1,84 +0,0 @@
package tls
import (
"bufio"
"bytes"
"github.com/9seconds/mtg/v2/essentials"
)
const (
SizeRecordType = 1
SizeVersion = 2
SizeSize = 2
SizeHeader = SizeRecordType + SizeVersion + SizeSize
MaxRecordSize = 16384
MaxRecordPayloadSize = MaxRecordSize - SizeHeader
DefaultBufferSize = 4096
TypeChangeCipherSpec = 0x14
TypeHandshake = 0x16
TypeApplicationData = 0x17
)
// TLS 1.2 is used for both TLS 1.2 and 1.3
var TLSVersion = [SizeVersion]byte{3, 3}
// Conn presents an established TLS 1.3 connection, after handshake
type Conn struct {
essentials.Conn
p *connPayload
}
type connPayload struct {
readBuf bytes.Buffer
connBuffered *bufio.Reader
read bool
write bool
}
func (c Conn) Write(p []byte) (int, error) {
if !c.p.write {
return c.Conn.Write(p)
}
return len(p), WriteRecord(c.Conn, p)
}
func (c Conn) Read(p []byte) (int, error) {
if !c.p.read {
return c.Conn.Read(p)
}
for {
if n, err := c.p.readBuf.Read(p); err == nil {
return n, nil
}
recordType, _, err := ReadRecord(c.p.connBuffered, &c.p.readBuf)
if err != nil {
return 0, err
}
if recordType != TypeApplicationData {
c.p.readBuf.Reset()
}
}
}
func New(conn essentials.Conn, read, write bool) Conn {
newConn := Conn{
Conn: conn,
p: &connPayload{
connBuffered: bufio.NewReaderSize(conn, DefaultBufferSize),
read: read,
write: write,
},
}
newConn.p.readBuf.Grow(DefaultBufferSize)
return newConn
}
-160
View File
@@ -1,160 +0,0 @@
package tls
import (
"io"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type ConnTestSuite struct {
suite.Suite
connMock *testlib.EssentialsConnMock
}
func (suite *ConnTestSuite) SetupTest() {
suite.connMock = &testlib.EssentialsConnMock{}
}
func (suite *ConnTestSuite) TearDownTest() {
suite.connMock.AssertExpectations(suite.T())
}
func (suite *ConnTestSuite) feedRead(raw []byte) {
suite.connMock.
On("Read", mock.AnythingOfType("[]uint8")).
Run(func(args mock.Arguments) {
copy(args.Get(0).([]byte), raw)
}).
Return(len(raw), nil).
Once()
suite.connMock.
On("Read", mock.AnythingOfType("[]uint8")).
Return(0, io.EOF).
Maybe()
}
func (suite *ConnTestSuite) TestReadTLSEnabled() {
payload := []byte("hello world")
suite.feedRead(MakeTLSRecord(0x17, payload))
conn := New(suite.connMock, true, false)
buf := make([]byte, 128)
n, err := conn.Read(buf)
suite.NoError(err)
suite.Equal(payload, buf[:n])
}
func (suite *ConnTestSuite) TestReadTLSSkipsNonApplicationData() {
raw := append(
MakeTLSRecord(0x14, []byte{1}),
MakeTLSRecord(0x17, []byte("real data"))...,
)
suite.feedRead(raw)
conn := New(suite.connMock, true, false)
buf := make([]byte, 128)
n, err := conn.Read(buf)
suite.NoError(err)
suite.Equal([]byte("real data"), buf[:n])
}
func (suite *ConnTestSuite) TestReadTLSMultipleRecords() {
raw := append(
MakeTLSRecord(0x17, []byte("first")),
MakeTLSRecord(0x17, []byte("second"))...,
)
suite.feedRead(raw)
conn := New(suite.connMock, true, false)
buf := make([]byte, 128)
n, err := conn.Read(buf)
suite.NoError(err)
suite.Equal([]byte("first"), buf[:n])
n, err = conn.Read(buf)
suite.NoError(err)
suite.Equal([]byte("second"), buf[:n])
}
func (suite *ConnTestSuite) TestReadTLSSmallBuffer() {
payload := []byte("hello world, this is a longer payload")
suite.feedRead(MakeTLSRecord(0x17, payload))
conn := New(suite.connMock, true, false)
small := make([]byte, 5)
n, err := conn.Read(small)
suite.NoError(err)
suite.Equal(payload[:5], small[:n])
rest := make([]byte, 128)
n, err = conn.Read(rest)
suite.NoError(err)
suite.Equal(payload[5:], rest[:n])
}
func (suite *ConnTestSuite) TestReadPassthrough() {
data := []byte("raw bytes")
suite.connMock.
On("Read", mock.AnythingOfType("[]uint8")).
Run(func(args mock.Arguments) {
copy(args.Get(0).([]byte), data)
}).
Return(len(data), nil).
Once()
conn := New(suite.connMock, false, false)
buf := make([]byte, 128)
n, err := conn.Read(buf)
suite.NoError(err)
suite.Equal(data, buf[:n])
}
func (suite *ConnTestSuite) TestWritePassthrough() {
data := []byte("outgoing data")
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(len(data), nil).
Once()
conn := New(suite.connMock, false, false)
n, err := conn.Write(data)
suite.NoError(err)
suite.Equal(len(data), n)
}
func (suite *ConnTestSuite) TestWriteTLSEnabled() {
data := []byte("outgoing data")
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(len(data), nil).
Once()
conn := New(suite.connMock, false, true)
n, err := conn.Write(data)
suite.NoError(err)
suite.Equal(len(data), n)
}
func TestConn(t *testing.T) {
t.Parallel()
suite.Run(t, &ConnTestSuite{})
}
-21
View File
@@ -1,21 +0,0 @@
package fake
import (
"bytes"
"sync"
)
var bytesPool = sync.Pool{
New: func() any {
return &bytes.Buffer{}
},
}
func acquireBuffer() *bytes.Buffer {
return bytesPool.Get().(*bytes.Buffer)
}
func releaseBuffer(b *bytes.Buffer) {
b.Reset()
bytesPool.Put(b)
}
-261
View File
@@ -1,261 +0,0 @@
package fake
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"slices"
"time"
)
const (
TypeHandshakeClient = 0x01
RandomLen = 32
// record_type(1) + version(2) + size(2) + handshake_type(1) + uint24_length(3) + client_version(2)
RandomOffset = 1 + 2 + 2 + 1 + 3 + 2
// https://datatracker.ietf.org/doc/html/rfc8701#name-grease-values
// https://medium.com/asecuritysite-when-bob-met-alice/in-cybersecurity-what-is-grease-9f8850558dea
GreaseMask = 0x0f0f
GreaseValueType = 0x0a0a
sniDNSNamesListType = 0
)
var (
emptyRandom = [RandomLen]byte{}
extTypeSNI = [2]byte{}
ErrCannotFindCipher = errors.New("cannot find a cipher")
)
type ClientHello struct {
Random [RandomLen]byte
SessionID []byte
CipherSuite uint16
}
func ReadClientHello(
conn net.Conn,
secret []byte,
hostname string,
tolerateTimeSkewness time.Duration,
) (*ClientHello, error) {
// This is how FakeTLS is organized:
// 1. We create sha256 HMAC with a given secret
// 2. We dump there a whole TLS frame except of the fact that random
// is filled with all zeroes
// 3. Digest is computed. This digest should be XORed with
// original client random
// 4. New digest should be all 0 except of last 4 bytes
// 5. Last 4 bytes are little endian uint32 of UNIX timestamp when
// this message was created.
clientHelloCopy, handshakeReader, err := parseClientHello(conn)
if err != nil {
return nil, fmt.Errorf("cannot read client hello: %w", err)
}
hello, err := parseHandshake(handshakeReader)
if err != nil {
return nil, fmt.Errorf("cannot parse handshake: %w", err)
}
sniHostnames, err := parseSNI(handshakeReader)
if err != nil {
return nil, fmt.Errorf("cannot parse SNI: %w", err)
}
if !slices.Contains(sniHostnames, hostname) {
return nil, fmt.Errorf("cannot find %s in %v", hostname, sniHostnames)
}
digest := hmac.New(sha256.New, secret)
// we write a copy of the handshake with client random all nullified.
digest.Write(clientHelloCopy.Next(RandomOffset))
clientHelloCopy.Next(RandomLen)
digest.Write(emptyRandom[:])
digest.Write(clientHelloCopy.Bytes())
computed := digest.Sum(nil)
for i := range RandomLen {
computed[i] ^= hello.Random[i]
}
if subtle.ConstantTimeCompare(emptyRandom[:RandomLen-4], computed[:RandomLen-4]) != 1 {
return nil, ErrBadDigest
}
timestamp := int64(binary.LittleEndian.Uint32(computed[RandomLen-4:]))
createdAt := time.Unix(timestamp, 0)
if tdiff := time.Since(createdAt).Abs(); tdiff > tolerateTimeSkewness {
return nil, fmt.Errorf("timestamp %q is too old %s", createdAt, tdiff)
}
return hello, nil
}
func parseHandshake(r io.Reader) (*ClientHello, error) {
// A protocol version of "3,3" (meaning TLS 1.2) is given.
header := [2]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read client version: %w", err)
}
hello := &ClientHello{}
if _, err := io.ReadFull(r, hello.Random[:]); err != nil {
return nil, fmt.Errorf("cannot read client random: %w", err)
}
if _, err := io.ReadFull(r, header[:1]); err != nil {
return nil, fmt.Errorf("cannot read session ID length: %w", err)
}
hello.SessionID = make([]byte, int(header[0]))
if _, err := io.ReadFull(r, hello.SessionID); err != nil {
return nil, fmt.Errorf("cannot read session id: %w", err)
}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read cipher suite length: %w", err)
}
cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
// Pick the first non-GREASE cipher suite from the list.
// Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a),
// so echoing them back is a trivial DPI fingerprint.
// cipherSuiteLen is in bytes; each cipher suite is 2 bytes.
for range cipherSuiteLen / 2 {
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read cipher suite: %w", err)
}
if hello.CipherSuite != 0 {
// do not forget we have to scan until the end
continue
}
if cs := binary.BigEndian.Uint16(header[:]); cs&GreaseMask != GreaseValueType {
hello.CipherSuite = cs
}
}
if hello.CipherSuite == 0 {
return nil, ErrCannotFindCipher
}
if _, err := io.ReadFull(r, header[:1]); err != nil {
return nil, fmt.Errorf("cannot read compression methods length: %w", err)
}
if _, err := io.CopyN(io.Discard, r, int64(header[0])); err != nil {
return nil, fmt.Errorf("cannot skip compression methods: %w", err)
}
return hello, nil
}
func parseSNI(r io.Reader) ([]string, error) {
header := [2]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read length of TLS extensions: %w", err)
}
extensionsLength := int64(binary.BigEndian.Uint16(header[:]))
buf := &bytes.Buffer{}
buf.Grow(int(extensionsLength))
if _, err := io.CopyN(buf, r, extensionsLength); err != nil {
return nil, fmt.Errorf("cannot read extensions: %w", err)
}
for buf.Len() > 0 {
// 00 00 - assigned value for extension "server name"
// 00 18 - 0x18 (24) bytes of "server name" extension data follows
// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
// 00 - list entry is type 0x00 "DNS hostname"
// 00 13 - 0x13 (19) bytes of hostname follows
// 65 78 61 ... 6e 65 74 - "example.ulfheim.net"
// 00 00 - assigned value for extension "server name"
extTypeB := buf.Next(2)
if len(extTypeB) != 2 {
return nil, fmt.Errorf("cannot read extension type: %v", extTypeB)
}
// 00 18 - 0x18 (24) bytes of "server name" extension data follows
lengthB := buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("cannot read extension %v length: %v", extTypeB, lengthB)
}
length := int(binary.BigEndian.Uint16(lengthB))
extDataB := buf.Next(length)
if len(extDataB) != length {
return nil, fmt.Errorf("cannot read extension %v data: len %d != %d", extTypeB, length, len(extDataB))
}
if !bytes.Equal(extTypeB, extTypeSNI[:]) {
continue
}
buf.Reset()
buf.Write(extDataB)
// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
lengthB = buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("cannot read the length of the SNI record: %v", lengthB)
}
length = int(binary.BigEndian.Uint16(lengthB))
if length == 0 {
return nil, nil
}
listType, err := buf.ReadByte()
if err != nil {
return nil, fmt.Errorf("cannot read SNI list type: %w", err)
}
// 00 - list entry is type 0x00 "DNS hostname"
if listType != sniDNSNamesListType {
return nil, fmt.Errorf("incorrect SNI list type %#x", listType)
}
names := []string{}
for buf.Len() > 0 {
// 00 13 - 0x13 (19) bytes of hostname follows
lengthB = buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("incorrect length of the hostname: %v", lengthB)
}
length = int(binary.BigEndian.Uint16(lengthB))
name := buf.Next(length)
if len(name) != length {
return nil, fmt.Errorf("incorrect length of SNI hostname: len %d != %d", length, len(name))
}
names = append(names, string(name))
}
return names, nil
}
return nil, nil
}

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