package api import ( "context" "net" "net/http" "strings" ) // contextKey for real IP. type contextKey string const ctxRealIP contextKey = "real_ip" // RealIP middleware extracts the client's real public IP. // Priority: X-Forwarded-For (from Traefik) > X-Real-IP > RemoteAddr. func RealIP(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ip := extractRealIP(r) if ip != "" { r = r.WithContext(context.WithValue(r.Context(), ctxRealIP, ip)) } next(w, r) } } // GetRealIP returns the client IP from context. func GetRealIP(r *http.Request) string { if ip, ok := r.Context().Value(ctxRealIP).(string); ok { return ip } return "" } func extractRealIP(r *http.Request) string { // 1. X-Forwarded-For (Traefik, nginx, etc.) if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Can contain multiple IPs: client, proxy1, proxy2 // First one is the original client parts := strings.Split(xff, ",") if len(parts) > 0 { ip := strings.TrimSpace(parts[0]) if isValidIP(ip) { return ip } } } // 2. X-Real-IP (some proxies use this) if xri := r.Header.Get("X-Real-IP"); xri != "" { ip := strings.TrimSpace(xri) if isValidIP(ip) { return ip } } // 3. RemoteAddr fallback (direct connection) host, _, err := net.SplitHostPort(r.RemoteAddr) if err == nil && isValidIP(host) { return host } return "" } func isValidIP(ip string) bool { // Accept both IPv4 and IPv6 parsed := net.ParseIP(ip) return parsed != nil }