package middleware import ( "net" "net/http" "strings" ) // RealIP rewrites r.RemoteAddr to the genuine client IP for a deployment where // a SINGLE trusted reverse proxy is the edge and nothing sits in front of it // (here: Dokploy's Traefik — DNS points straight at it). // // Such an edge APPENDS the TCP peer it actually saw to the right of // X-Forwarded-For, so the LAST entry is the real client and can't be spoofed: // a client may only prepend left-hand entries, which we ignore. This is the // opposite of chi's RealIP, which trusts the left-most X-Forwarded-For entry // (and X-Real-IP) and is therefore spoofable behind an appending proxy — // yielding the wrong GeoIP country and a forgeable rate-limit key. // // Downstream consumers (geoip.ClientIP, the rate limiter, the request logger) // all read r.RemoteAddr, so fixing it here fixes them all. When no forwarded // header is present (e.g. a direct hit on the container), RemoteAddr is left // untouched. func RealIP(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ip := clientIPFromEdge(r); ip != "" { r.RemoteAddr = ip } next.ServeHTTP(w, r) }) } func clientIPFromEdge(r *http.Request) string { // Right-most X-Forwarded-For entry = the IP the trusted edge observed. if xff := r.Header.Get("X-Forwarded-For"); xff != "" { parts := strings.Split(xff, ",") last := strings.TrimSpace(parts[len(parts)-1]) if net.ParseIP(last) != nil { return last } } // Fallback: the edge also overwrites X-Real-IP from the connection. if xr := strings.TrimSpace(r.Header.Get("X-Real-Ip")); net.ParseIP(xr) != nil { return xr } return "" }