Client IP Preservation¶
Apps behind multiple reverse proxies see the proxy's IP instead of the real client IP unless each layer correctly passes and trusts X-Forwarded-For.
Proxy Chain¶
All external and LAN traffic passes through two proxy hops before reaching a Kubernetes app:
graph LR
External["External client\n(cellular / internet)"]
LAN["LAN client"]
Router["Router\nport forward :443"]
Traefik["Traefik\nmacvlan :443"]
Gateway["Cilium Gateway API"]
App["App pod\n(Jellyfin, Emby, …)"]
External --> Router --> Traefik
LAN -->|"DNS → macvlan IP"| Traefik
Traefik -->|"X-Forwarded-For: <real-ip>"| Gateway --> App
-
Layer 1 — Router
Forwards external port
443→ Traefik's macvlan IP on port443. Connects directly to the macvlan interface — not the NAS host IP. -
Layer 2 — Traefik (NAS)
Receives real client IP via macvlan. Appends it to
X-Forwarded-Forand proxies to the K8s Gateway. -
Layer 3 — Cilium Gateway API
Envoy appends its pod IP to
X-Forwarded-Forand forwards to the app pod. -
App
Must trust upstream proxies to read
X-Forwarded-Forinstead of using the TCP source IP.
Why the Macvlan IP Matters¶
Traefik runs in Docker with two network interfaces:
| Interface | IP | Purpose |
|---|---|---|
pihole-macvlan |
static macvlan IP | Direct LAN presence — bypasses docker-proxy |
pihole-bridge |
bridge gateway IP | Internal Docker communication |
Docker's userland proxy (docker-proxy) is used for published ports (-p 9443:443 on the NAS host IP). It creates a new TCP connection, losing the original source IP. Traffic routed directly to the macvlan IP never touches docker-proxy — the kernel routes it straight to the container.
Router must forward to the macvlan IP
If the router forwards to the NAS host IP on a published port, traffic goes through docker-proxy and the real client IP is lost. The forward must target the macvlan IP on port 443 directly.
Router Port Forwarding and the MAC Address Requirement¶
Most consumer routers restrict port forwarding targets to either:
- A DHCP-assigned IP — selected from a lease table populated by the router's DHCP server
- A custom IP + MAC address pair — manually entered, so the router can resolve the ARP entry
Traefik's macvlan IP is outside the router's DHCP range and never appears in the lease table, so it can only be entered as a custom target — which requires a MAC address.
Docker assigns a random MAC to each container network interface on every restart. To get a stable, known MAC, it must be pinned in the compose file.
Pinning the MAC Address¶
The correct syntax depends on the Docker API version:
Set mac_address per network interface under networks::
The per-network mac_address requires API 1.44. Use the service-level field instead:
services:
traefik:
mac_address: "<chosen-mac>"
networks:
pihole-macvlan:
ipv4_address: <macvlan-ip>
Note
The service-level mac_address field is deprecated in Docker Engine ≥ v25.0, but works on the API 1.43 daemon shipped with Synology Container Manager.
Pick a MAC in the locally-administered unicast range (02:xx:xx:xx:xx:xx). A useful convention is to encode the IP octets in the last bytes so the MAC is self-documenting.
Approaches That Don't Work on This Setup¶
Two obvious alternatives were tried and ruled out:
Setting "userland-proxy": false in /var/packages/ContainerManager/etc/dockerd.json makes Docker use iptables DNAT instead of docker-proxy, which preserves source IPs for published ports.
Why it breaks: Synology DSM manages its own iptables rules. Docker's DNAT rules conflict with DSM's firewall, causing published port mappings to stop working entirely after a Container Manager restart.
Running Traefik with network_mode: host gives it direct access to the host network stack, also bypassing docker-proxy.
Why it breaks: Synology DSM's built-in reverse proxy already occupies ports 80 and 443 on the host. With host networking, Traefik must listen on alternate ports (e.g. 8001, 9443), but local DNS resolves *.hdhomelab.com to Traefik's macvlan IP on port 443. LAN clients can no longer reach Traefik on the expected port.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| App logs show the Docker bridge gateway IP | Router forwards to NAS host IP; docker-proxy SNAT | Change router forward to macvlan IP on port 443 |
App logs show a pod-range IP (10.x.x.x) |
Pod CIDR not in known proxies | Add the pod CIDR to known proxies |
| App logs show a K8s node IP | K8s node network not trusted | Add the K8s node CIDR to known proxies |
| App logs show Traefik's macvlan IP | Traefik macvlan IP not trusted | Add the macvlan subnet to known proxies |
| Correct IP for cellular, wrong for LAN (or vice versa) | Partial proxy trust | Ensure all proxy CIDRs are in known proxies |