Rate limits /metrics per client address, ahead of Basic Auth, for #101.
MetricsRateLimit() in internal/middleware uses httprate.LimitBy: 30 requests a minute per client address, every request counted. Over the limit the answer is 429 Too Many Requests with httprate's fixed body, the same whatever the credentials were.
The key comes from realIP(), the existing trusted-proxy logic, so a client that is not on a private or loopback address cannot choose its own key with X-Real-IP or X-Forwarded-For.
routes.go adds it to the /metrics router from #172, before MetricsAuth().
README documents the limit below the environment variable table.
Not visible in the diff: httprate sets X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset on every /metrics response, and Retry-After on a 429.
Disclosures:
Judgement call: IPv6 addresses in one /64 share an allowance (httprate's CanonicalizeIP), since a client usually holds a whole /64 and could otherwise rotate through it.
Judgement call: 30 a minute; one scraper every 15 seconds uses 4.
Not fixed, already there: realIP() takes the first X-Forwarded-For entry, so behind a trusted proxy that appends to a client-sent header and sets no X-Real-IP, the client chooses its key. Needs its own issue.
Unchanged: /metrics answers 404 when it is not configured and 401 when it is, so whether it is configured stays visible.
Model: opus-5-5
Rate limits `/metrics` per client address, ahead of Basic Auth, for https://git.eeqj.de/sneak/dnswatcher/issues/101.
- `MetricsRateLimit()` in `internal/middleware` uses `httprate.LimitBy`: 30 requests a minute per client address, every request counted. Over the limit the answer is `429 Too Many Requests` with httprate's fixed body, the same whatever the credentials were.
- The key comes from `realIP()`, the existing trusted-proxy logic, so a client that is not on a private or loopback address cannot choose its own key with `X-Real-IP` or `X-Forwarded-For`.
- `routes.go` adds it to the `/metrics` router from https://git.eeqj.de/sneak/dnswatcher/pulls/172, before `MetricsAuth()`.
- README documents the limit below the environment variable table.
Not visible in the diff: httprate sets `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` on every `/metrics` response, and `Retry-After` on a 429.
Disclosures:
- Judgement call: IPv6 addresses in one /64 share an allowance (httprate's `CanonicalizeIP`), since a client usually holds a whole /64 and could otherwise rotate through it.
- Judgement call: 30 a minute; one scraper every 15 seconds uses 4.
- Not fixed, already there: `realIP()` takes the first `X-Forwarded-For` entry, so behind a trusted proxy that appends to a client-sent header and sets no `X-Real-IP`, the client chooses its key. Needs its own issue.
- Unchanged: `/metrics` answers 404 when it is not configured and 401 when it is, so whether it is configured stays visible.
Model: opus-5-5
internal/middleware/middleware.go, the key function in MetricsRateLimit(): when a trusted proxy reports an IPv4 client in IPv6-mapped form (::ffff:203.0.113.1, as some proxies do when one socket takes both IPv4 and IPv6), httprate.CanonicalizeIP cuts it to its /64, which is :: for every such address. All IPv4 clients behind that proxy then share one allowance, so any one of them can lock every other one, the Prometheus scraper included, out of /metrics with 30 requests a minute. Acceptable: an IPv6-mapped IPv4 address is turned back into the plain IPv4 address before the /64 grouping, with a case in TestMetricsRateLimitKeysOnClientAddress where two different clients reported that way by the trusted proxy each get their own allowance.
Model: opus-5-5
Review failed on c6f1943.
- `internal/middleware/middleware.go`, the key function in `MetricsRateLimit()`: when a trusted proxy reports an IPv4 client in IPv6-mapped form (`::ffff:203.0.113.1`, as some proxies do when one socket takes both IPv4 and IPv6), `httprate.CanonicalizeIP` cuts it to its /64, which is `::` for every such address. All IPv4 clients behind that proxy then share one allowance, so any one of them can lock every other one, the Prometheus scraper included, out of `/metrics` with 30 requests a minute. Acceptable: an IPv6-mapped IPv4 address is turned back into the plain IPv4 address before the /64 grouping, with a case in `TestMetricsRateLimitKeysOnClientAddress` where two different clients reported that way by the trusted proxy each get their own allowance.
Model: opus-5-5
IPv6-mapped IPv4 from a trusted proxy: fixed. The rate limit turns ::ffff:203.0.113.1 back into 203.0.113.1 before the /64 grouping. TestMetricsRateLimitKeysOnClientAddress has a new case where two clients the trusted proxy reports that way each get their own allowance.
Rebased onto current next.
Model: opus-5-5
Rework for https://git.eeqj.de/sneak/dnswatcher/pulls/180#issuecomment-108563, now at 369f5ce:
- IPv6-mapped IPv4 from a trusted proxy: fixed. The rate limit turns `::ffff:203.0.113.1` back into `203.0.113.1` before the /64 grouping. `TestMetricsRateLimitKeysOnClientAddress` has a new case where two clients the trusted proxy reports that way each get their own allowance.
Rebased onto current `next`.
Model: opus-5-5
/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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Rate limits
/metricsper client address, ahead of Basic Auth, for #101.MetricsRateLimit()ininternal/middlewareuseshttprate.LimitBy: 30 requests a minute per client address, every request counted. Over the limit the answer is429 Too Many Requestswith httprate's fixed body, the same whatever the credentials were.realIP(), the existing trusted-proxy logic, so a client that is not on a private or loopback address cannot choose its own key withX-Real-IPorX-Forwarded-For.routes.goadds it to the/metricsrouter from #172, beforeMetricsAuth().Not visible in the diff: httprate sets
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reseton every/metricsresponse, andRetry-Afteron a 429.Disclosures:
CanonicalizeIP), since a client usually holds a whole /64 and could otherwise rotate through it.realIP()takes the firstX-Forwarded-Forentry, so behind a trusted proxy that appends to a client-sent header and sets noX-Real-IP, the client chooses its key. Needs its own issue./metricsanswers 404 when it is not configured and 401 when it is, so whether it is configured stays visible.Model: opus-5-5
Review failed on
c6f1943.internal/middleware/middleware.go, the key function inMetricsRateLimit(): when a trusted proxy reports an IPv4 client in IPv6-mapped form (::ffff:203.0.113.1, as some proxies do when one socket takes both IPv4 and IPv6),httprate.CanonicalizeIPcuts it to its /64, which is::for every such address. All IPv4 clients behind that proxy then share one allowance, so any one of them can lock every other one, the Prometheus scraper included, out of/metricswith 30 requests a minute. Acceptable: an IPv6-mapped IPv4 address is turned back into the plain IPv4 address before the /64 grouping, with a case inTestMetricsRateLimitKeysOnClientAddresswhere two different clients reported that way by the trusted proxy each get their own allowance.Model: opus-5-5
c6f1943bb9to369f5cea1aRework for #180 (comment), now at
369f5ce:::ffff:203.0.113.1back into203.0.113.1before the /64 grouping.TestMetricsRateLimitKeysOnClientAddresshas a new case where two clients the trusted proxy reports that way each get their own allowance.Rebased onto current
next.Model: opus-5-5
Review passed on
369f5ce.Model: opus-5-5
369f5cea1ato879d41eb2b