package httpserver import ( "net" "net/http" "net/netip" "strings" ) // Resolving the caller's network address behind a reverse proxy. // // WHAT THIS IS FOR // // Three limits on this API are keyed by the caller's address: failed logins // (auth.go), OAuth client registration, and OAuth authorization before the // caller has signed in. None of them has a better identity available — // registration is anonymous by definition, and a login attempt is anonymous // until the password has been judged. // // Behind a proxy, net/http reports the PROXY's address on every request. Those // three budgets then describe the proxy rather than the caller, which means one // bucket for the whole deployment: one person retrying a connector exhausts // everybody's registration allowance, and twenty failed passwords anywhere lock // out every user's sign-in. That is the fault this file exists to fix. // // WHY IT IS NOT JUST X-Forwarded-For // // The header is written by clients as readily as by proxies. Believing it // unconditionally is worse than the shared bucket rather than better: a caller // who reaches the API directly can put a different value in every request and // get a fresh budget each time, which is not a weakened limit but no limit at // all. The header carries information only about the hop that appended it, so // it is worth exactly as much as the peer that handed it over. // // Hence: believe it only when the immediate peer is a configured proxy, and // walk the chain from the right, where the entries were written by the hops // closest to us, discarding those that are themselves trusted proxies. The // first address that is not one of ours is the nearest thing to the real client // that the topology can actually vouch for. Everything to its left was supplied // by something we do not control and is never read. // // FAILING SAFE // // Every fallback in here returns the PEER address. That is deliberate and it is // the property worth preserving if this code is ever changed: a bad or missing // chain can only ever make a bucket coarser — more callers sharing one budget, // which is the old behaviour — and can never hand a caller a bucket of their // own. Spoofing gains nothing because no path exists from an untrusted input to // a distinct key. // proxyTrust turns a request into the address key used for rate limiting. // // A value rather than a package-level variable so that the trusted set is // wired once at construction and cannot be changed by anything holding a // request. An empty proxyTrust is valid and trusts nothing. type proxyTrust struct { // trusted networks, already masked by config parsing. trusted []netip.Prefix } // newProxyTrust builds the resolver from configuration. func newProxyTrust(trusted []netip.Prefix) proxyTrust { return proxyTrust{trusted: trusted} } // forwardedHeader is the de facto standard, and what Traefik, nginx, Envoy and // the cloud load balancers all append to. // // RFC 7239's `Forwarded:` header is deliberately NOT read. Supporting both // would mean deciding which wins when they disagree, and an attacker choosing // the one this code happens to prefer. One header, one meaning. const forwardedHeader = "X-Forwarded-For" // clientAddr returns the rate-limiting key for the caller's address. // // The port is stripped: a browser opens a new source port per connection, so // keying on host:port would give every attempt its own budget and limit nothing // at all. IPv6 is keyed by /64 — see bucketKey. func (t proxyTrust) clientAddr(r *http.Request) string { peer, ok := parseHost(r.RemoteAddr) if !ok { // RemoteAddr is not something this code recognises — a test server with // a synthetic value, or a unix socket. Key by it verbatim, which is // what this function did before proxies were considered at all. return strings.TrimSpace(r.RemoteAddr) } peerKey := bucketKey(peer) // Nothing is trusted, so nothing is read. The common case, and the default. if len(t.trusted) == 0 || !t.contains(peer) { return peerKey } if client, ok := t.forwardedClient(r); ok { return bucketKey(client) } return peerKey } // forwardedClient walks the forwarded chain from the right and returns the // first address that is not one of our own proxies. // // It reports false — meaning "fall back to the peer" — for an absent header, a // chain that is entirely trusted proxies, and a malformed entry. The last of // those is the interesting one: a chain that cannot be parsed cannot be // reasoned about, and the safe reading of "10.0.0.1, ???, 10.0.0.2" is that // everything to the left of the damage is unusable. Skipping the bad entry and // carrying on would let a caller put anything it likes in the header and have // this code step over it to reach the value the caller wanted read. func (t proxyTrust) forwardedClient(r *http.Request) (netip.Addr, bool) { // Values(), not Get(), because a chain may arrive as several headers as // well as one comma-separated list; they are the same list in HTTP's terms // and the rightmost entry of the last header is the most recent hop. var chain []string for _, header := range r.Header.Values(forwardedHeader) { for _, entry := range strings.Split(header, ",") { chain = append(chain, strings.TrimSpace(entry)) } } for i := len(chain) - 1; i >= 0; i-- { entry := chain[i] if entry == "" { // A stray comma. Treated as damage rather than skipped, for the // reason in the doc comment above. return netip.Addr{}, false } addr, ok := parseForwardedAddr(entry) if !ok { return netip.Addr{}, false } if t.contains(addr) { // One of ours. Keep walking left, towards the client. continue } return addr, true } // Either there was no header, or every hop in it was a trusted proxy and // none of them recorded a client. Neither tells us who called. return netip.Addr{}, false } // contains reports whether an address is one of the configured proxies. func (t proxyTrust) contains(addr netip.Addr) bool { addr = addr.Unmap() for _, prefix := range t.trusted { if prefix.Contains(addr) { return true } } return false } // bucketKey is the string a rate-limit bucket is keyed by. // // IPv4 keys by the exact address, which is what this service has always done // and what the existing buckets contain. // // IPv6 keys by the /64 PREFIX instead. A single customer is routinely delegated // a whole /64 — often a /56 or shorter — and every address in it is one // machine's to choose. Keying by the full address would hand one caller // 18 quintillion budgets, which is a limit in form only. /64 is the smallest // unit that is reliably one subscriber rather than one interface, so it is the // narrowest honest key. func bucketKey(addr netip.Addr) string { addr = addr.Unmap().WithZone("") // a scope id is local to the host, never a caller identity if addr.Is4() { return addr.String() } prefix, err := addr.Prefix(64) if err != nil { return addr.String() } return prefix.String() } // parseHost splits "host:port" and parses the host. // // RemoteAddr always carries a port for TCP, but a test server, a unix socket or // a middleware that rewrote it may not, so a bare address is accepted too. func parseHost(remoteAddr string) (netip.Addr, bool) { raw := strings.TrimSpace(remoteAddr) if raw == "" { return netip.Addr{}, false } if host, _, err := net.SplitHostPort(raw); err == nil { raw = host } addr, err := netip.ParseAddr(strings.Trim(raw, "[]")) if err != nil { return netip.Addr{}, false } return addr, true } // parseForwardedAddr parses one entry of an X-Forwarded-For chain. // // Entries are bare addresses by the header's convention, but a port turns up in // practice — some proxies append one, and IPv6 is then bracketed. Both forms // are accepted; anything else is malformed and refused. // // "unknown", the obfuscated identifiers RFC 7239 permits, and empty entries are // all refused rather than skipped: they say the chain is not a list of // addresses, and this code declines to guess which of the remaining entries the // proxy meant. func parseForwardedAddr(entry string) (netip.Addr, bool) { if addr, err := netip.ParseAddr(entry); err == nil { return addr, true } // "[2001:db8::1]:443" or "203.0.113.7:443". if host, _, err := net.SplitHostPort(entry); err == nil { if addr, err := netip.ParseAddr(strings.Trim(host, "[]")); err == nil { return addr, true } } return netip.Addr{}, false }