package utils import ( "net" "strings" ) // ResolveClientIP selects the best client IP from proxy headers and the remote address. // It prefers a public IP from X-Forwarded-For, then X-Real-IP, and finally the remote IP. func ResolveClientIP(forwardedFor, realIP, remoteIP string) string { forwardedCandidates := splitClientIPs(forwardedFor) if ip := firstPublicIP(forwardedCandidates); ip != "" { return ip } if ip := normalizeIP(realIP); ip != "" && !IsPrivateOrReservedIP(ip) { return ip } if ip := normalizeIP(remoteIP); ip != "" && !IsPrivateOrReservedIP(ip) { return ip } if len(forwardedCandidates) > 0 { return forwardedCandidates[0] } if ip := normalizeIP(realIP); ip != "" { return ip } return normalizeIP(remoteIP) } // IsPrivateOrReservedIP reports whether the IP is private or from a non-public network range. func IsPrivateOrReservedIP(raw string) bool { ip := net.ParseIP(strings.TrimSpace(raw)) if ip == nil { return false } if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { return true } for _, cidr := range []string{ "100.64.0.0/10", "fc00::/7", } { _, network, err := net.ParseCIDR(cidr) if err == nil && network.Contains(ip) { return true } } return false } func splitClientIPs(forwardedFor string) []string { if strings.TrimSpace(forwardedFor) == "" { return nil } parts := strings.Split(forwardedFor, ",") result := make([]string, 0, len(parts)) for _, part := range parts { if ip := normalizeIP(part); ip != "" { result = append(result, ip) } } return result } func firstPublicIP(candidates []string) string { for _, candidate := range candidates { if !IsPrivateOrReservedIP(candidate) { return candidate } } return "" } func normalizeIP(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if host, _, err := net.SplitHostPort(raw); err == nil { raw = host } ip := net.ParseIP(raw) if ip == nil { return "" } return ip.String() }