// Modified for Tendermint
|
|
// Originally Copyright (c) 2013-2014 Conformal Systems LLC.
|
|
// https://github.com/conformal/btcd/blob/master/LICENSE
|
|
|
|
package pex
|
|
|
|
import (
|
|
crand "crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/minio/highwayhash"
|
|
|
|
"github.com/tendermint/tendermint/crypto"
|
|
tmmath "github.com/tendermint/tendermint/libs/math"
|
|
tmrand "github.com/tendermint/tendermint/libs/rand"
|
|
"github.com/tendermint/tendermint/libs/service"
|
|
tmsync "github.com/tendermint/tendermint/libs/sync"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
)
|
|
|
|
const (
|
|
bucketTypeNew = 0x01
|
|
bucketTypeOld = 0x02
|
|
)
|
|
|
|
// AddrBook is an address book used for tracking peers
|
|
// so we can gossip about them to others and select
|
|
// peers to dial.
|
|
// TODO: break this up?
|
|
type AddrBook interface {
|
|
service.Service
|
|
|
|
// Add our own addresses so we don't later add ourselves
|
|
AddOurAddress(*p2p.NetAddress)
|
|
// Check if it is our address
|
|
OurAddress(*p2p.NetAddress) bool
|
|
|
|
AddPrivateIDs([]string)
|
|
|
|
// Add and remove an address
|
|
AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error
|
|
RemoveAddress(*p2p.NetAddress)
|
|
|
|
// Check if the address is in the book
|
|
HasAddress(*p2p.NetAddress) bool
|
|
|
|
// Do we need more peers?
|
|
NeedMoreAddrs() bool
|
|
// Is Address Book Empty? Answer should not depend on being in your own
|
|
// address book, or private peers
|
|
Empty() bool
|
|
|
|
// Pick an address to dial
|
|
PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress
|
|
|
|
// Mark address
|
|
MarkGood(p2p.ID)
|
|
MarkAttempt(*p2p.NetAddress)
|
|
MarkBad(*p2p.NetAddress, time.Duration) // Move peer to bad peers list
|
|
// Add bad peers back to addrBook
|
|
ReinstateBadPeers()
|
|
|
|
IsGood(*p2p.NetAddress) bool
|
|
IsBanned(*p2p.NetAddress) bool
|
|
|
|
// Send a selection of addresses to peers
|
|
GetSelection() []*p2p.NetAddress
|
|
// Send a selection of addresses with bias
|
|
GetSelectionWithBias(biasTowardsNewAddrs int) []*p2p.NetAddress
|
|
|
|
Size() int
|
|
|
|
// Persist to disk
|
|
Save()
|
|
}
|
|
|
|
var _ AddrBook = (*addrBook)(nil)
|
|
|
|
// addrBook - concurrency safe peer address manager.
|
|
// Implements AddrBook.
|
|
type addrBook struct {
|
|
service.BaseService
|
|
|
|
// accessed concurrently
|
|
mtx tmsync.Mutex
|
|
rand *tmrand.Rand
|
|
ourAddrs map[string]struct{}
|
|
privateIDs map[p2p.ID]struct{}
|
|
addrLookup map[p2p.ID]*knownAddress // new & old
|
|
badPeers map[p2p.ID]*knownAddress // blacklisted peers
|
|
bucketsOld []map[string]*knownAddress
|
|
bucketsNew []map[string]*knownAddress
|
|
nOld int
|
|
nNew int
|
|
|
|
// immutable after creation
|
|
filePath string
|
|
key string // random prefix for bucket placement
|
|
routabilityStrict bool
|
|
hashKey []byte
|
|
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newHashKey() []byte {
|
|
result := make([]byte, highwayhash.Size)
|
|
crand.Read(result)
|
|
return result
|
|
}
|
|
|
|
// NewAddrBook creates a new address book.
|
|
// Use Start to begin processing asynchronous address updates.
|
|
func NewAddrBook(filePath string, routabilityStrict bool) AddrBook {
|
|
am := &addrBook{
|
|
rand: tmrand.NewRand(),
|
|
ourAddrs: make(map[string]struct{}),
|
|
privateIDs: make(map[p2p.ID]struct{}),
|
|
addrLookup: make(map[p2p.ID]*knownAddress),
|
|
badPeers: make(map[p2p.ID]*knownAddress),
|
|
filePath: filePath,
|
|
routabilityStrict: routabilityStrict,
|
|
hashKey: newHashKey(),
|
|
}
|
|
am.init()
|
|
am.BaseService = *service.NewBaseService(nil, "AddrBook", am)
|
|
return am
|
|
}
|
|
|
|
// Initialize the buckets.
|
|
// When modifying this, don't forget to update loadFromFile()
|
|
func (a *addrBook) init() {
|
|
a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits
|
|
// New addr buckets
|
|
a.bucketsNew = make([]map[string]*knownAddress, newBucketCount)
|
|
for i := range a.bucketsNew {
|
|
a.bucketsNew[i] = make(map[string]*knownAddress)
|
|
}
|
|
// Old addr buckets
|
|
a.bucketsOld = make([]map[string]*knownAddress, oldBucketCount)
|
|
for i := range a.bucketsOld {
|
|
a.bucketsOld[i] = make(map[string]*knownAddress)
|
|
}
|
|
}
|
|
|
|
// OnStart implements Service.
|
|
func (a *addrBook) OnStart() error {
|
|
if err := a.BaseService.OnStart(); err != nil {
|
|
return err
|
|
}
|
|
a.loadFromFile(a.filePath)
|
|
|
|
// wg.Add to ensure that any invocation of .Wait()
|
|
// later on will wait for saveRoutine to terminate.
|
|
a.wg.Add(1)
|
|
go a.saveRoutine()
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnStop implements Service.
|
|
func (a *addrBook) OnStop() {
|
|
a.BaseService.OnStop()
|
|
}
|
|
|
|
func (a *addrBook) Wait() {
|
|
a.wg.Wait()
|
|
}
|
|
|
|
func (a *addrBook) FilePath() string {
|
|
return a.filePath
|
|
}
|
|
|
|
//-------------------------------------------------------
|
|
|
|
// AddOurAddress one of our addresses.
|
|
func (a *addrBook) AddOurAddress(addr *p2p.NetAddress) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
a.Logger.Info("Add our address to book", "addr", addr)
|
|
a.ourAddrs[addr.String()] = struct{}{}
|
|
}
|
|
|
|
// OurAddress returns true if it is our address.
|
|
func (a *addrBook) OurAddress(addr *p2p.NetAddress) bool {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
_, ok := a.ourAddrs[addr.String()]
|
|
return ok
|
|
}
|
|
|
|
func (a *addrBook) AddPrivateIDs(ids []string) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
for _, id := range ids {
|
|
a.privateIDs[p2p.ID(id)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// AddAddress implements AddrBook
|
|
// Add address to a "new" bucket. If it's already in one, only add it probabilistically.
|
|
// Returns error if the addr is non-routable. Does not add self.
|
|
// NOTE: addr must not be nil
|
|
func (a *addrBook) AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
return a.addAddress(addr, src)
|
|
}
|
|
|
|
// RemoveAddress implements AddrBook - removes the address from the book.
|
|
func (a *addrBook) RemoveAddress(addr *p2p.NetAddress) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
a.removeAddress(addr)
|
|
}
|
|
|
|
// IsGood returns true if peer was ever marked as good and haven't
|
|
// done anything wrong since then.
|
|
func (a *addrBook) IsGood(addr *p2p.NetAddress) bool {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
return a.addrLookup[addr.ID].isOld()
|
|
}
|
|
|
|
// IsBanned returns true if the peer is currently banned
|
|
func (a *addrBook) IsBanned(addr *p2p.NetAddress) bool {
|
|
a.mtx.Lock()
|
|
_, ok := a.badPeers[addr.ID]
|
|
a.mtx.Unlock()
|
|
|
|
return ok
|
|
}
|
|
|
|
// HasAddress returns true if the address is in the book.
|
|
func (a *addrBook) HasAddress(addr *p2p.NetAddress) bool {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
ka := a.addrLookup[addr.ID]
|
|
return ka != nil
|
|
}
|
|
|
|
// NeedMoreAddrs implements AddrBook - returns true if there are not have enough addresses in the book.
|
|
func (a *addrBook) NeedMoreAddrs() bool {
|
|
return a.Size() < needAddressThreshold
|
|
}
|
|
|
|
// Empty implements AddrBook - returns true if there are no addresses in the address book.
|
|
// Does not count the peer appearing in its own address book, or private peers.
|
|
func (a *addrBook) Empty() bool {
|
|
return a.Size() == 0
|
|
}
|
|
|
|
// PickAddress implements AddrBook. It picks an address to connect to.
|
|
// The address is picked randomly from an old or new bucket according
|
|
// to the biasTowardsNewAddrs argument, which must be between [0, 100] (or else is truncated to that range)
|
|
// and determines how biased we are to pick an address from a new bucket.
|
|
// PickAddress returns nil if the AddrBook is empty or if we try to pick
|
|
// from an empty bucket.
|
|
func (a *addrBook) PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
bookSize := a.size()
|
|
if bookSize <= 0 {
|
|
if bookSize < 0 {
|
|
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld))
|
|
}
|
|
return nil
|
|
}
|
|
if biasTowardsNewAddrs > 100 {
|
|
biasTowardsNewAddrs = 100
|
|
}
|
|
if biasTowardsNewAddrs < 0 {
|
|
biasTowardsNewAddrs = 0
|
|
}
|
|
|
|
// Bias between new and old addresses.
|
|
oldCorrelation := math.Sqrt(float64(a.nOld)) * (100.0 - float64(biasTowardsNewAddrs))
|
|
newCorrelation := math.Sqrt(float64(a.nNew)) * float64(biasTowardsNewAddrs)
|
|
|
|
// pick a random peer from a random bucket
|
|
var bucket map[string]*knownAddress
|
|
pickFromOldBucket := (newCorrelation+oldCorrelation)*a.rand.Float64() < oldCorrelation
|
|
if (pickFromOldBucket && a.nOld == 0) ||
|
|
(!pickFromOldBucket && a.nNew == 0) {
|
|
return nil
|
|
}
|
|
// loop until we pick a random non-empty bucket
|
|
for len(bucket) == 0 {
|
|
if pickFromOldBucket {
|
|
bucket = a.bucketsOld[a.rand.Intn(len(a.bucketsOld))]
|
|
} else {
|
|
bucket = a.bucketsNew[a.rand.Intn(len(a.bucketsNew))]
|
|
}
|
|
}
|
|
// pick a random index and loop over the map to return that index
|
|
randIndex := a.rand.Intn(len(bucket))
|
|
for _, ka := range bucket {
|
|
if randIndex == 0 {
|
|
return ka.Addr
|
|
}
|
|
randIndex--
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarkGood implements AddrBook - it marks the peer as good and
|
|
// moves it into an "old" bucket.
|
|
func (a *addrBook) MarkGood(id p2p.ID) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
ka := a.addrLookup[id]
|
|
if ka == nil {
|
|
return
|
|
}
|
|
ka.markGood()
|
|
if ka.isNew() {
|
|
a.moveToOld(ka)
|
|
}
|
|
}
|
|
|
|
// MarkAttempt implements AddrBook - it marks that an attempt was made to connect to the address.
|
|
func (a *addrBook) MarkAttempt(addr *p2p.NetAddress) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
ka := a.addrLookup[addr.ID]
|
|
if ka == nil {
|
|
return
|
|
}
|
|
ka.markAttempt()
|
|
}
|
|
|
|
// MarkBad implements AddrBook. Kicks address out from book, places
|
|
// the address in the badPeers pool.
|
|
func (a *addrBook) MarkBad(addr *p2p.NetAddress, banTime time.Duration) {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
if a.addBadPeer(addr, banTime) {
|
|
a.removeAddress(addr)
|
|
}
|
|
}
|
|
|
|
// ReinstateBadPeers removes bad peers from ban list and places them into a new
|
|
// bucket.
|
|
func (a *addrBook) ReinstateBadPeers() {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
for _, ka := range a.badPeers {
|
|
if ka.isBanned() {
|
|
continue
|
|
}
|
|
|
|
bucket, err := a.calcNewBucket(ka.Addr, ka.Src)
|
|
if err != nil {
|
|
a.Logger.Error("Failed to calculate new bucket (bad peer won't be reinstantiated)",
|
|
"addr", ka.Addr, "err", err)
|
|
continue
|
|
}
|
|
|
|
a.addToNewBucket(ka, bucket)
|
|
delete(a.badPeers, ka.ID())
|
|
|
|
a.Logger.Info("Reinstated address", "addr", ka.Addr)
|
|
}
|
|
}
|
|
|
|
// GetSelection implements AddrBook.
|
|
// It randomly selects some addresses (old & new). Suitable for peer-exchange protocols.
|
|
// Must never return a nil address.
|
|
func (a *addrBook) GetSelection() []*p2p.NetAddress {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
bookSize := a.size()
|
|
if bookSize <= 0 {
|
|
if bookSize < 0 {
|
|
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
numAddresses := tmmath.MaxInt(
|
|
tmmath.MinInt(minGetSelection, bookSize),
|
|
bookSize*getSelectionPercent/100)
|
|
numAddresses = tmmath.MinInt(maxGetSelection, numAddresses)
|
|
|
|
// XXX: instead of making a list of all addresses, shuffling, and slicing a random chunk,
|
|
// could we just select a random numAddresses of indexes?
|
|
allAddr := make([]*p2p.NetAddress, bookSize)
|
|
i := 0
|
|
for _, ka := range a.addrLookup {
|
|
allAddr[i] = ka.Addr
|
|
i++
|
|
}
|
|
|
|
// Fisher-Yates shuffle the array. We only need to do the first
|
|
// `numAddresses' since we are throwing the rest.
|
|
for i := 0; i < numAddresses; i++ {
|
|
// pick a number between current index and the end
|
|
j := tmrand.Intn(len(allAddr)-i) + i
|
|
allAddr[i], allAddr[j] = allAddr[j], allAddr[i]
|
|
}
|
|
|
|
// slice off the limit we are willing to share.
|
|
return allAddr[:numAddresses]
|
|
}
|
|
|
|
func percentageOfNum(p, n int) int {
|
|
return int(math.Round((float64(p) / float64(100)) * float64(n)))
|
|
}
|
|
|
|
// GetSelectionWithBias implements AddrBook.
|
|
// It randomly selects some addresses (old & new). Suitable for peer-exchange protocols.
|
|
// Must never return a nil address.
|
|
//
|
|
// Each address is picked randomly from an old or new bucket according to the
|
|
// biasTowardsNewAddrs argument, which must be between [0, 100] (or else is truncated to
|
|
// that range) and determines how biased we are to pick an address from a new
|
|
// bucket.
|
|
func (a *addrBook) GetSelectionWithBias(biasTowardsNewAddrs int) []*p2p.NetAddress {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
bookSize := a.size()
|
|
if bookSize <= 0 {
|
|
if bookSize < 0 {
|
|
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if biasTowardsNewAddrs > 100 {
|
|
biasTowardsNewAddrs = 100
|
|
}
|
|
if biasTowardsNewAddrs < 0 {
|
|
biasTowardsNewAddrs = 0
|
|
}
|
|
|
|
numAddresses := tmmath.MaxInt(
|
|
tmmath.MinInt(minGetSelection, bookSize),
|
|
bookSize*getSelectionPercent/100)
|
|
numAddresses = tmmath.MinInt(maxGetSelection, numAddresses)
|
|
|
|
// number of new addresses that, if possible, should be in the beginning of the selection
|
|
// if there are no enough old addrs, will choose new addr instead.
|
|
numRequiredNewAdd := tmmath.MaxInt(percentageOfNum(biasTowardsNewAddrs, numAddresses), numAddresses-a.nOld)
|
|
selection := a.randomPickAddresses(bucketTypeNew, numRequiredNewAdd)
|
|
selection = append(selection, a.randomPickAddresses(bucketTypeOld, numAddresses-len(selection))...)
|
|
return selection
|
|
}
|
|
|
|
//------------------------------------------------
|
|
|
|
// Size returns the number of addresses in the book.
|
|
func (a *addrBook) Size() int {
|
|
a.mtx.Lock()
|
|
defer a.mtx.Unlock()
|
|
|
|
return a.size()
|
|
}
|
|
|
|
func (a *addrBook) size() int {
|
|
return a.nNew + a.nOld
|
|
}
|
|
|
|
//----------------------------------------------------------
|
|
|
|
// Save persists the address book to disk.
|
|
func (a *addrBook) Save() {
|
|
a.saveToFile(a.filePath) // thread safe
|
|
}
|
|
|
|
func (a *addrBook) saveRoutine() {
|
|
defer a.wg.Done()
|
|
|
|
saveFileTicker := time.NewTicker(dumpAddressInterval)
|
|
out:
|
|
for {
|
|
select {
|
|
case <-saveFileTicker.C:
|
|
a.saveToFile(a.filePath)
|
|
case <-a.Quit():
|
|
break out
|
|
}
|
|
}
|
|
saveFileTicker.Stop()
|
|
a.saveToFile(a.filePath)
|
|
}
|
|
|
|
//----------------------------------------------------------
|
|
|
|
func (a *addrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAddress {
|
|
switch bucketType {
|
|
case bucketTypeNew:
|
|
return a.bucketsNew[bucketIdx]
|
|
case bucketTypeOld:
|
|
return a.bucketsOld[bucketIdx]
|
|
default:
|
|
panic("Invalid bucket type")
|
|
}
|
|
}
|
|
|
|
// Adds ka to new bucket. Returns false if it couldn't do it cuz buckets full.
|
|
// NOTE: currently it always returns true.
|
|
func (a *addrBook) addToNewBucket(ka *knownAddress, bucketIdx int) error {
|
|
// Consistency check to ensure we don't add an already known address
|
|
if ka.isOld() {
|
|
return errAddrBookOldAddressNewBucket{ka.Addr, bucketIdx}
|
|
}
|
|
|
|
addrStr := ka.Addr.String()
|
|
bucket := a.getBucket(bucketTypeNew, bucketIdx)
|
|
|
|
// Already exists?
|
|
if _, ok := bucket[addrStr]; ok {
|
|
return nil
|
|
}
|
|
|
|
// Enforce max addresses.
|
|
if len(bucket) > newBucketSize {
|
|
a.Logger.Info("new bucket is full, expiring new")
|
|
a.expireNew(bucketIdx)
|
|
}
|
|
|
|
// Add to bucket.
|
|
bucket[addrStr] = ka
|
|
// increment nNew if the peer doesnt already exist in a bucket
|
|
if ka.addBucketRef(bucketIdx) == 1 {
|
|
a.nNew++
|
|
}
|
|
|
|
// Add it to addrLookup
|
|
a.addrLookup[ka.ID()] = ka
|
|
return nil
|
|
}
|
|
|
|
// Adds ka to old bucket. Returns false if it couldn't do it cuz buckets full.
|
|
func (a *addrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool {
|
|
// Sanity check
|
|
if ka.isNew() {
|
|
a.Logger.Error(fmt.Sprintf("Cannot add new address to old bucket: %v", ka))
|
|
return false
|
|
}
|
|
if len(ka.Buckets) != 0 {
|
|
a.Logger.Error(fmt.Sprintf("Cannot add already old address to another old bucket: %v", ka))
|
|
return false
|
|
}
|
|
|
|
addrStr := ka.Addr.String()
|
|
bucket := a.getBucket(bucketTypeOld, bucketIdx)
|
|
|
|
// Already exists?
|
|
if _, ok := bucket[addrStr]; ok {
|
|
return true
|
|
}
|
|
|
|
// Enforce max addresses.
|
|
if len(bucket) > oldBucketSize {
|
|
return false
|
|
}
|
|
|
|
// Add to bucket.
|
|
bucket[addrStr] = ka
|
|
if ka.addBucketRef(bucketIdx) == 1 {
|
|
a.nOld++
|
|
}
|
|
|
|
// Ensure in addrLookup
|
|
a.addrLookup[ka.ID()] = ka
|
|
|
|
return true
|
|
}
|
|
|
|
func (a *addrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx int) {
|
|
if ka.BucketType != bucketType {
|
|
a.Logger.Error(fmt.Sprintf("Bucket type mismatch: %v", ka))
|
|
return
|
|
}
|
|
bucket := a.getBucket(bucketType, bucketIdx)
|
|
delete(bucket, ka.Addr.String())
|
|
if ka.removeBucketRef(bucketIdx) == 0 {
|
|
if bucketType == bucketTypeNew {
|
|
a.nNew--
|
|
} else {
|
|
a.nOld--
|
|
}
|
|
delete(a.addrLookup, ka.ID())
|
|
}
|
|
}
|
|
|
|
func (a *addrBook) removeFromAllBuckets(ka *knownAddress) {
|
|
for _, bucketIdx := range ka.Buckets {
|
|
bucket := a.getBucket(ka.BucketType, bucketIdx)
|
|
delete(bucket, ka.Addr.String())
|
|
}
|
|
ka.Buckets = nil
|
|
if ka.BucketType == bucketTypeNew {
|
|
a.nNew--
|
|
} else {
|
|
a.nOld--
|
|
}
|
|
delete(a.addrLookup, ka.ID())
|
|
}
|
|
|
|
//----------------------------------------------------------
|
|
|
|
func (a *addrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress {
|
|
bucket := a.getBucket(bucketType, bucketIdx)
|
|
var oldest *knownAddress
|
|
for _, ka := range bucket {
|
|
if oldest == nil || ka.LastAttempt.Before(oldest.LastAttempt) {
|
|
oldest = ka
|
|
}
|
|
}
|
|
return oldest
|
|
}
|
|
|
|
// adds the address to a "new" bucket. if its already in one,
|
|
// it only adds it probabilistically
|
|
func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error {
|
|
if addr == nil || src == nil {
|
|
return ErrAddrBookNilAddr{addr, src}
|
|
}
|
|
|
|
if err := addr.Valid(); err != nil {
|
|
return ErrAddrBookInvalidAddr{Addr: addr, AddrErr: err}
|
|
}
|
|
|
|
if _, ok := a.badPeers[addr.ID]; ok {
|
|
return ErrAddressBanned{addr}
|
|
}
|
|
|
|
if _, ok := a.privateIDs[addr.ID]; ok {
|
|
return ErrAddrBookPrivate{addr}
|
|
}
|
|
|
|
if _, ok := a.privateIDs[src.ID]; ok {
|
|
return ErrAddrBookPrivateSrc{src}
|
|
}
|
|
|
|
// TODO: we should track ourAddrs by ID and by IP:PORT and refuse both.
|
|
if _, ok := a.ourAddrs[addr.String()]; ok {
|
|
return ErrAddrBookSelf{addr}
|
|
}
|
|
|
|
if a.routabilityStrict && !addr.Routable() {
|
|
return ErrAddrBookNonRoutable{addr}
|
|
}
|
|
|
|
ka := a.addrLookup[addr.ID]
|
|
if ka != nil {
|
|
// If its already old and the address ID's are the same, ignore it.
|
|
// Thereby avoiding issues with a node on the network attempting to change
|
|
// the IP of a known node ID. (Which could yield an eclipse attack on the node)
|
|
if ka.isOld() && ka.Addr.ID == addr.ID {
|
|
return nil
|
|
}
|
|
// Already in max new buckets.
|
|
if len(ka.Buckets) == maxNewBucketsPerAddress {
|
|
return nil
|
|
}
|
|
// The more entries we have, the less likely we are to add more.
|
|
factor := int32(2 * len(ka.Buckets))
|
|
if a.rand.Int31n(factor) != 0 {
|
|
return nil
|
|
}
|
|
} else {
|
|
ka = newKnownAddress(addr, src)
|
|
}
|
|
|
|
bucket, err := a.calcNewBucket(addr, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.addToNewBucket(ka, bucket)
|
|
}
|
|
|
|
func (a *addrBook) randomPickAddresses(bucketType byte, num int) []*p2p.NetAddress {
|
|
var buckets []map[string]*knownAddress
|
|
switch bucketType {
|
|
case bucketTypeNew:
|
|
buckets = a.bucketsNew
|
|
case bucketTypeOld:
|
|
buckets = a.bucketsOld
|
|
default:
|
|
panic("unexpected bucketType")
|
|
}
|
|
total := 0
|
|
for _, bucket := range buckets {
|
|
total += len(bucket)
|
|
}
|
|
addresses := make([]*knownAddress, 0, total)
|
|
for _, bucket := range buckets {
|
|
for _, ka := range bucket {
|
|
addresses = append(addresses, ka)
|
|
}
|
|
}
|
|
selection := make([]*p2p.NetAddress, 0, num)
|
|
chosenSet := make(map[string]bool, num)
|
|
rand.Shuffle(total, func(i, j int) {
|
|
addresses[i], addresses[j] = addresses[j], addresses[i]
|
|
})
|
|
for _, addr := range addresses {
|
|
if chosenSet[addr.Addr.String()] {
|
|
continue
|
|
}
|
|
chosenSet[addr.Addr.String()] = true
|
|
selection = append(selection, addr.Addr)
|
|
if len(selection) >= num {
|
|
return selection
|
|
}
|
|
}
|
|
return selection
|
|
}
|
|
|
|
// Make space in the new buckets by expiring the really bad entries.
|
|
// If no bad entries are available we remove the oldest.
|
|
func (a *addrBook) expireNew(bucketIdx int) {
|
|
for addrStr, ka := range a.bucketsNew[bucketIdx] {
|
|
// If an entry is bad, throw it away
|
|
if ka.isBad() {
|
|
a.Logger.Info(fmt.Sprintf("expiring bad address %v", addrStr))
|
|
a.removeFromBucket(ka, bucketTypeNew, bucketIdx)
|
|
return
|
|
}
|
|
}
|
|
|
|
// If we haven't thrown out a bad entry, throw out the oldest entry
|
|
oldest := a.pickOldest(bucketTypeNew, bucketIdx)
|
|
a.removeFromBucket(oldest, bucketTypeNew, bucketIdx)
|
|
}
|
|
|
|
// Promotes an address from new to old. If the destination bucket is full,
|
|
// demote the oldest one to a "new" bucket.
|
|
// TODO: Demote more probabilistically?
|
|
func (a *addrBook) moveToOld(ka *knownAddress) error {
|
|
// Sanity check
|
|
if ka.isOld() {
|
|
a.Logger.Error(fmt.Sprintf("Cannot promote address that is already old %v", ka))
|
|
return nil
|
|
}
|
|
if len(ka.Buckets) == 0 {
|
|
a.Logger.Error(fmt.Sprintf("Cannot promote address that isn't in any new buckets %v", ka))
|
|
return nil
|
|
}
|
|
|
|
// Remove from all (new) buckets.
|
|
a.removeFromAllBuckets(ka)
|
|
// It's officially old now.
|
|
ka.BucketType = bucketTypeOld
|
|
|
|
// Try to add it to its oldBucket destination.
|
|
oldBucketIdx, err := a.calcOldBucket(ka.Addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
added := a.addToOldBucket(ka, oldBucketIdx)
|
|
if !added {
|
|
// No room; move the oldest to a new bucket
|
|
oldest := a.pickOldest(bucketTypeOld, oldBucketIdx)
|
|
a.removeFromBucket(oldest, bucketTypeOld, oldBucketIdx)
|
|
newBucketIdx, err := a.calcNewBucket(oldest.Addr, oldest.Src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.addToNewBucket(oldest, newBucketIdx)
|
|
|
|
// Finally, add our ka to old bucket again.
|
|
added = a.addToOldBucket(ka, oldBucketIdx)
|
|
if !added {
|
|
a.Logger.Error(fmt.Sprintf("Could not re-add ka %v to oldBucketIdx %v", ka, oldBucketIdx))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *addrBook) removeAddress(addr *p2p.NetAddress) {
|
|
ka := a.addrLookup[addr.ID]
|
|
if ka == nil {
|
|
return
|
|
}
|
|
a.Logger.Info("Remove address from book", "addr", addr)
|
|
a.removeFromAllBuckets(ka)
|
|
}
|
|
|
|
func (a *addrBook) addBadPeer(addr *p2p.NetAddress, banTime time.Duration) bool {
|
|
// check it exists in addrbook
|
|
ka := a.addrLookup[addr.ID]
|
|
// check address is not already there
|
|
if ka == nil {
|
|
return false
|
|
}
|
|
|
|
if _, alreadyBadPeer := a.badPeers[addr.ID]; !alreadyBadPeer {
|
|
// add to bad peer list
|
|
ka.ban(banTime)
|
|
a.badPeers[addr.ID] = ka
|
|
a.Logger.Info("Add address to blacklist", "addr", addr)
|
|
}
|
|
return true
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// calculate bucket placements
|
|
|
|
// hash(key + sourcegroup + int64(hash(key + group + sourcegroup)) % bucket_per_group) % num_new_buckets
|
|
func (a *addrBook) calcNewBucket(addr, src *p2p.NetAddress) (int, error) {
|
|
data1 := []byte{}
|
|
data1 = append(data1, []byte(a.key)...)
|
|
data1 = append(data1, []byte(a.groupKey(addr))...)
|
|
data1 = append(data1, []byte(a.groupKey(src))...)
|
|
hash1, err := a.hash(data1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
hash64 := binary.BigEndian.Uint64(hash1)
|
|
hash64 %= newBucketsPerGroup
|
|
var hashbuf [8]byte
|
|
binary.BigEndian.PutUint64(hashbuf[:], hash64)
|
|
data2 := []byte{}
|
|
data2 = append(data2, []byte(a.key)...)
|
|
data2 = append(data2, a.groupKey(src)...)
|
|
data2 = append(data2, hashbuf[:]...)
|
|
|
|
hash2, err := a.hash(data2)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
result := int(binary.BigEndian.Uint64(hash2) % newBucketCount)
|
|
return result, nil
|
|
}
|
|
|
|
// hash(key + group + int64(hash(key + addr)) % buckets_per_group) % num_old_buckets
|
|
func (a *addrBook) calcOldBucket(addr *p2p.NetAddress) (int, error) {
|
|
data1 := []byte{}
|
|
data1 = append(data1, []byte(a.key)...)
|
|
data1 = append(data1, []byte(addr.String())...)
|
|
hash1, err := a.hash(data1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
hash64 := binary.BigEndian.Uint64(hash1)
|
|
hash64 %= oldBucketsPerGroup
|
|
var hashbuf [8]byte
|
|
binary.BigEndian.PutUint64(hashbuf[:], hash64)
|
|
data2 := []byte{}
|
|
data2 = append(data2, []byte(a.key)...)
|
|
data2 = append(data2, a.groupKey(addr)...)
|
|
data2 = append(data2, hashbuf[:]...)
|
|
|
|
hash2, err := a.hash(data2)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
result := int(binary.BigEndian.Uint64(hash2) % oldBucketCount)
|
|
return result, nil
|
|
}
|
|
|
|
// Return a string representing the network group of this address.
|
|
// This is the /16 for IPv4 (e.g. 1.2.0.0), the /32 (/36 for he.net) for IPv6, the string
|
|
// "local" for a local address and the string "unroutable" for an unroutable
|
|
// address.
|
|
func (a *addrBook) groupKey(na *p2p.NetAddress) string {
|
|
return groupKeyFor(na, a.routabilityStrict)
|
|
}
|
|
|
|
func groupKeyFor(na *p2p.NetAddress, routabilityStrict bool) string {
|
|
if routabilityStrict && na.Local() {
|
|
return "local"
|
|
}
|
|
if routabilityStrict && !na.Routable() {
|
|
return "unroutable"
|
|
}
|
|
|
|
if ipv4 := na.IP.To4(); ipv4 != nil {
|
|
return na.IP.Mask(net.CIDRMask(16, 32)).String()
|
|
}
|
|
|
|
if na.RFC6145() || na.RFC6052() {
|
|
// last four bytes are the ip address
|
|
ip := na.IP[12:16]
|
|
return ip.Mask(net.CIDRMask(16, 32)).String()
|
|
}
|
|
|
|
if na.RFC3964() {
|
|
ip := na.IP[2:6]
|
|
return ip.Mask(net.CIDRMask(16, 32)).String()
|
|
}
|
|
|
|
if na.RFC4380() {
|
|
// teredo tunnels have the last 4 bytes as the v4 address XOR
|
|
// 0xff.
|
|
ip := net.IP(make([]byte, 4))
|
|
for i, byte := range na.IP[12:16] {
|
|
ip[i] = byte ^ 0xff
|
|
}
|
|
return ip.Mask(net.CIDRMask(16, 32)).String()
|
|
}
|
|
|
|
if na.OnionCatTor() {
|
|
// group is keyed off the first 4 bits of the actual onion key.
|
|
return fmt.Sprintf("tor:%d", na.IP[6]&((1<<4)-1))
|
|
}
|
|
|
|
// OK, so now we know ourselves to be a IPv6 address.
|
|
// bitcoind uses /32 for everything, except for Hurricane Electric's
|
|
// (he.net) IP range, which it uses /36 for.
|
|
bits := 32
|
|
heNet := &net.IPNet{IP: net.ParseIP("2001:470::"), Mask: net.CIDRMask(32, 128)}
|
|
if heNet.Contains(na.IP) {
|
|
bits = 36
|
|
}
|
|
ipv6Mask := net.CIDRMask(bits, 128)
|
|
return na.IP.Mask(ipv6Mask).String()
|
|
}
|
|
|
|
func (a *addrBook) hash(b []byte) ([]byte, error) {
|
|
hasher, err := highwayhash.New64(a.hashKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hasher.Write(b)
|
|
return hasher.Sum(nil), nil
|
|
}
|