wire: skip IPv4-mapped IPv6 addresses in addrv2 messages
What changed, and why it matters
This commit fixes a network-address handling bug in btcd, a Bitcoin implementation. When another node sent an IPv4 address disguised as an IPv6 address inside newer 'addrv2' peer messages, btcd would accept it. The change makes btcd reject those mismatched addresses, matching the behavior of the main Bitcoin Core software. This prevents inconsistent address records from spreading between nodes and avoids potential confusion or attacks that rely on address format tricks.
Apply the patch and ensure nodes are upgraded. Monitor for any peer behavior changes in addrv2 handling; no immediate incident response is required beyond normal patching.
Security signals we found
Protocol conformance fix for BIP155 addrv2 address encoding
Rejection of IPv4-mapped IPv6 addresses sent under wrong network ID
Differential fuzzing discovery against Bitcoin Core
Potential peer-to-peer address relay inconsistency / de-anonymization vector
No memory corruption or cryptographic weakness evident
Evidence from the diff
The patch adds detection for IPv4-mapped IPv6 addresses (::ffff:0:0/96, RFC 4291) in wire/netaddressv2.go. In readNetAddressV2(), when parsing an addrv2 entry with networkID 0x02 (IPv6), the code now checks isIPv4Mapped() and returns ErrSkippedNetworkID if the 16-byte address begins with the ::ffff:0:0/96 prefix. BIP155 specifies that such addresses must be encoded with networkID 0x01 (IPv4), so this aligns btcd with Bitcoin Core’s addrv2 parser. Tests cover the helper and the skip behavior.
Changed components
wire/netaddressv2.gowire/netaddressv2_test.gobtcd P2P addrv2 message parsingInspect captured patch +92 / −0
diff --git a/wire/netaddressv2.go b/wire/netaddressv2.go
index ccad266..767ef3c 100644
--- a/wire/netaddressv2.go
+++ b/wire/netaddressv2.go
@@ -1,6 +1,7 @@
package wire
import (
+ "bytes"
"encoding/base32"
"encoding/binary"
"fmt"
@@ -69,6 +70,16 @@ func isOnionCatTor(ip net.IP) bool {
return onionCatNet.Contains(ip)
}
+// ipv4MappedPrefix is the prefix for IPv4-mapped IPv6 addresses (::ffff:0:0/96)
+// as defined by RFC 4291.
+var ipv4MappedPrefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
+
+// isIPv4Mapped returns whether a given 16-byte IPv6 address is actually an
+// IPv4-mapped IPv6 address (::ffff:0:0/96).
+func isIPv4Mapped(addr []byte) bool {
+ return bytes.HasPrefix(addr, ipv4MappedPrefix)
+}
+
// NetAddressV2 defines information about a peer on the network including the
// last time it was seen, the services it supports, its address, and port. This
// struct is used in the addrv2 message (MsgAddrV2) and can contain larger
@@ -336,6 +347,12 @@ func readNetAddressV2(r io.Reader, pver uint32, na *NetAddressV2) error {
if isOnionCatTor(addr.addr[:]) {
return ErrSkippedNetworkID
}
+
+ // Skip IPv4-mapped IPv6 addresses (RFC 4291). These addresses
+ // should use networkID 0x01 (IPv4), not 0x02 (IPv6).
+ if isIPv4Mapped(addr.addr[:]) {
+ return ErrSkippedNetworkID
+ }
case torv2:
addr := &torv2Addr{}
addr.netID = torv2
diff --git a/wire/netaddressv2_test.go b/wire/netaddressv2_test.go
index bd5084b..8aea5c8 100644
--- a/wire/netaddressv2_test.go
+++ b/wire/netaddressv2_test.go
@@ -7,6 +7,69 @@ import (
"time"
)
+// TestIsIPv4Mapped tests that isIPv4Mapped correctly identifies IPv4-mapped
+// IPv6 addresses.
+func TestIsIPv4Mapped(t *testing.T) {
+ tests := []struct {
+ name string
+ addr []byte
+ expected bool
+ }{
+ {
+ name: "IPv4-mapped ::ffff:192.0.2.1",
+ addr: []byte{
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x02, 0x01,
+ },
+ expected: true,
+ },
+ {
+ name: "IPv4-mapped ::ffff:127.0.0.1",
+ addr: []byte{
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x01,
+ },
+ expected: true,
+ },
+ {
+ name: "Regular IPv6",
+ addr: []byte{
+ 0x20, 0x01, 0x09, 0xe8, 0x26, 0x15, 0x73, 0x00,
+ 0x09, 0x54, 0x12, 0x63, 0xef, 0xc8, 0x2e, 0x34,
+ },
+ expected: false,
+ },
+ {
+ name: "OnionCat Tor address",
+ addr: []byte{
+ 0xfd, 0x87, 0xd8, 0x7e, 0xeb, 0x43, 0xff, 0xfe,
+ 0xcc, 0x39, 0xa8, 0x73, 0x69, 0x15, 0xff, 0xff,
+ },
+ expected: false,
+ },
+ {
+ name: "Short address (IPv4)",
+ addr: []byte{0x7f, 0x00, 0x00, 0x01},
+ expected: false,
+ },
+ {
+ name: "Empty address",
+ addr: []byte{},
+ expected: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result := isIPv4Mapped(test.addr)
+ if result != test.expected {
+ t.Errorf("isIPv4Mapped(%v) = %v, want %v",
+ test.addr, result, test.expected)
+ }
+ })
+ }
+}
+
// TestNetAddressV2FromBytes tests that NetAddressV2FromBytes works as
// expected.
func TestNetAddressV2FromBytes(t *testing.T) {
@@ -167,6 +230,18 @@ func TestReadNetAddressV2(t *testing.T) {
ErrSkippedNetworkID,
},
+ // IPv4-mapped IPv6 encoding is skipped (::ffff:192.0.2.1).
+ {
+ []byte{
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xff, 0xc0, 0x00, 0x02, 0x01, 0x22,
+ 0x22,
+ },
+ "",
+ ErrSkippedNetworkID,
+ },
+
// Valid ipv6 encoding.
{
[]byte{
Why this scored 51/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.