2 Commits
Author SHA1 Message Date
sneak 369f5cea1a metrics: rate limit /metrics per client address before Basic Auth (closes #101)
check / check (push) Successful in 1m7s
/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; an IPv4 address a proxy reports in
IPv6-mapped form counts as the plain IPv4 address. A Prometheus
server scraping every 15 seconds sends 4 requests a minute.

Model: opus-5-5
2026-10-01 19:57:09 +00:00
clawbot bde047f2a3 script/install-precommit: work where .git is a file (closes #129)
check / check (push) Successful in 1m9s
The script wrote the hook to .git/hooks, which fails when .git is a
file rather than a directory, as in a clone made with
--separate-git-dir. It now asks git for the repository's own git
directory with `git rev-parse --git-common-dir`, creates its hooks
directory if missing, and writes the hook there. In an ordinary clone
that is .git/hooks, so nothing moves. Before writing anything it stops
with an error when its top directory is not the top of the checkout
git finds, so a copy inside another repository cannot replace that
repository's hook. git's core.hooksPath setting is not followed; where
it is in force, git does not run the installed hook, as before.

Model: opus-5-5
2026-10-01 21:45:57 +02:00
4 changed files with 34 additions and 5 deletions
+2 -2
View File
@@ -22,6 +22,8 @@ https://git.eeqj.de/sneak/dnswatcher/issues/104
- 2026-10-01: `/metrics` allows each client address 30 requests a minute, - 2026-10-01: `/metrics` allows each client address 30 requests a minute,
counted before Basic Auth, and answers 429 beyond that (closes #101). 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 - 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). 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 - 2026-10-01: wildcard CORS now applies only to the public routes, not to
@@ -106,7 +108,5 @@ https://git.eeqj.de/sneak/dnswatcher/issues/104
- README accuracy sweep: https://git.eeqj.de/sneak/dnswatcher/issues/108 - README accuracy sweep: https://git.eeqj.de/sneak/dnswatcher/issues/108
- README sections required by policy: - README sections required by policy:
https://git.eeqj.de/sneak/dnswatcher/issues/173 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 - fixed root server order: https://git.eeqj.de/sneak/dnswatcher/issues/138
- review toward 1.0: https://git.eeqj.de/sneak/dnswatcher/issues/144 - review toward 1.0: https://git.eeqj.de/sneak/dnswatcher/issues/144
+12 -2
View File
@@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"net" "net"
"net/http" "net/http"
"net/netip"
"strings" "strings"
"time" "time"
@@ -286,13 +287,22 @@ func (m *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
// trusted proxy cannot get a fresh allowance by sending its own // trusted proxy cannot get a fresh allowance by sending its own
// X-Real-IP or X-Forwarded-For. CanonicalizeIP counts all IPv6 // X-Real-IP or X-Forwarded-For. CanonicalizeIP counts all IPv6
// addresses in one /64 as one client, since a client usually holds a // addresses in one /64 as one client, since a client usually holds a
// whole /64. // 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.
func (m *Middleware) MetricsRateLimit() func(http.Handler) http.Handler { func (m *Middleware) MetricsRateLimit() func(http.Handler) http.Handler {
return httprate.LimitBy( return httprate.LimitBy(
metricsRequestLimit, metricsRequestLimit,
metricsRequestWindow, metricsRequestWindow,
func(request *http.Request) (string, error) { func(request *http.Request) (string, error) {
return httprate.CanonicalizeIP(realIP(request)), nil ip := realIP(request)
addr, err := netip.ParseAddr(ip)
if err == nil {
ip = addr.Unmap().String()
}
return httprate.CanonicalizeIP(ip), nil
}, },
) )
} }
+6
View File
@@ -443,6 +443,12 @@ func TestMetricsRateLimitKeysOnClientAddress(t *testing.T) {
trustedProxy, "203.0.113.2", trustedProxy, "203.0.113.2",
http.StatusOK, 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", "same IPv6 /64",
"[2001:db8::1]:4000", "", "[2001:db8::1]:4000", "",
+14 -1
View File
@@ -7,7 +7,20 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
hook=".git/hooks/pre-commit" # 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"
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook" printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
chmod +x "$hook" chmod +x "$hook"
echo "pre-commit hook installed: runs script/precommit" echo "pre-commit hook installed: runs script/precommit"