diff --git a/contrib/sni-router/Caddyfile b/contrib/sni-router/Caddyfile index d3ec52803..fabb184f0 100644 --- a/contrib/sni-router/Caddyfile +++ b/contrib/sni-router/Caddyfile @@ -10,14 +10,14 @@ # 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. + # `allow` lists the networks permitted to send PROXY headers. + # HAProxy runs in the host netns and reaches Caddy via host loopback + # (see docker-compose.yml), so the only legitimate peer is loopback. servers :8443 { listener_wrappers { proxy_protocol { timeout 5s - allow 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 + allow 127.0.0.0/8 ::1/128 } tls } diff --git a/contrib/sni-router/README.md b/contrib/sni-router/README.md index aed6e9301..7254354b3 100644 --- a/contrib/sni-router/README.md +++ b/contrib/sni-router/README.md @@ -58,6 +58,30 @@ must stay in sync: If you disable one, disable all four, otherwise the backend will fail to parse the connection. +## Why host networking for HAProxy + +HAProxy runs in the host network namespace (`network_mode: host` in +`docker-compose.yml`) so it sees the real client source IP on every +inbound connection. With the default bridge networking + published +ports the source IP is rewritten to the bridge gateway — by Docker's +userland proxy (`docker-proxy`), by rootless Podman's `slirp4netns` +or `pasta`, or by NAT on the Docker host — and the PROXY v2 header +HAProxy then sends to mtg and Caddy carries that useless address. +Host networking lifts HAProxy out of the rewrite path; mtg and Caddy +stay on the compose bridge and HAProxy dials them via host loopback +(`127.0.0.1`). + +Trade-off: HAProxy occupies the host's `:443` and `:80` directly, so +nothing else on the host may listen on those ports. For a dedicated +mtg/SNI-router host that is the intended layout. + +Rootless Podman users binding the privileged ports `:80`/`:443` need +the host-side sysctl once (rootful Docker handles this implicitly): + +```bash +sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 +``` + ## Fronting loop (why `[domain-fronting]` is set explicitly) When mtg sees TLS that isn't valid Telegram (a probe or a browser diff --git a/contrib/sni-router/docker-compose.yml b/contrib/sni-router/docker-compose.yml index a365003b8..2d09b9f01 100644 --- a/contrib/sni-router/docker-compose.yml +++ b/contrib/sni-router/docker-compose.yml @@ -24,9 +24,12 @@ x-domain-env: &domain-env services: haproxy: image: haproxy:lts-alpine - ports: - - "443:443" - - "80:80" + # Host networking so HAProxy sees the real client source IP on + # inbound (bridge networking with published ports rewrites it to + # the bridge gateway under both Docker's userland-proxy and + # rootless Podman's slirp4netns/pasta). See "Why host networking" + # in README.md. + network_mode: host volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro,Z environment: @@ -35,15 +38,14 @@ services: - mtg - web restart: unless-stopped - sysctls: - - net.ipv4.ip_unprivileged_port_start=80 mtg: image: nineseconds/mtg:2 volumes: - ./mtg-config.toml:/config/config.toml:ro,Z - expose: - - "3128" + # Publish on host loopback so the host-mode HAProxy can reach it. + ports: + - "127.0.0.1:3128:3128" restart: unless-stopped extra_hosts: - "host.containers.internal:host-gateway" @@ -54,9 +56,12 @@ services: - ./Caddyfile:/etc/caddy/Caddyfile:ro,Z - caddy_data:/data - ./www:/srv:ro,Z - expose: - - "80" - - "8443" + # Publish on host loopback so the host-mode HAProxy can reach it. + # Caddy's HTTP listener is mapped off :80 (occupied by HAProxy) + # to :8080; haproxy.cfg dials it on 127.0.0.1:8080 for ACME. + ports: + - "127.0.0.1:8080:80" + - "127.0.0.1:8443:8443" environment: <<: *domain-env restart: unless-stopped diff --git a/contrib/sni-router/haproxy.cfg b/contrib/sni-router/haproxy.cfg index 14aba963b..ec8f2646d 100644 --- a/contrib/sni-router/haproxy.cfg +++ b/contrib/sni-router/haproxy.cfg @@ -50,14 +50,19 @@ 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 + # + # HAProxy runs in the host network namespace (see docker-compose.yml) + # and mtg publishes :3128 on host loopback, so we dial 127.0.0.1. + server mtg 127.0.0.1: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 + server web 127.0.0.1:8443 send-proxy-v2 backend web_acme mode http - server web web:80 + # Caddy's :80 is published on host 127.0.0.1:8080 (HAProxy occupies + # the host's :80 itself). + server web 127.0.0.1:8080