package config // HTTP_TRUSTED_PROXIES parsing. // // The setting decides whether a client-supplied header is believed, so the // tests worth having are about what happens when it is WRONG: unset, empty, // mistyped. Every one of those must end in "trust nothing", because the // alternative — trusting something the operator did not write — is the whole // risk this setting carries. import ( "net/netip" "testing" ) func TestTrustedProxiesUnsetTrustsNothing(t *testing.T) { for _, raw := range []string{"", " ", ",", " , , "} { got, err := parseTrustedProxies(raw) if err != nil { t.Errorf("parseTrustedProxies(%q): unexpected error %v", raw, err) } if len(got) != 0 { t.Errorf("parseTrustedProxies(%q) = %v, want empty — an unset value must trust nothing", raw, got) } } } func TestTrustedProxiesParsesCIDRsAndBareAddresses(t *testing.T) { got, err := parseTrustedProxies(" 10.0.0.0/8 , 172.17.0.1 , fd00::/8 , ::1 ") if err != nil { t.Fatalf("unexpected error: %v", err) } want := []string{"10.0.0.0/8", "172.17.0.1/32", "fd00::/8", "::1/128"} if len(got) != len(want) { t.Fatalf("parsed %d entries (%v), want %d", len(got), got, len(want)) } for i, w := range want { if got[i].String() != w { t.Errorf("entry %d = %q, want %q", i, got[i].String(), w) } } } // A bare address must become a single-host block that contains that host and // nothing else — the operator wrote one proxy, not a network. func TestTrustedProxyBareAddressIsOneHost(t *testing.T) { got, err := parseTrustedProxies("172.17.0.1") if err != nil { t.Fatalf("unexpected error: %v", err) } if !got[0].Contains(netip.MustParseAddr("172.17.0.1")) { t.Error("the host itself is not in its own single-host block") } if got[0].Contains(netip.MustParseAddr("172.17.0.2")) { t.Error("a bare address was widened beyond one host") } } // A block written with host bits set is common and easy to misread. Masking it // at parse time makes it mean what its author meant; unmasked, netip.Prefix // .Contains reports false for everything. func TestTrustedProxyCIDRWithHostBitsIsMasked(t *testing.T) { got, err := parseTrustedProxies("10.1.2.3/8") if err != nil { t.Fatalf("unexpected error: %v", err) } if want := "10.0.0.0/8"; got[0].String() != want { t.Fatalf("got %q, want %q", got[0].String(), want) } if !got[0].Contains(netip.MustParseAddr("10.9.9.9")) { t.Error("the masked block does not contain an address inside it") } } // An IPv4-mapped address names an IPv4 host, and must match the peer address // Go reports for an IPv4 connection. func TestTrustedProxyIPv4MappedIsUnmapped(t *testing.T) { got, err := parseTrustedProxies("::ffff:10.0.0.1") if err != nil { t.Fatalf("unexpected error: %v", err) } if !got[0].Contains(netip.MustParseAddr("10.0.0.1")) { t.Errorf("%q does not contain 10.0.0.1", got[0].String()) } } // Malformed entries stop startup. Skipping one would leave the deployment // trusting a shorter list than the operator wrote, and the consequence — every // user sharing one rate-limit bucket — is silent. func TestTrustedProxiesRejectMalformedEntries(t *testing.T) { for _, raw := range []string{ "banana", "10.0.0.0/33", "10.0.0.0/8, banana", "300.1.2.3", "10.0.0.1:8080", "*", "https://proxy.internal", "fd00::/200", } { if _, err := parseTrustedProxies(raw); err == nil { t.Errorf("parseTrustedProxies(%q) was accepted; it must refuse and stop startup", raw) } } } // The error has to name the entry and show the shape expected, because it is // read by an operator at 3am with a container that will not boot. func TestTrustedProxiesErrorNamesTheEntry(t *testing.T) { _, err := parseTrustedProxies("10.0.0.0/8, banana") if err == nil { t.Fatal("expected an error") } for _, want := range []string{"HTTP_TRUSTED_PROXIES", "banana"} { if !contains(err.Error(), want) { t.Errorf("error %q does not mention %q", err.Error(), want) } } } func contains(haystack, needle string) bool { return len(haystack) >= len(needle) && (haystack == needle || len(needle) == 0 || indexOf(haystack, needle) >= 0) } func indexOf(haystack, needle string) int { for i := 0; i+len(needle) <= len(haystack); i++ { if haystack[i:i+len(needle)] == needle { return i } } return -1 }