117 lines
4.0 KiB
Go
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
|
|
}
|