// Package clientip says who sent a request and how, believing a proxy's // X-Forwarded-* headers only when the request came from a proxy the // configuration names. // // Believed from anyone, X-Forwarded-Host decides the address written into the // setup scripts clients pipe to a shell, and X-Forwarded-For lets one client // claim to be any number of others to whatever counts attempts per address. // Believed from nobody, every client behind a proxy is one address, and a // limit per address is a limit on all of them together. package clientip import ( "net" "net/http" "net/netip" "strings" "sync/atomic" ) var trusted atomic.Pointer[[]netip.Prefix] // SetTrusted names the proxies whose forwarding headers are believed: // addresses or CIDR prefixes. Anything unparsable is skipped and returned. func SetTrusted(list []string) (skipped []string) { var out []netip.Prefix for _, s := range list { s = strings.TrimSpace(s) if p, err := netip.ParsePrefix(s); err == nil { out = append(out, p.Masked()) continue } if a, err := netip.ParseAddr(s); err == nil { out = append(out, netip.PrefixFrom(a, a.BitLen())) continue } skipped = append(skipped, s) } trusted.Store(&out) return skipped } func isTrusted(addr string) bool { a, err := netip.ParseAddr(addr) if err != nil { return false } a = a.Unmap() list := trusted.Load() if list == nil { return false } for _, p := range *list { if p.Contains(a) { return true } } return false } // peer is the address the connection came from. func peer(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } // fromProxy reports whether the request came through a trusted proxy. func fromProxy(r *http.Request) bool { return isTrusted(peer(r)) } // Addr is the client's address: the connection's, or through trusted // proxies the nearest address in X-Forwarded-For that is not one of them. func Addr(r *http.Request) string { p := peer(r) if !isTrusted(p) { return p } hops := strings.Split(r.Header.Get("X-Forwarded-For"), ",") for i := len(hops) - 1; i >= 0; i-- { h := strings.TrimSpace(hops[i]) if h == "" { continue } if !isTrusted(h) { return h } } return p } // Proto is "https" when the client spoke TLS: to this server, or to a // trusted proxy that says so. func Proto(r *http.Request) string { if r.TLS != nil { return "https" } if fromProxy(r) { if v := first(r.Header.Get("X-Forwarded-Proto")); v != "" { return strings.ToLower(v) } } return "http" } // Host is the host the client asked for: Host, or what a trusted proxy says // it was asked for. func Host(r *http.Request) string { if fromProxy(r) { if v := first(r.Header.Get("X-Forwarded-Host")); v != "" { return v } } return r.Host } func first(v string) string { if i := strings.IndexByte(v, ','); i >= 0 { v = v[:i] } return strings.TrimSpace(v) }