218 lines
8.3 KiB
Go
218 lines
8.3 KiB
Go
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
|
|
}
|