addrmgr: fix IsRoutable for IPv6 addresses starting with 0

In this commit, we fix `IsRoutable` to correctly reject IPv6 addresses
whose first 16-bit group is zero (i.e., in the `0000::/16` reserved
block per RFC 4291). Differential fuzzing between btcd and Bitcoin Core
revealed that addresses like `0:9881:8181:8181:fe00🅰️9e:9801` were
slipping through as routable when they shouldn't be.

We add a new `zero6Net` (`0000::/16`) definition alongside the existing
`zero4Net`, and introduce an `IsZero` helper that checks both. The /16
prefix width is intentional: the broader /8 reservation would
incorrectly catch allocated sub-ranges like `0064:ff9b::/96` (RFC 6052,
NAT64). We also carve out an exception for RFC 6145 translated IPv4
addresses (`::ffff:0:0:0/96`), which live within `0000::/16` but are
valid for routing.

Test coverage includes the original bug report address, various
zero-prefix IPv6 addresses, the RFC 6145 exclusion, and corresponding
`GroupKey` entries to confirm they land in the "unroutable" bucket.

Fixes #2431
This commit is contained in:
Eric Grill 2026-01-14 20:14:56 +00:00 committed by Olaoluwa Osuntokun
parent 50f056f3c2
commit 4a4e2d2495
2 changed files with 125 additions and 2 deletions

View file

@ -92,6 +92,13 @@ var (
// (0.0.0.0/8).
zero4Net = ipNet("0.0.0.0", 8, 32)
// zero6Net defines the IPv6 address block for addresses with a zero
// first 16-bit group (0000::/16). These addresses are in the IANA
// reserved range and should not be routable on the public internet.
// We use /16 rather than the full /8 reservation to avoid catching
// allocated sub-ranges like 0064:ff9b::/96 (RFC 6052).
zero6Net = ipNet("::", 16, 128)
// heNet defines the Hurricane Electric IPv6 address block.
heNet = ipNet("2001:470::", 32, 128)
)
@ -113,6 +120,29 @@ func IsLocal(na *wire.NetAddress) bool {
return na.IP.IsLoopback() || zero4Net.Contains(na.IP)
}
// IsZero returns whether or not the given address is in a reserved zero
// address block. This includes IPv4 addresses starting with 0 (0.0.0.0/8)
// and IPv6 addresses with a zero first group (0000::/16). Addresses in
// the RFC 6145 sub-range (::ffff:0:0:0/96) are excluded since they are
// valid translated IPv4 addresses used for routing.
func IsZero(na *wire.NetAddress) bool {
if zero4Net.Contains(na.IP) {
return true
}
if zero6Net.Contains(na.IP) {
// Exclude RFC 6145 (::ffff:0:0:0/96) translated IPv4
// addresses which are allocated within the zero block.
if rfc6145Net.Contains(na.IP) {
return false
}
return true
}
return false
}
// IsOnionCatTor returns whether or not the passed address is in the IPv6 range
// used by bitcoin to support Tor (fd87:d87e:eb43::/48). Note that this range
// is the same range used by OnionCat, which is part of the RFC4193 unique local
@ -244,7 +274,8 @@ func IsRoutable(na *wire.NetAddressV2) bool {
return IsValid(lna) && !(IsRFC1918(lna) || IsRFC2544(lna) ||
IsRFC3927(lna) || IsRFC4862(lna) || IsRFC3849(lna) ||
IsRFC4843(lna) || IsRFC7343(lna) || IsRFC5737(lna) ||
IsRFC6598(lna) || IsLocal(lna) || (IsRFC4193(lna) &&
IsRFC6598(lna) || IsLocal(lna) || IsZero(lna) ||
(IsRFC4193(lna) &&
!IsOnionCatTor(lna)))
}

View file

@ -79,7 +79,7 @@ func TestIPTypes(t *testing.T) {
newIPTest("64:ff9b::1", false, false, false, false, false, false,
false, false, false, false, true, false, false, false, false, true, true),
newIPTest("::ffff:abcd:ef12:1", false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false, true, true),
false, false, false, false, false, false, false, false, false, true, false),
newIPTest("::1", false, false, false, false, false, false, false, false,
false, false, false, false, false, false, true, true, false),
newIPTest("198.18.0.1", false, true, false, false, false, false, false,
@ -88,6 +88,27 @@ func TestIPTypes(t *testing.T) {
false, false, false, false, false, true, false, false, true, false),
newIPTest("203.0.113.1", false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, true, false),
// IPv6 zero-prefix addresses (0000::/16 reserved range).
// These should NOT be routable per RFC 4291.
newIPTest("0:9881:8181:8181:fe00:a:9e:9801", false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, true, false),
newIPTest("0::1", false, false, false, false, false, false, false,
false, false, false, false, false, false, false, true,
true, false),
newIPTest("0:1::1", false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, true, false),
newIPTest("0:ffff::1", false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, true, false),
// RFC 6145 translated IPv4 addresses (::ffff:0:0:0/96) are
// within 0000::/16 but should still be routable.
newIPTest("::ffff:0:0c01:0203", false, false, false, false,
false, false, false, false, false, false, false, true,
false, false, false, true, true),
}
t.Logf("Running %d tests", len(tests))
@ -153,6 +174,75 @@ func TestIPTypes(t *testing.T) {
}
}
// TestIsZero ensures the IsZero function correctly identifies addresses in the
// reserved zero address blocks while excluding allocated sub-ranges.
func TestIsZero(t *testing.T) {
tests := []struct {
name string
ip string
want bool
}{
// IPv4 zero addresses (0.0.0.0/8).
{name: "ipv4 zero", ip: "0.0.0.0", want: true},
{name: "ipv4 zero prefix", ip: "0.1.2.3", want: true},
{name: "ipv4 zero 0.255.255.255", ip: "0.255.255.255", want: true},
// IPv4 non-zero addresses.
{name: "ipv4 normal", ip: "1.2.3.4", want: false},
{name: "ipv4 loopback", ip: "127.0.0.1", want: false},
{name: "ipv4 private", ip: "192.168.1.1", want: false},
// IPv6 zero-prefix addresses (0000::/16 reserved).
{name: "ipv6 zero all", ip: "::", want: true},
{name: "ipv6 loopback", ip: "::1", want: true},
{name: "ipv6 zero prefix 0::2", ip: "0::2", want: true},
{name: "ipv6 zero prefix 0:1::1", ip: "0:1::1", want: true},
{name: "ipv6 zero prefix 0:ffff::1", ip: "0:ffff::1", want: true},
{
name: "ipv6 zero prefix from bug report",
ip: "0:9881:8181:8181:fe00:a:9e:9801",
want: true,
},
{
name: "ipv6 zero prefix unallocated",
ip: "::ffff:abcd:ef12:1",
want: true,
},
// RFC 6145 addresses (::ffff:0:0:0/96) — allocated sub-range
// of 0000::/16 that should NOT be flagged as zero.
{
name: "ipv6 rfc6145 translated ipv4",
ip: "::ffff:0:0c01:0203",
want: false,
},
{
name: "ipv6 rfc6145 another",
ip: "::ffff:0:1.2.3.4",
want: false,
},
// IPv6 non-zero-prefix addresses.
{name: "ipv6 normal", ip: "2602:100::1", want: false},
{name: "ipv6 rfc6052", ip: "64:ff9b::1", want: false},
{name: "ipv6 private", ip: "fd00:dead::1", want: false},
{name: "ipv6 teredo", ip: "2001::1", want: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
nip := net.ParseIP(test.ip)
na := wire.NewNetAddressIPPort(
nip, 8333, wire.SFNodeNetwork,
)
if got := addrmgr.IsZero(na); got != test.want {
t.Errorf("IsZero(%s) = %v, want %v",
test.ip, got, test.want)
}
})
}
}
// TestGroupKey tests the GroupKey function to ensure it properly groups various
// IP addresses.
func TestGroupKey(t *testing.T) {
@ -178,6 +268,8 @@ func TestGroupKey(t *testing.T) {
{name: "ipv6 rfc4843 2001:10::/28", ip: "2001:10::1234", expected: "unroutable"},
{name: "ipv6 rfc7343 2001:20::/28", ip: "2001:20::1234", expected: "unroutable"},
{name: "ipv6 rfc4862 fe80::/64", ip: "fe80::1234", expected: "unroutable"},
{name: "ipv6 zero prefix 0::/16", ip: "0:9881:8181:8181:fe00:a:9e:9801", expected: "unroutable"},
{name: "ipv6 zero prefix 0:1::1", ip: "0:1::1", expected: "unroutable"},
// IPv4 normal.
{name: "ipv4 normal class a", ip: "12.1.2.3", expected: "12.1.0.0"},