Files
krow_backend/go-api/internal/config/trustedproxies_test.go
Aravind f2aa3b3ad8
Some checks failed
CI / fixture (push) Has been cancelled
CI / test (push) Has been cancelled
mcp connection
2026-09-22 10:58:02 +05:30

135 lines
4.2 KiB
Go

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
}