package httpx import ( "net/http" "net/netip" "strings" ) // clientInfo is the resolved view of the requester after applying the // trusted-proxy policy. It feeds rate limiting, sessions, and the audit log. type clientInfo struct { IP netip.Addr Proto string // effective scheme: "http" or "https" Host string // effective host header } func parsePeer(remoteAddr string) netip.Addr { if ap, err := netip.ParseAddrPort(remoteAddr); err == nil { return ap.Addr().Unmap() } if a, err := netip.ParseAddr(remoteAddr); err == nil { return a.Unmap() } return netip.Addr{} } func isTrusted(a netip.Addr, trusted []netip.Prefix) bool { if !a.IsValid() { return false } for _, p := range trusted { if p.Contains(a) { return true } } return false } // resolveClient applies ยง12: X-Forwarded-For / X-Forwarded-Proto / // X-Forwarded-Host are honored only when the direct peer is inside // TRUSTED_PROXY_CIDRS; otherwise the headers are ignored entirely. // // The client IP is found by walking the X-Forwarded-For chain from the // rightmost entry (appended by our own proxy) towards the left, skipping // addresses that are themselves trusted proxies; the first untrusted address // is the real client. A malformed entry stops the walk (everything further // left is attacker-controllable). func resolveClient(r *http.Request, trusted []netip.Prefix, defaultProto string) clientInfo { peer := parsePeer(r.RemoteAddr) info := clientInfo{IP: peer, Proto: defaultProto, Host: r.Host} if len(trusted) == 0 || !isTrusted(peer, trusted) { return info } var chain []string for _, h := range r.Header.Values("X-Forwarded-For") { for part := range strings.SplitSeq(h, ",") { if part = strings.TrimSpace(part); part != "" { chain = append(chain, part) } } } for i := len(chain) - 1; i >= 0; i-- { a := parsePeer(chain[i]) if !a.IsValid() { break } info.IP = a if !isTrusted(a, trusted) { break } // a is itself a trusted proxy: keep walking left. If the whole chain // is trusted, the leftmost entry stands as the best available answer. } switch p := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))); p { case "http", "https": info.Proto = p } if h := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); h != "" { info.Host = h } return info }