1package middleware
2
3import (
4 "net"
5 "net/http"
6 "strings"
7
8 "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
9)
10
11// cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.
12// Cf-Connecting-Ip is only trusted when the immediate upstream matches these.
13// Source: https://www.cloudflare.com/ips/
14var cloudflareNets = httphelper.ParseTrustedProxies([]string{
15 // IPv4
16 "173.245.48.0/20",
17 "103.21.244.0/22",
18 "103.22.200.0/22",
19 "103.31.4.0/22",
20 "141.101.64.0/18",
21 "108.162.192.0/18",
22 "190.93.240.0/20",
23 "188.114.96.0/20",
24 "197.234.240.0/22",
25 "198.41.128.0/17",
26 "162.158.0.0/15",
27 "104.16.0.0/13",
28 "104.24.0.0/14",
29 "172.64.0.0/13",
30 "131.0.72.0/22",
31 // IPv6
32 "2400:cb00::/32",
33 "2606:4700::/32",
34 "2803:f800::/32",
35 "2405:b500::/32",
36 "2405:8100::/32",
37 "2a06:98c0::/29",
38 "2c0f:f248::/32",
39})
40
41func normalizeIP(raw string) string {
42 ip := strings.TrimSpace(raw)
43 if ip == "" {
44 return ""
45 }
46
47 if host, _, err := net.SplitHostPort(ip); err == nil {
48 ip = host
49 }
50
51 parsed := net.ParseIP(ip)
52 if parsed == nil {
53 return ""
54 }
55
56 return parsed.String()
57}
58
59// GetClientIP extracts the client IP address from the request.
60// It only honors X-Forwarded-For and X-Real-IP headers when the request
61// originates from a trusted proxy. This prevents attackers from spoofing
62// their IP address when Gordon is exposed directly without a reverse proxy.
63func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string {
64 // Extract the direct connection IP from RemoteAddr
65 remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
66 if err != nil {
67 remoteIP = r.RemoteAddr
68 }
69 if normalized := normalizeIP(remoteIP); normalized != "" {
70 remoteIP = normalized
71 }
72
73 // Only honor proxy headers if the request comes from a trusted proxy
74 if httphelper.IsTrustedProxy(remoteIP, trustedNets) {
75 // Cf-Connecting-IP is set by Cloudflare to the true client IP.
76 // Only trust it when the immediate upstream is a known Cloudflare IP,
77 // not any generic trusted proxy (e.g. local LB/nginx), because a
78 // non-Cloudflare proxy would blindly forward a spoofed header.
79 if httphelper.IsTrustedProxy(remoteIP, cloudflareNets) {
80 if cfIP := r.Header.Get("Cf-Connecting-Ip"); cfIP != "" {
81 if ip := normalizeIP(cfIP); ip != "" {
82 return ip
83 }
84 }
85 }
86
87 // Check X-Forwarded-For for proxied requests.
88 // SECURITY: Parse from right to left and return the first untrusted IP.
89 // This prevents spoofing when a trusted proxy appends to an attacker-
90 // supplied XFF chain.
91 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
92 parts := strings.Split(xff, ",")
93 validChain := make([]string, 0, len(parts))
94 for _, part := range parts {
95 if ip := normalizeIP(part); ip != "" {
96 validChain = append(validChain, ip)
97 }
98 }
99
100 for i := len(validChain) - 1; i >= 0; i-- {
101 if !httphelper.IsTrustedProxy(validChain[i], trustedNets) {
102 return validChain[i]
103 }
104 }
105
106 if len(validChain) > 0 {
107 return validChain[0]
108 }
109 }
110
111 // Check X-Real-IP
112 if xri := r.Header.Get("X-Real-IP"); xri != "" {
113 if ip := normalizeIP(xri); ip != "" {
114 return ip
115 }
116 }
117 }
118
119 // Fall back to RemoteAddr (direct connection IP)
120 return remoteIP
121}