1package middleware
2
3import (
4 "net"
5 "net/http"
6 "strings"
7)
8
9// cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.
10// Cf-Connecting-Ip is only trusted when the immediate upstream matches these.
11// Source: https://www.cloudflare.com/ips/
12var cloudflareNets = ParseTrustedProxies([]string{
13 // IPv4
14 "173.245.48.0/20",
15 "103.21.244.0/22",
16 "103.22.200.0/22",
17 "103.31.4.0/22",
18 "141.101.64.0/18",
19 "108.162.192.0/18",
20 "190.93.240.0/20",
21 "188.114.96.0/20",
22 "197.234.240.0/22",
23 "198.41.128.0/17",
24 "162.158.0.0/15",
25 "104.16.0.0/13",
26 "104.24.0.0/14",
27 "172.64.0.0/13",
28 "131.0.72.0/22",
29 // IPv6
30 "2400:cb00::/32",
31 "2606:4700::/32",
32 "2803:f800::/32",
33 "2405:b500::/32",
34 "2405:8100::/32",
35 "2a06:98c0::/29",
36 "2c0f:f248::/32",
37})
38
39func normalizeIP(raw string) string {
40 ip := strings.TrimSpace(raw)
41 if ip == "" {
42 return ""
43 }
44
45 if host, _, err := net.SplitHostPort(ip); err == nil {
46 ip = host
47 }
48
49 parsed := net.ParseIP(ip)
50 if parsed == nil {
51 return ""
52 }
53
54 return parsed.String()
55}
56
57// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.
58// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
59// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
60func ParseTrustedProxies(proxies []string) []*net.IPNet {
61 var nets []*net.IPNet
62 for _, proxy := range proxies {
63 // Try parsing as CIDR
64 _, ipNet, err := net.ParseCIDR(proxy)
65 if err == nil {
66 nets = append(nets, ipNet)
67 continue
68 }
69 // Try parsing as single IP
70 ip := net.ParseIP(proxy)
71 if ip != nil {
72 // Convert single IP to /32 or /128 CIDR
73 bits := 32
74 if ip.To4() == nil {
75 bits = 128
76 }
77 nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
78 }
79 }
80 return nets
81}
82
83// IsTrustedProxy checks if the given IP address is from a trusted proxy.
84// Returns false if trustedNets is empty or the IP cannot be parsed.
85func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
86 if len(trustedNets) == 0 {
87 return false
88 }
89 parsedIP := net.ParseIP(ip)
90 if parsedIP == nil {
91 return false
92 }
93 for _, ipNet := range trustedNets {
94 if ipNet.Contains(parsedIP) {
95 return true
96 }
97 }
98 return false
99}
100
101// GetClientIP extracts the client IP address from the request.
102// It only honors X-Forwarded-For and X-Real-IP headers when the request
103// originates from a trusted proxy. This prevents attackers from spoofing
104// their IP address when Gordon is exposed directly without a reverse proxy.
105func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string {
106 // Extract the direct connection IP from RemoteAddr
107 remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
108 if err != nil {
109 remoteIP = r.RemoteAddr
110 }
111 if normalized := normalizeIP(remoteIP); normalized != "" {
112 remoteIP = normalized
113 }
114
115 // Only honor proxy headers if the request comes from a trusted proxy
116 if IsTrustedProxy(remoteIP, trustedNets) {
117 // Cf-Connecting-IP is set by Cloudflare to the true client IP.
118 // Only trust it when the immediate upstream is a known Cloudflare IP,
119 // not any generic trusted proxy (e.g. local LB/nginx), because a
120 // non-Cloudflare proxy would blindly forward a spoofed header.
121 if IsTrustedProxy(remoteIP, cloudflareNets) {
122 if cfIP := r.Header.Get("Cf-Connecting-Ip"); cfIP != "" {
123 if ip := normalizeIP(cfIP); ip != "" {
124 return ip
125 }
126 }
127 }
128
129 // Check X-Forwarded-For for proxied requests.
130 // SECURITY: Parse from right to left and return the first untrusted IP.
131 // This prevents spoofing when a trusted proxy appends to an attacker-
132 // supplied XFF chain.
133 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
134 parts := strings.Split(xff, ",")
135 validChain := make([]string, 0, len(parts))
136 for _, part := range parts {
137 if ip := normalizeIP(part); ip != "" {
138 validChain = append(validChain, ip)
139 }
140 }
141
142 for i := len(validChain) - 1; i >= 0; i-- {
143 if !IsTrustedProxy(validChain[i], trustedNets) {
144 return validChain[i]
145 }
146 }
147
148 if len(validChain) > 0 {
149 return validChain[0]
150 }
151 }
152
153 // Check X-Real-IP
154 if xri := r.Header.Get("X-Real-IP"); xri != "" {
155 if ip := normalizeIP(xri); ip != "" {
156 return ip
157 }
158 }
159 }
160
161 // Fall back to RemoteAddr (direct connection IP)
162 return remoteIP
163}