110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
package clientip
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Resolver resolves the real client IP from an HTTP request, accounting for
|
|
// trusted reverse proxies that set forwarding headers.
|
|
type Resolver struct {
|
|
mu sync.RWMutex
|
|
trusted []*net.IPNet
|
|
}
|
|
|
|
// NewResolver creates a Resolver with the given trusted proxy CIDRs.
|
|
// If trusted is nil or empty, forwarding headers are never consulted.
|
|
func NewResolver(trusted []*net.IPNet) *Resolver {
|
|
return &Resolver{trusted: trusted}
|
|
}
|
|
|
|
// ClientIP returns the resolved client IP address string.
|
|
// The returned value is always normalized via net.ParseIP().String().
|
|
func (r *Resolver) ClientIP(req *http.Request) string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
remoteIP := parseRemoteAddr(req.RemoteAddr)
|
|
|
|
// If the connecting IP is not a trusted proxy, ignore forwarding headers.
|
|
if !r.isTrusted(remoteIP) {
|
|
return normalize(remoteIP)
|
|
}
|
|
|
|
// Check X-Forwarded-For: walk right-to-left, return first untrusted IP.
|
|
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
|
ips := strings.Split(xff, ",")
|
|
for i := len(ips) - 1; i >= 0; i-- {
|
|
candidate := strings.TrimSpace(ips[i])
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
parsed := net.ParseIP(candidate)
|
|
if parsed == nil {
|
|
continue
|
|
}
|
|
if !r.isTrusted(parsed) {
|
|
return normalize(parsed)
|
|
}
|
|
}
|
|
// All IPs in chain are trusted — return leftmost
|
|
for _, raw := range ips {
|
|
candidate := strings.TrimSpace(raw)
|
|
if parsed := net.ParseIP(candidate); parsed != nil {
|
|
return normalize(parsed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: X-Real-IP
|
|
if realIP := req.Header.Get("X-Real-IP"); realIP != "" {
|
|
if parsed := net.ParseIP(realIP); parsed != nil {
|
|
return normalize(parsed)
|
|
}
|
|
}
|
|
|
|
return normalize(remoteIP)
|
|
}
|
|
|
|
// parseRemoteAddr extracts the IP from r.RemoteAddr using net.SplitHostPort.
|
|
// Handles IPv4, IPv6 (bracket notation), and bare IPs without port.
|
|
func parseRemoteAddr(addr string) net.IP {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
// No port — try parsing as bare IP
|
|
host = addr
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return net.IPv4zero
|
|
}
|
|
return ip
|
|
}
|
|
|
|
func (r *Resolver) isTrusted(ip net.IP) bool {
|
|
for _, cidr := range r.trusted {
|
|
if cidr.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// normalize returns the canonical string form of an IP.
|
|
func normalize(ip net.IP) string {
|
|
if ip == nil {
|
|
return "0.0.0.0"
|
|
}
|
|
return ip.String()
|
|
}
|
|
|
|
// UpdateTrustedCIDRs updates the Resolver's trusted proxy list in place.
|
|
// Safe to call concurrently with ClientIP — protected by RWMutex.
|
|
func (r *Resolver) UpdateTrustedCIDRs(cidrs []*net.IPNet) {
|
|
r.mu.Lock()
|
|
r.trusted = cidrs
|
|
r.mu.Unlock()
|
|
}
|