Skip to content

Add docker-compose example with HAProxy SNI router#462

Merged
9seconds merged 3 commits into9seconds:masterfrom
dolonet:contrib/docker-sni-router
Apr 14, 2026
Merged

Add docker-compose example with HAProxy SNI router#462
9seconds merged 3 commits into9seconds:masterfrom
dolonet:contrib/docker-sni-router

Conversation

@dolonet
Copy link
Copy Markdown
Contributor

@dolonet dolonet commented Apr 10, 2026

Summary

A turnkey docker compose setup in contrib/sni-router/ that deploys
mtg behind an HAProxy SNI router alongside a real Caddy web server with
automatic HTTPS.

Motivation

The setup recommended in BEST_PRACTICES.md — own domain, real web
server, SNI-based routing — requires piecing together several services.
This bundle packages it so operators can clone and run with minimal
configuration.

How it works:

  • HAProxy on :443 peeks at the TLS ClientHello SNI.
  • Connections with the configured domain SNI → mtg (FakeTLS).
  • Everything else → Caddy (real TLS, real certificate, real content).
  • HAProxy on :80 redirects to HTTPS.

Active probes see a genuine website with a valid Let's Encrypt
certificate. Passive DPI sees consistent SNI/IP because the domain's DNS
points to this server.

Files

File Purpose
docker-compose.yml Three services: haproxy, mtg, caddy
haproxy.cfg SNI routing rules (edit the domain)
mtg-config.toml Minimal mtg config (paste your secret)
Caddyfile Caddy auto-HTTPS on internal port
www/index.html Placeholder site content
README.md Quick start, architecture diagram, ACME notes

Test plan

  • docker compose config validates the compose file
  • Deploy on a test VPS with a real domain, verify:
    • curl https://DOMAIN/ returns the Caddy-served page
    • openssl s_client -connect IP:443 -servername random.example gets Caddy's cert
    • Telegram client connects through the proxy

Refs: #458

Turnkey deployment: HAProxy on :443 peeks at the TLS SNI and routes
Telegram clients to mtg while forwarding everything else (including DPI
probes) to a real Caddy web server with automatic HTTPS.

This is the setup recommended in BEST_PRACTICES.md, packaged so that
operators can clone and run it with minimal configuration.

Refs: 9seconds#458
Add an ACL that routes /.well-known/acme-challenge/ requests on :80
to Caddy instead of redirecting to HTTPS, so Let's Encrypt certificate
issuance works out of the box.

Also simplify Caddyfile to use Caddy's http_port/https_port directives.
@ivulit
Copy link
Copy Markdown
Contributor

ivulit commented Apr 11, 2026

I believe that in this scheme, you should use the PROXY protocol to pass the real client IP to MTG and the web server. MTG supports the PROXY protocol for both inbound and outbound connections, so please consider the configuration changes required to use the PROXY protocol.

@9seconds
Copy link
Copy Markdown
Owner

Nice one! But agree with @ivulit about PROXY protocol

Without this, mtg and Caddy see HAProxy's container IP for every
connection, which breaks meaningful logging, abuse handling, and any
IP-based blocklist logic.  HAProxy sends a PROXY protocol v2 header on
its TCP backends; mtg enables proxy-protocol-listener, and Caddy wraps
:8443 with a proxy_protocol listener before tls.

The :80 path (ACME HTTP-01 passthrough) is unchanged — client IP there
is not useful and HAProxy's http mode already adds X-Forwarded-For if
anyone wants it.

Requested in 9seconds#462 review.
@dolonet
Copy link
Copy Markdown
Contributor Author

dolonet commented Apr 13, 2026

Pushed 170346b wiring up PROXY protocol v2 end-to-end:

  • haproxy.cfgsend-proxy-v2 on the mtg and web backends (left web_acme alone — ACME HTTP-01 doesn't need it)
  • mtg-config.tomlproxy-protocol-listener = true
  • Caddyfilelistener_wrappers { proxy_protocol { allow <RFC1918> } tls } on :8443
  • README.md — new section flagging that the three settings must stay in sync

Verified on a test VPS: HAProxy emits a well-formed v2 header and Caddy's wrapper unwraps it — RemoteAddr/LocalAddr in Caddy match the real client port and HAProxy's frontend port (would otherwise be the backend-side ephemeral port). mtg-side parsing not tested end-to-end, but the config key is the one referenced from internal/cli/run_proxy.go.

@9seconds
Copy link
Copy Markdown
Owner

Thanks!

@9seconds 9seconds merged commit 5953f93 into 9seconds:master Apr 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants