1 Commits
Author SHA1 Message Date
sneak c6f1943bb9 metrics: rate limit /metrics per client address before Basic Auth (closes #101)
check / check (push) Successful in 1m9s
/metrics is behind a password, and REPO_POLICIES.md requires rate
limiting on password logins. Each client address may now send it 30
requests a minute, counted by httprate before Basic Auth, so failed
logins use up the allowance and a request over it gets 429 without
the password being checked. The address is the one the existing
trusted-proxy logic in internal/middleware works out, with IPv6
addresses grouped by /64. A Prometheus server scraping every 15
seconds sends 4 requests a minute.

Model: opus-5-5
2026-10-01 19:37:09 +00:00
4 changed files with 5 additions and 34 deletions
+2 -2
View File
@@ -22,8 +22,6 @@ https://git.eeqj.de/sneak/dnswatcher/issues/104
- 2026-10-01: `/metrics` allows each client address 30 requests a minute,
counted before Basic Auth, and answers 429 beyond that (closes #101).
- 2026-10-01: `script/install-precommit` asks git for the repository's git
directory, so `make hooks` also works where `.git` is a file (closes #129).
- 2026-10-01: `TODO.md` brought up to date: open issues listed by URL, every
Completed Steps entry cut to at most two lines (closes #146).
- 2026-10-01: wildcard CORS now applies only to the public routes, not to
@@ -108,5 +106,7 @@ https://git.eeqj.de/sneak/dnswatcher/issues/104
- README accuracy sweep: https://git.eeqj.de/sneak/dnswatcher/issues/108
- README sections required by policy:
https://git.eeqj.de/sneak/dnswatcher/issues/173
- `script/install-precommit` in a linked worktree:
https://git.eeqj.de/sneak/dnswatcher/issues/129
- fixed root server order: https://git.eeqj.de/sneak/dnswatcher/issues/138
- review toward 1.0: https://git.eeqj.de/sneak/dnswatcher/issues/144
+2 -12
View File
@@ -5,7 +5,6 @@ import (
"log/slog"
"net"
"net/http"
"net/netip"
"strings"
"time"
@@ -287,22 +286,13 @@ func (m *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
// trusted proxy cannot get a fresh allowance by sending its own
// X-Real-IP or X-Forwarded-For. CanonicalizeIP counts all IPv6
// addresses in one /64 as one client, since a client usually holds a
// whole /64. An IPv4 address a proxy reports in IPv6-mapped form
// (::ffff:203.0.113.1) is turned back into plain IPv4 first, as every
// such address is in the same /64.
// whole /64.
func (m *Middleware) MetricsRateLimit() func(http.Handler) http.Handler {
return httprate.LimitBy(
metricsRequestLimit,
metricsRequestWindow,
func(request *http.Request) (string, error) {
ip := realIP(request)
addr, err := netip.ParseAddr(ip)
if err == nil {
ip = addr.Unmap().String()
}
return httprate.CanonicalizeIP(ip), nil
return httprate.CanonicalizeIP(realIP(request)), nil
},
)
}
-6
View File
@@ -443,12 +443,6 @@ func TestMetricsRateLimitKeysOnClientAddress(t *testing.T) {
trustedProxy, "203.0.113.2",
http.StatusOK,
},
{
"another client behind the proxy, IPv6-mapped",
trustedProxy, "::ffff:203.0.113.1",
trustedProxy, "::ffff:203.0.113.2",
http.StatusOK,
},
{
"same IPv6 /64",
"[2001:db8::1]:4000", "",
+1 -14
View File
@@ -7,20 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
# Stop if this directory is not the top of its own git checkout, for
# example a copy inside another repository, whose hook must not be
# replaced.
if [ "$(git rev-parse --show-toplevel)" != "$ROOT" ]; then
echo "install-precommit: $ROOT is not the top of a git checkout" >&2
exit 1
fi
# Ask git for the repository's own git directory: .git is a file, not
# a directory, in some checkouts (for example a clone made with
# --separate-git-dir). core.hooksPath is deliberately not followed, so
# the hook is never written outside this repository.
hooks="$(git rev-parse --git-common-dir)/hooks"
mkdir -p "$hooks"
hook="$hooks/pre-commit"
hook=".git/hooks/pre-commit"
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
chmod +x "$hook"
echo "pre-commit hook installed: runs script/precommit"