package middleware import ( "net" "net/http" "strings" "github.com/bnema/gordon/internal/adapters/in/http/httphelper" ) // cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges. // Cf-Connecting-Ip is only trusted when the immediate upstream matches these. // Source: https://www.cloudflare.com/ips/ var cloudflareNets = httphelper.ParseTrustedProxies([]string{ // IPv4 "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13", "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22", // IPv6 "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32", }) func normalizeIP(raw string) string { ip := strings.TrimSpace(raw) if ip == "" { return "" } if host, _, err := net.SplitHostPort(ip); err == nil { ip = host } parsed := net.ParseIP(ip) if parsed == nil { return "" } return parsed.String() } // GetClientIP extracts the client IP address from the request. // It only honors X-Forwarded-For and X-Real-IP headers when the request // originates from a trusted proxy. This prevents attackers from spoofing // their IP address when Gordon is exposed directly without a reverse proxy. func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string { // Extract the direct connection IP from RemoteAddr remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { remoteIP = r.RemoteAddr } if normalized := normalizeIP(remoteIP); normalized != "" { remoteIP = normalized } // Only honor proxy headers if the request comes from a trusted proxy if httphelper.IsTrustedProxy(remoteIP, trustedNets) { // Cf-Connecting-IP is set by Cloudflare to the true client IP. // Only trust it when the immediate upstream is a known Cloudflare IP, // not any generic trusted proxy (e.g. local LB/nginx), because a // non-Cloudflare proxy would blindly forward a spoofed header. if httphelper.IsTrustedProxy(remoteIP, cloudflareNets) { if cfIP := r.Header.Get("Cf-Connecting-Ip"); cfIP != "" { if ip := normalizeIP(cfIP); ip != "" { return ip } } } if ip := forwardedForClientIP(r, trustedNets); ip != "" { return ip } if ip := realIPHeader(r); ip != "" { return ip } } // Fall back to RemoteAddr (direct connection IP) return remoteIP } // forwardedForClientIP returns the right-most untrusted X-Forwarded-For hop. // SECURITY: Parse from right to left so a trusted proxy appending to an // attacker-supplied chain cannot be spoofed. All header lines are joined, since // some proxies add a new X-Forwarded-For line instead of appending to the last. func forwardedForClientIP(r *http.Request, trustedNets []*net.IPNet) string { values := r.Header.Values("X-Forwarded-For") if len(values) == 0 { return "" } parts := strings.Split(strings.Join(values, ","), ",") validChain := make([]string, 0, len(parts)) for _, part := range parts { if ip := normalizeIP(part); ip != "" { validChain = append(validChain, ip) } } for i := len(validChain) - 1; i >= 0; i-- { if !httphelper.IsTrustedProxy(validChain[i], trustedNets) { return validChain[i] } } if len(validChain) > 0 { return validChain[0] } return "" } // realIPHeader returns X-Real-IP only when it is set exactly once, since a // repeated header means one of the values may come from the client. func realIPHeader(r *http.Request) string { values := r.Header.Values("X-Real-IP") if len(values) != 1 { return "" } return normalizeIP(values[0]) } // IsTrustedCloudflarePeer reports whether the request's immediate peer is both // a trusted proxy and a published Cloudflare address. Only then does // Cf-Connecting-Ip carry a client IP set by Cloudflare rather than by the client. func IsTrustedCloudflarePeer(r *http.Request, trustedNets []*net.IPNet) bool { return httphelper.IsTrustedSource(r, trustedNets) && httphelper.IsTrustedSource(r, cloudflareNets) }