clientip.go RawHistoryFolder
1package middleware
2
3import (
4 "net"
5 "net/http"
6 "strings"
7)
8
9func normalizeIP(raw string) string {
10 ip := strings.TrimSpace(raw)
11 if ip == "" {
12 return ""
13 }
14
15 if host, _, err := net.SplitHostPort(ip); err == nil {
16 ip = host
17 }
18
19 parsed := net.ParseIP(strings.TrimSpace(ip))
20 if parsed == nil {
21 return ""
22 }
23
24 return parsed.String()
25}
26
27// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.
28// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
29// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
30func ParseTrustedProxies(proxies []string) []*net.IPNet {
31 var nets []*net.IPNet
32 for _, proxy := range proxies {
33 // Try parsing as CIDR
34 _, ipNet, err := net.ParseCIDR(proxy)
35 if err == nil {
36 nets = append(nets, ipNet)
37 continue
38 }
39 // Try parsing as single IP
40 ip := net.ParseIP(proxy)
41 if ip != nil {
42 // Convert single IP to /32 or /128 CIDR
43 bits := 32
44 if ip.To4() == nil {
45 bits = 128
46 }
47 nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
48 }
49 }
50 return nets
51}
52
53// IsTrustedProxy checks if the given IP address is from a trusted proxy.
54// Returns false if trustedNets is empty or the IP cannot be parsed.
55func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
56 if len(trustedNets) == 0 {
57 return false
58 }
59 parsedIP := net.ParseIP(ip)
60 if parsedIP == nil {
61 return false
62 }
63 for _, ipNet := range trustedNets {
64 if ipNet.Contains(parsedIP) {
65 return true
66 }
67 }
68 return false
69}
70
71// GetClientIP extracts the client IP address from the request.
72// It only honors X-Forwarded-For and X-Real-IP headers when the request
73// originates from a trusted proxy. This prevents attackers from spoofing
74// their IP address when Gordon is exposed directly without a reverse proxy.
75func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string {
76 // Extract the direct connection IP from RemoteAddr
77 remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
78 if err != nil {
79 remoteIP = r.RemoteAddr
80 }
81 if normalized := normalizeIP(remoteIP); normalized != "" {
82 remoteIP = normalized
83 }
84
85 // Only honor proxy headers if the request comes from a trusted proxy
86 if IsTrustedProxy(remoteIP, trustedNets) {
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 !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}