Skip to content

Commit d0502e7

Browse files
committed
Merge remote-tracking branch 'origin/stable' into v2
2 parents fefc479 + 30aa9d3 commit d0502e7

File tree

96 files changed

+3920
-1427
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+3920
-1427
lines changed

.github/workflows/ci.yaml

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ jobs:
4848
- uses: jdx/mise-action@v3
4949
name: Install mise
5050

51+
- name: Cache Go modules and build
52+
uses: actions/cache@v5
53+
with:
54+
path: |
55+
~/go/pkg/mod
56+
~/.cache/go-build
57+
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
58+
restore-keys: |
59+
${{ runner.os }}-go-
60+
5161
- name: Run tests
5262
run: mise tasks run covtest
5363

@@ -69,6 +79,16 @@ jobs:
6979
- uses: jdx/mise-action@v3
7080
name: Install mise
7181

82+
- name: Cache Go modules and build
83+
uses: actions/cache@v5
84+
with:
85+
path: |
86+
~/go/pkg/mod
87+
~/.cache/go-build
88+
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
89+
restore-keys: |
90+
${{ runner.os }}-go-
91+
7292
- name: Run fuzzing
7393
run: mise tasks run 'test:fuzz:*'
7494

@@ -86,6 +106,16 @@ jobs:
86106
- uses: jdx/mise-action@v3
87107
name: Install mise
88108

109+
- name: Cache Go modules and build
110+
uses: actions/cache@v5
111+
with:
112+
path: |
113+
~/go/pkg/mod
114+
~/.cache/go-build
115+
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
116+
restore-keys: |
117+
${{ runner.os }}-go-
118+
89119
- name: Run linter
90120
run: mise tasks run lint
91121

@@ -123,14 +153,6 @@ jobs:
123153
- name: Setup BuildX
124154
uses: docker/setup-buildx-action@v3
125155

126-
- name: Setup cache
127-
uses: actions/cache@v5
128-
with:
129-
path: /tmp/buildx-cache
130-
key: ${{ runner.os }}-buildx-${{ github.sha }}
131-
restore-keys: |
132-
${{ runner.os }}-buildx-
133-
134156
- name: Login to DockerHub
135157
if: github.event_name != 'pull_request'
136158
uses: docker/login-action@v3
@@ -147,13 +169,13 @@ jobs:
147169
password: ${{ secrets.GITHUB_TOKEN }}
148170

149171
- name: Build and push
150-
uses: docker/build-push-action@v2
172+
uses: docker/build-push-action@v6
151173
with:
152174
pull: true
153175
context: .
154176
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
155177
push: ${{ github.event_name != 'pull_request' }}
156178
tags: ${{ steps.meta.outputs.tags }}
157179
labels: ${{ steps.meta.outputs.labels }}
158-
cache-from: type=local,src=/tmp/buildx-cache
159-
cache-to: type=local,dest=/tmp/buildx-cache
180+
cache-from: type=gha
181+
cache-to: type=gha,mode=max

.github/workflows/govulncheck.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
uses: actions/setup-go@v6
3636
with:
3737
go-version-file: go.mod
38+
cache: true
3839

3940
- name: Check for vulnerabilities
4041
run: |

.goreleaser.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ archives:
5454
- LICENSE
5555
- README.md
5656
- SECURITY.md
57+
- BEST_PRACTICES.md
58+
- example.config.toml
5759

5860
gomod:
5961
proxy: true

.mise.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ run = "govulncheck ./..."
3333

3434
[tasks.test]
3535
description = "Run tests"
36-
run = "go test -v ./..."
36+
run = "go test -v -race ./..."
3737

3838
[tasks.covtest]
3939
description = "Run tests with code coverage"
40-
run = "go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./..."
40+
run = "go test -coverprofile=coverage.txt -covermode=atomic -count=2 -race -v ./..."
4141

4242
[tasks.test-all]
4343
description = "Run all tests"
@@ -48,7 +48,7 @@ depends = [
4848

4949
[tasks."test:fuzz:client-hello"]
5050
description = "Run fuzzy test for ClientHello"
51-
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls"
51+
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
5252

5353
[tasks."test:fuzz:client-handshake"]
5454
description = "Run fuzzy test for ClientHandshake"

BEST_PRACTICES.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Best practices
2+
3+
This is unfortunate, but since 2018 many things were changed. Most of them
4+
became way worse. Previous iterations of censorship systems were very dumb,
5+
DPI were primitive and filtered very obvious things. Nowadays they are
6+
way more intelligent and it is very naive to treat them frivolously.
7+
8+
In 2026 is not enough to pretend that your mtg installation is a Microsoft
9+
website that sits in Amsterdam Digital Ocean location. Now your installation
10+
has to be a website that is mtg in disguise. Yes, it requires a bit more effort
11+
but this effort is probably less than rotating proxies each other day.
12+
13+
mtproto traffic, even with FakeTLS, has its specifics that are probably
14+
very well known by DPI systems. These specifics are not something unique but
15+
could mark an IP address as suspicious. Now let's think:
16+
17+
1. You have a proxy in Amsterdam Digital Ocean that tells it is microsoft.com
18+
how hard could it be to find out that this is probably fake? 1 or probably 2
19+
DNS queries for `microsoft.com`? In case of some CDN, there are ECS-powered
20+
resolvers that are very capable to return results from POV of some subnets.
21+
If censor sees no relevant results, will they be afraid to block IP?
22+
2. You have a proxy in Amsterdam Digital Ocean that tells it is a website from
23+
the same public subnet. But not the same. Would it be hard to make these DNS
24+
queries and ban IP?
25+
26+
The correct way of having this proxy is following:
27+
28+
1. Register a domain name
29+
2. Get some VPS, probably in your domestic location
30+
3. Set that domain name from a step 1 to IP address of that VPS
31+
4. Generate a couple of HTML pages by LLMs or even copy them from elsewhere
32+
5. Set some webserver and issue TLS certificates with Let's Encrypt or any other
33+
name
34+
6. Set mtg before this webserver.
35+
7. Use sing-box or anything like that to provide local socks5 interface and
36+
have VPNized uplinks
37+
8. Set up mtg to use socks5 from a 7 step.
38+
39+
In that case you will get a match of DNS and SNI in requests. As a side effect,
40+
your proxy will work with XTLS and its friends: XTLS in sniff mode ignores
41+
IP address a client wants to connect to. Instead, it reads SNI and connect
42+
to resolved address: a clever idea if user does not have a trustworthy DNS
43+
set up.
44+
45+
Yes, this is much longer that usual technique, and requires more effort. But
46+
this is could probably be very well automated to some reasonable extent.
47+
48+
Unfortunately, this is a best practice right now.
49+
50+
Do not also forget about other implementation, like
51+
[telemt](https://github.com/telemt/telemt). Try everything. Use VPNs. It does
52+
not really matter which project you are going to use as long it helps you to
53+
stay connected.
54+
55+
_March 2026._

Dockerfile

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ FROM golang:1.26-alpine AS build
55

66
ENV CGO_ENABLED=0
77

8-
RUN set -x \
9-
&& apk --no-cache --update add \
10-
bash \
11-
ca-certificates \
12-
git
8+
RUN --mount=type=cache,target=/var/cache/apk \
9+
set -x \
10+
&& apk --update add \
11+
bash \
12+
ca-certificates \
13+
git
14+
15+
COPY go.mod go.sum /app/
16+
WORKDIR /app
17+
18+
RUN go mod download
1319

1420
COPY . /app
15-
WORKDIR /app
1621

1722
RUN set -x \
1823
&& version="$(git describe --exact-match HEAD 2>/dev/null || git describe --tags --always)" \

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,33 @@ goal: to give a possibility to connect to Telegram in a restricted,
3838
censored environment. But it does it slightly differently in details
3939
that probably matter.
4040

41+
* **Domain fronting**
42+
43+
For years mtg supports domain fronting. This technique means that it fallbacks
44+
to accessing a real website in case if request fails. It could fail by many
45+
reasons: anti-replay protection, accidental access to the webserver or
46+
stale request. Anyway, if mtg rejects this request, it does not break a
47+
connection. It connects to the websites and replicates everything that client
48+
has sent, and simply proxies it back as is. Users will see a response from
49+
the real website, _byte-to-byte identical_ to the response of the real netloc.
50+
51+
* **Doppelganger**
52+
53+
mtg also is a doppelganger of the website it fronts. Sure, with domain fronting
54+
users will see replies of the real website in case if something will go wrong.
55+
But what about such cases when _everything is fine_?
56+
57+
In that case mtg mimics TLS connection statistical characteristics as close as
58+
possible. Different application have different statistics of their patterns.
59+
Big CDN steadily pumping the data, small websites burst with short easily
60+
compressiable chunks of traffic.
61+
62+
mtg artificially emulates those delays to be statistically indistinguishable
63+
from the real website even if it covers connection of the very specific app.
64+
It also follows 2 most common patterns of traffic chunking, so censors
65+
will have to put more resources to find out that we have Telegram here
66+
but not a hookah webshop served by nginx.
67+
4168
* **Resource-efficient**
4269

4370
It has to be resource-efficient. It does not mean that you will see
@@ -93,6 +120,8 @@ that probably matter.
93120
software (written in Golang) with a minimum effort + you can replace
94121
some parts with those you want.
95122

123+
Please also to read about [best practices](https://github.com/9seconds/mtg/blob/master/BEST_PRACTICES.md).
124+
96125
### Version 2
97126

98127
If you use version 1.x before, you are probably noticed some major
@@ -398,6 +427,55 @@ or if you are using docker:
398427
$ docker exec mtg-proxy /mtg access /config.toml
399428
```
400429

430+
## Doppelganger
431+
432+
mtg can mimic real websites, please take a look at relevant section in example
433+
config file.
434+
435+
mtg comes with some very good precollected statistics coming from
436+
[ok.ru](https://ok.ru/). It does not mean that you have to cover yourself
437+
by pretending that mtg is _ok.ru_. **Do not do that: ok.ru comes from very specific
438+
ASNs, but not from VPS providers you are going to use.** What I want to say
439+
is that defaults are very good enough to use as is because ok.ru for public
440+
pages has a very generic profile of TLS packets delay.
441+
442+
But for better results it is recommended to teach mtg about the website you
443+
will use as a domain front. In order to do that, you need to specify URLs
444+
from this website. Just go to it, open WebDeveloper console and pick up
445+
random URLs. For better results they have to be **from the same domain name
446+
you are going to use as a disguise** but serve light and heavy content: pages,
447+
images etc. Do not use many, 2-3 will probably work.
448+
449+
mtg will crawl these pages periodically, accumulating statistics and
450+
using it as you go.
451+
452+
```toml
453+
[defense.doppelganger]
454+
urls = [
455+
"https://lalala.com/index.html",
456+
"https://lalala.com/contacts.html",
457+
]
458+
```
459+
460+
This is not very necessary. Keep in mind these rules:
461+
462+
1. If you are not sure what is this all about, do nothing. Defaults are good.
463+
2. All URLs must be HTTPS
464+
3. All URLs should be from the same domain name (but this is not a rule)
465+
4. Do not use a lot of pages. Use _different_ pages. mtg will start using this
466+
statistics when it will accumulate enough anyway.
467+
5. These URLs should be directly accessible from mtg without proxies whatsoever
468+
6. Do not create huge raids. mtg will repeatedly crawl in raids, making N repeats.
469+
Do not use high N, you do not want to be noticeable.
470+
7. It makes no sense to have small delay between raids. Usually webservers
471+
do not update their TLS settings each hour.
472+
8. If you have some specific knowledge if webserver is using
473+
[TLS Dynamic Record Sizing](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/), you
474+
can use a very specific setting. This are Cloudflare, Go standard webservers,
475+
[caddy](https://caddyserver.com/) and [H2O](https://h2o.examp1e.net/). If so,
476+
you can enable `drs` setting.
477+
9. **If you are not sure, touch nothing!**
478+
401479
## Metrics
402480

403481
Out of the box, mtg works with

example.config.toml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,66 @@ tcp = "5s"
206206
http = "10s"
207207
idle = "1m"
208208

209+
# mtg has to mimic real websites. It does not mean domain fronting, it also
210+
# means that traffic characteristics should be similar to real world traffic.
211+
# websites and applications behave differently, their traffic patterns are also
212+
# different. Applications do bursts of RPC-style messages (or JSON communication,
213+
# does not really matter), while websites pump heavy content in HTTP2 streams
214+
#
215+
# It means that statistically there is a different between traffic shape:
216+
# delays between packets are also different.
217+
# In order to avoid censorship detection based on these patterns, there is a
218+
# mtg subsystem called "Doppelganger" that aims to mimic website statistics
219+
# as close as it could.
220+
#
221+
# Delays between TLS packets are not constant. There are many factors
222+
# that come in play. Application should generate some response, it could
223+
# send some headers first and stream content with chunked encoding. So
224+
# some first packets could come as soon as possible, with some delays
225+
# after first ones. Such phenomenon is described by different statistic
226+
# distribution. There are 2 distribution that describe it: lognormal
227+
# distribution and Weibul distribution. Lognormal is all about steady streams
228+
# of heavy content like a video. Weibul is great about short bursts like
229+
# user who requested a static page an a couple of images.
230+
[defense.doppelganger]
231+
# This is a list of URLs that would be crawled by mtg to approximate delay
232+
# statistics. They MUST be HTTPS urls.
233+
#
234+
# You can come to the website and collect different URLs, with light and
235+
# heavy content. We recommend to search for CDNs.
236+
urls = [
237+
# "https://st-ok.cdn-vk.ru/res/react/vendor/clsx-2.1.1-amd.js"
238+
]
239+
# A collection is done in raids. Each raid makes this number of requests to
240+
# each URL in this list. Do not use a huge number, 10 is probably ok.
241+
repeats-per-raid = 10
242+
# This is a duration between each raid. It makes no sense to have a small number
243+
# here as you would start to make a noticeable activity. Usually traffic patterns
244+
# do not change a lot, so do not expect different results if you request
245+
# each 10 minutes.
246+
raid-each = "6h"
247+
# This enables dynamic tls record sizing.
248+
#
249+
# Some modern stacks and platforms start to use the technique that is called
250+
# DRS. They start with small TLS packets and ramp up eventually. First packets
251+
# are usually about MTU size, after that we get 4k and eventually max size.
252+
# This is done with a good intention: to minimize a time to the first byte,
253+
# so application could start doing something with the data right after first
254+
# RTT.
255+
#
256+
# Apparently, about 90% of application do not employ this technique, they use
257+
# max size always: nginx, apache, java stuff. But Golang tools, angie and
258+
# some specific patches activate this technique.
259+
#
260+
# In order to mimic a real website we need to know something about software
261+
# it uses. Usually nobody cares: openssl does 16384, Python does it, nginx
262+
# does it. So this setting is disabled by default.
263+
#
264+
# https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
265+
# https://aws.github.io/s2n-tls/usage-guide/ch08-record-sizes.html
266+
# https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
267+
drs = false
268+
209269
# Some countries do active probing on Telegram connections. This technique
210270
# allows to protect from such effort.
211271
#

0 commit comments

Comments
 (0)