mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-26 01:19:32 +00:00
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.
128 lines
2.6 KiB
Go
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
|
|
}
|