librats 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native-src/src/dht.cpp +106 -79
- package/native-src/src/dht.h +26 -18
- package/native-src/src/krpc.cpp +13 -1
- package/native-src/src/librats.cpp +4 -4
- package/native-src/src/librats_discovery.cpp +21 -15
- package/package.json +1 -1
package/native-src/src/dht.cpp
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "network_utils.h"
|
|
3
3
|
#include "logger.h"
|
|
4
4
|
#include "socket.h"
|
|
5
|
+
#include "sha1.h"
|
|
5
6
|
#include "json.hpp"
|
|
6
7
|
#include <random>
|
|
7
8
|
#include <algorithm>
|
|
@@ -27,6 +28,16 @@
|
|
|
27
28
|
|
|
28
29
|
namespace librats {
|
|
29
30
|
|
|
31
|
+
// Normalize an IPv6-mapped IPv4 address (::ffff:x.x.x.x -> x.x.x.x) so a node reached over a
|
|
32
|
+
// dual-stack socket compares equal regardless of how its source address was rendered.
|
|
33
|
+
static std::string normalize_ip_for_compare(const std::string& ip) {
|
|
34
|
+
static const std::string ipv4_mapped_prefix = "::ffff:";
|
|
35
|
+
if (ip.compare(0, ipv4_mapped_prefix.size(), ipv4_mapped_prefix) == 0) {
|
|
36
|
+
return ip.substr(ipv4_mapped_prefix.size());
|
|
37
|
+
}
|
|
38
|
+
return ip;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
|
|
31
42
|
DhtClient::DhtClient(int port, const std::string& bind_address, const std::string& data_directory,
|
|
32
43
|
AddressFamily address_family)
|
|
@@ -35,6 +46,14 @@ DhtClient::DhtClient(int port, const std::string& bind_address, const std::strin
|
|
|
35
46
|
node_id_ = generate_node_id();
|
|
36
47
|
routing_table_.resize(NODE_ID_SIZE * 8); // 160 buckets for 160-bit node IDs
|
|
37
48
|
|
|
49
|
+
// Seed the BEP 5 write-token secret with fresh randomness (current and previous generations).
|
|
50
|
+
{
|
|
51
|
+
std::random_device rd;
|
|
52
|
+
for (auto& b : token_secret_) b = static_cast<uint8_t>(rd());
|
|
53
|
+
for (auto& b : token_secret_prev_) b = static_cast<uint8_t>(rd());
|
|
54
|
+
token_secret_rotated_at_ = std::chrono::steady_clock::now();
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
if (data_directory_.empty()) {
|
|
39
58
|
data_directory_ = ".";
|
|
40
59
|
}
|
|
@@ -449,9 +468,6 @@ void DhtClient::maintenance_loop() {
|
|
|
449
468
|
// Cleanup stale nodes every 1 minute
|
|
450
469
|
cleanup_stale_nodes();
|
|
451
470
|
|
|
452
|
-
// Cleanup stale peer tokens
|
|
453
|
-
cleanup_stale_peer_tokens();
|
|
454
|
-
|
|
455
471
|
// Cleanup stale pending searches
|
|
456
472
|
cleanup_stale_searches();
|
|
457
473
|
|
|
@@ -893,8 +909,8 @@ void DhtClient::handle_krpc_get_peers(const KrpcMessage& message, const Peer& se
|
|
|
893
909
|
add_node(sender_node, true, false);
|
|
894
910
|
}
|
|
895
911
|
|
|
896
|
-
// Generate a token for this peer
|
|
897
|
-
std::string token = generate_token(sender);
|
|
912
|
+
// Generate a token for this peer, bound to this info_hash (BEP 5)
|
|
913
|
+
std::string token = generate_token(sender, message.info_hash);
|
|
898
914
|
|
|
899
915
|
// Use neighbor_id only for spider-contacted IPs, real node_id for organic DHT traffic
|
|
900
916
|
NodeId response_id = use_neighbor_id ? neighbor_id(message.info_hash) : node_id_;
|
|
@@ -930,7 +946,7 @@ void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer
|
|
|
930
946
|
// Note: We still want to collect announces even when ignoring other requests
|
|
931
947
|
|
|
932
948
|
// In spider mode, skip token verification for maximum collection
|
|
933
|
-
if (!is_spider && !verify_token(sender, message.token)) {
|
|
949
|
+
if (!is_spider && !verify_token(sender, message.info_hash, message.token)) {
|
|
934
950
|
LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
|
|
935
951
|
auto error = KrpcProtocol::create_error(message.transaction_id, KrpcErrorCode::ProtocolError, "Invalid token");
|
|
936
952
|
send_krpc_message(error, sender);
|
|
@@ -938,7 +954,7 @@ void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer
|
|
|
938
954
|
}
|
|
939
955
|
#else
|
|
940
956
|
// Verify token
|
|
941
|
-
if (!verify_token(sender, message.token)) {
|
|
957
|
+
if (!verify_token(sender, message.info_hash, message.token)) {
|
|
942
958
|
LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
|
|
943
959
|
auto error = KrpcProtocol::create_error(message.transaction_id, KrpcErrorCode::ProtocolError, "Invalid token");
|
|
944
960
|
send_krpc_message(error, sender);
|
|
@@ -1036,6 +1052,24 @@ void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sen
|
|
|
1036
1052
|
}
|
|
1037
1053
|
#endif
|
|
1038
1054
|
|
|
1055
|
+
// Anti-spoofing: if this response matches a search transaction we issued, require it to come
|
|
1056
|
+
// from the exact endpoint we queried. Off-path attackers can guess/observe a transaction ID but
|
|
1057
|
+
// cannot send from the queried node's address, so this drops forged responses before they can
|
|
1058
|
+
// inject routing-table entries or poison the lookup. (Responses with an unknown transaction ID
|
|
1059
|
+
// are not tied to any outstanding query and fall through to the existing handling.)
|
|
1060
|
+
{
|
|
1061
|
+
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1062
|
+
auto trans_it = transaction_to_search_.find(message.transaction_id);
|
|
1063
|
+
if (trans_it != transaction_to_search_.end()) {
|
|
1064
|
+
if (normalize_ip_for_compare(sender.ip) != normalize_ip_for_compare(trans_it->second.queried_endpoint.ip)) {
|
|
1065
|
+
LOG_DHT_WARN("Dropping search response from unexpected source " << sender.ip << ":" << sender.port
|
|
1066
|
+
<< " (queried " << trans_it->second.queried_endpoint.ip << ":"
|
|
1067
|
+
<< trans_it->second.queried_endpoint.port << ") - possible spoofing");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1039
1073
|
// Normal mode: add nodes to routing table
|
|
1040
1074
|
KrpcNode krpc_node(message.response_id, sender.ip, sender.port);
|
|
1041
1075
|
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
@@ -1400,33 +1434,45 @@ NodeId DhtClient::neighbor_id(const NodeId& target) const {
|
|
|
1400
1434
|
return result;
|
|
1401
1435
|
}
|
|
1402
1436
|
|
|
1403
|
-
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
std::
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
std::string token = oss.str();
|
|
1413
|
-
|
|
1414
|
-
// Store token for this peer with timestamp
|
|
1415
|
-
{
|
|
1416
|
-
std::lock_guard<std::mutex> lock(peer_tokens_mutex_);
|
|
1417
|
-
peer_tokens_[peer] = PeerToken(token);
|
|
1437
|
+
void DhtClient::maybe_rotate_token_secret_unlocked() {
|
|
1438
|
+
// Caller holds token_secret_mutex_.
|
|
1439
|
+
auto now = std::chrono::steady_clock::now();
|
|
1440
|
+
if (now - token_secret_rotated_at_ >= std::chrono::minutes(TOKEN_SECRET_ROTATION_MINUTES)) {
|
|
1441
|
+
token_secret_prev_ = token_secret_;
|
|
1442
|
+
std::random_device rd;
|
|
1443
|
+
for (auto& b : token_secret_) b = static_cast<uint8_t>(rd());
|
|
1444
|
+
token_secret_rotated_at_ = now;
|
|
1445
|
+
LOG_DHT_DEBUG("Rotated DHT write-token secret");
|
|
1418
1446
|
}
|
|
1419
|
-
|
|
1420
|
-
return token;
|
|
1421
1447
|
}
|
|
1422
1448
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1449
|
+
std::string DhtClient::compute_token_with_secret(const Peer& peer, const InfoHash& info_hash,
|
|
1450
|
+
const std::array<uint8_t, 16>& secret) const {
|
|
1451
|
+
// SHA1(secret || querier_ip || info_hash). Including the secret makes the token unforgeable;
|
|
1452
|
+
// including the info_hash binds it to a single target so one token cannot announce arbitrary hashes.
|
|
1453
|
+
std::vector<uint8_t> data;
|
|
1454
|
+
data.reserve(secret.size() + peer.ip.size() + info_hash.size());
|
|
1455
|
+
data.insert(data.end(), secret.begin(), secret.end());
|
|
1456
|
+
data.insert(data.end(), peer.ip.begin(), peer.ip.end());
|
|
1457
|
+
data.insert(data.end(), info_hash.begin(), info_hash.end());
|
|
1458
|
+
return SHA1::hash_bytes(data);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
std::string DhtClient::generate_token(const Peer& peer, const InfoHash& info_hash) {
|
|
1462
|
+
std::lock_guard<std::mutex> lock(token_secret_mutex_);
|
|
1463
|
+
maybe_rotate_token_secret_unlocked();
|
|
1464
|
+
return compute_token_with_secret(peer, info_hash, token_secret_);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
bool DhtClient::verify_token(const Peer& peer, const InfoHash& info_hash, const std::string& token) {
|
|
1468
|
+
if (token.empty()) {
|
|
1469
|
+
return false;
|
|
1428
1470
|
}
|
|
1429
|
-
|
|
1471
|
+
std::lock_guard<std::mutex> lock(token_secret_mutex_);
|
|
1472
|
+
maybe_rotate_token_secret_unlocked();
|
|
1473
|
+
// Accept tokens minted with either the current or the previous secret (rotation window).
|
|
1474
|
+
return token == compute_token_with_secret(peer, info_hash, token_secret_)
|
|
1475
|
+
|| token == compute_token_with_secret(peer, info_hash, token_secret_prev_);
|
|
1430
1476
|
}
|
|
1431
1477
|
|
|
1432
1478
|
void DhtClient::cleanup_stale_nodes() {
|
|
@@ -1468,32 +1514,6 @@ void DhtClient::cleanup_stale_nodes() {
|
|
|
1468
1514
|
}
|
|
1469
1515
|
}
|
|
1470
1516
|
|
|
1471
|
-
void DhtClient::cleanup_stale_peer_tokens() {
|
|
1472
|
-
std::lock_guard<std::mutex> lock(peer_tokens_mutex_);
|
|
1473
|
-
|
|
1474
|
-
auto now = std::chrono::steady_clock::now();
|
|
1475
|
-
auto stale_threshold = std::chrono::minutes(10); // Tokens valid for 10 minutes (BEP 5 recommends tokens expire)
|
|
1476
|
-
|
|
1477
|
-
size_t total_before = peer_tokens_.size();
|
|
1478
|
-
|
|
1479
|
-
auto it = peer_tokens_.begin();
|
|
1480
|
-
while (it != peer_tokens_.end()) {
|
|
1481
|
-
if (now - it->second.created_at > stale_threshold) {
|
|
1482
|
-
LOG_DHT_DEBUG("Removing stale token for peer " << it->first.ip << ":" << it->first.port);
|
|
1483
|
-
it = peer_tokens_.erase(it);
|
|
1484
|
-
} else {
|
|
1485
|
-
++it;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
size_t total_after = peer_tokens_.size();
|
|
1490
|
-
|
|
1491
|
-
if (total_before > total_after) {
|
|
1492
|
-
LOG_DHT_DEBUG("Cleaned up " << (total_before - total_after) << " stale peer tokens "
|
|
1493
|
-
<< "(from " << total_before << " to " << total_after << ")");
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
1517
|
void DhtClient::print_statistics() {
|
|
1498
1518
|
auto now = std::chrono::steady_clock::now();
|
|
1499
1519
|
|
|
@@ -1573,13 +1593,6 @@ void DhtClient::print_statistics() {
|
|
|
1573
1593
|
nodes_being_replaced = nodes_being_replaced_.size();
|
|
1574
1594
|
}
|
|
1575
1595
|
|
|
1576
|
-
// Peer tokens statistics
|
|
1577
|
-
size_t peer_tokens_count = 0;
|
|
1578
|
-
{
|
|
1579
|
-
std::lock_guard<std::mutex> tokens_lock(peer_tokens_mutex_);
|
|
1580
|
-
peer_tokens_count = peer_tokens_.size();
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
1596
|
// Spider mode statistics
|
|
1584
1597
|
#ifdef RATS_SEARCH_FEATURES
|
|
1585
1598
|
size_t spider_pool_size = 0;
|
|
@@ -1625,9 +1638,8 @@ void DhtClient::print_statistics() {
|
|
|
1625
1638
|
LOG_DHT_INFO(" Pending ping verifications: " << pending_pings
|
|
1626
1639
|
<< " (nodes being replaced: " << nodes_being_replaced << ")");
|
|
1627
1640
|
LOG_DHT_INFO("[STORED DATA]");
|
|
1628
|
-
LOG_DHT_INFO(" Announced peers: " << announced_peers_total
|
|
1641
|
+
LOG_DHT_INFO(" Announced peers: " << announced_peers_total
|
|
1629
1642
|
<< " across " << announced_peers_infohashes << " infohashes");
|
|
1630
|
-
LOG_DHT_INFO(" Peer tokens: " << peer_tokens_count);
|
|
1631
1643
|
|
|
1632
1644
|
// Best/Worst nodes analysis
|
|
1633
1645
|
if (!all_nodes.empty()) {
|
|
@@ -2449,7 +2461,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
|
|
|
2449
2461
|
|
|
2450
2462
|
// Send query to this node
|
|
2451
2463
|
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
2452
|
-
transaction_to_search_[transaction_id] = SearchTransaction(hash_key, node.id);
|
|
2464
|
+
transaction_to_search_[transaction_id] = SearchTransaction(hash_key, node.id, node.peer);
|
|
2453
2465
|
search.node_states[node.id] |= SearchNodeFlags::QUERIED;
|
|
2454
2466
|
search.invoke_count++;
|
|
2455
2467
|
|
|
@@ -2637,18 +2649,10 @@ void DhtClient::handle_ping_verification_response(const std::string& transaction
|
|
|
2637
2649
|
const auto& verification = it->second;
|
|
2638
2650
|
|
|
2639
2651
|
// Security check: Verify response comes from the IP we pinged
|
|
2640
|
-
//
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
return ip.substr(ipv4_mapped_prefix.size());
|
|
2645
|
-
}
|
|
2646
|
-
return ip;
|
|
2647
|
-
};
|
|
2648
|
-
|
|
2649
|
-
std::string responder_ip_normalized = normalize_ip(responder.ip);
|
|
2650
|
-
std::string expected_ip_normalized = normalize_ip(verification.old_node.peer.ip);
|
|
2651
|
-
|
|
2652
|
+
// (normalize IPv6-mapped IPv4 addresses ::ffff:x.x.x.x -> x.x.x.x for comparison)
|
|
2653
|
+
std::string responder_ip_normalized = normalize_ip_for_compare(responder.ip);
|
|
2654
|
+
std::string expected_ip_normalized = normalize_ip_for_compare(verification.old_node.peer.ip);
|
|
2655
|
+
|
|
2652
2656
|
if (responder_ip_normalized != expected_ip_normalized) {
|
|
2653
2657
|
LOG_DHT_WARN("Ping verification response from wrong IP " << responder.ip
|
|
2654
2658
|
<< " (expected " << verification.old_node.peer.ip << ") - ignoring");
|
|
@@ -2894,7 +2898,30 @@ bool DhtClient::load_routing_table() {
|
|
|
2894
2898
|
LOG_DHT_WARN("Unsupported routing table version: " << version);
|
|
2895
2899
|
return false;
|
|
2896
2900
|
}
|
|
2897
|
-
|
|
2901
|
+
|
|
2902
|
+
// Restore our own node ID so it stays stable across restarts (matches libtorrent's
|
|
2903
|
+
// calculate_node_id). The external address is not known yet at load time, which is
|
|
2904
|
+
// libtorrent's "external_address.is_unspecified()" case -> reuse the saved ID as long
|
|
2905
|
+
// as it is valid (non-zero). If the external IP is later discovered and the restored
|
|
2906
|
+
// ID is no longer valid for it (BEP 42), set_external_ip() regenerates it then.
|
|
2907
|
+
// Restored before bucketing the loaded nodes below, since get_bucket_index() is
|
|
2908
|
+
// computed relative to node_id_.
|
|
2909
|
+
if (routing_data.contains("node_id")) {
|
|
2910
|
+
try {
|
|
2911
|
+
NodeId saved_id = hex_to_node_id(routing_data["node_id"].get<std::string>());
|
|
2912
|
+
bool all_zeros = std::all_of(saved_id.begin(), saved_id.end(),
|
|
2913
|
+
[](uint8_t b) { return b == 0; });
|
|
2914
|
+
if (!all_zeros) {
|
|
2915
|
+
node_id_ = saved_id;
|
|
2916
|
+
LOG_DHT_INFO("Restored DHT node ID from disk: " << node_id_to_hex(node_id_));
|
|
2917
|
+
} else {
|
|
2918
|
+
LOG_DHT_WARN("Saved node ID is invalid (zero/malformed), keeping generated one");
|
|
2919
|
+
}
|
|
2920
|
+
} catch (const std::exception& e) {
|
|
2921
|
+
LOG_DHT_WARN("Failed to restore saved node ID, keeping generated one: " << e.what());
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2898
2925
|
// Load nodes
|
|
2899
2926
|
const auto& nodes_array = routing_data["nodes"];
|
|
2900
2927
|
size_t loaded_count = 0;
|
package/native-src/src/dht.h
CHANGED
|
@@ -443,24 +443,24 @@ private:
|
|
|
443
443
|
// 3. routing_table_mutex_ (core routing data)
|
|
444
444
|
// 4. spider_nodes_mutex_ (Spider mode: node pool and visited tracking) [RATS_SEARCH_FEATURES]
|
|
445
445
|
// 5. announced_peers_mutex_ (Stored peer data)
|
|
446
|
-
// 6.
|
|
446
|
+
// 6. token_secret_mutex_ (DHT write-token secret)
|
|
447
447
|
// 7. shutdown_mutex_ (Lowest priority - can be locked independently)
|
|
448
448
|
//
|
|
449
449
|
// Routing table (k-buckets)
|
|
450
450
|
std::vector<std::vector<DhtNode>> routing_table_;
|
|
451
451
|
mutable std::mutex routing_table_mutex_; // Lock order: 3
|
|
452
452
|
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
std::
|
|
463
|
-
std::mutex
|
|
453
|
+
// DHT write-token secret (BEP 5). Announce tokens are SHA1(secret || querier_ip || info_hash),
|
|
454
|
+
// so they are unforgeable (an attacker does not know the secret) and bound to BOTH the
|
|
455
|
+
// querier's address and the specific info_hash. The secret rotates periodically; we keep the
|
|
456
|
+
// previous secret so tokens handed out shortly before a rotation still verify. This replaces the
|
|
457
|
+
// old, trivially forgeable std::hash("ip:port") scheme. Guarded by token_secret_mutex_, which is
|
|
458
|
+
// a leaf lock (never held while acquiring any other DHT mutex).
|
|
459
|
+
static constexpr int TOKEN_SECRET_ROTATION_MINUTES = 5; // acceptance window ~= 2x this
|
|
460
|
+
std::array<uint8_t, 16> token_secret_;
|
|
461
|
+
std::array<uint8_t, 16> token_secret_prev_;
|
|
462
|
+
std::chrono::steady_clock::time_point token_secret_rotated_at_;
|
|
463
|
+
std::mutex token_secret_mutex_; // Lock order: 6
|
|
464
464
|
|
|
465
465
|
|
|
466
466
|
|
|
@@ -501,11 +501,12 @@ private:
|
|
|
501
501
|
struct SearchTransaction {
|
|
502
502
|
std::string info_hash_hex;
|
|
503
503
|
NodeId queried_node_id;
|
|
504
|
+
Peer queried_endpoint; // endpoint we actually sent the query to (anti-spoofing)
|
|
504
505
|
std::chrono::steady_clock::time_point sent_at;
|
|
505
|
-
|
|
506
|
+
|
|
506
507
|
SearchTransaction() = default;
|
|
507
|
-
SearchTransaction(const std::string& hash, const NodeId& id)
|
|
508
|
-
: info_hash_hex(hash), queried_node_id(id),
|
|
508
|
+
SearchTransaction(const std::string& hash, const NodeId& id, const Peer& endpoint)
|
|
509
|
+
: info_hash_hex(hash), queried_node_id(id), queried_endpoint(endpoint),
|
|
509
510
|
sent_at(std::chrono::steady_clock::now()) {}
|
|
510
511
|
};
|
|
511
512
|
std::unordered_map<std::string, SearchTransaction> transaction_to_search_; // transaction_id -> SearchTransaction
|
|
@@ -634,13 +635,20 @@ private:
|
|
|
634
635
|
NodeId neighbor_id(const NodeId& target) const;
|
|
635
636
|
|
|
636
637
|
|
|
637
|
-
|
|
638
|
-
|
|
638
|
+
// BEP 5 write tokens: bound to the querier address + info_hash and authenticated with a rotating
|
|
639
|
+
// secret. generate_token is called when answering get_peers; verify_token gates announce_peer.
|
|
640
|
+
std::string generate_token(const Peer& peer, const InfoHash& info_hash);
|
|
641
|
+
bool verify_token(const Peer& peer, const InfoHash& info_hash, const std::string& token);
|
|
642
|
+
// Computes SHA1(secret || peer.ip || info_hash) as a hex digest. Port is intentionally excluded
|
|
643
|
+
// (a NAT may present a different source port on the subsequent announce).
|
|
644
|
+
std::string compute_token_with_secret(const Peer& peer, const InfoHash& info_hash,
|
|
645
|
+
const std::array<uint8_t, 16>& secret) const;
|
|
646
|
+
// Rotates the token secret if older than TOKEN_SECRET_ROTATION_MINUTES. Caller holds token_secret_mutex_.
|
|
647
|
+
void maybe_rotate_token_secret_unlocked();
|
|
639
648
|
|
|
640
649
|
|
|
641
650
|
|
|
642
651
|
void cleanup_stale_nodes();
|
|
643
|
-
void cleanup_stale_peer_tokens();
|
|
644
652
|
void refresh_buckets();
|
|
645
653
|
void print_statistics();
|
|
646
654
|
|
package/native-src/src/krpc.cpp
CHANGED
|
@@ -451,7 +451,19 @@ std::unique_ptr<KrpcMessage> KrpcProtocol::decode_error(const BencodeValue& data
|
|
|
451
451
|
|
|
452
452
|
// Utility functions
|
|
453
453
|
std::string KrpcProtocol::generate_transaction_id() {
|
|
454
|
-
|
|
454
|
+
// A predictable transaction ID lets an off-path attacker forge responses to our queries and
|
|
455
|
+
// poison lookups. We make the ID unpredictable (2 random bytes) while keeping it collision-free
|
|
456
|
+
// (2 monotonic counter bytes guarantee uniqueness among any 65536 consecutive/outstanding
|
|
457
|
+
// transactions). The result is a 4-byte opaque binary string echoed back verbatim in KRPC.
|
|
458
|
+
static thread_local std::mt19937 rng(std::random_device{}());
|
|
459
|
+
uint32_t counter = transaction_counter_.fetch_add(1, std::memory_order_relaxed);
|
|
460
|
+
uint16_t rnd = static_cast<uint16_t>(rng() & 0xFFFF);
|
|
461
|
+
char tid[4];
|
|
462
|
+
tid[0] = static_cast<char>((rnd >> 8) & 0xFF);
|
|
463
|
+
tid[1] = static_cast<char>(rnd & 0xFF);
|
|
464
|
+
tid[2] = static_cast<char>((counter >> 8) & 0xFF);
|
|
465
|
+
tid[3] = static_cast<char>(counter & 0xFF);
|
|
466
|
+
return std::string(tid, sizeof(tid));
|
|
455
467
|
}
|
|
456
468
|
|
|
457
469
|
std::string KrpcProtocol::node_id_to_string(const NodeId& id) {
|
|
@@ -658,11 +658,9 @@ void RatsClient::handle_disconnect(socket_t socket) {
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
// Remove from poller, peers map, close socket
|
|
662
661
|
poller_remove(socket);
|
|
663
662
|
remove_peer(socket);
|
|
664
|
-
|
|
665
|
-
|
|
663
|
+
|
|
666
664
|
if (was_validated) {
|
|
667
665
|
if (disconnect_callback_) {
|
|
668
666
|
disconnect_callback_(socket, peer_id);
|
|
@@ -682,7 +680,9 @@ void RatsClient::handle_disconnect(socket_t socket) {
|
|
|
682
680
|
}), "config-save-disconnect");
|
|
683
681
|
}
|
|
684
682
|
}
|
|
685
|
-
|
|
683
|
+
|
|
684
|
+
close_socket(socket);
|
|
685
|
+
|
|
686
686
|
LOG_CLIENT_INFO("Peer disconnected: " << peer_id);
|
|
687
687
|
}
|
|
688
688
|
|
|
@@ -265,22 +265,19 @@ std::chrono::seconds RatsClient::calculate_discovery_interval() const {
|
|
|
265
265
|
|
|
266
266
|
void RatsClient::automatic_discovery_loop() {
|
|
267
267
|
LOG_CLIENT_INFO("Automatic peer discovery loop started");
|
|
268
|
-
|
|
269
|
-
// Initial delay to let DHT bootstrap
|
|
270
|
-
{
|
|
271
|
-
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
272
|
-
if (shutdown_cv_.wait_for(lock, std::chrono::seconds(INITIAL_DISCOVERY_DELAY_SECONDS), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
|
|
273
|
-
LOG_CLIENT_INFO("Automatic peer discovery loop stopped during initial delay");
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
268
|
|
|
278
|
-
// Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
269
|
+
// Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it BEFORE
|
|
270
|
+
// the initial bootstrap delay below. The DHT starts receiving KRPC responses (each carrying
|
|
271
|
+
// our address in the "ip" field) immediately on bootstrap, and that in-DHT vote can reach
|
|
272
|
+
// EXTERNAL_IP_VOTE_THRESHOLD within a few seconds. Probing STUN first lets set_external_ip()
|
|
273
|
+
// adopt the address and regenerate the node ID up front, so the DHT bootstraps under the
|
|
274
|
+
// correct ID and the ip-field voting silently confirms the already-adopted address instead
|
|
275
|
+
// of racing it to consensus. The voting remains the fallback when STUN is unavailable or the
|
|
276
|
+
// address later changes.
|
|
277
|
+
//
|
|
278
|
+
// This runs on the discovery thread (not a standalone one) on purpose: stop_dht_discovery()
|
|
279
|
+
// joins this thread via stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so
|
|
280
|
+
// set_external_ip() can never touch a destroyed DhtClient.
|
|
284
281
|
if (auto_discovery_running_.load() && running_.load()) {
|
|
285
282
|
auto mapped = discover_public_address("stun.l.google.com", 19302, 4000);
|
|
286
283
|
if (mapped) {
|
|
@@ -296,6 +293,15 @@ void RatsClient::automatic_discovery_loop() {
|
|
|
296
293
|
}
|
|
297
294
|
}
|
|
298
295
|
|
|
296
|
+
// Initial delay to let DHT bootstrap
|
|
297
|
+
{
|
|
298
|
+
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
299
|
+
if (shutdown_cv_.wait_for(lock, std::chrono::seconds(INITIAL_DISCOVERY_DELAY_SECONDS), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
|
|
300
|
+
LOG_CLIENT_INFO("Automatic peer discovery loop stopped during initial delay");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
299
305
|
// Announce immediately - this also discovers peers during traversal
|
|
300
306
|
announce_rats_peer();
|
|
301
307
|
|