package netutil import ( "net" "testing" ) func TestIsBlockedIP(t *testing.T) { blocked := []struct { name string ip string }{ // IPv4 loopback {"loopback 127.0.0.1", "127.0.0.1"}, {"loopback 127.0.0.2", "127.0.0.2"}, {"loopback 127.255.255.255", "127.255.255.255"}, // IPv4 unspecified {"unspecified 0.0.0.0", "0.0.0.0"}, {"unspecified 0.1.2.3", "0.1.2.3"}, // RFC1918 {"RFC1918 10.0.0.1", "10.0.0.1"}, {"RFC1918 10.255.255.255", "10.255.255.255"}, {"RFC1918 172.16.0.1", "172.16.0.1"}, {"RFC1918 172.31.255.255", "172.31.255.255"}, {"RFC1918 192.168.0.1", "192.168.0.1"}, {"RFC1918 192.168.255.255", "192.168.255.255"}, // Link-local (APIPA / AWS metadata) {"link-local 169.254.0.1", "169.254.0.1"}, {"link-local 169.254.169.254", "169.254.169.254"}, // Shared address space (carrier-grade NAT) {"CGN 100.64.0.1", "100.64.0.1"}, {"CGN 100.127.255.255", "100.127.255.255"}, // Multicast {"multicast 224.0.0.1", "224.0.0.1"}, {"multicast 239.255.255.255", "239.255.255.255"}, // Reserved {"reserved 240.0.0.1", "240.0.0.1"}, {"broadcast 255.255.255.255", "255.255.255.255"}, // IPv6 loopback {"IPv6 loopback ::1", "::1"}, // IPv6 unspecified {"IPv6 unspecified ::", "::"}, // IPv6 link-local {"IPv6 link-local fe80::1", "fe80::1"}, {"IPv6 link-local fe80::dead:beef", "fe80::dead:beef"}, // IPv6 ULA {"IPv6 ULA fc00::1", "fc00::1"}, {"IPv6 ULA fd00::1", "fd00::1"}, // IPv6 multicast {"IPv6 multicast ff02::1", "ff02::1"}, } for _, tc := range blocked { t.Run(tc.name, func(t *testing.T) { ip := net.ParseIP(tc.ip) if ip == nil { t.Fatalf("failed to parse IP %q", tc.ip) } if !IsBlockedIP(ip) { t.Errorf("IsBlockedIP(%q) = false, want true", tc.ip) } }) } allowed := []struct { name string ip string }{ {"public 8.8.8.8", "8.8.8.8"}, {"public 1.1.1.1", "1.1.1.1"}, {"public 198.51.100.1", "198.51.100.1"}, // RFC5737 TEST-NET-2 — a documentation-only range; // not assigned to any real host, but intentionally left unblocked here because // it has no special routing treatment (unlike RFC1918/loopback/link-local) and // blocking it would require tracking every RFC5737 range without meaningful // security benefit (no server should ever listen on a TEST-NET address). {"public 151.101.1.1", "151.101.1.1"}, // Fastly {"public IPv6 2001:4860:4860::8888", "2001:4860:4860::8888"}, // Google DNS {"public IPv6 2606:4700:4700::1111", "2606:4700:4700::1111"}, // Cloudflare DNS } for _, tc := range allowed { t.Run(tc.name, func(t *testing.T) { ip := net.ParseIP(tc.ip) if ip == nil { t.Fatalf("failed to parse IP %q", tc.ip) } if IsBlockedIP(ip) { t.Errorf("IsBlockedIP(%q) = true, want false", tc.ip) } }) } } func TestIsBlockedIPv6MappedIPv4(t *testing.T) { // ::ffff:192.168.1.1 is an IPv6-mapped IPv4 address — should be blocked as RFC1918. // Construct it manually as a 16-byte IP. mapped := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1} if !IsBlockedIP(mapped) { t.Errorf("IsBlockedIP(::ffff:192.168.1.1) = false, want true (IPv6-mapped IPv4 must be normalized)") } // ::ffff:8.8.8.8 — IPv6-mapped public IP — should be allowed. mappedPublic := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 8, 8, 8, 8} if IsBlockedIP(mappedPublic) { t.Errorf("IsBlockedIP(::ffff:8.8.8.8) = true, want false") } } func TestIsBlockedIPEdgeCases(t *testing.T) { // The boundary between RFC1918 and public ranges. // 172.15.255.255 is NOT private (just below 172.16.0.0/12). notPrivate := net.ParseIP("172.15.255.255") if IsBlockedIP(notPrivate) { t.Errorf("IsBlockedIP(172.15.255.255) = true, want false (outside 172.16.0.0/12)") } // 172.32.0.0 is NOT private (just above 172.31.255.255). notPrivate2 := net.ParseIP("172.32.0.0") if IsBlockedIP(notPrivate2) { t.Errorf("IsBlockedIP(172.32.0.0) = true, want false (outside 172.16.0.0/12)") } // CGN: 100.63.255.255 is NOT in 100.64.0.0/10. notCGN := net.ParseIP("100.63.255.255") if IsBlockedIP(notCGN) { t.Errorf("IsBlockedIP(100.63.255.255) = true, want false (outside 100.64.0.0/10)") } // CGN: 100.128.0.0 is NOT in 100.64.0.0/10. notCGN2 := net.ParseIP("100.128.0.0") if IsBlockedIP(notCGN2) { t.Errorf("IsBlockedIP(100.128.0.0) = true, want false (outside 100.64.0.0/10)") } } // TestBlockedCIDRsValid verifies that all entries in blockedCIDRStrings parse // successfully. This catches programming errors in the CIDR list without // requiring a startup panic. The init() function records parse failures in // blockedCIDRParseErrors rather than panicking; this test makes those failures // visible as test failures during CI. func TestBlockedCIDRsValid(t *testing.T) { for _, msg := range BlockedCIDRParseErrors() { t.Errorf("CIDR parse error: %s", msg) } }