Bucket IPv6 rate-limit keys by /64 (closes #125)
All checks were successful
check / check (push) Successful in 2m53s

Rate-limit keys were per-address, i.e. per /128 for IPv6. A routed /64
is the normal residential and mobile IPv6 allocation, so a client could
rotate source addresses inside its own prefix and mint a fresh bucket
per request, evading every limiter here at the network layer with no
spoofing and nothing to detect.

The shared key function now reduces the client address to a bucket by
family: IPv4 keys on the full address, IPv6 on its /64 prefix. All four
limiter instances (login, password change, and the receiver's
per-entrypoint and aggregate limits) go through that one function, so
all of them are covered.

Both branches of that function are covered by tests: the direct-peer
branch, and the trusted-proxy branch that takes the client address out
of X-Forwarded-For. The second is the one a production deployment
takes, since it is required to run behind a reverse proxy with
TRUSTED_PROXIES set.

IPv4-mapped addresses (::ffff:1.2.3.4) key as the IPv4 address they
carry rather than being masked, which would otherwise collapse every
IPv4 client behind a mapping proxy into the ::ffff:0:0/96 bucket. An
unparseable RemoteAddr still keys on its raw value, so those stay in
distinct buckets instead of collapsing together.

No new configuration surface.
This commit is contained in:
2026-08-17 20:54:33 +00:00
parent c378690977
commit 9826cc600f
3 changed files with 463 additions and 20 deletions

View File

@@ -935,7 +935,14 @@ Every limiter here — receiver, login, and password change — identifies
the client the same way, through one shared key function: the
connection's own address, unless the peer is listed in
`TRUSTED_PROXIES`, in which case the forwarded client address is used
instead. See [Trusted proxies](#trusted-proxies). Deployed without that
instead. That address becomes a bucket by family: IPv4 keys on the full
address, IPv6 on its `/64` prefix. A routed `/64` is the normal
residential and mobile IPv6 allocation, so keying IPv6 per address would
let one subscriber rotate source addresses and mint a fresh bucket per
request, evading these limits at the network layer without spoofing
anything; the cost is that distinct clients inside one `/64` share a
bucket. IPv4-mapped addresses (`::ffff:1.2.3.4`) key as the IPv4 address
they carry. See [Trusted proxies](#trusted-proxies). Deployed without that
variable set, a client behind a reverse proxy shares one bucket with
every other client behind the same proxy. Set `TRUSTED_PROXIES` to the
proxy's address to get per-client limits back. What the shared bucket