package webui import ( "net" "net/http" "net/netip" "strings" "mailgoserver/internal/toolbox" ) // cloudflareRanges are Cloudflare's published proxy IP ranges (fetched live from // https://www.cloudflare.com/ips-v4 and /ips-v6 rather than trusted from memory, // since a stale list here would either wrongly trust an attacker-controlled hop or // wrongly distrust Cloudflare's own edge) — expanded when "cloudflare" appears in // the trusted_proxies config value. Cloudflare rotates these occasionally; re-fetch // and update this list if IP resolution behind Cloudflare ever looks wrong. var cloudflareRanges = []string{ "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13", "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22", "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32", } // parseTrustedProxies reads the [Server] trusted_proxies config value — a // comma-separated list of CIDRs and/or the literal word "cloudflare" — into parsed // prefixes. Unparseable entries are skipped (logged by the caller) rather than // failing startup over a typo in a security-adjacent but non-fatal setting. func parseTrustedProxies(raw string, logger *toolbox.Logger) []netip.Prefix { var out []netip.Prefix for _, entry := range strings.Split(raw, ",") { entry = strings.TrimSpace(entry) if entry == "" { continue } if strings.EqualFold(entry, "cloudflare") { for _, cidr := range cloudflareRanges { if p, err := netip.ParsePrefix(cidr); err == nil { out = append(out, p) } } continue } p, err := netip.ParsePrefix(entry) if err != nil { // A bare IP (no /mask) is a common typo for "trust this one proxy" — // accept it as a /32 or /128 host route rather than silently dropping it. if addr, addrErr := netip.ParseAddr(entry); addrErr == nil { bits := 32 if addr.Is6() { bits = 128 } out = append(out, netip.PrefixFrom(addr, bits)) continue } if logger != nil { logger.Error("trusted_proxies: skipping unparseable entry %q: %v", entry, err) } continue } out = append(out, p) } return out } func isTrustedProxy(trusted []netip.Prefix, addr netip.Addr) bool { for _, p := range trusted { if p.Contains(addr) { return true } } return false } // requestIP returns the best-effort real client IP for r. Forwarded headers // (CF-Connecting-IP, X-Forwarded-For, X-Real-IP) are only honored when the direct // TCP peer (r.RemoteAddr) is itself a configured trusted proxy — otherwise a client // with no proxy in front of it could simply set these headers itself and spoof any // IP for every audit log entry and IP-based check in the app. When trusted, // X-Forwarded-For is walked from the right (the hop closest to us) skipping any // entries that are themselves trusted proxies, landing on the first untrusted (i.e. // real client) address — the standard correct algorithm, since the leftmost entry is // client-supplied and trivially spoofable even through a legitimate proxy. func (a *App) requestIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { host = r.RemoteAddr } peer, parseErr := netip.ParseAddr(host) if parseErr != nil || !isTrustedProxy(a.trustedProxies, peer) { return host } if cf := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cf != "" { return cf } if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { hops := strings.Split(fwd, ",") for i := len(hops) - 1; i >= 0; i-- { hop := strings.TrimSpace(hops[i]) if hop == "" { continue } if addr, err := netip.ParseAddr(hop); err == nil && isTrustedProxy(a.trustedProxies, addr) { continue // another hop we also trust — keep walking left for the real client } return hop } } if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" { return realIP } return host }