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