nginx: trust X-Forwarded-For only from TRUSTED_PROXIES (closes #64)
check / check (push) Successful in 50s

nginx trusted X-Forwarded-For from every RFC1918 address, so a client
reaching it from one could write a new address on each request and
get a fresh rate-limit allowance. The container's TRUSTED_PROXIES now
names the reverse proxies nginx trusts, none by default.
bin/entrypoint.sh makes each entry a CIDR, checks it with the new
"netwatch-server check-cidr", which runs the server's own
TRUSTED_PROXIES parsing, and writes one set_real_ip_from line per
entry into /etc/nginx/trusted-proxies.conf, which nginx.conf includes.
The backend is started with TRUSTED_PROXIES=127.0.0.1/32, since nginx
is its only client. The viewport test mounts an empty file there.

Model: opus-5-5
This commit is contained in:
2026-09-29 05:48:43 +00:00
parent d2f219ca19
commit 02dbd1a89b
11 changed files with 129 additions and 34 deletions
@@ -29,7 +29,3 @@ func ClientIP(
) string {
return clientIP(remoteAddr, header, trusted)
}
func ParseTrustedProxies(cidrs []string) ([]netip.Prefix, error) {
return parseTrustedProxies(cidrs)
}
+6 -4
View File
@@ -64,7 +64,7 @@ func New(
_ fx.Lifecycle,
params Params,
) (*Middleware, error) {
trusted, err := parseTrustedProxies(params.Config.TrustedProxies)
trusted, err := ParseTrustedProxies(params.Config.TrustedProxies)
if err != nil {
return nil, err
}
@@ -77,9 +77,11 @@ func New(
return s, nil
}
// parseTrustedProxies converts the TRUSTED_PROXIES entries into
// prefixes, failing fast on any malformed entry.
func parseTrustedProxies(cidrs []string) ([]netip.Prefix, error) {
// ParseTrustedProxies converts the TRUSTED_PROXIES entries into
// prefixes, failing fast on any malformed entry. Each entry must be
// a CIDR; a lone address is refused. "netwatch-server check-cidr"
// runs it too.
func ParseTrustedProxies(cidrs []string) ([]netip.Prefix, error) {
prefixes := make([]netip.Prefix, 0, len(cidrs))
for _, cidr := range cidrs {
+20 -3
View File
@@ -36,15 +36,32 @@ func mustPrefixes(t *testing.T, cidrs ...string) []netip.Prefix {
return prefixes
}
// TestParseTrustedProxiesRejectsMalformed includes entries nginx would
// read as another address or look up as a hostname, in the CIDR form
// bin/entrypoint.sh gives "netwatch-server check-cidr".
func TestParseTrustedProxiesRejectsMalformed(t *testing.T) {
t.Parallel()
_, err := middleware.ParseTrustedProxies([]string{"not-a-cidr"})
if err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXIES") {
t.Fatalf("error = %v, want one naming TRUSTED_PROXIES", err)
for _, cidr := range []string{
"not-a-cidr", "10.0.0.1", "1.2.3/32", "172.30/32", "10/32",
"cafe/32", "999.1.1.1/32", "10.0.0.0/33", "::1/129",
"fe80::1%eth0/128",
} {
_, err := middleware.ParseTrustedProxies([]string{cidr})
if err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXIES") {
t.Errorf("%q: error = %v, want one naming TRUSTED_PROXIES",
cidr, err)
}
}
}
func TestParseTrustedProxiesAcceptsCIDRs(t *testing.T) {
t.Parallel()
mustPrefixes(t, "172.17.0.1/32", "10.0.0.0/8", "2001:db8::1/128",
"2001:db8::/32", "::ffff:192.0.2.1/128")
}
type clientIPCase struct {
name string
remoteAddr string