Skip to content

Commit e293f43

Browse files
committed
p2p: Improved peer selection with /24 subnet deduplication to disadvantage 'spy nodes'
1 parent 125622d commit e293f43

File tree

1 file changed

+182
-99
lines changed

1 file changed

+182
-99
lines changed

src/p2p/net_node.inl

Lines changed: 182 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ namespace nodetool
14141414
}
14151415

14161416

1417-
MDEBUG("Connecting to " << na.str() << "(peer_type=" << peer_type << ", last_seen: "
1417+
MDEBUG("Connecting to " << na.str() << " (peer_type=" << peer_type << ", last_seen: "
14181418
<< (last_seen_stamp ? epee::misc_utils::get_time_interval_string(time(NULL) - last_seen_stamp):"never")
14191419
<< ")...");
14201420

@@ -1571,33 +1571,78 @@ namespace nodetool
15711571
return false;
15721572
}
15731573
//-----------------------------------------------------------------------------------
1574+
// Find a single candidate from the given peer list in the given zone and connect to it if possible
15741575
template<class t_payload_net_handler>
15751576
bool node_server<t_payload_net_handler>::make_new_connection_from_peerlist(network_zone& zone, bool use_white_list)
15761577
{
1577-
size_t max_random_index = 0;
15781578

1579-
std::set<size_t> tried_peers;
1579+
// Local helper method to get the host string, i.e. the pure IP address without port
1580+
const auto get_host_string = [](const epee::net_utils::network_address &address) {
1581+
if (address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
1582+
{
1583+
const boost::asio::ip::address_v6 actual_ip = address.as<const epee::net_utils::ipv6_network_address>().ip();
1584+
if (actual_ip.is_v4_mapped())
1585+
{
1586+
boost::asio::ip::address_v4 v4ip = make_address_v4_from_v6(actual_ip);
1587+
uint32_t actual_ipv4;
1588+
memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4));
1589+
return epee::net_utils::ipv4_network_address(actual_ipv4, 0).host_str();
1590+
}
1591+
}
1592+
return address.host_str();
1593+
};
1594+
1595+
// Get the current list of known peers of the desired kind, ordered by 'last_seen'.
1596+
// Deduplicate ports right away, i.e. if we know several peers on the same host address but
1597+
// with different ports, take only one of them, to avoid giving such peers undue weight
1598+
// and make it impossible to game peer selection by advertising on a large number of ports.
1599+
// Making this list also insulates us from any changes that may happen to the original list
1600+
// while we are working here, and allows an easy retry in the inner try loop.
1601+
std::vector<peerlist_entry> peers;
1602+
std::unordered_set<std::string> hosts;
1603+
size_t total_peers_size = 0;
1604+
zone.m_peerlist.foreach(use_white_list, [&peers, &hosts, &total_peers_size, &get_host_string](const peerlist_entry &peer)
1605+
{
1606+
++total_peers_size;
1607+
const std::string host_string = get_host_string(peer.adr);
1608+
const auto it = hosts.find(host_string);
1609+
if (it == hosts.end())
1610+
{
1611+
peers.push_back(peer);
1612+
hosts.insert(host_string);
1613+
}
1614+
// else ignore this additional peer on the same IP number
15801615

1581-
size_t try_count = 0;
1616+
return true;
1617+
});
1618+
1619+
const size_t peers_size = peers.size();
1620+
MDEBUG("Looking at " << peers_size << " port-deduplicated peers out of " << total_peers_size
1621+
<< ", i.e. dropping " << (total_peers_size - peers_size));
1622+
1623+
std::set<uint64_t> tried_peers; // all peers ever tried
1624+
1625+
// Outer try loop, with up to 3 attempts to actually connect to a suitable randomly choosen candidate
15821626
size_t rand_count = 0;
1583-
while(rand_count < (max_random_index+1)*3 && try_count < 10 && !zone.m_net_server.is_stop_signal_sent())
1627+
while ((rand_count < 3) && !zone.m_net_server.is_stop_signal_sent())
15841628
{
15851629
++rand_count;
1586-
size_t random_index;
1630+
15871631
const uint32_t next_needed_pruning_stripe = m_payload_handler.get_next_needed_pruning_stripe().second;
15881632

1589-
// build a set of all the /16 we're connected to, and prefer a peer that's not in that set
1590-
std::set<uint32_t> classB;
1591-
if (&zone == &m_network_zones.at(epee::net_utils::zone::public_)) // at returns reference, not copy
1633+
// Build a list of all distinct /24 subnets we are connected to now right now; to catch
1634+
// any connection changes, re-build the list for every outer try loop pass
1635+
std::set<uint32_t> connected_subnets;
1636+
const bool is_public_zone = &zone == &m_network_zones.at(epee::net_utils::zone::public_);
1637+
if (is_public_zone)
15921638
{
15931639
zone.m_net_server.get_config_object().foreach_connection([&](const p2p_connection_context& cntxt)
15941640
{
15951641
if (cntxt.m_remote_address.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id())
15961642
{
1597-
15981643
const epee::net_utils::network_address na = cntxt.m_remote_address;
15991644
const uint32_t actual_ip = na.as<const epee::net_utils::ipv4_network_address>().ip();
1600-
classB.insert(actual_ip & 0x0000ffff);
1645+
connected_subnets.insert(actual_ip & 0x00ffffff);
16011646
}
16021647
else if (cntxt.m_remote_address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
16031648
{
@@ -1608,100 +1653,128 @@ namespace nodetool
16081653
boost::asio::ip::address_v4 v4ip = make_address_v4_from_v6(actual_ip);
16091654
uint32_t actual_ipv4;
16101655
memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4));
1611-
classB.insert(actual_ipv4 & ntohl(0xffff0000));
1656+
connected_subnets.insert(actual_ipv4 & ntohl(0xffffff00));
16121657
}
16131658
}
16141659
return true;
16151660
});
16161661
}
16171662

1618-
auto get_host_string = [](const epee::net_utils::network_address &address) {
1619-
if (address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
1620-
{
1621-
boost::asio::ip::address_v6 actual_ip = address.as<const epee::net_utils::ipv6_network_address>().ip();
1622-
if (actual_ip.is_v4_mapped())
1623-
{
1624-
boost::asio::ip::address_v4 v4ip = make_address_v4_from_v6(actual_ip);
1625-
uint32_t actual_ipv4;
1626-
memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4));
1627-
return epee::net_utils::ipv4_network_address(actual_ipv4, 0).host_str();
1628-
}
1629-
}
1630-
return address.host_str();
1631-
};
1632-
std::unordered_set<std::string> hosts_added;
1633-
std::deque<size_t> filtered;
1634-
const size_t limit = use_white_list ? 20 : std::numeric_limits<size_t>::max();
1663+
std::vector<peerlist_entry> *candidate_peers;
1664+
std::vector<peerlist_entry> subnet_peers;
1665+
std::vector<peerlist_entry> filtered;
1666+
1667+
// Inner try loop: Find candidates first with subnet deduplication, if none found again without.
1668+
// Finding none happens if all candidates are from subnets we are already connected to and/or
1669+
// they don't offer the needed stripe when pruning. Only actually loop and deduplicate if we are
1670+
// in the public zone because private zones don't have subnets.
16351671
for (int step = 0; step < 2; ++step)
16361672
{
1637-
bool skip_duplicate_class_B = step == 0;
1638-
size_t idx = 0, skipped = 0;
1639-
zone.m_peerlist.foreach (use_white_list, [&classB, &filtered, &idx, &skipped, skip_duplicate_class_B, limit, next_needed_pruning_stripe, &hosts_added, &get_host_string](const peerlist_entry &pe){
1640-
if (filtered.size() >= limit)
1641-
return false;
1642-
bool skip = false;
1643-
if (skip_duplicate_class_B && pe.adr.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id())
1644-
{
1645-
const epee::net_utils::network_address na = pe.adr;
1646-
uint32_t actual_ip = na.as<const epee::net_utils::ipv4_network_address>().ip();
1647-
skip = classB.find(actual_ip & 0x0000ffff) != classB.end();
1648-
}
1649-
else if (skip_duplicate_class_B && pe.adr.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
1673+
if ((step == 1) && !is_public_zone)
1674+
break;
1675+
1676+
candidate_peers = &peers;
1677+
if ((step == 0) && is_public_zone)
1678+
{
1679+
// Deduplicate subnets using 3 steps
1680+
1681+
// Step 1: Prepare to access the peers in a random order
1682+
std::vector<size_t> shuffled_indexes(peers.size());
1683+
std::iota(shuffled_indexes.begin(), shuffled_indexes.end(), 0);
1684+
std::shuffle(shuffled_indexes.begin(), shuffled_indexes.end(), crypto::random_device{});
1685+
1686+
// Step 2: Deduplicate by only taking 1 candidate from each /24 subnet that occurs, the FIRST
1687+
// candidate seen from each subnet within the now random order
1688+
std::set<uint32_t> subnets = connected_subnets;
1689+
for (size_t index : shuffled_indexes)
16501690
{
1651-
const epee::net_utils::network_address na = pe.adr;
1652-
const boost::asio::ip::address_v6 &actual_ip = na.as<const epee::net_utils::ipv6_network_address>().ip();
1653-
if (actual_ip.is_v4_mapped())
1691+
const peerlist_entry &peer = peers[index];
1692+
bool take = true;
1693+
if (peer.adr.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id())
16541694
{
1655-
boost::asio::ip::address_v4 v4ip = make_address_v4_from_v6(actual_ip);
1656-
uint32_t actual_ipv4;
1657-
memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4));
1658-
skip = classB.find(actual_ipv4 & ntohl(0xffff0000)) != classB.end();
1695+
const epee::net_utils::network_address na = peer.adr;
1696+
const uint32_t actual_ip = na.as<const epee::net_utils::ipv4_network_address>().ip();
1697+
const uint32_t subnet = actual_ip & 0x00ffffff;
1698+
take = subnets.find(subnet) == subnets.end();
1699+
if (take)
1700+
// This subnet is now "occupied", don't take any more candidates from this one
1701+
subnets.insert(subnet);
16591702
}
1703+
else if (peer.adr.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
1704+
{
1705+
const epee::net_utils::network_address na = peer.adr;
1706+
const boost::asio::ip::address_v6 &actual_ip = na.as<const epee::net_utils::ipv6_network_address>().ip();
1707+
if (actual_ip.is_v4_mapped())
1708+
{
1709+
boost::asio::ip::address_v4 v4ip = make_address_v4_from_v6(actual_ip);
1710+
uint32_t actual_ipv4;
1711+
memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4));
1712+
uint32_t subnet = actual_ipv4 & ntohl(0xffffff00);
1713+
take = subnets.find(subnet) == subnets.end();
1714+
if (take)
1715+
subnets.insert(subnet);
1716+
}
1717+
// else 'take' stays true, we will take an IPv6 address that is not V4 mapped
1718+
}
1719+
if (take)
1720+
subnet_peers.push_back(peer);
16601721
}
16611722

1662-
// consider each host once, to avoid giving undue inflence to hosts running several nodes
1663-
if (!skip)
1723+
// Step 3: Put back into order according to 'last_seen', i.e. most recently seen first
1724+
std::sort(subnet_peers.begin(), subnet_peers.end(), [](const peerlist_entry &a, const peerlist_entry &b)
16641725
{
1665-
const auto i = hosts_added.find(get_host_string(pe.adr));
1666-
if (i != hosts_added.end())
1667-
skip = true;
1668-
}
1726+
return a.last_seen > b.last_seen;
1727+
});
16691728

1670-
if (skip)
1671-
++skipped;
1672-
else if (next_needed_pruning_stripe == 0 || pe.pruning_seed == 0)
1673-
filtered.push_back(idx);
1674-
else if (next_needed_pruning_stripe == tools::get_pruning_stripe(pe.pruning_seed))
1675-
filtered.push_front(idx);
1676-
++idx;
1677-
hosts_added.insert(get_host_string(pe.adr));
1678-
return true;
1679-
});
1680-
if (skipped == 0 || !filtered.empty())
1729+
const size_t subnet_peers_size = subnet_peers.size();
1730+
MDEBUG("Looking at " << subnet_peers_size << " subnet-deduplicated peers out of " << peers_size
1731+
<< ", i.e. dropping " << (peers_size - subnet_peers_size));
1732+
1733+
candidate_peers = &subnet_peers;
1734+
} // deduplicate
1735+
// else, for step 1 / second pass of inner try loop, take all peers from all subnets
1736+
1737+
// Take as many candidates as we need and care about stripes if pruning
1738+
const size_t limit = use_white_list ? 20 : std::numeric_limits<size_t>::max();
1739+
for (auto &peer : *candidate_peers) {
1740+
if (filtered.size() >= limit)
1741+
break;
1742+
1743+
if (next_needed_pruning_stripe == 0 || peer.pruning_seed == 0)
1744+
filtered.push_back(peer);
1745+
else if (next_needed_pruning_stripe == tools::get_pruning_stripe(peer.pruning_seed))
1746+
filtered.insert(filtered.begin(), peer);
1747+
// else wrong stripe, skip
1748+
}
1749+
1750+
if (!filtered.empty())
16811751
break;
1682-
if (skipped)
1683-
MDEBUG("Skipping " << skipped << " possible peers as they share a class B with existing peers");
1684-
}
1752+
} // inner try loop
1753+
16851754
if (filtered.empty())
16861755
{
16871756
MINFO("No available peer in " << (use_white_list ? "white" : "gray") << " list filtered by " << next_needed_pruning_stripe);
16881757
return false;
16891758
}
1759+
1760+
size_t random_index;
16901761
if (use_white_list)
16911762
{
1692-
// if using the white list, we first pick in the set of peers we've already been using earlier
1693-
random_index = get_random_index_with_fixed_probability(std::min<uint64_t>(filtered.size() - 1, 20));
1763+
// If using the white list, we first pick in the set of peers we've already been using earlier;
1764+
// that "fixed probability" heavily favors the peers most recently seen in the candidate list
1765+
random_index = get_random_index_with_fixed_probability(filtered.size() - 1);
1766+
16941767
CRITICAL_REGION_LOCAL(m_used_stripe_peers_mutex);
16951768
if (next_needed_pruning_stripe > 0 && next_needed_pruning_stripe <= (1ul << CRYPTONOTE_PRUNING_LOG_STRIPES) && !m_used_stripe_peers[next_needed_pruning_stripe-1].empty())
16961769
{
16971770
const epee::net_utils::network_address na = m_used_stripe_peers[next_needed_pruning_stripe-1].front();
16981771
m_used_stripe_peers[next_needed_pruning_stripe-1].pop_front();
16991772
for (size_t i = 0; i < filtered.size(); ++i)
17001773
{
1701-
peerlist_entry pe;
1702-
if (zone.m_peerlist.get_white_peer_by_index(pe, filtered[i]) && pe.adr == na)
1774+
const peerlist_entry &peer = filtered[i];
1775+
if (peer.adr == na)
17031776
{
1704-
MDEBUG("Reusing stripe " << next_needed_pruning_stripe << " peer " << pe.adr.str());
1777+
MDEBUG("Reusing stripe " << next_needed_pruning_stripe << " peer " << peer.adr.str());
17051778
random_index = i;
17061779
break;
17071780
}
@@ -1710,52 +1783,54 @@ namespace nodetool
17101783
}
17111784
else
17121785
random_index = crypto::rand_idx(filtered.size());
1713-
17141786
CHECK_AND_ASSERT_MES(random_index < filtered.size(), false, "random_index < filtered.size() failed!!");
1715-
random_index = filtered[random_index];
1716-
CHECK_AND_ASSERT_MES(random_index < (use_white_list ? zone.m_peerlist.get_white_peers_count() : zone.m_peerlist.get_gray_peers_count()),
1717-
false, "random_index < peers size failed!!");
17181787

1719-
if(tried_peers.count(random_index))
1720-
continue;
1721-
1722-
tried_peers.insert(random_index);
1723-
peerlist_entry pe = AUTO_VAL_INIT(pe);
1724-
bool r = use_white_list ? zone.m_peerlist.get_white_peer_by_index(pe, random_index):zone.m_peerlist.get_gray_peer_by_index(pe, random_index);
1725-
CHECK_AND_ASSERT_MES(r, false, "Failed to get random peer from peerlist(white:" << use_white_list << ")");
1788+
// We have our final candidate for this pass of the outer try loop
1789+
const peerlist_entry &candidate = filtered[random_index];
17261790

1727-
++try_count;
1791+
if (tried_peers.count(candidate.id))
1792+
// Already tried, don't try that one again
1793+
continue;
1794+
tried_peers.insert(candidate.id);
17281795

17291796
_note("Considering connecting (out) to " << (use_white_list ? "white" : "gray") << " list peer: " <<
1730-
peerid_to_string(pe.id) << " " << pe.adr.str() << ", pruning seed " << epee::string_tools::to_string_hex(pe.pruning_seed) <<
1731-
" (stripe " << next_needed_pruning_stripe << " needed)");
1797+
peerid_to_string(candidate.id) << " " << candidate.adr.str() << ", pruning seed " << epee::string_tools::to_string_hex(candidate.pruning_seed) <<
1798+
" (stripe " << next_needed_pruning_stripe << " needed), in loop pass " << rand_count);
17321799

1733-
if(zone.m_our_address == pe.adr)
1800+
if (zone.m_our_address == candidate.adr)
1801+
// It's ourselves, obviously don't take that
17341802
continue;
17351803

1736-
if(is_peer_used(pe)) {
1804+
if (is_peer_used(candidate)) {
17371805
_note("Peer is used");
17381806
continue;
17391807
}
17401808

1741-
if(!is_remote_host_allowed(pe.adr))
1809+
if (!is_remote_host_allowed(candidate.adr)) {
1810+
_note("Not allowed");
17421811
continue;
1812+
}
17431813

1744-
if(is_addr_recently_failed(pe.adr))
1814+
if (is_addr_recently_failed(candidate.adr)) {
1815+
_note("Recently failed");
17451816
continue;
1817+
}
17461818

1747-
MDEBUG("Selected peer: " << peerid_to_string(pe.id) << " " << pe.adr.str()
1748-
<< ", pruning seed " << epee::string_tools::to_string_hex(pe.pruning_seed) << " "
1749-
<< "[peer_list=" << (use_white_list ? white : gray)
1750-
<< "] last_seen: " << (pe.last_seen ? epee::misc_utils::get_time_interval_string(time(NULL) - pe.last_seen) : "never"));
1819+
MDEBUG("Selected peer: " << peerid_to_string(candidate.id) << " " << candidate.adr.str()
1820+
<< ", pruning seed " << epee::string_tools::to_string_hex(candidate.pruning_seed) << " "
1821+
<< "[peer_list=" << (use_white_list ? white : gray)
1822+
<< "] last_seen: " << (candidate.last_seen ? epee::misc_utils::get_time_interval_string(time(NULL) - candidate.last_seen) : "never"));
17511823

1752-
if(!try_to_connect_and_handshake_with_new_peer(pe.adr, false, pe.last_seen, use_white_list ? white : gray)) {
1753-
_note("Handshake failed");
1824+
const time_t begin_connect = time(NULL);
1825+
if (!try_to_connect_and_handshake_with_new_peer(candidate.adr, false, candidate.last_seen, use_white_list ? white : gray)) {
1826+
time_t fail_connect = time(NULL);
1827+
_note("Handshake failed after " << epee::misc_utils::get_time_interval_string(fail_connect - begin_connect));
17541828
continue;
17551829
}
17561830

17571831
return true;
1758-
}
1832+
} // outer try loop
1833+
17591834
return false;
17601835
}
17611836
//-----------------------------------------------------------------------------------
@@ -1974,6 +2049,14 @@ namespace nodetool
19742049
++count;
19752050
return true;
19762051
});
2052+
2053+
// Refresh the counter in the zone. The thread that the 'run' method sets up for the
2054+
// job to update the counters runs only once every second. If we only rely on that,
2055+
// 'try_to_connect_and_handshake_with_new_peer' called in 'make_new_connection_from_peerlist'
2056+
// will often fail right away because it thinks there are still enough connections, with
2057+
// perfectly good new peer candidates totally wasted, and a bad success rate choosing peers
2058+
zone.m_current_number_of_out_peers = count;
2059+
19772060
return count;
19782061
}
19792062
//-----------------------------------------------------------------------------------

0 commit comments

Comments
 (0)