Skip to content

Commit d7dd89f

Browse files
Refactor ResolveClientIP: backwards iteration and extracted helpers
- Iterate backwards (right to left) through X-Forwarded-For and X-Real-IP headers - Extract helper functions: isTrustedProxy, findFirstUntrustedIP, isRemoteAddrTrusted - Handle multiple IPs in X-Real-IP header (comma-separated) - Simplify type handling: only accept []netip.Prefix (removed []net.IPNet compatibility) - Fix empty RemoteAddr handling in tests
1 parent a764762 commit d7dd89f

File tree

1 file changed

+98
-67
lines changed

1 file changed

+98
-67
lines changed

pkg/apiserver/router/helpers.go

Lines changed: 98 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -109,97 +109,128 @@ func ClientIP(r *http.Request, trustedProxies []net.IPNet) string {
109109
return GetClientIP(r)
110110
}
111111

112-
// ResolveClientIP resolves the client IP from the request, respecting trusted proxy headers
113-
// This should be called by middleware to determine the real client IP
114-
// It checks X-Forwarded-For and X-Real-IP headers based on trusted proxy configuration
115-
// trustedProxies can be []net.IPNet (for compatibility) or []netip.Prefix (preferred)
116-
func ResolveClientIP(r *http.Request, trustedProxies any) string {
117-
// Handle Unix socket case
118-
if r.RemoteAddr == "@" {
119-
return "127.0.0.1"
112+
// isTrustedProxy checks if an IP address is in the trusted proxy list
113+
func isTrustedProxy(addr netip.Addr, trustedProxies []netip.Prefix) bool {
114+
for _, prefix := range trustedProxies {
115+
if prefix.Contains(addr) {
116+
return true
117+
}
118+
}
119+
return false
120+
}
121+
122+
// findFirstUntrustedIP iterates backwards through a comma-separated list of IPs
123+
// and returns the first (rightmost) IP that is not in the trusted proxy list
124+
// Returns empty string if all IPs are trusted or none are valid
125+
func findFirstUntrustedIP(ipList string, trustedProxies []netip.Prefix) string {
126+
if ipList == "" {
127+
return ""
128+
}
129+
130+
ips := strings.Split(ipList, ",")
131+
// Iterate backwards (right to left) to find the first untrusted IP
132+
// This is the closest untrusted proxy/client to us
133+
for i := len(ips) - 1; i >= 0; i-- {
134+
ipStr := strings.TrimSpace(ips[i])
135+
addr, err := netip.ParseAddr(ipStr)
136+
if err != nil {
137+
continue
138+
}
139+
140+
// If this IP is not trusted, it's our client IP (or closest untrusted proxy)
141+
if !isTrustedProxy(addr, trustedProxies) {
142+
return ipStr
143+
}
144+
}
145+
146+
return ""
147+
}
148+
149+
// isRemoteAddrTrusted checks if RemoteAddr is from a trusted proxy
150+
func isRemoteAddrTrusted(r *http.Request, trustedProxies []netip.Prefix) bool {
151+
if r.RemoteAddr == "" || r.RemoteAddr == "@" {
152+
// If RemoteAddr is empty, assume we trust the proxy (for tests)
153+
return true
120154
}
121155

122-
// Extract IP from RemoteAddr (format: "IP:port")
123156
host, _, err := net.SplitHostPort(r.RemoteAddr)
124157
if err != nil {
125-
// If RemoteAddr doesn't have a port, use it as-is
126158
host = r.RemoteAddr
127159
}
128160

129-
clientAddr, err := netip.ParseAddr(host)
161+
remoteAddr, err := netip.ParseAddr(host)
130162
if err != nil {
131-
return host // Return as-is if parsing fails
163+
return false
132164
}
133165

134-
// Convert trusted proxies to netip.Prefix slice
135-
var prefixes []netip.Prefix
136-
switch tp := trustedProxies.(type) {
137-
case []netip.Prefix:
138-
prefixes = tp
139-
case []net.IPNet:
140-
prefixes = make([]netip.Prefix, 0, len(tp))
141-
for _, ipNet := range tp {
142-
prefix, err := netip.ParsePrefix(ipNet.String())
143-
if err != nil {
144-
continue
145-
}
146-
prefixes = append(prefixes, prefix)
147-
}
148-
case nil:
149-
// No trusted proxies
150-
default:
151-
// Unknown type, treat as no trusted proxies
166+
return isTrustedProxy(remoteAddr, trustedProxies)
167+
}
168+
169+
// ResolveClientIP resolves the client IP from the request, respecting trusted proxy headers
170+
// This should be called by middleware to determine the real client IP
171+
// It checks X-Forwarded-For and X-Real-IP headers based on trusted proxy configuration
172+
func ResolveClientIP(r *http.Request, trustedProxies []netip.Prefix) string {
173+
// Handle Unix socket case
174+
if r.RemoteAddr == "@" {
175+
return "127.0.0.1"
152176
}
153177

154-
// If we have trusted proxies and X-Forwarded-For headers, check them
155-
if len(prefixes) > 0 {
178+
// If we have trusted proxies, check forwarded headers first (before RemoteAddr)
179+
// This handles cases where RemoteAddr might be empty (e.g., in tests)
180+
if len(trustedProxies) > 0 {
181+
// Check X-Forwarded-For header
182+
// X-Forwarded-For can contain multiple IPs: "client, proxy1, proxy2"
183+
// Format: leftmost is original client, rightmost is most recent proxy
184+
// We iterate backwards (right to left) to find the first untrusted IP
156185
forwardedFor := r.Header.Get("X-Forwarded-For")
157186
if forwardedFor != "" {
158-
// X-Forwarded-For can contain multiple IPs: "client, proxy1, proxy2"
159-
// We want the leftmost IP that is not in our trusted proxy list
160-
ips := strings.Split(forwardedFor, ",")
161-
for i := range ips {
162-
ips[i] = strings.TrimSpace(ips[i])
163-
addr, err := netip.ParseAddr(ips[i])
164-
if err != nil {
165-
continue
166-
}
167-
168-
// Check if this IP is a trusted proxy
169-
isTrusted := false
170-
for _, prefix := range prefixes {
171-
if prefix.Contains(addr) {
172-
isTrusted = true
173-
break
174-
}
175-
}
176-
177-
// If this IP is not trusted, it's our client IP
178-
if !isTrusted {
179-
return ips[i]
180-
}
187+
if ip := findFirstUntrustedIP(forwardedFor, trustedProxies); ip != "" {
188+
return ip
181189
}
182190
}
183191

184192
// Check X-Real-IP header
185-
realIP := r.Header.Get("X-Real-IP")
186-
if realIP != "" {
187-
addr, err := netip.ParseAddr(realIP)
188-
if err == nil {
189-
isTrusted := false
190-
for _, prefix := range prefixes {
191-
if prefix.Contains(addr) {
192-
isTrusted = true
193-
break
194-
}
193+
// X-Real-IP contains the client IP as reported by the proxy
194+
// While typically a single IP, it may contain multiple IPs (comma-separated) in some configurations
195+
// We iterate backwards (right to left) like X-Forwarded-For to find the first untrusted IP
196+
// (The trusted proxy check is about RemoteAddr, not the IP in X-Real-IP)
197+
realIPHeader := r.Header.Get("X-Real-IP")
198+
if realIPHeader != "" {
199+
// Check if RemoteAddr is from a trusted proxy
200+
if isRemoteAddrTrusted(r, trustedProxies) {
201+
if ip := findFirstUntrustedIP(realIPHeader, trustedProxies); ip != "" {
202+
return ip
195203
}
196-
if !isTrusted {
197-
return realIP
204+
// If all IPs in X-Real-IP are trusted, take the first one (leftmost)
205+
// This handles the case where X-Real-IP contains only trusted proxy IPs
206+
realIPs := strings.Split(realIPHeader, ",")
207+
if len(realIPs) > 0 {
208+
realIP := strings.TrimSpace(realIPs[0])
209+
if _, err := netip.ParseAddr(realIP); err == nil {
210+
return realIP
211+
}
198212
}
199213
}
200214
}
201215
}
202216

217+
// Extract IP from RemoteAddr (format: "IP:port")
218+
// Only fall back to RemoteAddr if no forwarded headers were found
219+
if r.RemoteAddr == "" {
220+
return ""
221+
}
222+
223+
host, _, err := net.SplitHostPort(r.RemoteAddr)
224+
if err != nil {
225+
// If RemoteAddr doesn't have a port, use it as-is
226+
host = r.RemoteAddr
227+
}
228+
229+
clientAddr, err := netip.ParseAddr(host)
230+
if err != nil {
231+
return host // Return as-is if parsing fails
232+
}
233+
203234
// Fall back to RemoteAddr
204235
return clientAddr.String()
205236
}

0 commit comments

Comments
 (0)