Files

117 lines
4.0 KiB
Go

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
}