addrmgr: fix IsRoutable for IPv6 addresses starting with 0
What changed, and why it matters
This commit fixes a bug in btcd's address manager where certain invalid IPv6 addresses starting with '0:' were incorrectly treated as valid internet-routable addresses. These addresses belong to a reserved block that should never be routed. The fix adds a check to reject them, with a special exception for a specific translated-IPv4 range. Because Bitcoin nodes share peer addresses with each other, accepting such addresses could let an attacker pollute the peer database with unusable or specially crafted entries, potentially degrading network connectivity.
Review whether any cached addrman entries from prior versions contain now-rejected zero-prefix IPv6 addresses and consider whether addrman eviction or bootstrap from trusted seeds is warranted. Ensure the fix is included in the next release and monitor for similar discrepancies in other address classification functions.
Security signals we found
Incorrect routability classification of reserved IPv6 range
Peer address database pollution vector
Differential fuzzing discovery against Bitcoin Core
RFC 4291 reserved address handling
Special-case carve-out for RFC 6145 translated IPv4
Evidence from the diff
The patch modifies addrmgr/network.go to add a zero6Net (0000::/16) definition and an IsZero helper that checks both IPv4 0.0.0.0/8 and IPv6 0000::/16 reserved blocks, while carving out an exception for RFC 6145 translated IPv4 addresses (::ffff:0:0:0/96). IsRoutable is updated to also reject IsZero addresses. Tests are added covering the reported address 0:9881:8181:8181:fe00:a:9e:9801, other zero-prefix cases, the RFC 6145 exclusion, and GroupKey routing classification.
Changed components
addrmgr/network.goaddrmgr/network_test.goIsRoutable functionIsZero helper (new)GroupKey classificationInspect captured patch +125 / −2
diff --git a/addrmgr/network.go b/addrmgr/network.go
index a99d784..66c0743 100644
--- a/addrmgr/network.go
+++ b/addrmgr/network.go
@@ -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)))
}
diff --git a/addrmgr/network_test.go b/addrmgr/network_test.go
index 0b7b33c..f92035d 100644
--- a/addrmgr/network_test.go
+++ b/addrmgr/network_test.go
@@ -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"},
Why this scored 64/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.