Skip to content

Commit a36834f

Browse files
committed
Merge #9037: net: Add test-before-evict discipline to addrman
e68172e Add test-before-evict discipline to addrman (Ethan Heilman) Pull request description: This change implement countermeasures 3 (test-before-evict) suggested in our paper: ["Eclipse Attacks on Bitcoin’s Peer-to-Peer Network"](http://cs-people.bu.edu/heilman/eclipse/). # Design: A collision occurs when an address, addr1, is being moved to the tried table from the new table, but maps to a position in the tried table which already contains an address (addr2). The current behavior is that addr1 would evict addr2 from the tried table. This change ensures that during a collision, addr1 is not inserted into tried but instead inserted into a buffer (setTriedCollisions). The to-be-evicted address, addr2, is then tested by [a feeler connection](bitcoin/bitcoin#8282). If addr2 is found to be online, we remove addr1 from the buffer and addr2 is not evicted, on the other hand if addr2 is found be offline it is replaced by addr1. An additional small advantage of this change is that, as no more than ten addresses can be in the test buffer at once, and addresses are only cleared one at a time from the test buffer (at 2 minute intervals), thus an attacker is forced to wait at least two minutes to insert a new address into tried after filling up the test buffer. This rate limits an attacker attempting to launch an eclipse attack. # Risk mitigation: - To prevent this functionality from being used as a DoS vector, we limit the number of addresses which are to be tested to ten. If we have more than ten addresses to test, we drop new addresses being added to tried if they would evict an address. Since the feeler thread only creates one new connection every 2 minutes the additional network overhead is limited. - An address in tried gains immunity from tests for 4 hours after it has been tested or successfully connected to. # Tests: This change includes additional addrman unittests which test this behavior. I ran an instance of this change with a much smaller tried table (2 buckets of 64 addresses) so that collisions were much more likely and observed evictions. ``` 2016-10-27 07:20:26 Swapping 208.12.64.252:8333 for 68.62.95.247:8333 in tried table 2016-10-27 07:20:26 Moving 208.12.64.252:8333 to tried ``` I documented tests we ran against similar earlier versions of this change in #6355. # Security Benefit This is was originally posted in PR #8282 see [this comment for full details](bitcoin/bitcoin#8282 (comment)). To determine the security benefit of these larger numbers of IPs in the tried table I modeled the attack presented in [Eclipse Attacks on Bitcoin’s Peer-to-Peer Network](https://eprint.iacr.org/2015/263). ![attackergraph40000-10-1000short-line](https://cloud.githubusercontent.com/assets/274814/17366828/372af458-595b-11e6-81e5-2c9f97282305.png) **Default node:** 595 attacker IPs for ~50% attack success. **Default node + test-before-evict:** 620 attacker IPs for ~50% attack success. **Feeler node:** 5540 attacker IPs for ~50% attack success. **Feeler node + test-before-evict:** 8600 attacker IPs for ~50% attack success. The node running feeler connections has 10 times as many online IP addresses in its tried table making an attack 10 times harder (i.e. requiring the an attacker require 10 times as many IP addresses in different /16s). Adding test-before-evict increases resistance of the node by an additional 3000 attacker IP addresses. Below I graph the attack over even greater attacker resources (i.e. more attacker controled IP addresses). Note that test-before-evict maintains some security far longer even against an attacker with 50,000 IPs. If this node had a larger tried table test-before-evict could greatly boost a nodes resistance to eclipse attacks. ![attacker graph long view](https://cloud.githubusercontent.com/assets/274814/17367108/96f46d64-595c-11e6-91cd-edba160598e7.png) Tree-SHA512: fdad4d26aadeaad9bcdc71929b3eb4e1f855b3ee3541fbfbe25dca8d7d0a1667815402db0cb4319db6bd3fcd32d67b5bbc0e12045c4252d62d6239b7d77c4395
2 parents 20e3b9a + e68172e commit a36834f

File tree

4 files changed

+319
-19
lines changed

4 files changed

+319
-19
lines changed

src/addrman.cpp

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ void CAddrMan::MakeTried(CAddrInfo& info, int nId)
187187
info.fInTried = true;
188188
}
189189

190-
void CAddrMan::Good_(const CService& addr, int64_t nTime)
190+
void CAddrMan::Good_(const CService& addr, bool test_before_evict, int64_t nTime)
191191
{
192192
int nId;
193193

@@ -233,10 +233,22 @@ void CAddrMan::Good_(const CService& addr, int64_t nTime)
233233
if (nUBucket == -1)
234234
return;
235235

236-
LogPrint(BCLog::ADDRMAN, "Moving %s to tried\n", addr.ToString());
236+
// which tried bucket to move the entry to
237+
int tried_bucket = info.GetTriedBucket(nKey);
238+
int tried_bucket_pos = info.GetBucketPosition(nKey, false, tried_bucket);
239+
240+
// Will moving this address into tried evict another entry?
241+
if (test_before_evict && (vvTried[tried_bucket][tried_bucket_pos] != -1)) {
242+
LogPrint(BCLog::ADDRMAN, "addrman", "Collision inserting element into tried table, moving %s to m_tried_collisions=%d\n", addr.ToString(), m_tried_collisions.size());
243+
if (m_tried_collisions.size() < ADDRMAN_SET_TRIED_COLLISION_SIZE) {
244+
m_tried_collisions.insert(nId);
245+
}
246+
} else {
247+
LogPrint(BCLog::ADDRMAN, "Moving %s to tried\n", addr.ToString());
237248

238-
// move nId to the tried tables
239-
MakeTried(info, nId);
249+
// move nId to the tried tables
250+
MakeTried(info, nId);
251+
}
240252
}
241253

242254
bool CAddrMan::Add_(const CAddress& addr, const CNetAddr& source, int64_t nTimePenalty)
@@ -521,3 +533,82 @@ void CAddrMan::SetServices_(const CService& addr, ServiceFlags nServices)
521533
int CAddrMan::RandomInt(int nMax){
522534
return GetRandInt(nMax);
523535
}
536+
537+
void CAddrMan::ResolveCollisions_()
538+
{
539+
for (std::set<int>::iterator it = m_tried_collisions.begin(); it != m_tried_collisions.end();) {
540+
int id_new = *it;
541+
542+
bool erase_collision = false;
543+
544+
// If id_new not found in mapInfo remove it from m_tried_collisions
545+
if (mapInfo.count(id_new) != 1) {
546+
erase_collision = true;
547+
} else {
548+
CAddrInfo& info_new = mapInfo[id_new];
549+
550+
// Which tried bucket to move the entry to.
551+
int tried_bucket = info_new.GetTriedBucket(nKey);
552+
int tried_bucket_pos = info_new.GetBucketPosition(nKey, false, tried_bucket);
553+
if (!info_new.IsValid()) { // id_new may no longer map to a valid address
554+
erase_collision = true;
555+
} else if (vvTried[tried_bucket][tried_bucket_pos] != -1) { // The position in the tried bucket is not empty
556+
557+
// Get the to-be-evicted address that is being tested
558+
int id_old = vvTried[tried_bucket][tried_bucket_pos];
559+
CAddrInfo& info_old = mapInfo[id_old];
560+
561+
// Has successfully connected in last X hours
562+
if (GetAdjustedTime() - info_old.nLastSuccess < ADDRMAN_REPLACEMENT_HOURS*(60*60)) {
563+
erase_collision = true;
564+
} else if (GetAdjustedTime() - info_old.nLastTry < ADDRMAN_REPLACEMENT_HOURS*(60*60)) { // attempted to connect and failed in last X hours
565+
566+
// Give address at least 60 seconds to successfully connect
567+
if (GetAdjustedTime() - info_old.nLastTry > 60) {
568+
LogPrint(BCLog::ADDRMAN, "addrman", "Swapping %s for %s in tried table\n", info_new.ToString(), info_old.ToString());
569+
570+
// Replaces an existing address already in the tried table with the new address
571+
Good_(info_new, false, GetAdjustedTime());
572+
erase_collision = true;
573+
}
574+
}
575+
} else { // Collision is not actually a collision anymore
576+
Good_(info_new, false, GetAdjustedTime());
577+
erase_collision = true;
578+
}
579+
}
580+
581+
if (erase_collision) {
582+
m_tried_collisions.erase(it++);
583+
} else {
584+
it++;
585+
}
586+
}
587+
}
588+
589+
CAddrInfo CAddrMan::SelectTriedCollision_()
590+
{
591+
if (m_tried_collisions.size() == 0) return CAddrInfo();
592+
593+
std::set<int>::iterator it = m_tried_collisions.begin();
594+
595+
// Selects a random element from m_tried_collisions
596+
std::advance(it, GetRandInt(m_tried_collisions.size()));
597+
int id_new = *it;
598+
599+
// If id_new not found in mapInfo remove it from m_tried_collisions
600+
if (mapInfo.count(id_new) != 1) {
601+
m_tried_collisions.erase(it);
602+
return CAddrInfo();
603+
}
604+
605+
CAddrInfo& newInfo = mapInfo[id_new];
606+
607+
// which tried bucket to move the entry to
608+
int tried_bucket = newInfo.GetTriedBucket(nKey);
609+
int tried_bucket_pos = newInfo.GetBucketPosition(nKey, false, tried_bucket);
610+
611+
int id_old = vvTried[tried_bucket][tried_bucket_pos];
612+
613+
return mapInfo[id_old];
614+
}

src/addrman.h

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ class CAddrInfo : public CAddress
165165
//! ... in at least this many days
166166
#define ADDRMAN_MIN_FAIL_DAYS 7
167167

168+
//! how recent a successful connection should be before we allow an address to be evicted from tried
169+
#define ADDRMAN_REPLACEMENT_HOURS 4
170+
168171
//! the maximum percentage of nodes to return in a getaddr call
169172
#define ADDRMAN_GETADDR_MAX_PCT 23
170173

@@ -176,6 +179,9 @@ class CAddrInfo : public CAddress
176179
#define ADDRMAN_NEW_BUCKET_COUNT (1 << ADDRMAN_NEW_BUCKET_COUNT_LOG2)
177180
#define ADDRMAN_BUCKET_SIZE (1 << ADDRMAN_BUCKET_SIZE_LOG2)
178181

182+
//! the maximum number of tried addr collisions to store
183+
#define ADDRMAN_SET_TRIED_COLLISION_SIZE 10
184+
179185
/**
180186
* Stochastical (IP) address manager
181187
*/
@@ -212,6 +218,9 @@ class CAddrMan
212218
//! last time Good was called (memory only)
213219
int64_t nLastGood;
214220

221+
//! Holds addrs inserted into tried table that collide with existing entries. Test-before-evict discpline used to resolve these collisions.
222+
std::set<int> m_tried_collisions;
223+
215224
protected:
216225
//! secret key to randomize bucket select with
217226
uint256 nKey;
@@ -239,7 +248,7 @@ class CAddrMan
239248
void ClearNew(int nUBucket, int nUBucketPos);
240249

241250
//! Mark an entry "good", possibly moving it from "new" to "tried".
242-
void Good_(const CService &addr, int64_t nTime);
251+
void Good_(const CService &addr, bool test_before_evict, int64_t time);
243252

244253
//! Add an entry to the "new" table.
245254
bool Add_(const CAddress &addr, const CNetAddr& source, int64_t nTimePenalty);
@@ -250,6 +259,12 @@ class CAddrMan
250259
//! Select an address to connect to, if newOnly is set to true, only the new table is selected from.
251260
CAddrInfo Select_(bool newOnly);
252261

262+
//! See if any to-be-evicted tried table entries have been tested and if so resolve the collisions.
263+
void ResolveCollisions_();
264+
265+
//! Return a random to-be-evicted tried table address.
266+
CAddrInfo SelectTriedCollision_();
267+
253268
//! Wraps GetRandInt to allow tests to override RandomInt and make it determinismistic.
254269
virtual int RandomInt(int nMax);
255270

@@ -537,11 +552,11 @@ class CAddrMan
537552
}
538553

539554
//! Mark an entry as accessible.
540-
void Good(const CService &addr, int64_t nTime = GetAdjustedTime())
555+
void Good(const CService &addr, bool test_before_evict = true, int64_t nTime = GetAdjustedTime())
541556
{
542557
LOCK(cs);
543558
Check();
544-
Good_(addr, nTime);
559+
Good_(addr, test_before_evict, nTime);
545560
Check();
546561
}
547562

@@ -554,6 +569,28 @@ class CAddrMan
554569
Check();
555570
}
556571

572+
//! See if any to-be-evicted tried table entries have been tested and if so resolve the collisions.
573+
void ResolveCollisions()
574+
{
575+
LOCK(cs);
576+
Check();
577+
ResolveCollisions_();
578+
Check();
579+
}
580+
581+
//! Randomly select an address in tried that another address is attempting to evict.
582+
CAddrInfo SelectTriedCollision()
583+
{
584+
CAddrInfo ret;
585+
{
586+
LOCK(cs);
587+
Check();
588+
ret = SelectTriedCollision_();
589+
Check();
590+
}
591+
return ret;
592+
}
593+
557594
/**
558595
* Choose an address to connect to.
559596
*/

src/net.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1828,11 +1828,18 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
18281828
}
18291829
}
18301830

1831+
addrman.ResolveCollisions();
1832+
18311833
int64_t nANow = GetAdjustedTime();
18321834
int nTries = 0;
18331835
while (!interruptNet)
18341836
{
1835-
CAddrInfo addr = addrman.Select(fFeeler);
1837+
CAddrInfo addr = addrman.SelectTriedCollision();
1838+
1839+
// SelectTriedCollision returns an invalid address if it is empty.
1840+
if (!fFeeler || !addr.IsValid()) {
1841+
addr = addrman.Select(fFeeler);
1842+
}
18361843

18371844
// if we selected an invalid address, restart
18381845
if (!addr.IsValid() || setConnected.count(addr.GetGroup()) || IsLocal(addr))

0 commit comments

Comments
 (0)