package hscontrol import ( "fmt" "net" "net/http" "net/netip" realclientip "github.com/realclientip/realclientip-go" ) const ( headerTrueClientIP = "True-Client-IP" headerXRealIP = "X-Real-IP" headerXForwardedFor = "X-Forwarded-For" ) var proxyHeaders = [...]string{headerTrueClientIP, headerXRealIP, headerXForwardedFor} // trustedProxyRealIP rewrites r.RemoteAddr from proxy headers when the // peer is in trusted; for any other peer the headers are stripped so a // downstream handler cannot read a spoofed value. X-Forwarded-For uses // [realclientip.RightmostTrustedRangeStrategy] so prepending a value cannot win in a // proxy chain. func trustedProxyRealIP(trusted []netip.Prefix) (func(http.Handler) http.Handler, error) { ranges := make([]net.IPNet, 0, len(trusted)) for _, p := range trusted { ranges = append(ranges, prefixToIPNet(p)) } trueClientIP, err := realclientip.NewSingleIPHeaderStrategy(headerTrueClientIP) if err != nil { return nil, fmt.Errorf("%s strategy: %w", headerTrueClientIP, err) } xRealIP, err := realclientip.NewSingleIPHeaderStrategy(headerXRealIP) if err != nil { return nil, fmt.Errorf("%s strategy: %w", headerXRealIP, err) } xForwardedFor, err := realclientip.NewRightmostTrustedRangeStrategy(headerXForwardedFor, ranges) if err != nil { return nil, fmt.Errorf("%s strategy: %w", headerXForwardedFor, err) } strategy := realclientip.NewChainStrategy(trueClientIP, xRealIP, xForwardedFor) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !peerTrusted(r.RemoteAddr, trusted) { for _, h := range proxyHeaders { r.Header.Del(h) } next.ServeHTTP(w, r) return } // Proxy headers carry no port; write the IP alone so // `remote=` logs the resolved client, not the proxy's // ephemeral TCP port. if ip := strategy.ClientIP(r.Header, r.RemoteAddr); ip != "" { r.RemoteAddr = ip } next.ServeHTTP(w, r) }) }, nil } // peerTrusted returns false on unparseable input so callers fall // through to the header-stripping path. func peerTrusted(remoteAddr string, trusted []netip.Prefix) bool { addr, ok := parsePeerAddr(remoteAddr) if !ok { return false } for _, p := range trusted { if p.Contains(addr) { return true } } return false } func parsePeerAddr(remoteAddr string) (netip.Addr, bool) { if remoteAddr == "" { return netip.Addr{}, false } ap, err := netip.ParseAddrPort(remoteAddr) if err == nil { return ap.Addr(), true } host, _, splitErr := net.SplitHostPort(remoteAddr) if splitErr != nil { host = remoteAddr } addr, err := netip.ParseAddr(host) if err != nil { return netip.Addr{}, false } return addr, true } // prefixToIPNet bridges to realclientip-go, which predates net/netip. func prefixToIPNet(p netip.Prefix) net.IPNet { addr := p.Addr() if addr.Is4() { b := addr.As4() return net.IPNet{IP: b[:], Mask: net.CIDRMask(p.Bits(), 32)} } b := addr.As16() return net.IPNet{IP: b[:], Mask: net.CIDRMask(p.Bits(), 128)} }