1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/utils/client_ip.go

88 lines
2.0 KiB
Go

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()
}