mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 11:54:01 +03:00
Merge pull request #462 from dolonet/contrib/docker-sni-router
Add docker-compose example with HAProxy SNI router
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
{
|
||||
# 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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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 |
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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:
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user