librats 1.0.2 → 2.2.0
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 +145 -331
- package/binding.gyp +16 -3
- package/lib/index.d.ts +288 -696
- package/lib/index.js +407 -44
- package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
- package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
- package/native-src/CMakeLists.txt +404 -179
- package/native-src/LICENSE +1 -1
- package/native-src/src/librats/bindings/rats.cpp +762 -0
- package/native-src/src/librats/bindings/rats.h +380 -0
- package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
- package/native-src/src/librats/bittorrent/bencode.h +176 -0
- package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
- package/native-src/src/librats/bittorrent/bitfield.h +76 -0
- package/native-src/src/librats/bittorrent/byte_io.h +58 -0
- package/native-src/src/librats/bittorrent/choker.cpp +25 -0
- package/native-src/src/librats/bittorrent/choker.h +46 -0
- package/native-src/src/librats/bittorrent/client.cpp +413 -0
- package/native-src/src/librats/bittorrent/client.h +227 -0
- package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
- package/native-src/src/librats/bittorrent/disk_io.h +150 -0
- package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
- package/native-src/src/librats/bittorrent/extensions.h +94 -0
- package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
- package/native-src/src/librats/bittorrent/file_storage.h +79 -0
- package/native-src/src/librats/bittorrent/log.h +42 -0
- package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
- package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
- package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
- package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
- package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
- package/native-src/src/librats/bittorrent/peer_list.h +75 -0
- package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
- package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
- package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
- package/native-src/src/librats/bittorrent/reactor.h +89 -0
- package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
- package/native-src/src/librats/bittorrent/resume_data.h +41 -0
- package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
- package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
- package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
- package/native-src/src/librats/bittorrent/torrent.h +260 -0
- package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
- package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
- package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
- package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
- package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
- package/native-src/src/librats/bittorrent/tracker.h +108 -0
- package/native-src/src/librats/bittorrent/types.cpp +206 -0
- package/native-src/src/librats/bittorrent/types.h +86 -0
- package/native-src/src/librats/core/address.cpp +35 -0
- package/native-src/src/librats/core/address.h +78 -0
- package/native-src/src/librats/core/bytes.h +69 -0
- package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
- package/native-src/src/librats/core/chained_send_buffer.h +183 -0
- package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
- package/native-src/src/librats/core/endpoint_parse.h +31 -0
- package/native-src/src/librats/core/event_bus.h +70 -0
- package/native-src/src/librats/core/host_endpoint.h +56 -0
- package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
- package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
- package/native-src/src/librats/core/ip_address.cpp +120 -0
- package/native-src/src/librats/core/ip_address.h +109 -0
- package/native-src/src/librats/core/mpsc_queue.h +47 -0
- package/native-src/src/librats/core/notifier.h +74 -0
- package/native-src/src/librats/core/receive_buffer.cpp +219 -0
- package/native-src/src/librats/core/receive_buffer.h +171 -0
- package/native-src/src/librats/core/service_registry.h +58 -0
- package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
- package/native-src/src/librats/core/socket.h +496 -0
- package/native-src/src/librats/core/timer_queue.h +105 -0
- package/native-src/src/librats/core/types.cpp +43 -0
- package/native-src/src/librats/core/types.h +103 -0
- package/native-src/src/librats/core/wakeup_pipe.h +83 -0
- package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
- package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
- package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
- package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
- package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
- package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
- package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
- package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
- package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
- package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
- package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
- package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
- package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
- package/native-src/src/librats/crypto/hkdf.c +266 -0
- package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
- package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
- package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
- package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
- package/native-src/src/librats/crypto/poly1305.h +37 -0
- package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
- package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
- package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
- package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
- package/native-src/src/librats/dht/announce.cpp +37 -0
- package/native-src/src/librats/dht/announce.h +41 -0
- package/native-src/src/librats/dht/bep42.cpp +109 -0
- package/native-src/src/librats/dht/bep42.h +48 -0
- package/native-src/src/librats/dht/dht.cpp +501 -0
- package/native-src/src/librats/dht/dht.h +119 -0
- package/native-src/src/librats/dht/dht_runner.cpp +103 -0
- package/native-src/src/librats/dht/dht_runner.h +71 -0
- package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
- package/native-src/src/librats/dht/dos_blocker.h +47 -0
- package/native-src/src/librats/dht/find_peers.cpp +52 -0
- package/native-src/src/librats/dht/find_peers.h +73 -0
- package/native-src/src/librats/dht/id.h +167 -0
- package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
- package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
- package/native-src/src/librats/dht/log.h +38 -0
- package/native-src/src/librats/dht/node.cpp +473 -0
- package/native-src/src/librats/dht/node.h +164 -0
- package/native-src/src/librats/dht/node_entry.h +81 -0
- package/native-src/src/librats/dht/observer.h +72 -0
- package/native-src/src/librats/dht/persistence.cpp +90 -0
- package/native-src/src/librats/dht/persistence.h +32 -0
- package/native-src/src/librats/dht/routing_table.cpp +559 -0
- package/native-src/src/librats/dht/routing_table.h +185 -0
- package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
- package/native-src/src/librats/dht/rpc_manager.h +77 -0
- package/native-src/src/librats/dht/storage.cpp +92 -0
- package/native-src/src/librats/dht/storage.h +74 -0
- package/native-src/src/librats/dht/transport.h +27 -0
- package/native-src/src/librats/dht/traversal.cpp +326 -0
- package/native-src/src/librats/dht/traversal.h +120 -0
- package/native-src/src/librats/dht/udp_transport.cpp +49 -0
- package/native-src/src/librats/dht/udp_transport.h +51 -0
- package/native-src/src/librats/mdns/log.h +22 -0
- package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
- package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
- package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
- package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
- package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
- package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
- package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
- package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
- package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/config.h +110 -0
- package/native-src/src/librats/node/dial_service.h +54 -0
- package/native-src/src/librats/node/dialer.cpp +264 -0
- package/native-src/src/librats/node/dialer.h +188 -0
- package/native-src/src/librats/node/host_events.h +26 -0
- package/native-src/src/librats/node/identify.cpp +130 -0
- package/native-src/src/librats/node/identify.h +71 -0
- package/native-src/src/librats/node/nat_status.cpp +103 -0
- package/native-src/src/librats/node/nat_status.h +118 -0
- package/native-src/src/librats/node/node.cpp +865 -0
- package/native-src/src/librats/node/node.h +344 -0
- package/native-src/src/librats/node/node_context.h +33 -0
- package/native-src/src/librats/node/peer_network.h +91 -0
- package/native-src/src/librats/peer/peer.h +49 -0
- package/native-src/src/librats/peer/peer_book.cpp +181 -0
- package/native-src/src/librats/peer/peer_book.h +88 -0
- package/native-src/src/librats/peer/peer_id.cpp +72 -0
- package/native-src/src/librats/peer/peer_id.h +62 -0
- package/native-src/src/librats/peer/peer_info.h +37 -0
- package/native-src/src/librats/peer/peer_table.cpp +170 -0
- package/native-src/src/librats/peer/peer_table.h +148 -0
- package/native-src/src/librats/security/handshaker.h +66 -0
- package/native-src/src/librats/security/identity.h +43 -0
- package/native-src/src/librats/security/noise_security.cpp +122 -0
- package/native-src/src/librats/security/noise_security.h +37 -0
- package/native-src/src/librats/security/plaintext_security.h +106 -0
- package/native-src/src/librats/security/session.h +37 -0
- package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
- package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
- package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
- package/native-src/src/librats/subsystems/bittorrent.h +136 -0
- package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
- package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
- package/native-src/src/librats/subsystems/dht_service.h +36 -0
- package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
- package/native-src/src/librats/subsystems/file_transfer.h +367 -0
- package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
- package/native-src/src/librats/subsystems/hole_punch.h +290 -0
- package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
- package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
- package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
- package/native-src/src/librats/subsystems/message_json.cpp +112 -0
- package/native-src/src/librats/subsystems/message_json.h +88 -0
- package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
- package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
- package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
- package/native-src/src/librats/subsystems/ping_service.h +66 -0
- package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
- package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
- package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
- package/native-src/src/librats/subsystems/pubsub.h +175 -0
- package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
- package/native-src/src/librats/subsystems/reconnection.h +126 -0
- package/native-src/src/librats/subsystems/relay.cpp +1142 -0
- package/native-src/src/librats/subsystems/relay.h +211 -0
- package/native-src/src/librats/subsystems/relay_service.h +46 -0
- package/native-src/src/librats/transport/connection.cpp +343 -0
- package/native-src/src/librats/transport/connection.h +283 -0
- package/native-src/src/librats/transport/link.h +96 -0
- package/native-src/src/librats/transport/reactor.cpp +588 -0
- package/native-src/src/librats/transport/reactor.h +262 -0
- package/native-src/src/librats/transport/reactor_pool.h +81 -0
- package/native-src/src/librats/transport/relay_link.cpp +208 -0
- package/native-src/src/librats/transport/relay_link.h +303 -0
- package/native-src/src/librats/transport/tcp_link.cpp +49 -0
- package/native-src/src/librats/transport/tcp_link.h +43 -0
- package/native-src/src/librats/transport/udp_mux.cpp +617 -0
- package/native-src/src/librats/transport/udp_mux.h +363 -0
- package/native-src/src/librats/transport/udp_packet.cpp +121 -0
- package/native-src/src/librats/transport/udp_packet.h +190 -0
- package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
- package/native-src/src/librats/transport/udp_stream.h +614 -0
- package/native-src/src/librats/util/features.h.in +51 -0
- package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
- package/native-src/src/librats/util/fs.h +136 -0
- package/native-src/src/librats/util/json.cpp +1002 -0
- package/native-src/src/librats/util/json.h +444 -0
- package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
- package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
- package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
- package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
- package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
- package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
- package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
- package/native-src/src/librats/util/rats_export.h +69 -0
- package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
- package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
- package/native-src/src/librats/wire/frame.cpp +76 -0
- package/native-src/src/librats/wire/frame.h +111 -0
- package/native-src/src/librats/wire/message_router.cpp +45 -0
- package/native-src/src/librats/wire/message_router.h +47 -0
- package/package.json +5 -4
- package/scripts/build-librats.js +1 -0
- package/scripts/postinstall.js +3 -3
- package/scripts/prepare-package.js +4 -4
- package/scripts/verify-installation.js +63 -105
- package/src/librats_node.cpp +1067 -1323
- package/native-src/src/bencode.cpp +0 -485
- package/native-src/src/bencode.h +0 -145
- package/native-src/src/bittorrent.cpp +0 -14
- package/native-src/src/bittorrent.h +0 -74
- package/native-src/src/bt_bitfield.cpp +0 -372
- package/native-src/src/bt_bitfield.h +0 -316
- package/native-src/src/bt_choker.cpp +0 -228
- package/native-src/src/bt_choker.h +0 -147
- package/native-src/src/bt_client.cpp +0 -1047
- package/native-src/src/bt_client.h +0 -445
- package/native-src/src/bt_create_torrent.cpp +0 -677
- package/native-src/src/bt_create_torrent.h +0 -473
- package/native-src/src/bt_extension.cpp +0 -469
- package/native-src/src/bt_extension.h +0 -309
- package/native-src/src/bt_file_storage.cpp +0 -261
- package/native-src/src/bt_file_storage.h +0 -298
- package/native-src/src/bt_handshake.cpp +0 -134
- package/native-src/src/bt_handshake.h +0 -157
- package/native-src/src/bt_messages.cpp +0 -364
- package/native-src/src/bt_messages.h +0 -324
- package/native-src/src/bt_network.cpp +0 -1007
- package/native-src/src/bt_network.h +0 -417
- package/native-src/src/bt_peer_connection.cpp +0 -742
- package/native-src/src/bt_peer_connection.h +0 -592
- package/native-src/src/bt_piece_picker.cpp +0 -786
- package/native-src/src/bt_piece_picker.h +0 -473
- package/native-src/src/bt_resume_data.cpp +0 -410
- package/native-src/src/bt_resume_data.h +0 -249
- package/native-src/src/bt_torrent.cpp +0 -2120
- package/native-src/src/bt_torrent.h +0 -641
- package/native-src/src/bt_torrent_info.cpp +0 -659
- package/native-src/src/bt_torrent_info.h +0 -418
- package/native-src/src/bt_types.h +0 -621
- package/native-src/src/chained_send_buffer.cpp +0 -75
- package/native-src/src/chained_send_buffer.h +0 -137
- package/native-src/src/crypto/hkdf.c +0 -266
- package/native-src/src/crypto/poly1305.h +0 -36
- package/native-src/src/dht.cpp +0 -3311
- package/native-src/src/dht.h +0 -717
- package/native-src/src/disk_io.cpp +0 -632
- package/native-src/src/disk_io.h +0 -315
- package/native-src/src/file_transfer.cpp +0 -1415
- package/native-src/src/file_transfer.h +0 -286
- package/native-src/src/fs.h +0 -108
- package/native-src/src/gossipsub.cpp +0 -1139
- package/native-src/src/gossipsub.h +0 -403
- package/native-src/src/ice.cpp +0 -893
- package/native-src/src/ice.h +0 -559
- package/native-src/src/json.hpp +0 -25526
- package/native-src/src/librats.cpp +0 -2378
- package/native-src/src/librats.h +0 -2324
- package/native-src/src/librats_bittorrent.cpp +0 -601
- package/native-src/src/librats_c.cpp +0 -1557
- package/native-src/src/librats_c.h +0 -323
- package/native-src/src/librats_discovery.cpp +0 -402
- package/native-src/src/librats_encryption.cpp +0 -275
- package/native-src/src/librats_file_transfer.cpp +0 -144
- package/native-src/src/librats_gossipsub.cpp +0 -289
- package/native-src/src/librats_ice.cpp +0 -213
- package/native-src/src/librats_log_macros.h +0 -36
- package/native-src/src/librats_logging.cpp +0 -173
- package/native-src/src/librats_mdns.cpp +0 -166
- package/native-src/src/librats_persistence.cpp +0 -796
- package/native-src/src/librats_portmap.cpp +0 -419
- package/native-src/src/librats_reconnection.cpp +0 -218
- package/native-src/src/librats_statistic.cpp +0 -105
- package/native-src/src/librats_storage.cpp +0 -189
- package/native-src/src/rats_export.h +0 -17
- package/native-src/src/receive_buffer.cpp +0 -82
- package/native-src/src/receive_buffer.h +0 -127
- package/native-src/src/socket.h +0 -228
- package/native-src/src/threadmanager.cpp +0 -105
- package/native-src/src/threadmanager.h +0 -53
- package/native-src/src/tracker.cpp +0 -1264
- package/native-src/src/tracker.h +0 -319
- package/native-src/src/turn.cpp +0 -762
- package/native-src/src/turn.h +0 -460
- package/native-src/src/wakeup_pipe.h +0 -60
- /package/native-src/src/{os.h → librats/util/os.h} +0 -0
package/native-src/src/dht.cpp
DELETED
|
@@ -1,3311 +0,0 @@
|
|
|
1
|
-
#include "dht.h"
|
|
2
|
-
#include "network_utils.h"
|
|
3
|
-
#include "logger.h"
|
|
4
|
-
#include "socket.h"
|
|
5
|
-
#include "sha1.h"
|
|
6
|
-
#include "json.hpp"
|
|
7
|
-
#include <random>
|
|
8
|
-
#include <algorithm>
|
|
9
|
-
#include <sstream>
|
|
10
|
-
#include <iomanip>
|
|
11
|
-
#include <cstring>
|
|
12
|
-
#include <cmath>
|
|
13
|
-
#include <fstream>
|
|
14
|
-
|
|
15
|
-
#ifdef _WIN32
|
|
16
|
-
#include <winsock2.h>
|
|
17
|
-
#include <ws2tcpip.h>
|
|
18
|
-
#else
|
|
19
|
-
#include <arpa/inet.h>
|
|
20
|
-
#include <netinet/in.h>
|
|
21
|
-
#endif
|
|
22
|
-
|
|
23
|
-
// DHT module logging macros
|
|
24
|
-
#define LOG_DHT_DEBUG(message) LOG_DEBUG("dht", message)
|
|
25
|
-
#define LOG_DHT_INFO(message) LOG_INFO("dht", message)
|
|
26
|
-
#define LOG_DHT_WARN(message) LOG_WARN("dht", message)
|
|
27
|
-
#define LOG_DHT_ERROR(message) LOG_ERROR("dht", message)
|
|
28
|
-
|
|
29
|
-
namespace librats {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
DhtClient::DhtClient(int port, const std::string& bind_address, const std::string& data_directory,
|
|
43
|
-
AddressFamily address_family)
|
|
44
|
-
: port_(port), bind_address_(bind_address), data_directory_(data_directory),
|
|
45
|
-
address_family_(address_family), socket_(INVALID_SOCKET_VALUE), running_(false) {
|
|
46
|
-
node_id_ = generate_node_id();
|
|
47
|
-
routing_table_.resize(NODE_ID_SIZE * 8); // 160 buckets for 160-bit node IDs
|
|
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
|
-
|
|
57
|
-
if (data_directory_.empty()) {
|
|
58
|
-
data_directory_ = ".";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
LOG_DHT_INFO("DHT client created with node ID: " << node_id_to_hex(node_id_) <<
|
|
62
|
-
" family: " << (is_ipv6() ? "IPv6" : "IPv4") <<
|
|
63
|
-
(bind_address_.empty() ? "" : " bind address: " + bind_address_) <<
|
|
64
|
-
" data directory: " << data_directory_);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
DhtClient::~DhtClient() {
|
|
68
|
-
stop();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
bool DhtClient::start() {
|
|
72
|
-
if (running_) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
LOG_DHT_INFO("Starting DHT client on port " << port_ <<
|
|
77
|
-
(bind_address_.empty() ? "" : " bound to " + bind_address_));
|
|
78
|
-
|
|
79
|
-
// Initialize socket library (safe to call multiple times)
|
|
80
|
-
if (!init_socket_library()) {
|
|
81
|
-
LOG_DHT_ERROR("Failed to initialize socket library");
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Each family gets its own single-family socket. IPv6 uses V6ONLY so the two
|
|
86
|
-
// sockets can share the same port and never cross-deliver mapped addresses.
|
|
87
|
-
socket_ = create_udp_socket(port_, bind_address_, address_family_);
|
|
88
|
-
// Fallback to free port
|
|
89
|
-
if (!is_valid_socket(socket_) && port_ > 0) {
|
|
90
|
-
// Requested port is not available, try ephemeral port as fallback
|
|
91
|
-
int original_port = port_;
|
|
92
|
-
LOG_DHT_WARN("UDP port " << original_port << " is not available, falling back to ephemeral port");
|
|
93
|
-
socket_ = create_udp_socket(0, bind_address_, address_family_);
|
|
94
|
-
if (is_valid_socket(socket_)) {
|
|
95
|
-
port_ = get_bound_port(socket_);
|
|
96
|
-
LOG_DHT_INFO("Fell back from port " << original_port << " to ephemeral port " << port_);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (!is_valid_socket(socket_)) {
|
|
100
|
-
LOG_DHT_ERROR("Failed to create " << (is_ipv6() ? "IPv6" : "IPv4") << " UDP socket on port " << port_);
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!set_socket_nonblocking(socket_)) {
|
|
105
|
-
LOG_DHT_WARN("Failed to set socket to non-blocking mode");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
running_ = true;
|
|
109
|
-
|
|
110
|
-
// Load saved routing table before starting threads
|
|
111
|
-
if (load_routing_table()) {
|
|
112
|
-
LOG_DHT_INFO("Loaded routing table from disk (" << get_routing_table_size() << " nodes)");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Start network and maintenance threads
|
|
116
|
-
network_thread_ = std::thread(&DhtClient::network_loop, this);
|
|
117
|
-
maintenance_thread_ = std::thread(&DhtClient::maintenance_loop, this);
|
|
118
|
-
|
|
119
|
-
LOG_DHT_INFO("DHT client started successfully");
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
void DhtClient::stop() {
|
|
124
|
-
if (!running_) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
LOG_DHT_INFO("Stopping DHT client");
|
|
129
|
-
|
|
130
|
-
// Trigger immediate shutdown of all background threads
|
|
131
|
-
shutdown_immediate();
|
|
132
|
-
|
|
133
|
-
// Wait for threads to finish
|
|
134
|
-
if (network_thread_.joinable()) {
|
|
135
|
-
network_thread_.join();
|
|
136
|
-
}
|
|
137
|
-
if (maintenance_thread_.joinable()) {
|
|
138
|
-
maintenance_thread_.join();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Save routing table before closing
|
|
142
|
-
if (save_routing_table()) {
|
|
143
|
-
LOG_DHT_INFO("Saved routing table to disk (" << get_routing_table_size() << " nodes)");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Close socket
|
|
147
|
-
if (is_valid_socket(socket_)) {
|
|
148
|
-
close_socket(socket_);
|
|
149
|
-
socket_ = INVALID_SOCKET_VALUE;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
LOG_DHT_INFO("DHT client stopped");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
void DhtClient::shutdown_immediate() {
|
|
156
|
-
LOG_DHT_INFO("Triggering immediate shutdown of DHT background threads");
|
|
157
|
-
|
|
158
|
-
running_.store(false);
|
|
159
|
-
|
|
160
|
-
// Notify all waiting threads to wake up immediately
|
|
161
|
-
shutdown_cv_.notify_all();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
bool DhtClient::bootstrap(const std::vector<Peer>& bootstrap_nodes) {
|
|
165
|
-
if (!running_) {
|
|
166
|
-
LOG_DHT_ERROR("DHT client not running");
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
LOG_DHT_INFO("Bootstrapping DHT with " << bootstrap_nodes.size() << " nodes");
|
|
171
|
-
LOG_DHT_DEBUG("Bootstrap nodes:");
|
|
172
|
-
for (const auto& peer : bootstrap_nodes) {
|
|
173
|
-
LOG_DHT_DEBUG(" - " << peer.ip << ":" << peer.port);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Send ping to bootstrap nodes
|
|
179
|
-
LOG_DHT_DEBUG("Sending PING to all bootstrap nodes");
|
|
180
|
-
for (const auto& peer : bootstrap_nodes) {
|
|
181
|
-
send_krpc_ping(peer);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Start node discovery by finding our own node
|
|
185
|
-
LOG_DHT_DEBUG("Starting node discovery by finding our own node ID: " << node_id_to_hex(node_id_));
|
|
186
|
-
for (const auto& peer : bootstrap_nodes) {
|
|
187
|
-
send_krpc_find_node(peer, node_id_);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
LOG_DHT_DEBUG("Bootstrap process initiated");
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
bool DhtClient::find_peers(const InfoHash& info_hash, PeerDiscoveryCallback callback) {
|
|
195
|
-
if (!running_) {
|
|
196
|
-
LOG_DHT_ERROR("DHT client not running");
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
201
|
-
LOG_DHT_INFO("Finding peers for info hash: " << hash_key);
|
|
202
|
-
|
|
203
|
-
// Get initial nodes from routing table
|
|
204
|
-
auto closest_nodes = find_closest_nodes(info_hash, K_BUCKET_SIZE);
|
|
205
|
-
|
|
206
|
-
if (closest_nodes.empty()) {
|
|
207
|
-
LOG_DHT_WARN("No nodes in routing table to query for info_hash " << hash_key);
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
DeferredCallbacks deferred;
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
215
|
-
|
|
216
|
-
// Check if a search is already ongoing for this info_hash
|
|
217
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
218
|
-
if (search_it != pending_searches_.end()) {
|
|
219
|
-
// Search already in progress - just add the callback to the list
|
|
220
|
-
LOG_DHT_INFO("Search already in progress for info hash " << hash_key << " - adding callback to existing search");
|
|
221
|
-
search_it->second.callbacks.push_back(callback);
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Create new search
|
|
226
|
-
PendingSearch new_search(info_hash);
|
|
227
|
-
new_search.callbacks.push_back(callback);
|
|
228
|
-
|
|
229
|
-
// Initialize search_nodes with closest nodes from routing table (already sorted)
|
|
230
|
-
new_search.search_nodes = std::move(closest_nodes);
|
|
231
|
-
|
|
232
|
-
auto insert_result = pending_searches_.emplace(hash_key, std::move(new_search));
|
|
233
|
-
PendingSearch& search_ref = insert_result.first->second;
|
|
234
|
-
|
|
235
|
-
LOG_DHT_DEBUG("Initialized search with " << search_ref.search_nodes.size() << " nodes from routing table");
|
|
236
|
-
|
|
237
|
-
// Start sending requests
|
|
238
|
-
add_search_requests(search_ref, deferred);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Invoke callbacks outside the lock to avoid deadlock
|
|
242
|
-
deferred.invoke();
|
|
243
|
-
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port, PeerDiscoveryCallback callback) {
|
|
248
|
-
if (!running_) {
|
|
249
|
-
LOG_DHT_ERROR("DHT client not running");
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (port == 0) {
|
|
254
|
-
port = port_;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
258
|
-
LOG_DHT_INFO("Announcing peer for info hash: " << hash_key << " on port " << port);
|
|
259
|
-
|
|
260
|
-
// BEP 5 compliant announce:
|
|
261
|
-
// 1. Perform iterative Kademlia lookup (like find_peers)
|
|
262
|
-
// 2. Collect tokens from responding nodes
|
|
263
|
-
// 3. Send announce_peer to k closest nodes with their tokens
|
|
264
|
-
|
|
265
|
-
// Get initial nodes from routing table
|
|
266
|
-
auto closest_nodes = find_closest_nodes(info_hash, K_BUCKET_SIZE);
|
|
267
|
-
|
|
268
|
-
if (closest_nodes.empty()) {
|
|
269
|
-
LOG_DHT_WARN("No nodes in routing table to announce to for info_hash " << hash_key);
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
DeferredCallbacks deferred;
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
277
|
-
|
|
278
|
-
// Check if a search/announce is already ongoing for this info_hash
|
|
279
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
280
|
-
if (search_it != pending_searches_.end()) {
|
|
281
|
-
if (search_it->second.is_announce) {
|
|
282
|
-
LOG_DHT_INFO("Announce already in progress for info hash " << hash_key);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
// Regular find_peers in progress - let it complete, then user can announce again
|
|
286
|
-
LOG_DHT_WARN("find_peers already in progress for info hash " << hash_key << " - announce will wait");
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Create new search with announce flag
|
|
291
|
-
PendingSearch new_search(info_hash);
|
|
292
|
-
new_search.is_announce = true;
|
|
293
|
-
new_search.announce_port = port;
|
|
294
|
-
|
|
295
|
-
// Add callback if provided - peers discovered during traversal will be returned through it
|
|
296
|
-
if (callback) {
|
|
297
|
-
new_search.callbacks.push_back(callback);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Initialize search_nodes with closest nodes from routing table (already sorted)
|
|
301
|
-
new_search.search_nodes = std::move(closest_nodes);
|
|
302
|
-
|
|
303
|
-
auto insert_result = pending_searches_.emplace(hash_key, std::move(new_search));
|
|
304
|
-
PendingSearch& search_ref = insert_result.first->second;
|
|
305
|
-
|
|
306
|
-
LOG_DHT_DEBUG("Initialized announce search with " << search_ref.search_nodes.size() << " nodes from routing table");
|
|
307
|
-
|
|
308
|
-
// Start sending requests
|
|
309
|
-
add_search_requests(search_ref, deferred);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Invoke callbacks outside the lock to avoid deadlock
|
|
313
|
-
deferred.invoke();
|
|
314
|
-
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
void DhtClient::cancel_search(const InfoHash& info_hash) {
|
|
319
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
320
|
-
|
|
321
|
-
LOG_DHT_INFO("Cancelling search/announce for info hash: " << hash_key);
|
|
322
|
-
|
|
323
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
324
|
-
|
|
325
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
326
|
-
if (search_it == pending_searches_.end()) {
|
|
327
|
-
LOG_DHT_DEBUG("No active search found for info hash " << hash_key);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Mark as finished to prevent further queries
|
|
332
|
-
search_it->second.is_finished = true;
|
|
333
|
-
|
|
334
|
-
// Remove all associated transactions
|
|
335
|
-
std::vector<std::string> transactions_to_remove;
|
|
336
|
-
for (const auto& [tx_id, tx] : transaction_to_search_) {
|
|
337
|
-
if (tx.info_hash_hex == hash_key) {
|
|
338
|
-
transactions_to_remove.push_back(tx_id);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (const auto& tx_id : transactions_to_remove) {
|
|
343
|
-
transaction_to_search_.erase(tx_id);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Remove the search
|
|
347
|
-
pending_searches_.erase(search_it);
|
|
348
|
-
|
|
349
|
-
LOG_DHT_INFO("Cancelled search for info hash " << hash_key <<
|
|
350
|
-
", removed " << transactions_to_remove.size() << " pending transactions");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
size_t DhtClient::get_routing_table_size() const {
|
|
354
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
355
|
-
size_t total = 0;
|
|
356
|
-
for (const auto& bucket : routing_table_) {
|
|
357
|
-
total += bucket.size();
|
|
358
|
-
}
|
|
359
|
-
return total;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
size_t DhtClient::get_pending_ping_verifications_count() const {
|
|
363
|
-
std::lock_guard<std::mutex> lock(pending_pings_mutex_);
|
|
364
|
-
return pending_pings_.size();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
bool DhtClient::is_search_active(const InfoHash& info_hash) const {
|
|
368
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
369
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
370
|
-
auto it = pending_searches_.find(hash_key);
|
|
371
|
-
return it != pending_searches_.end() && !it->second.is_finished;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
bool DhtClient::is_announce_active(const InfoHash& info_hash) const {
|
|
375
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
376
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
377
|
-
auto it = pending_searches_.find(hash_key);
|
|
378
|
-
return it != pending_searches_.end() && !it->second.is_finished && it->second.is_announce;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
size_t DhtClient::get_active_searches_count() const {
|
|
382
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
383
|
-
size_t count = 0;
|
|
384
|
-
for (const auto& [hash, search] : pending_searches_) {
|
|
385
|
-
if (!search.is_finished) {
|
|
386
|
-
count++;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return count;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
size_t DhtClient::get_active_announces_count() const {
|
|
393
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
394
|
-
size_t count = 0;
|
|
395
|
-
for (const auto& [hash, search] : pending_searches_) {
|
|
396
|
-
if (!search.is_finished && search.is_announce) {
|
|
397
|
-
count++;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return count;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
std::vector<Peer> DhtClient::get_default_bootstrap_nodes() {
|
|
404
|
-
// Hostnames are resolved to the instance's family at send time (A for IPv4, AAAA for
|
|
405
|
-
// IPv6). dht.libtorrent.org publishes an AAAA record, giving the IPv6 network a
|
|
406
|
-
// reliable bootstrap entry point.
|
|
407
|
-
return {
|
|
408
|
-
{"router.bittorrent.com", 6881},
|
|
409
|
-
{"dht.transmissionbt.com", 6881},
|
|
410
|
-
{"router.utorrent.com", 6881},
|
|
411
|
-
{"dht.libtorrent.org", 25401},
|
|
412
|
-
{"dht.aelitis.com", 6881}
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
void DhtClient::network_loop() {
|
|
417
|
-
LOG_DHT_DEBUG("Network loop started");
|
|
418
|
-
|
|
419
|
-
while (running_) {
|
|
420
|
-
Peer sender;
|
|
421
|
-
auto data = receive_udp_data(socket_, 1500, sender, -1); // MTU size, blocking
|
|
422
|
-
|
|
423
|
-
if (!data.empty()) {
|
|
424
|
-
LOG_DHT_DEBUG("Received " << data.size() << " bytes from " << sender.ip << ":" << sender.port);
|
|
425
|
-
handle_message(data, sender);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Use conditional variable for responsive shutdown
|
|
429
|
-
{
|
|
430
|
-
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
431
|
-
if (shutdown_cv_.wait_for(lock, std::chrono::milliseconds(10), [this] { return !running_.load(); })) {
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
LOG_DHT_DEBUG("Network loop stopped");
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
void DhtClient::maintenance_loop() {
|
|
441
|
-
LOG_DHT_DEBUG("Maintenance loop started");
|
|
442
|
-
|
|
443
|
-
auto last_bucket_refresh = std::chrono::steady_clock::now();
|
|
444
|
-
auto last_ping_verification_cleanup = std::chrono::steady_clock::now();
|
|
445
|
-
auto last_general_cleanup = std::chrono::steady_clock::now();
|
|
446
|
-
auto last_stats_print = std::chrono::steady_clock::now();
|
|
447
|
-
auto last_search_timeout_check = std::chrono::steady_clock::now();
|
|
448
|
-
auto last_search_node_cleanup = std::chrono::steady_clock::now();
|
|
449
|
-
auto last_routing_table_save = std::chrono::steady_clock::now();
|
|
450
|
-
|
|
451
|
-
while (running_) {
|
|
452
|
-
auto now = std::chrono::steady_clock::now();
|
|
453
|
-
|
|
454
|
-
// Check for timed out search requests every 2 seconds (frequent check)
|
|
455
|
-
if (now - last_search_timeout_check >= std::chrono::seconds(2)) {
|
|
456
|
-
cleanup_timed_out_search_requests();
|
|
457
|
-
last_search_timeout_check = now;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Clean up finalized node_states entries in active searches every 10 seconds
|
|
461
|
-
if (now - last_search_node_cleanup >= std::chrono::seconds(10)) {
|
|
462
|
-
cleanup_search_node_states();
|
|
463
|
-
last_search_node_cleanup = now;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// General cleanup operations every 1 minute (like previously)
|
|
467
|
-
if (now - last_general_cleanup >= std::chrono::minutes(1)) {
|
|
468
|
-
// Cleanup stale nodes every 1 minute
|
|
469
|
-
cleanup_stale_nodes();
|
|
470
|
-
|
|
471
|
-
// Cleanup stale pending searches
|
|
472
|
-
cleanup_stale_searches();
|
|
473
|
-
|
|
474
|
-
// Cleanup stale announced peers
|
|
475
|
-
cleanup_stale_announced_peers();
|
|
476
|
-
|
|
477
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
478
|
-
// Cleanup stale spider transactions (requests without responses)
|
|
479
|
-
cleanup_stale_spider_transactions();
|
|
480
|
-
#endif
|
|
481
|
-
|
|
482
|
-
last_general_cleanup = now;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Refresh buckets every 30 minutes
|
|
486
|
-
if (now - last_bucket_refresh >= std::chrono::minutes(30)) {
|
|
487
|
-
refresh_buckets();
|
|
488
|
-
last_bucket_refresh = now;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Frequent maintenance: ping verifications time out at ~30s, so check often
|
|
492
|
-
if (now - last_ping_verification_cleanup >= std::chrono::seconds(30)) {
|
|
493
|
-
cleanup_stale_ping_verifications();
|
|
494
|
-
last_ping_verification_cleanup = now;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Print DHT statistics every 10 seconds
|
|
498
|
-
if (now - last_stats_print >= std::chrono::seconds(10)) {
|
|
499
|
-
print_statistics();
|
|
500
|
-
last_stats_print = now;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Save routing table every 5 minutes
|
|
504
|
-
if (now - last_routing_table_save >= std::chrono::minutes(5)) {
|
|
505
|
-
if (save_routing_table()) {
|
|
506
|
-
LOG_DHT_DEBUG("Periodic routing table save completed");
|
|
507
|
-
}
|
|
508
|
-
last_routing_table_save = now;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Execute maintenance loop every 1 second
|
|
512
|
-
{
|
|
513
|
-
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
514
|
-
if (shutdown_cv_.wait_for(lock, std::chrono::seconds(1), [this] { return !running_.load(); })) {
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
LOG_DHT_DEBUG("Maintenance loop stopped");
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
void DhtClient::handle_message(const std::vector<uint8_t>& data, const Peer& sender) {
|
|
524
|
-
LOG_DHT_DEBUG("Processing message of " << data.size() << " bytes from " << sender.ip << ":" << sender.port);
|
|
525
|
-
|
|
526
|
-
auto krpc_message = KrpcProtocol::decode_message(data);
|
|
527
|
-
if (!krpc_message) {
|
|
528
|
-
LOG_DHT_DEBUG("Failed to decode KRPC message from " << sender.ip << ":" << sender.port);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
handle_krpc_message(*krpc_message, sender);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
void DhtClient::add_node(const DhtNode& node, bool confirmed, bool no_verify) {
|
|
536
|
-
// IPv4 and IPv6 are separate Kademlia networks (BEP 32). Reject any node whose
|
|
537
|
-
// address family does not match this instance to keep the routing table clean.
|
|
538
|
-
if (network_utils::is_valid_ipv6(node.peer.ip) != is_ipv6()) {
|
|
539
|
-
LOG_DHT_DEBUG("Ignoring " << (is_ipv6() ? "non-IPv6" : "non-IPv4") << " node "
|
|
540
|
-
<< node.peer.ip << ":" << node.peer.port << " (wrong family)");
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
|
|
545
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
546
|
-
|
|
547
|
-
int bucket_index = get_bucket_index(node.id);
|
|
548
|
-
auto& bucket = routing_table_[bucket_index];
|
|
549
|
-
|
|
550
|
-
LOG_DHT_DEBUG("Adding node " << node_id_to_hex(node.id) << " at " << node.peer.ip << ":" << node.peer.port
|
|
551
|
-
<< " to bucket " << bucket_index << " (confirmed=" << confirmed << ")");
|
|
552
|
-
|
|
553
|
-
// Check if node already exists
|
|
554
|
-
auto it = std::find_if(bucket.begin(), bucket.end(),
|
|
555
|
-
[&node](const DhtNode& existing) {
|
|
556
|
-
return existing.id == node.id;
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
if (it != bucket.end()) {
|
|
560
|
-
// Update existing node - mark as successful since it contacted us
|
|
561
|
-
LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " already exists in bucket " << bucket_index << ", updating");
|
|
562
|
-
it->peer = node.peer;
|
|
563
|
-
if (confirmed) {
|
|
564
|
-
it->mark_success();
|
|
565
|
-
}
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Bucket has space - just add
|
|
570
|
-
if (bucket.size() < K_BUCKET_SIZE) {
|
|
571
|
-
DhtNode new_node = node;
|
|
572
|
-
if (confirmed) {
|
|
573
|
-
new_node.fail_count = 0; // Node contacted us, so it's confirmed good
|
|
574
|
-
}
|
|
575
|
-
bucket.push_back(new_node);
|
|
576
|
-
LOG_DHT_DEBUG("Added new node " << node_id_to_hex(node.id) << " to bucket " << bucket_index << " (size: " << bucket.size() << "/" << K_BUCKET_SIZE << ")");
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Bucket is full - first check for nodes with failures (stale nodes)
|
|
581
|
-
auto worst_it = std::max_element(bucket.begin(), bucket.end(),
|
|
582
|
-
[](const DhtNode& a, const DhtNode& b) {
|
|
583
|
-
// Find node with highest fail_count
|
|
584
|
-
return a.fail_count < b.fail_count;
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
if (worst_it != bucket.end() && worst_it->fail_count > 0) {
|
|
588
|
-
// Found a stale node - replace it immediately
|
|
589
|
-
LOG_DHT_DEBUG("Replacing stale node " << node_id_to_hex(worst_it->id)
|
|
590
|
-
<< " (fail_count=" << static_cast<int>(worst_it->fail_count) << ")"
|
|
591
|
-
<< " with " << node_id_to_hex(node.id));
|
|
592
|
-
DhtNode new_node = node;
|
|
593
|
-
if (confirmed) {
|
|
594
|
-
new_node.fail_count = 0; // Node contacted us, so it's confirmed good
|
|
595
|
-
}
|
|
596
|
-
// else: keep fail_count = 0xff (unpinged) from constructor
|
|
597
|
-
*worst_it = new_node;
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// All nodes are good - find the "worst" good node for ping verification or replacement
|
|
602
|
-
// Worst = highest RTT among nodes not already being pinged
|
|
603
|
-
DhtNode* worst = nullptr;
|
|
604
|
-
for (auto& existing : bucket) {
|
|
605
|
-
if (nodes_being_replaced_.find(existing.id) == nodes_being_replaced_.end()) {
|
|
606
|
-
if (!worst || existing.is_worse_than(*worst)) {
|
|
607
|
-
worst = &existing;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!worst) {
|
|
613
|
-
LOG_DHT_DEBUG("All nodes in bucket already have pending pings - dropping candidate " << node_id_to_hex(node.id));
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// If no_verify is set, directly replace the worst node without ping verification
|
|
618
|
-
if (no_verify) {
|
|
619
|
-
LOG_DHT_DEBUG("Force adding node " << node_id_to_hex(node.id)
|
|
620
|
-
<< " - replacing worst node " << node_id_to_hex(worst->id)
|
|
621
|
-
<< " (rtt=" << worst->rtt << "ms) without ping verification");
|
|
622
|
-
DhtNode new_node = node;
|
|
623
|
-
if (confirmed) {
|
|
624
|
-
new_node.fail_count = 0;
|
|
625
|
-
}
|
|
626
|
-
*worst = new_node;
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Initiate ping to worst node - if it doesn't respond, replace with candidate
|
|
631
|
-
LOG_DHT_DEBUG("All nodes good, pinging worst node " << node_id_to_hex(worst->id)
|
|
632
|
-
<< " (rtt=" << worst->rtt << "ms) to verify");
|
|
633
|
-
initiate_ping_verification(node, *worst, bucket_index);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
std::vector<DhtNode> DhtClient::find_closest_nodes(const NodeId& target, size_t count) {
|
|
637
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
638
|
-
|
|
639
|
-
auto result = find_closest_nodes_unlocked(target, count);
|
|
640
|
-
|
|
641
|
-
return result;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
std::vector<DhtNode> DhtClient::find_closest_nodes_unlocked(const NodeId& target, size_t count) {
|
|
645
|
-
LOG_DHT_DEBUG("Finding closest nodes to target " << node_id_to_hex(target) << " (max " << count << " nodes)");
|
|
646
|
-
|
|
647
|
-
// Find closest bucket to target
|
|
648
|
-
int target_bucket = get_bucket_index(target);
|
|
649
|
-
|
|
650
|
-
// Candidate nodes to be closest to target
|
|
651
|
-
std::vector<DhtNode> candidates;
|
|
652
|
-
// Reserve extra space: 3x count + buffer for 2 full buckets to avoid reallocation
|
|
653
|
-
candidates.reserve(count * 3 + K_BUCKET_SIZE * 2);
|
|
654
|
-
|
|
655
|
-
// Add nodes from ideal bucket
|
|
656
|
-
if (target_bucket >= 0 && static_cast<size_t>(target_bucket) < routing_table_.size()) {
|
|
657
|
-
const auto& bucket = routing_table_[target_bucket];
|
|
658
|
-
candidates.insert(candidates.end(), bucket.begin(), bucket.end());
|
|
659
|
-
LOG_DHT_DEBUG("Collected " << bucket.size() << " nodes from target bucket " << target_bucket);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Add nodes from buckets above and below the ideal bucket
|
|
663
|
-
// Collect more candidates than needed to ensure we get the actual closest ones after sorting
|
|
664
|
-
size_t desired_candidates = count * 3; // Collect 3x more candidates for better selection
|
|
665
|
-
int low = target_bucket - 1;
|
|
666
|
-
int high = target_bucket + 1;
|
|
667
|
-
const int max_bucket_index = static_cast<int>(routing_table_.size()) - 1;
|
|
668
|
-
int buckets_checked = 1; // Already checked target_bucket
|
|
669
|
-
|
|
670
|
-
while (candidates.size() < desired_candidates && (low >= 0 || high <= max_bucket_index)) {
|
|
671
|
-
// Search left (closer buckets)
|
|
672
|
-
if (low >= 0) {
|
|
673
|
-
const auto& bucket = routing_table_[low];
|
|
674
|
-
if (!bucket.empty()) {
|
|
675
|
-
candidates.insert(candidates.end(), bucket.begin(), bucket.end());
|
|
676
|
-
LOG_DHT_DEBUG("Collected " << bucket.size() << " nodes from bucket " << low);
|
|
677
|
-
}
|
|
678
|
-
low--;
|
|
679
|
-
buckets_checked++;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Search right (farther buckets)
|
|
683
|
-
if (high <= max_bucket_index) {
|
|
684
|
-
const auto& bucket = routing_table_[high];
|
|
685
|
-
if (!bucket.empty()) {
|
|
686
|
-
candidates.insert(candidates.end(), bucket.begin(), bucket.end());
|
|
687
|
-
LOG_DHT_DEBUG("Collected " << bucket.size() << " nodes from bucket " << high);
|
|
688
|
-
}
|
|
689
|
-
high++;
|
|
690
|
-
buckets_checked++;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
LOG_DHT_DEBUG("Bucket-aware collection: checked " << buckets_checked << " buckets, collected "
|
|
695
|
-
<< candidates.size() << " candidate nodes around target bucket " << target_bucket);
|
|
696
|
-
|
|
697
|
-
if (candidates.empty()) {
|
|
698
|
-
LOG_DHT_DEBUG("No candidates found in routing table");
|
|
699
|
-
return candidates;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Use partial_sort to efficiently get only the 'count' closest nodes - O(n log k) vs O(n log n)
|
|
703
|
-
size_t sort_count = (std::min)(count, candidates.size());
|
|
704
|
-
std::partial_sort(
|
|
705
|
-
candidates.begin(),
|
|
706
|
-
candidates.begin() + sort_count,
|
|
707
|
-
candidates.end(),
|
|
708
|
-
[&target, this](const DhtNode& a, const DhtNode& b) {
|
|
709
|
-
return is_closer(a.id, b.id, target);
|
|
710
|
-
}
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
// Return up to 'count' closest nodes
|
|
714
|
-
if (candidates.size() > count) {
|
|
715
|
-
candidates.resize(count);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
LOG_DHT_DEBUG("Found " << candidates.size() << " closest nodes to target " << node_id_to_hex(target));
|
|
719
|
-
for (size_t i = 0; i < candidates.size(); ++i) {
|
|
720
|
-
LOG_DHT_DEBUG(" [" << i << "] " << node_id_to_hex(candidates[i].id) << " at " << candidates[i].peer.ip << ":" << candidates[i].peer.port);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Debug alternative: Compare with full routing table algorithm
|
|
724
|
-
/*
|
|
725
|
-
candidates.clear();
|
|
726
|
-
for (const auto& bucket : routing_table_) {
|
|
727
|
-
candidates.insert(candidates.end(), bucket.begin(), bucket.end());
|
|
728
|
-
}
|
|
729
|
-
sort_count = (std::min)(count, candidates.size());
|
|
730
|
-
std::partial_sort(
|
|
731
|
-
candidates.begin(),
|
|
732
|
-
candidates.begin() + sort_count,
|
|
733
|
-
candidates.end(),
|
|
734
|
-
[&target, this](const DhtNode& a, const DhtNode& b) {
|
|
735
|
-
return is_closer(a.id, b.id, target);
|
|
736
|
-
}
|
|
737
|
-
);
|
|
738
|
-
// Return up to 'count' closest nodes
|
|
739
|
-
if (candidates.size() > count) {
|
|
740
|
-
candidates.resize(count);
|
|
741
|
-
}
|
|
742
|
-
LOG_DHT_DEBUG("Found " << candidates.size() << " closest nodes to target " << node_id_to_hex(target));
|
|
743
|
-
for (size_t i = 0; i < candidates.size(); ++i) {
|
|
744
|
-
LOG_DHT_DEBUG(" +[" << i << "] " << node_id_to_hex(candidates[i].id) << " at " << candidates[i].peer.ip << ":" << candidates[i].peer.port);
|
|
745
|
-
}
|
|
746
|
-
*/
|
|
747
|
-
// End of debug alternative
|
|
748
|
-
|
|
749
|
-
return candidates;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
int DhtClient::get_bucket_index(const NodeId& id) {
|
|
753
|
-
NodeId distance = xor_distance(node_id_, id);
|
|
754
|
-
|
|
755
|
-
// Find the position of the most significant bit
|
|
756
|
-
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
757
|
-
if (distance[i] != 0) {
|
|
758
|
-
for (int j = 7; j >= 0; --j) {
|
|
759
|
-
if (distance[i] & (1 << j)) {
|
|
760
|
-
return static_cast<int>(i * 8 + (7 - j));
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return NODE_ID_SIZE * 8 - 1; // All bits are 0, maximum distance
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
// KRPC message handling
|
|
772
|
-
void DhtClient::handle_krpc_message(const KrpcMessage& message, const Peer& sender) {
|
|
773
|
-
LOG_DHT_DEBUG("Handling KRPC message type " << static_cast<int>(message.type) << " from " << sender.ip << ":" << sender.port);
|
|
774
|
-
|
|
775
|
-
switch (message.type) {
|
|
776
|
-
case KrpcMessageType::Query:
|
|
777
|
-
switch (message.query_type) {
|
|
778
|
-
case KrpcQueryType::Ping:
|
|
779
|
-
handle_krpc_ping(message, sender);
|
|
780
|
-
break;
|
|
781
|
-
case KrpcQueryType::FindNode:
|
|
782
|
-
handle_krpc_find_node(message, sender);
|
|
783
|
-
break;
|
|
784
|
-
case KrpcQueryType::GetPeers:
|
|
785
|
-
handle_krpc_get_peers(message, sender);
|
|
786
|
-
break;
|
|
787
|
-
case KrpcQueryType::AnnouncePeer:
|
|
788
|
-
handle_krpc_announce_peer(message, sender);
|
|
789
|
-
break;
|
|
790
|
-
}
|
|
791
|
-
break;
|
|
792
|
-
case KrpcMessageType::Response:
|
|
793
|
-
handle_krpc_response(message, sender);
|
|
794
|
-
break;
|
|
795
|
-
case KrpcMessageType::Error:
|
|
796
|
-
handle_krpc_error(message, sender);
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
void DhtClient::handle_krpc_ping(const KrpcMessage& message, const Peer& sender) {
|
|
802
|
-
LOG_DHT_DEBUG("Handling KRPC PING from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
|
|
803
|
-
|
|
804
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
805
|
-
// Spider mode: check if ignoring requests
|
|
806
|
-
if (spider_mode_.load() && spider_ignore_.load()) {
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
bool is_spider = spider_mode_.load();
|
|
810
|
-
// Only use neighbor_id for IPs we've contacted via spider (to avoid polluting other DHT clients)
|
|
811
|
-
bool use_neighbor_id = is_spider && is_spider_contacted_ip(sender.ip);
|
|
812
|
-
#else
|
|
813
|
-
bool is_spider = false;
|
|
814
|
-
bool use_neighbor_id = false;
|
|
815
|
-
#endif
|
|
816
|
-
|
|
817
|
-
// Add sender to routing table (or spider pool in spider mode)
|
|
818
|
-
KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
|
|
819
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
820
|
-
|
|
821
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
822
|
-
if (is_spider) {
|
|
823
|
-
add_spider_node(sender_node);
|
|
824
|
-
}
|
|
825
|
-
#endif
|
|
826
|
-
// Adding spider related node very undesirable but ok
|
|
827
|
-
if (!use_neighbor_id)
|
|
828
|
-
{
|
|
829
|
-
add_node(sender_node, true, false);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Respond with ping response
|
|
833
|
-
// Use neighbor_id only for spider-contacted IPs, real node_id for organic DHT traffic
|
|
834
|
-
NodeId response_id = use_neighbor_id ? neighbor_id(message.sender_id) : node_id_;
|
|
835
|
-
auto response = KrpcProtocol::create_ping_response(message.transaction_id, response_id);
|
|
836
|
-
send_krpc_message(response, sender);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
void DhtClient::handle_krpc_find_node(const KrpcMessage& message, const Peer& sender) {
|
|
840
|
-
LOG_DHT_DEBUG("Handling KRPC FIND_NODE from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
|
|
841
|
-
|
|
842
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
843
|
-
// Spider mode: check if ignoring requests
|
|
844
|
-
if (spider_mode_.load() && spider_ignore_.load()) {
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
bool is_spider = spider_mode_.load();
|
|
848
|
-
// Only use neighbor_id for IPs we've contacted via spider (to avoid polluting other DHT clients)
|
|
849
|
-
bool use_neighbor_id = is_spider && is_spider_contacted_ip(sender.ip);
|
|
850
|
-
#else
|
|
851
|
-
bool is_spider = false;
|
|
852
|
-
bool use_neighbor_id = false;
|
|
853
|
-
#endif
|
|
854
|
-
|
|
855
|
-
// Add sender to routing table (or spider pool in spider mode)
|
|
856
|
-
KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
|
|
857
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
858
|
-
|
|
859
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
860
|
-
if (is_spider) {
|
|
861
|
-
add_spider_node(sender_node);
|
|
862
|
-
}
|
|
863
|
-
#endif
|
|
864
|
-
// Adding spider related node very undesirable but ok
|
|
865
|
-
if (!use_neighbor_id)
|
|
866
|
-
{
|
|
867
|
-
add_node(sender_node, true, false);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Find closest nodes
|
|
871
|
-
auto closest_nodes = find_closest_nodes(message.target_id, K_BUCKET_SIZE);
|
|
872
|
-
auto krpc_nodes = dht_nodes_to_krpc_nodes(closest_nodes);
|
|
873
|
-
|
|
874
|
-
// Respond with closest nodes
|
|
875
|
-
// Use neighbor_id only for spider-contacted IPs, real node_id for organic DHT traffic
|
|
876
|
-
NodeId response_id = use_neighbor_id ? neighbor_id(message.target_id) : node_id_;
|
|
877
|
-
auto response = KrpcProtocol::create_find_node_response(message.transaction_id, response_id, krpc_nodes);
|
|
878
|
-
send_krpc_message(response, sender);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
void DhtClient::handle_krpc_get_peers(const KrpcMessage& message, const Peer& sender) {
|
|
882
|
-
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));
|
|
883
|
-
|
|
884
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
885
|
-
// Spider mode: check if ignoring requests
|
|
886
|
-
if (spider_mode_.load() && spider_ignore_.load()) {
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
bool is_spider = spider_mode_.load();
|
|
890
|
-
// Only use neighbor_id for IPs we've contacted via spider (to avoid polluting other DHT clients)
|
|
891
|
-
bool use_neighbor_id = is_spider && is_spider_contacted_ip(sender.ip);
|
|
892
|
-
#else
|
|
893
|
-
bool is_spider = false;
|
|
894
|
-
bool use_neighbor_id = false;
|
|
895
|
-
#endif
|
|
896
|
-
|
|
897
|
-
// Add sender to routing table (or spider pool in spider mode)
|
|
898
|
-
KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
|
|
899
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
900
|
-
|
|
901
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
902
|
-
if (is_spider) {
|
|
903
|
-
add_spider_node(sender_node);
|
|
904
|
-
}
|
|
905
|
-
#endif
|
|
906
|
-
// Adding spider related node very undesirable but ok
|
|
907
|
-
if (!use_neighbor_id)
|
|
908
|
-
{
|
|
909
|
-
add_node(sender_node, true, false);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Generate a token for this peer, bound to this info_hash (BEP 5)
|
|
913
|
-
std::string token = generate_token(sender, message.info_hash);
|
|
914
|
-
|
|
915
|
-
// Use neighbor_id only for spider-contacted IPs, real node_id for organic DHT traffic
|
|
916
|
-
NodeId response_id = use_neighbor_id ? neighbor_id(message.info_hash) : node_id_;
|
|
917
|
-
|
|
918
|
-
// First check if we have announced peers for this info_hash
|
|
919
|
-
auto announced_peers = get_announced_peers(message.info_hash);
|
|
920
|
-
|
|
921
|
-
KrpcMessage response;
|
|
922
|
-
if (!announced_peers.empty()) {
|
|
923
|
-
// Return the peers we have stored
|
|
924
|
-
response = KrpcProtocol::create_get_peers_response(message.transaction_id, response_id, announced_peers, token);
|
|
925
|
-
LOG_DHT_DEBUG("Responding to KRPC GET_PEERS with " << announced_peers.size() << " announced peers for info_hash " << node_id_to_hex(message.info_hash));
|
|
926
|
-
} else {
|
|
927
|
-
// Return closest nodes
|
|
928
|
-
auto closest_nodes = find_closest_nodes(message.info_hash, K_BUCKET_SIZE);
|
|
929
|
-
auto krpc_nodes = dht_nodes_to_krpc_nodes(closest_nodes);
|
|
930
|
-
response = KrpcProtocol::create_get_peers_response_with_nodes(message.transaction_id, response_id, krpc_nodes, token);
|
|
931
|
-
LOG_DHT_DEBUG("Responding to KRPC GET_PEERS with " << krpc_nodes.size() << " closest nodes for info_hash " << node_id_to_hex(message.info_hash));
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
send_krpc_message(response, sender);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer& sender) {
|
|
938
|
-
LOG_DHT_DEBUG("Handling KRPC ANNOUNCE_PEER from " << node_id_to_hex(message.sender_id) << " at " << sender.ip << ":" << sender.port);
|
|
939
|
-
|
|
940
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
941
|
-
bool is_spider = spider_mode_.load();
|
|
942
|
-
// Only use neighbor_id for IPs we've contacted via spider (to avoid polluting other DHT clients)
|
|
943
|
-
bool use_neighbor_id = is_spider && is_spider_contacted_ip(sender.ip);
|
|
944
|
-
|
|
945
|
-
// Spider mode: check if ignoring requests (but still process announces for callback)
|
|
946
|
-
// Note: We still want to collect announces even when ignoring other requests
|
|
947
|
-
|
|
948
|
-
// In spider mode, skip token verification for maximum collection
|
|
949
|
-
if (!is_spider && !verify_token(sender, message.info_hash, message.token)) {
|
|
950
|
-
LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
|
|
951
|
-
auto error = KrpcProtocol::create_error(message.transaction_id, KrpcErrorCode::ProtocolError, "Invalid token");
|
|
952
|
-
send_krpc_message(error, sender);
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
#else
|
|
956
|
-
// Verify token
|
|
957
|
-
if (!verify_token(sender, message.info_hash, message.token)) {
|
|
958
|
-
LOG_DHT_WARN("Invalid token from " << sender.ip << ":" << sender.port << " for KRPC ANNOUNCE_PEER");
|
|
959
|
-
auto error = KrpcProtocol::create_error(message.transaction_id, KrpcErrorCode::ProtocolError, "Invalid token");
|
|
960
|
-
send_krpc_message(error, sender);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
bool is_spider = false;
|
|
964
|
-
bool use_neighbor_id = false;
|
|
965
|
-
#endif
|
|
966
|
-
|
|
967
|
-
// Add sender to routing table (or spider pool in spider mode)
|
|
968
|
-
KrpcNode krpc_node(message.sender_id, sender.ip, sender.port);
|
|
969
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
970
|
-
|
|
971
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
972
|
-
if (is_spider) {
|
|
973
|
-
add_spider_node(sender_node);
|
|
974
|
-
}
|
|
975
|
-
#endif
|
|
976
|
-
// Adding spider related node very undesirable but ok
|
|
977
|
-
if (!use_neighbor_id)
|
|
978
|
-
{
|
|
979
|
-
add_node(sender_node, true, false);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Determine the actual port (BEP 5: implied_port support)
|
|
983
|
-
// If implied_port is set, use the UDP source port instead of the explicit port field
|
|
984
|
-
uint16_t peer_port = message.implied_port ? sender.port : message.port;
|
|
985
|
-
|
|
986
|
-
// Store the peer announcement
|
|
987
|
-
Peer announcing_peer(sender.ip, peer_port);
|
|
988
|
-
store_announced_peer(message.info_hash, announcing_peer);
|
|
989
|
-
|
|
990
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
991
|
-
// Spider mode: invoke announce callback (ensured hash - peer definitely has it)
|
|
992
|
-
if (is_spider) {
|
|
993
|
-
SpiderAnnounceCallback callback;
|
|
994
|
-
{
|
|
995
|
-
std::lock_guard<std::mutex> lock(spider_callbacks_mutex_);
|
|
996
|
-
callback = spider_announce_callback_;
|
|
997
|
-
}
|
|
998
|
-
if (callback) {
|
|
999
|
-
LOG_DHT_DEBUG("Spider: invoking announce callback for info_hash " << node_id_to_hex(message.info_hash)
|
|
1000
|
-
<< " from " << announcing_peer.ip << ":" << announcing_peer.port);
|
|
1001
|
-
callback(message.info_hash, announcing_peer);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
#endif
|
|
1005
|
-
|
|
1006
|
-
// Respond with acknowledgment
|
|
1007
|
-
// Use neighbor_id only for spider-contacted IPs, real node_id for organic DHT traffic
|
|
1008
|
-
NodeId response_id = use_neighbor_id ? neighbor_id(message.info_hash) : node_id_;
|
|
1009
|
-
auto response = KrpcProtocol::create_announce_peer_response(message.transaction_id, response_id);
|
|
1010
|
-
send_krpc_message(response, sender);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sender) {
|
|
1014
|
-
LOG_DHT_DEBUG("Handling KRPC response from " << sender.ip << ":" << sender.port);
|
|
1015
|
-
|
|
1016
|
-
// BEP 42: nodes echo our external address back in the top-level "ip" field. Responses
|
|
1017
|
-
// here are always replies to queries we sent, so the source is a node we chose to contact;
|
|
1018
|
-
// we still require a consensus of distinct responders before regenerating our node ID.
|
|
1019
|
-
if (!message.external_ip.empty()) {
|
|
1020
|
-
maybe_update_external_ip(message.external_ip, sender);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// Check if this is a ping verification response before normal processing
|
|
1024
|
-
handle_ping_verification_response(message.transaction_id, message.response_id, sender);
|
|
1025
|
-
|
|
1026
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
1027
|
-
// Check if this response is for a spider transaction
|
|
1028
|
-
// This is based on how the request was sent, NOT current spider_mode_ state
|
|
1029
|
-
// This ensures spider responses go to spider pool even if mode was toggled
|
|
1030
|
-
bool is_spider_tx = is_spider_transaction(message.transaction_id);
|
|
1031
|
-
|
|
1032
|
-
if (is_spider_tx) {
|
|
1033
|
-
// Spider transaction response: add nodes to spider pool, NOT to routing table
|
|
1034
|
-
// This keeps the routing table stable and clean
|
|
1035
|
-
KrpcNode krpc_node(message.response_id, sender.ip, sender.port);
|
|
1036
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
1037
|
-
add_spider_node(sender_node);
|
|
1038
|
-
|
|
1039
|
-
// Add discovered nodes to spider pool
|
|
1040
|
-
std::vector<DhtNode> discovered_nodes;
|
|
1041
|
-
discovered_nodes.reserve(message.nodes.size());
|
|
1042
|
-
for (const auto& node : message.nodes) {
|
|
1043
|
-
discovered_nodes.push_back(krpc_node_to_dht_node(node));
|
|
1044
|
-
}
|
|
1045
|
-
add_spider_nodes(discovered_nodes);
|
|
1046
|
-
|
|
1047
|
-
LOG_DHT_DEBUG("Spider transaction response: added " << (1 + message.nodes.size()) << " nodes to spider pool");
|
|
1048
|
-
|
|
1049
|
-
// Spider responses don't need search tracking - they're just for node discovery
|
|
1050
|
-
// Early return to avoid polluting normal DHT operations
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
#endif
|
|
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
|
-
|
|
1073
|
-
// Normal mode: add nodes to routing table
|
|
1074
|
-
KrpcNode krpc_node(message.response_id, sender.ip, sender.port);
|
|
1075
|
-
DhtNode sender_node = krpc_node_to_dht_node(krpc_node);
|
|
1076
|
-
add_node(sender_node, true, false);
|
|
1077
|
-
|
|
1078
|
-
// Add any nodes from the response (these are nodes we heard about, not confirmed)
|
|
1079
|
-
for (const auto& node : message.nodes) {
|
|
1080
|
-
DhtNode dht_node = krpc_node_to_dht_node(node);
|
|
1081
|
-
add_node(dht_node, false, false); // Not confirmed - just heard about from another node
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// Check if this is a response to a pending search (get_peers with peers)
|
|
1085
|
-
if (!message.peers.empty()) {
|
|
1086
|
-
handle_get_peers_response_for_search(message.transaction_id, sender, message.peers);
|
|
1087
|
-
}
|
|
1088
|
-
// Check if this is a response to a pending search (get_peers with nodes)
|
|
1089
|
-
else if (!message.nodes.empty()) {
|
|
1090
|
-
handle_get_peers_response_with_nodes(message.transaction_id, sender, message.nodes);
|
|
1091
|
-
}
|
|
1092
|
-
else {
|
|
1093
|
-
// Empty response (no peers, no nodes) - still need to mark as responded
|
|
1094
|
-
// This can happen when a node has no information about the info_hash
|
|
1095
|
-
handle_get_peers_empty_response(message.transaction_id, sender);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Save write token if present (needed for announce_peer after traversal completes)
|
|
1099
|
-
if (!message.token.empty()) {
|
|
1100
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1101
|
-
auto trans_it = transaction_to_search_.find(message.transaction_id);
|
|
1102
|
-
if (trans_it != transaction_to_search_.end()) {
|
|
1103
|
-
auto search_it = pending_searches_.find(trans_it->second.info_hash_hex);
|
|
1104
|
-
if (search_it != pending_searches_.end()) {
|
|
1105
|
-
save_write_token(search_it->second, trans_it->second.queried_node_id, message.token);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Clean up finished searches AFTER all response data has been processed
|
|
1111
|
-
// This ensures peers and nodes are fully handled before removing the search
|
|
1112
|
-
{
|
|
1113
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1114
|
-
auto trans_it = transaction_to_search_.find(message.transaction_id);
|
|
1115
|
-
if (trans_it != transaction_to_search_.end()) {
|
|
1116
|
-
const std::string& hash_key = trans_it->second.info_hash_hex;
|
|
1117
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
1118
|
-
if (search_it != pending_searches_.end() && search_it->second.is_finished) {
|
|
1119
|
-
LOG_DHT_DEBUG("Cleaning up finished search for info_hash " << hash_key
|
|
1120
|
-
<< " after processing transaction " << message.transaction_id);
|
|
1121
|
-
pending_searches_.erase(search_it);
|
|
1122
|
-
}
|
|
1123
|
-
// Always remove the transaction mapping after processing
|
|
1124
|
-
transaction_to_search_.erase(trans_it);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
void DhtClient::handle_krpc_error(const KrpcMessage& message, const Peer& sender) {
|
|
1130
|
-
LOG_DHT_WARN("Received KRPC error from " << sender.ip << ":" << sender.port
|
|
1131
|
-
<< " - Code: " << static_cast<int>(message.error_code)
|
|
1132
|
-
<< " Message: " << message.error_message);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// KRPC sending functions
|
|
1136
|
-
bool DhtClient::send_krpc_message(const KrpcMessage& message, const Peer& peer) {
|
|
1137
|
-
// BEP 42: stamp the requester's external address into every response we send so the
|
|
1138
|
-
// remote node can derive an IP-based node ID. Only copy the message when needed.
|
|
1139
|
-
const KrpcMessage* out = &message;
|
|
1140
|
-
KrpcMessage stamped;
|
|
1141
|
-
if (message.type == KrpcMessageType::Response && message.external_ip.empty()) {
|
|
1142
|
-
stamped = message;
|
|
1143
|
-
stamped.external_ip = peer.ip;
|
|
1144
|
-
stamped.external_port = peer.port;
|
|
1145
|
-
out = &stamped;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
auto data = KrpcProtocol::encode_message(*out);
|
|
1149
|
-
if (data.empty()) {
|
|
1150
|
-
LOG_DHT_ERROR("Failed to encode KRPC message");
|
|
1151
|
-
return false;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
LOG_DHT_DEBUG("Sending KRPC message (" << data.size() << " bytes) to " << peer.ip << ":" << peer.port);
|
|
1155
|
-
int result = send_udp_data(socket_, data, peer.ip, peer.port, address_family_);
|
|
1156
|
-
|
|
1157
|
-
if (result > 0) {
|
|
1158
|
-
LOG_DHT_DEBUG("Successfully sent KRPC message to " << peer.ip << ":" << peer.port);
|
|
1159
|
-
} else {
|
|
1160
|
-
LOG_DHT_ERROR("Failed to send KRPC message to " << peer.ip << ":" << peer.port);
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return result > 0;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
void DhtClient::send_krpc_ping(const Peer& peer) {
|
|
1167
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1168
|
-
auto message = KrpcProtocol::create_ping_query(transaction_id, node_id_);
|
|
1169
|
-
send_krpc_message(message, peer);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
void DhtClient::send_krpc_find_node(const Peer& peer, const NodeId& target) {
|
|
1173
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1174
|
-
auto message = KrpcProtocol::create_find_node_query(transaction_id, node_id_, target);
|
|
1175
|
-
// BEP 32: ask only for nodes of our own family (separate Kademlia networks)
|
|
1176
|
-
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
1177
|
-
send_krpc_message(message, peer);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
void DhtClient::send_krpc_get_peers(const Peer& peer, const InfoHash& info_hash) {
|
|
1181
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1182
|
-
auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, info_hash);
|
|
1183
|
-
// BEP 32: ask only for nodes of our own family (separate Kademlia networks)
|
|
1184
|
-
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
1185
|
-
send_krpc_message(message, peer);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
void DhtClient::send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token, bool implied_port) {
|
|
1189
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1190
|
-
auto message = KrpcProtocol::create_announce_peer_query(transaction_id, node_id_, info_hash, port, token, implied_port);
|
|
1191
|
-
send_krpc_message(message, peer);
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// Conversion utilities
|
|
1195
|
-
KrpcNode DhtClient::dht_node_to_krpc_node(const DhtNode& node) {
|
|
1196
|
-
return KrpcNode(node.id, node.peer.ip, node.peer.port);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
DhtNode DhtClient::krpc_node_to_dht_node(const KrpcNode& node) {
|
|
1200
|
-
Peer peer(node.ip, node.port);
|
|
1201
|
-
return DhtNode(node.id, peer);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
std::vector<KrpcNode> DhtClient::dht_nodes_to_krpc_nodes(const std::vector<DhtNode>& nodes) {
|
|
1205
|
-
std::vector<KrpcNode> krpc_nodes;
|
|
1206
|
-
krpc_nodes.reserve(nodes.size());
|
|
1207
|
-
for (const auto& node : nodes) {
|
|
1208
|
-
krpc_nodes.push_back(dht_node_to_krpc_node(node));
|
|
1209
|
-
}
|
|
1210
|
-
return krpc_nodes;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
std::vector<DhtNode> DhtClient::krpc_nodes_to_dht_nodes(const std::vector<KrpcNode>& nodes) {
|
|
1214
|
-
std::vector<DhtNode> dht_nodes;
|
|
1215
|
-
dht_nodes.reserve(nodes.size());
|
|
1216
|
-
for (const auto& node : nodes) {
|
|
1217
|
-
dht_nodes.push_back(krpc_node_to_dht_node(node));
|
|
1218
|
-
}
|
|
1219
|
-
return dht_nodes;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
NodeId DhtClient::generate_node_id() {
|
|
1223
|
-
NodeId id;
|
|
1224
|
-
std::random_device rd;
|
|
1225
|
-
std::mt19937 gen(rd());
|
|
1226
|
-
std::uniform_int_distribution<> dis(0, 255);
|
|
1227
|
-
|
|
1228
|
-
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
1229
|
-
id[i] = dis(gen);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return id;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// ============================================================================
|
|
1236
|
-
// BEP 42: deriving the node ID from the external IP address
|
|
1237
|
-
// ============================================================================
|
|
1238
|
-
namespace {
|
|
1239
|
-
|
|
1240
|
-
// CRC-32C (Castagnoli) — the polynomial mandated by BEP 42. Standard parameters:
|
|
1241
|
-
// reflected, init 0xFFFFFFFF, final XOR 0xFFFFFFFF. Bytes are processed in array order,
|
|
1242
|
-
// matching the BEP 42 reference implementation and its published test vectors.
|
|
1243
|
-
uint32_t crc32c(const uint8_t* data, size_t len) {
|
|
1244
|
-
uint32_t crc = 0xFFFFFFFFu;
|
|
1245
|
-
for (size_t i = 0; i < len; ++i) {
|
|
1246
|
-
crc ^= data[i];
|
|
1247
|
-
for (int k = 0; k < 8; ++k) {
|
|
1248
|
-
crc = (crc & 1u) ? (crc >> 1) ^ 0x82F63B78u : (crc >> 1);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return crc ^ 0xFFFFFFFFu;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Mask the leading octets of an IP per BEP 42 (4 octets for IPv4, 8 for IPv6).
|
|
1255
|
-
// Returns the number of octets written to `out`, or 0 on a parse failure.
|
|
1256
|
-
int masked_octets(const std::string& ip, uint8_t out[8]) {
|
|
1257
|
-
static const uint8_t v4_mask[4] = { 0x03, 0x0f, 0x3f, 0xff };
|
|
1258
|
-
static const uint8_t v6_mask[8] = { 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff };
|
|
1259
|
-
|
|
1260
|
-
if (network_utils::is_valid_ipv6(ip)) {
|
|
1261
|
-
struct in6_addr addr6;
|
|
1262
|
-
if (inet_pton(AF_INET6, ip.c_str(), &addr6) != 1) return 0;
|
|
1263
|
-
for (int i = 0; i < 8; ++i) out[i] = addr6.s6_addr[i] & v6_mask[i];
|
|
1264
|
-
return 8;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
struct in_addr addr;
|
|
1268
|
-
if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) return 0;
|
|
1269
|
-
uint32_t h = ntohl(addr.s_addr);
|
|
1270
|
-
uint8_t ipb[4] = {
|
|
1271
|
-
static_cast<uint8_t>((h >> 24) & 0xff), static_cast<uint8_t>((h >> 16) & 0xff),
|
|
1272
|
-
static_cast<uint8_t>((h >> 8) & 0xff), static_cast<uint8_t>(h & 0xff)
|
|
1273
|
-
};
|
|
1274
|
-
for (int i = 0; i < 4; ++i) out[i] = ipb[i] & v4_mask[i];
|
|
1275
|
-
return 4;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// Compute the 3 deterministic prefix bytes of a BEP 42 node ID for (ip, seed).
|
|
1279
|
-
// Only the high 5 bits of prefix[2] are deterministic (the low 3 bits are random in a real ID).
|
|
1280
|
-
bool bep42_prefix(const std::string& ip, uint8_t seed, uint8_t prefix[3]) {
|
|
1281
|
-
uint8_t octets[8];
|
|
1282
|
-
int num = masked_octets(ip, octets);
|
|
1283
|
-
if (num == 0) return false;
|
|
1284
|
-
octets[0] |= static_cast<uint8_t>((seed & 0x7) << 5);
|
|
1285
|
-
uint32_t c = crc32c(octets, static_cast<size_t>(num));
|
|
1286
|
-
prefix[0] = static_cast<uint8_t>((c >> 24) & 0xff);
|
|
1287
|
-
prefix[1] = static_cast<uint8_t>((c >> 16) & 0xff);
|
|
1288
|
-
prefix[2] = static_cast<uint8_t>((c >> 8) & 0xf8);
|
|
1289
|
-
return true;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
} // namespace
|
|
1293
|
-
|
|
1294
|
-
bool DhtClient::is_public_address(const std::string& ip) {
|
|
1295
|
-
// Shared with automatic port forwarding; the canonical classifier lives in
|
|
1296
|
-
// network_utils so both subsystems agree on what "publicly routable" means.
|
|
1297
|
-
return network_utils::is_public_ip(ip);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
bool DhtClient::generate_node_id_from_ip(const std::string& ip, NodeId& out, std::mt19937& gen) {
|
|
1301
|
-
uint8_t prefix[3];
|
|
1302
|
-
std::uniform_int_distribution<int> dis(0, 255);
|
|
1303
|
-
uint8_t seed = static_cast<uint8_t>(dis(gen));
|
|
1304
|
-
if (!bep42_prefix(ip, seed, prefix)) return false;
|
|
1305
|
-
|
|
1306
|
-
out[0] = prefix[0];
|
|
1307
|
-
out[1] = prefix[1];
|
|
1308
|
-
out[2] = static_cast<uint8_t>(prefix[2] | (dis(gen) & 0x7)); // low 3 bits are random
|
|
1309
|
-
for (int i = 3; i < 19; ++i) out[i] = static_cast<uint8_t>(dis(gen));
|
|
1310
|
-
out[19] = seed;
|
|
1311
|
-
return true;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
bool DhtClient::verify_node_id_for_ip(const NodeId& id, const std::string& ip) {
|
|
1315
|
-
// Local/private addresses cannot be verified (their IDs would be "wrong" anyway).
|
|
1316
|
-
if (!is_public_address(ip)) return true;
|
|
1317
|
-
uint8_t prefix[3];
|
|
1318
|
-
if (!bep42_prefix(ip, id[19], prefix)) return true; // unparseable -> don't reject
|
|
1319
|
-
return id[0] == prefix[0]
|
|
1320
|
-
&& id[1] == prefix[1]
|
|
1321
|
-
&& (id[2] & 0xf8) == prefix[2];
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
void DhtClient::rebuild_routing_table_unlocked() {
|
|
1325
|
-
// node_id_ changed; every node's bucket index is now stale. Re-bucket all of them.
|
|
1326
|
-
std::vector<DhtNode> all;
|
|
1327
|
-
for (auto& bucket : routing_table_) {
|
|
1328
|
-
for (auto& node : bucket) all.push_back(std::move(node));
|
|
1329
|
-
bucket.clear();
|
|
1330
|
-
}
|
|
1331
|
-
for (auto& node : all) {
|
|
1332
|
-
int bucket_index = get_bucket_index(node.id);
|
|
1333
|
-
if (bucket_index < 0 || bucket_index >= static_cast<int>(routing_table_.size())) continue;
|
|
1334
|
-
auto& bucket = routing_table_[bucket_index];
|
|
1335
|
-
if (bucket.size() < K_BUCKET_SIZE) bucket.push_back(std::move(node));
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
void DhtClient::set_external_ip(const std::string& ip) {
|
|
1340
|
-
// Only public addresses of this instance's family can derive a valid node ID.
|
|
1341
|
-
if (!is_public_address(ip)) return;
|
|
1342
|
-
if (network_utils::is_valid_ipv6(ip) != is_ipv6()) return;
|
|
1343
|
-
|
|
1344
|
-
{
|
|
1345
|
-
std::lock_guard<std::mutex> ext_lock(external_ip_mutex_);
|
|
1346
|
-
if (ip == external_address_) return; // already using this address
|
|
1347
|
-
external_address_ = ip;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
1351
|
-
// If our current ID already matches this IP, keep it (avoids needless churn).
|
|
1352
|
-
if (verify_node_id_for_ip(node_id_, ip)) {
|
|
1353
|
-
LOG_DHT_DEBUG("External IP " << ip << " already matches current node ID (BEP 42)");
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
std::random_device rd;
|
|
1358
|
-
std::mt19937 gen(rd());
|
|
1359
|
-
NodeId new_id;
|
|
1360
|
-
if (!generate_node_id_from_ip(ip, new_id, gen)) return;
|
|
1361
|
-
|
|
1362
|
-
NodeId old_id = node_id_;
|
|
1363
|
-
node_id_ = new_id;
|
|
1364
|
-
rebuild_routing_table_unlocked();
|
|
1365
|
-
LOG_DHT_INFO("Regenerated DHT node ID from external IP " << ip << " (BEP 42): "
|
|
1366
|
-
<< node_id_to_hex(old_id) << " -> " << node_id_to_hex(new_id));
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
std::string DhtClient::get_external_address() const {
|
|
1370
|
-
std::lock_guard<std::mutex> lock(external_ip_mutex_);
|
|
1371
|
-
return external_address_;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
void DhtClient::maybe_update_external_ip(const std::string& reported_ip, const Peer& responder) {
|
|
1375
|
-
// Only consider public addresses of our own family.
|
|
1376
|
-
if (!is_public_address(reported_ip)) return;
|
|
1377
|
-
if (network_utils::is_valid_ipv6(reported_ip) != is_ipv6()) return;
|
|
1378
|
-
|
|
1379
|
-
std::string winner;
|
|
1380
|
-
{
|
|
1381
|
-
std::lock_guard<std::mutex> lock(external_ip_mutex_);
|
|
1382
|
-
if (reported_ip == external_address_) return; // already adopted
|
|
1383
|
-
|
|
1384
|
-
// One vote per distinct responder so a single peer cannot force a change.
|
|
1385
|
-
if (external_ip_voters_.size() >= MAX_EXTERNAL_IP_VOTERS) {
|
|
1386
|
-
external_ip_voters_.clear();
|
|
1387
|
-
external_ip_votes_.clear();
|
|
1388
|
-
}
|
|
1389
|
-
if (!external_ip_voters_.insert(responder.ip).second) return; // this responder already voted
|
|
1390
|
-
|
|
1391
|
-
int votes = ++external_ip_votes_[reported_ip];
|
|
1392
|
-
// A vote for an address other than our current one is genuinely interesting
|
|
1393
|
-
// (startup ramp-up, or a possible address change), so surface it at INFO.
|
|
1394
|
-
LOG_DHT_INFO("External IP vote from " << responder.ip << " -> " << reported_ip
|
|
1395
|
-
<< " (" << votes << "/" << EXTERNAL_IP_VOTE_THRESHOLD << ")");
|
|
1396
|
-
if (votes >= EXTERNAL_IP_VOTE_THRESHOLD) {
|
|
1397
|
-
winner = reported_ip;
|
|
1398
|
-
external_ip_votes_.clear();
|
|
1399
|
-
external_ip_voters_.clear();
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (!winner.empty()) {
|
|
1404
|
-
LOG_DHT_INFO("External IP consensus reached: " << winner
|
|
1405
|
-
<< " (>= " << EXTERNAL_IP_VOTE_THRESHOLD << " responders agree)");
|
|
1406
|
-
set_external_ip(winner);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
NodeId DhtClient::xor_distance(const NodeId& a, const NodeId& b) {
|
|
1411
|
-
NodeId result;
|
|
1412
|
-
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
1413
|
-
result[i] = a[i] ^ b[i];
|
|
1414
|
-
}
|
|
1415
|
-
return result;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
bool DhtClient::is_closer(const NodeId& a, const NodeId& b, const NodeId& target) {
|
|
1419
|
-
NodeId dist_a = xor_distance(a, target);
|
|
1420
|
-
NodeId dist_b = xor_distance(b, target);
|
|
1421
|
-
|
|
1422
|
-
return std::lexicographical_compare(dist_a.begin(), dist_a.end(),
|
|
1423
|
-
dist_b.begin(), dist_b.end());
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
NodeId DhtClient::neighbor_id(const NodeId& target) const {
|
|
1427
|
-
// Spider mode optimization from legacy implementation:
|
|
1428
|
-
// Combine first 10 bytes of target with last 10 bytes of our node ID
|
|
1429
|
-
// This makes us appear "close" to any target in the DHT keyspace,
|
|
1430
|
-
// causing other nodes to route more queries to us
|
|
1431
|
-
NodeId result;
|
|
1432
|
-
std::copy(target.begin(), target.begin() + 10, result.begin());
|
|
1433
|
-
std::copy(node_id_.begin() + 10, node_id_.end(), result.begin() + 10);
|
|
1434
|
-
return result;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
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");
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
|
|
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;
|
|
1470
|
-
}
|
|
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_);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
void DhtClient::cleanup_stale_nodes() {
|
|
1479
|
-
std::lock_guard<std::mutex> routing_lock(routing_table_mutex_);
|
|
1480
|
-
|
|
1481
|
-
auto now = std::chrono::steady_clock::now();
|
|
1482
|
-
auto stale_threshold = std::chrono::minutes(15);
|
|
1483
|
-
constexpr uint8_t MAX_FAIL_COUNT = 3; // Remove after 3 consecutive failures
|
|
1484
|
-
|
|
1485
|
-
size_t total_removed = 0;
|
|
1486
|
-
|
|
1487
|
-
for (auto& bucket : routing_table_) {
|
|
1488
|
-
auto old_size = bucket.size();
|
|
1489
|
-
|
|
1490
|
-
bucket.erase(std::remove_if(bucket.begin(), bucket.end(),
|
|
1491
|
-
[now, stale_threshold, MAX_FAIL_COUNT](const DhtNode& node) {
|
|
1492
|
-
// Remove if too many failures
|
|
1493
|
-
if (node.pinged() && node.fail_count >= MAX_FAIL_COUNT) {
|
|
1494
|
-
LOG_DHT_DEBUG("Removing failed node " << node_id_to_hex(node.id)
|
|
1495
|
-
<< " (fail_count=" << static_cast<int>(node.fail_count) << ")");
|
|
1496
|
-
return true;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// Remove if never responded and too old
|
|
1500
|
-
if (!node.pinged() && now - node.last_seen > stale_threshold) {
|
|
1501
|
-
LOG_DHT_DEBUG("Removing unresponsive node " << node_id_to_hex(node.id)
|
|
1502
|
-
<< " (never responded, age > 15min)");
|
|
1503
|
-
return true;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
return false;
|
|
1507
|
-
}), bucket.end());
|
|
1508
|
-
|
|
1509
|
-
total_removed += (old_size - bucket.size());
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
if (total_removed > 0) {
|
|
1513
|
-
LOG_DHT_DEBUG("Cleaned up " << total_removed << " stale/failed nodes from routing table");
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
void DhtClient::print_statistics() {
|
|
1518
|
-
auto now = std::chrono::steady_clock::now();
|
|
1519
|
-
|
|
1520
|
-
// Routing table statistics
|
|
1521
|
-
size_t filled_buckets = 0;
|
|
1522
|
-
size_t total_nodes = 0;
|
|
1523
|
-
size_t max_bucket_size = 0;
|
|
1524
|
-
size_t confirmed_nodes = 0;
|
|
1525
|
-
size_t unpinged_nodes = 0;
|
|
1526
|
-
size_t failed_nodes = 0;
|
|
1527
|
-
|
|
1528
|
-
// Collect all nodes for best/worst analysis
|
|
1529
|
-
std::vector<std::pair<DhtNode, int>> all_nodes; // node + bucket index
|
|
1530
|
-
|
|
1531
|
-
{
|
|
1532
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
1533
|
-
for (size_t bucket_idx = 0; bucket_idx < routing_table_.size(); ++bucket_idx) {
|
|
1534
|
-
const auto& bucket = routing_table_[bucket_idx];
|
|
1535
|
-
if (!bucket.empty()) {
|
|
1536
|
-
filled_buckets++;
|
|
1537
|
-
total_nodes += bucket.size();
|
|
1538
|
-
max_bucket_size = (std::max)(max_bucket_size, bucket.size());
|
|
1539
|
-
|
|
1540
|
-
for (const auto& node : bucket) {
|
|
1541
|
-
all_nodes.emplace_back(node, static_cast<int>(bucket_idx));
|
|
1542
|
-
|
|
1543
|
-
if (node.confirmed()) {
|
|
1544
|
-
confirmed_nodes++;
|
|
1545
|
-
} else if (!node.pinged()) {
|
|
1546
|
-
unpinged_nodes++;
|
|
1547
|
-
} else if (node.fail_count > 0) {
|
|
1548
|
-
failed_nodes++;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// Pending searches statistics
|
|
1556
|
-
size_t pending_searches = 0;
|
|
1557
|
-
size_t pending_announces = 0;
|
|
1558
|
-
size_t total_search_nodes = 0;
|
|
1559
|
-
size_t total_found_peers = 0;
|
|
1560
|
-
size_t total_write_tokens = 0;
|
|
1561
|
-
size_t active_transactions = 0;
|
|
1562
|
-
{
|
|
1563
|
-
std::lock_guard<std::mutex> search_lock(pending_searches_mutex_);
|
|
1564
|
-
pending_searches = pending_searches_.size();
|
|
1565
|
-
active_transactions = transaction_to_search_.size();
|
|
1566
|
-
for (const auto& [hash, search] : pending_searches_) {
|
|
1567
|
-
total_search_nodes += search.search_nodes.size();
|
|
1568
|
-
total_found_peers += search.found_peers.size();
|
|
1569
|
-
total_write_tokens += search.write_tokens.size();
|
|
1570
|
-
if (search.is_announce) {
|
|
1571
|
-
pending_announces++;
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
// Announced peers statistics
|
|
1577
|
-
size_t announced_peers_total = 0;
|
|
1578
|
-
size_t announced_peers_infohashes = 0;
|
|
1579
|
-
{
|
|
1580
|
-
std::lock_guard<std::mutex> peers_lock(announced_peers_mutex_);
|
|
1581
|
-
announced_peers_infohashes = announced_peers_.size();
|
|
1582
|
-
for (const auto& entry : announced_peers_) {
|
|
1583
|
-
announced_peers_total += entry.second.size();
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// Ping verification statistics
|
|
1588
|
-
size_t pending_pings = 0;
|
|
1589
|
-
size_t nodes_being_replaced = 0;
|
|
1590
|
-
{
|
|
1591
|
-
std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
|
|
1592
|
-
pending_pings = pending_pings_.size();
|
|
1593
|
-
nodes_being_replaced = nodes_being_replaced_.size();
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// Spider mode statistics
|
|
1597
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
1598
|
-
size_t spider_pool_size = 0;
|
|
1599
|
-
size_t spider_visited_count = 0;
|
|
1600
|
-
size_t spider_pending_transactions = 0;
|
|
1601
|
-
size_t spider_contacted_ips_count = 0;
|
|
1602
|
-
bool is_spider_active = spider_mode_.load();
|
|
1603
|
-
{
|
|
1604
|
-
std::lock_guard<std::mutex> spider_lock(spider_nodes_mutex_);
|
|
1605
|
-
spider_pool_size = spider_nodes_.size();
|
|
1606
|
-
spider_visited_count = spider_visited_.size();
|
|
1607
|
-
spider_pending_transactions = spider_transactions_.size();
|
|
1608
|
-
spider_contacted_ips_count = spider_contacted_ips_.size();
|
|
1609
|
-
}
|
|
1610
|
-
#endif
|
|
1611
|
-
|
|
1612
|
-
// Print main statistics
|
|
1613
|
-
LOG_DHT_INFO("=== DHT GLOBAL STATISTICS ===");
|
|
1614
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
1615
|
-
if (is_spider_active || spider_pending_transactions > 0 || spider_contacted_ips_count > 0) {
|
|
1616
|
-
LOG_DHT_INFO("[SPIDER MODE " << (is_spider_active ? "ACTIVE" : "INACTIVE") << "]");
|
|
1617
|
-
LOG_DHT_INFO(" Spider pool size: " << spider_pool_size << "/" << MAX_SPIDER_NODES);
|
|
1618
|
-
LOG_DHT_INFO(" Visited nodes: " << spider_visited_count << "/" << MAX_SPIDER_VISITED);
|
|
1619
|
-
LOG_DHT_INFO(" Contacted IPs: " << spider_contacted_ips_count << "/" << MAX_SPIDER_CONTACTED_IPS);
|
|
1620
|
-
LOG_DHT_INFO(" Pending transactions: " << spider_pending_transactions);
|
|
1621
|
-
if (is_spider_active) {
|
|
1622
|
-
LOG_DHT_INFO(" Ignore mode: " << (spider_ignore_.load() ? "ON" : "OFF"));
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
#endif
|
|
1626
|
-
LOG_DHT_INFO("[ROUTING TABLE]");
|
|
1627
|
-
LOG_DHT_INFO(" Total nodes: " << total_nodes << " (confirmed: " << confirmed_nodes
|
|
1628
|
-
<< ", unpinged: " << unpinged_nodes << ", failed: " << failed_nodes << ")");
|
|
1629
|
-
LOG_DHT_INFO(" Filled buckets: " << filled_buckets << "/" << routing_table_.size()
|
|
1630
|
-
<< ", Max bucket size: " << max_bucket_size << "/" << K_BUCKET_SIZE);
|
|
1631
|
-
LOG_DHT_INFO("[ACTIVE OPERATIONS]");
|
|
1632
|
-
LOG_DHT_INFO(" Pending searches: " << pending_searches
|
|
1633
|
-
<< " (announces: " << pending_announces
|
|
1634
|
-
<< ", nodes: " << total_search_nodes
|
|
1635
|
-
<< ", peers: " << total_found_peers
|
|
1636
|
-
<< ", tokens: " << total_write_tokens << ")");
|
|
1637
|
-
LOG_DHT_INFO(" Active transactions: " << active_transactions);
|
|
1638
|
-
LOG_DHT_INFO(" Pending ping verifications: " << pending_pings
|
|
1639
|
-
<< " (nodes being replaced: " << nodes_being_replaced << ")");
|
|
1640
|
-
LOG_DHT_INFO("[STORED DATA]");
|
|
1641
|
-
LOG_DHT_INFO(" Announced peers: " << announced_peers_total
|
|
1642
|
-
<< " across " << announced_peers_infohashes << " infohashes");
|
|
1643
|
-
|
|
1644
|
-
// Best/Worst nodes analysis
|
|
1645
|
-
if (!all_nodes.empty()) {
|
|
1646
|
-
// Sort by quality: confirmed first, then by RTT (lower is better)
|
|
1647
|
-
std::sort(all_nodes.begin(), all_nodes.end(),
|
|
1648
|
-
[](const std::pair<DhtNode, int>& a, const std::pair<DhtNode, int>& b) {
|
|
1649
|
-
// Confirmed nodes are better
|
|
1650
|
-
if (a.first.confirmed() != b.first.confirmed()) {
|
|
1651
|
-
return a.first.confirmed();
|
|
1652
|
-
}
|
|
1653
|
-
// Lower fail_count is better
|
|
1654
|
-
if (a.first.fail_count != b.first.fail_count) {
|
|
1655
|
-
return a.first.fail_count < b.first.fail_count;
|
|
1656
|
-
}
|
|
1657
|
-
// Lower RTT is better (0xffff = unknown, treat as worst)
|
|
1658
|
-
return a.first.rtt < b.first.rtt;
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
// Calculate RTT statistics (excluding unknown)
|
|
1662
|
-
uint32_t rtt_sum = 0;
|
|
1663
|
-
uint16_t rtt_min = 0xffff;
|
|
1664
|
-
uint16_t rtt_max = 0;
|
|
1665
|
-
size_t rtt_count = 0;
|
|
1666
|
-
for (const auto& [node, bucket_idx] : all_nodes) {
|
|
1667
|
-
if (node.rtt != 0xffff) {
|
|
1668
|
-
rtt_sum += node.rtt;
|
|
1669
|
-
rtt_min = (std::min)(rtt_min, node.rtt);
|
|
1670
|
-
rtt_max = (std::max)(rtt_max, node.rtt);
|
|
1671
|
-
rtt_count++;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
LOG_DHT_INFO("[RTT STATISTICS]");
|
|
1676
|
-
if (rtt_count > 0) {
|
|
1677
|
-
LOG_DHT_INFO(" Known RTT nodes: " << rtt_count << "/" << total_nodes);
|
|
1678
|
-
LOG_DHT_INFO(" RTT min/avg/max: " << rtt_min << "ms / "
|
|
1679
|
-
<< (rtt_sum / rtt_count) << "ms / " << rtt_max << "ms");
|
|
1680
|
-
} else {
|
|
1681
|
-
LOG_DHT_INFO(" No RTT data available");
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
// Show top 5 best nodes
|
|
1685
|
-
LOG_DHT_INFO("[TOP 5 BEST NODES]");
|
|
1686
|
-
size_t best_count = (std::min)(size_t(5), all_nodes.size());
|
|
1687
|
-
for (size_t i = 0; i < best_count; ++i) {
|
|
1688
|
-
const auto& [node, bucket_idx] = all_nodes[i];
|
|
1689
|
-
auto age_seconds = std::chrono::duration_cast<std::chrono::seconds>(now - node.last_seen).count();
|
|
1690
|
-
std::string status = node.confirmed() ? "confirmed" :
|
|
1691
|
-
(node.pinged() ? "pinged" : "unpinged");
|
|
1692
|
-
std::string rtt_str = (node.rtt == 0xffff) ? "N/A" : std::to_string(node.rtt) + "ms";
|
|
1693
|
-
|
|
1694
|
-
LOG_DHT_INFO(" #" << (i + 1) << " " << node.peer.ip << ":" << node.peer.port
|
|
1695
|
-
<< " | bucket:" << bucket_idx << " rtt:" << rtt_str
|
|
1696
|
-
<< " fails:" << static_cast<int>(node.fail_count)
|
|
1697
|
-
<< " " << status << " age:" << age_seconds << "s");
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// Show top 5 worst nodes
|
|
1701
|
-
LOG_DHT_INFO("[TOP 5 WORST NODES]");
|
|
1702
|
-
size_t worst_start = all_nodes.size() > 5 ? all_nodes.size() - 5 : 0;
|
|
1703
|
-
for (size_t i = all_nodes.size(); i > worst_start; --i) {
|
|
1704
|
-
const auto& [node, bucket_idx] = all_nodes[i - 1];
|
|
1705
|
-
auto age_seconds = std::chrono::duration_cast<std::chrono::seconds>(now - node.last_seen).count();
|
|
1706
|
-
std::string status = node.confirmed() ? "confirmed" :
|
|
1707
|
-
(node.pinged() ? "pinged" : "unpinged");
|
|
1708
|
-
std::string rtt_str = (node.rtt == 0xffff) ? "N/A" : std::to_string(node.rtt) + "ms";
|
|
1709
|
-
|
|
1710
|
-
LOG_DHT_INFO(" #" << (all_nodes.size() - i + 1) << " " << node.peer.ip << ":" << node.peer.port
|
|
1711
|
-
<< " | bucket:" << bucket_idx << " rtt:" << rtt_str
|
|
1712
|
-
<< " fails:" << static_cast<int>(node.fail_count)
|
|
1713
|
-
<< " " << status << " age:" << age_seconds << "s");
|
|
1714
|
-
}
|
|
1715
|
-
} else {
|
|
1716
|
-
LOG_DHT_INFO(" No nodes in routing table");
|
|
1717
|
-
}
|
|
1718
|
-
LOG_DHT_INFO("=== END DHT STATISTICS ===");
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
void DhtClient::refresh_buckets() {
|
|
1722
|
-
// Find random nodes in each bucket to refresh
|
|
1723
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
1724
|
-
|
|
1725
|
-
for (size_t i = 0; i < routing_table_.size(); ++i) {
|
|
1726
|
-
if (routing_table_[i].empty()) {
|
|
1727
|
-
// Generate a random node ID in this bucket's range
|
|
1728
|
-
NodeId random_id = generate_node_id();
|
|
1729
|
-
|
|
1730
|
-
// Set the appropriate bits to place it in bucket i
|
|
1731
|
-
int byte_index = static_cast<int>(i / 8);
|
|
1732
|
-
int bit_index = static_cast<int>(i % 8);
|
|
1733
|
-
|
|
1734
|
-
if (static_cast<size_t>(byte_index) < NODE_ID_SIZE) {
|
|
1735
|
-
// Clear the target bit and higher bits
|
|
1736
|
-
for (size_t j = static_cast<size_t>(byte_index); j < NODE_ID_SIZE; ++j) {
|
|
1737
|
-
random_id[j] = node_id_[j];
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
// Set the target bit
|
|
1741
|
-
random_id[byte_index] |= (1 << (7 - bit_index));
|
|
1742
|
-
|
|
1743
|
-
// Find nodes to query
|
|
1744
|
-
auto closest_nodes = find_closest_nodes_unlocked(random_id, ALPHA);
|
|
1745
|
-
for (const auto& node : closest_nodes) {
|
|
1746
|
-
send_krpc_find_node(node.peer, random_id);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
void DhtClient::cleanup_stale_searches() {
|
|
1754
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1755
|
-
|
|
1756
|
-
auto now = std::chrono::steady_clock::now();
|
|
1757
|
-
auto stale_threshold = std::chrono::minutes(5); // Remove searches older than 5 minutes
|
|
1758
|
-
|
|
1759
|
-
// Clean up stale searches (by info_hash)
|
|
1760
|
-
auto search_it = pending_searches_.begin();
|
|
1761
|
-
while (search_it != pending_searches_.end()) {
|
|
1762
|
-
if (now - search_it->second.created_at > stale_threshold) {
|
|
1763
|
-
LOG_DHT_DEBUG("Removing stale pending search for info_hash " << search_it->first);
|
|
1764
|
-
search_it = pending_searches_.erase(search_it);
|
|
1765
|
-
} else {
|
|
1766
|
-
++search_it;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// Clean up stale transaction mappings (remove ones that point to non-existent searches)
|
|
1771
|
-
auto trans_it = transaction_to_search_.begin();
|
|
1772
|
-
while (trans_it != transaction_to_search_.end()) {
|
|
1773
|
-
if (pending_searches_.find(trans_it->second.info_hash_hex) == pending_searches_.end()) {
|
|
1774
|
-
LOG_DHT_DEBUG("Removing stale transaction mapping " << trans_it->first << " -> " << trans_it->second.info_hash_hex);
|
|
1775
|
-
trans_it = transaction_to_search_.erase(trans_it);
|
|
1776
|
-
} else {
|
|
1777
|
-
++trans_it;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
void DhtClient::cleanup_search_node_states() {
|
|
1783
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1784
|
-
constexpr size_t MAX_NODE_STATES = 200; // Twice bigger than MAX_SEARCH_NODES
|
|
1785
|
-
|
|
1786
|
-
for (auto& [hash_key, search] : pending_searches_) {
|
|
1787
|
-
if (search.is_finished) {
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// Skip cleanup if node_states is within acceptable limits
|
|
1792
|
-
if (search.node_states.size() <= MAX_NODE_STATES) {
|
|
1793
|
-
continue;
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
// Erase while iterating using iterator-based loop
|
|
1797
|
-
size_t removed = 0;
|
|
1798
|
-
auto it = search.node_states.begin();
|
|
1799
|
-
while (it != search.node_states.end()) {
|
|
1800
|
-
// В cleanup_search_node_states:
|
|
1801
|
-
if ((it->second & SearchNodeFlags::ABANDONED) &&
|
|
1802
|
-
((it->second & (SearchNodeFlags::TIMED_OUT | SearchNodeFlags::RESPONDED)) ||
|
|
1803
|
-
!(it->second & SearchNodeFlags::QUERIED))) { // Never queried = safe to remove immediately
|
|
1804
|
-
it = search.node_states.erase(it);
|
|
1805
|
-
removed++;
|
|
1806
|
-
} else {
|
|
1807
|
-
++it;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
if (removed > 0) {
|
|
1812
|
-
LOG_DHT_DEBUG("Cleaned up " << removed << " abandoned nodes for search " << hash_key
|
|
1813
|
-
<< " (remaining: " << search.node_states.size() << ")");
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
void DhtClient::cleanup_timed_out_search_requests() {
|
|
1819
|
-
std::vector<DeferredCallbacks> all_deferred;
|
|
1820
|
-
|
|
1821
|
-
{
|
|
1822
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1823
|
-
|
|
1824
|
-
if (pending_searches_.empty()) {
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
auto now = std::chrono::steady_clock::now();
|
|
1829
|
-
// - Short timeout (2s): Free up the slot by increasing branch_factor, but keep waiting for late response
|
|
1830
|
-
// - Full timeout (15s): Mark node as failed and remove the transaction
|
|
1831
|
-
auto short_timeout_threshold = std::chrono::seconds(2);
|
|
1832
|
-
auto full_timeout_threshold = std::chrono::seconds(15);
|
|
1833
|
-
|
|
1834
|
-
// Collect transactions that need short timeout or full timeout
|
|
1835
|
-
std::vector<std::string> short_timeout_transactions;
|
|
1836
|
-
std::vector<std::string> full_timeout_transactions;
|
|
1837
|
-
|
|
1838
|
-
for (const auto& [transaction_id, trans_info] : transaction_to_search_) {
|
|
1839
|
-
auto elapsed = now - trans_info.sent_at;
|
|
1840
|
-
|
|
1841
|
-
if (elapsed > full_timeout_threshold) {
|
|
1842
|
-
full_timeout_transactions.push_back(transaction_id);
|
|
1843
|
-
} else if (elapsed > short_timeout_threshold) {
|
|
1844
|
-
// Check if this node already has short timeout
|
|
1845
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
1846
|
-
if (search_it != pending_searches_.end()) {
|
|
1847
|
-
auto& search = search_it->second;
|
|
1848
|
-
// Only process if not already marked with short timeout
|
|
1849
|
-
auto state_it = search.node_states.find(trans_info.queried_node_id);
|
|
1850
|
-
if (state_it == search.node_states.end() || !(state_it->second & SearchNodeFlags::SHORT_TIMEOUT)) {
|
|
1851
|
-
short_timeout_transactions.push_back(transaction_id);
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
// Group by search to batch process and call add_search_requests once per search
|
|
1858
|
-
std::unordered_set<std::string> affected_searches;
|
|
1859
|
-
|
|
1860
|
-
// Process short timeouts first - these nodes are slow but we still wait for a response
|
|
1861
|
-
for (const auto& transaction_id : short_timeout_transactions) {
|
|
1862
|
-
auto trans_it = transaction_to_search_.find(transaction_id);
|
|
1863
|
-
if (trans_it == transaction_to_search_.end()) {
|
|
1864
|
-
continue;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
const auto& trans_info = trans_it->second;
|
|
1868
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
1869
|
-
|
|
1870
|
-
if (search_it != pending_searches_.end()) {
|
|
1871
|
-
auto& search = search_it->second;
|
|
1872
|
-
|
|
1873
|
-
if (!search.is_finished) {
|
|
1874
|
-
// Check if this node was abandoned during truncation
|
|
1875
|
-
auto state_it = search.node_states.find(trans_info.queried_node_id);
|
|
1876
|
-
if (state_it != search.node_states.end() &&
|
|
1877
|
-
(state_it->second & SearchNodeFlags::ABANDONED)) {
|
|
1878
|
-
// Node was abandoned, skip short timeout processing
|
|
1879
|
-
continue;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// Mark node with short timeout (add flag, preserving existing flags)
|
|
1883
|
-
search.node_states[trans_info.queried_node_id] |= SearchNodeFlags::SHORT_TIMEOUT;
|
|
1884
|
-
|
|
1885
|
-
// Increase branch factor to allow another request (opening up a slot)
|
|
1886
|
-
search.branch_factor++;
|
|
1887
|
-
|
|
1888
|
-
LOG_DHT_DEBUG("Short timeout for node " << node_id_to_hex(trans_info.queried_node_id)
|
|
1889
|
-
<< " in search " << trans_info.info_hash_hex
|
|
1890
|
-
<< " - increased branch_factor to " << search.branch_factor
|
|
1891
|
-
<< " (still waiting for late response)");
|
|
1892
|
-
|
|
1893
|
-
affected_searches.insert(trans_info.info_hash_hex);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
// Note: We DON'T remove the transaction - we're still waiting for a possible late response
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// Process full timeouts - these nodes have completely failed
|
|
1900
|
-
for (const auto& transaction_id : full_timeout_transactions) {
|
|
1901
|
-
auto trans_it = transaction_to_search_.find(transaction_id);
|
|
1902
|
-
if (trans_it == transaction_to_search_.end()) {
|
|
1903
|
-
continue;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
const auto& trans_info = trans_it->second;
|
|
1907
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
1908
|
-
|
|
1909
|
-
if (search_it != pending_searches_.end()) {
|
|
1910
|
-
auto& search = search_it->second;
|
|
1911
|
-
|
|
1912
|
-
if (!search.is_finished) {
|
|
1913
|
-
// Get current flags for this node
|
|
1914
|
-
uint8_t& flags = search.node_states[trans_info.queried_node_id];
|
|
1915
|
-
|
|
1916
|
-
// Check if this node was abandoned during truncation
|
|
1917
|
-
if (flags & SearchNodeFlags::ABANDONED) {
|
|
1918
|
-
// Node was abandoned, invoke_count already decremented
|
|
1919
|
-
// Mark as timed out so cleanup_search_node_states can remove it from node_states
|
|
1920
|
-
flags |= SearchNodeFlags::TIMED_OUT;
|
|
1921
|
-
transaction_to_search_.erase(trans_it);
|
|
1922
|
-
continue;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
bool had_short_timeout = flags & SearchNodeFlags::SHORT_TIMEOUT;
|
|
1926
|
-
|
|
1927
|
-
// Always decrement invoke_count on full timeout (node was still in-flight)
|
|
1928
|
-
if (search.invoke_count > 0) {
|
|
1929
|
-
search.invoke_count--;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
if (had_short_timeout) {
|
|
1933
|
-
// Restore branch factor since node fully timed out
|
|
1934
|
-
if (search.branch_factor > static_cast<int>(ALPHA)) {
|
|
1935
|
-
search.branch_factor--;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
LOG_DHT_DEBUG("Full timeout for node " << node_id_to_hex(trans_info.queried_node_id)
|
|
1939
|
-
<< " in search " << trans_info.info_hash_hex
|
|
1940
|
-
<< " (had short timeout) - restored branch_factor to " << search.branch_factor
|
|
1941
|
-
<< ", invoke_count now: " << search.invoke_count);
|
|
1942
|
-
} else {
|
|
1943
|
-
LOG_DHT_DEBUG("Full timeout for node " << node_id_to_hex(trans_info.queried_node_id)
|
|
1944
|
-
<< " in search " << trans_info.info_hash_hex
|
|
1945
|
-
<< " - invoke_count now: " << search.invoke_count);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// Mark the node as timed out (add flag, preserving history)
|
|
1949
|
-
flags |= SearchNodeFlags::TIMED_OUT;
|
|
1950
|
-
|
|
1951
|
-
// Mark the node as failed in routing table (BEP 5 compliance)
|
|
1952
|
-
{
|
|
1953
|
-
std::lock_guard<std::mutex> rt_lock(routing_table_mutex_);
|
|
1954
|
-
int bucket_index = get_bucket_index(trans_info.queried_node_id);
|
|
1955
|
-
auto& bucket = routing_table_[bucket_index];
|
|
1956
|
-
auto node_it = std::find_if(bucket.begin(), bucket.end(),
|
|
1957
|
-
[&trans_info](const DhtNode& n) { return n.id == trans_info.queried_node_id; });
|
|
1958
|
-
if (node_it != bucket.end()) {
|
|
1959
|
-
node_it->mark_failed();
|
|
1960
|
-
LOG_DHT_DEBUG("Marked node " << node_id_to_hex(trans_info.queried_node_id)
|
|
1961
|
-
<< " as failed in routing table (fail_count="
|
|
1962
|
-
<< static_cast<int>(node_it->fail_count) << ")");
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
affected_searches.insert(trans_info.info_hash_hex);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
// Remove the fully timed out transaction
|
|
1971
|
-
transaction_to_search_.erase(trans_it);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
if (!short_timeout_transactions.empty() || !full_timeout_transactions.empty()) {
|
|
1975
|
-
LOG_DHT_DEBUG("Timeout handling: " << short_timeout_transactions.size() << " short timeouts, "
|
|
1976
|
-
<< full_timeout_transactions.size() << " full timeouts");
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
// Continue searches that had timeout events
|
|
1980
|
-
for (const auto& hash_key : affected_searches) {
|
|
1981
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
1982
|
-
if (search_it != pending_searches_.end() && !search_it->second.is_finished) {
|
|
1983
|
-
LOG_DHT_DEBUG("Continuing search " << hash_key << " after timeout handling");
|
|
1984
|
-
DeferredCallbacks deferred;
|
|
1985
|
-
add_search_requests(search_it->second, deferred);
|
|
1986
|
-
if (deferred.should_invoke) {
|
|
1987
|
-
all_deferred.push_back(std::move(deferred));
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
// Clean up finished searches
|
|
1993
|
-
for (const auto& hash_key : affected_searches) {
|
|
1994
|
-
auto search_it = pending_searches_.find(hash_key);
|
|
1995
|
-
if (search_it != pending_searches_.end() && search_it->second.is_finished) {
|
|
1996
|
-
LOG_DHT_DEBUG("Removing finished search " << hash_key << " after timeout handling");
|
|
1997
|
-
pending_searches_.erase(search_it);
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
// Invoke all deferred callbacks outside the lock to avoid deadlock
|
|
2003
|
-
for (auto& deferred : all_deferred) {
|
|
2004
|
-
deferred.invoke();
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
void DhtClient::handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder) {
|
|
2009
|
-
DeferredCallbacks deferred;
|
|
2010
|
-
{
|
|
2011
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
2012
|
-
auto trans_it = transaction_to_search_.find(transaction_id);
|
|
2013
|
-
if (trans_it != transaction_to_search_.end()) {
|
|
2014
|
-
const auto& trans_info = trans_it->second;
|
|
2015
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
2016
|
-
if (search_it != pending_searches_.end()) {
|
|
2017
|
-
auto& pending_search = search_it->second;
|
|
2018
|
-
|
|
2019
|
-
// Check if this node was abandoned during truncation
|
|
2020
|
-
auto state_it = pending_search.node_states.find(trans_info.queried_node_id);
|
|
2021
|
-
if (state_it != pending_search.node_states.end() &&
|
|
2022
|
-
(state_it->second & SearchNodeFlags::ABANDONED)) {
|
|
2023
|
-
// Mark as responded so cleanup_search_node_states can remove it
|
|
2024
|
-
state_it->second |= SearchNodeFlags::RESPONDED;
|
|
2025
|
-
LOG_DHT_DEBUG("Ignoring empty response from abandoned node "
|
|
2026
|
-
<< node_id_to_hex(trans_info.queried_node_id));
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
uint8_t& flags = pending_search.node_states[trans_info.queried_node_id];
|
|
2031
|
-
|
|
2032
|
-
if (flags & SearchNodeFlags::RESPONDED) {
|
|
2033
|
-
LOG_DHT_DEBUG("Ignoring duplicate response from node " << node_id_to_hex(trans_info.queried_node_id));
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
// Decrement invoke count
|
|
2038
|
-
if (pending_search.invoke_count > 0) {
|
|
2039
|
-
pending_search.invoke_count--;
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
// Restore branch_factor if had short timeout
|
|
2043
|
-
if (flags & SearchNodeFlags::SHORT_TIMEOUT) {
|
|
2044
|
-
if (pending_search.branch_factor > static_cast<int>(ALPHA)) {
|
|
2045
|
-
pending_search.branch_factor--;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
// Mark as responded
|
|
2050
|
-
flags |= SearchNodeFlags::RESPONDED;
|
|
2051
|
-
|
|
2052
|
-
LOG_DHT_DEBUG("Empty get_peers response from " << responder.ip << ":" << responder.port
|
|
2053
|
-
<< " for info_hash " << trans_info.info_hash_hex
|
|
2054
|
-
<< " (invoke_count now: " << pending_search.invoke_count << ")");
|
|
2055
|
-
|
|
2056
|
-
// Continue search
|
|
2057
|
-
add_search_requests(pending_search, deferred);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
deferred.invoke();
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
void DhtClient::handle_get_peers_response_for_search(const std::string& transaction_id, const Peer& responder, const std::vector<Peer>& peers) {
|
|
2066
|
-
DeferredCallbacks deferred_immediate; // For new peers callbacks
|
|
2067
|
-
DeferredCallbacks deferred_completion; // For search completion callbacks
|
|
2068
|
-
|
|
2069
|
-
{
|
|
2070
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
2071
|
-
auto trans_it = transaction_to_search_.find(transaction_id);
|
|
2072
|
-
if (trans_it != transaction_to_search_.end()) {
|
|
2073
|
-
const auto& trans_info = trans_it->second;
|
|
2074
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
2075
|
-
if (search_it != pending_searches_.end()) {
|
|
2076
|
-
auto& pending_search = search_it->second;
|
|
2077
|
-
|
|
2078
|
-
// Check if this node was abandoned during truncation
|
|
2079
|
-
auto state_it = pending_search.node_states.find(trans_info.queried_node_id);
|
|
2080
|
-
if (state_it != pending_search.node_states.end() &&
|
|
2081
|
-
(state_it->second & SearchNodeFlags::ABANDONED)) {
|
|
2082
|
-
// Mark as responded so cleanup_search_node_states can remove it
|
|
2083
|
-
state_it->second |= SearchNodeFlags::RESPONDED;
|
|
2084
|
-
LOG_DHT_DEBUG("Ignoring response from abandoned node "
|
|
2085
|
-
<< node_id_to_hex(trans_info.queried_node_id)
|
|
2086
|
-
<< " - invoke_count already decremented during truncation");
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
// Get flags for this node and mark as responded
|
|
2091
|
-
uint8_t& flags = pending_search.node_states[trans_info.queried_node_id];
|
|
2092
|
-
|
|
2093
|
-
// Check if already responded (duplicate response)
|
|
2094
|
-
if (flags & SearchNodeFlags::RESPONDED) {
|
|
2095
|
-
LOG_DHT_DEBUG("Ignoring duplicate response from node "
|
|
2096
|
-
<< node_id_to_hex(trans_info.queried_node_id));
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
// Decrement invoke count since we received a response
|
|
2101
|
-
if (pending_search.invoke_count > 0) {
|
|
2102
|
-
pending_search.invoke_count--;
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
// If this node had short timeout, restore the branch factor (late response arrived)
|
|
2106
|
-
if (flags & SearchNodeFlags::SHORT_TIMEOUT) {
|
|
2107
|
-
if (pending_search.branch_factor > static_cast<int>(ALPHA)) {
|
|
2108
|
-
pending_search.branch_factor--;
|
|
2109
|
-
}
|
|
2110
|
-
LOG_DHT_DEBUG("Late response from node " << node_id_to_hex(trans_info.queried_node_id)
|
|
2111
|
-
<< " (had short timeout) - restored branch_factor to " << pending_search.branch_factor);
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
// Mark as responded (add flag, preserving history including SHORT_TIMEOUT)
|
|
2115
|
-
flags |= SearchNodeFlags::RESPONDED;
|
|
2116
|
-
|
|
2117
|
-
LOG_DHT_DEBUG("Found pending search for KRPC transaction " << transaction_id
|
|
2118
|
-
<< " - received " << peers.size() << " peers for info_hash " << trans_info.info_hash_hex
|
|
2119
|
-
<< " from " << responder.ip << ":" << responder.port
|
|
2120
|
-
<< " (invoke_count now: " << pending_search.invoke_count << ")");
|
|
2121
|
-
|
|
2122
|
-
// Accumulate peers (with deduplication) - continue search like reference implementation
|
|
2123
|
-
if (!peers.empty()) {
|
|
2124
|
-
// Collect only new (non-duplicate) peers for immediate callback
|
|
2125
|
-
std::vector<Peer> new_peers;
|
|
2126
|
-
new_peers.reserve(peers.size());
|
|
2127
|
-
|
|
2128
|
-
for (const auto& peer : peers) {
|
|
2129
|
-
// Check if peer already exists in found_peers
|
|
2130
|
-
auto it = std::find_if(pending_search.found_peers.begin(),
|
|
2131
|
-
pending_search.found_peers.end(),
|
|
2132
|
-
[&peer](const Peer& p) {
|
|
2133
|
-
return p.ip == peer.ip && p.port == peer.port;
|
|
2134
|
-
});
|
|
2135
|
-
if (it == pending_search.found_peers.end()) {
|
|
2136
|
-
pending_search.found_peers.push_back(peer);
|
|
2137
|
-
new_peers.push_back(peer);
|
|
2138
|
-
LOG_DHT_DEBUG(" [new] found peer for hash(" << trans_info.info_hash_hex << ") = " << peer.ip << ":" << peer.port);
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
// Collect immediate callbacks for new peers
|
|
2143
|
-
if (!new_peers.empty()) {
|
|
2144
|
-
LOG_DHT_DEBUG("Invoking " << pending_search.callbacks.size() << " callbacks with "
|
|
2145
|
-
<< new_peers.size() << " new peers for info_hash " << trans_info.info_hash_hex);
|
|
2146
|
-
deferred_immediate.should_invoke = true;
|
|
2147
|
-
deferred_immediate.peers = std::move(new_peers);
|
|
2148
|
-
deferred_immediate.info_hash = pending_search.info_hash;
|
|
2149
|
-
deferred_immediate.callbacks = pending_search.callbacks;
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
LOG_DHT_DEBUG("Accumulated " << pending_search.found_peers.size() << " total peers for info_hash " << trans_info.info_hash_hex);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// Continue search - let add_search_requests determine when to finish
|
|
2156
|
-
add_search_requests(pending_search, deferred_completion);
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// DON'T remove the transaction mapping here - it will be removed at the end of handle_krpc_response
|
|
2160
|
-
// This ensures all response data is fully processed before cleanup
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
// Invoke all callbacks outside the lock to avoid deadlock
|
|
2165
|
-
deferred_immediate.invoke();
|
|
2166
|
-
deferred_completion.invoke();
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
void DhtClient::handle_get_peers_response_with_nodes(const std::string& transaction_id, const Peer& responder, const std::vector<KrpcNode>& nodes) {
|
|
2171
|
-
// This function is called when get_peers returns nodes instead of peers
|
|
2172
|
-
// Add the new nodes to search_nodes and continue the search
|
|
2173
|
-
|
|
2174
|
-
DeferredCallbacks deferred;
|
|
2175
|
-
|
|
2176
|
-
{
|
|
2177
|
-
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
2178
|
-
|
|
2179
|
-
auto trans_it = transaction_to_search_.find(transaction_id);
|
|
2180
|
-
if (trans_it != transaction_to_search_.end()) {
|
|
2181
|
-
const auto& trans_info = trans_it->second;
|
|
2182
|
-
auto search_it = pending_searches_.find(trans_info.info_hash_hex);
|
|
2183
|
-
if (search_it != pending_searches_.end()) {
|
|
2184
|
-
auto& pending_search = search_it->second;
|
|
2185
|
-
|
|
2186
|
-
// Check if this node was abandoned during truncation
|
|
2187
|
-
auto state_it = pending_search.node_states.find(trans_info.queried_node_id);
|
|
2188
|
-
if (state_it != pending_search.node_states.end() &&
|
|
2189
|
-
(state_it->second & SearchNodeFlags::ABANDONED)) {
|
|
2190
|
-
// Mark as responded so cleanup_search_node_states can remove it
|
|
2191
|
-
state_it->second |= SearchNodeFlags::RESPONDED;
|
|
2192
|
-
LOG_DHT_DEBUG("Ignoring response from abandoned node "
|
|
2193
|
-
<< node_id_to_hex(trans_info.queried_node_id)
|
|
2194
|
-
<< " - invoke_count already decremented during truncation");
|
|
2195
|
-
return;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// Get flags for this node and mark as responded
|
|
2199
|
-
uint8_t& flags = pending_search.node_states[trans_info.queried_node_id];
|
|
2200
|
-
|
|
2201
|
-
// Check if already responded (duplicate response)
|
|
2202
|
-
if (flags & SearchNodeFlags::RESPONDED) {
|
|
2203
|
-
LOG_DHT_DEBUG("Ignoring duplicate response from node "
|
|
2204
|
-
<< node_id_to_hex(trans_info.queried_node_id));
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
// Decrement invoke count since we received a response
|
|
2209
|
-
if (pending_search.invoke_count > 0) {
|
|
2210
|
-
pending_search.invoke_count--;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
// If this node had short timeout, restore the branch factor (late response arrived)
|
|
2214
|
-
if (flags & SearchNodeFlags::SHORT_TIMEOUT) {
|
|
2215
|
-
if (pending_search.branch_factor > static_cast<int>(ALPHA)) {
|
|
2216
|
-
pending_search.branch_factor--;
|
|
2217
|
-
}
|
|
2218
|
-
LOG_DHT_DEBUG("Late response from node " << node_id_to_hex(trans_info.queried_node_id)
|
|
2219
|
-
<< " (had short timeout) - restored branch_factor to " << pending_search.branch_factor);
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
// Mark as responded (add flag, preserving history including SHORT_TIMEOUT)
|
|
2223
|
-
flags |= SearchNodeFlags::RESPONDED;
|
|
2224
|
-
|
|
2225
|
-
LOG_DHT_DEBUG("Processing get_peers response with " << nodes.size()
|
|
2226
|
-
<< " nodes for info_hash " << trans_info.info_hash_hex << " from " << responder.ip << ":" << responder.port
|
|
2227
|
-
<< " (invoke_count now: " << pending_search.invoke_count << ")");
|
|
2228
|
-
|
|
2229
|
-
// Add new nodes to search_nodes (sorted by distance)
|
|
2230
|
-
size_t nodes_added = 0;
|
|
2231
|
-
for (const auto& node : nodes) {
|
|
2232
|
-
DhtNode dht_node = krpc_node_to_dht_node(node);
|
|
2233
|
-
size_t old_size = pending_search.search_nodes.size();
|
|
2234
|
-
add_node_to_search(pending_search, dht_node);
|
|
2235
|
-
if (pending_search.search_nodes.size() > old_size) {
|
|
2236
|
-
nodes_added++;
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
LOG_DHT_DEBUG("Added " << nodes_added << " new nodes to search_nodes (total: " << pending_search.search_nodes.size() << ")");
|
|
2241
|
-
|
|
2242
|
-
// Continue search with new nodes
|
|
2243
|
-
add_search_requests(pending_search, deferred);
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
// DON'T remove the transaction mapping here - it will be removed at the end of handle_krpc_response
|
|
2247
|
-
// This ensures all response data is fully processed before cleanup
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
// Invoke callbacks outside the lock to avoid deadlock
|
|
2252
|
-
deferred.invoke();
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
void DhtClient::add_node_to_search(PendingSearch& search, const DhtNode& node) {
|
|
2257
|
-
// Check if node already exists in search (node is "known" if it's in node_states map)
|
|
2258
|
-
if (search.node_states.find(node.id) != search.node_states.end()) {
|
|
2259
|
-
LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " already known for search - skipping");
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
// Find insertion point to maintain sorted order (closest first)
|
|
2264
|
-
auto insert_pos = std::lower_bound(search.search_nodes.begin(), search.search_nodes.end(), node,
|
|
2265
|
-
[&search, this](const DhtNode& a, const DhtNode& b) {
|
|
2266
|
-
return is_closer(a.id, b.id, search.info_hash);
|
|
2267
|
-
});
|
|
2268
|
-
|
|
2269
|
-
search.search_nodes.insert(insert_pos, node);
|
|
2270
|
-
// Mark node as known (add to map with no flags set - will get QUERIED flag when query is sent)
|
|
2271
|
-
search.node_states[node.id] = 0;
|
|
2272
|
-
|
|
2273
|
-
// Limit search_nodes size to avoid unbounded growth
|
|
2274
|
-
constexpr size_t MAX_SEARCH_NODES = 100;
|
|
2275
|
-
if (search.search_nodes.size() > MAX_SEARCH_NODES) {
|
|
2276
|
-
// Before truncating, clean up counters for in-flight queries being discarded
|
|
2277
|
-
for (size_t i = MAX_SEARCH_NODES; i < search.search_nodes.size(); ++i) {
|
|
2278
|
-
const auto& discarded_node = search.search_nodes[i];
|
|
2279
|
-
auto state_it = search.node_states.find(discarded_node.id);
|
|
2280
|
-
if (state_it != search.node_states.end()) {
|
|
2281
|
-
uint8_t flags = state_it->second;
|
|
2282
|
-
// If queried but not responded/failed, it's in-flight
|
|
2283
|
-
if ((flags & SearchNodeFlags::QUERIED) &&
|
|
2284
|
-
!(flags & (SearchNodeFlags::RESPONDED | SearchNodeFlags::TIMED_OUT))) {
|
|
2285
|
-
// Decrement invoke_count since this request is being abandoned
|
|
2286
|
-
if (search.invoke_count > 0) {
|
|
2287
|
-
search.invoke_count--;
|
|
2288
|
-
LOG_DHT_DEBUG("Decrementing invoke_count for abandoned node "
|
|
2289
|
-
<< node_id_to_hex(discarded_node.id)
|
|
2290
|
-
<< " (now: " << search.invoke_count << ")");
|
|
2291
|
-
}
|
|
2292
|
-
// If it had short timeout, also restore branch factor
|
|
2293
|
-
if (flags & SearchNodeFlags::SHORT_TIMEOUT) {
|
|
2294
|
-
if (search.branch_factor > static_cast<int>(ALPHA)) {
|
|
2295
|
-
search.branch_factor--;
|
|
2296
|
-
LOG_DHT_DEBUG("Decrementing branch_factor for abandoned node with short_timeout "
|
|
2297
|
-
<< node_id_to_hex(discarded_node.id)
|
|
2298
|
-
<< " (now: " << search.branch_factor << ")");
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
// Mark as ABANDONED instead of removing - prevents double invoke_count decrement
|
|
2303
|
-
// when late response arrives (response handlers check this flag)
|
|
2304
|
-
state_it->second |= SearchNodeFlags::ABANDONED;
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
search.search_nodes.resize(MAX_SEARCH_NODES);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
void DhtClient::save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token) {
|
|
2312
|
-
// Save the write token received from a node (BEP 5 compliant)
|
|
2313
|
-
// This token will be used later when sending announce_peer to this node
|
|
2314
|
-
|
|
2315
|
-
if (token.empty()) {
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
// Only save token if we don't already have one from this node
|
|
2320
|
-
// (first token is usually the valid one)
|
|
2321
|
-
if (search.write_tokens.find(node_id) == search.write_tokens.end()) {
|
|
2322
|
-
search.write_tokens[node_id] = token;
|
|
2323
|
-
LOG_DHT_DEBUG("Saved write token from node " << node_id_to_hex(node_id)
|
|
2324
|
-
<< " for info_hash " << node_id_to_hex(search.info_hash)
|
|
2325
|
-
<< " (total tokens: " << search.write_tokens.size() << ")");
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
void DhtClient::send_announce_to_closest_nodes(PendingSearch& search) {
|
|
2330
|
-
// BEP 5: Send announce_peer to the k closest nodes that:
|
|
2331
|
-
// 1. Responded to our get_peers query
|
|
2332
|
-
// 2. Gave us a valid write token
|
|
2333
|
-
|
|
2334
|
-
if (!search.is_announce) {
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
std::string hash_key = node_id_to_hex(search.info_hash);
|
|
2339
|
-
|
|
2340
|
-
LOG_DHT_INFO("Sending announce_peer to closest nodes for info_hash " << hash_key
|
|
2341
|
-
<< " on port " << search.announce_port);
|
|
2342
|
-
|
|
2343
|
-
// Collect nodes that responded and have tokens, sorted by distance (closest first)
|
|
2344
|
-
std::vector<std::pair<DhtNode, std::string>> announce_targets;
|
|
2345
|
-
announce_targets.reserve(K_BUCKET_SIZE);
|
|
2346
|
-
|
|
2347
|
-
for (const auto& node : search.search_nodes) {
|
|
2348
|
-
if (announce_targets.size() >= K_BUCKET_SIZE) {
|
|
2349
|
-
break; // We have enough targets
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
// Check if node responded successfully
|
|
2353
|
-
auto state_it = search.node_states.find(node.id);
|
|
2354
|
-
if (state_it == search.node_states.end()) {
|
|
2355
|
-
continue;
|
|
2356
|
-
}
|
|
2357
|
-
if (!(state_it->second & SearchNodeFlags::RESPONDED)) {
|
|
2358
|
-
continue; // Node didn't respond
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
// Check if we have a token from this node
|
|
2362
|
-
auto token_it = search.write_tokens.find(node.id);
|
|
2363
|
-
if (token_it == search.write_tokens.end()) {
|
|
2364
|
-
LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " responded but no token - skipping");
|
|
2365
|
-
continue; // No token from this node
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
announce_targets.emplace_back(node, token_it->second);
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
if (announce_targets.empty()) {
|
|
2372
|
-
LOG_DHT_WARN("No nodes with tokens to announce to for info_hash " << hash_key);
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
LOG_DHT_INFO("Announcing to " << announce_targets.size() << " closest nodes with tokens");
|
|
2377
|
-
|
|
2378
|
-
// BEP 5: Use implied_port when the announce port is the same as the DHT port.
|
|
2379
|
-
// This tells the receiving node to use the UDP source port from the packet,
|
|
2380
|
-
// which is more accurate when behind NAT.
|
|
2381
|
-
bool use_implied_port = (search.announce_port == port_);
|
|
2382
|
-
|
|
2383
|
-
// Send announce_peer to each target
|
|
2384
|
-
for (const auto& [node, token] : announce_targets) {
|
|
2385
|
-
LOG_DHT_DEBUG("Sending announce_peer to node " << node_id_to_hex(node.id)
|
|
2386
|
-
<< " at " << node.peer.ip << ":" << node.peer.port
|
|
2387
|
-
<< " with token (distance: " << get_bucket_index(node.id) << ")"
|
|
2388
|
-
<< (use_implied_port ? " [implied_port]" : ""));
|
|
2389
|
-
|
|
2390
|
-
send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token, use_implied_port);
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
LOG_DHT_INFO("Announce completed: sent announce_peer to " << announce_targets.size()
|
|
2394
|
-
<< " nodes for info_hash " << hash_key);
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& deferred) {
|
|
2398
|
-
// Returns true if search is done (completed or should be finished)
|
|
2399
|
-
|
|
2400
|
-
if (search.is_finished) {
|
|
2401
|
-
return true;
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
std::string hash_key = node_id_to_hex(search.info_hash);
|
|
2405
|
-
|
|
2406
|
-
LOG_DHT_DEBUG("Adding search requests for info_hash " << hash_key);
|
|
2407
|
-
|
|
2408
|
-
const int k = static_cast<int>(K_BUCKET_SIZE); // Target number of results
|
|
2409
|
-
int loop_index = -1;
|
|
2410
|
-
int results_found = 0; // Nodes that have responded
|
|
2411
|
-
int queries_in_flight = 0; // Requests currently in flight
|
|
2412
|
-
int timed_out_count = 0; // Nodes that timed out
|
|
2413
|
-
int queries_sent = 0; // Queries sent this round
|
|
2414
|
-
|
|
2415
|
-
// Iterate through search_nodes (sorted by distance, closest first)
|
|
2416
|
-
// Important: We must continue iterating to count results even when we can't send more requests
|
|
2417
|
-
for (auto& node : search.search_nodes) {
|
|
2418
|
-
loop_index++;
|
|
2419
|
-
|
|
2420
|
-
// Stop if we have enough completed results
|
|
2421
|
-
if (results_found >= k) {
|
|
2422
|
-
break;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// Get flags for this node (0 if not in map, meaning just "known")
|
|
2426
|
-
auto state_it = search.node_states.find(node.id);
|
|
2427
|
-
uint8_t flags = (state_it != search.node_states.end()) ? state_it->second : 0;
|
|
2428
|
-
|
|
2429
|
-
// Usually it doesn't happen, but if it does, we skip it
|
|
2430
|
-
if (flags & SearchNodeFlags::ABANDONED) {
|
|
2431
|
-
continue;
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
// Check if this node has already responded (counts toward results)
|
|
2435
|
-
if (flags & SearchNodeFlags::RESPONDED) {
|
|
2436
|
-
results_found++;
|
|
2437
|
-
continue;
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
// Skip nodes that have timed out (don't count as results or in-flight)
|
|
2441
|
-
if (flags & SearchNodeFlags::TIMED_OUT) {
|
|
2442
|
-
timed_out_count++;
|
|
2443
|
-
continue;
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
// Check if this node was already queried
|
|
2447
|
-
if (flags & SearchNodeFlags::QUERIED) {
|
|
2448
|
-
// Only count as in-flight if not responded yet
|
|
2449
|
-
// (TIMED_OUT already handled above, RESPONDED handled above too)
|
|
2450
|
-
// This case handles nodes that are QUERIED but still waiting for response
|
|
2451
|
-
queries_in_flight++;
|
|
2452
|
-
continue;
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
// Check if we have capacity to send more requests
|
|
2456
|
-
// Important: use 'continue' not 'break' to keep counting results
|
|
2457
|
-
// Use adaptive branch_factor (increases on short timeout, restores on response/full timeout)
|
|
2458
|
-
if (search.invoke_count >= search.branch_factor) {
|
|
2459
|
-
continue;
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
// Send query to this node
|
|
2463
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
2464
|
-
transaction_to_search_[transaction_id] = SearchTransaction(hash_key, node.id, node.peer);
|
|
2465
|
-
search.node_states[node.id] |= SearchNodeFlags::QUERIED;
|
|
2466
|
-
search.invoke_count++;
|
|
2467
|
-
|
|
2468
|
-
LOG_DHT_DEBUG("Querying node " << node_id_to_hex(node.id) << " at " << node.peer.ip << ":" << node.peer.port);
|
|
2469
|
-
|
|
2470
|
-
auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, search.info_hash);
|
|
2471
|
-
// BEP 32: request peers/nodes of our own family only
|
|
2472
|
-
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
2473
|
-
send_krpc_message(message, node.peer);
|
|
2474
|
-
|
|
2475
|
-
queries_sent++;
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
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() << "]:");
|
|
2479
|
-
LOG_DHT_DEBUG(" * search_nodes: " << search.search_nodes.size());
|
|
2480
|
-
LOG_DHT_DEBUG(" * queries_sent: " << queries_sent);
|
|
2481
|
-
LOG_DHT_DEBUG(" * invoke_count: " << search.invoke_count);
|
|
2482
|
-
LOG_DHT_DEBUG(" * branch_factor: " << search.branch_factor);
|
|
2483
|
-
LOG_DHT_DEBUG(" * results_found: " << results_found);
|
|
2484
|
-
LOG_DHT_DEBUG(" * queries_in_flight: " << queries_in_flight);
|
|
2485
|
-
LOG_DHT_DEBUG(" * timed_out: " << timed_out_count);
|
|
2486
|
-
LOG_DHT_DEBUG(" * peers_found: " << search.found_peers.size());
|
|
2487
|
-
LOG_DHT_DEBUG(" * callbacks: " << search.callbacks.size());
|
|
2488
|
-
LOG_DHT_DEBUG(" * loop_index: " << loop_index);
|
|
2489
|
-
LOG_DHT_DEBUG(" * node_states: " << search.node_states.size());
|
|
2490
|
-
|
|
2491
|
-
if ((results_found >= k && queries_in_flight == 0) || search.invoke_count == 0) {
|
|
2492
|
-
auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
2493
|
-
std::chrono::steady_clock::now() - search.created_at
|
|
2494
|
-
).count();
|
|
2495
|
-
|
|
2496
|
-
// Count final stats for completion log
|
|
2497
|
-
int queried_total = 0, responded_total = 0, timed_out_total = 0, short_timeout_total = 0, abandoned_total = 0;
|
|
2498
|
-
for (const auto& [id, f] : search.node_states) {
|
|
2499
|
-
if (f & SearchNodeFlags::QUERIED) queried_total++;
|
|
2500
|
-
if (f & SearchNodeFlags::RESPONDED) responded_total++;
|
|
2501
|
-
if (f & SearchNodeFlags::TIMED_OUT) timed_out_total++;
|
|
2502
|
-
if (f & SearchNodeFlags::SHORT_TIMEOUT) short_timeout_total++;
|
|
2503
|
-
if (f & SearchNodeFlags::ABANDONED) abandoned_total++;
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
LOG_DHT_INFO("=== " << (search.is_announce ? "Announce" : "Search") << " Completed for info_hash " << hash_key << " ===");
|
|
2507
|
-
LOG_DHT_INFO(" Duration: " << duration_ms << "ms");
|
|
2508
|
-
LOG_DHT_INFO(" Total nodes queried: " << queried_total);
|
|
2509
|
-
LOG_DHT_INFO(" Total nodes responded: " << responded_total);
|
|
2510
|
-
LOG_DHT_INFO(" Total nodes timed out: " << timed_out_total);
|
|
2511
|
-
LOG_DHT_INFO(" Nodes with short timeout: " << short_timeout_total);
|
|
2512
|
-
LOG_DHT_INFO(" Nodes abandoned (truncation): " << abandoned_total);
|
|
2513
|
-
LOG_DHT_INFO(" Final branch_factor: " << search.branch_factor << " (initial: " << ALPHA << ")");
|
|
2514
|
-
if (search.is_announce) {
|
|
2515
|
-
LOG_DHT_INFO(" Write tokens collected: " << search.write_tokens.size());
|
|
2516
|
-
LOG_DHT_INFO(" Announce port: " << search.announce_port);
|
|
2517
|
-
} else {
|
|
2518
|
-
LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
|
|
2519
|
-
LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
// If this is an announce search, send announce_peer to k closest nodes with tokens
|
|
2523
|
-
if (search.is_announce) {
|
|
2524
|
-
send_announce_to_closest_nodes(search);
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
// Collect callbacks for deferred invocation (avoid deadlock - don't call user callbacks while holding mutex)
|
|
2528
|
-
deferred.should_invoke = true;
|
|
2529
|
-
deferred.callbacks = search.callbacks;
|
|
2530
|
-
deferred.peers = search.found_peers;
|
|
2531
|
-
deferred.info_hash = search.info_hash;
|
|
2532
|
-
|
|
2533
|
-
search.is_finished = true;
|
|
2534
|
-
return true;
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
return false;
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
// Peer announcement storage management
|
|
2541
|
-
void DhtClient::store_announced_peer(const InfoHash& info_hash, const Peer& peer) {
|
|
2542
|
-
std::lock_guard<std::mutex> lock(announced_peers_mutex_);
|
|
2543
|
-
|
|
2544
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
2545
|
-
auto& peers = announced_peers_[hash_key];
|
|
2546
|
-
|
|
2547
|
-
// Check if peer already exists
|
|
2548
|
-
auto it = std::find_if(peers.begin(), peers.end(),
|
|
2549
|
-
[&peer](const AnnouncedPeer& announced) {
|
|
2550
|
-
return announced.peer.ip == peer.ip && announced.peer.port == peer.port;
|
|
2551
|
-
});
|
|
2552
|
-
|
|
2553
|
-
if (it != peers.end()) {
|
|
2554
|
-
// Update existing peer's timestamp
|
|
2555
|
-
it->announced_at = std::chrono::steady_clock::now();
|
|
2556
|
-
LOG_DHT_DEBUG("Updated existing announced peer " << peer.ip << ":" << peer.port
|
|
2557
|
-
<< " for info_hash " << hash_key);
|
|
2558
|
-
} else {
|
|
2559
|
-
// Add new peer
|
|
2560
|
-
peers.emplace_back(peer);
|
|
2561
|
-
LOG_DHT_DEBUG("Stored new announced peer " << peer.ip << ":" << peer.port
|
|
2562
|
-
<< " for info_hash " << hash_key << " (total: " << peers.size() << ")");
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
std::vector<Peer> DhtClient::get_announced_peers(const InfoHash& info_hash) {
|
|
2567
|
-
std::lock_guard<std::mutex> lock(announced_peers_mutex_);
|
|
2568
|
-
|
|
2569
|
-
std::string hash_key = node_id_to_hex(info_hash);
|
|
2570
|
-
auto it = announced_peers_.find(hash_key);
|
|
2571
|
-
|
|
2572
|
-
std::vector<Peer> peers;
|
|
2573
|
-
if (it != announced_peers_.end()) {
|
|
2574
|
-
peers.reserve(it->second.size());
|
|
2575
|
-
for (const auto& announced : it->second) {
|
|
2576
|
-
peers.push_back(announced.peer);
|
|
2577
|
-
}
|
|
2578
|
-
LOG_DHT_DEBUG("Retrieved " << peers.size() << " announced peers for info_hash " << hash_key);
|
|
2579
|
-
} else {
|
|
2580
|
-
LOG_DHT_DEBUG("No announced peers found for info_hash " << hash_key);
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
return peers;
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
void DhtClient::cleanup_stale_announced_peers() {
|
|
2587
|
-
std::lock_guard<std::mutex> lock(announced_peers_mutex_);
|
|
2588
|
-
|
|
2589
|
-
auto now = std::chrono::steady_clock::now();
|
|
2590
|
-
auto stale_threshold = std::chrono::minutes(30); // BEP 5 standard: 30 minutes
|
|
2591
|
-
|
|
2592
|
-
size_t total_before = 0;
|
|
2593
|
-
size_t total_after = 0;
|
|
2594
|
-
|
|
2595
|
-
for (auto it = announced_peers_.begin(); it != announced_peers_.end(); ) {
|
|
2596
|
-
auto& peers = it->second;
|
|
2597
|
-
total_before += peers.size();
|
|
2598
|
-
|
|
2599
|
-
// Remove stale peers
|
|
2600
|
-
peers.erase(std::remove_if(peers.begin(), peers.end(),
|
|
2601
|
-
[now, stale_threshold](const AnnouncedPeer& announced) {
|
|
2602
|
-
return now - announced.announced_at > stale_threshold;
|
|
2603
|
-
}), peers.end());
|
|
2604
|
-
|
|
2605
|
-
total_after += peers.size();
|
|
2606
|
-
|
|
2607
|
-
// Remove empty info_hash entries
|
|
2608
|
-
if (peers.empty()) {
|
|
2609
|
-
LOG_DHT_DEBUG("Removing empty announced peers entry for info_hash " << it->first);
|
|
2610
|
-
it = announced_peers_.erase(it);
|
|
2611
|
-
} else {
|
|
2612
|
-
++it;
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
if (total_before > total_after) {
|
|
2617
|
-
LOG_DHT_DEBUG("Cleaned up " << (total_before - total_after) << " stale announced peers "
|
|
2618
|
-
<< "(from " << total_before << " to " << total_after << ")");
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
// Ping-before-replace eviction implementation
|
|
2623
|
-
void DhtClient::initiate_ping_verification(const DhtNode& candidate_node, const DhtNode& old_node, int bucket_index) {
|
|
2624
|
-
// NOTE: pending_pings_mutex_ is already held by caller (add_node)
|
|
2625
|
-
|
|
2626
|
-
std::string ping_transaction_id = KrpcProtocol::generate_transaction_id();
|
|
2627
|
-
|
|
2628
|
-
LOG_DHT_DEBUG("Initiating ping verification: pinging OLD node " << node_id_to_hex(old_node.id)
|
|
2629
|
-
<< " at " << old_node.peer.ip << ":" << old_node.peer.port
|
|
2630
|
-
<< " to check if alive. Candidate " << node_id_to_hex(candidate_node.id)
|
|
2631
|
-
<< " waiting to replace if old node fails. (transaction: " << ping_transaction_id << ")");
|
|
2632
|
-
|
|
2633
|
-
// Store ping verification state and mark old node as being pinged
|
|
2634
|
-
pending_pings_.emplace(ping_transaction_id, PingVerification(candidate_node, old_node, bucket_index));
|
|
2635
|
-
nodes_being_replaced_.insert(old_node.id);
|
|
2636
|
-
|
|
2637
|
-
// BEP 5: Send ping to the OLD node to verify it's still alive
|
|
2638
|
-
// If old node responds -> keep it, discard candidate
|
|
2639
|
-
// If old node times out -> replace with candidate
|
|
2640
|
-
auto message = KrpcProtocol::create_ping_query(ping_transaction_id, node_id_);
|
|
2641
|
-
send_krpc_message(message, old_node.peer);
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
void DhtClient::handle_ping_verification_response(const std::string& transaction_id, const NodeId& responder_id, const Peer& responder) {
|
|
2645
|
-
std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
|
|
2646
|
-
|
|
2647
|
-
auto it = pending_pings_.find(transaction_id);
|
|
2648
|
-
if (it != pending_pings_.end()) {
|
|
2649
|
-
const auto& verification = it->second;
|
|
2650
|
-
|
|
2651
|
-
// Security check: Verify response comes from the IP we pinged
|
|
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
|
-
|
|
2656
|
-
if (responder_ip_normalized != expected_ip_normalized) {
|
|
2657
|
-
LOG_DHT_WARN("Ping verification response from wrong IP " << responder.ip
|
|
2658
|
-
<< " (expected " << verification.old_node.peer.ip << ") - ignoring");
|
|
2659
|
-
return; // Don't remove from pending_pings_, let it timeout naturally
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
// BEP 5: We pinged the OLD node to check if it's still alive
|
|
2663
|
-
if (responder_id == verification.old_node.id) {
|
|
2664
|
-
// Calculate RTT
|
|
2665
|
-
auto rtt_duration = std::chrono::steady_clock::now() - verification.ping_sent_at;
|
|
2666
|
-
uint16_t rtt_ms = static_cast<uint16_t>(
|
|
2667
|
-
(std::min)(static_cast<int64_t>(0xfffe),
|
|
2668
|
-
static_cast<int64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(rtt_duration).count())));
|
|
2669
|
-
|
|
2670
|
-
// Old node responded - it's still alive! Keep it, discard the candidate.
|
|
2671
|
-
LOG_DHT_DEBUG("Old node " << node_id_to_hex(verification.old_node.id)
|
|
2672
|
-
<< " responded to ping (rtt=" << rtt_ms << "ms) - keeping it, discarding candidate "
|
|
2673
|
-
<< node_id_to_hex(verification.candidate_node.id));
|
|
2674
|
-
|
|
2675
|
-
// Update old node in routing table
|
|
2676
|
-
{
|
|
2677
|
-
std::lock_guard<std::mutex> rt_lock(routing_table_mutex_);
|
|
2678
|
-
auto& bucket = routing_table_[verification.bucket_index];
|
|
2679
|
-
auto node_it = std::find_if(bucket.begin(), bucket.end(),
|
|
2680
|
-
[&verification](const DhtNode& n) { return n.id == verification.old_node.id; });
|
|
2681
|
-
if (node_it != bucket.end()) {
|
|
2682
|
-
node_it->mark_success();
|
|
2683
|
-
node_it->update_rtt(rtt_ms);
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
// Candidate is discarded (not added to routing table)
|
|
2687
|
-
} else {
|
|
2688
|
-
// Different node ID responded from the same IP:port!
|
|
2689
|
-
// This means the old node is gone and this IP now belongs to a different node.
|
|
2690
|
-
// Replace the old node with the candidate.
|
|
2691
|
-
LOG_DHT_DEBUG("Different node " << node_id_to_hex(responder_id)
|
|
2692
|
-
<< " responded from " << responder.ip << ":" << responder.port
|
|
2693
|
-
<< " (expected old node " << node_id_to_hex(verification.old_node.id)
|
|
2694
|
-
<< ") - old node is gone, replacing with candidate "
|
|
2695
|
-
<< node_id_to_hex(verification.candidate_node.id));
|
|
2696
|
-
|
|
2697
|
-
// Replace old node with candidate
|
|
2698
|
-
DhtNode candidate = verification.candidate_node;
|
|
2699
|
-
candidate.mark_success(); // The responder just proved it's alive
|
|
2700
|
-
perform_replacement(candidate, verification.old_node, verification.bucket_index);
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
// Remove tracking entries
|
|
2704
|
-
nodes_being_replaced_.erase(verification.old_node.id);
|
|
2705
|
-
pending_pings_.erase(it);
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
void DhtClient::cleanup_stale_ping_verifications() {
|
|
2710
|
-
std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
|
|
2711
|
-
|
|
2712
|
-
auto now = std::chrono::steady_clock::now();
|
|
2713
|
-
auto timeout_threshold = std::chrono::seconds(30); // 30 second timeout for ping responses
|
|
2714
|
-
|
|
2715
|
-
auto it = pending_pings_.begin();
|
|
2716
|
-
while (it != pending_pings_.end()) {
|
|
2717
|
-
if (now - it->second.ping_sent_at > timeout_threshold) {
|
|
2718
|
-
const auto& verification = it->second;
|
|
2719
|
-
|
|
2720
|
-
// BEP 5: Old node didn't respond (timeout) - it's dead, replace with candidate!
|
|
2721
|
-
LOG_DHT_DEBUG("Old node " << node_id_to_hex(verification.old_node.id)
|
|
2722
|
-
<< " timed out after 30s - replacing with candidate " << node_id_to_hex(verification.candidate_node.id));
|
|
2723
|
-
|
|
2724
|
-
// Perform the replacement with a fresh candidate
|
|
2725
|
-
DhtNode fresh_candidate = verification.candidate_node;
|
|
2726
|
-
perform_replacement(fresh_candidate, verification.old_node, verification.bucket_index);
|
|
2727
|
-
|
|
2728
|
-
// Remove tracking entries
|
|
2729
|
-
nodes_being_replaced_.erase(verification.old_node.id);
|
|
2730
|
-
|
|
2731
|
-
it = pending_pings_.erase(it);
|
|
2732
|
-
} else {
|
|
2733
|
-
++it;
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
bool DhtClient::perform_replacement(const DhtNode& candidate_node, const DhtNode& node_to_replace, int bucket_index) {
|
|
2739
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
2740
|
-
|
|
2741
|
-
auto& bucket = routing_table_[bucket_index];
|
|
2742
|
-
auto it = std::find_if(bucket.begin(), bucket.end(),
|
|
2743
|
-
[&node_to_replace](const DhtNode& node) {
|
|
2744
|
-
return node.id == node_to_replace.id;
|
|
2745
|
-
});
|
|
2746
|
-
|
|
2747
|
-
if (it != bucket.end()) {
|
|
2748
|
-
LOG_DHT_DEBUG("Replacing old node " << node_id_to_hex(node_to_replace.id)
|
|
2749
|
-
<< " with " << node_id_to_hex(candidate_node.id) << " in bucket " << bucket_index);
|
|
2750
|
-
*it = candidate_node;
|
|
2751
|
-
return true;
|
|
2752
|
-
} else {
|
|
2753
|
-
LOG_DHT_WARN("Could not find node " << node_id_to_hex(node_to_replace.id)
|
|
2754
|
-
<< " to replace in bucket " << bucket_index);
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
return false;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
// Utility functions implementation
|
|
2761
|
-
NodeId string_to_node_id(const std::string& str) {
|
|
2762
|
-
NodeId id;
|
|
2763
|
-
size_t copy_size = (std::min)(str.size(), NODE_ID_SIZE);
|
|
2764
|
-
std::copy(str.begin(), str.begin() + copy_size, id.begin());
|
|
2765
|
-
return id;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
std::string node_id_to_string(const NodeId& id) {
|
|
2769
|
-
return std::string(id.begin(), id.end());
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
NodeId hex_to_node_id(const std::string& hex) {
|
|
2773
|
-
NodeId id;
|
|
2774
|
-
if (hex.size() != NODE_ID_SIZE * 2) {
|
|
2775
|
-
return id; // Return zero-filled ID on error
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
2779
|
-
std::string byte_str = hex.substr(i * 2, 2);
|
|
2780
|
-
id[i] = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
return id;
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
std::string node_id_to_hex(const NodeId& id) {
|
|
2787
|
-
std::ostringstream oss;
|
|
2788
|
-
oss << std::hex << std::setfill('0');
|
|
2789
|
-
for (uint8_t byte : id) {
|
|
2790
|
-
oss << std::setw(2) << static_cast<int>(byte);
|
|
2791
|
-
}
|
|
2792
|
-
return oss.str();
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
// Routing table persistence implementation
|
|
2796
|
-
std::string DhtClient::routing_table_file_path() const {
|
|
2797
|
-
// IPv6 instance uses a distinct filename suffix so it doesn't collide with the
|
|
2798
|
-
// IPv4 instance bound to the same port.
|
|
2799
|
-
const char* suffix = is_ipv6() ? "_v6" : "";
|
|
2800
|
-
#ifdef TESTING
|
|
2801
|
-
if (port_ == 0) {
|
|
2802
|
-
std::ostringstream oss;
|
|
2803
|
-
oss << "dht_routing_" << this << suffix << ".json";
|
|
2804
|
-
return oss.str();
|
|
2805
|
-
}
|
|
2806
|
-
return "dht_routing_" + std::to_string(port_) + suffix + ".json";
|
|
2807
|
-
#else
|
|
2808
|
-
return data_directory_ + "/dht_routing_" + std::to_string(port_) + suffix + ".json";
|
|
2809
|
-
#endif
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
bool DhtClient::save_routing_table() {
|
|
2813
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
2814
|
-
|
|
2815
|
-
try {
|
|
2816
|
-
nlohmann::json routing_data;
|
|
2817
|
-
routing_data["version"] = 1;
|
|
2818
|
-
routing_data["family"] = is_ipv6() ? "ipv6" : "ipv4";
|
|
2819
|
-
routing_data["node_id"] = node_id_to_hex(node_id_);
|
|
2820
|
-
routing_data["saved_at"] = std::chrono::system_clock::now().time_since_epoch().count();
|
|
2821
|
-
|
|
2822
|
-
nlohmann::json nodes_array = nlohmann::json::array();
|
|
2823
|
-
|
|
2824
|
-
// Save only good nodes (confirmed with fail_count == 0)
|
|
2825
|
-
size_t saved_count = 0;
|
|
2826
|
-
for (const auto& bucket : routing_table_) {
|
|
2827
|
-
for (const auto& node : bucket) {
|
|
2828
|
-
// Only save confirmed good nodes
|
|
2829
|
-
if (node.confirmed()) {
|
|
2830
|
-
nlohmann::json node_data;
|
|
2831
|
-
node_data["id"] = node_id_to_hex(node.id);
|
|
2832
|
-
node_data["ip"] = node.peer.ip;
|
|
2833
|
-
node_data["port"] = node.peer.port;
|
|
2834
|
-
|
|
2835
|
-
// Save RTT if known
|
|
2836
|
-
if (node.rtt != 0xffff) {
|
|
2837
|
-
node_data["rtt"] = node.rtt;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
nodes_array.push_back(node_data);
|
|
2841
|
-
saved_count++;
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
|
|
2846
|
-
routing_data["nodes"] = nodes_array;
|
|
2847
|
-
routing_data["count"] = saved_count;
|
|
2848
|
-
|
|
2849
|
-
// Determine file path
|
|
2850
|
-
std::string file_path = routing_table_file_path();
|
|
2851
|
-
|
|
2852
|
-
// Write to file
|
|
2853
|
-
std::ofstream file(file_path);
|
|
2854
|
-
if (!file.is_open()) {
|
|
2855
|
-
LOG_DHT_ERROR("Failed to open routing table file for writing: " << file_path);
|
|
2856
|
-
return false;
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
file << routing_data.dump(2);
|
|
2860
|
-
file.close();
|
|
2861
|
-
|
|
2862
|
-
LOG_DHT_DEBUG("Saved " << saved_count << " confirmed nodes to " << file_path);
|
|
2863
|
-
return true;
|
|
2864
|
-
|
|
2865
|
-
} catch (const std::exception& e) {
|
|
2866
|
-
LOG_DHT_ERROR("Exception while saving routing table: " << e.what());
|
|
2867
|
-
return false;
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
bool DhtClient::load_routing_table() {
|
|
2872
|
-
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
2873
|
-
|
|
2874
|
-
try {
|
|
2875
|
-
// Determine file path
|
|
2876
|
-
std::string file_path = routing_table_file_path();
|
|
2877
|
-
|
|
2878
|
-
// Check if file exists
|
|
2879
|
-
std::ifstream file(file_path);
|
|
2880
|
-
if (!file.is_open()) {
|
|
2881
|
-
LOG_DHT_DEBUG("No saved routing table found at " << file_path);
|
|
2882
|
-
return false;
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
// Parse JSON
|
|
2886
|
-
nlohmann::json routing_data;
|
|
2887
|
-
file >> routing_data;
|
|
2888
|
-
file.close();
|
|
2889
|
-
|
|
2890
|
-
// Validate format
|
|
2891
|
-
if (!routing_data.contains("version") || !routing_data.contains("nodes")) {
|
|
2892
|
-
LOG_DHT_WARN("Invalid routing table file format");
|
|
2893
|
-
return false;
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
int version = routing_data["version"];
|
|
2897
|
-
if (version != 1) {
|
|
2898
|
-
LOG_DHT_WARN("Unsupported routing table version: " << version);
|
|
2899
|
-
return false;
|
|
2900
|
-
}
|
|
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
|
-
|
|
2925
|
-
// Load nodes
|
|
2926
|
-
const auto& nodes_array = routing_data["nodes"];
|
|
2927
|
-
size_t loaded_count = 0;
|
|
2928
|
-
|
|
2929
|
-
for (const auto& node_data : nodes_array) {
|
|
2930
|
-
try {
|
|
2931
|
-
std::string node_id_hex = node_data["id"];
|
|
2932
|
-
std::string ip = node_data["ip"];
|
|
2933
|
-
int port = node_data["port"];
|
|
2934
|
-
|
|
2935
|
-
// Skip nodes that don't belong to this instance's family
|
|
2936
|
-
if (network_utils::is_valid_ipv6(ip) != is_ipv6()) {
|
|
2937
|
-
continue;
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
NodeId node_id = hex_to_node_id(node_id_hex);
|
|
2941
|
-
Peer peer(ip, port);
|
|
2942
|
-
DhtNode node(node_id, peer);
|
|
2943
|
-
|
|
2944
|
-
// Restore RTT if available
|
|
2945
|
-
if (node_data.contains("rtt")) {
|
|
2946
|
-
node.rtt = node_data["rtt"];
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// Mark as confirmed (fail_count = 0)
|
|
2950
|
-
node.fail_count = 0;
|
|
2951
|
-
|
|
2952
|
-
// Add to appropriate bucket
|
|
2953
|
-
int bucket_index = get_bucket_index(node.id);
|
|
2954
|
-
auto& bucket = routing_table_[bucket_index];
|
|
2955
|
-
|
|
2956
|
-
// Check if bucket has space
|
|
2957
|
-
if (bucket.size() < K_BUCKET_SIZE) {
|
|
2958
|
-
bucket.push_back(node);
|
|
2959
|
-
loaded_count++;
|
|
2960
|
-
} else {
|
|
2961
|
-
// Bucket full - try to replace a worse node
|
|
2962
|
-
auto worst_it = std::max_element(bucket.begin(), bucket.end(),
|
|
2963
|
-
[](const DhtNode& a, const DhtNode& b) {
|
|
2964
|
-
return a.is_worse_than(b);
|
|
2965
|
-
});
|
|
2966
|
-
|
|
2967
|
-
if (worst_it != bucket.end() && worst_it->is_worse_than(node)) {
|
|
2968
|
-
*worst_it = node;
|
|
2969
|
-
loaded_count++;
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
} catch (const std::exception& e) {
|
|
2974
|
-
LOG_DHT_WARN("Failed to load node from routing table: " << e.what());
|
|
2975
|
-
continue;
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
LOG_DHT_INFO("Loaded " << loaded_count << " nodes from routing table file");
|
|
2980
|
-
return loaded_count > 0;
|
|
2981
|
-
|
|
2982
|
-
} catch (const std::exception& e) {
|
|
2983
|
-
LOG_DHT_ERROR("Exception while loading routing table: " << e.what());
|
|
2984
|
-
return false;
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
void DhtClient::set_data_directory(const std::string& directory) {
|
|
2989
|
-
data_directory_ = directory;
|
|
2990
|
-
if (data_directory_.empty()) {
|
|
2991
|
-
data_directory_ = ".";
|
|
2992
|
-
}
|
|
2993
|
-
LOG_DHT_DEBUG("Data directory set to: " << data_directory_);
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
#ifdef RATS_SEARCH_FEATURES
|
|
2997
|
-
// ============================================================================
|
|
2998
|
-
// SPIDER MODE IMPLEMENTATION
|
|
2999
|
-
// ============================================================================
|
|
3000
|
-
|
|
3001
|
-
void DhtClient::set_spider_mode(bool enable) {
|
|
3002
|
-
bool was_enabled = spider_mode_.exchange(enable);
|
|
3003
|
-
|
|
3004
|
-
if (enable && !was_enabled) {
|
|
3005
|
-
LOG_DHT_INFO("Spider mode ENABLED - aggressive node discovery active (separate node pool)");
|
|
3006
|
-
|
|
3007
|
-
// Initialize spider pool with nodes from routing table
|
|
3008
|
-
// Lock order: routing_table_mutex_ (3) -> spider_nodes_mutex_ (4)
|
|
3009
|
-
std::lock_guard<std::mutex> rt_lock(routing_table_mutex_);
|
|
3010
|
-
std::lock_guard<std::mutex> spider_lock(spider_nodes_mutex_);
|
|
3011
|
-
|
|
3012
|
-
spider_nodes_.clear();
|
|
3013
|
-
spider_visited_.clear();
|
|
3014
|
-
|
|
3015
|
-
// Seed spider pool with current routing table nodes
|
|
3016
|
-
for (const auto& bucket : routing_table_) {
|
|
3017
|
-
for (const auto& node : bucket) {
|
|
3018
|
-
if (spider_nodes_.size() < MAX_SPIDER_NODES) {
|
|
3019
|
-
spider_nodes_.push_back(node);
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
LOG_DHT_INFO("Spider pool initialized with " << spider_nodes_.size() << " nodes from routing table");
|
|
3025
|
-
|
|
3026
|
-
} else if (!enable && was_enabled) {
|
|
3027
|
-
LOG_DHT_INFO("Spider mode DISABLED - normal DHT operation");
|
|
3028
|
-
|
|
3029
|
-
// Clear spider state
|
|
3030
|
-
cleanup_spider_state();
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
void DhtClient::add_spider_node(const DhtNode& node) {
|
|
3035
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3036
|
-
|
|
3037
|
-
// Check if node already exists in pool
|
|
3038
|
-
auto it = std::find_if(spider_nodes_.begin(), spider_nodes_.end(),
|
|
3039
|
-
[&node](const DhtNode& existing) {
|
|
3040
|
-
return existing.id == node.id;
|
|
3041
|
-
});
|
|
3042
|
-
|
|
3043
|
-
if (it != spider_nodes_.end()) {
|
|
3044
|
-
// Update existing node
|
|
3045
|
-
it->peer = node.peer;
|
|
3046
|
-
it->last_seen = std::chrono::steady_clock::now();
|
|
3047
|
-
return;
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
// Add new node if pool isn't full
|
|
3051
|
-
if (spider_nodes_.size() < MAX_SPIDER_NODES) {
|
|
3052
|
-
spider_nodes_.push_back(node);
|
|
3053
|
-
LOG_DHT_DEBUG("Added spider node " << node_id_to_hex(node.id) << " at "
|
|
3054
|
-
<< node.peer.ip << ":" << node.peer.port
|
|
3055
|
-
<< " (pool size: " << spider_nodes_.size() << ")");
|
|
3056
|
-
} else {
|
|
3057
|
-
// Replace a random node (simple eviction strategy)
|
|
3058
|
-
static std::random_device rd;
|
|
3059
|
-
static std::mt19937 gen(rd());
|
|
3060
|
-
std::uniform_int_distribution<size_t> dis(0, spider_nodes_.size() - 1);
|
|
3061
|
-
size_t replace_idx = dis(gen);
|
|
3062
|
-
spider_nodes_[replace_idx] = node;
|
|
3063
|
-
LOG_DHT_DEBUG("Replaced spider node at index " << replace_idx << " with "
|
|
3064
|
-
<< node_id_to_hex(node.id));
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
|
-
void DhtClient::add_spider_nodes(const std::vector<DhtNode>& nodes) {
|
|
3069
|
-
for (const auto& node : nodes) {
|
|
3070
|
-
add_spider_node(node);
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
|
-
bool DhtClient::is_spider_node_visited(const NodeId& id) const {
|
|
3075
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3076
|
-
return spider_visited_.find(id) != spider_visited_.end();
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
void DhtClient::mark_spider_node_visited(const NodeId& id) {
|
|
3080
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3081
|
-
|
|
3082
|
-
// Clean up if too many visited nodes
|
|
3083
|
-
if (spider_visited_.size() >= MAX_SPIDER_VISITED) {
|
|
3084
|
-
// Clear half of the visited set (simple eviction)
|
|
3085
|
-
auto it = spider_visited_.begin();
|
|
3086
|
-
std::advance(it, spider_visited_.size() / 2);
|
|
3087
|
-
spider_visited_.erase(spider_visited_.begin(), it);
|
|
3088
|
-
LOG_DHT_DEBUG("Cleaned up spider visited set, now " << spider_visited_.size() << " entries");
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
spider_visited_.insert(id);
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
void DhtClient::cleanup_spider_state() {
|
|
3095
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3096
|
-
|
|
3097
|
-
size_t nodes_count = spider_nodes_.size();
|
|
3098
|
-
size_t visited_count = spider_visited_.size();
|
|
3099
|
-
size_t transactions_count = spider_transactions_.size();
|
|
3100
|
-
size_t contacted_ips_count = spider_contacted_ips_.size();
|
|
3101
|
-
|
|
3102
|
-
spider_nodes_.clear();
|
|
3103
|
-
spider_visited_.clear();
|
|
3104
|
-
spider_transactions_.clear();
|
|
3105
|
-
spider_contacted_ips_.clear();
|
|
3106
|
-
|
|
3107
|
-
LOG_DHT_DEBUG("Cleaned up spider state: " << nodes_count << " nodes, "
|
|
3108
|
-
<< visited_count << " visited entries, "
|
|
3109
|
-
<< transactions_count << " pending transactions, "
|
|
3110
|
-
<< contacted_ips_count << " contacted IPs cleared");
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
void DhtClient::cleanup_stale_spider_transactions() {
|
|
3114
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3115
|
-
|
|
3116
|
-
auto now = std::chrono::steady_clock::now();
|
|
3117
|
-
auto timeout = std::chrono::seconds(60); // 60 second timeout for spider transactions
|
|
3118
|
-
|
|
3119
|
-
size_t removed = 0;
|
|
3120
|
-
auto it = spider_transactions_.begin();
|
|
3121
|
-
while (it != spider_transactions_.end()) {
|
|
3122
|
-
if (now - it->second > timeout) {
|
|
3123
|
-
it = spider_transactions_.erase(it);
|
|
3124
|
-
removed++;
|
|
3125
|
-
} else {
|
|
3126
|
-
++it;
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
if (removed > 0) {
|
|
3131
|
-
LOG_DHT_DEBUG("Cleaned up " << removed << " stale spider transactions");
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
void DhtClient::mark_spider_transaction(const std::string& transaction_id) {
|
|
3136
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3137
|
-
spider_transactions_[transaction_id] = std::chrono::steady_clock::now();
|
|
3138
|
-
}
|
|
3139
|
-
|
|
3140
|
-
bool DhtClient::is_spider_transaction(const std::string& transaction_id) {
|
|
3141
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3142
|
-
auto it = spider_transactions_.find(transaction_id);
|
|
3143
|
-
if (it != spider_transactions_.end()) {
|
|
3144
|
-
spider_transactions_.erase(it); // Remove after checking - one-time use
|
|
3145
|
-
return true;
|
|
3146
|
-
}
|
|
3147
|
-
return false;
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
void DhtClient::mark_spider_contacted_ip(const std::string& ip) {
|
|
3151
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3152
|
-
|
|
3153
|
-
// Limit size to avoid unbounded growth
|
|
3154
|
-
if (spider_contacted_ips_.size() >= MAX_SPIDER_CONTACTED_IPS) {
|
|
3155
|
-
// Clear half of the set (simple eviction)
|
|
3156
|
-
auto it = spider_contacted_ips_.begin();
|
|
3157
|
-
std::advance(it, spider_contacted_ips_.size() / 2);
|
|
3158
|
-
spider_contacted_ips_.erase(spider_contacted_ips_.begin(), it);
|
|
3159
|
-
LOG_DHT_DEBUG("Cleaned up spider contacted IPs, now " << spider_contacted_ips_.size() << " entries");
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
spider_contacted_ips_.insert(ip);
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
bool DhtClient::is_spider_contacted_ip(const std::string& ip) const {
|
|
3166
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3167
|
-
return spider_contacted_ips_.count(ip) > 0;
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
size_t DhtClient::get_spider_pool_size() const {
|
|
3171
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3172
|
-
return spider_nodes_.size();
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
size_t DhtClient::get_spider_visited_count() const {
|
|
3176
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3177
|
-
return spider_visited_.size();
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
void DhtClient::clear_spider_state() {
|
|
3181
|
-
cleanup_spider_state();
|
|
3182
|
-
}
|
|
3183
|
-
|
|
3184
|
-
void DhtClient::set_spider_announce_callback(SpiderAnnounceCallback callback) {
|
|
3185
|
-
std::lock_guard<std::mutex> lock(spider_callbacks_mutex_);
|
|
3186
|
-
spider_announce_callback_ = std::move(callback);
|
|
3187
|
-
LOG_DHT_DEBUG("Spider announce callback set");
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
void DhtClient::set_spider_ignore(bool ignore) {
|
|
3191
|
-
spider_ignore_.store(ignore);
|
|
3192
|
-
LOG_DHT_DEBUG("Spider ignore mode set to " << (ignore ? "true" : "false"));
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
void DhtClient::spider_walk() {
|
|
3196
|
-
if (!running_) {
|
|
3197
|
-
return;
|
|
3198
|
-
}
|
|
3199
|
-
|
|
3200
|
-
// Get a random unvisited node from the spider pool and send find_node
|
|
3201
|
-
DhtNode target_node;
|
|
3202
|
-
bool found = false;
|
|
3203
|
-
size_t pool_size = 0; // Cache sizes under lock for logging
|
|
3204
|
-
size_t visited_size = 0;
|
|
3205
|
-
|
|
3206
|
-
{
|
|
3207
|
-
std::lock_guard<std::mutex> lock(spider_nodes_mutex_);
|
|
3208
|
-
|
|
3209
|
-
if (!spider_nodes_.empty()) {
|
|
3210
|
-
// Try to find an unvisited node (up to 10 attempts)
|
|
3211
|
-
static std::random_device rd;
|
|
3212
|
-
static std::mt19937 gen(rd());
|
|
3213
|
-
|
|
3214
|
-
for (int attempt = 0; attempt < 10 && !found; ++attempt) {
|
|
3215
|
-
std::uniform_int_distribution<size_t> dis(0, spider_nodes_.size() - 1);
|
|
3216
|
-
size_t idx = dis(gen);
|
|
3217
|
-
|
|
3218
|
-
const auto& node = spider_nodes_[idx];
|
|
3219
|
-
if (spider_visited_.find(node.id) == spider_visited_.end()) {
|
|
3220
|
-
target_node = node;
|
|
3221
|
-
spider_visited_.insert(node.id);
|
|
3222
|
-
found = true;
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
// If all sampled nodes are visited, just pick any random one
|
|
3227
|
-
// This ensures we keep walking even when most nodes are visited
|
|
3228
|
-
if (!found && !spider_nodes_.empty()) {
|
|
3229
|
-
std::uniform_int_distribution<size_t> dis(0, spider_nodes_.size() - 1);
|
|
3230
|
-
target_node = spider_nodes_[dis(gen)];
|
|
3231
|
-
found = true;
|
|
3232
|
-
LOG_DHT_DEBUG("Spider walk: all sampled nodes visited, reusing node");
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
// Clean up visited set if it's getting too large
|
|
3237
|
-
if (spider_visited_.size() >= MAX_SPIDER_VISITED) {
|
|
3238
|
-
auto it = spider_visited_.begin();
|
|
3239
|
-
std::advance(it, spider_visited_.size() / 2);
|
|
3240
|
-
spider_visited_.erase(spider_visited_.begin(), it);
|
|
3241
|
-
LOG_DHT_DEBUG("Spider walk: cleaned up visited set to " << spider_visited_.size() << " entries");
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
// Cache sizes for logging outside the lock
|
|
3245
|
-
pool_size = spider_nodes_.size();
|
|
3246
|
-
visited_size = spider_visited_.size();
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
if (found) {
|
|
3250
|
-
// Spider optimization: use neighbor_id to appear close to the target node
|
|
3251
|
-
// This makes us look like we're in the same neighborhood as the node we're querying
|
|
3252
|
-
NodeId query_id = neighbor_id(target_node.id);
|
|
3253
|
-
|
|
3254
|
-
// Generate a random target for find_node
|
|
3255
|
-
NodeId random_target = generate_node_id();
|
|
3256
|
-
|
|
3257
|
-
// Send find_node with our neighbor_id (pretending to be close to target_node)
|
|
3258
|
-
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
3259
|
-
|
|
3260
|
-
// Mark this transaction as spider - responses will go to spider pool
|
|
3261
|
-
// even if spider_mode_ is disabled before response arrives
|
|
3262
|
-
mark_spider_transaction(transaction_id);
|
|
3263
|
-
|
|
3264
|
-
// Mark this IP as spider-contacted - we'll respond with neighbor_id to requests from this IP
|
|
3265
|
-
mark_spider_contacted_ip(target_node.peer.ip);
|
|
3266
|
-
|
|
3267
|
-
auto message = KrpcProtocol::create_find_node_query(transaction_id, query_id, random_target);
|
|
3268
|
-
send_krpc_message(message, target_node.peer);
|
|
3269
|
-
|
|
3270
|
-
LOG_DHT_DEBUG("Spider walk: sent find_node to " << target_node.peer.ip << ":" << target_node.peer.port
|
|
3271
|
-
<< " with neighbor_id (pool: " << pool_size << ", visited: " << visited_size << ")");
|
|
3272
|
-
} else {
|
|
3273
|
-
// No nodes in spider pool, seed from routing table or bootstrap
|
|
3274
|
-
LOG_DHT_DEBUG("Spider walk: no nodes in spider pool, seeding...");
|
|
3275
|
-
|
|
3276
|
-
// First try to seed from routing table
|
|
3277
|
-
// Lock order: routing_table_mutex_ (3) -> spider_nodes_mutex_ (4)
|
|
3278
|
-
{
|
|
3279
|
-
std::lock_guard<std::mutex> rt_lock(routing_table_mutex_);
|
|
3280
|
-
std::lock_guard<std::mutex> spider_lock(spider_nodes_mutex_);
|
|
3281
|
-
|
|
3282
|
-
for (const auto& bucket : routing_table_) {
|
|
3283
|
-
for (const auto& node : bucket) {
|
|
3284
|
-
if (spider_nodes_.size() < MAX_SPIDER_NODES) {
|
|
3285
|
-
spider_nodes_.push_back(node);
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
// Cache size for check outside the lock
|
|
3291
|
-
pool_size = spider_nodes_.size();
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
// If still empty, bootstrap (rate-limited to once per 30 seconds)
|
|
3295
|
-
if (pool_size == 0) {
|
|
3296
|
-
auto now = std::chrono::steady_clock::now();
|
|
3297
|
-
if (now - last_spider_bootstrap_ >= std::chrono::seconds(30)) {
|
|
3298
|
-
last_spider_bootstrap_ = now;
|
|
3299
|
-
LOG_DHT_DEBUG("Spider walk: routing table empty too, re-bootstrapping");
|
|
3300
|
-
for (const auto& bootstrap : get_default_bootstrap_nodes()) {
|
|
3301
|
-
send_krpc_find_node(bootstrap, node_id_);
|
|
3302
|
-
}
|
|
3303
|
-
} else {
|
|
3304
|
-
LOG_DHT_DEBUG("Spider walk: skipping re-bootstrap (rate limited, last was < 30s ago)");
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
#endif // RATS_SEARCH_FEATURES
|
|
3310
|
-
|
|
3311
|
-
} // namespace librats
|