librats 0.5.1 → 0.5.3

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.
@@ -210,7 +210,7 @@ bool DhtClient::find_peers(const InfoHash& info_hash, PeerDiscoveryCallback call
210
210
  return true;
211
211
  }
212
212
 
213
- bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
213
+ bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port, PeerDiscoveryCallback callback) {
214
214
  if (!running_) {
215
215
  LOG_DHT_ERROR("DHT client not running");
216
216
  return false;
@@ -220,24 +220,64 @@ bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
220
220
  port = port_;
221
221
  }
222
222
 
223
- LOG_DHT_INFO("Announcing peer for info hash: " << node_id_to_hex(info_hash) << " on port " << port);
223
+ std::string hash_key = node_id_to_hex(info_hash);
224
+ LOG_DHT_INFO("Announcing peer for info hash: " << hash_key << " on port " << port);
224
225
 
225
- // First find nodes close to the info hash and send get_peers to them
226
- // This is the proper BEP 5 flow: get_peers -> collect tokens -> announce_peer
227
- auto closest_nodes = find_closest_nodes(info_hash, ALPHA);
228
- for (const auto& node : closest_nodes) {
229
- // Generate transaction ID and track this as a pending announce for KRPC
230
- std::string transaction_id = KrpcProtocol::generate_transaction_id();
231
-
232
- {
233
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
234
- pending_announces_.emplace(transaction_id, PendingAnnounce(info_hash, port));
226
+ // BEP 5 compliant announce:
227
+ // 1. Perform iterative Kademlia lookup (like find_peers)
228
+ // 2. Collect tokens from responding nodes
229
+ // 3. Send announce_peer to k closest nodes with their tokens
230
+
231
+ // Get initial nodes from routing table
232
+ auto closest_nodes = find_closest_nodes(info_hash, K_BUCKET_SIZE);
233
+
234
+ if (closest_nodes.empty()) {
235
+ LOG_DHT_WARN("No nodes in routing table to announce to for info_hash " << hash_key);
236
+ return false;
237
+ }
238
+
239
+ DeferredCallbacks deferred;
240
+
241
+ {
242
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
243
+
244
+ // Check if a search/announce is already ongoing for this info_hash
245
+ auto search_it = pending_searches_.find(hash_key);
246
+ if (search_it != pending_searches_.end()) {
247
+ if (search_it->second.is_announce) {
248
+ LOG_DHT_INFO("Announce already in progress for info hash " << hash_key);
249
+ return true;
235
250
  }
236
-
237
- auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, info_hash);
238
- send_krpc_message(message, node.peer);
251
+ // Regular find_peers in progress - let it complete, then user can announce again
252
+ LOG_DHT_WARN("find_peers already in progress for info hash " << hash_key << " - announce will wait");
253
+ return false;
254
+ }
255
+
256
+ // Create new search with announce flag
257
+ PendingSearch new_search(info_hash);
258
+ new_search.is_announce = true;
259
+ new_search.announce_port = port;
260
+
261
+ // Add callback if provided - peers discovered during traversal will be returned through it
262
+ if (callback) {
263
+ new_search.callbacks.push_back(callback);
264
+ }
265
+
266
+ // Initialize search_nodes with closest nodes from routing table (already sorted)
267
+ new_search.search_nodes = std::move(closest_nodes);
268
+
269
+ auto insert_result = pending_searches_.emplace(hash_key, std::move(new_search));
270
+ PendingSearch& search_ref = insert_result.first->second;
271
+
272
+ LOG_DHT_DEBUG("Initialized announce search with " << search_ref.search_nodes.size() << " nodes from routing table");
273
+
274
+ // Start sending requests
275
+ add_search_requests(search_ref, deferred);
239
276
  }
240
277
 
278
+ // Invoke callbacks outside the lock to avoid deadlock
279
+ deferred.invoke();
280
+
241
281
  return true;
242
282
  }
243
283
 
@@ -262,6 +302,13 @@ bool DhtClient::is_search_active(const InfoHash& info_hash) const {
262
302
  return it != pending_searches_.end() && !it->second.is_finished;
263
303
  }
264
304
 
305
+ bool DhtClient::is_announce_active(const InfoHash& info_hash) const {
306
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
307
+ std::string hash_key = node_id_to_hex(info_hash);
308
+ auto it = pending_searches_.find(hash_key);
309
+ return it != pending_searches_.end() && !it->second.is_finished && it->second.is_announce;
310
+ }
311
+
265
312
  size_t DhtClient::get_active_searches_count() const {
266
313
  std::lock_guard<std::mutex> lock(pending_searches_mutex_);
267
314
  size_t count = 0;
@@ -273,6 +320,17 @@ size_t DhtClient::get_active_searches_count() const {
273
320
  return count;
274
321
  }
275
322
 
323
+ size_t DhtClient::get_active_announces_count() const {
324
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
325
+ size_t count = 0;
326
+ for (const auto& [hash, search] : pending_searches_) {
327
+ if (!search.is_finished && search.is_announce) {
328
+ count++;
329
+ }
330
+ }
331
+ return count;
332
+ }
333
+
276
334
  std::vector<Peer> DhtClient::get_default_bootstrap_nodes() {
277
335
  return {
278
336
  {"router.bittorrent.com", 6881},
@@ -340,9 +398,6 @@ void DhtClient::maintenance_loop() {
340
398
  // Cleanup stale peer tokens
341
399
  cleanup_stale_peer_tokens();
342
400
 
343
- // Cleanup stale pending announces
344
- cleanup_stale_announces();
345
-
346
401
  // Cleanup stale pending searches
347
402
  cleanup_stale_searches();
348
403
 
@@ -402,7 +457,7 @@ void DhtClient::handle_message(const std::vector<uint8_t>& data, const Peer& sen
402
457
  handle_krpc_message(*krpc_message, sender);
403
458
  }
404
459
 
405
- void DhtClient::add_node(const DhtNode& node, bool confirmed) {
460
+ void DhtClient::add_node(const DhtNode& node, bool confirmed, bool no_verify) {
406
461
  std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
407
462
  std::lock_guard<std::mutex> lock(routing_table_mutex_);
408
463
 
@@ -460,7 +515,7 @@ void DhtClient::add_node(const DhtNode& node, bool confirmed) {
460
515
  return;
461
516
  }
462
517
 
463
- // All nodes are good - find the "worst" good node for ping verification
518
+ // All nodes are good - find the "worst" good node for ping verification or replacement
464
519
  // Worst = highest RTT among nodes not already being pinged
465
520
  DhtNode* worst = nullptr;
466
521
  for (auto& existing : bucket) {
@@ -476,6 +531,19 @@ void DhtClient::add_node(const DhtNode& node, bool confirmed) {
476
531
  return;
477
532
  }
478
533
 
534
+ // If no_verify is set, directly replace the worst node without ping verification
535
+ if (no_verify) {
536
+ LOG_DHT_DEBUG("Force adding node " << node_id_to_hex(node.id)
537
+ << " - replacing worst node " << node_id_to_hex(worst->id)
538
+ << " (rtt=" << worst->rtt << "ms) without ping verification");
539
+ DhtNode new_node = node;
540
+ if (confirmed) {
541
+ new_node.fail_count = 0;
542
+ }
543
+ *worst = new_node;
544
+ return;
545
+ }
546
+
479
547
  // Initiate ping to worst node - if it doesn't respond, replace with candidate
480
548
  LOG_DHT_DEBUG("All nodes good, pinging worst node " << node_id_to_hex(worst->id)
481
549
  << " (rtt=" << worst->rtt << "ms) to verify");
@@ -502,7 +570,7 @@ std::vector<DhtNode> DhtClient::find_closest_nodes_unlocked(const NodeId& target
502
570
  candidates.reserve(count * 3 + K_BUCKET_SIZE * 2);
503
571
 
504
572
  // Add nodes from ideal bucket
505
- if (target_bucket < routing_table_.size()) {
573
+ if (target_bucket >= 0 && static_cast<size_t>(target_bucket) < routing_table_.size()) {
506
574
  const auto& bucket = routing_table_[target_bucket];
507
575
  candidates.insert(candidates.end(), bucket.begin(), bucket.end());
508
576
  LOG_DHT_DEBUG("Collected " << bucket.size() << " nodes from target bucket " << target_bucket);
@@ -602,11 +670,11 @@ int DhtClient::get_bucket_index(const NodeId& id) {
602
670
  NodeId distance = xor_distance(node_id_, id);
603
671
 
604
672
  // Find the position of the most significant bit
605
- for (int i = 0; i < NODE_ID_SIZE; ++i) {
673
+ for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
606
674
  if (distance[i] != 0) {
607
675
  for (int j = 7; j >= 0; --j) {
608
676
  if (distance[i] & (1 << j)) {
609
- return i * 8 + (7 - j);
677
+ return static_cast<int>(i * 8 + (7 - j));
610
678
  }
611
679
  }
612
680
  }
@@ -650,10 +718,20 @@ void DhtClient::handle_krpc_message(const KrpcMessage& message, const Peer& send
650
718
  void DhtClient::handle_krpc_ping(const KrpcMessage& message, const Peer& sender) {
651
719
  LOG_DHT_DEBUG("Handling KRPC PING from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
652
720
 
721
+ #ifdef RATS_SEARCH_FEATURES
722
+ // Spider mode: check if ignoring requests
723
+ if (spider_mode_.load() && spider_ignore_.load()) {
724
+ return;
725
+ }
726
+ bool no_verify = spider_mode_.load();
727
+ #else
728
+ bool no_verify = false;
729
+ #endif
730
+
653
731
  // Add sender to routing table
654
732
  KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
655
733
  DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
656
- add_node(sender_node);
734
+ add_node(sender_node, true, no_verify);
657
735
 
658
736
  // Respond with ping response
659
737
  auto response = KrpcProtocol::create_ping_response(message.transaction_id, node_id_);
@@ -663,10 +741,20 @@ void DhtClient::handle_krpc_ping(const KrpcMessage& message, const Peer& sender)
663
741
  void DhtClient::handle_krpc_find_node(const KrpcMessage& message, const Peer& sender) {
664
742
  LOG_DHT_DEBUG("Handling KRPC FIND_NODE from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
665
743
 
744
+ #ifdef RATS_SEARCH_FEATURES
745
+ // Spider mode: check if ignoring requests
746
+ if (spider_mode_.load() && spider_ignore_.load()) {
747
+ return;
748
+ }
749
+ bool no_verify = spider_mode_.load();
750
+ #else
751
+ bool no_verify = false;
752
+ #endif
753
+
666
754
  // Add sender to routing table
667
755
  KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
668
756
  DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
669
- add_node(sender_node);
757
+ add_node(sender_node, true, no_verify);
670
758
 
671
759
  // Find closest nodes
672
760
  auto closest_nodes = find_closest_nodes(message.target_id, K_BUCKET_SIZE);
@@ -680,10 +768,20 @@ void DhtClient::handle_krpc_find_node(const KrpcMessage& message, const Peer& se
680
768
  void DhtClient::handle_krpc_get_peers(const KrpcMessage& message, const Peer& sender) {
681
769
  LOG_DHT_DEBUG("Handling KRPC GET_PEERS from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port << " for info_hash " << node_id_to_hex(message.info_hash));
682
770
 
771
+ #ifdef RATS_SEARCH_FEATURES
772
+ // Spider mode: check if ignoring requests
773
+ if (spider_mode_.load() && spider_ignore_.load()) {
774
+ return;
775
+ }
776
+ bool no_verify = spider_mode_.load();
777
+ #else
778
+ bool no_verify = false;
779
+ #endif
780
+
683
781
  // Add sender to routing table
684
782
  KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
685
783
  DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
686
- add_node(sender_node);
784
+ add_node(sender_node, true, no_verify);
687
785
 
688
786
  // Generate a token for this peer
689
787
  std::string token = generate_token(sender);
@@ -710,6 +808,20 @@ void DhtClient::handle_krpc_get_peers(const KrpcMessage& message, const Peer& se
710
808
  void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer& sender) {
711
809
  LOG_DHT_DEBUG("Handling KRPC ANNOUNCE_PEER from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
712
810
 
811
+ #ifdef RATS_SEARCH_FEATURES
812
+ bool is_spider = spider_mode_.load();
813
+
814
+ // Spider mode: check if ignoring requests (but still process announces for callback)
815
+ // Note: We still want to collect announces even when ignoring other requests
816
+
817
+ // In spider mode, skip token verification for maximum collection
818
+ if (!is_spider && !verify_token(sender, message.token)) {
819
+ LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
820
+ auto error = KrpcProtocol::create_error(message.transaction_id, KrpcErrorCode::ProtocolError, "Invalid token");
821
+ send_krpc_message(error, sender);
822
+ return;
823
+ }
824
+ #else
713
825
  // Verify token
714
826
  if (!verify_token(sender, message.token)) {
715
827
  LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
@@ -717,16 +829,37 @@ void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer
717
829
  send_krpc_message(error, sender);
718
830
  return;
719
831
  }
832
+ bool is_spider = false;
833
+ #endif
720
834
 
721
835
  // Add sender to routing table
722
836
  KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
723
837
  DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
724
- add_node(sender_node);
838
+ add_node(sender_node, true, is_spider);
839
+
840
+ // Determine the actual port (BEP 5: implied_port support)
841
+ uint16_t peer_port = message.port;
725
842
 
726
843
  // Store the peer announcement
727
- Peer announcing_peer(sender.ip, message.port);
844
+ Peer announcing_peer(sender.ip, peer_port);
728
845
  store_announced_peer(message.info_hash, announcing_peer);
729
846
 
847
+ #ifdef RATS_SEARCH_FEATURES
848
+ // Spider mode: invoke announce callback (ensured hash - peer definitely has it)
849
+ if (is_spider) {
850
+ SpiderAnnounceCallback callback;
851
+ {
852
+ std::lock_guard<std::mutex> lock(spider_callbacks_mutex_);
853
+ callback = spider_announce_callback_;
854
+ }
855
+ if (callback) {
856
+ LOG_DHT_DEBUG("Spider: invoking announce callback for info_hash " << node_id_to_hex(message.info_hash)
857
+ << " from " << announcing_peer.ip << ":" << announcing_peer.port);
858
+ callback(message.info_hash, announcing_peer);
859
+ }
860
+ }
861
+ #endif
862
+
730
863
  // Respond with acknowledgment
731
864
  auto response = KrpcProtocol::create_announce_peer_response(message.transaction_id, node_id_);
732
865
  send_krpc_message(response, sender);
@@ -738,15 +871,21 @@ void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sen
738
871
  // Check if this is a ping verification response before normal processing
739
872
  handle_ping_verification_response(message.transaction_id, message.response_id, sender);
740
873
 
874
+ #ifdef RATS_SEARCH_FEATURES
875
+ bool no_verify = spider_mode_.load();
876
+ #else
877
+ bool no_verify = false;
878
+ #endif
879
+
741
880
  // Add responder to routing table
742
881
  KrpcNode krpc_node(message.response_id, sender.ip, sender.port);
743
882
  DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
744
- add_node(sender_node);
883
+ add_node(sender_node, true, no_verify);
745
884
 
746
885
  // Add any nodes from the response (these are nodes we heard about, not confirmed)
747
886
  for (const auto& node : message.nodes) {
748
887
  DhtNode dht_node = krpc_node_to_dht_node(node);
749
- add_node(dht_node, false); // Not confirmed - just heard about from another node
888
+ add_node(dht_node, false, no_verify); // Not confirmed - just heard about from another node
750
889
  }
751
890
 
752
891
  // Check if this is a response to a pending search (get_peers with peers)
@@ -763,9 +902,16 @@ void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sen
763
902
  handle_get_peers_empty_response(message.transaction_id, sender);
764
903
  }
765
904
 
766
- // Check if this is a response to a pending announce (get_peers with token)
905
+ // Save write token if present (needed for announce_peer after traversal completes)
767
906
  if (!message.token.empty()) {
768
- handle_get_peers_response_for_announce(message.transaction_id, sender, message.token);
907
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
908
+ auto trans_it = transaction_to_search_.find(message.transaction_id);
909
+ if (trans_it != transaction_to_search_.end()) {
910
+ auto search_it = pending_searches_.find(trans_it->second.info_hash_hex);
911
+ if (search_it != pending_searches_.end()) {
912
+ save_write_token(search_it->second, trans_it->second.queried_node_id, message.token);
913
+ }
914
+ }
769
915
  }
770
916
 
771
917
  // Clean up finished searches AFTER all response data has been processed
@@ -1028,8 +1174,10 @@ void DhtClient::print_statistics() {
1028
1174
 
1029
1175
  // Pending searches statistics
1030
1176
  size_t pending_searches = 0;
1177
+ size_t pending_announces = 0;
1031
1178
  size_t total_search_nodes = 0;
1032
1179
  size_t total_found_peers = 0;
1180
+ size_t total_write_tokens = 0;
1033
1181
  size_t active_transactions = 0;
1034
1182
  {
1035
1183
  std::lock_guard<std::mutex> search_lock(pending_searches_mutex_);
@@ -1038,16 +1186,13 @@ void DhtClient::print_statistics() {
1038
1186
  for (const auto& [hash, search] : pending_searches_) {
1039
1187
  total_search_nodes += search.search_nodes.size();
1040
1188
  total_found_peers += search.found_peers.size();
1189
+ total_write_tokens += search.write_tokens.size();
1190
+ if (search.is_announce) {
1191
+ pending_announces++;
1192
+ }
1041
1193
  }
1042
1194
  }
1043
1195
 
1044
- // Pending announces statistics
1045
- size_t pending_announces_count = 0;
1046
- {
1047
- std::lock_guard<std::mutex> announce_lock(pending_announces_mutex_);
1048
- pending_announces_count = pending_announces_.size();
1049
- }
1050
-
1051
1196
  // Announced peers statistics
1052
1197
  size_t announced_peers_total = 0;
1053
1198
  size_t announced_peers_infohashes = 0;
@@ -1084,9 +1229,11 @@ void DhtClient::print_statistics() {
1084
1229
  << ", Max bucket size: " << max_bucket_size << "/" << K_BUCKET_SIZE);
1085
1230
  LOG_DHT_INFO("[ACTIVE OPERATIONS]");
1086
1231
  LOG_DHT_INFO(" Pending searches: " << pending_searches
1087
- << " (nodes: " << total_search_nodes << ", found peers: " << total_found_peers << ")");
1232
+ << " (announces: " << pending_announces
1233
+ << ", nodes: " << total_search_nodes
1234
+ << ", peers: " << total_found_peers
1235
+ << ", tokens: " << total_write_tokens << ")");
1088
1236
  LOG_DHT_INFO(" Active transactions: " << active_transactions);
1089
- LOG_DHT_INFO(" Pending announces: " << pending_announces_count);
1090
1237
  LOG_DHT_INFO(" Pending ping verifications: " << pending_pings
1091
1238
  << " (nodes being replaced: " << nodes_being_replaced << ")");
1092
1239
  LOG_DHT_INFO("[STORED DATA]");
@@ -1184,9 +1331,9 @@ void DhtClient::refresh_buckets() {
1184
1331
  int byte_index = static_cast<int>(i / 8);
1185
1332
  int bit_index = static_cast<int>(i % 8);
1186
1333
 
1187
- if (byte_index < NODE_ID_SIZE) {
1334
+ if (static_cast<size_t>(byte_index) < NODE_ID_SIZE) {
1188
1335
  // Clear the target bit and higher bits
1189
- for (int j = byte_index; j < NODE_ID_SIZE; ++j) {
1336
+ for (size_t j = static_cast<size_t>(byte_index); j < NODE_ID_SIZE; ++j) {
1190
1337
  random_id[j] = node_id_[j];
1191
1338
  }
1192
1339
 
@@ -1203,23 +1350,6 @@ void DhtClient::refresh_buckets() {
1203
1350
  }
1204
1351
  }
1205
1352
 
1206
- void DhtClient::cleanup_stale_announces() {
1207
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
1208
-
1209
- auto now = std::chrono::steady_clock::now();
1210
- auto stale_threshold = std::chrono::minutes(5); // Remove announces older than 5 minutes
1211
-
1212
- auto it = pending_announces_.begin();
1213
- while (it != pending_announces_.end()) {
1214
- if (now - it->second.created_at > stale_threshold) {
1215
- LOG_DHT_DEBUG("Removing stale pending announce for transaction " << it->first);
1216
- it = pending_announces_.erase(it);
1217
- } else {
1218
- ++it;
1219
- }
1220
- }
1221
- }
1222
-
1223
1353
  void DhtClient::cleanup_stale_searches() {
1224
1354
  std::lock_guard<std::mutex> lock(pending_searches_mutex_);
1225
1355
 
@@ -1475,24 +1605,6 @@ void DhtClient::cleanup_timed_out_search_requests() {
1475
1605
  }
1476
1606
  }
1477
1607
 
1478
- void DhtClient::handle_get_peers_response_for_announce(const std::string& transaction_id, const Peer& responder, const std::string& token) {
1479
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
1480
-
1481
- auto it = pending_announces_.find(transaction_id);
1482
- if (it != pending_announces_.end()) {
1483
- const auto& pending_announce = it->second;
1484
- LOG_DHT_DEBUG("Found pending announce for transaction " << transaction_id
1485
- << " - sending announce_peer for info_hash " << node_id_to_hex(pending_announce.info_hash)
1486
- << " to " << responder.ip << ":" << responder.port);
1487
-
1488
- // Send announce_peer with the received token
1489
- send_krpc_announce_peer(responder, pending_announce.info_hash, pending_announce.port, token);
1490
-
1491
- // Remove the pending announce since we've handled it
1492
- pending_announces_.erase(it);
1493
- }
1494
- }
1495
-
1496
1608
  void DhtClient::handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder) {
1497
1609
  DeferredCallbacks deferred;
1498
1610
  {
@@ -1796,6 +1908,86 @@ void DhtClient::add_node_to_search(PendingSearch& search, const DhtNode& node) {
1796
1908
  }
1797
1909
  }
1798
1910
 
1911
+ void DhtClient::save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token) {
1912
+ // Save the write token received from a node (BEP 5 compliant)
1913
+ // This token will be used later when sending announce_peer to this node
1914
+
1915
+ if (token.empty()) {
1916
+ return;
1917
+ }
1918
+
1919
+ // Only save token if we don't already have one from this node
1920
+ // (first token is usually the valid one)
1921
+ if (search.write_tokens.find(node_id) == search.write_tokens.end()) {
1922
+ search.write_tokens[node_id] = token;
1923
+ LOG_DHT_DEBUG("Saved write token from node " << node_id_to_hex(node_id)
1924
+ << " for info_hash " << node_id_to_hex(search.info_hash)
1925
+ << " (total tokens: " << search.write_tokens.size() << ")");
1926
+ }
1927
+ }
1928
+
1929
+ void DhtClient::send_announce_to_closest_nodes(PendingSearch& search) {
1930
+ // BEP 5: Send announce_peer to the k closest nodes that:
1931
+ // 1. Responded to our get_peers query
1932
+ // 2. Gave us a valid write token
1933
+
1934
+ if (!search.is_announce) {
1935
+ return;
1936
+ }
1937
+
1938
+ std::string hash_key = node_id_to_hex(search.info_hash);
1939
+
1940
+ LOG_DHT_INFO("Sending announce_peer to closest nodes for info_hash " << hash_key
1941
+ << " on port " << search.announce_port);
1942
+
1943
+ // Collect nodes that responded and have tokens, sorted by distance (closest first)
1944
+ std::vector<std::pair<DhtNode, std::string>> announce_targets;
1945
+ announce_targets.reserve(K_BUCKET_SIZE);
1946
+
1947
+ for (const auto& node : search.search_nodes) {
1948
+ if (announce_targets.size() >= K_BUCKET_SIZE) {
1949
+ break; // We have enough targets
1950
+ }
1951
+
1952
+ // Check if node responded successfully
1953
+ auto state_it = search.node_states.find(node.id);
1954
+ if (state_it == search.node_states.end()) {
1955
+ continue;
1956
+ }
1957
+ if (!(state_it->second & SearchNodeFlags::RESPONDED)) {
1958
+ continue; // Node didn't respond
1959
+ }
1960
+
1961
+ // Check if we have a token from this node
1962
+ auto token_it = search.write_tokens.find(node.id);
1963
+ if (token_it == search.write_tokens.end()) {
1964
+ LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " responded but no token - skipping");
1965
+ continue; // No token from this node
1966
+ }
1967
+
1968
+ announce_targets.emplace_back(node, token_it->second);
1969
+ }
1970
+
1971
+ if (announce_targets.empty()) {
1972
+ LOG_DHT_WARN("No nodes with tokens to announce to for info_hash " << hash_key);
1973
+ return;
1974
+ }
1975
+
1976
+ LOG_DHT_INFO("Announcing to " << announce_targets.size() << " closest nodes with tokens");
1977
+
1978
+ // Send announce_peer to each target
1979
+ for (const auto& [node, token] : announce_targets) {
1980
+ LOG_DHT_DEBUG("Sending announce_peer to node " << node_id_to_hex(node.id)
1981
+ << " at " << node.peer.ip << ":" << node.peer.port
1982
+ << " with token (distance: " << get_bucket_index(node.id) << ")");
1983
+
1984
+ send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token);
1985
+ }
1986
+
1987
+ LOG_DHT_INFO("Announce completed: sent announce_peer to " << announce_targets.size()
1988
+ << " nodes for info_hash " << hash_key);
1989
+ }
1990
+
1799
1991
  bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& deferred) {
1800
1992
  // Returns true if search is done (completed or should be finished)
1801
1993
 
@@ -1875,7 +2067,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1875
2067
  queries_sent++;
1876
2068
  }
1877
2069
 
1878
- LOG_DHT_DEBUG("Search [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
2070
+ LOG_DHT_DEBUG((search.is_announce ? "Announce" : "Search") << " [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
1879
2071
  LOG_DHT_DEBUG(" * search_nodes: " << search.search_nodes.size());
1880
2072
  LOG_DHT_DEBUG(" * queries_sent: " << queries_sent);
1881
2073
  LOG_DHT_DEBUG(" * invoke_count: " << search.invoke_count);
@@ -1903,7 +2095,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1903
2095
  if (f & SearchNodeFlags::ABANDONED) abandoned_total++;
1904
2096
  }
1905
2097
 
1906
- LOG_DHT_INFO("=== Search Completed for info_hash " << hash_key << " ===");
2098
+ LOG_DHT_INFO("=== " << (search.is_announce ? "Announce" : "Search") << " Completed for info_hash " << hash_key << " ===");
1907
2099
  LOG_DHT_INFO(" Duration: " << duration_ms << "ms");
1908
2100
  LOG_DHT_INFO(" Total nodes queried: " << queried_total);
1909
2101
  LOG_DHT_INFO(" Total nodes responded: " << responded_total);
@@ -1911,8 +2103,18 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1911
2103
  LOG_DHT_INFO(" Nodes with short timeout: " << short_timeout_total);
1912
2104
  LOG_DHT_INFO(" Nodes abandoned (truncation): " << abandoned_total);
1913
2105
  LOG_DHT_INFO(" Final branch_factor: " << search.branch_factor << " (initial: " << ALPHA << ")");
1914
- LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
1915
- LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
2106
+ if (search.is_announce) {
2107
+ LOG_DHT_INFO(" Write tokens collected: " << search.write_tokens.size());
2108
+ LOG_DHT_INFO(" Announce port: " << search.announce_port);
2109
+ } else {
2110
+ LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
2111
+ LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
2112
+ }
2113
+
2114
+ // If this is an announce search, send announce_peer to k closest nodes with tokens
2115
+ if (search.is_announce) {
2116
+ send_announce_to_closest_nodes(search);
2117
+ }
1916
2118
 
1917
2119
  // Collect callbacks for deferred invocation (avoid deadlock - don't call user callbacks while holding mutex)
1918
2120
  deferred.should_invoke = true;
@@ -2038,6 +2240,25 @@ void DhtClient::handle_ping_verification_response(const std::string& transaction
2038
2240
  if (it != pending_pings_.end()) {
2039
2241
  const auto& verification = it->second;
2040
2242
 
2243
+ // Security check: Verify response comes from the IP we pinged
2244
+ // Normalize IPv6-mapped IPv4 addresses (::ffff:x.x.x.x -> x.x.x.x) for comparison
2245
+ auto normalize_ip = [](const std::string& ip) -> std::string {
2246
+ const std::string ipv4_mapped_prefix = "::ffff:";
2247
+ if (ip.compare(0, ipv4_mapped_prefix.size(), ipv4_mapped_prefix) == 0) {
2248
+ return ip.substr(ipv4_mapped_prefix.size());
2249
+ }
2250
+ return ip;
2251
+ };
2252
+
2253
+ std::string responder_ip_normalized = normalize_ip(responder.ip);
2254
+ std::string expected_ip_normalized = normalize_ip(verification.old_node.peer.ip);
2255
+
2256
+ if (responder_ip_normalized != expected_ip_normalized) {
2257
+ LOG_DHT_WARN("Ping verification response from wrong IP " << responder.ip
2258
+ << " (expected " << verification.old_node.peer.ip << ") - ignoring");
2259
+ return; // Don't remove from pending_pings_, let it timeout naturally
2260
+ }
2261
+
2041
2262
  // BEP 5: We pinged the OLD node to check if it's still alive
2042
2263
  if (responder_id == verification.old_node.id) {
2043
2264
  // Calculate RTT
@@ -2064,9 +2285,19 @@ void DhtClient::handle_ping_verification_response(const std::string& transaction
2064
2285
  }
2065
2286
  // Candidate is discarded (not added to routing table)
2066
2287
  } else {
2067
- LOG_DHT_WARN("Ping verification response from unexpected node " << node_id_to_hex(responder_id)
2068
- << " at " << responder.ip << ":" << responder.port
2069
- << " (expected old node " << node_id_to_hex(verification.old_node.id) << ")");
2288
+ // Different node ID responded from the same IP:port!
2289
+ // This means the old node is gone and this IP now belongs to a different node.
2290
+ // Replace the old node with the candidate.
2291
+ LOG_DHT_DEBUG("Different node " << node_id_to_hex(responder_id)
2292
+ << " responded from " << responder.ip << ":" << responder.port
2293
+ << " (expected old node " << node_id_to_hex(verification.old_node.id)
2294
+ << ") - old node is gone, replacing with candidate "
2295
+ << node_id_to_hex(verification.candidate_node.id));
2296
+
2297
+ // Replace old node with candidate
2298
+ DhtNode candidate = verification.candidate_node;
2299
+ candidate.mark_success(); // The responder just proved it's alive
2300
+ perform_replacement(candidate, verification.old_node, verification.bucket_index);
2070
2301
  }
2071
2302
 
2072
2303
  // Remove tracking entries
@@ -2339,4 +2570,74 @@ void DhtClient::set_data_directory(const std::string& directory) {
2339
2570
  LOG_DHT_DEBUG("Data directory set to: " << data_directory_);
2340
2571
  }
2341
2572
 
2573
+ #ifdef RATS_SEARCH_FEATURES
2574
+ // ============================================================================
2575
+ // SPIDER MODE IMPLEMENTATION
2576
+ // ============================================================================
2577
+
2578
+ void DhtClient::set_spider_mode(bool enable) {
2579
+ spider_mode_.store(enable);
2580
+ if (enable) {
2581
+ LOG_DHT_INFO("Spider mode ENABLED - aggressive node discovery active");
2582
+ } else {
2583
+ LOG_DHT_INFO("Spider mode DISABLED - normal DHT operation");
2584
+ }
2585
+ }
2586
+
2587
+ void DhtClient::set_spider_announce_callback(SpiderAnnounceCallback callback) {
2588
+ std::lock_guard<std::mutex> lock(spider_callbacks_mutex_);
2589
+ spider_announce_callback_ = std::move(callback);
2590
+ LOG_DHT_DEBUG("Spider announce callback set");
2591
+ }
2592
+
2593
+ void DhtClient::set_spider_ignore(bool ignore) {
2594
+ spider_ignore_.store(ignore);
2595
+ LOG_DHT_DEBUG("Spider ignore mode set to " << (ignore ? "true" : "false"));
2596
+ }
2597
+
2598
+ void DhtClient::spider_walk() {
2599
+ if (!running_) {
2600
+ return;
2601
+ }
2602
+
2603
+ // Get a random node from the routing table and send find_node
2604
+ DhtNode target_node;
2605
+ bool found = false;
2606
+
2607
+ {
2608
+ std::lock_guard<std::mutex> lock(routing_table_mutex_);
2609
+
2610
+ // Collect all nodes from non-empty buckets
2611
+ std::vector<DhtNode*> all_nodes;
2612
+ for (auto& bucket : routing_table_) {
2613
+ for (auto& node : bucket) {
2614
+ all_nodes.push_back(&node);
2615
+ }
2616
+ }
2617
+
2618
+ if (!all_nodes.empty()) {
2619
+ // Pick a random node
2620
+ static std::random_device rd;
2621
+ static std::mt19937 gen(rd());
2622
+ std::uniform_int_distribution<size_t> dis(0, all_nodes.size() - 1);
2623
+ target_node = *all_nodes[dis(gen)];
2624
+ found = true;
2625
+ }
2626
+ }
2627
+
2628
+ if (found) {
2629
+ // Generate a random target ID for find_node (like spider.js does)
2630
+ NodeId random_target = generate_node_id();
2631
+ send_krpc_find_node(target_node.peer, random_target);
2632
+ LOG_DHT_DEBUG("Spider walk: sent find_node to " << target_node.peer.ip << ":" << target_node.peer.port);
2633
+ } else {
2634
+ // No nodes in routing table, bootstrap
2635
+ LOG_DHT_DEBUG("Spider walk: no nodes in routing table, re-bootstrapping");
2636
+ for (const auto& bootstrap : get_default_bootstrap_nodes()) {
2637
+ send_krpc_find_node(bootstrap, node_id_);
2638
+ }
2639
+ }
2640
+ }
2641
+ #endif // RATS_SEARCH_FEATURES
2642
+
2342
2643
  } // namespace librats