diff --git a/contrib/sni-router/Caddyfile b/contrib/sni-router/Caddyfile new file mode 100644 index 0000000..9621307 --- /dev/null +++ b/contrib/sni-router/Caddyfile @@ -0,0 +1,19 @@ +{ + # Caddy listens on 8443 behind HAProxy, which passes raw TLS through. + # Caddy terminates TLS itself and auto-obtains a Let's Encrypt certificate. + # + # If your domain's DNS already points to this server, ACME HTTP-01 challenge + # works through the HAProxy http frontend (:80 → redirect). For DNS-01 + # or other ACME methods, see https://caddyserver.com/docs/automatic-https +} + +{$DOMAIN}:8443 { + tls { + # Use the ACME HTTP-01 challenge on port 80. + # HAProxy forwards :80 as HTTP, so Caddy can answer the challenge + # if you add an acl exception in haproxy.cfg (see README), or use + # DNS-01 instead. + } + root * /srv + file_server +} diff --git a/contrib/sni-router/README.md b/contrib/sni-router/README.md new file mode 100644 index 0000000..2ddcede --- /dev/null +++ b/contrib/sni-router/README.md @@ -0,0 +1,83 @@ +# 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 +``` + +## ACME (Let's Encrypt) notes + +Caddy needs to answer the ACME HTTP-01 challenge on port 80. The +default `haproxy.cfg` redirects all `:80` traffic to HTTPS. If Caddy +cannot obtain a certificate, either: + +1. Temporarily stop HAProxy, let Caddy bind `:80` directly for the + initial certificate, then start the full stack; or +2. Use DNS-01 validation in the Caddyfile (requires a DNS provider + plugin); or +3. Add an HAProxy ACL that passes `/.well-known/acme-challenge/` + requests to the Caddy backend instead of redirecting. + +## 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 | diff --git a/contrib/sni-router/docker-compose.yml b/contrib/sni-router/docker-compose.yml new file mode 100644 index 0000000..6216a00 --- /dev/null +++ b/contrib/sni-router/docker-compose.yml @@ -0,0 +1,53 @@ +# 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: + - "8443" + environment: + DOMAIN: ${DOMAIN:-example.com} + restart: unless-stopped + +volumes: + caddy_data: diff --git a/contrib/sni-router/haproxy.cfg b/contrib/sni-router/haproxy.cfg new file mode 100644 index 0000000..4762074 --- /dev/null +++ b/contrib/sni-router/haproxy.cfg @@ -0,0 +1,47 @@ +# 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 — redirect to HTTPS ------------------------------------------- + +frontend http + bind *:80 + mode http + 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 + server mtg mtg:3128 + +backend web + server web web:8443 diff --git a/contrib/sni-router/mtg-config.toml b/contrib/sni-router/mtg-config.toml new file mode 100644 index 0000000..8ce9291 --- /dev/null +++ b/contrib/sni-router/mtg-config.toml @@ -0,0 +1,13 @@ +# 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" + +[defense.anti-replay] +enabled = true +max-size = "1mib" +error-rate = 0.001 diff --git a/contrib/sni-router/www/index.html b/contrib/sni-router/www/index.html new file mode 100644 index 0000000..97f105d --- /dev/null +++ b/contrib/sni-router/www/index.html @@ -0,0 +1,5 @@ + + +
Replace this with your own content.
+