Traefik running on Docker Swarm, X-Real-IP and X-Forward-For are incorrect

Hello everyone,

I have a traefik service running on Docker Swarm, and it seems like there is an issue regarding X-Real-IP and X-Forwarded-For headers

This is the output I am getting

"[INFO] X-Forwarded-For: , ip: 10.0.0.2, X-Real-IP:  10.0.0.2"

The domain.com directly points to the server where traefik is listening on via A record, so there is no LB's in front currently. The 10.0.0.2 is also a bit weird, the docker network is 10.0.1.0/24, and my VPC CIDR is set to 10.0.1.0/24 as well.

Any help is appreciated

> docker network inspect internal
[
    {
        "Name": "internal",
        "Id": "npnme4jlce9k58h02z9mf0zmj",
        "Created": "2025-01-15T07:22:28.526343178Z",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.0.1.0/24",
                    "Gateway": "10.0.1.1"
                }
            ]
        },

And this is my plugin code that is printing this

func (d *Decoder) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	d.setForwardedHeaders(req)
}

func (d *Decoder) setForwardedHeaders(req *http.Request) {
	ip := d.clientIP(req)

	d.logInfo(fmt.Sprintf(
		"X-Forwarded-For: %s, ip: %s, X-Real-IP: %s",
		req.Header.Get("X-Forwarded-For"),
		ip,
		req.Header.Get("X-Real-IP"),
	))

	if ip == "" {
		return
	}

	oldXFF := req.Header.Get("X-Forwarded-For")
	if oldXFF == "" {
		req.Header.Set("X-Forwarded-For", ip)
	}

	xRealIP := req.Header.Get("X-Real-IP")
	if xRealIP == "" {
		req.Header.Set("X-Real-IP", ip)
	}
}

func (d *Decoder) clientIP(req *http.Request) string {
	xff := req.Header.Get("X-Forwarded-For")
	if xff != "" {
		parts := strings.Split(xff, ",")
		ipStr := strings.TrimSpace(parts[0])
		if net.ParseIP(ipStr) != nil {
			return ipStr
		}
	}

	remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
	if err == nil && net.ParseIP(remoteIP) != nil {
		return remoteIP
	}

	return ""
}

My file provider

middleware:
    query-decoder:
      plugin:
        decoder:
          headers:
            X-Processed-By: "X"

    fps-forward-header:
      headers:
        customRequestHeaders:
          Host: 'remote_url'

  routers:
    fps-forward-router:
      rule: 'Host(`domain.com`) && PathPrefix(`/path`)'
      priority: 100
      entryPoints:
        - websecure
      service: fps-forward-service
      middlewares:
        - query-decoder
        - fps-forward-header
  services:
    fps-forward-service:
      loadBalancer:
        servers:
          - url: 'https://external_url'
        passHostHeader: true

Static config

api:
  insecure: true
  dashboard: true

experimental:
  localPlugins:
    decoder:
      moduleName: traefik/decoder

log:
  # level: INFO
  level: DEBUG
  format: json

entrypoints:
  web:
    address: :80
  websecure:
    address: :443
    http:
      tls:
        domains:
          - main: domain.com

providers:
  swarm:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
  file:
    filename: /etc/traefik/providers/file.prod.yml

My stack containing traefik

networks:
  internal:
    external: true

services:
  traefik:
    image: traefik/traefik:experimental-master
    command: --configFile=/etc/traefik/traefik.prod.yml
    deploy:
      placement:
        constraints: [node.role==manager]
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /config/traefik:/etc/traefik
      - /data/traefik/plugins:/plugins-local/src/seotraefik
    networks:
      - internal

Docker inspect

     "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "ccb10d1f8595e6ff955a45c6e3cc6361979cbe0c99780e3aeabcbc4ffe227185",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {
                "80/tcp": null
            },
            "SandboxKey": "/var/run/docker/netns/ccb10d1f8595",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "",
            "Gateway": "",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "",
            "IPPrefixLen": 0,
            "IPv6Gateway": "",
            "MacAddress": "",
            "Networks": {
                "ingress": {
                    "IPAMConfig": {
                        "IPv4Address": "10.0.0.106"
                    },
                    "Links": null,
                    "Aliases": [
                        "732f837a7313"
                    ],
                    "NetworkID": "kosrbb1k154jmn7smd2qj50l2",
                    "EndpointID": "e568eeb026d5e1ec8c5dc27c4725a239ecc7ae75e255540502e989891ffa1fed",
                    "Gateway": "",
                    "IPAddress": "10.0.0.106",
                    "IPPrefixLen": 24,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:0a:00:00:6a",
                    "DriverOpts": null
                },
                "internal": {
                    "IPAMConfig": {
                        "IPv4Address": "10.0.1.125"
                    },
                    "Links": null,
                    "Aliases": [
                        "732f837a7313"
                    ],
                    "NetworkID": "npnme4jlce9k58h02z9mf0zmj",
                    "EndpointID": "89914a4cd9814ebc393fea790452d67d66eb03ce054f0e28c724e74e77b9b36c",
                    "Gateway": "",
                    "IPAddress": "10.0.1.125",
                    "IPPrefixLen": 24,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:0a:00:01:7d",
                    "DriverOpts": null
                }
            }
        }
    }
]

Docker Swarm by default will create an ingress network, load balancing all incoming requests to the services, thereby changing the IP. You need to set the ports into host mode (full example):

    ports:
      # listen on host ports without ingress network
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
1 Like

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.