Pulse/internal/cloudcp/proxytrust/client_ip.go
rcourtman c7e50d5602 Harden provider-hosted MSP isolation
Broker provider control-plane Docker access through a socket proxy, remove broad host mounts, align audit and rate-limit proxy trust, harden tenant runtime containers, restrict workspace report logo paths, and update provider deploy guardrails.
2026-06-02 21:10:13 +01:00

128 lines
2.6 KiB
Go

package proxytrust
import (
"net"
"net/http"
"os"
"strings"
"sync"
)
var (
trustedProxyOnce sync.Once
trustedProxyCIDRs []*net.IPNet
)
// ClientIP resolves the client address only through explicitly trusted proxy
// hops. Untrusted peers cannot spoof audit or rate-limit identity with XFF.
func ClientIP(r *http.Request) string {
if r == nil {
return ""
}
remote := ExtractRemoteIP(r.RemoteAddr)
if remote == "" {
return ""
}
if IsTrustedProxyIP(remote) {
if xff := rightMostUntrustedForwardedIP(r.Header.Get("X-Forwarded-For")); xff != "" {
return xff
}
if realIP := strings.TrimSpace(strings.Trim(r.Header.Get("X-Real-IP"), "[]")); net.ParseIP(realIP) != nil {
return realIP
}
}
return remote
}
func ExtractRemoteIP(remoteAddr string) string {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return strings.Trim(remoteAddr, "[]")
}
return strings.Trim(host, "[]")
}
func rightMostUntrustedForwardedIP(header string) string {
parts := strings.Split(header, ",")
var leftMostValid string
for i := len(parts) - 1; i >= 0; i-- {
candidate := strings.TrimSpace(strings.Trim(parts[i], "[]"))
if net.ParseIP(candidate) == nil {
continue
}
leftMostValid = candidate
if !IsTrustedProxyIP(candidate) {
return candidate
}
}
return leftMostValid
}
func IsTrustedProxyIP(rawIP string) bool {
ip := net.ParseIP(strings.Trim(rawIP, "[]"))
if ip == nil {
return false
}
trustedProxyOnce.Do(loadTrustedProxyCIDRs)
if len(trustedProxyCIDRs) == 0 {
return false
}
for _, network := range trustedProxyCIDRs {
if network.Contains(ip) {
return true
}
}
return false
}
func loadTrustedProxyCIDRs() {
raw := strings.TrimSpace(os.Getenv("CP_TRUSTED_PROXY_CIDRS"))
if raw == "" {
// Backward-compatible fallback to the shared setting used by the app server.
raw = strings.TrimSpace(os.Getenv("PULSE_TRUSTED_PROXY_CIDRS"))
}
if raw == "" {
return
}
for _, entry := range strings.Split(raw, ",") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
_, network, err := net.ParseCIDR(entry)
if err != nil {
continue
}
network.IP = network.IP.Mask(network.Mask)
trustedProxyCIDRs = append(trustedProxyCIDRs, network)
continue
}
ip := net.ParseIP(entry)
if ip == nil {
continue
}
bits := 32
if ip.To4() == nil {
bits = 128
}
mask := net.CIDRMask(bits, bits)
trustedProxyCIDRs = append(trustedProxyCIDRs, &net.IPNet{
IP: ip.Mask(mask),
Mask: mask,
})
}
}
func ResetForTesting() {
trustedProxyOnce = sync.Once{}
trustedProxyCIDRs = nil
}