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.
@@ -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
- std::string DhtClient::generate_token(const Peer& peer) {
1404
- // Simple token generation (in real implementation, use proper cryptographic hash)
1405
- std::string data = peer.ip + ":" + std::to_string(peer.port);
1406
- std::hash<std::string> hasher;
1407
- size_t hash = hasher(data);
1408
-
1409
- // Convert hash to hex string
1410
- std::ostringstream oss;
1411
- oss << std::hex << hash;
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
- bool DhtClient::verify_token(const Peer& peer, const std::string& token) {
1424
- std::lock_guard<std::mutex> lock(peer_tokens_mutex_);
1425
- auto it = peer_tokens_.find(peer);
1426
- if (it != peer_tokens_.end()) {
1427
- return it->second.token == token;
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
- return false;
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
- // Normalize IPv6-mapped IPv4 addresses (::ffff:x.x.x.x -> x.x.x.x) for comparison
2641
- auto normalize_ip = [](const std::string& ip) -> std::string {
2642
- const std::string ipv4_mapped_prefix = "::ffff:";
2643
- if (ip.compare(0, ipv4_mapped_prefix.size(), ipv4_mapped_prefix) == 0) {
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;
@@ -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. peer_tokens_mutex_ (Token validation data)
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
- // Tokens for peers (use Peer directly as key for efficiency)
454
- struct PeerToken {
455
- std::string token;
456
- std::chrono::steady_clock::time_point created_at;
457
-
458
- PeerToken() : created_at(std::chrono::steady_clock::now()) {}
459
- PeerToken(const std::string& t)
460
- : token(t), created_at(std::chrono::steady_clock::now()) {}
461
- };
462
- std::unordered_map<Peer, PeerToken> peer_tokens_;
463
- std::mutex peer_tokens_mutex_; // Lock order: 6
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
- std::string generate_token(const Peer& peer);
638
- bool verify_token(const Peer& peer, const std::string& token);
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
 
@@ -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
- return std::to_string(++transaction_counter_);
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
- close_socket(socket);
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 before
279
- // we announce, so we announce under the correct ID. This runs on the discovery thread (not
280
- // a standalone one) on purpose: stop_dht_discovery() joins this thread via
281
- // stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so set_external_ip()
282
- // can never touch a destroyed DhtClient. It complements the in-DHT "ip"-field voting, which
283
- // keeps the node ID correct if the address changes or STUN is unavailable.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",