package services import ( "context" "net" "net/http" "strings" ) type clientIPContextKey struct{} func WithClientIP(r *http.Request, ip string) *http.Request { return r.WithContext(context.WithValue(r.Context(), clientIPContextKey{}, ip)) } func ClientIPFromContext(r *http.Request) (string, bool) { ip, ok := r.Context().Value(clientIPContextKey{}).(string) return ip, ok && ip != "" } // ClientIP resolves the effective client IP. When trustedProxies is empty, // forwarded headers are trusted for easy reverse-proxy/container defaults. func ClientIP(remoteAddr, forwardedFor, realIP string, trustedProxies []string) string { remoteIP := IPOnly(remoteAddr) if len(trustedProxies) == 0 || remoteTrusted(remoteIP, trustedProxies) { if ip := firstForwardedIP(forwardedFor); ip != "" { return IPOnly(ip) } if ip := strings.TrimSpace(realIP); ip != "" { return IPOnly(ip) } } return remoteIP } func IPOnly(remoteAddr string) string { host := strings.TrimSpace(remoteAddr) if splitHost, _, err := net.SplitHostPort(remoteAddr); err == nil { host = splitHost } return strings.Trim(host, "[]") } func IsProtectedProxyIP(ip string, trustedProxies []string) bool { parsed := net.ParseIP(IPOnly(ip)) if parsed == nil { return false } if parsed.IsLoopback() { return true } return remoteTrusted(parsed.String(), trustedProxies) } func ProtectedBanTarget(target string, trustedProxies []string) bool { normalized, err := NormalizeBanTarget(target) if err != nil { return false } if !strings.Contains(normalized, "/") { return IsProtectedProxyIP(normalized, trustedProxies) } _, targetNet, err := net.ParseCIDR(normalized) if err != nil { return false } if targetNet.Contains(net.ParseIP("127.0.0.1")) || targetNet.Contains(net.ParseIP("::1")) { return true } for _, trusted := range trustedProxies { trusted = strings.TrimSpace(trusted) if trusted == "" { continue } if strings.Contains(trusted, "/") { if _, trustedNet, err := net.ParseCIDR(trusted); err == nil && networksOverlap(targetNet, trustedNet) { return true } continue } if ip := net.ParseIP(IPOnly(trusted)); ip != nil && targetNet.Contains(ip) { return true } } return false } func firstForwardedIP(forwardedFor string) string { var fallback string for _, part := range strings.Split(forwardedFor, ",") { ip := IPOnly(part) if net.ParseIP(ip) == nil { continue } if fallback == "" { fallback = ip } if isExternalIP(ip) { return ip } } return fallback } func remoteTrusted(remoteIP string, trustedProxies []string) bool { parsed := net.ParseIP(remoteIP) if parsed == nil { return false } for _, trusted := range trustedProxies { trusted = strings.TrimSpace(trusted) if trusted == "" { continue } if strings.Contains(trusted, "/") { if _, network, err := net.ParseCIDR(trusted); err == nil && network.Contains(parsed) { return true } continue } if ip := net.ParseIP(trusted); ip != nil && ip.Equal(parsed) { return true } } return false } func isExternalIP(ip string) bool { parsed := net.ParseIP(IPOnly(ip)) return parsed != nil && !parsed.IsLoopback() && !parsed.IsPrivate() && !parsed.IsLinkLocalUnicast() && !parsed.IsLinkLocalMulticast() && !parsed.IsUnspecified() } func networksOverlap(a, b *net.IPNet) bool { return a.Contains(b.IP) || b.Contains(a.IP) }