Files
spsays-bff/internal/middleware/realip.go
T
2026-06-18 23:52:17 +05:30

48 lines
1.7 KiB
Go

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 ""
}