diff --git a/addrmgr/addrmanager.go b/addrmgr/addrmanager.go index d5631b9e89..402e433005 100644 --- a/addrmgr/addrmanager.go +++ b/addrmgr/addrmanager.go @@ -6,12 +6,14 @@ package addrmgr import ( + "encoding/base32" "encoding/binary" "encoding/json" "fmt" "net" "os" "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -53,6 +55,14 @@ type AddrManager struct { // Tried addresses are addresses that have been tested. addrTried [triedBucketCount][]*KnownAddress + // addrNewStats maintains statistics about addresses in each + // new bucket. + addrNewStats [newBucketCount]bucketStats + + // addrTriedStats maintains statistics about addresses in each + // tried bucket. + addrTriedStats [triedBucketCount]bucketStats + // addrChanged signals whether the address manager needs to have its state // serialized and saved to the file system. addrChanged bool @@ -95,6 +105,21 @@ type AddrManager struct { triedBucketSize int } +// addrTypeFilter specifies the wanted network address types for address +// selection. +type addrTypeFilter struct { + wantIPv4 bool + wantIPv6 bool + wantTORv3 bool +} + +// bucketStats tracks the number of addresses by type within a single bucket. +type bucketStats struct { + numIPv4 uint16 + numIPv6 uint16 + numTORv3 uint16 +} + // serializedKnownAddress is used to represent the serializable state of a // known address. It excludes convenience fields that can be derived from the // address manager's state. @@ -217,6 +242,59 @@ const ( serialisationVersion = 1 ) +// increment increases the count for the given address type in the bucket counts. +func (bs *bucketStats) increment(addrType NetAddressType) { + switch addrType { + case IPv4Address: + bs.numIPv4++ + case IPv6Address: + bs.numIPv6++ + case TORv3Address: + bs.numTORv3++ + } +} + +// decrement decreases the count for the given address type in the bucket counts. +func (bs *bucketStats) decrement(addrType NetAddressType) { + switch addrType { + case IPv4Address: + bs.numIPv4-- + case IPv6Address: + bs.numIPv6-- + case TORv3Address: + bs.numTORv3-- + } +} + +// total returns the sum of address counts matching the filter. +func (bs *bucketStats) total(filter addrTypeFilter) int { + sum := 0 + if filter.wantIPv4 { + sum += int(bs.numIPv4) + } + if filter.wantIPv6 { + sum += int(bs.numIPv6) + } + if filter.wantTORv3 { + sum += int(bs.numTORv3) + } + return sum +} + +// matches returns true if the bucket statistics have any addresses matching the filter. +func (bs *bucketStats) matches(filter addrTypeFilter) bool { + return (filter.wantIPv4 && bs.numIPv4 > 0) || + (filter.wantIPv6 && bs.numIPv6 > 0) || + (filter.wantTORv3 && bs.numTORv3 > 0) +} + +// matches returns true if the address type matches the filter criteria. +func (f addrTypeFilter) matches(addrType NetAddressType) bool { + return (f.wantIPv4 && addrType == IPv4Address) || + (f.wantIPv6 && addrType == IPv6Address) || + (f.wantTORv3 && addrType == TORv3Address) +} + // addOrUpdateAddress is a helper function to either update an address already known // to the address manager, or to add the address if not already known. func (a *AddrManager) addOrUpdateAddress(netAddr, srcAddr *NetAddress) { @@ -290,6 +368,7 @@ func (a *AddrManager) addOrUpdateAddress(netAddr, srcAddr *NetAddress) { // Add to new bucket. ka.refs++ a.addrNew[bucket][addrKey] = ka + a.addrNewStats[bucket].increment(netAddr.Type) a.addrChanged = true log.Tracef("Added new address %s for a total of %d addresses", addrKey, @@ -309,6 +388,7 @@ func (a *AddrManager) expireNew(bucket int) { if v.isBad() { log.Tracef("expiring bad address %v", k) delete(a.addrNew[bucket], k) + a.addrNewStats[bucket].decrement(v.na.Type) a.addrChanged = true v.refs-- if v.refs == 0 { @@ -329,6 +409,7 @@ func (a *AddrManager) expireNew(bucket int) { log.Tracef("expiring oldest address %v", key) delete(a.addrNew[bucket], key) + a.addrNewStats[bucket].decrement(oldest.na.Type) a.addrChanged = true oldest.refs-- if oldest.refs == 0 { @@ -567,6 +648,7 @@ func (a *AddrManager) deserializePeers(filePath string) error { } ka.refs++ a.addrNew[i][val] = ka + a.addrNewStats[i].increment(ka.na.Type) } } for i := range sam.TriedBuckets { @@ -580,6 +662,7 @@ func (a *AddrManager) deserializePeers(filePath string) error { ka.tried = true a.nTried++ a.addrTried[i] = append(a.addrTried[i], ka) + a.addrTriedStats[i].increment(ka.na.Type) } } @@ -730,6 +813,8 @@ func (a *AddrManager) reset() { for i := range a.addrTried { a.addrTried[i] = nil } + a.addrTriedStats = [triedBucketCount]bucketStats{} + a.addrNewStats = [newBucketCount]bucketStats{} a.addrChanged = true a.getNewBucket = func(netAddr, srcAddr *NetAddress) int { return getNewBucket(a.key, netAddr, srcAddr) @@ -744,6 +829,20 @@ func (a *AddrManager) reset() { // returns the result. If the host string is not recognized as any known type, // then an unknown address type is returned without error. func EncodeHost(host string) (NetAddressType, []byte) { + // Check if this is a valid TORv3 address. + if len(host) == 62 && strings.HasSuffix(host, ".onion") { + // TORv3 addresses tend to be lowercase by convention, but + // Go's base32.StdEncoding.DecodeString expects uppercase + // input. Convert to uppercase for successful decoding. + torAddressBytes, err := base32.StdEncoding.DecodeString( + strings.ToUpper(host[:56])) + if err == nil { + if pubkey, valid := isTORv3(torAddressBytes); valid { + return TORv3Address, pubkey[:] + } + } + } + // Look for IPv4 or IPv6 addresses if ip := net.ParseIP(host); ip != nil { if isIPv4(ip) { @@ -757,12 +856,12 @@ func EncodeHost(host string) (NetAddressType, []byte) { } // GetAddress returns a single address that should be routable. It picks a -// random one from the possible addresses with preference given to ones that -// have not been used recently and should not pick 'close' addresses -// consecutively. +// random one from the possible addresses that satisfy the provided filter +// with preference given to ones that have not been used recently and should +// not pick 'close' addresses consecutively. // // This function is safe for concurrent access. -func (a *AddrManager) GetAddress() *KnownAddress { +func (a *AddrManager) GetAddress(filterFn NetAddressTypeFilter) *KnownAddress { a.mtx.Lock() defer a.mtx.Unlock() @@ -770,21 +869,68 @@ func (a *AddrManager) GetAddress() *KnownAddress { return nil } + filter := addrTypeFilter{ + wantIPv4: filterFn(IPv4Address), + wantIPv6: filterFn(IPv6Address), + wantTORv3: filterFn(TORv3Address), + } + + if !filter.wantIPv4 && !filter.wantIPv6 && !filter.wantTORv3 { + return nil + } + + // Collect indices of tried and new buckets that match the filter. + var triedBucketIdxsBuf [triedBucketCount]int + var newBucketIdxsBuf [newBucketCount]int + triedBucketIdxs := triedBucketIdxsBuf[:0] + newBucketIdxs := newBucketIdxsBuf[:0] + for i := range a.addrTriedStats { + if a.addrTriedStats[i].matches(filter) { + triedBucketIdxs = append(triedBucketIdxs, i) + } + } + for i := range a.addrNewStats { + if a.addrNewStats[i].matches(filter) { + newBucketIdxs = append(newBucketIdxs, i) + } + } + + numTried := len(triedBucketIdxs) + numNew := len(newBucketIdxs) + + // Return early if no buckets match the filter. + if numTried == 0 && numNew == 0 { + return nil + } + // Use a 50% chance for choosing between tried and new table entries. large := 1 << 30 factor := 1.0 - if a.nTried > 0 && (a.nNew == 0 || rand.IntN(2) == 0) { + if numTried > 0 && (numNew == 0 || rand.IntN(2) == 0) { // Tried entry. for { - // Pick a random bucket. - bucket := rand.IntN(len(a.addrTried)) - if len(a.addrTried[bucket]) == 0 { - continue - } + // Pick a random bucket from buckets matching the filter. + bucketIdx := triedBucketIdxs[rand.IntN(numTried)] + bucket := a.addrTried[bucketIdx] - // Then, a random entry in the list. - randEntry := rand.IntN(len(a.addrTried[bucket])) - ka := a.addrTried[bucket][randEntry] + // Calculate total number of tried addresses + // matching the filter, then pick a random entry. + counts := a.addrTriedStats[bucketIdx] + totalMatching := counts.total(filter) + nth := rand.IntN(totalMatching) + + // Find the nth address matching the filter. + var ka *KnownAddress + for _, addr := range bucket { + if !filter.matches(addr.na.Type) { + continue + } + if nth == 0 { + ka = addr + break + } + nth-- + } randval := rand.IntN(large) if float64(randval) < (factor * ka.chance() * float64(large)) { @@ -796,22 +942,29 @@ func (a *AddrManager) GetAddress() *KnownAddress { } else { // New node. for { - // Pick a random bucket. - bucket := rand.IntN(len(a.addrNew)) - if len(a.addrNew[bucket]) == 0 { - continue - } + // Pick a random bucket from the buckets matching the filter. + bucketIdx := newBucketIdxs[rand.IntN(numNew)] + bucket := a.addrNew[bucketIdx] - // Then, a random entry in it. + // Calculate total number of new addresses + // matching the filter, then pick a random entry. + bucketStats := a.addrNewStats[bucketIdx] + totalMatching := bucketStats.total(filter) + nth := rand.IntN(totalMatching) + + // Find the nth address matching the filter. var ka *KnownAddress - nth := rand.IntN(len(a.addrNew[bucket])) - for _, value := range a.addrNew[bucket] { + for _, addr := range bucket { + if !filter.matches(addr.na.Type) { + continue + } if nth == 0 { - ka = value + ka = addr break } nth-- } + randval := rand.IntN(large) if float64(randval) < (factor * ka.chance() * float64(large)) { log.Tracef("Selected %s from new bucket", ka.na) @@ -917,6 +1070,7 @@ func (a *AddrManager) Good(addr *NetAddress) error { // we check for existence so we can record the first one if _, ok := a.addrNew[i][addrKey]; ok { delete(a.addrNew[i], addrKey) + a.addrNewStats[i].decrement(ka.na.Type) a.addrChanged = true ka.refs-- if addrNewAvailableIndex == -1 { @@ -938,6 +1092,7 @@ func (a *AddrManager) Good(addr *NetAddress) error { if len(a.addrTried[bucket]) < a.triedBucketSize { ka.tried = true a.addrTried[bucket] = append(a.addrTried[bucket], ka) + a.addrTriedStats[bucket].increment(ka.na.Type) a.addrChanged = true a.nTried++ return nil @@ -960,6 +1115,8 @@ func (a *AddrManager) Good(addr *NetAddress) error { // Replace oldest tried address in bucket with ka. ka.tried = true a.addrTried[bucket][oldestTriedIndex] = ka + a.addrTriedStats[bucket].decrement(rmka.na.Type) + a.addrTriedStats[bucket].increment(ka.na.Type) rmka.tried = false rmka.refs++ @@ -975,6 +1132,7 @@ func (a *AddrManager) Good(addr *NetAddress) error { // We made sure there is space here just above. a.addrNew[newBucket][rmkey] = rmka + a.addrNewStats[newBucket].increment(rmka.na.Type) return nil } @@ -1082,6 +1240,9 @@ const ( // Ipv6Strong represents a connection state between two IPv6 addresses. Ipv6Strong + + // Private represents a connection state between two TORv3 addresses. + Private ) // getRemoteReachabilityFromLocal returns the type of connection reachability @@ -1093,6 +1254,16 @@ func getRemoteReachabilityFromLocal(localAddr, remoteAddr *NetAddress) NetAddres case !remoteAddr.IsRoutable(): return Unreachable + case remoteAddr.Type == TORv3Address: + switch { + case localAddr.Type == TORv3Address: + return Private + case localAddr.IsRoutable() && localAddr.Type == IPv4Address: + return Ipv4 + default: + return Default + } + case isRFC4380(remoteAddr.IP): switch { case !localAddr.IsRoutable(): @@ -1109,6 +1280,8 @@ func getRemoteReachabilityFromLocal(localAddr, remoteAddr *NetAddress) NetAddres switch { case localAddr.IsRoutable() && localAddr.Type == IPv4Address: return Ipv4 + case localAddr.Type == TORv3Address: + return Ipv4 default: return Unreachable } @@ -1121,6 +1294,8 @@ func getRemoteReachabilityFromLocal(localAddr, remoteAddr *NetAddress) NetAddres return Teredo case localAddr.Type == IPv4Address: return Ipv4 + case localAddr.Type == TORv3Address: + return Ipv6Strong // Is our IPv6 tunneled? case isRFC3964(localAddr.IP) || isRFC6052(localAddr.IP) || diff --git a/addrmgr/addrmanager_test.go b/addrmgr/addrmanager_test.go index 3f6609af2c..2310ec9574 100644 --- a/addrmgr/addrmanager_test.go +++ b/addrmgr/addrmanager_test.go @@ -23,6 +23,7 @@ const ( routableIPv6Addr = "2003::" nonRoutableIPv4Addr = "255.255.255.255" nonRoutableIPv6Addr = "::1" + torv3Host = "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion" ) // natfAny defines a filter that will allow network addresses of any type. @@ -30,11 +31,21 @@ func natfAny(addrType NetAddressType) bool { return true } +// natfOnlyIPv4 defines a filter that will only allow IPv4 netAddrs. +func natfOnlyIPv4(addrType NetAddressType) bool { + return addrType == IPv4Address +} + // natfOnlyIPv6 defines a filter that will only allow IPv6 netAddrs. func natfOnlyIPv6(addrType NetAddressType) bool { return addrType == IPv6Address } +// natfOnlyTORv3 defines a filter that will only allow TORv3 netAddrs. +func natfOnlyTORv3(addrType NetAddressType) bool { + return addrType == TORv3Address +} + // addAddressByIP is a convenience function that adds an address to the // address manager given a valid string representation of an ip address and // a port. @@ -50,7 +61,7 @@ func (a *AddrManager) addAddressByIP(addr string, port uint16) { func TestAddOrUpdateAddress(t *testing.T) { amgr := New("testaddaddressupdate") amgr.Start() - if ka := amgr.GetAddress(); ka != nil { + if ka := amgr.GetAddress(natfAny); ka != nil { t.Fatal("address manager should contain no addresses") } @@ -61,7 +72,7 @@ func TestAddOrUpdateAddress(t *testing.T) { } na := NewNetAddressFromIPPort(net.ParseIP(nonRoutableIPv4Addr), 8333, 0) amgr.addOrUpdateAddress(na, na) - if ka := amgr.GetAddress(); ka != nil { + if ka := amgr.GetAddress(natfAny); ka != nil { t.Fatal("address manager should contain no addresses") } @@ -72,7 +83,7 @@ func TestAddOrUpdateAddress(t *testing.T) { } na = NewNetAddressFromIPPort(net.ParseIP(routableIPv4Addr), 8333, 0) amgr.addOrUpdateAddress(na, na) - ka := amgr.GetAddress() + ka := amgr.GetAddress(natfAny) newlyAddedAddr := ka.NetAddress() if ka == nil { t.Fatal("address manager should contain newly added known address") @@ -94,7 +105,7 @@ func TestAddOrUpdateAddress(t *testing.T) { // The address should be in the address manager with a new timestamp. // The network address reference held by the known address should also // differ. - updatedKnownAddress := amgr.GetAddress() + updatedKnownAddress := amgr.GetAddress(natfAny) netAddrFromUpdate := updatedKnownAddress.NetAddress() if updatedKnownAddress == nil { t.Fatal("address manager should contain updated known address") @@ -230,7 +241,7 @@ func TestStartStop(t *testing.T) { amgr = New(dir) amgr.Start() - knownAddress := amgr.GetAddress() + knownAddress := amgr.GetAddress(natfAny) if knownAddress == nil { t.Fatal("address manager should contain known address") } @@ -338,13 +349,13 @@ func TestGetAddress(t *testing.T) { n := New("testgetaddress") // Get an address from an empty set (should error). - if rv := n.GetAddress(); rv != nil { + if rv := n.GetAddress(natfAny); rv != nil { t.Fatalf("GetAddress failed - got: %v, want: %v", rv, nil) } // Add a new address and get it. n.addAddressByIP(routableIPv4Addr, 8333) - ka := n.GetAddress() + ka := n.GetAddress(natfAny) if ka == nil { t.Fatal("did not get an address where there is one in the pool") } @@ -363,7 +374,7 @@ func TestGetAddress(t *testing.T) { // Verify that the previously added address still exists in the address // manager after being marked as good. - ka = n.GetAddress() + ka = n.GetAddress(natfAny) if ka == nil { t.Fatal("did not get an address when one was expected") } @@ -394,7 +405,7 @@ func TestAttempt(t *testing.T) { // Add a new address and get it. n.addAddressByIP(routableIPv4Addr, 8333) - ka := n.GetAddress() + ka := n.GetAddress(natfAny) if !ka.LastAttempt().IsZero() { t.Fatal("address should not have been attempted") @@ -425,7 +436,7 @@ func TestConnected(t *testing.T) { // Add a new address and get it n.addAddressByIP(routableIPv4Addr, 8333) - ka := n.GetAddress() + ka := n.GetAddress(natfAny) na := ka.NetAddress() // make it an hour ago na.Timestamp = time.Unix(time.Now().Add(time.Hour*-1).Unix(), 0) @@ -630,7 +641,7 @@ func TestSetServices(t *testing.T) { // Ensure that the services field for a network address returned from the // address manager is not mutated by a call to SetServices. - knownAddress := addressManager.GetAddress() + knownAddress := addressManager.GetAddress(natfAny) if knownAddress == nil { t.Fatal("expected known address, got nil") } @@ -663,39 +674,44 @@ func TestSetServices(t *testing.T) { func TestAddLocalAddress(t *testing.T) { var tests = []struct { name string - ip net.IP + host string priority AddressPriority valid bool }{{ name: "non-routable local IPv4 address", - ip: net.ParseIP("192.168.0.100"), + host: "192.168.0.100", priority: InterfacePrio, valid: false, }, { name: "routable IPv4 address", - ip: net.ParseIP("204.124.1.1"), + host: "204.124.1.1", priority: InterfacePrio, valid: true, }, { name: "routable IPv4 address with bound priority", - ip: net.ParseIP("204.124.1.1"), + host: "204.124.1.1", priority: BoundPrio, valid: true, }, { name: "non-routable local IPv6 address", - ip: net.ParseIP("::1"), + host: "::1", priority: InterfacePrio, valid: false, }, { name: "non-routable local IPv6 address 2", - ip: net.ParseIP("fe80::1"), + host: "fe80::1", priority: InterfacePrio, valid: false, }, { name: "routable IPv6 address", - ip: net.ParseIP("2620:100::1"), + host: "2620:100::1", priority: InterfacePrio, valid: true, + }, { + name: "routable TORv3 address", + host: torv3Host, + priority: ManualPrio, + valid: true, }} const testPort = 8333 @@ -704,7 +720,14 @@ func TestAddLocalAddress(t *testing.T) { amgr := New("testaddlocaladdress") validLocalAddresses := make(map[string]struct{}) for _, test := range tests { - netAddr := NewNetAddressFromIPPort(test.ip, testPort, testServices) + addrType, addrBytes := EncodeHost(test.host) + netAddr, err := NewNetAddressFromParams(addrType, addrBytes, testPort, + time.Unix(time.Now().Unix(), 0), testServices) + if err != nil { + t.Fatalf("%q: failed to create NetAddress: %v", test.name, err) + return + } + result := amgr.AddLocalAddress(netAddr, test.priority) if result == nil && !test.valid { t.Errorf("%q: address should have been accepted", test.name) @@ -732,8 +755,12 @@ func TestAddLocalAddress(t *testing.T) { // Ensure that all of the addresses that were expected to be added to the // address manager are also returned from a call to LocalAddresses. for _, localAddr := range amgr.LocalAddresses() { - localAddrIP := net.ParseIP(localAddr.Address) - netAddr := NewNetAddressFromIPPort(localAddrIP, testPort, testServices) + addrType, addrBytes := EncodeHost(localAddr.Address) + netAddr, err := NewNetAddressFromParams(addrType, addrBytes, testPort, + time.Unix(time.Now().Unix(), 0), testServices) + if err != nil { + t.Fatalf("failed to create NetAddress from LocalAddr: %v", err) + } netAddrKey := netAddr.Key() if _, ok := validLocalAddresses[netAddrKey]; !ok { t.Errorf("expected to find local address with key %v", netAddrKey) @@ -760,12 +787,21 @@ func TestGetBestLocalAddress(t *testing.T) { newAddressFromIP(net.ParseIP("2001:470::1")), } + // TORv3 address. + torAddrType, torAddrBytes := EncodeHost(torv3Host) + torAddr, err := NewNetAddressFromParams(torAddrType, torAddrBytes, 0, + time.Unix(time.Now().Unix(), 0), wire.SFNodeNetwork) + if err != nil { + t.Fatalf("failed to create TORv3 NetAddress: %v", err) + } + var tests = []struct { remoteAddr *NetAddress want0 *NetAddress want1 *NetAddress want2 *NetAddress want3 *NetAddress + want4 *NetAddress }{{ // Remote connection from public IPv4. newAddressFromIP(net.ParseIP("204.124.8.1")), @@ -773,6 +809,7 @@ func TestGetBestLocalAddress(t *testing.T) { newAddressFromIP(net.IPv4zero), newAddressFromIP(net.ParseIP("204.124.8.100")), newAddressFromIP(net.IPv4zero), + torAddr, }, { // Remote connection from private IPv4. newAddressFromIP(net.ParseIP("172.16.0.254")), @@ -780,6 +817,7 @@ func TestGetBestLocalAddress(t *testing.T) { newAddressFromIP(net.IPv4zero), newAddressFromIP(net.IPv4zero), newAddressFromIP(net.IPv4zero), + newAddressFromIP(net.IPv4zero), }, { // Remote connection from public IPv6. newAddressFromIP(net.ParseIP("2602:100:abcd::102")), @@ -787,6 +825,7 @@ func TestGetBestLocalAddress(t *testing.T) { newAddressFromIP(net.IPv6zero), newAddressFromIP(net.ParseIP("2001:470::1")), newAddressFromIP(net.ParseIP("2001:470::1")), + torAddr, }} amgr := New("testgetbestlocaladdress") @@ -852,21 +891,18 @@ func TestGetBestLocalAddress(t *testing.T) { continue } } - /* - // Add a Tor generated IP address - localAddr = wire.NetAddress{IP: net.ParseIP("fd87:d87e:eb43:25::1")} - amgr.AddLocalAddress(&localAddr, ManualPrio) - - // Test against want3 - for x, test := range tests { - got := amgr.GetBestLocalAddress(&test.remoteAddr) - if !test.want3.IP.Equal(got.IP) { - t.Errorf("TestGetBestLocalAddress test3 #%d failed for remote address %s: want %s got %s", - x, test.remoteAddr.IP, test.want3.IP, got.IP) - continue - } + + // Test4: Add TORv3 address with ManualPrio + amgr.AddLocalAddress(torAddr, ManualPrio) + for x, test := range tests { + remoteAddr := test.remoteAddr + want := test.want4 + got := amgr.GetBestLocalAddress(remoteAddr, natfAny) + if got.Type != want.Type || !reflect.DeepEqual(got.IP, want.IP) { + t.Errorf("TestGetBestLocalAddress test4 #%d failed for remote address %s: want %s, got %s", + x, remoteAddr, want, got) } - */ + } } // TestIsExternalAddrCandidate makes sure that when a remote peer suggests that @@ -982,14 +1018,67 @@ func TestIsExternalAddrCandidate(t *testing.T) { remoteAddr: routableIPv6Addr, expectedBool: true, expectedReach: Ipv6Weak, + }, { + name: "torv3 to torv3", + localAddr: torAddress, + remoteAddr: torAddress, + expectedBool: false, + expectedReach: Private, + }, { + name: "routable ipv4 to torv3", + localAddr: routableIPv4Addr, + remoteAddr: torAddress, + expectedBool: true, + expectedReach: Ipv4, + }, { + name: "non-routable ipv4 to torv3", + localAddr: nonRoutableIPv4Addr, + remoteAddr: torAddress, + expectedBool: false, + expectedReach: Default, + }, { + name: "routable ipv6 to torv3", + localAddr: routableIPv6Addr, + remoteAddr: torAddress, + expectedBool: true, + expectedReach: Default, + }, { + name: "non-routable ipv6 to torv3", + localAddr: nonRoutableIPv6Addr, + remoteAddr: torAddress, + expectedBool: false, + expectedReach: Default, + }, { + name: "torv3 to routable ipv4", + localAddr: torAddress, + remoteAddr: routableIPv4Addr, + expectedBool: false, + expectedReach: Ipv4, + }, { + name: "torv3 to routable ipv6", + localAddr: torAddress, + remoteAddr: routableIPv6Addr, + expectedBool: false, + expectedReach: Ipv6Strong, }} + createNetAddr := func(addr string) *NetAddress { + addrType, addrBytes := EncodeHost(addr) + if addrType == UnknownAddressType { + t.Fatalf("unable to parse address: %s", addr) + } + na, err := NewNetAddressFromParams(addrType, addrBytes, 8333, + time.Time{}, wire.SFNodeNetwork) + if err != nil { + t.Fatalf("failed to create NetAddress from %s: %v", addr, err) + } + return na + } + addressManager := New("TestIsExternalAddrCandidate") for _, test := range tests { - localIP := net.ParseIP(test.localAddr) - remoteIP := net.ParseIP(test.remoteAddr) - localNa := NewNetAddressFromIPPort(localIP, 8333, wire.SFNodeNetwork) - remoteNa := NewNetAddressFromIPPort(remoteIP, 8333, wire.SFNodeNetwork) + localNa := createNetAddr(test.localAddr) + remoteNa := createNetAddr(test.remoteAddr) goodReach, reach := addressManager.IsExternalAddrCandidate(localNa, remoteNa) if goodReach != test.expectedBool { @@ -1003,3 +1092,78 @@ func TestIsExternalAddrCandidate(t *testing.T) { } } } + +// TestGetAddressWithFilter ensures that GetAddress returns addresses matching +// the provided filter. +func TestGetAddressWithFilter(t *testing.T) { + ipv4Addr := NewNetAddressFromIPPort(net.ParseIP(routableIPv4Addr), 8333, 0) + ipv6Addr := NewNetAddressFromIPPort(net.ParseIP(routableIPv6Addr), 8333, 0) + + addrType, addrBytes := EncodeHost(torv3Host) + torv3Addr, _ := NewNetAddressFromParams(addrType, addrBytes, 8333, + time.Unix(time.Now().Unix(), 0), 0) + + tests := []struct { + name string + addresses []*NetAddress + filter NetAddressTypeFilter + wantType NetAddressType + wantNil bool + }{{ + name: "returns address matching IPv4 filter", + addresses: []*NetAddress{ipv4Addr, ipv6Addr}, + filter: natfOnlyIPv4, + wantType: IPv4Address, + }, { + name: "returns address matching IPv6 filter", + addresses: []*NetAddress{ipv4Addr, ipv6Addr}, + filter: natfOnlyIPv6, + wantType: IPv6Address, + }, { + name: "returns address matching TORv3 filter", + addresses: []*NetAddress{ipv4Addr, ipv6Addr, torv3Addr}, + filter: natfOnlyTORv3, + wantType: TORv3Address, + }, { + name: "returns nil when no matching IPv4 addresses", + addresses: []*NetAddress{ipv6Addr}, + filter: natfOnlyIPv4, + wantNil: true, + }, { + name: "returns nil when no matching IPv6 addresses", + addresses: []*NetAddress{ipv4Addr}, + filter: natfOnlyIPv6, + wantNil: true, + }, { + name: "returns nil when address manager empty", + addresses: []*NetAddress{}, + filter: natfAny, + wantNil: true, + }} + + for _, test := range tests { + amgr := New("TestGetAddressWithFilter") + amgr.AddAddresses(test.addresses, ipv4Addr) + + ka := amgr.GetAddress(test.filter) + + if test.wantNil { + if ka != nil { + t.Errorf("%q: expected nil, got address: %v", test.name, ka.NetAddress()) + } + continue + } + + if ka == nil { + t.Errorf("%q: expected address, got nil", test.name) + continue + } + + // Verify the address type matches expected type. + gotType := ka.NetAddress().Type + if gotType != test.wantType { + t.Errorf("%q: unexpected address type: got %v, want %v", + test.name, gotType, test.wantType) + } + } +} diff --git a/addrmgr/go.mod b/addrmgr/go.mod index e360f32da5..27e6604c79 100644 --- a/addrmgr/go.mod +++ b/addrmgr/go.mod @@ -1,4 +1,4 @@ -module github.com/decred/dcrd/addrmgr/v3 +module github.com/decred/dcrd/addrmgr/v4 go 1.19 diff --git a/addrmgr/netaddress.go b/addrmgr/netaddress.go index 668705a3cd..9ceaa9886f 100644 --- a/addrmgr/netaddress.go +++ b/addrmgr/netaddress.go @@ -1,13 +1,15 @@ -// Copyright (c) 2024 The Decred developers +// Copyright (c) 2021-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package addrmgr import ( + "encoding/base32" "fmt" "net" "strconv" + "strings" "time" "github.com/decred/dcrd/wire" @@ -36,6 +38,9 @@ type NetAddress struct { // IsRoutable returns a boolean indicating whether the network address is // routable. func (netAddr *NetAddress) IsRoutable() bool { + if netAddr.Type == TORv3Address { + return true + } return IsRoutable(netAddr.IP) } @@ -48,6 +53,15 @@ func (netAddr *NetAddress) ipString() string { return net.IP(netIP).String() case IPv4Address: return net.IP(netIP).String() + case TORv3Address: + var publicKey [32]byte + copy(publicKey[:], netIP) + checksum := calcTORv3Checksum(publicKey) + var torAddressBytes [35]byte + copy(torAddressBytes[:32], publicKey[:]) + copy(torAddressBytes[32:34], checksum[:]) + torAddressBytes[34] = torV3VersionByte + return strings.ToLower(base32.StdEncoding.EncodeToString(torAddressBytes[:])) + ".onion" } // If the netAddr.Type is not recognized in the switch: @@ -83,13 +97,16 @@ func (netAddr *NetAddress) AddService(service wire.ServiceFlag) { // deriveNetAddressType attempts to determine the network address type from the // address' raw bytes. If the type cannot be determined, an error is returned. -func deriveNetAddressType(addrBytes []byte) (NetAddressType, error) { +// The claimedType parameter provides a hint for ambiguous byte lengths. +func deriveNetAddressType(claimedType NetAddressType, addrBytes []byte) (NetAddressType, error) { len := len(addrBytes) switch { case isIPv4(addrBytes): return IPv4Address, nil case len == 16: return IPv6Address, nil + case len == 32 && claimedType == TORv3Address: + return TORv3Address, nil } str := fmt.Sprintf("unable to determine address type from raw network "+ "address bytes: %v", addrBytes) @@ -115,7 +132,7 @@ func canonicalizeIP(addrType NetAddressType, addrBytes []byte) []byte { // checkNetAddressType returns an error if the suggested address type does not // appear to match the provided address. func checkNetAddressType(addrType NetAddressType, addrBytes []byte) error { - derivedAddressType, err := deriveNetAddressType(addrBytes) + derivedAddressType, err := deriveNetAddressType(addrType, addrBytes) if err != nil { return err } @@ -131,14 +148,14 @@ func checkNetAddressType(addrType NetAddressType, addrBytes []byte) error { // NewNetAddressFromParams creates a new network address from the given // parameters. If the provided address type does not appear to match the // address, an error is returned. -func NewNetAddressFromParams(netAddressType NetAddressType, addrBytes []byte, port uint16, timestamp time.Time, services wire.ServiceFlag) (*NetAddress, error) { - canonicalizedIP := canonicalizeIP(netAddressType, addrBytes) - err := checkNetAddressType(netAddressType, canonicalizedIP) +func NewNetAddressFromParams(addrType NetAddressType, addrBytes []byte, port uint16, timestamp time.Time, services wire.ServiceFlag) (*NetAddress, error) { + canonicalizedIP := canonicalizeIP(addrType, addrBytes) + err := checkNetAddressType(addrType, canonicalizedIP) if err != nil { return nil, err } return &NetAddress{ - Type: netAddressType, + Type: addrType, IP: canonicalizedIP, Port: port, Services: services, @@ -173,7 +190,7 @@ func (a *AddrManager) newNetAddressFromString(addr string) (*NetAddress, error) // the derived network address type. Furthermore, other types of network // addresses (like Tor) will not be recognized. func NewNetAddressFromIPPort(ip net.IP, port uint16, services wire.ServiceFlag) *NetAddress { - netAddressType, _ := deriveNetAddressType(ip) + netAddressType, _ := deriveNetAddressType(UnknownAddressType, ip) timestamp := time.Unix(time.Now().Unix(), 0) canonicalizedIP := canonicalizeIP(netAddressType, ip) return &NetAddress{ diff --git a/addrmgr/netaddress_test.go b/addrmgr/netaddress_test.go index 010711942d..b70ad8890b 100644 --- a/addrmgr/netaddress_test.go +++ b/addrmgr/netaddress_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2024 The Decred developers +// Copyright (c) 2021-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -14,6 +14,15 @@ import ( "github.com/decred/dcrd/wire" ) +var ( + torAddress = "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion" + torAddressBytes = []byte{ + 0xb8, 0x39, 0x1d, 0x20, 0x03, 0xbb, 0x3b, 0xd2, + 0x85, 0xb0, 0x35, 0xac, 0x8e, 0xb3, 0x0c, 0x80, + 0xc4, 0xe2, 0xa2, 0x9b, 0xb7, 0xa2, 0xf0, 0xce, + 0x0d, 0xf8, 0x74, 0x3c, 0x37, 0xec, 0x35, 0x93} +) + // TestIpString verifies that IpString will correctly return the string // representation of a NetAddress' IP field. func TestIpString(t *testing.T) { @@ -39,71 +48,74 @@ func TestIpString(t *testing.T) { // value. func TestKey(t *testing.T) { tests := []struct { - ip string + host string port uint16 want string }{ // IPv4 // Localhost - {ip: "127.0.0.1", port: 8333, want: "127.0.0.1:8333"}, - {ip: "127.0.0.1", port: 8334, want: "127.0.0.1:8334"}, + {host: "127.0.0.1", port: 8333, want: "127.0.0.1:8333"}, + {host: "127.0.0.1", port: 8334, want: "127.0.0.1:8334"}, // Class A - {ip: "1.0.0.1", port: 8333, want: "1.0.0.1:8333"}, - {ip: "2.2.2.2", port: 8334, want: "2.2.2.2:8334"}, - {ip: "27.253.252.251", port: 8335, want: "27.253.252.251:8335"}, - {ip: "123.3.2.1", port: 8336, want: "123.3.2.1:8336"}, + {host: "1.0.0.1", port: 8333, want: "1.0.0.1:8333"}, + {host: "2.2.2.2", port: 8334, want: "2.2.2.2:8334"}, + {host: "27.253.252.251", port: 8335, want: "27.253.252.251:8335"}, + {host: "123.3.2.1", port: 8336, want: "123.3.2.1:8336"}, // Private Class A - {ip: "10.0.0.1", port: 8333, want: "10.0.0.1:8333"}, - {ip: "10.1.1.1", port: 8334, want: "10.1.1.1:8334"}, - {ip: "10.2.2.2", port: 8335, want: "10.2.2.2:8335"}, - {ip: "10.10.10.10", port: 8336, want: "10.10.10.10:8336"}, + {host: "10.0.0.1", port: 8333, want: "10.0.0.1:8333"}, + {host: "10.1.1.1", port: 8334, want: "10.1.1.1:8334"}, + {host: "10.2.2.2", port: 8335, want: "10.2.2.2:8335"}, + {host: "10.10.10.10", port: 8336, want: "10.10.10.10:8336"}, // Class B - {ip: "128.0.0.1", port: 8333, want: "128.0.0.1:8333"}, - {ip: "129.1.1.1", port: 8334, want: "129.1.1.1:8334"}, - {ip: "180.2.2.2", port: 8335, want: "180.2.2.2:8335"}, - {ip: "191.10.10.10", port: 8336, want: "191.10.10.10:8336"}, + {host: "128.0.0.1", port: 8333, want: "128.0.0.1:8333"}, + {host: "129.1.1.1", port: 8334, want: "129.1.1.1:8334"}, + {host: "180.2.2.2", port: 8335, want: "180.2.2.2:8335"}, + {host: "191.10.10.10", port: 8336, want: "191.10.10.10:8336"}, // Private Class B - {ip: "172.16.0.1", port: 8333, want: "172.16.0.1:8333"}, - {ip: "172.16.1.1", port: 8334, want: "172.16.1.1:8334"}, - {ip: "172.16.2.2", port: 8335, want: "172.16.2.2:8335"}, - {ip: "172.16.172.172", port: 8336, want: "172.16.172.172:8336"}, + {host: "172.16.0.1", port: 8333, want: "172.16.0.1:8333"}, + {host: "172.16.1.1", port: 8334, want: "172.16.1.1:8334"}, + {host: "172.16.2.2", port: 8335, want: "172.16.2.2:8335"}, + {host: "172.16.172.172", port: 8336, want: "172.16.172.172:8336"}, // Class C - {ip: "193.0.0.1", port: 8333, want: "193.0.0.1:8333"}, - {ip: "200.1.1.1", port: 8334, want: "200.1.1.1:8334"}, - {ip: "205.2.2.2", port: 8335, want: "205.2.2.2:8335"}, - {ip: "223.10.10.10", port: 8336, want: "223.10.10.10:8336"}, + {host: "193.0.0.1", port: 8333, want: "193.0.0.1:8333"}, + {host: "200.1.1.1", port: 8334, want: "200.1.1.1:8334"}, + {host: "205.2.2.2", port: 8335, want: "205.2.2.2:8335"}, + {host: "223.10.10.10", port: 8336, want: "223.10.10.10:8336"}, // Private Class C - {ip: "192.168.0.1", port: 8333, want: "192.168.0.1:8333"}, - {ip: "192.168.1.1", port: 8334, want: "192.168.1.1:8334"}, - {ip: "192.168.2.2", port: 8335, want: "192.168.2.2:8335"}, - {ip: "192.168.192.192", port: 8336, want: "192.168.192.192:8336"}, + {host: "192.168.0.1", port: 8333, want: "192.168.0.1:8333"}, + {host: "192.168.1.1", port: 8334, want: "192.168.1.1:8334"}, + {host: "192.168.2.2", port: 8335, want: "192.168.2.2:8335"}, + {host: "192.168.192.192", port: 8336, want: "192.168.192.192:8336"}, // IPv6 // Localhost - {ip: "::1", port: 8333, want: "[::1]:8333"}, - {ip: "fe80::1", port: 8334, want: "[fe80::1]:8334"}, + {host: "::1", port: 8333, want: "[::1]:8333"}, + {host: "fe80::1", port: 8334, want: "[fe80::1]:8334"}, // Link-local - {ip: "fe80::1:1", port: 8333, want: "[fe80::1:1]:8333"}, - {ip: "fe91::2:2", port: 8334, want: "[fe91::2:2]:8334"}, - {ip: "fea2::3:3", port: 8335, want: "[fea2::3:3]:8335"}, - {ip: "feb3::4:4", port: 8336, want: "[feb3::4:4]:8336"}, + {host: "fe80::1:1", port: 8333, want: "[fe80::1:1]:8333"}, + {host: "fe91::2:2", port: 8334, want: "[fe91::2:2]:8334"}, + {host: "fea2::3:3", port: 8335, want: "[fea2::3:3]:8335"}, + {host: "feb3::4:4", port: 8336, want: "[feb3::4:4]:8336"}, // Site-local - {ip: "fec0::1:1", port: 8333, want: "[fec0::1:1]:8333"}, - {ip: "fed1::2:2", port: 8334, want: "[fed1::2:2]:8334"}, - {ip: "fee2::3:3", port: 8335, want: "[fee2::3:3]:8335"}, - {ip: "fef3::4:4", port: 8336, want: "[fef3::4:4]:8336"}, + {host: "fec0::1:1", port: 8333, want: "[fec0::1:1]:8333"}, + {host: "fed1::2:2", port: 8334, want: "[fed1::2:2]:8334"}, + {host: "fee2::3:3", port: 8335, want: "[fee2::3:3]:8335"}, + {host: "fef3::4:4", port: 8336, want: "[fef3::4:4]:8336"}, + + // TORv3 + {host: torAddress, port: 8333, want: torAddress + ":8333"}, } for _, test := range tests { - host_ip := test.ip + host_ip := test.host addrType, addrBytes := EncodeHost(host_ip) netAddr, err := NewNetAddressFromParams(addrType, addrBytes, test.port, @@ -206,6 +218,19 @@ func TestNewNetAddressFromParams(t *testing.T) { }, error_expected: false, }, + { + name: "32 byte torv3 address stored in 32 bytes", + addrType: TORv3Address, + addrBytes: torAddressBytes, + want: &NetAddress{ + IP: torAddressBytes, + Port: port, + Services: services, + Timestamp: timestamp, + Type: TORv3Address, + }, + error_expected: false, + }, { name: "Error: Cannot derive net address type", addrType: UnknownAddressType, diff --git a/addrmgr/network.go b/addrmgr/network.go index ccaa2933d5..14ae95353c 100644 --- a/addrmgr/network.go +++ b/addrmgr/network.go @@ -1,14 +1,21 @@ // Copyright (c) 2013-2014 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package addrmgr import ( + "fmt" "net" + + "golang.org/x/crypto/sha3" ) +// torV3VersionByte represents the version byte used when encoding and decoding +// a torv3 host name. +const torV3VersionByte = byte(3) + var ( // rfc1918Nets specifies the IPv4 private address blocks as defined by // RFC1918 (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16). @@ -105,6 +112,7 @@ const ( IPv4Address NetAddressType = 1 IPv6Address NetAddressType = 2 // TorV2Address NetAddressType = 3 // No longer supported + TORv3Address NetAddressType = 4 ) // NetAddressTypeFilter represents a function that returns whether a particular @@ -204,6 +212,51 @@ func isRFC6598(netIP net.IP) bool { return rfc6598Net.Contains(netIP) } +// calcTORv3Checksum returns the checksum bytes given a 32 byte +// TORv3 public key. +func calcTORv3Checksum(publicKey [32]byte) [2]byte { + const ( + prefix = ".onion checksum" + prefixLen = len(prefix) + inputLen = prefixLen + len(publicKey) + 1 + ) + var input [inputLen]byte + copy(input[:], prefix) + copy(input[prefixLen:], publicKey[:]) + input[inputLen-1] = torV3VersionByte + digest := sha3.Sum256(input[:]) + + var result [2]byte + copy(result[:], digest[:]) + return result +} + +// isTORv3 returns whether or not the passed address is a valid TORv3 address +// with the checksum and version bytes. If it is valid, it also returns the +// public key of the tor v3 address. +func isTORv3(addressBytes []byte) ([32]byte, bool) { + var publicKey [32]byte + if len(addressBytes) != 35 { + return publicKey, false + } + + version := addressBytes[34] + if version != torV3VersionByte { + return publicKey, false + } + + copy(publicKey[:], addressBytes[:32]) + computedChecksum := calcTORv3Checksum(publicKey) + + var checksum [2]byte + copy(checksum[:], addressBytes[32:34]) + if computedChecksum != checksum { + return publicKey, false + } + + return publicKey, true +} + // isValid returns whether or not the passed address is valid. The address is // considered invalid under the following circumstances: // IPv4: It is either a zero or all bits set address. @@ -231,6 +284,10 @@ func IsRoutable(netIP net.IP) bool { // address. func (na *NetAddress) GroupKey() string { netIP := net.IP(na.IP) + if na.Type == TORv3Address { + // Group is keyed off the first 4 bits of the public key. + return fmt.Sprintf("torv3:%d", netIP[0]&0xf) + } if isLocal(netIP) { return "local" } diff --git a/addrmgr/network_test.go b/addrmgr/network_test.go index 19b576809f..bc22f0946f 100644 --- a/addrmgr/network_test.go +++ b/addrmgr/network_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2014 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -8,6 +8,7 @@ package addrmgr import ( "net" "testing" + "time" "github.com/decred/dcrd/wire" ) @@ -146,55 +147,63 @@ func TestIPTypes(t *testing.T) { func TestGroupKey(t *testing.T) { tests := []struct { name string - ip string + host string expected string }{ // Local addresses. - {name: "ipv4 localhost", ip: "127.0.0.1", expected: "local"}, - {name: "ipv6 localhost", ip: "::1", expected: "local"}, - {name: "ipv4 zero", ip: "0.0.0.0", expected: "local"}, - {name: "ipv4 first octet zero", ip: "0.1.2.3", expected: "local"}, + {name: "ipv4 localhost", host: "127.0.0.1", expected: "local"}, + {name: "ipv6 localhost", host: "::1", expected: "local"}, + {name: "ipv4 zero", host: "0.0.0.0", expected: "local"}, + {name: "ipv4 first octet zero", host: "0.1.2.3", expected: "local"}, // Unroutable addresses. - {name: "ipv4 invalid bcast", ip: "255.255.255.255", expected: "unroutable"}, - {name: "ipv4 rfc1918 10/8", ip: "10.1.2.3", expected: "unroutable"}, - {name: "ipv4 rfc1918 172.16/12", ip: "172.16.1.2", expected: "unroutable"}, - {name: "ipv4 rfc1918 192.168/16", ip: "192.168.1.2", expected: "unroutable"}, - {name: "ipv6 rfc3849 2001:db8::/32", ip: "2001:db8::1234", expected: "unroutable"}, - {name: "ipv4 rfc3927 169.254/16", ip: "169.254.1.2", expected: "unroutable"}, - {name: "ipv6 rfc4193 fc00::/7", ip: "fc00::1234", expected: "unroutable"}, - {name: "ipv6 rfc4843 2001:10::/28", ip: "2001:10::1234", expected: "unroutable"}, - {name: "ipv6 rfc4862 fe80::/64", ip: "fe80::1234", expected: "unroutable"}, + {name: "ipv4 invalid bcast", host: "255.255.255.255", expected: "unroutable"}, + {name: "ipv4 rfc1918 10/8", host: "10.1.2.3", expected: "unroutable"}, + {name: "ipv4 rfc1918 172.16/12", host: "172.16.1.2", expected: "unroutable"}, + {name: "ipv4 rfc1918 192.168/16", host: "192.168.1.2", expected: "unroutable"}, + {name: "ipv6 rfc3849 2001:db8::/32", host: "2001:db8::1234", expected: "unroutable"}, + {name: "ipv4 rfc3927 169.254/16", host: "169.254.1.2", expected: "unroutable"}, + {name: "ipv6 rfc4193 fc00::/7", host: "fc00::1234", expected: "unroutable"}, + {name: "ipv6 rfc4843 2001:10::/28", host: "2001:10::1234", expected: "unroutable"}, + {name: "ipv6 rfc4862 fe80::/64", host: "fe80::1234", expected: "unroutable"}, // IPv4 normal. - {name: "ipv4 normal class a", ip: "12.1.2.3", expected: "12.1.0.0"}, - {name: "ipv4 normal class b", ip: "173.1.2.3", expected: "173.1.0.0"}, - {name: "ipv4 normal class c", ip: "196.1.2.3", expected: "196.1.0.0"}, + {name: "ipv4 normal class a", host: "12.1.2.3", expected: "12.1.0.0"}, + {name: "ipv4 normal class b", host: "173.1.2.3", expected: "173.1.0.0"}, + {name: "ipv4 normal class c", host: "196.1.2.3", expected: "196.1.0.0"}, // IPv6/IPv4 translations. - {name: "ipv6 rfc3964 with ipv4 encap", ip: "2002:0c01:0203::", expected: "12.1.0.0"}, - {name: "ipv6 rfc4380 toredo ipv4", ip: "2001:0:1234::f3fe:fdfc", expected: "12.1.0.0"}, - {name: "ipv6 rfc6052 well-known prefix with ipv4", ip: "64:ff9b::0c01:0203", expected: "12.1.0.0"}, - {name: "ipv6 rfc6145 translated ipv4", ip: "::ffff:0:0c01:0203", expected: "12.1.0.0"}, - - // // Tor. - // {name: "ipv6 tor onioncat", ip: "fd87:d87e:eb43:1234::5678", expected: "tor:2"}, - // {name: "ipv6 tor onioncat 2", ip: "fd87:d87e:eb43:1245::6789", expected: "tor:2"}, - // {name: "ipv6 tor onioncat 3", ip: "fd87:d87e:eb43:1345::6789", expected: "tor:3"}, + {name: "ipv6 rfc3964 with ipv4 encap", host: "2002:0c01:0203::", expected: "12.1.0.0"}, + {name: "ipv6 rfc4380 toredo ipv4", host: "2001:0:1234::f3fe:fdfc", expected: "12.1.0.0"}, + {name: "ipv6 rfc6052 well-known prefix with ipv4", host: "64:ff9b::0c01:0203", expected: "12.1.0.0"}, + {name: "ipv6 rfc6145 translated ipv4", host: "::ffff:0:0c01:0203", expected: "12.1.0.0"}, // IPv6 normal. - {name: "ipv6 normal", ip: "2602:100::1", expected: "2602:100::"}, - {name: "ipv6 normal 2", ip: "2602:0100::1234", expected: "2602:100::"}, - {name: "ipv6 hurricane electric", ip: "2001:470:1f10:a1::2", expected: "2001:470:1000::"}, - {name: "ipv6 hurricane electric 2", ip: "2001:0470:1f10:a1::2", expected: "2001:470:1000::"}, + {name: "ipv6 normal", host: "2602:100::1", expected: "2602:100::"}, + {name: "ipv6 normal 2", host: "2602:0100::1234", expected: "2602:100::"}, + {name: "ipv6 hurricane electric", host: "2001:470:1f10:a1::2", expected: "2001:470:1000::"}, + {name: "ipv6 hurricane electric 2", host: "2001:0470:1f10:a1::2", expected: "2001:470:1000::"}, + + // TORv3 + { + name: "torv3", + host: "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion", + expected: "torv3:8", + }, } - for i, test := range tests { - nip := net.ParseIP(test.ip) - na := NewNetAddressFromIPPort(nip, 8333, wire.SFNodeNetwork) - if key := na.GroupKey(); key != test.expected { - t.Errorf("TestGroupKey #%d (%s): unexpected group key "+ - "- got '%s', want '%s'", i, test.name, key, test.expected) + ts := time.Now() + for _, test := range tests { + addrType, addrBytes := EncodeHost(test.host) + na, err := NewNetAddressFromParams(addrType, addrBytes, 8333, ts, + wire.SFNodeNetwork) + if err != nil { + t.Fatalf("%q: failed to create NetAddress: %v", test.name, err) + } + actualKey := na.GroupKey() + if actualKey != test.expected { + t.Errorf("%q: unexpected group key - got %s, want %s", + test.name, actualKey, test.expected) } } } diff --git a/go.mod b/go.mod index 014ee94264..84527268d4 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/base58 v1.0.6 - github.com/decred/dcrd/addrmgr/v3 v3.0.0 + github.com/decred/dcrd/addrmgr/v4 v4.0.0 github.com/decred/dcrd/bech32 v1.1.4 github.com/decred/dcrd/blockchain/stake/v5 v5.0.2 github.com/decred/dcrd/blockchain/standalone/v2 v2.2.2 @@ -27,7 +27,7 @@ require ( github.com/decred/dcrd/gcs/v4 v4.1.1 github.com/decred/dcrd/math/uint256 v1.0.2 github.com/decred/dcrd/mixing v0.6.0 - github.com/decred/dcrd/peer/v3 v3.2.0 + github.com/decred/dcrd/peer/v4 v4.0.0 github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.4.0 github.com/decred/dcrd/rpcclient/v8 v8.1.0 github.com/decred/dcrd/txscript/v4 v4.1.2 @@ -61,7 +61,7 @@ require ( ) replace ( - github.com/decred/dcrd/addrmgr/v3 => ./addrmgr + github.com/decred/dcrd/addrmgr/v4 => ./addrmgr github.com/decred/dcrd/bech32 => ./bech32 github.com/decred/dcrd/blockchain/stake/v5 => ./blockchain/stake github.com/decred/dcrd/blockchain/standalone/v2 => ./blockchain/standalone @@ -85,7 +85,7 @@ replace ( github.com/decred/dcrd/limits => ./limits github.com/decred/dcrd/math/uint256 => ./math/uint256 github.com/decred/dcrd/mixing => ./mixing - github.com/decred/dcrd/peer/v3 => ./peer + github.com/decred/dcrd/peer/v4 => ./peer github.com/decred/dcrd/rpc/jsonrpc/types/v4 => ./rpc/jsonrpc/types github.com/decred/dcrd/rpcclient/v8 => ./rpcclient github.com/decred/dcrd/txscript/v4 => ./txscript diff --git a/internal/netsync/manager.go b/internal/netsync/manager.go index 14a9573d55..3d081b6556 100644 --- a/internal/netsync/manager.go +++ b/internal/netsync/manager.go @@ -24,7 +24,7 @@ import ( "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/mixing" "github.com/decred/dcrd/mixing/mixpool" - peerpkg "github.com/decred/dcrd/peer/v3" + peerpkg "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/wire" ) diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index e230331496..35ee351484 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -9,7 +9,7 @@ import ( "net" "time" - "github.com/decred/dcrd/addrmgr/v3" + "github.com/decred/dcrd/addrmgr/v4" "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v4" @@ -21,7 +21,7 @@ import ( "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/mixing" "github.com/decred/dcrd/mixing/mixpool" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/dcrd/wire" diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index e642d448a6..daeafc3996 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -26,7 +26,7 @@ import ( "time" "github.com/davecgh/go-spew/spew" - "github.com/decred/dcrd/addrmgr/v3" + "github.com/decred/dcrd/addrmgr/v4" "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" @@ -45,7 +45,7 @@ import ( "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/mixing" "github.com/decred/dcrd/mixing/mixpool" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" diff --git a/internal/staging/banmanager/banmanager.go b/internal/staging/banmanager/banmanager.go index f85bfb81e5..04d0e74f66 100644 --- a/internal/staging/banmanager/banmanager.go +++ b/internal/staging/banmanager/banmanager.go @@ -12,7 +12,7 @@ import ( "time" "github.com/decred/dcrd/connmgr/v3" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" ) // Config is the configuration struct for the ban manager. diff --git a/internal/staging/banmanager/banmanager_test.go b/internal/staging/banmanager/banmanager_test.go index b4a7bd563b..e963d9b9cc 100644 --- a/internal/staging/banmanager/banmanager_test.go +++ b/internal/staging/banmanager/banmanager_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/wire" "github.com/decred/go-socks/socks" ) diff --git a/log.go b/log.go index ebd69c1081..879cf26c3b 100644 --- a/log.go +++ b/log.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2017 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -10,7 +10,7 @@ import ( "os" "path/filepath" - "github.com/decred/dcrd/addrmgr/v3" + "github.com/decred/dcrd/addrmgr/v4" "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/connmgr/v3" "github.com/decred/dcrd/database/v3" @@ -23,7 +23,7 @@ import ( "github.com/decred/dcrd/internal/netsync" "github.com/decred/dcrd/internal/rpcserver" "github.com/decred/dcrd/mixing/mixpool" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/slog" "github.com/jrick/logrotate/rotator" diff --git a/peer/example_test.go b/peer/example_test.go index 90e21c26b8..281b1a8784 100644 --- a/peer/example_test.go +++ b/peer/example_test.go @@ -10,7 +10,7 @@ import ( "net" "time" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/wire" ) diff --git a/peer/go.mod b/peer/go.mod index d77a1bcfca..918a18dac2 100644 --- a/peer/go.mod +++ b/peer/go.mod @@ -1,4 +1,4 @@ -module github.com/decred/dcrd/peer/v3 +module github.com/decred/dcrd/peer/v4 go 1.18 @@ -26,3 +26,5 @@ require ( golang.org/x/sys v0.30.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) + +replace github.com/decred/dcrd/wire => ../wire diff --git a/peer/go.sum b/peer/go.sum index 6f22a14b1a..c2de04b25b 100644 --- a/peer/go.sum +++ b/peer/go.sum @@ -23,8 +23,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/txscript/v4 v4.1.2 h1:1EP7ZmBDl2LBeAMTEygxY8rVNN3+lkGqrsb4u64x+II= github.com/decred/dcrd/txscript/v4 v4.1.2/go.mod h1:r5/8qfCnl6TFrE369gggUayVIryM1oC7BLoRfa27Ckw= -github.com/decred/dcrd/wire v1.7.1 h1:kDuHBiY1Qv9rBxYKgC2RgyPy7IOA2WRf00jqHwpr16I= -github.com/decred/dcrd/wire v1.7.1/go.mod h1:eP9XRsMloy+phlntkTAaAm611JgLv8NqY1YJoRxkNKU= github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U= github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= diff --git a/peer/log.go b/peer/log.go index 5a087873b2..3c8245acba 100644 --- a/peer/log.go +++ b/peer/log.go @@ -1,5 +1,5 @@ // Copyright (c) 2015-2016 The btcsuite developers -// Copyright (c) 2016-2024 The Decred developers +// Copyright (c) 2016-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -136,6 +136,9 @@ func messageSummary(msg wire.Message) string { case *wire.MsgAddr: return fmt.Sprintf("%d addr", len(msg.AddrList)) + case *wire.MsgAddrV2: + return fmt.Sprintf("%d addrv2", len(msg.AddrList)) + case *wire.MsgPing: // No summary - perhaps add nonce. diff --git a/peer/peer.go b/peer/peer.go index 85a9c5b34d..679544b707 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -30,7 +30,7 @@ import ( const ( // MaxProtocolVersion is the max protocol version the peer supports. - MaxProtocolVersion = wire.BatchedCFiltersV2Version + MaxProtocolVersion = wire.TORv3Version // outputBufferSize is the number of elements the output channels use. outputBufferSize = 5000 @@ -108,6 +108,9 @@ type MessageListeners struct { // OnAddr is invoked when a peer receives an addr wire message. OnAddr func(p *Peer, msg *wire.MsgAddr) + // OnAddrV2 is invoked when a peer receives an addrv2 wire message. + OnAddrV2 func(p *Peer, msg *wire.MsgAddrV2) + // OnPing is invoked when a peer receives a ping wire message. OnPing func(p *Peer, msg *wire.MsgPing) @@ -325,13 +328,13 @@ func minUint32(a, b uint32) uint32 { // newNetAddress attempts to extract the IP address and port from the passed // net.Addr interface and create a NetAddress structure using that information. -func newNetAddress(addr net.Addr, services wire.ServiceFlag) (*wire.NetAddress, error) { +func newNetAddress(addr net.Addr, services wire.ServiceFlag) (*wire.NetAddressV2, error) { // addr will be a net.TCPAddr when not using a proxy. if tcpAddr, ok := addr.(*net.TCPAddr); ok { ip := tcpAddr.IP port := uint16(tcpAddr.Port) - na := wire.NewNetAddressIPPort(ip, port, services) - return na, nil + na := wire.NewNetAddressV2IPPort(ip, port, services) + return &na, nil } // addr will be a socks.ProxiedAddr when using a proxy. @@ -341,8 +344,8 @@ func newNetAddress(addr net.Addr, services wire.ServiceFlag) (*wire.NetAddress, ip = net.ParseIP("0.0.0.0") } port := uint16(proxiedAddr.Port) - na := wire.NewNetAddressIPPort(ip, port, services) - return na, nil + na := wire.NewNetAddressV2IPPort(ip, port, services) + return &na, nil } // For the most part, addr should be one of the two above cases, but @@ -357,8 +360,8 @@ func newNetAddress(addr net.Addr, services wire.ServiceFlag) (*wire.NetAddress, if err != nil { return nil, err } - na := wire.NewNetAddressIPPort(ip, uint16(port), services) - return na, nil + na := wire.NewNetAddressV2IPPort(ip, uint16(port), services) + return &na, nil } // outMsg is used to house a message to be sent along with a channel to signal @@ -426,7 +429,7 @@ type AddrFunc func(remoteAddr *wire.NetAddress) *wire.NetAddress // HostToNetAddrFunc is a func which takes a host, port, services and returns // the netaddress. type HostToNetAddrFunc func(host string, port uint16, - services wire.ServiceFlag) (*wire.NetAddress, error) + services wire.ServiceFlag) (*wire.NetAddressV2, error) // NOTE: The overall data flow of a peer is split into 3 goroutines. Inbound // messages are read via the inHandler goroutine and generally dispatched to @@ -477,7 +480,7 @@ type Peer struct { inbound bool flagsMtx sync.Mutex // protects the peer flags below - na *wire.NetAddress + na *wire.NetAddressV2 id int32 userAgent string services wire.ServiceFlag @@ -610,7 +613,7 @@ func (p *Peer) ID() int32 { // NA returns the peer network address. // // This function is safe for concurrent access. -func (p *Peer) NA() *wire.NetAddress { +func (p *Peer) NA() *wire.NetAddressV2 { p.flagsMtx.Lock() if p.na == nil { p.flagsMtx.Unlock() @@ -870,6 +873,38 @@ func (p *Peer) PushAddrMsg(addresses []*wire.NetAddress) ([]*wire.NetAddress, er return msg.AddrList, nil } +// PushAddrV2Msg sends an addrv2 message to the connected peer using the +// provided addresses. This function is useful over manually sending the +// message via QueueMessage since it automatically limits the addresses to the +// maximum number allowed by the message and randomizes the chosen addresses +// when there are too many. It returns the addresses that were actually sent +// and no message will be sent if there are no entries in the provided addresses +// slice. +// +// This function is safe for concurrent access. +func (p *Peer) PushAddrV2Msg(addresses []wire.NetAddressV2) []wire.NetAddressV2 { + // Nothing to send. + if len(addresses) == 0 { + return nil + } + + // Copy the addresses to avoid mutating the caller's slice. + addrs := make([]wire.NetAddressV2, len(addresses)) + copy(addrs, addresses) + + // Randomize the addresses sent if there are more than the maximum allowed. + if len(addrs) > wire.MaxAddrPerV2Msg { + rand.ShuffleSlice(addrs) + addrs = addrs[:wire.MaxAddrPerV2Msg] + } + + msg := wire.NewMsgAddrV2() + msg.AddrList = addrs + + p.QueueMessage(msg, nil) + return addrs +} + // PushGetBlocksMsg sends a getblocks message for the provided block locator // and stop hash. It will ignore back-to-back duplicate requests. // @@ -1405,6 +1440,11 @@ out: p.cfg.Listeners.OnAddr(p, msg) } + case *wire.MsgAddrV2: + if p.cfg.Listeners.OnAddrV2 != nil { + p.cfg.Listeners.OnAddrV2(p, msg) + } + case *wire.MsgPing: p.handlePingMsg(msg) if p.cfg.Listeners.OnPing != nil { @@ -2028,19 +2068,24 @@ func (p *Peer) localVersionMsg() (*wire.MsgVersion, error) { } } - theirNA := p.NA() + peerNA := p.NA() // If we are behind a proxy and the connection comes from the proxy then // we return an unroutable address as their address. This is to prevent // leaking the tor proxy address. + var theirNA *wire.NetAddress if p.cfg.Proxy != "" { proxyaddress, _, err := net.SplitHostPort(p.cfg.Proxy) // invalid proxy means poorly configured, be on the safe side. - if err != nil || p.na.IP.String() == proxyaddress { + if err != nil || net.IP(p.na.IP).String() == proxyaddress { theirNA = wire.NewNetAddressIPPort(net.IP([]byte{0, 0, 0, 0}), 0, - theirNA.Services) + peerNA.Services) } } + if theirNA == nil { + theirNA = wire.NewNetAddressTimestamp(peerNA.Timestamp, peerNA.Services, + peerNA.IP, peerNA.Port) + } // Create a wire.NetAddress with only the services set to use as the // "addrme" in the version message. @@ -2294,7 +2339,8 @@ func NewOutboundPeer(cfg *Config, addr string) (*Peer, error) { } p.na = na } else { - p.na = wire.NewNetAddressIPPort(net.ParseIP(host), uint16(port), 0) + na := wire.NewNetAddressV2IPPort(net.ParseIP(host), uint16(port), 0) + p.na = &na } return p, nil diff --git a/peer/peer_test.go b/peer/peer_test.go index aa74fe6b1c..2fae203f10 100644 --- a/peer/peer_test.go +++ b/peer/peer_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2015-2016 The btcsuite developers -// Copyright (c) 2016-2024 The Decred developers +// Copyright (c) 2016-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -336,6 +336,9 @@ func TestPeerListeners(t *testing.T) { OnAddr: func(p *Peer, msg *wire.MsgAddr) { ok <- msg }, + OnAddrV2: func(p *Peer, msg *wire.MsgAddrV2) { + ok <- msg + }, OnPing: func(p *Peer, msg *wire.MsgPing) { ok <- msg }, @@ -458,130 +461,145 @@ func TestPeerListeners(t *testing.T) { return } + const pver = wire.ProtocolVersion tests := []struct { listener string msg wire.Message - }{ - { - "OnGetAddr", - wire.NewMsgGetAddr(), - }, - { - "OnAddr", - wire.NewMsgAddr(), - }, - { - "OnPing", - wire.NewMsgPing(42), - }, - { - "OnPong", - wire.NewMsgPong(42), - }, - { - "OnMemPool", - wire.NewMsgMemPool(), - }, - { - "OnTx", - wire.NewMsgTx(), - }, - { - "OnBlock", - wire.NewMsgBlock(wire.NewBlockHeader(0, &chainhash.Hash{}, - &chainhash.Hash{}, &chainhash.Hash{}, 1, [6]byte{}, - 1, 1, 1, 1, 1, 1, 1, 1, 1, [32]byte{}, - binary.LittleEndian.Uint32([]byte{0xb0, 0x1d, 0xfa, 0xce}))), - }, - { - "OnInv", - wire.NewMsgInv(), - }, - { - "OnHeaders", - wire.NewMsgHeaders(), - }, - { - "OnNotFound", - wire.NewMsgNotFound(), - }, - { - "OnGetData", - wire.NewMsgGetData(), - }, - { - "OnGetBlocks", - wire.NewMsgGetBlocks(&chainhash.Hash{}), - }, - { - "OnGetHeaders", - wire.NewMsgGetHeaders(), - }, - { - "OnGetCFilter", - wire.NewMsgGetCFilter(&chainhash.Hash{}, - wire.GCSFilterRegular), - }, - { - "OnGetCFHeaders", - wire.NewMsgGetCFHeaders(), - }, - { - "OnGetCFTypes", - wire.NewMsgGetCFTypes(), - }, - { - "OnCFilter", - wire.NewMsgCFilter(&chainhash.Hash{}, - wire.GCSFilterRegular, []byte("payload")), - }, - { - "OnCFHeaders", - wire.NewMsgCFHeaders(), - }, - { - "OnCFTypes", - wire.NewMsgCFTypes([]wire.FilterType{ - wire.GCSFilterRegular, wire.GCSFilterExtended, - }), - }, - { - "OnFeeFilter", - wire.NewMsgFeeFilter(15000), - }, - { - "OnGetCFilterV2", - wire.NewMsgGetCFilterV2(&chainhash.Hash{}), - }, - { - "OnCFilterV2", - wire.NewMsgCFilterV2(&chainhash.Hash{}, nil, 0, nil), - }, + pver uint32 + }{{ + listener: "OnGetAddr", + msg: wire.NewMsgGetAddr(), + pver: pver, + }, { + listener: "OnAddr", + msg: wire.NewMsgAddr(), + pver: wire.AddrV2Version - 1, + }, { + listener: "OnAddrV2", + msg: func() *wire.MsgAddrV2 { + msg := wire.NewMsgAddrV2() + msg.AddAddress(wire.NewNetAddressV2(wire.IPv4Address, + net.ParseIP("127.0.0.1").To4(), 8333, time.Now(), wire.SFNodeNetwork)) + return msg + }(), + pver: pver, + }, { + listener: "OnPing", + msg: wire.NewMsgPing(42), + pver: pver, + }, { + listener: "OnPong", + msg: wire.NewMsgPong(42), + pver: pver, + }, { + listener: "OnMemPool", + msg: wire.NewMsgMemPool(), + pver: pver, + }, { + listener: "OnTx", + msg: wire.NewMsgTx(), + pver: pver, + }, { + listener: "OnBlock", + msg: wire.NewMsgBlock(wire.NewBlockHeader(0, &chainhash.Hash{}, + &chainhash.Hash{}, &chainhash.Hash{}, 1, [6]byte{}, + 1, 1, 1, 1, 1, 1, 1, 1, 1, [32]byte{}, + binary.LittleEndian.Uint32([]byte{0xb0, 0x1d, 0xfa, 0xce}))), + pver: pver, + }, { + listener: "OnInv", + msg: wire.NewMsgInv(), + pver: pver, + }, { + listener: "OnHeaders", + msg: wire.NewMsgHeaders(), + pver: pver, + }, { + listener: "OnNotFound", + msg: wire.NewMsgNotFound(), + pver: pver, + }, { + listener: "OnGetData", + msg: wire.NewMsgGetData(), + pver: pver, + }, { + listener: "OnGetBlocks", + msg: wire.NewMsgGetBlocks(&chainhash.Hash{}), + pver: pver, + }, { + listener: "OnGetHeaders", + msg: wire.NewMsgGetHeaders(), + pver: pver, + }, { + listener: "OnGetCFilter", + msg: wire.NewMsgGetCFilter(&chainhash.Hash{}, + wire.GCSFilterRegular), + pver: pver, + }, { + listener: "OnGetCFHeaders", + msg: wire.NewMsgGetCFHeaders(), + pver: pver, + }, { + listener: "OnGetCFTypes", + msg: wire.NewMsgGetCFTypes(), + pver: pver, + }, { + listener: "OnCFilter", + msg: wire.NewMsgCFilter(&chainhash.Hash{}, + wire.GCSFilterRegular, []byte("payload")), + pver: pver, + }, { + listener: "OnCFHeaders", + msg: wire.NewMsgCFHeaders(), + pver: pver, + }, { + listener: "OnCFTypes", + msg: wire.NewMsgCFTypes([]wire.FilterType{ + wire.GCSFilterRegular, wire.GCSFilterExtended, + }), + pver: pver, + }, { + listener: "OnFeeFilter", + msg: wire.NewMsgFeeFilter(15000), + pver: pver, + }, { + listener: "OnGetCFilterV2", + msg: wire.NewMsgGetCFilterV2(&chainhash.Hash{}), + pver: pver, + }, { + listener: "OnCFilterV2", + msg: wire.NewMsgCFilterV2(&chainhash.Hash{}, nil, 0, nil), + pver: pver, + }, // only one version message is allowed // only one verack message is allowed { - "OnSendHeaders", - wire.NewMsgSendHeaders(), - }, - { - "OnGetInitState", - wire.NewMsgGetInitState(), - }, - { - "OnInitState", - wire.NewMsgInitState(), - }, - { - "OnGetCFiltersV2", - wire.NewMsgGetCFsV2(&chainhash.Hash{}, &chainhash.Hash{}), - }, - { - "OnCFiltersV2", - wire.NewMsgCFiltersV2([]wire.MsgCFilterV2{}), + listener: "OnSendHeaders", + msg: wire.NewMsgSendHeaders(), + pver: pver, + }, { + listener: "OnGetInitState", + msg: wire.NewMsgGetInitState(), + pver: pver, + }, { + listener: "OnInitState", + msg: wire.NewMsgInitState(), + pver: pver, + }, { + listener: "OnGetCFiltersV2", + msg: wire.NewMsgGetCFsV2(&chainhash.Hash{}, &chainhash.Hash{}), + pver: pver, + }, { + listener: "OnCFiltersV2", + msg: wire.NewMsgCFiltersV2([]wire.MsgCFilterV2{}), + pver: pver, }, } t.Logf("Running %d tests", len(tests)) for _, test := range tests { + testPver := test.pver + inPeer.protocolVersion = testPver + outPeer.protocolVersion = testPver // Queue the test message outPeer.QueueMessage(test.msg, nil) select { @@ -934,6 +952,85 @@ func TestUpdateLastBlockHeight(t *testing.T) { } } +// TestPushAddrV2Msg ensures that the PushAddrV2Msg returns the expected +// number of addresses. +func TestPushAddrV2Msg(t *testing.T) { + addr := wire.NewNetAddressV2(wire.IPv4Address, net.ParseIP("192.168.0.1"), + 8333, time.Now(), wire.SFNodeNetwork) + + tests := []struct { + name string + addrs []wire.NetAddressV2 + wantSentLen int + }{ + { + name: "nil address list", + addrs: nil, + wantSentLen: 0, + }, { + name: "empty address list", + addrs: []wire.NetAddressV2{}, + wantSentLen: 0, + }, { + name: "single address", + addrs: []wire.NetAddressV2{addr}, + wantSentLen: 1, + }, { + name: "multiple addresses under limit", + addrs: func() []wire.NetAddressV2 { + addrs := make([]wire.NetAddressV2, 10) + for i := range addrs { + addrs[i] = addr + } + return addrs + }(), + wantSentLen: 10, + }, { + name: "addresses over MaxAddrPerV2Msg limit", + addrs: func() []wire.NetAddressV2 { + addrs := make([]wire.NetAddressV2, wire.MaxAddrPerV2Msg+100) + for i := range addrs { + addrs[i] = addr + } + return addrs + }(), + wantSentLen: wire.MaxAddrPerV2Msg, + }, + } + + // Create a mock connection. + inConn, outConn := pipe( + &conn{laddr: "10.0.0.1:9108", raddr: "10.0.0.2:9108"}, + &conn{laddr: "10.0.0.2:9108", raddr: "10.0.0.1:9108"}, + ) + + // Create a peer with the connection. + cfg := &Config{} + peer, err := NewOutboundPeer(cfg, inConn.laddr) + if err != nil { + t.Fatalf("NewOutboundPeer: unexpected err: %v", err) + } + peer.AssociateConnection(outConn) + + for _, test := range tests { + // Test the PushAddrV2Msg function. + sent := peer.PushAddrV2Msg(test.addrs) + + // Check the number of addresses sent. + gotSentLen := 0 + if sent != nil { + gotSentLen = len(sent) + } + if gotSentLen != test.wantSentLen { + t.Errorf("%s: expected %d addresses sent, got %d", + test.name, test.wantSentLen, gotSentLen) + } + } + + peer.Disconnect() + peer.WaitForDisconnect() +} + func init() { // Allow self connection when running the tests. allowSelfConns = true diff --git a/rpcadaptors.go b/rpcadaptors.go index 66e81ddbe0..aa2d1b78f6 100644 --- a/rpcadaptors.go +++ b/rpcadaptors.go @@ -23,7 +23,7 @@ import ( "github.com/decred/dcrd/internal/rpcserver" "github.com/decred/dcrd/mixing" "github.com/decred/dcrd/mixing/mixpool" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/wire" ) @@ -179,7 +179,7 @@ func (cm *rpcConnManager) removeNode(cmp func(*serverPeer) bool) error { found := disconnectPeer(state.persistentPeers, cmp, func(sp *serverPeer) { // Update the group counts since the peer will be removed from the // persistent peers just after this func returns. - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() state.outboundGroups[remoteAddr.GroupKey()]-- connReq := sp.connReq.Load() @@ -255,7 +255,7 @@ func (cm *rpcConnManager) disconnectNode(cmp func(sp *serverPeer) bool) error { found = disconnectPeer(state.outboundPeers, cmp, func(sp *serverPeer) { // Update the group counts since the peer will be removed from the // persistent peers just after this func returns. - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() state.outboundGroups[remoteAddr.GroupKey()]-- }) if !found { diff --git a/server.go b/server.go index e97526aa19..4bc0ad2ec6 100644 --- a/server.go +++ b/server.go @@ -24,7 +24,7 @@ import ( "sync/atomic" "time" - "github.com/decred/dcrd/addrmgr/v3" + "github.com/decred/dcrd/addrmgr/v4" "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/certgen" @@ -48,7 +48,7 @@ import ( "github.com/decred/dcrd/math/uint256" "github.com/decred/dcrd/mixing" "github.com/decred/dcrd/mixing/mixpool" - "github.com/decred/dcrd/peer/v3" + "github.com/decred/dcrd/peer/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/wire" "github.com/syndtr/goleveldb/leveldb" @@ -77,7 +77,7 @@ const ( connectionRetryInterval = time.Second * 5 // maxProtocolVersion is the max protocol version the server supports. - maxProtocolVersion = wire.BatchedCFiltersV2Version + maxProtocolVersion = wire.TORv3Version // These fields are used to track known addresses on a per-peer basis. // @@ -418,20 +418,6 @@ func (ps *peerState) ForAllPeers(closure func(sp *serverPeer)) { ps.Unlock() } -// connectionsWithIP returns the number of connections with the given IP. -// -// This function MUST be called with the embedded mutex locked (for reads). -func (ps *peerState) connectionsWithIP(ip net.IP) int { - var total int - ps.forAllPeers(func(sp *serverPeer) { - if ip.Equal(sp.NA().IP) { - total++ - } - - }) - return total -} - type resolveIPFn func(string) ([]net.IP, error) // hostToNetAddress parses and returns an address manager network address given @@ -973,6 +959,34 @@ func (sp *serverPeer) addressKnown(na *addrmgr.NetAddress) bool { return sp.knownAddresses.Contains([]byte(na.Key())) } +// wireToAddrmgrNetAddressType converts a wire network address type to +// an address manager net address type. +func wireToAddrmgrNetAddressType(addrType wire.NetAddressType) addrmgr.NetAddressType { + switch addrType { + case wire.IPv4Address: + return addrmgr.IPv4Address + case wire.IPv6Address: + return addrmgr.IPv6Address + case wire.TORv3Address: + return addrmgr.TORv3Address + } + return addrmgr.UnknownAddressType +} + +// addrmgrToWireNetAddressType converts an address manager net address type to +// a wire net address type. +func addrmgrToWireNetAddressType(addrType addrmgr.NetAddressType) wire.NetAddressType { + switch addrType { + case addrmgr.IPv4Address: + return wire.IPv4Address + case addrmgr.IPv6Address: + return wire.IPv6Address + case addrmgr.TORv3Address: + return wire.TORv3Address + } + return wire.UnknownAddressType +} + // wireToAddrmgrNetAddress converts a wire NetAddress to an address manager // NetAddress. func wireToAddrmgrNetAddress(netAddr *wire.NetAddress) *addrmgr.NetAddress { @@ -992,6 +1006,24 @@ func wireToAddrmgrNetAddresses(netAddr []*wire.NetAddress) []*addrmgr.NetAddress return addrs } +// wireToAddrmgrNetAddressesV2 converts a collection of version 2 wire +// network addresses to a collection of address manager network addresses. If +// any addresses are not able to be converted, an error is returned. +func wireToAddrmgrNetAddressesV2(netAddrs []wire.NetAddressV2) ([]*addrmgr.NetAddress, error) { + addrs := make([]*addrmgr.NetAddress, len(netAddrs)) + for i := range netAddrs { + wireAddr := &netAddrs[i] + addrType := wireToAddrmgrNetAddressType(wireAddr.Type) + addr, err := addrmgr.NewNetAddressFromParams(addrType, wireAddr.IP, + wireAddr.Port, wireAddr.Timestamp, wireAddr.Services) + if err != nil { + return nil, err + } + addrs[i] = addr + } + return addrs, nil +} + // addrmgrToWireNetAddress converts an address manager net address to a wire net // address. func addrmgrToWireNetAddress(netAddr *addrmgr.NetAddress) *wire.NetAddress { @@ -999,6 +1031,14 @@ func addrmgrToWireNetAddress(netAddr *addrmgr.NetAddress) *wire.NetAddress { netAddr.IP, netAddr.Port) } +// addrmgrToWireNetAddressV2 converts an address manager net address to a v2 wire +// net address. +func addrmgrToWireNetAddressV2(netAddr *addrmgr.NetAddress) wire.NetAddressV2 { + addrType := addrmgrToWireNetAddressType(netAddr.Type) + return wire.NewNetAddressV2(addrType, netAddr.IP, netAddr.Port, + netAddr.Timestamp, netAddr.Services) +} + // pushAddrMsg sends an addr message to the connected peer using the provided // addresses. func (sp *serverPeer) pushAddrMsg(addresses []*addrmgr.NetAddress) { @@ -1070,10 +1110,66 @@ func isSupportedNetAddrTypeV1(addrType addrmgr.NetAddressType) bool { return addrType == addrmgr.IPv4Address || addrType == addrmgr.IPv6Address } +// isSupportedNetAddressTypeV2 returns whether the provided address manager +// network address type is supported by the addrv2 wire message. +func isSupportedNetAddressTypeV2(addrType addrmgr.NetAddressType) bool { + switch addrType { + case addrmgr.IPv4Address, addrmgr.IPv6Address, addrmgr.TORv3Address: + return true + } + return false +} + // natfSupported returns a filter for the address types supported by the // protocol version. func natfSupported(pver uint32) addrmgr.NetAddressTypeFilter { - return isSupportedNetAddrTypeV1 + switch { + case pver <= wire.AddrV2Version: + return isSupportedNetAddrTypeV1 + } + return isSupportedNetAddressTypeV2 +} + +// pushAddrV2Msg sends an addrv2 message to the connected peer using the +// provided addresses. +func (sp *serverPeer) pushAddrV2Msg(addresses []*addrmgr.NetAddress) { + // Filter addresses already known to the peer. + addrs := make([]wire.NetAddressV2, 0, len(addresses)) + for _, addr := range addresses { + if !sp.addressKnown(addr) { + wireNetAddr := addrmgrToWireNetAddressV2(addr) + addrs = append(addrs, wireNetAddr) + } + } + known := sp.PushAddrV2Msg(addrs) + knownNetAddrs, err := wireToAddrmgrNetAddressesV2(known) + if err != nil { + peerLog.Errorf("Failed to convert known addresses: %v", err) + return + } + sp.addKnownAddresses(knownNetAddrs) +} + +// NA returns the address manager network address for the peer. +// +// This method shadows the embedded peer.Peer.NA() method to provide the +// address manager representation directly, eliminating the need for explicit +// conversion at call sites. +// +// This function is safe for concurrent access. +func (sp *serverPeer) NA() *addrmgr.NetAddress { + wireNA := sp.Peer.NA() + if wireNA == nil { + return nil + } + + return &addrmgr.NetAddress{ + Type: wireToAddrmgrNetAddressType(wireNA.Type), + IP: wireNA.IP, + Port: wireNA.Port, + Timestamp: wireNA.Timestamp, + Services: wireNA.Services, + } } // OnVersion is invoked when a peer receives a version wire message and is used @@ -1091,7 +1187,7 @@ func (sp *serverPeer) OnVersion(_ *peer.Peer, msg *wire.MsgVersion) { // it is updated regardless in the case a new minimum protocol version is // enforced and the remote node has not upgraded yet. isInbound := sp.Inbound() - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() addrManager := sp.server.addrManager if !cfg.SimNet && !cfg.RegNet && !isInbound { err := addrManager.SetServices(remoteAddr, msg.Services) @@ -1162,12 +1258,19 @@ func (sp *serverPeer) OnVersion(_ *peer.Peer, msg *wire.MsgVersion) { // known tip. if !cfg.DisableListen && sp.server.syncManager.IsCurrent() { // Get address that best matches. - addrTypeFilter := natfSupported(uint32(msg.ProtocolVersion)) + msgProtocolVersion := uint32(msg.ProtocolVersion) + addrTypeFilter := natfSupported(msgProtocolVersion) lna := addrManager.GetBestLocalAddress(remoteAddr, addrTypeFilter) if lna.IsRoutable() { - // Filter addresses the peer already knows about. addresses := []*addrmgr.NetAddress{lna} - sp.pushAddrMsg(addresses) + if msgProtocolVersion >= wire.AddrV2Version { + sp.pushAddrV2Msg(addresses) + } else { + sp.pushAddrMsg(addresses) + } + } else { + srvrLog.Debugf("Local address %s is not routable and will not "+ + "be broadcast to outbound peer %v", lna.Key(), sp.Addr()) } } @@ -1759,8 +1862,12 @@ func (sp *serverPeer) OnGetAddr(_ *peer.Peer, msg *wire.MsgGetAddr) { addrTypeFilter := natfSupported(pver) addrCache := sp.server.addrManager.AddressCache(addrTypeFilter) - // Push the addresses. - sp.pushAddrMsg(addrCache) + // Push addresses using version-appropriate message type. + if pver >= wire.AddrV2Version { + sp.pushAddrV2Msg(addrCache) + } else { + sp.pushAddrMsg(addrCache) + } } // OnAddr is invoked when a peer receives an addr wire message and is used to @@ -1804,7 +1911,62 @@ func (sp *serverPeer) OnAddr(_ *peer.Peer, msg *wire.MsgAddr) { // Add addresses to server address manager. The address manager handles // the details of things such as preventing duplicate addresses, max // addresses, and last seen updates. - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() + sp.server.addrManager.AddAddresses(addrList, remoteAddr) +} + +// OnAddrV2 is invoked when a peer receives an addrv2 wire message and is used +// to notify the server about advertised addresses. +func (sp *serverPeer) OnAddrV2(_ *peer.Peer, msg *wire.MsgAddrV2) { + // Ignore addresses when running on the simulation and regression test + // networks. This helps prevent the networks from becoming another public + // test network since they will not be able to learn about other peers that + // have not specifically been provided. + if cfg.SimNet || cfg.RegNet { + return + } + + // Do not add more addresses if the peer is disconnecting. + if !sp.Connected() { + peerLog.Debugf("Not adding addresses from disconnecting peer %v", sp) + return + } + + addrList, err := wireToAddrmgrNetAddressesV2(msg.AddrList) + if err != nil { + // If the peer sent an address that cannot be used to construct a valid + // address manager network address, disconnect and ban the peer. This + // can occur if the network address type claimed by the peer does not + // match the canonical form of the address, such as an IPv4-mapped IPv6 + // address with an IPv6 network address type. + peerLog.Errorf("Failed to decode address from peer %v: %v", sp, err) + const reason = "sent invalid addrv2 message" + sp.server.BanPeer(sp, reason) + return + } + + now := time.Now() + for _, na := range addrList { + // Do not add more addresses if the peer is disconnecting. + if !sp.Connected() { + return + } + + // Set the timestamp to 5 days ago if it's more than 10 minutes + // in the future so this address is one of the first to be + // removed when space is needed. + if na.Timestamp.After(now.Add(time.Minute * 10)) { + na.Timestamp = now.Add(-1 * time.Hour * 24 * 5) + } + + // Add address to known addresses for this peer. + sp.addKnownAddress(na) + } + + // Add addresses to server address manager. The address manager handles + // the details of things such as preventing duplicate addresses, max + // addresses, and last seen updates. + remoteAddr := sp.NA() sp.server.addrManager.AddAddresses(addrList, remoteAddr) } @@ -2329,17 +2491,19 @@ func newPeerConfig(sp *serverPeer) *peer.Config { OnGetCFTypes: sp.OnGetCFTypes, OnGetAddr: sp.OnGetAddr, OnAddr: sp.OnAddr, + OnAddrV2: sp.OnAddrV2, OnRead: sp.OnRead, OnWrite: sp.OnWrite, OnNotFound: sp.OnNotFound, }, NewestBlock: sp.newestBlock, - HostToNetAddress: func(host string, port uint16, services wire.ServiceFlag) (*wire.NetAddress, error) { + HostToNetAddress: func(host string, port uint16, services wire.ServiceFlag) (*wire.NetAddressV2, error) { address, err := hostToNetAddress(host, port, services, dcrdLookup) if err != nil { return nil, err } - return addrmgrToWireNetAddress(address), nil + na := addrmgrToWireNetAddressV2(address) + return &na, nil }, Proxy: cfg.Proxy, UserAgentName: userAgentName, @@ -2440,6 +2604,20 @@ out: srvrLog.Tracef("Peer handler done") } +// connectionsWithIP returns the number of connections with the given IP. +// +// This function MUST be called with the embedded mutex locked (for reads). +func (ps *peerState) connectionsWithIP(ip net.IP) int { + var total int + ps.forAllPeers(func(sp *serverPeer) { + if ip.Equal(sp.NA().IP) { + total++ + } + + }) + return total +} + // handleAddPeer deals with adding new peers and includes logic such as // categorizing the type of peer, limiting the maximum allowed number of peers, // and local external address resolution. @@ -2462,7 +2640,7 @@ func (s *server) handleAddPeer(sp *serverPeer) bool { // Limit max number of connections from a single IP. However, allow // whitelisted inbound peers and localhost connections regardless. isInboundWhitelisted := sp.isWhitelisted && sp.Inbound() - peerIP := sp.NA().IP + peerIP := net.IP(sp.NA().IP) if cfg.MaxSameIP > 0 && !isInboundWhitelisted && !peerIP.IsLoopback() && state.connectionsWithIP(peerIP)+1 > cfg.MaxSameIP { @@ -2507,7 +2685,7 @@ func (s *server) handleAddPeer(sp *serverPeer) bool { } // The peer is an outbound peer at this point. - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() state.outboundGroups[remoteAddr.GroupKey()]++ if sp.persistent { state.persistentPeers[sp.ID()] = sp @@ -2614,7 +2792,7 @@ func (s *server) DonePeer(sp *serverPeer) { } if _, ok := list[sp.ID()]; ok { if !sp.Inbound() && sp.VersionKnown() { - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() state.outboundGroups[remoteAddr.GroupKey()]-- } if !sp.Inbound() { @@ -2641,7 +2819,7 @@ func (s *server) DonePeer(sp *serverPeer) { if !cfg.SimNet && !cfg.RegNet && sp.VerAckReceived() && sp.VersionKnown() && sp.NA() != nil { - remoteAddr := wireToAddrmgrNetAddress(sp.NA()) + remoteAddr := sp.NA() err := s.addrManager.Connected(remoteAddr) if err != nil { srvrLog.Errorf("Marking address as connected failed: %v", err) @@ -4231,12 +4409,19 @@ func newServer(ctx context.Context, profiler *profileServer, // network. var newAddressFunc func() (net.Addr, error) if !cfg.SimNet && !cfg.RegNet && len(cfg.ConnectPeers) == 0 { + filter := func(addrType addrmgr.NetAddressType) bool { + switch addrType { + case addrmgr.IPv4Address, addrmgr.IPv6Address: + return true + case addrmgr.TORv3Address: + // Require .onion reachability. + return !cfg.NoOnion && (cfg.Proxy != "" || cfg.OnionProxy != "") + } + return false + } newAddressFunc = func() (net.Addr, error) { for tries := 0; tries < 100; tries++ { - // Note that this does not filter by address type. Unsupported - // network address types should be pruned from the address - // manager's internal storage prior to calling this function. - addr := s.addrManager.GetAddress() + addr := s.addrManager.GetAddress(filter) if addr == nil { break } @@ -4477,13 +4662,24 @@ func initListeners(ctx context.Context, params *chaincfg.Params, amgr *addrmgr.A // addrStringToNetAddr takes an address in the form of 'host:port' and returns // a net.Addr which maps to the original address with any host names resolved -// to IP addresses. +// to IP addresses, if applicable for the respective address type. func addrStringToNetAddr(addr string) (net.Addr, error) { host, strPort, err := net.SplitHostPort(addr) if err != nil { return nil, err } + // Determine the network that the address belongs to and return early if + // a DNS lookup should not be performed for the address. + addrType, _ := addrmgr.EncodeHost(host) + switch addrType { + case addrmgr.TORv3Address: + return &simpleAddr{ + net: "tcp", + addr: addr, + }, nil + } + // Attempt to look up an IP address associated with the parsed host. // The dcrdLookup function will transparently handle performing the // lookup over Tor if necessary. diff --git a/server_test.go b/server_test.go index 9eea2244cc..36fd290445 100644 --- a/server_test.go +++ b/server_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2024 The Decred developers +// Copyright (c) 2024-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -9,8 +9,9 @@ import ( "net" "reflect" "testing" + "time" - "github.com/decred/dcrd/addrmgr/v3" + "github.com/decred/dcrd/addrmgr/v4" "github.com/decred/dcrd/wire" ) @@ -21,6 +22,7 @@ func TestHostToNetAddress(t *testing.T) { // lookupFunc provided to the address manager instance for each test. const hostnameForLookup = "hostname.test" const services = wire.SFNodeNetwork + const torv3Host = "xa4r2iadxm55fbnqgwwi5mymqdcofiu3w6rpbtqn7b2dyn7mgwj64jyd.onion" tests := []struct { name string @@ -30,22 +32,19 @@ func TestHostToNetAddress(t *testing.T) { wantErr bool want *addrmgr.NetAddress }{{ - // name: "valid onion address", - // host: "a5ccbdkubbr2jlcp.onion", - // port: 8333, - // lookupFunc: nil, - // wantErr: false, - // want: NewNetAddressFromIPPort( - // net.ParseIP("fd87:d87e:eb43:744:208d:5408:63a4:ac4f"), 8333, - // services), - // }, { - // name: "invalid onion address", - // host: "0000000000000000.onion", - // port: 8333, - // lookupFunc: nil, - // wantErr: true, - // want: nil, - // }, { + name: "valid TORv3 address", + host: torv3Host, + port: 9108, + lookupFunc: nil, + wantErr: false, + want: func() *addrmgr.NetAddress { + addrType, addrBytes := addrmgr.EncodeHost(torv3Host) + now := time.Unix(time.Now().Unix(), 0) + na, _ := addrmgr.NewNetAddressFromParams(addrType, addrBytes, + 9108, now, services) + return na + }(), + }, { name: "unresolvable host name", host: hostnameForLookup, port: 8333, diff --git a/wire/common.go b/wire/common.go index 0e507de08c..3fc667a78c 100644 --- a/wire/common.go +++ b/wire/common.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -153,6 +153,13 @@ type uint32Time time.Time // time.Time since it is otherwise ambiguous. type int64Time time.Time +// uint64Time represents a unix timestamp encoded with a uint64. It is used as +// a way to signal the readElement function how to decode a timestamp into a Go +// time.Time since it is otherwise ambiguous. The uint64 value is rejected if +// it is larger than the maximum int64 value since it would overflow when +// converted to an int64 for the time.Unix call. +type uint64Time time.Time + // readElement reads the next sequence of bytes from r using little endian // depending on the concrete type of element pointed to. func readElement(r io.Reader, element interface{}) error { @@ -237,6 +244,19 @@ func readElement(r io.Reader, element interface{}) error { *e = int64Time(time.Unix(int64(rv), 0)) return nil + case *uint64Time: + rv, err := binarySerializer.Uint64(r, binary.LittleEndian) + if err != nil { + return err + } + // Reject timestamps that would overflow when converted to int64. + if rv > math.MaxInt64 { + return messageError("readElement", ErrInvalidMsg, + "timestamp exceeds maximum allowed value") + } + *e = uint64Time(time.Unix(int64(rv), 0)) + return nil + // Message header checksum. case *[4]byte: _, err := io.ReadFull(r, e[:]) @@ -353,6 +373,14 @@ func readElement(r io.Reader, element interface{}) error { } *e = RejectCode(rv) return nil + + case *NetAddressType: + rv, err := binarySerializer.Uint8(r) + if err != nil { + return err + } + *e = NetAddressType(rv) + return nil } // Fall back to the slower binary.Read if a fast path was not available diff --git a/wire/error.go b/wire/error.go index cdb62800ad..dd7fbd1cc1 100644 --- a/wire/error.go +++ b/wire/error.go @@ -153,6 +153,14 @@ const ( // ErrTooManyCFilters is returned when the number of committed filters // exceeds the maximum allowed in a batch. ErrTooManyCFilters + + // ErrTooFewAddrs is returned when an address list contains fewer addresses + // than the minimum required. + ErrTooFewAddrs + + // ErrUnknownNetAddrType is returned when a network address type is not + // recognized or supported. + ErrUnknownNetAddrType ) // Map of ErrorCode values back to their constant names for pretty printing. @@ -193,6 +201,8 @@ var errorCodeStrings = map[ErrorCode]string{ ErrTooManyMixPairReqUTXOs: "ErrTooManyMixPairReqUTXOs", ErrTooManyPrevMixMsgs: "ErrTooManyPrevMixMsgs", ErrTooManyCFilters: "ErrTooManyCFilters", + ErrTooFewAddrs: "ErrTooFewAddrs", + ErrUnknownNetAddrType: "ErrUnknownNetAddrType", } // String returns the ErrorCode as a human-readable name. diff --git a/wire/message.go b/wire/message.go index 90bd522b0e..548b200f9d 100644 --- a/wire/message.go +++ b/wire/message.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -32,6 +32,7 @@ const ( CmdVerAck = "verack" CmdGetAddr = "getaddr" CmdAddr = "addr" + CmdAddrV2 = "addrv2" CmdGetBlocks = "getblocks" CmdInv = "inv" CmdGetData = "getdata" @@ -120,6 +121,9 @@ func makeEmptyMessage(command string) (Message, error) { case CmdAddr: msg = &MsgAddr{} + case CmdAddrV2: + msg = &MsgAddrV2{} + case CmdGetBlocks: msg = &MsgGetBlocks{} diff --git a/wire/message_test.go b/wire/message_test.go index 8cda8ce215..43f55a2bc6 100644 --- a/wire/message_test.go +++ b/wire/message_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -59,6 +59,9 @@ func TestMessage(t *testing.T) { msgVerack := NewMsgVerAck() msgGetAddr := NewMsgGetAddr() msgAddr := NewMsgAddr() + msgAddrV2 := NewMsgAddrV2() + msgAddrV2.AddAddress(NewNetAddressV2IPPort(net.ParseIP("127.0.0.1").To4(), + 8333, SFNodeNetwork)) msgGetBlocks := NewMsgGetBlocks(&chainhash.Hash{}) msgBlock := &testBlock msgInv := NewMsgInv() @@ -101,7 +104,8 @@ func TestMessage(t *testing.T) { {msgVersion, msgVersion, pver, MainNet, 125}, {msgVerack, msgVerack, pver, MainNet, 24}, {msgGetAddr, msgGetAddr, pver, MainNet, 24}, - {msgAddr, msgAddr, pver, MainNet, 25}, + {msgAddr, msgAddr, AddrV2Version - 1, MainNet, 25}, + {msgAddrV2, msgAddrV2, pver, MainNet, 48}, {msgGetBlocks, msgGetBlocks, pver, MainNet, 61}, {msgBlock, msgBlock, pver, MainNet, 522}, {msgInv, msgInv, pver, MainNet, 25}, @@ -334,7 +338,7 @@ func TestReadMessageWireErrors(t *testing.T) { // Message with a valid header, but wrong format. [8] { badMessageBytes, - pver, + AddrV2Version - 1, dcrnet, len(badMessageBytes), &MessageError{}, diff --git a/wire/msgaddr.go b/wire/msgaddr.go index 0418a03ed2..c44fd27fd1 100644 --- a/wire/msgaddr.go +++ b/wire/msgaddr.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2015 The btcsuite developers -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -60,6 +60,12 @@ func (msg *MsgAddr) ClearAddresses() { // This is part of the Message interface implementation. func (msg *MsgAddr) BtcDecode(r io.Reader, pver uint32) error { const op = "MsgAddr.BtcDecode" + if pver >= AddrV2Version { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + count, err := ReadVarInt(r, pver) if err != nil { return err @@ -89,6 +95,12 @@ func (msg *MsgAddr) BtcDecode(r io.Reader, pver uint32) error { // This is part of the Message interface implementation. func (msg *MsgAddr) BtcEncode(w io.Writer, pver uint32) error { const op = "MsgAddr.BtcEncode" + if pver >= AddrV2Version { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + // Protocol versions before MultipleAddressVersion only allowed 1 address // per message. count := len(msg.AddrList) @@ -122,6 +134,10 @@ func (msg *MsgAddr) Command() string { // MaxPayloadLength returns the maximum length the payload can be for the // receiver. This is part of the Message interface implementation. func (msg *MsgAddr) MaxPayloadLength(pver uint32) uint32 { + if pver >= AddrV2Version { + return 0 + } + // Num addresses (size of varInt for max address per message) + max allowed // addresses * max address size. return uint32(VarIntSerializeSize(MaxAddrPerMsg)) + diff --git a/wire/msgaddr_test.go b/wire/msgaddr_test.go index 97654fe5f4..c737037ce6 100644 --- a/wire/msgaddr_test.go +++ b/wire/msgaddr_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -19,7 +19,9 @@ import ( // TestAddr tests the MsgAddr API. func TestAddr(t *testing.T) { - pver := ProtocolVersion + // Use protocol version before AddrV2Version since addr message is + // deprecated as of AddrV2Version. + pver := AddrV2Version - 1 // Ensure the command is expected value. wantCmd := "addr" @@ -132,20 +134,20 @@ func TestAddrWire(t *testing.T) { buf []byte // Wire encoding pver uint32 // Protocol version for wire encoding }{ - // Latest protocol version with no addresses. + // Protocol version before AddrV2Version with no addresses. { noAddr, noAddr, noAddrEncoded, - ProtocolVersion, + AddrV2Version - 1, }, - // Latest protocol version with multiple addresses. + // Protocol version before AddrV2Version with multiple addresses. { multiAddr, multiAddr, multiAddrEncoded, - ProtocolVersion, + AddrV2Version - 1, }, } @@ -183,7 +185,9 @@ func TestAddrWire(t *testing.T) { // TestAddrWireErrors performs negative tests against wire encode and decode // of MsgAddr to confirm error paths work correctly. func TestAddrWireErrors(t *testing.T) { - pver := ProtocolVersion + // Use protocol version before AddrV2Version since addr message is + // deprecated as of AddrV2Version. + pver := AddrV2Version - 1 // A couple of NetAddresses to use for testing. na := &NetAddress{ @@ -267,3 +271,72 @@ func TestAddrWireErrors(t *testing.T) { } } } + +// TestAddrVersionEnforcement verifies that addr messages are properly rejected +// for protocol versions >= AddrV2Version. +func TestAddrVersionEnforcement(t *testing.T) { + tests := []struct { + name string + pver uint32 + wantErr error + wantMaxPayload uint32 + }{{ + name: "addr message rejected for AddrV2Version", + pver: AddrV2Version, + wantErr: ErrMsgInvalidForPVer, + wantMaxPayload: 0, + }, { + name: "addr message rejected for ProtocolVersion", + pver: ProtocolVersion, + wantErr: ErrMsgInvalidForPVer, + wantMaxPayload: 0, + }, { + name: "addr message works for version before AddrV2Version", + pver: AddrV2Version - 1, + wantErr: nil, + wantMaxPayload: 30003, + }} + + // Create a basic addr message with one address. + na := &NetAddress{ + Timestamp: time.Unix(0x495fab29, 0), // 2009-01-03 12:15:05 -0600 CST + Services: SFNodeNetwork, + IP: net.ParseIP("127.0.0.1"), + Port: 8333, + } + msg := NewMsgAddr() + msg.AddAddress(na) + + // Encode message with old protocol version to use for decode tests. + var validEncoded bytes.Buffer + if err := msg.BtcEncode(&validEncoded, AddrV2Version-1); err != nil { + t.Fatalf("failed to encode with old version: %v", err) + } + encodedBytes := validEncoded.Bytes() + + for _, test := range tests { + // Test BtcEncode + var buf bytes.Buffer + err := msg.BtcEncode(&buf, test.pver) + if !errors.Is(err, test.wantErr) { + t.Errorf("%q: BtcEncode wrong error - got: %v, want: %v", + test.name, err, test.wantErr) + } + + // Test BtcDecode + var decoded MsgAddr + rbuf := bytes.NewReader(encodedBytes) + err = decoded.BtcDecode(rbuf, test.pver) + if !errors.Is(err, test.wantErr) { + t.Errorf("%q: BtcDecode wrong error - got: %v, want: %v", + test.name, err, test.wantErr) + } + + // Test MaxPayloadLength + maxPayload := msg.MaxPayloadLength(test.pver) + if maxPayload != test.wantMaxPayload { + t.Errorf("%q: MaxPayloadLength wrong value - got: %d, want: %d", + test.name, maxPayload, test.wantMaxPayload) + } + } +} diff --git a/wire/msgaddrv2.go b/wire/msgaddrv2.go new file mode 100644 index 0000000000..a30cc1b2eb --- /dev/null +++ b/wire/msgaddrv2.go @@ -0,0 +1,314 @@ +// Copyright (c) 2025 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wire + +import ( + "fmt" + "io" + "net" +) + +// MaxAddrPerV2Msg is the maximum number of addresses that can be in a single +// Decred addrv2 protocol message. +const MaxAddrPerV2Msg = 1000 + +// MsgAddrV2 implements the Message interface and represents a wire +// addrv2 message. It is used to provide a list of known active peers on the +// network. An active peer is considered one that has transmitted a message +// within the last 3 hours. Nodes which have not transmitted in that time +// frame should be forgotten. Each message is limited to a maximum number of +// addresses. +type MsgAddrV2 struct { + // AddrList contains the addresses that will be sent to or have been + // received from a peer. Instead of manually appending addresses to this + // field directly, consumers should use the convenience functions on an + // instance of this message to add addresses. + AddrList []NetAddressV2 +} + +// AddAddress adds a known address to the message. If the maximum number of +// addresses has been reached, then an error is returned. +func (msg *MsgAddrV2) AddAddress(na NetAddressV2) error { + const op = "MsgAddrV2.AddAddress" + if len(msg.AddrList)+1 > MaxAddrPerV2Msg { + msg := fmt.Sprintf("too many addresses in message [max %v]", + MaxAddrPerV2Msg) + return messageError(op, ErrTooManyAddrs, msg) + } + + msg.AddrList = append(msg.AddrList, na) + return nil +} + +// AddAddresses adds multiple known addresses to the message. If the number of +// addresses exceeds the maximum allowed then an error is returned. +func (msg *MsgAddrV2) AddAddresses(netAddrs ...NetAddressV2) error { + for _, na := range netAddrs { + err := msg.AddAddress(na) + if err != nil { + return err + } + } + return nil +} + +// ClearAddresses removes all addresses from the message. +func (msg *MsgAddrV2) ClearAddresses() { + msg.AddrList = []NetAddressV2{} +} + +// readNetAddressV2 reads an encoded version 2 wire network address from the +// provided reader into the provided NetAddressV2. +func readNetAddressV2(op string, r io.Reader, pver uint32, na *NetAddressV2) error { + err := readElement(r, (*uint64Time)(&na.Timestamp)) + if err != nil { + return err + } + + // Read the service flags. + err = readElement(r, &na.Services) + if err != nil { + return err + } + + // Read the network id to determine the expected length of the ip field. + err = readElement(r, &na.Type) + if err != nil { + return err + } + + // Read the ip bytes with a length varying by the network id type. + switch na.Type { + case IPv4Address: + var ip [4]byte + err := readElement(r, &ip) + if err != nil { + return err + } + na.IP = ip[:] + + case IPv6Address: + var ip [16]byte + err := readElement(r, &ip) + if err != nil { + return err + } + na.IP = ip[:] + + case TORv3Address: + if pver < TORv3Version { + msg := fmt.Sprintf("TORv3 addresses require protocol version %d "+ + "or higher", TORv3Version) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + var ip [32]byte + err := readElement(r, &ip) + if err != nil { + return err + } + na.IP = ip[:] + + default: + msg := fmt.Sprintf("cannot decode unknown network address type %v", + na.Type) + return messageError(op, ErrUnknownNetAddrType, msg) + } + + err = readElement(r, &na.Port) + if err != nil { + return err + } + + return nil +} + +// writeNetAddressV2 serializes an address manager network address to the +// provided writer. +func writeNetAddressV2(op string, w io.Writer, pver uint32, na NetAddressV2) error { + err := writeElement(w, uint64(na.Timestamp.Unix())) + if err != nil { + return err + } + + err = writeElements(w, na.Services, na.Type) + if err != nil { + return err + } + + netAddrIP := na.IP + addrLen := len(netAddrIP) + + switch na.Type { + case IPv4Address: + if addrLen != 4 { + msg := fmt.Sprintf("invalid IPv4 address length: %d", addrLen) + return messageError(op, ErrInvalidMsg, msg) + } + var ip [4]byte + copy(ip[:], netAddrIP) + err = writeElement(w, ip) + if err != nil { + return err + } + + case IPv6Address: + if addrLen != 16 { + msg := fmt.Sprintf("invalid IPv6 address length: %d", addrLen) + return messageError(op, ErrInvalidMsg, msg) + } + var ip [16]byte + copy(ip[:], net.IP(netAddrIP).To16()) + err = writeElement(w, ip) + if err != nil { + return err + } + + case TORv3Address: + if pver < TORv3Version { + msg := fmt.Sprintf("TORv3 addresses require protocol version %d "+ + "or higher", TORv3Version) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + if len(netAddrIP) != 32 { + msg := fmt.Sprintf("invalid TORv3 address length: %d", len(netAddrIP)) + return messageError(op, ErrInvalidMsg, msg) + } + var ip [32]byte + copy(ip[:], netAddrIP) + err = writeElement(w, ip) + if err != nil { + return err + } + + default: + msg := fmt.Sprintf("cannot encode unknown network address type %v", + na.Type) + return messageError(op, ErrUnknownNetAddrType, msg) + } + + return writeElement(w, na.Port) +} + +// BtcDecode decodes r using the wire protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgAddrV2) BtcDecode(r io.Reader, pver uint32) error { + const op = "MsgAddrV2.BtcDecode" + + // Ensure peers sending msgaddrv2 are on the expected minimum version. + if pver < AddrV2Version { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + // Read the total number of addresses in this message. + count, err := ReadVarInt(r, pver) + if err != nil { + return err + } + + if count == 0 { + return messageError(op, ErrTooFewAddrs, + "no addresses for message [count 0, min 1]") + } + + // Limit to max addresses per message. + if count > MaxAddrPerV2Msg { + msg := fmt.Sprintf("too many addresses for message [count %v, max %v]", + count, MaxAddrPerV2Msg) + return messageError(op, ErrTooManyAddrs, msg) + } + + addrs := make([]NetAddressV2, count) + for i := uint64(0); i < count; i++ { + err := readNetAddressV2(op, r, pver, &addrs[i]) + if err != nil { + return err + } + } + + msg.AddrList = addrs + return nil +} + +// BtcEncode encodes the receiver to w using the wire protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgAddrV2) BtcEncode(w io.Writer, pver uint32) error { + const op = "MsgAddrV2.BtcEncode" + if pver < AddrV2Version { + msg := fmt.Sprintf("%s message invalid for protocol version %d", + msg.Command(), pver) + return messageError(op, ErrMsgInvalidForPVer, msg) + } + + count := len(msg.AddrList) + if count > MaxAddrPerV2Msg { + msg := fmt.Sprintf("too many addresses for message [count %v, max %v]", + count, MaxAddrPerV2Msg) + return messageError(op, ErrTooManyAddrs, msg) + } + + if count == 0 { + return messageError(op, ErrTooFewAddrs, + "no addresses for message [count 0, min 1]") + } + + err := WriteVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + + for _, na := range msg.AddrList { + err = writeNetAddressV2(op, w, pver, na) + if err != nil { + return err + } + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgAddrV2) Command() string { + return CmdAddrV2 +} + +// maxNetAddressPayloadV2 returns the max payload size for an address manager +// network address based on the protocol version. +func maxNetAddressPayloadV2(pver uint32) uint32 { + const ( + timestampSize = 8 + servicesSize = 8 + addressTypeSize = 1 + portSize = 2 + ) + + maxAddressSize := uint32(16) // IPv6 is 16 bytes + if pver >= TORv3Version { + maxAddressSize = 32 + } + + return timestampSize + servicesSize + addressTypeSize + + maxAddressSize + portSize +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgAddrV2) MaxPayloadLength(pver uint32) uint32 { + if pver < AddrV2Version { + return 0 + } + return uint32(VarIntSerializeSize(MaxAddrPerV2Msg)) + + (MaxAddrPerV2Msg * maxNetAddressPayloadV2(pver)) +} + +// NewMsgAddrV2 returns a new wire addrv2 message that conforms to the +// Message interface. See MsgAddrV2 for details. +func NewMsgAddrV2() *MsgAddrV2 { + return &MsgAddrV2{ + AddrList: make([]NetAddressV2, 0, MaxAddrPerV2Msg), + } +} diff --git a/wire/msgaddrv2_test.go b/wire/msgaddrv2_test.go new file mode 100644 index 0000000000..b342cf56c8 --- /dev/null +++ b/wire/msgaddrv2_test.go @@ -0,0 +1,464 @@ +// Copyright (c) 2025 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wire + +import ( + "bytes" + "errors" + "io" + "reflect" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" +) + +// newNetAddressV2 is a convenience function for constructing a new v2 network +// address. +func newNetAddressV2(addrType NetAddressType, addrBytes []byte, port uint16) NetAddressV2 { + timestamp := time.Unix(0x495fab29, 0) // 2009-01-03 12:15:05 -0600 CST + netAddr := NewNetAddressV2(addrType, addrBytes, port, timestamp, + SFNodeNetwork) + return netAddr +} + +var ( + ipv4IpBytes = []byte{0x7f, 0x00, 0x00, 0x01} + ipv6IpBytes = []byte{ + 0x26, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + } + torV3IpBytes = []byte{ + 0xb8, 0x39, 0x1d, 0x20, 0x03, 0xbb, 0x3b, 0xd2, + 0x85, 0xb0, 0x35, 0xac, 0x8e, 0xb3, 0x0c, 0x80, + 0xc4, 0xe2, 0xa2, 0x9b, 0xb7, 0xa2, 0xf0, 0xce, + 0x0d, 0xf8, 0x74, 0x3c, 0x37, 0xec, 0x35, 0x93, + } + + ipv4NetAddress = newNetAddressV2(IPv4Address, ipv4IpBytes, 8333) + ipv6NetAddress = newNetAddressV2(IPv6Address, ipv6IpBytes, 8333) + torv3NetAddress = newNetAddressV2(TORv3Address, torV3IpBytes, 8333) + + serializedIPv4NetAddressBytes = []byte{ + 0x29, 0xab, 0x5f, 0x49, 0x00, 0x00, 0x00, 0x00, // Timestamp + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Services + 0x01, // Type (IPv4) + 0x7f, 0x00, 0x00, 0x01, // IP + 0x8d, 0x20, // Port 8333 (little-endian) + } + serializedIPv6NetAddressBytes = []byte{ + 0x29, 0xab, 0x5f, 0x49, 0x00, 0x00, 0x00, 0x00, // Timestamp + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Services + 0x02, // Type (IPv6) + 0x26, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // IP (upper) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // IP (lower) + 0x8d, 0x20, // Port 8333 (little-endian) + } + serializedUnknownNetAddressBytes = []byte{ + 0x29, 0xab, 0x5f, 0x49, 0x00, 0x00, 0x00, 0x00, // Timestamp + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Services + 0x00, // Type (Unknown) + 0x7f, 0x00, 0x00, 0x01, // IP + 0x8d, 0x20, // Port 8333 (little-endian) + } + serializedTORv3NetAddressBytes = []byte{ + 0x29, 0xab, 0x5f, 0x49, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, // Type (TORv3) + 0xb8, 0x39, 0x1d, 0x20, 0x03, 0xbb, 0x3b, 0xd2, // IP + 0x85, 0xb0, 0x35, 0xac, 0x8e, 0xb3, 0x0c, 0x80, // IP + 0xc4, 0xe2, 0xa2, 0x9b, 0xb7, 0xa2, 0xf0, 0xce, // IP + 0x0d, 0xf8, 0x74, 0x3c, 0x37, 0xec, 0x35, 0x93, // IP + 0x8d, 0x20, // Port 8333 (little-endian) + } +) + +// TestAddrV2MaxPayloadLength verifies the maximum payload length equals the +// expected value at various protocol versions and does not exceed the maximum +// message size for any protocol message. +func TestAddrV2MaxPayloadLength(t *testing.T) { + tests := []struct { + name string + pver uint32 + want uint32 + }{{ + name: "protocol version 11", + pver: AddrV2Version - 1, + want: 0, + }, { + name: "protocol version 12", + pver: AddrV2Version, + want: 35003, + }, { + name: "latest protocol version", + pver: ProtocolVersion, + want: 51003, + }} + + for _, test := range tests { + // Ensure max payload is expected value for latest protocol version. + msg := NewMsgAddrV2() + result := msg.MaxPayloadLength(test.pver) + if result != test.want { + t.Errorf("%s: wrong max payload length - got %v, want %d", + test.name, result, test.want) + continue + } + + // Ensure max payload length is not more than the maximum allowed for + // any protocol message. + if result > MaxMessagePayload { + t.Errorf("%s: payload length exceeds maximum message payload - "+ + "got %d, want less than %d.", test.name, result, + MaxMessagePayload) + continue + } + } +} + +// TestAddrV2 tests the MsgAddrV2 API. +func TestAddrV2(t *testing.T) { + // Ensure the command is expected value. + wantCmd := "addrv2" + msg := NewMsgAddrV2() + if cmd := msg.Command(); cmd != wantCmd { + t.Errorf("NewMsgAddrV2: wrong command - got %v want %v", + cmd, wantCmd) + } + + // Ensure NetAddresses are added properly. + err := msg.AddAddress(ipv4NetAddress) + if err != nil { + t.Errorf("AddAddress: %v", err) + } + if !reflect.DeepEqual(msg.AddrList[0], ipv4NetAddress) { + t.Errorf("AddAddress: wrong address added - got %v, want %v", + spew.Sprint(msg.AddrList[0]), spew.Sprint(ipv4NetAddress)) + } + + // Ensure the address list is cleared properly. + msg.ClearAddresses() + if len(msg.AddrList) != 0 { + t.Errorf("ClearAddresses: address list is not empty - "+ + "got %v [%v], want %v", len(msg.AddrList), + spew.Sprint(msg.AddrList[0]), 0) + } + + // Ensure adding more than the max allowed addresses per message returns + // error. + for i := 0; i < MaxAddrPerV2Msg+1; i++ { + err = msg.AddAddress(ipv4NetAddress) + } + if !errors.Is(err, ErrTooManyAddrs) { + t.Errorf("AddAddress: expected ErrTooManyAddrs, got %v", err) + } + + // Make sure adding multiple addresses also returns an error when the + // message is at max capacity. + err = msg.AddAddresses(ipv4NetAddress) + if !errors.Is(err, ErrTooManyAddrs) { + t.Errorf("AddAddresses: expected ErrTooManyAddrs, got %v", err) + } +} + +// TestAddrV2Wire tests the MsgAddrV2 wire encode and decode for various +// numbers of addresses at the latest protocol version. +func TestAddrV2Wire(t *testing.T) { + pver := ProtocolVersion + tests := []struct { + name string + addrs []NetAddressV2 + wantBytes []byte + }{{ + name: "latest protocol version with one address", + addrs: []NetAddressV2{ + ipv4NetAddress, + }, + wantBytes: bytes.Join([][]byte{ + {0x01}, + serializedIPv4NetAddressBytes, + }, []byte{}), + }, { + name: "latest protocol version with multiple addresses", + addrs: []NetAddressV2{ + ipv4NetAddress, + ipv6NetAddress, + torv3NetAddress, + }, + wantBytes: bytes.Join([][]byte{ + {0x03}, + serializedIPv4NetAddressBytes, + serializedIPv6NetAddressBytes, + serializedTORv3NetAddressBytes, + }, []byte{}), + }, { + name: "latest protocol version with maximum addresses", + addrs: func() []NetAddressV2 { + var addrs []NetAddressV2 + for i := 0; i < MaxAddrPerV2Msg; i++ { + addrs = append(addrs, ipv6NetAddress) + } + return addrs + }(), + wantBytes: func() []byte { + parts := [][]byte{{0xfd, 0xe8, 0x03}} // Varint address count: 1000 + for i := 0; i < MaxAddrPerV2Msg; i++ { + parts = append(parts, serializedIPv6NetAddressBytes) + } + return bytes.Join(parts, []byte{}) + }(), + }} + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + subject := NewMsgAddrV2() + subject.AddAddresses(test.addrs...) + + // Encode the message to the wire format and ensure it serializes + // correctly. + var buf bytes.Buffer + err := subject.BtcEncode(&buf, pver) + if err != nil { + t.Errorf("%q: error encoding message - %v", test.name, err) + continue + } + if !bytes.Equal(buf.Bytes(), test.wantBytes) { + t.Errorf("%q: mismatched bytes -- got: %s want: %s", test.name, + spew.Sdump(buf.Bytes()), spew.Sdump(test.wantBytes)) + continue + } + + // Decode the message from the wire format and ensure it deserializes + // correctly. + var msg MsgAddrV2 + rbuf := bytes.NewReader(test.wantBytes) + err = msg.BtcDecode(rbuf, pver) + if err != nil { + t.Errorf("%q: error decoding message - %v", test.name, err) + continue + } + if !reflect.DeepEqual(&msg, subject) { + t.Errorf("%q: mismatched message - got: %s want: %s", i, + spew.Sdump(msg), spew.Sdump(subject)) + continue + } + } +} + +// TestAddrV2BtcDecode verifies decode behavior for various error conditions. +func TestAddrV2BtcDecode(t *testing.T) { + pver := ProtocolVersion + + tests := []struct { + name string + pver uint32 + wireBytes []byte + wantAddrs []NetAddressV2 + wantErr error + }{{ + name: "addrv2 message invalid for pver 11", + pver: AddrV2Version - 1, + wireBytes: bytes.Join([][]byte{ + {0x01}, + serializedIPv4NetAddressBytes, + }, []byte{}), + wantAddrs: nil, + wantErr: ErrMsgInvalidForPVer, + }, { + name: "message with no addresses", + pver: pver, + wireBytes: []byte{ + 0x00, // Varint address count + }, + wantAddrs: nil, + wantErr: ErrTooFewAddrs, + }, { + name: "message missing expected addresses", + pver: pver, + wireBytes: []byte{ + 0x01, // Varint address count + }, + wantAddrs: nil, + wantErr: io.EOF, + }, { + name: "message with too many addresses", + pver: pver, + wireBytes: []byte{ + 0xfd, 0xe9, 0x03, // Varint address count: MaxAddrPerV2Msg+1 + }, + wantAddrs: nil, + wantErr: ErrTooManyAddrs, + }, { + name: "address with overflowed timestamp", + pver: pver, + wireBytes: []byte{ + 0x01, // Varint address count + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, // Timestamp (MaxInt64+1) + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Services + 0x01, // Type (IPv4) + 0x7f, 0x00, 0x00, 0x01, // IP + 0x8d, 0x20, // Port 8333 (little-endian) + }, + wantAddrs: nil, + wantErr: ErrInvalidMsg, + }, { + name: "message with valid types and unknown type", + pver: pver, + wireBytes: bytes.Join([][]byte{ + {0x04}, + serializedIPv4NetAddressBytes, + serializedIPv6NetAddressBytes, + serializedTORv3NetAddressBytes, + serializedUnknownNetAddressBytes, + }, []byte{}), + wantAddrs: nil, + wantErr: ErrUnknownNetAddrType, + }, { + name: "message with TORv3 address invalid on pver 12", + pver: AddrV2Version, + wireBytes: bytes.Join([][]byte{ + {0x01}, + serializedTORv3NetAddressBytes, + }, []byte{}), + wantAddrs: nil, + wantErr: ErrMsgInvalidForPVer, + }, { + name: "message with multiple valid addresses", + pver: pver, + wireBytes: bytes.Join([][]byte{ + {0x03}, + serializedIPv4NetAddressBytes, + serializedIPv6NetAddressBytes, + serializedTORv3NetAddressBytes, + }, []byte{}), + wantAddrs: []NetAddressV2{ + ipv4NetAddress, + ipv6NetAddress, + torv3NetAddress, + }, + wantErr: nil, + }} + + for _, test := range tests { + var msg MsgAddrV2 + rbuf := bytes.NewReader(test.wireBytes) + err := msg.BtcDecode(rbuf, test.pver) + + if !errors.Is(err, test.wantErr) { + t.Errorf("%q: wrong error - got: %v, want: %v", test.name, err, test.wantErr) + continue + } + + if test.wantErr == nil && !reflect.DeepEqual(msg.AddrList, test.wantAddrs) { + t.Errorf("%q: expected %d addresses, got %d - want: %s, got: %s", + test.name, len(test.wantAddrs), len(msg.AddrList), + spew.Sdump(test.wantAddrs), spew.Sdump(msg.AddrList)) + } + } +} + +// TestAddrV2BtcEncode performs negative tests against wire encoding +// of MsgAddrV2 to confirm error paths work correctly. +func TestAddrV2BtcEncode(t *testing.T) { + pver := ProtocolVersion + + tests := []struct { + name string + addrs []NetAddressV2 + pver uint32 + wantErr error + }{{ + name: "addrv2 message invalid for pver 11", + pver: AddrV2Version - 1, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: IPv4Address, + IP: ipv4IpBytes, + Port: 8333, + }}, + wantErr: ErrMsgInvalidForPVer, + }, { + name: "message with no addresses", + pver: pver, + addrs: nil, + wantErr: ErrTooFewAddrs, + }, { + name: "message with too many addresses", + pver: pver, + addrs: make([]NetAddressV2, MaxAddrPerV2Msg+1), + wantErr: ErrTooManyAddrs, + }, { + name: "message with wrong size IPv4 address", + pver: pver, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: IPv4Address, + IP: make([]byte, 1), + Port: 8333, + }}, + wantErr: ErrInvalidMsg, + }, { + name: "message with wrong size IPv6 address", + pver: pver, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: IPv6Address, + IP: make([]byte, 1), + Port: 8333, + }}, + wantErr: ErrInvalidMsg, + }, { + name: "message with wrong size TORv3 address", + pver: pver, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: TORv3Address, + IP: make([]byte, 1), + Port: 8333, + }}, + wantErr: ErrInvalidMsg, + }, { + name: "message with TORv3 address invalid on pver 12", + pver: AddrV2Version, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: TORv3Address, + IP: torV3IpBytes, + Port: 8333, + }}, + wantErr: ErrMsgInvalidForPVer, + }, { + name: "message with unknown address type", + pver: pver, + addrs: []NetAddressV2{{ + Timestamp: time.Unix(0x495fab29, 0), + Services: SFNodeNetwork, + Type: UnknownAddressType, + IP: make([]byte, 1), + Port: 8333, + }}, + wantErr: ErrUnknownNetAddrType, + }} + + for _, test := range tests { + msg := NewMsgAddrV2() + msg.AddrList = test.addrs + ioLimit := int(msg.MaxPayloadLength(test.pver)) + + // Encode to wire format. + w := newFixedWriter(ioLimit) + err := msg.BtcEncode(w, test.pver) + if !errors.Is(err, test.wantErr) { + t.Errorf("%q: wrong error - got: %v, want: %v", test.name, err, + test.wantErr) + continue + } + } +} diff --git a/wire/netaddressv2.go b/wire/netaddressv2.go new file mode 100644 index 0000000000..251d20e1f8 --- /dev/null +++ b/wire/netaddressv2.go @@ -0,0 +1,81 @@ +// Copyright (c) 2025 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wire + +import ( + "net" + "time" +) + +// NetAddressType is used to indicate the type of a given network address. +type NetAddressType uint8 + +const ( + UnknownAddressType NetAddressType = 0 + IPv4Address NetAddressType = 1 + IPv6Address NetAddressType = 2 + TORv3Address NetAddressType = 3 +) + +// NetAddressV2 defines information about a peer on the network. +// +// The field order matches the wire protocol encoding order. +type NetAddressV2 struct { + // Timestamp is the last time the address was seen. + Timestamp time.Time + + // Services represents the service flags supported by this network address. + Services ServiceFlag + + // Type represents the type of network that the network address belongs to. + Type NetAddressType + + // IP address of the peer. It is defined as a byte array to support various + // address types that are not standard to the net package and therefore not + // entirely appropriate to store as a net.IP. + IP []byte + + // Port is the port of the remote peer. + Port uint16 +} + +// NewNetAddressV2 creates a new network address using the provided +// parameters without validation. +func NewNetAddressV2(netAddressType NetAddressType, addrBytes []byte, port uint16, timestamp time.Time, services ServiceFlag) NetAddressV2 { + return NetAddressV2{ + Timestamp: timestamp, + Services: services, + Type: netAddressType, + IP: addrBytes, + Port: port, + } +} + +// NewNetAddressV2IPPort returns a new NetAddressV2 using the provided IP, +// port, and supported services with a current timestamp. The address type is +// automatically determined from the IP (IPv4 or IPv6). +func NewNetAddressV2IPPort(ip net.IP, port uint16, services ServiceFlag) NetAddressV2 { + var addrType NetAddressType + var addrBytes []byte + + if ip4 := ip.To4(); ip4 != nil { + addrType = IPv4Address + addrBytes = ip4 + } else { + addrType = IPv6Address + addrBytes = ip.To16() + } + + // Limit the timestamp to one second precision since the protocol + // doesn't support better. + timestamp := time.Unix(time.Now().Unix(), 0) + return NetAddressV2{ + Timestamp: timestamp, + Services: services, + Type: addrType, + IP: addrBytes, + Port: port, + } +} diff --git a/wire/protocol.go b/wire/protocol.go index e347b2ee0b..ab779d913b 100644 --- a/wire/protocol.go +++ b/wire/protocol.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2025 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -17,7 +17,7 @@ const ( InitialProcotolVersion uint32 = 1 // ProtocolVersion is the latest protocol version this package supports. - ProtocolVersion uint32 = 11 + ProtocolVersion uint32 = 13 // NodeBloomVersion is the protocol version which added the SFNodeBloom // service flag (unused). @@ -58,6 +58,13 @@ const ( // BatchedCFiltersV2Version is the protocol version which adds support // for the batched getcfsv2 and cfiltersv2 messages. BatchedCFiltersV2Version uint32 = 11 + + // AddrV2Version is the protocol version which adds the addrv2 message. + AddrV2Version uint32 = 12 + + // TORv3Version is the protocol version which adds support for TORv3 + // network addresses to the addrv2 message. + TORv3Version uint32 = 13 ) // ServiceFlag identifies services supported by a Decred peer.