clientIP in internal/handlers/api.go takes the client's address from X-Forwarded-For or X-Real-IP whoever the connecting peer is. A client that connects directly can send a new X-Forwarded-For value with each POST /api/v1/login and never hit the login rate limit, and it picks the IP address and hostname recorded for its session. The request log has the opposite fault: it logs the TCP peer (ipFromHostPort(request.RemoteAddr) in internal/middleware/middleware.go), so behind a reverse proxy every line shows the proxy.
REPO_POLICIES.md ("Reverse proxy awareness") allows forwarded headers only from a configured set of trusted proxies. The standing ruling for every app sets the default: all RFC 1918 addresses.
Definition of done
New setting TRUSTED_PROXIES, read in internal/config: a comma-separated list of CIDR prefixes or single addresses.
Unset: the trusted set is 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16.
Set: the list replaces the default entirely.
Set to the empty string: nothing is trusted.
Set but not parseable: startup fails with an error naming TRUSTED_PROXIES, like the other settings.
A middleware in internal/middleware resolves the client address once per request and replaces request.RemoteAddr with it. It runs before Logging in internal/server/routes.go, so the request log, the login rate limiter and the session IP address and hostname (internal/handlers/auth.go, internal/handlers/api.go) all get the resolved address. clientIP stops reading headers.
Peer not trusted: the peer's address; both headers ignored.
Peer trusted: walk X-Forwarded-For from the right, skip trusted addresses, take the first untrusted one; when every entry is trusted, the leftmost. Without X-Forwarded-For, X-Real-IP. When an entry that the walk reaches does not parse as an IP address, the peer's address.
An IPv4-mapped IPv6 peer (::ffff:10.0.0.1) is unmapped before the check.
The trusted set is a go4.org/netipxIPSet (the decided library for IP address sets), not a hand-written list of ranges.
The IRC listener (internal/ircserver) is plain TCP with no headers and is left alone.
README.md: document TRUSTED_PROXIES under "Configuration", and rewrite the three passages that now say the headers are trusted from anyone: the hostname paragraph (near "Over the HTTP API, the server takes the client's IP address"), "Login rate limit", and "Reverse proxy". State that a peer inside the trusted set chooses its client address through X-Forwarded-For, so an operator whose clients connect directly from private addresses sets the list to the proxy's address alone. "Logging" says the logged address is the resolved client address.
Tests, table-driven, in the external test packages:
internal/middleware: untrusted peer with forged headers gives the peer; trusted peer with a chain gives the rightmost untrusted entry; an all-trusted chain gives the leftmost; X-Real-IP used only without X-Forwarded-For; an unparseable entry gives the peer; a mapped IPv6 peer.
internal/config: unset gives the default, a value replaces it, the empty string trusts nothing, an invalid value fails startup.
internal/handlers: a client connecting from an untrusted address cannot get past the login rate limit by changing X-Forwarded-For.
Read first: AGENTS.md, CONVENTIONS.md, REPO_POLICIES.md. The same rule is built in sneak/webhooker (internal/middleware/clientip.go on next); there an empty value counts as unset, here it trusts nothing.
Model: opus-5-5
`clientIP` in `internal/handlers/api.go` takes the client's address from `X-Forwarded-For` or `X-Real-IP` whoever the connecting peer is. A client that connects directly can send a new `X-Forwarded-For` value with each `POST /api/v1/login` and never hit the login rate limit, and it picks the IP address and hostname recorded for its session. The request log has the opposite fault: it logs the TCP peer (`ipFromHostPort(request.RemoteAddr)` in `internal/middleware/middleware.go`), so behind a reverse proxy every line shows the proxy.
`REPO_POLICIES.md` ("Reverse proxy awareness") allows forwarded headers only from a configured set of trusted proxies. The standing ruling for every app sets the default: all RFC 1918 addresses.
## Definition of done
- New setting `TRUSTED_PROXIES`, read in `internal/config`: a comma-separated list of CIDR prefixes or single addresses.
- Unset: the trusted set is `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16`.
- Set: the list replaces the default entirely.
- Set to the empty string: nothing is trusted.
- Set but not parseable: startup fails with an error naming `TRUSTED_PROXIES`, like the other settings.
- A middleware in `internal/middleware` resolves the client address once per request and replaces `request.RemoteAddr` with it. It runs before `Logging` in `internal/server/routes.go`, so the request log, the login rate limiter and the session IP address and hostname (`internal/handlers/auth.go`, `internal/handlers/api.go`) all get the resolved address. `clientIP` stops reading headers.
- Peer not trusted: the peer's address; both headers ignored.
- Peer trusted: walk `X-Forwarded-For` from the right, skip trusted addresses, take the first untrusted one; when every entry is trusted, the leftmost. Without `X-Forwarded-For`, `X-Real-IP`. When an entry that the walk reaches does not parse as an IP address, the peer's address.
- An IPv4-mapped IPv6 peer (`::ffff:10.0.0.1`) is unmapped before the check.
- The trusted set is a `go4.org/netipx` `IPSet` (the decided library for IP address sets), not a hand-written list of ranges.
- The IRC listener (`internal/ircserver`) is plain TCP with no headers and is left alone.
- `README.md`: document `TRUSTED_PROXIES` under "Configuration", and rewrite the three passages that now say the headers are trusted from anyone: the `hostname` paragraph (near "Over the HTTP API, the server takes the client's IP address"), "Login rate limit", and "Reverse proxy". State that a peer inside the trusted set chooses its client address through `X-Forwarded-For`, so an operator whose clients connect directly from private addresses sets the list to the proxy's address alone. "Logging" says the logged address is the resolved client address.
- Tests, table-driven, in the external test packages:
- `internal/middleware`: untrusted peer with forged headers gives the peer; trusted peer with a chain gives the rightmost untrusted entry; an all-trusted chain gives the leftmost; `X-Real-IP` used only without `X-Forwarded-For`; an unparseable entry gives the peer; a mapped IPv6 peer.
- `internal/config`: unset gives the default, a value replaces it, the empty string trusts nothing, an invalid value fails startup.
- `internal/handlers`: a client connecting from an untrusted address cannot get past the login rate limit by changing `X-Forwarded-For`.
Read first: `AGENTS.md`, `CONVENTIONS.md`, `REPO_POLICIES.md`. The same rule is built in `sneak/webhooker` (`internal/middleware/clientip.go` on `next`); there an empty value counts as unset, here it trusts nothing.
Model: opus-5-5
clawbot
self-assigned this 2026-11-10 20:52:23 +01:00
Built in #264, branch issue-259-trusted-proxies, as specified in the definition of done.
request.RemoteAddr keeps host:port form, with the TCP connection's port, so clientIP and the request log read it unchanged.
The README's nginx example proxies from 127.0.0.1, which the default set does not trust. The README tells that operator to set TRUSTED_PROXIES=127.0.0.1.
Model: opus-5-5
Built in https://git.eeqj.de/sneak/neoirc/pulls/264, branch `issue-259-trusted-proxies`, as specified in the definition of done.
- `request.RemoteAddr` keeps `host:port` form, with the TCP connection's port, so `clientIP` and the request log read it unchanged.
- The README's nginx example proxies from `127.0.0.1`, which the default set does not trust. The README tells that operator to set `TRUSTED_PROXIES=127.0.0.1`.
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.
clientIPininternal/handlers/api.gotakes the client's address fromX-Forwarded-FororX-Real-IPwhoever the connecting peer is. A client that connects directly can send a newX-Forwarded-Forvalue with eachPOST /api/v1/loginand never hit the login rate limit, and it picks the IP address and hostname recorded for its session. The request log has the opposite fault: it logs the TCP peer (ipFromHostPort(request.RemoteAddr)ininternal/middleware/middleware.go), so behind a reverse proxy every line shows the proxy.REPO_POLICIES.md("Reverse proxy awareness") allows forwarded headers only from a configured set of trusted proxies. The standing ruling for every app sets the default: all RFC 1918 addresses.Definition of done
TRUSTED_PROXIES, read ininternal/config: a comma-separated list of CIDR prefixes or single addresses.10.0.0.0/8,172.16.0.0/12and192.168.0.0/16.TRUSTED_PROXIES, like the other settings.internal/middlewareresolves the client address once per request and replacesrequest.RemoteAddrwith it. It runs beforeLoggingininternal/server/routes.go, so the request log, the login rate limiter and the session IP address and hostname (internal/handlers/auth.go,internal/handlers/api.go) all get the resolved address.clientIPstops reading headers.X-Forwarded-Forfrom the right, skip trusted addresses, take the first untrusted one; when every entry is trusted, the leftmost. WithoutX-Forwarded-For,X-Real-IP. When an entry that the walk reaches does not parse as an IP address, the peer's address.::ffff:10.0.0.1) is unmapped before the check.go4.org/netipxIPSet(the decided library for IP address sets), not a hand-written list of ranges.internal/ircserver) is plain TCP with no headers and is left alone.README.md: documentTRUSTED_PROXIESunder "Configuration", and rewrite the three passages that now say the headers are trusted from anyone: thehostnameparagraph (near "Over the HTTP API, the server takes the client's IP address"), "Login rate limit", and "Reverse proxy". State that a peer inside the trusted set chooses its client address throughX-Forwarded-For, so an operator whose clients connect directly from private addresses sets the list to the proxy's address alone. "Logging" says the logged address is the resolved client address.internal/middleware: untrusted peer with forged headers gives the peer; trusted peer with a chain gives the rightmost untrusted entry; an all-trusted chain gives the leftmost;X-Real-IPused only withoutX-Forwarded-For; an unparseable entry gives the peer; a mapped IPv6 peer.internal/config: unset gives the default, a value replaces it, the empty string trusts nothing, an invalid value fails startup.internal/handlers: a client connecting from an untrusted address cannot get past the login rate limit by changingX-Forwarded-For.Read first:
AGENTS.md,CONVENTIONS.md,REPO_POLICIES.md. The same rule is built insneak/webhooker(internal/middleware/clientip.goonnext); there an empty value counts as unset, here it trusts nothing.Model: opus-5-5
Built in #264, branch
issue-259-trusted-proxies, as specified in the definition of done.request.RemoteAddrkeepshost:portform, with the TCP connection's port, soclientIPand the request log read it unchanged.127.0.0.1, which the default set does not trust. The README tells that operator to setTRUSTED_PROXIES=127.0.0.1.Model: opus-5-5