From a7dfbf4414fe3f08f60909fb4cf36754269db75b Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:30:53 +0000 Subject: [PATCH] test: failing tests for blocked_networks config and extended SSRF ranges Table-driven coverage for the CGNAT, IETF-protocol, benchmark, and NAT64 ranges (IPv4, IPv6, and IPv4-mapped forms), an operator-supplied blocked_networks entry enforced by the dialer, and strict parsing that aborts startup naming the key and the offending value. Model: opus-4-8 --- .../config/blocked_networks_internal_test.go | 90 ++++++++++++++ .../blocked_networks_internal_test.go | 111 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 internal/config/blocked_networks_internal_test.go create mode 100644 internal/httpfetcher/blocked_networks_internal_test.go diff --git a/internal/config/blocked_networks_internal_test.go b/internal/config/blocked_networks_internal_test.go new file mode 100644 index 0000000..25373be --- /dev/null +++ b/internal/config/blocked_networks_internal_test.go @@ -0,0 +1,90 @@ +package config + +import ( + "testing" +) + +// TestBlockedNetworksParsed loads a valid blocked_networks list and checks +// each CIDR is parsed into the resolved prefixes in order. +func TestBlockedNetworksParsed(t *testing.T) { + t.Parallel() + + yamlContent := signingKeyLine + `blocked_networks: + - 203.0.113.0/24 + - 2001:db8::/32 +` + + c, err := configFromYAML(t, yamlContent) + if err != nil { + t.Fatalf("valid blocked_networks should load, got error: %v", err) + } + + want := []string{"203.0.113.0/24", "2001:db8::/32"} + if len(c.BlockedNetworks) != len(want) { + t.Fatalf("BlockedNetworks = %v, want %d entries", c.BlockedNetworks, len(want)) + } + + for i, w := range want { + if got := c.BlockedNetworks[i].String(); got != w { + t.Errorf("BlockedNetworks[%d] = %q, want %q", i, got, w) + } + } +} + +// TestBlockedNetworksOmittedIsEmpty confirms an omitted key leaves the +// operator list empty; the built-in defaults still apply in the fetcher. +func TestBlockedNetworksOmittedIsEmpty(t *testing.T) { + t.Parallel() + + c, err := configFromYAML(t, signingKeyLine) + if err != nil { + t.Fatalf("minimal config should be valid, got error: %v", err) + } + + if len(c.BlockedNetworks) != 0 { + t.Errorf("BlockedNetworks = %v, want empty", c.BlockedNetworks) + } +} + +// TestBlockedNetworksInvalidAbortsStartup checks that malformed values abort +// startup with an error naming the key and the offending value. +func TestBlockedNetworksInvalidAbortsStartup(t *testing.T) { + t.Parallel() + + runAbortCases(t, []abortCase{ + { + name: "not-a-cidr", + yaml: signingKeyLine + `blocked_networks: + - not-a-cidr +`, + wantErrSubstrings: []string{keyBlockedNetworks, "not-a-cidr"}, + }, + { + name: "bare-address-without-prefix", + yaml: signingKeyLine + `blocked_networks: + - 10.0.0.1 +`, + wantErrSubstrings: []string{keyBlockedNetworks, "10.0.0.1"}, + }, + { + name: "empty-entry", + yaml: signingKeyLine + `blocked_networks: + - "" +`, + wantErrSubstrings: []string{keyBlockedNetworks}, + }, + { + name: "non-string-entry", + yaml: signingKeyLine + `blocked_networks: + - 42 +`, + wantErrSubstrings: []string{keyBlockedNetworks}, + }, + { + name: "null-value", + yaml: signingKeyLine + `blocked_networks: +`, + wantErrSubstrings: []string{keyBlockedNetworks, nullValueText}, + }, + }) +} diff --git a/internal/httpfetcher/blocked_networks_internal_test.go b/internal/httpfetcher/blocked_networks_internal_test.go new file mode 100644 index 0000000..dfbf8f5 --- /dev/null +++ b/internal/httpfetcher/blocked_networks_internal_test.go @@ -0,0 +1,111 @@ +package httpfetcher + +import ( + "context" + "errors" + "net" + "net/http" + "net/netip" + "testing" +) + +// TestIsPrivateIPBlocksSpecialRanges covers the internal and special-use +// ranges added to the built-in blocklist, in IPv4, IPv6, and IPv4-mapped +// forms, alongside public controls that must stay reachable. +func TestIsPrivateIPBlocksSpecialRanges(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ip string + want bool + }{ + {"cgnat-low", "100.64.0.1", true}, + {"cgnat-high", "100.127.255.254", true}, + {"ietf-protocol", "192.0.0.1", true}, + {"benchmark-low", "198.18.0.1", true}, + {"benchmark-high", "198.19.255.254", true}, + {"nat64", "64:ff9b::1", true}, + {"nat64-embeds-private", "64:ff9b::a00:1", true}, // maps 10.0.0.1 + {"ipv4-mapped-private", "::ffff:10.0.0.1", true}, + {"cloud-metadata", "169.254.169.254", true}, + {"public-v4", "8.8.8.8", false}, + {"test-net-1-public", testPublicHost, false}, // TEST-NET-1, stays public + {"public-v6", "2001:4860:4860::8888", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ip := net.ParseIP(tc.ip) + if ip == nil { + t.Fatalf("failed to parse IP %q", tc.ip) + } + + got := isPrivateIP(ip) + if got != tc.want { + t.Errorf("isPrivateIP(%q) = %v, want %v", tc.ip, got, tc.want) + } + }) + } +} + +// transportOf returns the *http.Transport backing a fetcher, so a test can +// exercise the SSRF-safe dialer New installed with the operator blocklist. +func transportOf(t *testing.T, f *HTTPFetcher) *http.Transport { + t.Helper() + + transport, ok := f.client.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport is %T, want *http.Transport", f.client.Transport) + } + + return transport +} + +// TestDialerEnforcesBlockedNetworks proves an operator-supplied +// blocked_networks entry is enforced by the dialer, in addition to the +// built-in ranges, while an address outside both stays dialable. +func TestDialerEnforcesBlockedNetworks(t *testing.T) { + t.Parallel() + + cfg := DefaultConfig() + // TEST-NET-2 (198.51.100.0/24) is public to the built-in check, so + // blocking it can only come from the operator-supplied list. + cfg.BlockedNetworks = []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")} + + transport := transportOf(t, New(cfg)) + + blocked := []string{ + "198.51.100.5:80", // operator-supplied range + "10.0.0.5:80", // built-in RFC 1918, still enforced + "100.64.0.1:80", // built-in CGNAT range + } + + for _, addr := range blocked { + t.Run("blocked/"+addr, func(t *testing.T) { + t.Parallel() + + _, err := transport.DialContext(context.Background(), "tcp", addr) + if !errors.Is(err, ErrSSRFBlocked) { + t.Errorf("DialContext(%q) = %v, want ErrSSRFBlocked", addr, err) + } + }) + } + + t.Run("public-not-blocked", func(t *testing.T) { + t.Parallel() + + // A cancelled context makes the dial fail without touching the + // network; the point is only that a public literal outside every + // blocked range is not SSRF-blocked. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := transport.DialContext(ctx, "tcp", testPublicHost+":80") + if errors.Is(err, ErrSSRFBlocked) { + t.Errorf("public target SSRF-blocked with operator list set: %v", err) + } + }) +}