From d52530c52cd6b0c87e61cd8f02640a3d11364a5d Mon Sep 17 00:00:00 2001 From: Krispire Admin Date: Thu, 18 Jun 2026 23:52:17 +0530 Subject: [PATCH] fix: user location fix --- internal/middleware/realip.go | 47 +++++++++++++++++++++++++++++++++++ internal/server/server.go | 5 +++- pkg/geoip/geoip.go | 6 ++--- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 internal/middleware/realip.go diff --git a/internal/middleware/realip.go b/internal/middleware/realip.go new file mode 100644 index 0000000..f78389f --- /dev/null +++ b/internal/middleware/realip.go @@ -0,0 +1,47 @@ +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 "" +} diff --git a/internal/server/server.go b/internal/server/server.go index a2e2cca..19f5450 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -407,7 +407,10 @@ func (s *Server) setupRouter() { // Global middleware r.Use(chiMiddleware.RequestID) - r.Use(chiMiddleware.RealIP) + // Our RealIP (not chi's): a single trusted edge proxy (Dokploy's Traefik) + // appends the real client to the right of X-Forwarded-For, so we read the + // right-most entry. chi's RealIP trusts the spoofable left-most entry. + r.Use(middleware.RealIP) r.Use(middleware.Logger(s.logger)) r.Use(chiMiddleware.Recoverer) r.Use(chiMiddleware.Compress(5)) diff --git a/pkg/geoip/geoip.go b/pkg/geoip/geoip.go index 00261b1..7d311aa 100644 --- a/pkg/geoip/geoip.go +++ b/pkg/geoip/geoip.go @@ -32,9 +32,9 @@ func CountryFromCloudflare(r *http.Request) string { return cc } -// ClientIP returns the best guess at the caller's IP. The chi RealIP -// middleware already rewrites r.RemoteAddr from X-Forwarded-For / -// X-Real-IP, so we just strip the port. +// ClientIP returns the best guess at the caller's IP. The middleware.RealIP +// middleware already rewrites r.RemoteAddr to the real client (right-most +// X-Forwarded-For entry from the trusted edge proxy), so we just strip any port. func ClientIP(r *http.Request) string { host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) if err != nil {