package httpx import ( "fmt" "net" "net/http" "strings" ) // TrustedProxies is the parsed set of networks whose X-Forwarded-For is // honoured (A6). An empty set means "nothing is trusted": the peer address is // the client. type TrustedProxies []*net.IPNet // ParseTrustedProxies parses the `server.trusted_proxies` entries. A bare IP // ("127.0.0.1") becomes a single-host network (/32 or /128); a CIDR // ("10.0.0.0/8") is used verbatim. Empty entries are skipped. func ParseTrustedProxies(entries []string) (TrustedProxies, error) { var tp TrustedProxies for _, e := range entries { e = strings.TrimSpace(e) if e == "" { continue } if strings.Contains(e, "/") { _, n, err := net.ParseCIDR(e) if err != nil { return nil, fmt.Errorf("httpx: trusted proxy %q is not a valid CIDR: %w", e, err) } tp = append(tp, n) continue } ip := net.ParseIP(e) if ip == nil { return nil, fmt.Errorf("httpx: trusted proxy %q is not an IP or CIDR", e) } bits := 32 if ip.To4() == nil { bits = 128 } tp = append(tp, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)}) } return tp, nil } // Contains reports whether ip is one of the trusted proxies. func (t TrustedProxies) Contains(ip net.IP) bool { if ip == nil { return false } for _, n := range t { if n.Contains(ip) { return true } } return false } // ClientIP resolves the client address for r using the trusted proxies, and // SETS the request's X-Forwarded-For to that single value — it never appends to // a client-supplied chain (A6, SPEC-003 §4). // // The rules: // // - the immediate peer (RemoteAddr) is NOT trusted → any X-Forwarded-For is // ignored and the peer is the client (a client cannot spoof past the gate); // - the peer IS trusted → walk X-Forwarded-For left to right and return the // first address that is not itself a trusted proxy (the real client); if // every hop is trusted, the last hop is returned. // // The resolved value is written back with Header.Set (replace), never Add. func ClientIP(r *http.Request, trusted TrustedProxies) string { peer := remoteIP(r.RemoteAddr) ip := peer if trusted.Contains(peer) { ip = resolveForwarded(r.Header.Get("X-Forwarded-For"), trusted, peer) } if ip == nil { return "" } s := ip.String() r.Header.Set("X-Forwarded-For", s) // SET — never append (A6) return s } // resolveForwarded returns the leftmost non-trusted address in the header, // falling back to the last hop, then to the peer. func resolveForwarded(header string, trusted TrustedProxies, peer net.IP) net.IP { if strings.TrimSpace(header) == "" { return peer } var last net.IP for _, part := range strings.Split(header, ",") { ip := net.ParseIP(strings.TrimSpace(part)) if ip == nil { continue } last = ip if !trusted.Contains(ip) { return ip // the real client } } if last != nil { return last } return peer } // remoteIP extracts the IP from a "host:port" RemoteAddr. func remoteIP(addr string) net.IP { if addr == "" { return nil } host, _, err := net.SplitHostPort(addr) if err != nil { host = addr } return net.ParseIP(strings.Trim(strings.TrimSpace(host), "[]")) }