Skip to content

Commit 37e2f90

Browse files
committed
sync: merge upstream/master (9seconds/mtg) through 9bf7222
Upstream commits included: - 0c1d001 Add docker-compose example with HAProxy SNI router - d0412b2 Fix ACME HTTP-01 passthrough in HAProxy config - 602f85d Document firehol_level1 RFC1918 gotcha in blocklist defaults - 68a4685 Fix description of blocklist rejection behavior - 170346b Pass real client IPs through with PROXY protocol v2 - 5953f93 Merge PR 9seconds#462 from dolonet/contrib/docker-sni-router - 9bf7222 Merge PR 9seconds#467 from dolonet/docs/blocklist-lan-gotcha Conflict resolution: README.md kept as fork version (mtg-multi has its own README describing multi-secret and per-user stats features). example.config.toml auto-merged cleanly.
2 parents bc37bb9 + 9bf7222 commit 37e2f90

7 files changed

Lines changed: 268 additions & 0 deletions

File tree

contrib/sni-router/Caddyfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
# Caddy sits behind HAProxy which passes raw TLS through on :8443.
3+
# ACME HTTP-01 challenges arrive on :80 via HAProxy's acl passthrough.
4+
http_port 80
5+
https_port 8443
6+
7+
# HAProxy forwards connections to :8443 with a PROXY protocol v2
8+
# header (see haproxy.cfg `send-proxy-v2`). The proxy_protocol
9+
# listener wrapper strips the header and exposes the real client IP
10+
# to Caddy's access log. The `tls` wrapper must follow so that TLS
11+
# is terminated on the unwrapped connection.
12+
#
13+
# `allow` lists the networks permitted to send PROXY headers. These
14+
# ranges cover docker compose's default bridge networks; tighten
15+
# them if you pin a specific subnet in docker-compose.yml.
16+
servers :8443 {
17+
listener_wrappers {
18+
proxy_protocol {
19+
timeout 5s
20+
allow 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
21+
}
22+
tls
23+
}
24+
}
25+
}
26+
27+
{$DOMAIN} {
28+
root * /srv
29+
file_server
30+
}

contrib/sni-router/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# SNI-routing deployment for mtg
2+
3+
A turnkey `docker compose` setup that puts an SNI-aware TCP router
4+
(HAProxy) in front of mtg **and** a real web server (Caddy with
5+
automatic HTTPS).
6+
7+
## Why
8+
9+
Modern DPI systems actively probe suspected proxies. If the server
10+
closes the connection or returns something unexpected, the IP gets
11+
flagged. With this setup:
12+
13+
- **Telegram clients** connect to port 443, HAProxy sees the configured
14+
SNI and routes them to mtg (FakeTLS).
15+
- **Everything else** (browsers, DPI probes, scanners) is routed to
16+
Caddy, which responds with a real Let's Encrypt certificate and serves
17+
genuine web content.
18+
19+
Because your domain's DNS points to this server, the SNI/IP match is
20+
natural and passive DPI has nothing to flag.
21+
22+
## Quick start
23+
24+
```bash
25+
# 1. Point your domain's DNS A/AAAA record to this server's IP.
26+
27+
# 2. Generate an mtg secret:
28+
docker run --rm nineseconds/mtg:2 generate-secret --hex YOUR_DOMAIN
29+
30+
# 3. Edit the config files:
31+
# - mtg-config.toml → paste the secret
32+
# - haproxy.cfg → replace "example.com" in the SNI ACL
33+
# - .env or export → DOMAIN=your.domain
34+
35+
# 4. (Optional) put your site content into www/
36+
37+
# 5. Start:
38+
docker compose up -d
39+
40+
# 6. Verify:
41+
# - Open https://YOUR_DOMAIN in a browser → you should see the web page
42+
# - Configure Telegram with the proxy link from:
43+
docker compose exec mtg mtg access /config/config.toml
44+
```
45+
46+
## Real client IPs (PROXY protocol)
47+
48+
HAProxy forwards TCP connections to mtg and Caddy with a PROXY protocol
49+
v2 header so both backends see the real client IP instead of HAProxy's
50+
container address. The three pieces must stay in sync:
51+
52+
- `haproxy.cfg``send-proxy-v2` on the `mtg` and `web` backend `server` lines
53+
- `mtg-config.toml``proxy-protocol-listener = true`
54+
- `Caddyfile``listener_wrappers { proxy_protocol { ... } tls }` on `:8443`
55+
56+
If you disable one, disable all three, otherwise the backend will fail
57+
to parse the connection.
58+
59+
## ACME (Let's Encrypt) notes
60+
61+
HAProxy passes `/.well-known/acme-challenge/` requests on `:80` to
62+
Caddy so that HTTP-01 validation works out of the box. Make sure your
63+
domain's DNS A/AAAA record points to this server before starting.
64+
65+
## Architecture
66+
67+
```
68+
┌──────────────────┐
69+
:443 ──────>│ HAProxy │
70+
│ (TCP, SNI peek) │
71+
└──┬───────────┬───┘
72+
SNI match │ │ default
73+
v v
74+
┌─────────┐ ┌─────────┐
75+
│ mtg │ │ Caddy │
76+
│ :3128 │ │ :8443 │
77+
│ FakeTLS │ │ real TLS│
78+
└─────────┘ └─────────┘
79+
```
80+
81+
## Files
82+
83+
| File | Purpose |
84+
|---|---|
85+
| `docker-compose.yml` | Service definitions |
86+
| `haproxy.cfg` | SNI routing rules — **edit the domain** |
87+
| `mtg-config.toml` | mtg proxy config — **paste your secret** |
88+
| `Caddyfile` | Web server config (auto-HTTPS) |
89+
| `www/` | Static site content served by Caddy |
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SNI-routing deployment: HAProxy (443) -> mtg + real web backend
2+
#
3+
# This setup puts an SNI-aware TCP router in front of mtg so that:
4+
# - Telegram clients (FakeTLS with the correct SNI) are routed to mtg
5+
# - All other TLS traffic (including DPI probes) reaches the real web
6+
# server, which responds with a genuine certificate
7+
#
8+
# The result: active probes see a real website; passive DPI sees matching
9+
# SNI/IP because the domain resolves to this server's IP.
10+
#
11+
# Quick start:
12+
# 1. Set YOUR_DOMAIN below (and in mtg-config.toml)
13+
# 2. docker compose up -d
14+
# 3. mtg generate-secret YOUR_DOMAIN -> put it in mtg-config.toml
15+
# 4. docker compose restart mtg
16+
#
17+
# See BEST_PRACTICES.md and the project wiki for background.
18+
19+
services:
20+
haproxy:
21+
image: haproxy:lts-alpine
22+
ports:
23+
- "443:443"
24+
- "80:80"
25+
volumes:
26+
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
27+
depends_on:
28+
- mtg
29+
- web
30+
restart: unless-stopped
31+
32+
mtg:
33+
image: nineseconds/mtg:2
34+
volumes:
35+
- ./mtg-config.toml:/config/config.toml:ro
36+
expose:
37+
- "3128"
38+
restart: unless-stopped
39+
40+
web:
41+
image: caddy:alpine
42+
volumes:
43+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
44+
- caddy_data:/data
45+
- ./www:/srv:ro
46+
expose:
47+
- "80"
48+
- "8443"
49+
environment:
50+
DOMAIN: ${DOMAIN:-example.com}
51+
restart: unless-stopped
52+
53+
volumes:
54+
caddy_data:

contrib/sni-router/haproxy.cfg

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# HAProxy SNI router — Layer 4 (TCP mode)
2+
#
3+
# Inspects the SNI in the TLS ClientHello and routes traffic:
4+
# - SNI matching the mtg secret domain -> mtg (FakeTLS / MTProto)
5+
# - Everything else -> real web backend (Caddy)
6+
#
7+
# Because routing happens before TLS termination, each backend sees the
8+
# raw ClientHello and handles TLS itself. The real web backend therefore
9+
# presents a genuine certificate to any probe or browser.
10+
11+
global
12+
log stdout format raw local0 info
13+
maxconn 4096
14+
15+
defaults
16+
log global
17+
mode tcp
18+
option tcplog
19+
timeout connect 5s
20+
timeout client 60s
21+
timeout server 60s
22+
23+
# --- HTTP :80 — ACME challenges + redirect -----------------------------------
24+
25+
frontend http
26+
bind *:80
27+
mode http
28+
29+
# Let Caddy answer ACME HTTP-01 challenges for Let's Encrypt.
30+
acl is_acme path_beg /.well-known/acme-challenge/
31+
use_backend web_acme if is_acme
32+
33+
http-request redirect scheme https code 301
34+
35+
# --- TLS :443 — SNI-based routing -------------------------------------------
36+
37+
frontend tls
38+
bind *:443
39+
tcp-request inspect-delay 5s
40+
tcp-request content accept if { req_ssl_hello_type 1 }
41+
42+
# Route Telegram clients to mtg.
43+
# Replace "example.com" with the domain from your mtg secret.
44+
use_backend mtg if { req_ssl_sni -i example.com }
45+
46+
default_backend web
47+
48+
backend mtg
49+
# send-proxy-v2 prepends a PROXY protocol v2 header so mtg sees the
50+
# real client IP instead of HAProxy's. mtg must have
51+
# `proxy-protocol-listener = true` in its config.
52+
server mtg mtg:3128 send-proxy-v2
53+
54+
backend web
55+
# send-proxy-v2 prepends a PROXY protocol v2 header so Caddy logs the
56+
# real client IP instead of HAProxy's. Caddy must enable the
57+
# proxy_protocol listener wrapper on :8443 (see Caddyfile).
58+
server web web:8443 send-proxy-v2
59+
60+
backend web_acme
61+
mode http
62+
server web web:80

contrib/sni-router/mtg-config.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Minimal mtg configuration for the SNI-router setup.
2+
#
3+
# 1. Generate a secret: mtg generate-secret --hex example.com
4+
# 2. Paste it below.
5+
# 3. Replace example.com with your actual domain everywhere.
6+
7+
secret = "PASTE_YOUR_SECRET_HERE"
8+
bind-to = "0.0.0.0:3128"
9+
10+
# HAProxy in front sends PROXY protocol v2 headers so mtg can see the
11+
# real client IP. Keep this in sync with haproxy.cfg (`send-proxy-v2`).
12+
proxy-protocol-listener = true
13+
14+
[defense.anti-replay]
15+
enabled = true
16+
max-size = "1mib"
17+
error-rate = 0.001

contrib/sni-router/www/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head><meta charset="utf-8"><title>Welcome</title></head>
4+
<body><h1>It works!</h1><p>Replace this with your own content.</p></body>
5+
</html>

example.config.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,17 @@ download-concurrency = 2
334334
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
335335
# You can provider links here (starts with https:// or http://) or
336336
# path to a local file, but in this case it should be absolute.
337+
#
338+
# NOTE: the default list below (firehol_level1.netset) includes bogon
339+
# networks, and therefore RFC1918 ranges as well (10.0.0.0/8,
340+
# 172.16.0.0/12, 192.168.0.0/16). If you run mtg on a home/LAN network
341+
# and connect from a client on the same LAN, that client will be
342+
# rejected with "ip was blacklisted" and the connection dropped (TCP
343+
# close, no response). If you see this, you can either disable this section
344+
# (enabled = false), replace firehol_level1 with a narrower list that
345+
# does not include bogons (e.g. firehol_abusers_1d), or connect via
346+
# a public IP/domain with hairpin NAT on your router. See README for
347+
# details.
337348
urls = [
338349
"https://iplists.firehol.org/files/firehol_level1.netset",
339350
# "/local.file"

0 commit comments

Comments
 (0)