Skip to content

contrib/sni-router: use host networking for HAProxy to preserve client IPs#520

Closed
dolonet wants to merge 1 commit into
masterfrom
contrib/sni-router-host-mode
Closed

contrib/sni-router: use host networking for HAProxy to preserve client IPs#520
dolonet wants to merge 1 commit into
masterfrom
contrib/sni-router-host-mode

Conversation

@dolonet
Copy link
Copy Markdown
Collaborator

@dolonet dolonet commented May 18, 2026

Fixes #498.

Problem

With the current compose, HAProxy runs on the compose bridge and the host's :443/:80 are published via ports:. Docker's userland proxy (default) and rootless Podman's slirp4netns/pasta both rewrite the source IP to the bridge gateway on the way in. HAProxy then stamps that gateway address (e.g. 172.28.0.1) into the PROXY v2 header it sends to mtg and Caddy, so neither backend ever logs the real client IP. Affects every runtime the example targets: rootful Docker (any userland-proxy setting), rootless Podman, and Docker on Fedora 29+.

Fix

Move HAProxy into the host network namespace (network_mode: host). It binds the host's :443/:80 directly, sees the real client IP, and forwards it via PROXY v2 to the backends. mtg and Caddy stay on the compose bridge and publish on host loopback; HAProxy dials them at 127.0.0.1. Caddy's proxy_protocol allow is tightened to loopback only.

Trade-off

HAProxy now occupies the host's :443 and :80; nothing else on the host may listen on those ports. For a dedicated mtg/SNI-router host that is the intended layout. README has a new "Why host networking" section documenting this and the rootless-Podman sysctl prerequisite.

Notes

  • The sysctls: net.ipv4.ip_unprivileged_port_start=80 line is removed: it is incompatible with network_mode: host (Docker refuses to apply namespaced sysctls when the netns is shared with the host). The equivalent host-side setting is documented in README for rootless Podman users.
  • Image is left at nineseconds/mtg:2; compose: fix non-functional 'host' option #514 handles the bump independently. The host = "web" fronting target already on master needs an mtg image carrying config: accept hostname for [domain-fronting] target #480 to work — same prerequisite as today, unchanged by this PR.
  • Confirmed end-to-end (real public client IPs in mtg/Caddy logs) by @bam80 in Can't see real client IPs passed with PROXY protocol v2 #498 on his Fedora/Docker and rootless Podman setups. Locally validated haproxy -c, caddy validate, and YAML parse on the final files.

…t IPs

Move HAProxy into the host network namespace so it sees the real
client source IP on inbound connections.  With bridge networking +
published ports the source IP is rewritten to the bridge gateway by
the runtime (Docker's userland-proxy, rootless Podman's slirp4netns
or pasta), and the PROXY v2 header HAProxy then sends to mtg and
Caddy carries that useless address.

mtg and Caddy stay on the compose bridge and publish their ports on
host loopback; the host-mode HAProxy dials them at 127.0.0.1.  Caddy's
proxy_protocol allow list is tightened to loopback only.

The 'sysctls: net.ipv4.ip_unprivileged_port_start=80' line is removed
because Docker refuses to apply namespaced sysctls when the netns is
shared with the host.  Rootless Podman users binding the privileged
ports need the equivalent host-side sysctl once; this is documented
in README.md.

Fixes #498.
@dolonet
Copy link
Copy Markdown
Collaborator Author

dolonet commented May 18, 2026

Superseded by #522, which is a strict superset: adds explicit IPv6 binds on HAProxy frontends (bind [::]:443 v6only alongside bind 0.0.0.0:443) and tightens Caddy's PROXY allow list with 127.0.0.0/8 for the new loopback hop from host-mode HAProxy. Closing in favour of #522.

@dolonet dolonet closed this May 18, 2026
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.

Can't see real client IPs passed with PROXY protocol v2

1 participant