diff --git a/fetchguard/fetch.go b/fetchguard/fetch.go index fd1d81d..3e5c9de 100644 --- a/fetchguard/fetch.go +++ b/fetchguard/fetch.go @@ -155,6 +155,9 @@ func dialControl(policy Policy) func(network, address string, c syscall.RawConn) if err != nil { return fmt.Errorf("fetchguard: unparseable dial address %q: %w", host, err) } + if addr.Zone() != "" { + return errPrivateIP + } addr = addr.Unmap() if isReservedOrPrivate(addr) { return errPrivateIP diff --git a/fetchguard/fetch_test.go b/fetchguard/fetch_test.go index f50a21a..1e743b2 100644 --- a/fetchguard/fetch_test.go +++ b/fetchguard/fetch_test.go @@ -304,3 +304,12 @@ func withTestLoopback(srv *httptest.Server, p Policy) Policy { p.skipReservedCheck = true return p } + +func TestDialControlRejectsZonedAndSpecialUse(t *testing.T) { + ctl := dialControl(Policy{}) + for _, a := range []string{"[fe80::1%eth0]:443", "198.18.0.1:443"} { + if err := ctl("tcp", a, nil); !errors.Is(err, errPrivateIP) { + t.Errorf("%s: got %v", a, err) + } + } +} diff --git a/fetchguard/ip.go b/fetchguard/ip.go index 681c201..cc90e43 100644 --- a/fetchguard/ip.go +++ b/fetchguard/ip.go @@ -2,24 +2,28 @@ package fetchguard import "net/netip" -// privateV4 is a literal port of ManualCoverUrlFetcher.php PRIVATE_V4_CIDRS. -var privateV4 = []netip.Prefix{ - netip.MustParsePrefix("127.0.0.0/8"), - netip.MustParsePrefix("10.0.0.0/8"), - netip.MustParsePrefix("172.16.0.0/12"), - netip.MustParsePrefix("192.168.0.0/16"), - netip.MustParsePrefix("169.254.0.0/16"), - netip.MustParsePrefix("100.64.0.0/10"), - netip.MustParsePrefix("0.0.0.0/8"), -} +// privateV4 is the IANA IPv4 special-purpose non-public set. It is a strict +// superset of ManualCoverUrlFetcher.php's lists (06-VERIFICATION gap 3). +var privateV4 = mustPrefixes( + "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", + "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", + "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", + "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", +) -// privateV6 is a literal port of PRIVATE_V6_PREFIXES. PHP lists bare "::1" -// as a prefix-less loopback literal; it is expressed here as ::1/128 so -// Prefix.Contains works uniformly with the CIDR entries. -var privateV6 = []netip.Prefix{ - netip.MustParsePrefix("::1/128"), - netip.MustParsePrefix("fe80::/10"), - netip.MustParsePrefix("fc00::/7"), +// privateV6 is the IANA IPv6 special-purpose non-public set. 2002::/16 and +// 64:ff9b::/96 are handled by embeddedTransitionIPv4 instead. +var privateV6 = mustPrefixes( + "::/96", "100::/64", "2001::/23", "2001:db8::/32", "3fff::/20", + "5f00::/16", "fc00::/7", "fe80::/10", "fec0::/10", "ff00::/8", +) + +func mustPrefixes(cidrs ...string) []netip.Prefix { + out := make([]netip.Prefix, len(cidrs)) + for i, c := range cidrs { + out[i] = netip.MustParsePrefix(c) + } + return out } var ( @@ -35,7 +39,7 @@ func isReservedOrPrivate(addr netip.Addr) bool { if !addr.IsValid() { return true } - addr = addr.Unmap() + addr = addr.WithZone("").Unmap() if addr.IsMulticast() || addr.IsUnspecified() { return true } diff --git a/fetchguard/ip_test.go b/fetchguard/ip_test.go index d43a13d..832ed59 100644 --- a/fetchguard/ip_test.go +++ b/fetchguard/ip_test.go @@ -77,3 +77,16 @@ func TestIsReservedOrPrivateIPv6Transitions(t *testing.T) { }) } } + +func TestIsReservedOrPrivateSpecialUseSmoke(t *testing.T) { + for _, s := range []string{"198.18.0.1", "192.0.0.1", "240.0.0.1", "255.255.255.255", "2001:db8::1", "fec0::1", "fe80::1%eth0"} { + if !isReservedOrPrivate(netip.MustParseAddr(s)) { + t.Errorf("%s should be non-public", s) + } + } + for _, s := range []string{"8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"} { + if isReservedOrPrivate(netip.MustParseAddr(s)) { + t.Errorf("%s should be public", s) + } + } +}