package pex import ( "encoding/hex" "fmt" "io/ioutil" "math" "net" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" tmrand "github.com/tendermint/tendermint/libs/rand" "github.com/tendermint/tendermint/p2p" ) // FIXME These tests should not rely on .(*addrBook) assertions func TestAddrBookPickAddress(t *testing.T) { fname := createTempFileName(t, "addrbook_test") // 0 addresses book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) assert.Zero(t, book.Size()) addr := book.PickAddress(50) assert.Nil(t, addr, "expected no address") randAddrs := randNetAddressPairs(t, 1) addrSrc := randAddrs[0] err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) // pick an address when we only have new address addr = book.PickAddress(0) assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(50) assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(100) assert.NotNil(t, addr, "expected an address") // pick an address when we only have old address book.MarkGood(addrSrc.addr.ID) addr = book.PickAddress(0) assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(50) assert.NotNil(t, addr, "expected an address") // in this case, nNew==0 but we biased 100% to new, so we return nil addr = book.PickAddress(100) assert.Nil(t, addr, "did not expected an address") } func TestAddrBookSaveLoad(t *testing.T) { fname := createTempFileName(t, "addrbook_test") // 0 addresses book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) book.Save() book = NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) err := book.Start() require.NoError(t, err) assert.True(t, book.Empty()) // 100 addresses randAddrs := randNetAddressPairs(t, 100) for _, addrSrc := range randAddrs { err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) } assert.Equal(t, 100, book.Size()) book.Save() book = NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) err = book.Start() require.NoError(t, err) assert.Equal(t, 100, book.Size()) } func TestAddrBookLookup(t *testing.T) { fname := createTempFileName(t, "addrbook_test") randAddrs := randNetAddressPairs(t, 100) book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) for _, addrSrc := range randAddrs { addr := addrSrc.addr src := addrSrc.src err := book.AddAddress(addr, src) require.NoError(t, err) ka := book.HasAddress(addr) assert.True(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) } } func TestAddrBookPromoteToOld(t *testing.T) { fname := createTempFileName(t, "addrbook_test") randAddrs := randNetAddressPairs(t, 100) book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) for _, addrSrc := range randAddrs { err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) } // Attempt all addresses. for _, addrSrc := range randAddrs { book.MarkAttempt(addrSrc.addr) } // Promote half of them for i, addrSrc := range randAddrs { if i%2 == 0 { book.MarkGood(addrSrc.addr.ID) } } // TODO: do more testing :) selection := book.GetSelection() t.Logf("selection: %v", selection) if len(selection) > book.Size() { t.Errorf("selection could not be bigger than the book") } selection = book.GetSelectionWithBias(30) t.Logf("selection: %v", selection) if len(selection) > book.Size() { t.Errorf("selection with bias could not be bigger than the book") } assert.Equal(t, book.Size(), 100, "expecting book size to be 100") } func TestAddrBookHandlesDuplicates(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) randAddrs := randNetAddressPairs(t, 100) differentSrc := randIPv4Address(t) for _, addrSrc := range randAddrs { err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) err = book.AddAddress(addrSrc.addr, addrSrc.src) // duplicate require.NoError(t, err) err = book.AddAddress(addrSrc.addr, differentSrc) // different src require.NoError(t, err) } assert.Equal(t, 100, book.Size()) } type netAddressPair struct { addr *p2p.NetAddress src *p2p.NetAddress } func randNetAddressPairs(t *testing.T, n int) []netAddressPair { randAddrs := make([]netAddressPair, n) for i := 0; i < n; i++ { randAddrs[i] = netAddressPair{addr: randIPv4Address(t), src: randIPv4Address(t)} } return randAddrs } func randIPv4Address(t *testing.T) *p2p.NetAddress { for { ip := fmt.Sprintf("%v.%v.%v.%v", tmrand.Intn(254)+1, tmrand.Intn(255), tmrand.Intn(255), tmrand.Intn(255), ) port := tmrand.Intn(65535-1) + 1 id := p2p.NodeID(hex.EncodeToString(tmrand.Bytes(p2p.NodeIDByteLength))) idAddr := p2p.IDAddressString(id, fmt.Sprintf("%v:%v", ip, port)) addr, err := p2p.NewNetAddressString(idAddr) assert.Nil(t, err, "error generating rand network address") if addr.Routable() { return addr } } } func TestAddrBookRemoveAddress(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) addr := randIPv4Address(t) err := book.AddAddress(addr, addr) require.NoError(t, err) assert.Equal(t, 1, book.Size()) book.RemoveAddress(addr) assert.Equal(t, 0, book.Size()) nonExistingAddr := randIPv4Address(t) book.RemoveAddress(nonExistingAddr) assert.Equal(t, 0, book.Size()) } func TestAddrBookGetSelectionWithOneMarkedGood(t *testing.T) { // create a book with 10 addresses, 1 good/old and 9 new book, _ := createAddrBookWithMOldAndNNewAddrs(t, 1, 9) addrs := book.GetSelectionWithBias(biasToSelectNewPeers) assert.NotNil(t, addrs) assertMOldAndNNewAddrsInSelection(t, 1, 9, addrs, book) } func TestAddrBookGetSelectionWithOneNotMarkedGood(t *testing.T) { // create a book with 10 addresses, 9 good/old and 1 new book, _ := createAddrBookWithMOldAndNNewAddrs(t, 9, 1) addrs := book.GetSelectionWithBias(biasToSelectNewPeers) assert.NotNil(t, addrs) assertMOldAndNNewAddrsInSelection(t, 9, 1, addrs, book) } func TestAddrBookGetSelectionReturnsNilWhenAddrBookIsEmpty(t *testing.T) { book, _ := createAddrBookWithMOldAndNNewAddrs(t, 0, 0) addrs := book.GetSelectionWithBias(biasToSelectNewPeers) assert.Nil(t, addrs) } func TestAddrBookGetSelection(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) // 1) empty book assert.Empty(t, book.GetSelection()) // 2) add one address addr := randIPv4Address(t) err := book.AddAddress(addr, addr) require.NoError(t, err) assert.Equal(t, 1, len(book.GetSelection())) assert.Equal(t, addr, book.GetSelection()[0]) // 3) add a bunch of addresses randAddrs := randNetAddressPairs(t, 100) for _, addrSrc := range randAddrs { err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) } // check there is no duplicates addrs := make(map[string]*p2p.NetAddress) selection := book.GetSelection() for _, addr := range selection { if dup, ok := addrs[addr.String()]; ok { t.Fatalf("selection %v contains duplicates %v", selection, dup) } addrs[addr.String()] = addr } if len(selection) > book.Size() { t.Errorf("selection %v could not be bigger than the book", selection) } } func TestAddrBookGetSelectionWithBias(t *testing.T) { const biasTowardsNewAddrs = 30 fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) // 1) empty book selection := book.GetSelectionWithBias(biasTowardsNewAddrs) assert.Empty(t, selection) // 2) add one address addr := randIPv4Address(t) err := book.AddAddress(addr, addr) require.NoError(t, err) selection = book.GetSelectionWithBias(biasTowardsNewAddrs) assert.Equal(t, 1, len(selection)) assert.Equal(t, addr, selection[0]) // 3) add a bunch of addresses randAddrs := randNetAddressPairs(t, 100) for _, addrSrc := range randAddrs { err := book.AddAddress(addrSrc.addr, addrSrc.src) require.NoError(t, err) } // check there is no duplicates addrs := make(map[string]*p2p.NetAddress) selection = book.GetSelectionWithBias(biasTowardsNewAddrs) for _, addr := range selection { if dup, ok := addrs[addr.String()]; ok { t.Fatalf("selection %v contains duplicates %v", selection, dup) } addrs[addr.String()] = addr } if len(selection) > book.Size() { t.Fatalf("selection %v could not be bigger than the book", selection) } // 4) mark 80% of the addresses as good randAddrsLen := len(randAddrs) for i, addrSrc := range randAddrs { if int((float64(i)/float64(randAddrsLen))*100) >= 20 { book.MarkGood(addrSrc.addr.ID) } } selection = book.GetSelectionWithBias(biasTowardsNewAddrs) // check that ~70% of addresses returned are good good := 0 for _, addr := range selection { if book.IsGood(addr) { good++ } } got, expected := int((float64(good)/float64(len(selection)))*100), 100-biasTowardsNewAddrs // compute some slack to protect against small differences due to rounding: slack := int(math.Round(float64(100) / float64(len(selection)))) if got > expected+slack { t.Fatalf( "got more good peers (%% got: %d, %% expected: %d, number of good addrs: %d, total: %d)", got, expected, good, len(selection), ) } if got < expected-slack { t.Fatalf( "got fewer good peers (%% got: %d, %% expected: %d, number of good addrs: %d, total: %d)", got, expected, good, len(selection), ) } } func TestAddrBookHasAddress(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) addr := randIPv4Address(t) err := book.AddAddress(addr, addr) require.NoError(t, err) assert.True(t, book.HasAddress(addr)) book.RemoveAddress(addr) assert.False(t, book.HasAddress(addr)) } func testCreatePrivateAddrs(t *testing.T, numAddrs int) ([]*p2p.NetAddress, []string) { t.Helper() addrs := make([]*p2p.NetAddress, numAddrs) for i := 0; i < numAddrs; i++ { addrs[i] = randIPv4Address(t) } private := make([]string, numAddrs) for i, addr := range addrs { private[i] = string(addr.ID) } return addrs, private } func TestBanBadPeers(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) addr := randIPv4Address(t) _ = book.AddAddress(addr, addr) book.MarkBad(addr, 1*time.Second) // addr should not reachable assert.False(t, book.HasAddress(addr)) assert.True(t, book.IsBanned(addr)) err := book.AddAddress(addr, addr) // book should not add address from the blacklist assert.Error(t, err) time.Sleep(1 * time.Second) book.ReinstateBadPeers() // address should be reinstated in the new bucket assert.EqualValues(t, 1, book.Size()) assert.True(t, book.HasAddress(addr)) assert.False(t, book.IsGood(addr)) } func TestAddrBookEmpty(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) // Check that empty book is empty require.True(t, book.Empty()) // Check that book with our address is empty book.AddOurAddress(randIPv4Address(t)) require.True(t, book.Empty()) // Check that book with private addrs is empty _, privateIds := testCreatePrivateAddrs(t, 5) book.AddPrivateIDs(privateIds) require.True(t, book.Empty()) // Check that book with address is not empty err := book.AddAddress(randIPv4Address(t), randIPv4Address(t)) require.NoError(t, err) require.False(t, book.Empty()) } func TestPrivatePeers(t *testing.T) { fname := createTempFileName(t, "addrbook_test") book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) addrs, private := testCreatePrivateAddrs(t, 10) book.AddPrivateIDs(private) // private addrs must not be added for _, addr := range addrs { err := book.AddAddress(addr, addr) if assert.Error(t, err) { _, ok := err.(ErrAddrBookPrivate) assert.True(t, ok) } } // addrs coming from private peers must not be added err := book.AddAddress(randIPv4Address(t), addrs[0]) if assert.Error(t, err) { _, ok := err.(ErrAddrBookPrivateSrc) assert.True(t, ok) } } func testAddrBookAddressSelection(t *testing.T, bookSize int) { // generate all combinations of old (m) and new addresses for nBookOld := 0; nBookOld <= bookSize; nBookOld++ { nBookNew := bookSize - nBookOld dbgStr := fmt.Sprintf("book of size %d (new %d, old %d)", bookSize, nBookNew, nBookOld) // create book and get selection book, _ := createAddrBookWithMOldAndNNewAddrs(t, nBookOld, nBookNew) addrs := book.GetSelectionWithBias(biasToSelectNewPeers) assert.NotNil(t, addrs, "%s - expected a non-nil selection", dbgStr) nAddrs := len(addrs) assert.NotZero(t, nAddrs, "%s - expected at least one address in selection", dbgStr) // check there's no nil addresses for _, addr := range addrs { if addr == nil { t.Fatalf("%s - got nil address in selection %v", dbgStr, addrs) } } // XXX: shadowing nOld, nNew := countOldAndNewAddrsInSelection(addrs, book) // Given: // n - num new addrs, m - num old addrs // k - num new addrs expected in the beginning (based on bias %) // i=min(n, max(k,r-m)), aka expNew // j=min(m, r-i), aka expOld // // We expect this layout: // indices: 0...i-1 i...i+j-1 // addresses: N0..Ni-1 O0..Oj-1 // // There is at least one partition and at most three. var ( k = percentageOfNum(biasToSelectNewPeers, nAddrs) expNew = tmmath.MinInt(nNew, tmmath.MaxInt(k, nAddrs-nBookOld)) expOld = tmmath.MinInt(nOld, nAddrs-expNew) ) // Verify that the number of old and new addresses are as expected if nNew != expNew { t.Fatalf("%s - expected new addrs %d, got %d", dbgStr, expNew, nNew) } if nOld != expOld { t.Fatalf("%s - expected old addrs %d, got %d", dbgStr, expOld, nOld) } // Verify that the order of addresses is as expected // Get the sequence types and lengths of the selection seqLens, seqTypes, err := analyseSelectionLayout(book, addrs) assert.NoError(t, err, "%s", dbgStr) // Build a list with the expected lengths of partitions and another with the expected types, e.g.: // expSeqLens = [10, 22], expSeqTypes = [1, 2] // means we expect 10 new (type 1) addresses followed by 22 old (type 2) addresses. var expSeqLens []int var expSeqTypes []int switch { case expOld == 0: // all new addresses expSeqLens = []int{nAddrs} expSeqTypes = []int{1} case expNew == 0: // all old addresses expSeqLens = []int{nAddrs} expSeqTypes = []int{2} case nAddrs-expNew-expOld == 0: // new addresses, old addresses expSeqLens = []int{expNew, expOld} expSeqTypes = []int{1, 2} } assert.Equal(t, expSeqLens, seqLens, "%s - expected sequence lengths of old/new %v, got %v", dbgStr, expSeqLens, seqLens) assert.Equal(t, expSeqTypes, seqTypes, "%s - expected sequence types (1-new, 2-old) was %v, got %v", dbgStr, expSeqTypes, seqTypes) } } func TestMultipleAddrBookAddressSelection(t *testing.T) { // test books with smaller size, < N const N = 32 for bookSize := 1; bookSize < N; bookSize++ { testAddrBookAddressSelection(t, bookSize) } // Test for two books with sizes from following ranges ranges := [...][]int{{33, 100}, {100, 175}} bookSizes := make([]int, 0, len(ranges)) for _, r := range ranges { bookSizes = append(bookSizes, tmrand.Intn(r[1]-r[0])+r[0]) } t.Logf("Testing address selection for the following book sizes %v\n", bookSizes) for _, bookSize := range bookSizes { testAddrBookAddressSelection(t, bookSize) } } func TestAddrBookAddDoesNotOverwriteOldIP(t *testing.T) { fname := createTempFileName(t, "addrbook_test") // This test creates adds a peer to the address book and marks it good // It then attempts to override the peer's IP, by adding a peer with the same ID // but different IP. We distinguish the IP's by "RealIP" and "OverrideAttemptIP" peerID := "678503e6c8f50db7279c7da3cb9b072aac4bc0d5" peerRealIP := "1.1.1.1:26656" peerOverrideAttemptIP := "2.2.2.2:26656" SrcAddr := "b0dd378c3fbc4c156cd6d302a799f0d2e4227201@159.89.121.174:26656" // There is a chance that AddAddress will ignore the new peer its given. // So we repeat trying to override the peer several times, // to ensure we aren't in a case that got probabilistically ignored numOverrideAttempts := 10 peerRealAddr, err := p2p.NewNetAddressString(peerID + "@" + peerRealIP) require.Nil(t, err) peerOverrideAttemptAddr, err := p2p.NewNetAddressString(peerID + "@" + peerOverrideAttemptIP) require.Nil(t, err) src, err := p2p.NewNetAddressString(SrcAddr) require.Nil(t, err) book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) err = book.AddAddress(peerRealAddr, src) require.Nil(t, err) book.MarkAttempt(peerRealAddr) book.MarkGood(peerRealAddr.ID) // Double check that adding a peer again doesn't error err = book.AddAddress(peerRealAddr, src) require.Nil(t, err) // Try changing ip but keeping the same node id. (change 1.1.1.1 to 2.2.2.2) // This should just be ignored, and not error. for i := 0; i < numOverrideAttempts; i++ { err = book.AddAddress(peerOverrideAttemptAddr, src) require.Nil(t, err) } // Now check that the IP was not overridden. // This is done by sampling several peers from addr book // and ensuring they all have the correct IP. // In the expected functionality, this test should only have 1 Peer, hence will pass. for i := 0; i < numOverrideAttempts; i++ { selection := book.GetSelection() for _, addr := range selection { require.Equal(t, addr.IP, peerRealAddr.IP) } } } func TestAddrBookGroupKey(t *testing.T) { // non-strict routability testCases := []struct { name string ip string expKey string }{ // IPv4 normal. {"ipv4 normal class a", "12.1.2.3", "12.1.0.0"}, {"ipv4 normal class b", "173.1.2.3", "173.1.0.0"}, {"ipv4 normal class c", "196.1.2.3", "196.1.0.0"}, // IPv6/IPv4 translations. {"ipv6 rfc3964 with ipv4 encap", "2002:0c01:0203::", "12.1.0.0"}, {"ipv6 rfc4380 toredo ipv4", "2001:0:1234::f3fe:fdfc", "12.1.0.0"}, {"ipv6 rfc6052 well-known prefix with ipv4", "64:ff9b::0c01:0203", "12.1.0.0"}, {"ipv6 rfc6145 translated ipv4", "::ffff:0:0c01:0203", "12.1.0.0"}, // Tor. {"ipv6 tor onioncat", "fd87:d87e:eb43:1234::5678", "tor:2"}, {"ipv6 tor onioncat 2", "fd87:d87e:eb43:1245::6789", "tor:2"}, {"ipv6 tor onioncat 3", "fd87:d87e:eb43:1345::6789", "tor:3"}, // IPv6 normal. {"ipv6 normal", "2602:100::1", "2602:100::"}, {"ipv6 normal 2", "2602:0100::1234", "2602:100::"}, {"ipv6 hurricane electric", "2001:470:1f10:a1::2", "2001:470:1000::"}, {"ipv6 hurricane electric 2", "2001:0470:1f10:a1::2", "2001:470:1000::"}, } for i, tc := range testCases { nip := net.ParseIP(tc.ip) key := groupKeyFor(p2p.NewNetAddressIPPort(nip, 26656), false) assert.Equal(t, tc.expKey, key, "#%d", i) } // strict routability testCases = []struct { name string ip string expKey string }{ // Local addresses. {"ipv4 localhost", "127.0.0.1", "local"}, {"ipv6 localhost", "::1", "local"}, {"ipv4 zero", "0.0.0.0", "local"}, {"ipv4 first octet zero", "0.1.2.3", "local"}, // Unroutable addresses. {"ipv4 invalid bcast", "255.255.255.255", "unroutable"}, {"ipv4 rfc1918 10/8", "10.1.2.3", "unroutable"}, {"ipv4 rfc1918 172.16/12", "172.16.1.2", "unroutable"}, {"ipv4 rfc1918 192.168/16", "192.168.1.2", "unroutable"}, {"ipv6 rfc3849 2001:db8::/32", "2001:db8::1234", "unroutable"}, {"ipv4 rfc3927 169.254/16", "169.254.1.2", "unroutable"}, {"ipv6 rfc4193 fc00::/7", "fc00::1234", "unroutable"}, {"ipv6 rfc4843 2001:10::/28", "2001:10::1234", "unroutable"}, {"ipv6 rfc4862 fe80::/64", "fe80::1234", "unroutable"}, } for i, tc := range testCases { nip := net.ParseIP(tc.ip) key := groupKeyFor(p2p.NewNetAddressIPPort(nip, 26656), true) assert.Equal(t, tc.expKey, key, "#%d", i) } } func assertMOldAndNNewAddrsInSelection(t *testing.T, m, n int, addrs []*p2p.NetAddress, book *addrBook) { nOld, nNew := countOldAndNewAddrsInSelection(addrs, book) assert.Equal(t, m, nOld, "old addresses") assert.Equal(t, n, nNew, "new addresses") } func createTempFileName(t *testing.T, prefix string) string { t.Helper() f, err := ioutil.TempFile("", prefix) if err != nil { panic(err) } fname := f.Name() if err := f.Close(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Remove(fname) }) return fname } func createAddrBookWithMOldAndNNewAddrs(t *testing.T, nOld, nNew int) (book *addrBook, fname string) { t.Helper() fname = createTempFileName(t, "addrbook_test") book = NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) assert.Zero(t, book.Size()) randAddrs := randNetAddressPairs(t, nOld) for _, addr := range randAddrs { err := book.AddAddress(addr.addr, addr.src) require.NoError(t, err) book.MarkGood(addr.addr.ID) } randAddrs = randNetAddressPairs(t, nNew) for _, addr := range randAddrs { err := book.AddAddress(addr.addr, addr.src) require.NoError(t, err) } return } func countOldAndNewAddrsInSelection(addrs []*p2p.NetAddress, book *addrBook) (nOld, nNew int) { for _, addr := range addrs { if book.IsGood(addr) { nOld++ } else { nNew++ } } return } // Analyse the layout of the selection specified by 'addrs' // Returns: // - seqLens - the lengths of the sequences of addresses of same type // - seqTypes - the types of sequences in selection func analyseSelectionLayout(book *addrBook, addrs []*p2p.NetAddress) (seqLens, seqTypes []int, err error) { // address types are: 0 - nil, 1 - new, 2 - old var ( prevType = 0 currentSeqLen = 0 ) for _, addr := range addrs { addrType := 0 if book.IsGood(addr) { addrType = 2 } else { addrType = 1 } if addrType != prevType && prevType != 0 { seqLens = append(seqLens, currentSeqLen) seqTypes = append(seqTypes, prevType) currentSeqLen = 0 } currentSeqLen++ prevType = addrType } seqLens = append(seqLens, currentSeqLen) seqTypes = append(seqTypes, prevType) return }