package server import ( "net" "net/http" "strings" "github.com/gin-gonic/gin" ) func clientIP(ctx *gin.Context) string { if ctx == nil || ctx.Request == nil { return "" } remoteIP := remoteAddrIP(ctx.Request) // Only trust forwarding headers when remote hop looks like local/internal proxy. if isPrivateOrLoopback(remoteIP) { for _, candidate := range headerIPs(ctx.Request.Header) { if isPublicIP(candidate) { return candidate } } candidates := headerIPs(ctx.Request.Header) if len(candidates) > 0 { return candidates[0] } } return remoteIP } func headerIPs(header http.Header) []string { keys := []string{ "X-Forwarded-For", "X-Real-Ip", "CF-Connecting-IP", "X-Envoy-External-Address", "Fly-Client-IP", } out := make([]string, 0, 4) seen := map[string]bool{} for _, key := range keys { raw := strings.TrimSpace(header.Get(key)) if raw == "" { continue } for _, part := range strings.Split(raw, ",") { ip := normalizeIP(strings.TrimSpace(part)) if ip == "" || seen[ip] { continue } seen[ip] = true out = append(out, ip) } } return out } func remoteAddrIP(request *http.Request) string { host, _, err := net.SplitHostPort(strings.TrimSpace(request.RemoteAddr)) if err != nil { return normalizeIP(strings.TrimSpace(request.RemoteAddr)) } return normalizeIP(host) } func normalizeIP(raw string) string { ip := net.ParseIP(strings.TrimSpace(raw)) if ip == nil { return "" } return ip.String() } func isPublicIP(value string) bool { ip := net.ParseIP(value) if ip == nil || !ip.IsGlobalUnicast() { return false } return !isPrivateOrLoopback(value) } func isPrivateOrLoopback(value string) bool { ip := net.ParseIP(value) if ip == nil { return true } return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() }