Files
silo-server/internal/clientip/resolver.go
T

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