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.
- package/README.md +1 -1
- package/lib/index.d.ts +2 -1
- package/native-src/CMakeLists.txt +9 -0
- package/native-src/src/dht.cpp +386 -85
- package/native-src/src/dht.h +95 -22
- package/native-src/src/ice.cpp +2 -2
- package/native-src/src/librats.cpp +62 -42
- package/native-src/src/librats.h +5 -2
- package/native-src/src/librats_c.cpp +18 -2
- package/native-src/src/librats_c.h +3 -1
- package/native-src/src/librats_mdns.cpp +5 -0
- package/native-src/src/mdns.cpp +13 -7
- package/native-src/src/mdns.h +2 -0
- package/native-src/src/noise.cpp +3 -1
- package/native-src/src/os.cpp +3 -1
- package/native-src/src/socket.h +3 -1
- package/native-src/src/stun.cpp +2 -2
- package/package.json +1 -1
- package/src/librats_node.cpp +46 -1
package/native-src/src/dht.cpp
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
//
|
|
905
|
+
// Save write token if present (needed for announce_peer after traversal completes)
|
|
767
906
|
if (!message.token.empty()) {
|
|
768
|
-
|
|
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
|
-
<< " (
|
|
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 (
|
|
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
|
-
|
|
1915
|
-
|
|
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
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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
|