Skip to content

Commit b8aec5c

Browse files
laanwjPastaPastaPasta
authored andcommitted
Merge bitcoin#20197: p2p: protect onions in AttemptToEvictConnection(), add eviction protection test coverage
0cca08a Add unit test coverage for our onion peer eviction protection (Jon Atack) caa21f5 Protect onion+localhost peers in ProtectEvictionCandidatesByRatio() (Jon Atack) 8f1a53e Use EraseLastKElements() throughout SelectNodeToEvict() (Jon Atack) 8b1e156 Add m_inbound_onion to AttemptToEvictConnection() (Jon Atack) 72e30e8 Add unit tests for ProtectEvictionCandidatesByRatio() (Jon Atack) ca63b53 Use std::unordered_set instead of std::vector in IsEvicted() (Jon Atack) 41f84d5 Move peer eviction tests to a separate test file (Jon Atack) f126cbd Extract ProtectEvictionCandidatesByRatio from SelectNodeToEvict (Jon Atack) Pull request description: Now that bitcoin#19991 and bitcoin#20210 have been merged, we can determine inbound onion peers using `CNode::m_inbound_onion` and add it to the localhost peers protection in `AttemptToEvictConnection`, which was added in bitcoin#19670 to address issue bitcoin#19500. Update 28 February 2021: I've updated this to follow gmaxwell's suggestion in bitcoin#20197 (comment). This branch now protects up to 1/4 onion peers (connected via our tor control service), if any, sorted by longest uptime. If any (or all) onion slots remain after that operation, they are then allocated to protect localhost peers, or a minimum of 2 localhost peers in the case that no onion slots remain and 2 or more onion peers were protected, sorted as before by longest uptime. This patch also adds test coverage for the longest uptime, localhost, and onion peer eviction protection logic to build on the welcome initial unit testing of bitcoin#20477. Suggest reviewing the commits that move code with `colorMoved = dimmed-zebra` and `colorMovedWs = allow-indentation-change`. Closes bitcoin#11537. ACKs for top commit: laanwj: Code review ACK 0cca08a vasild: ACK 0cca08a Tree-SHA512: 2f5a63f942acaae7882920fc61f0185dcd51da85e5b736df9d1fc72343726dd17da740e02f30fa5dc5eb3b2d8345707aed96031bec143d48a2497a610aa19abd
1 parent dd92322 commit b8aec5c

File tree

6 files changed

+439
-169
lines changed

6 files changed

+439
-169
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ BITCOIN_TESTS =\
132132
test/merkleblock_tests.cpp \
133133
test/miner_tests.cpp \
134134
test/multisig_tests.cpp \
135+
test/net_peer_eviction_tests.cpp \
135136
test/net_tests.cpp \
136137
test/netbase_tests.cpp \
137138
test/pmt_tests.cpp \

src/net.cpp

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,12 @@ static bool CompareLocalHostTimeConnected(const NodeEvictionCandidate &a, const
935935
return a.nTimeConnected > b.nTimeConnected;
936936
}
937937

938+
static bool CompareOnionTimeConnected(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
939+
{
940+
if (a.m_is_onion != b.m_is_onion) return b.m_is_onion;
941+
return a.nTimeConnected > b.nTimeConnected;
942+
}
943+
938944
static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
939945
return a.nKeyedNetGroup < b.nKeyedNetGroup;
940946
}
@@ -965,13 +971,51 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
965971
return a.nTimeConnected > b.nTimeConnected;
966972
}
967973

968-
//! Sort an array by the specified comparator, then erase the last K elements.
969-
template<typename T, typename Comparator>
970-
static void EraseLastKElements(std::vector<T> &elements, Comparator comparator, size_t k)
974+
//! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
975+
template <typename T, typename Comparator>
976+
static void EraseLastKElements(
977+
std::vector<T>& elements, Comparator comparator, size_t k,
978+
std::function<bool(const NodeEvictionCandidate&)> predicate = [](const NodeEvictionCandidate& n) { return true; })
971979
{
972980
std::sort(elements.begin(), elements.end(), comparator);
973981
size_t eraseSize = std::min(k, elements.size());
974-
elements.erase(elements.end() - eraseSize, elements.end());
982+
elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
983+
}
984+
985+
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates)
986+
{
987+
// Protect the half of the remaining nodes which have been connected the longest.
988+
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
989+
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
990+
// these protected spots for onion and localhost peers, if any, even if they're not
991+
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
992+
// disadvantaged under our eviction criteria.
993+
const size_t initial_size = vEvictionCandidates.size();
994+
size_t total_protect_size = initial_size / 2;
995+
const size_t onion_protect_size = total_protect_size / 2;
996+
997+
if (onion_protect_size) {
998+
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
999+
EraseLastKElements(vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
1000+
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
1001+
}
1002+
1003+
const size_t localhost_min_protect_size{2};
1004+
if (onion_protect_size >= localhost_min_protect_size) {
1005+
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
1006+
// to localhost peers, sorted by longest uptime, as manually configured
1007+
// hidden services not using `-bind=addr[:port]=onion` will not be detected
1008+
// as inbound onion connections.
1009+
const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size())};
1010+
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
1011+
EraseLastKElements(vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
1012+
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
1013+
}
1014+
1015+
// Calculate how many we removed, and update our total number of peers that
1016+
// we want to protect based on uptime accordingly.
1017+
total_protect_size -= initial_size - vEvictionCandidates.size();
1018+
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
9751019
}
9761020

9771021
[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
@@ -988,30 +1032,17 @@ static void EraseLastKElements(std::vector<T> &elements, Comparator comparator,
9881032
// An attacker cannot manipulate this metric without performing useful work.
9891033
EraseLastKElements(vEvictionCandidates, CompareNodeTXTime, 4);
9901034
// Protect up to 8 non-tx-relay peers that have sent us novel blocks.
991-
std::sort(vEvictionCandidates.begin(), vEvictionCandidates.end(), CompareNodeBlockRelayOnlyTime);
992-
size_t erase_size = std::min(size_t(8), vEvictionCandidates.size());
993-
vEvictionCandidates.erase(std::remove_if(vEvictionCandidates.end() - erase_size, vEvictionCandidates.end(), [](NodeEvictionCandidate const &n) { return !n.fRelayTxes && n.fRelevantServices; }), vEvictionCandidates.end());
1035+
const size_t erase_size = std::min(size_t(8), vEvictionCandidates.size());
1036+
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, erase_size,
1037+
[](const NodeEvictionCandidate& n) { return !n.fRelayTxes && n.fRelevantServices; });
9941038

9951039
// Protect 4 nodes that most recently sent us novel blocks.
9961040
// An attacker cannot manipulate this metric without performing useful work.
9971041
EraseLastKElements(vEvictionCandidates, CompareNodeBlockTime, 4);
9981042

999-
// Protect the half of the remaining nodes which have been connected the longest.
1000-
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
1001-
// Reserve half of these protected spots for localhost peers, even if
1002-
// they're not longest-uptime overall. This helps protect tor peers, which
1003-
// tend to be otherwise disadvantaged under our eviction criteria.
1004-
size_t initial_size = vEvictionCandidates.size();
1005-
size_t total_protect_size = initial_size / 2;
1006-
1007-
// Pick out up to 1/4 peers that are localhost, sorted by longest uptime.
1008-
std::sort(vEvictionCandidates.begin(), vEvictionCandidates.end(), CompareLocalHostTimeConnected);
1009-
size_t local_erase_size = total_protect_size / 2;
1010-
vEvictionCandidates.erase(std::remove_if(vEvictionCandidates.end() - local_erase_size, vEvictionCandidates.end(), [](NodeEvictionCandidate const &n) { return n.m_is_local; }), vEvictionCandidates.end());
1011-
// Calculate how many we removed, and update our total number of peers that
1012-
// we want to protect based on uptime accordingly.
1013-
total_protect_size -= initial_size - vEvictionCandidates.size();
1014-
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
1043+
// Protect some of the remaining eviction candidates by ratios of desirable
1044+
// or disadvantaged characteristics.
1045+
ProtectEvictionCandidatesByRatio(vEvictionCandidates);
10151046

10161047
if (vEvictionCandidates.empty()) return std::nullopt;
10171048

@@ -1032,7 +1063,7 @@ static void EraseLastKElements(std::vector<T> &elements, Comparator comparator,
10321063
for (const NodeEvictionCandidate &node : vEvictionCandidates) {
10331064
std::vector<NodeEvictionCandidate> &group = mapNetGroupNodes[node.nKeyedNetGroup];
10341065
group.push_back(node);
1035-
int64_t grouptime = group[0].nTimeConnected;
1066+
const int64_t grouptime = group[0].nTimeConnected;
10361067

10371068
if (group.size() > nMostConnections || (group.size() == nMostConnections && grouptime > nMostConnectionsTime)) {
10381069
nMostConnections = group.size();
@@ -1100,7 +1131,8 @@ bool CConnman::AttemptToEvictConnection()
11001131
node->nLastBlockTime, node->nLastTXTime,
11011132
HasAllDesirableServiceFlags(node->nServices),
11021133
peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
1103-
node->m_prefer_evict, node->addr.IsLocal()};
1134+
node->m_prefer_evict, node->addr.IsLocal(),
1135+
node->m_inbound_onion};
11041136
vEvictionCandidates.push_back(candidate);
11051137
}
11061138
}

src/net.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ class CNode
459459

460460
std::atomic<int64_t> nLastSend{0};
461461
std::atomic<int64_t> nLastRecv{0};
462+
//! Unix epoch time at peer connection, in seconds.
462463
const int64_t nTimeConnected;
463464
std::atomic<int64_t> nTimeOffset{0};
464465
std::atomic<int64_t> nLastWarningTime{0};
@@ -1550,8 +1551,16 @@ struct NodeEvictionCandidate
15501551
uint64_t nKeyedNetGroup;
15511552
bool prefer_evict;
15521553
bool m_is_local;
1554+
bool m_is_onion;
15531555
};
15541556

1557+
/**
1558+
* Select an inbound peer to evict after filtering out (protecting) peers having
1559+
* distinct, difficult-to-forge characteristics. The protection logic picks out
1560+
* fixed numbers of desirable peers per various criteria, followed by (mostly)
1561+
* ratios of desirable or disadvantaged peers. If any eviction candidates
1562+
* remain, the selection logic chooses a peer to evict.
1563+
*/
15551564
[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates);
15561565

15571566
class CExplicitNetCleanup
@@ -1566,4 +1575,27 @@ void EraseObjectRequest(NodeId nodeId, const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED
15661575
void RequestObject(NodeId nodeId, const CInv& inv, std::chrono::microseconds current_time, bool fForce=false) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
15671576
size_t GetRequestedObjectCount(NodeId nodeId) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
15681577

1578+
/** Protect desirable or disadvantaged inbound peers from eviction by ratio.
1579+
*
1580+
* This function protects half of the peers which have been connected the
1581+
* longest, to replicate the non-eviction implicit behavior and preclude attacks
1582+
* that start later.
1583+
*
1584+
* Half of these protected spots (1/4 of the total) are reserved for onion peers
1585+
* connected via our tor control service, if any, sorted by longest uptime, even
1586+
* if they're not longest uptime overall. Any remaining slots of the 1/4 are
1587+
* then allocated to protect localhost peers, if any (or up to 2 localhost peers
1588+
* if no slots remain and 2 or more onion peers were protected), sorted by
1589+
* longest uptime, as manually configured hidden services not using
1590+
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections.
1591+
*
1592+
* This helps protect onion peers, which tend to be otherwise disadvantaged
1593+
* under our eviction criteria for their higher min ping times relative to IPv4
1594+
* and IPv6 peers, and favorise the diversity of peer connections.
1595+
*
1596+
* This function was extracted from SelectNodeToEvict() to be able to test the
1597+
* ratio-based protection logic deterministically.
1598+
*/
1599+
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates);
1600+
15691601
#endif // BITCOIN_NET_H

src/test/fuzz/node_eviction.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ FUZZ_TARGET(node_eviction)
3131
/* nKeyedNetGroup */ fuzzed_data_provider.ConsumeIntegral<uint64_t>(),
3232
/* prefer_evict */ fuzzed_data_provider.ConsumeBool(),
3333
/* m_is_local */ fuzzed_data_provider.ConsumeBool(),
34+
/* m_is_onion */ fuzzed_data_provider.ConsumeBool(),
3435
});
3536
}
3637
// Make a copy since eviction_candidates may be in some valid but otherwise

0 commit comments

Comments
 (0)