Skip to content

Commit

Permalink
Merge #20197: p2p: protect onions in AttemptToEvictConnection(), add …
Browse files Browse the repository at this point in the history
…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 #19991 and #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 #19670 to address issue #19500.

  Update 28 February 2021: I've updated this to follow gmaxwell's suggestion in #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 #20477.

  Suggest reviewing the commits that move code with `colorMoved = dimmed-zebra` and `colorMovedWs = allow-indentation-change`.

  Closes #11537.

ACKs for top commit:
  laanwj:
    Code review ACK 0cca08a
  vasild:
    ACK 0cca08a

Tree-SHA512: 2f5a63f942acaae7882920fc61f0185dcd51da85e5b736df9d1fc72343726dd17da740e02f30fa5dc5eb3b2d8345707aed96031bec143d48a2497a610aa19abd
  • Loading branch information
laanwj committed Mar 30, 2021
2 parents 1999baa + 0cca08a commit dede9eb
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 168 deletions.
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ BITCOIN_TESTS =\
test/merkleblock_tests.cpp \
test/miner_tests.cpp \
test/multisig_tests.cpp \
test/net_peer_eviction_tests.cpp \
test/net_tests.cpp \
test/netbase_tests.cpp \
test/pmt_tests.cpp \
Expand Down
82 changes: 57 additions & 25 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,12 @@ static bool CompareLocalHostTimeConnected(const NodeEvictionCandidate &a, const
return a.nTimeConnected > b.nTimeConnected;
}

static bool CompareOnionTimeConnected(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
{
if (a.m_is_onion != b.m_is_onion) return b.m_is_onion;
return a.nTimeConnected > b.nTimeConnected;
}

static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
return a.nKeyedNetGroup < b.nKeyedNetGroup;
}
Expand Down Expand Up @@ -870,13 +876,51 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
return a.nTimeConnected > b.nTimeConnected;
}

//! Sort an array by the specified comparator, then erase the last K elements.
template<typename T, typename Comparator>
static void EraseLastKElements(std::vector<T> &elements, Comparator comparator, size_t k)
//! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
template <typename T, typename Comparator>
static void EraseLastKElements(
std::vector<T>& elements, Comparator comparator, size_t k,
std::function<bool(const NodeEvictionCandidate&)> predicate = [](const NodeEvictionCandidate& n) { return true; })
{
std::sort(elements.begin(), elements.end(), comparator);
size_t eraseSize = std::min(k, elements.size());
elements.erase(elements.end() - eraseSize, elements.end());
elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
}

void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates)
{
// Protect the half of the remaining nodes which have been connected the longest.
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
// these protected spots for onion and localhost peers, if any, even if they're not
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
// disadvantaged under our eviction criteria.
const size_t initial_size = vEvictionCandidates.size();
size_t total_protect_size = initial_size / 2;
const size_t onion_protect_size = total_protect_size / 2;

if (onion_protect_size) {
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
EraseLastKElements(vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
}

const size_t localhost_min_protect_size{2};
if (onion_protect_size >= localhost_min_protect_size) {
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
// to localhost peers, sorted by longest uptime, as manually configured
// hidden services not using `-bind=addr[:port]=onion` will not be detected
// as inbound onion connections.
const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size())};
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
EraseLastKElements(vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
}

// Calculate how many we removed, and update our total number of peers that
// we want to protect based on uptime accordingly.
total_protect_size -= initial_size - vEvictionCandidates.size();
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
}

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

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

// Protect the half of the remaining nodes which have been connected the longest.
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
// Reserve half of these protected spots for localhost peers, even if
// they're not longest-uptime overall. This helps protect tor peers, which
// tend to be otherwise disadvantaged under our eviction criteria.
size_t initial_size = vEvictionCandidates.size();
size_t total_protect_size = initial_size / 2;

// Pick out up to 1/4 peers that are localhost, sorted by longest uptime.
std::sort(vEvictionCandidates.begin(), vEvictionCandidates.end(), CompareLocalHostTimeConnected);
size_t local_erase_size = total_protect_size / 2;
vEvictionCandidates.erase(std::remove_if(vEvictionCandidates.end() - local_erase_size, vEvictionCandidates.end(), [](NodeEvictionCandidate const &n) { return n.m_is_local; }), vEvictionCandidates.end());
// Calculate how many we removed, and update our total number of peers that
// we want to protect based on uptime accordingly.
total_protect_size -= initial_size - vEvictionCandidates.size();
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
// Protect some of the remaining eviction candidates by ratios of desirable
// or disadvantaged characteristics.
ProtectEvictionCandidatesByRatio(vEvictionCandidates);

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

Expand All @@ -937,7 +968,7 @@ static void EraseLastKElements(std::vector<T> &elements, Comparator comparator,
for (const NodeEvictionCandidate &node : vEvictionCandidates) {
std::vector<NodeEvictionCandidate> &group = mapNetGroupNodes[node.nKeyedNetGroup];
group.push_back(node);
int64_t grouptime = group[0].nTimeConnected;
const int64_t grouptime = group[0].nTimeConnected;

if (group.size() > nMostConnections || (group.size() == nMostConnections && grouptime > nMostConnectionsTime)) {
nMostConnections = group.size();
Expand Down Expand Up @@ -985,7 +1016,8 @@ bool CConnman::AttemptToEvictConnection()
node->nLastBlockTime, node->nLastTXTime,
HasAllDesirableServiceFlags(node->nServices),
peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
node->m_prefer_evict, node->addr.IsLocal()};
node->m_prefer_evict, node->addr.IsLocal(),
node->m_inbound_onion};
vEvictionCandidates.push_back(candidate);
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/net.h
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ class CNode

std::atomic<int64_t> nLastSend{0};
std::atomic<int64_t> nLastRecv{0};
//! Unix epoch time at peer connection, in seconds.
const int64_t nTimeConnected;
std::atomic<int64_t> nTimeOffset{0};
// Address of this peer
Expand Down Expand Up @@ -1278,8 +1279,39 @@ struct NodeEvictionCandidate
uint64_t nKeyedNetGroup;
bool prefer_evict;
bool m_is_local;
bool m_is_onion;
};

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

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

#endif // BITCOIN_NET_H
1 change: 1 addition & 0 deletions src/test/fuzz/node_eviction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ FUZZ_TARGET(node_eviction)
/* nKeyedNetGroup */ fuzzed_data_provider.ConsumeIntegral<uint64_t>(),
/* prefer_evict */ fuzzed_data_provider.ConsumeBool(),
/* m_is_local */ fuzzed_data_provider.ConsumeBool(),
/* m_is_onion */ fuzzed_data_provider.ConsumeBool(),
});
}
// Make a copy since eviction_candidates may be in some valid but otherwise
Expand Down
Loading

0 comments on commit dede9eb

Please sign in to comment.