diff --git a/contrib/sni-router/Caddyfile b/contrib/sni-router/Caddyfile index 426a3ad..d3ec528 100644 --- a/contrib/sni-router/Caddyfile +++ b/contrib/sni-router/Caddyfile @@ -3,6 +3,25 @@ # 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} { diff --git a/contrib/sni-router/README.md b/contrib/sni-router/README.md index 19b99b1..0e4113d 100644 --- a/contrib/sni-router/README.md +++ b/contrib/sni-router/README.md @@ -43,6 +43,19 @@ docker compose up -d 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 diff --git a/contrib/sni-router/haproxy.cfg b/contrib/sni-router/haproxy.cfg index dabc8f9..2a18c1b 100644 --- a/contrib/sni-router/haproxy.cfg +++ b/contrib/sni-router/haproxy.cfg @@ -46,10 +46,16 @@ frontend tls default_backend web backend mtg - server mtg mtg:3128 + # 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 - server web web:8443 + # 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 diff --git a/contrib/sni-router/mtg-config.toml b/contrib/sni-router/mtg-config.toml index 8ce9291..c45046a 100644 --- a/contrib/sni-router/mtg-config.toml +++ b/contrib/sni-router/mtg-config.toml @@ -7,6 +7,10 @@ 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"