librats 0.7.1 → 0.8.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.
Files changed (42) hide show
  1. package/native-src/CMakeLists.txt +15 -2
  2. package/native-src/src/bt_client.cpp +18 -5
  3. package/native-src/src/bt_client.h +3 -1
  4. package/native-src/src/bt_network.cpp +297 -184
  5. package/native-src/src/bt_network.h +25 -5
  6. package/native-src/src/bt_peer_connection.cpp +11 -0
  7. package/native-src/src/bt_peer_connection.h +6 -0
  8. package/native-src/src/bt_torrent.cpp +17 -6
  9. package/native-src/src/bt_torrent.h +1 -0
  10. package/native-src/src/bt_types.h +287 -20
  11. package/native-src/src/dht.cpp +26 -8
  12. package/native-src/src/dht.h +1 -1
  13. package/native-src/src/ice.cpp +1 -1
  14. package/native-src/src/io_poller.cpp +910 -0
  15. package/native-src/src/io_poller.h +138 -0
  16. package/native-src/src/krpc.cpp +8 -1
  17. package/native-src/src/krpc.h +3 -2
  18. package/native-src/src/librats.cpp +824 -1213
  19. package/native-src/src/librats.h +167 -54
  20. package/native-src/src/librats_bittorrent.cpp +4 -7
  21. package/native-src/src/librats_c.cpp +13 -1
  22. package/native-src/src/librats_c.h +2 -0
  23. package/native-src/src/librats_discovery.cpp +309 -0
  24. package/native-src/src/librats_encryption.cpp +130 -283
  25. package/native-src/src/librats_file_transfer.cpp +2 -6
  26. package/native-src/src/librats_gossipsub.cpp +1 -5
  27. package/native-src/src/librats_ice.cpp +1 -1
  28. package/native-src/src/librats_log_macros.h +36 -0
  29. package/native-src/src/librats_logging.cpp +10 -7
  30. package/native-src/src/librats_mdns.cpp +1 -11
  31. package/native-src/src/librats_persistence.cpp +1 -11
  32. package/native-src/src/librats_reconnection.cpp +2 -13
  33. package/native-src/src/librats_statistic.cpp +97 -0
  34. package/native-src/src/logger.h +40 -4
  35. package/native-src/src/network_utils.cpp +175 -373
  36. package/native-src/src/network_utils.h +2 -113
  37. package/native-src/src/socket.cpp +485 -883
  38. package/native-src/src/socket.h +36 -130
  39. package/native-src/src/stun.cpp +3 -5
  40. package/native-src/src/tracker.cpp +3 -2
  41. package/native-src/src/turn.cpp +4 -6
  42. package/package.json +1 -1
@@ -5,12 +5,14 @@
5
5
  * @brief Async network layer for BitTorrent peer connections
6
6
  *
7
7
  * Provides efficient multiplexed I/O for managing multiple peer connections
8
- * with non-blocking sockets. Buffers are owned by BtPeerConnection (single
9
- * source of truth) - this class only handles socket I/O.
8
+ * using platform-optimal I/O polling (epoll/kqueue/WSAPoll).
9
+ * Buffers are owned by BtPeerConnection (single source of truth) -
10
+ * this class only handles socket I/O.
10
11
  */
11
12
 
12
13
  #include "bt_types.h"
13
14
  #include "bt_peer_connection.h"
15
+ #include "io_poller.h"
14
16
  #include "socket.h"
15
17
 
16
18
  #include <vector>
@@ -37,7 +39,7 @@ struct BtNetworkConfig {
37
39
  size_t max_connections; ///< Maximum total connections
38
40
  bool enable_incoming; ///< Accept incoming connections
39
41
  int connect_timeout_ms; ///< Timeout for outgoing connections (ms)
40
- int select_timeout_ms; ///< Timeout for select() call (ms)
42
+ int poll_timeout_ms; ///< Timeout for I/O poll (ms)
41
43
  size_t send_buffer_high_water; ///< High water mark for send buffer
42
44
  PeerID peer_id; ///< Our peer ID (for incoming connections)
43
45
 
@@ -46,7 +48,7 @@ struct BtNetworkConfig {
46
48
  , max_connections(200)
47
49
  , enable_incoming(true)
48
50
  , connect_timeout_ms(30000)
49
- , select_timeout_ms(15)
51
+ , poll_timeout_ms(15)
50
52
  , send_buffer_high_water(1024 * 1024) // 1MB
51
53
  , peer_id{} {}
52
54
  };
@@ -150,10 +152,16 @@ struct DisconnectedEvent {
150
152
  /**
151
153
  * @brief Manages all BitTorrent peer connections
152
154
  *
153
- * Provides async I/O via select() multiplexing:
155
+ * Provides async I/O via platform-optimal multiplexing:
156
+ * - Linux: epoll (O(1) per event)
157
+ * - macOS: kqueue (O(1) per event)
158
+ * - Windows: WSAPoll (no FD_SETSIZE limit)
159
+ *
160
+ * Features:
154
161
  * - Listens for incoming connections
155
162
  * - Manages outgoing connection queue
156
163
  * - Handles non-blocking reads/writes
164
+ * - TCP_NODELAY for low-latency messaging
157
165
  * - Invokes callbacks for events
158
166
  *
159
167
  * Thread-safety: The manager runs its own I/O thread.
@@ -328,6 +336,9 @@ private:
328
336
  /// Main I/O loop (runs in separate thread)
329
337
  void io_loop();
330
338
 
339
+ /// Synchronize poller state with current connections (under mutex)
340
+ void sync_poller();
341
+
331
342
  /// Process the queue of pending connections
332
343
  void process_pending_connects();
333
344
 
@@ -360,6 +371,9 @@ private:
360
371
  /// Handle info_hash discovered from incoming connection handshake
361
372
  void on_incoming_info_hash(BtPeerConnection* conn, const BtInfoHash& info_hash);
362
373
 
374
+ /// Set TCP_NODELAY on a socket (disable Nagle's algorithm)
375
+ static bool set_tcp_nodelay(socket_t sock);
376
+
363
377
  //=========================================================================
364
378
  // Data Members
365
379
  //=========================================================================
@@ -373,6 +387,12 @@ private:
373
387
 
374
388
  mutable std::mutex mutex_;
375
389
 
390
+ /// Platform-optimal I/O poller (epoll/kqueue/WSAPoll)
391
+ std::unique_ptr<IOPoller> poller_;
392
+
393
+ /// Tracks current poller event mask per socket (to avoid redundant modify calls)
394
+ std::unordered_map<socket_t, uint32_t> poller_state_;
395
+
376
396
  /// Active connections by socket
377
397
  std::unordered_map<socket_t, SocketContext> connections_;
378
398
 
@@ -358,6 +358,9 @@ void BtPeerConnection::process_handshake() {
358
358
 
359
359
  handshake_received_ = true;
360
360
 
361
+ // Identify client from peer_id
362
+ peer_client_name_ = identify_client(peer_id_);
363
+
361
364
  // Format peer_id for logging (first 8 chars, printable only)
362
365
  std::string peer_id_str;
363
366
  for (size_t i = 0; i < 8 && i < peer_id_.size(); ++i) {
@@ -366,6 +369,7 @@ void BtPeerConnection::process_handshake() {
366
369
  }
367
370
 
368
371
  LOG_INFO("BtPeerConn", "Handshake received from " + ip_ + ", peer_id=" + peer_id_str +
372
+ ", client=" + peer_client_name_ +
369
373
  ", ext_protocol=" + (hs->extensions.extension_protocol ? "yes" : "no") +
370
374
  ", fast=" + (hs->extensions.fast ? "yes" : "no"));
371
375
 
@@ -604,6 +608,13 @@ void BtPeerConnection::parse_extension_handshake(const std::vector<uint8_t>& pay
604
608
  std::to_string(peer_ut_metadata_id_));
605
609
  }
606
610
  }
611
+
612
+ // Extract client version string from 'v' field
613
+ auto v_it = dict.find("v");
614
+ if (v_it != dict.end() && v_it->second.is_string()) {
615
+ peer_client_name_ = v_it->second.as_string();
616
+ LOG_INFO("BtPeerConn", "Peer " + ip_ + " client: " + peer_client_name_);
617
+ }
607
618
  }
608
619
 
609
620
  void BtPeerConnection::queue_send(const std::vector<uint8_t>& data) {
@@ -355,6 +355,11 @@ public:
355
355
  */
356
356
  uint8_t peer_ut_metadata_id() const { return peer_ut_metadata_id_; }
357
357
 
358
+ /**
359
+ * @brief Get peer's client name (from extension handshake "v" field, or identified from peer_id)
360
+ */
361
+ const std::string& peer_client_name() const { return peer_client_name_; }
362
+
358
363
  //=========================================================================
359
364
  // Sending Messages
360
365
  //=========================================================================
@@ -549,6 +554,7 @@ private:
549
554
  bool extension_handshake_received_;
550
555
  size_t peer_metadata_size_;
551
556
  uint8_t peer_ut_metadata_id_;
557
+ std::string peer_client_name_; ///< Client name (from "v" field or peer_id identification)
552
558
 
553
559
  // Protocol state
554
560
  bool am_choking_;
@@ -135,7 +135,7 @@ Torrent::Torrent(const BtInfoHash& info_hash,
135
135
  , have_pieces_(0) {
136
136
 
137
137
  LOG_INFO("Torrent", "Creating torrent '" + name + "' from magnet link (no metadata yet), info_hash=" +
138
- info_hash_to_hex(info_hash).substr(0, 16) + "...");
138
+ info_hash_to_hex(info_hash));
139
139
 
140
140
  // No picker until we have metadata
141
141
 
@@ -233,6 +233,7 @@ void Torrent::stop() {
233
233
  // Clear metadata exchange state
234
234
  metadata_buffer_.clear();
235
235
  metadata_pieces_received_.clear();
236
+ metadata_pieces_requested_.clear();
236
237
  peer_metadata_size_.clear();
237
238
  peer_ut_metadata_id_.clear();
238
239
 
@@ -725,6 +726,7 @@ void Torrent::tick() {
725
726
  // Clear metadata state
726
727
  metadata_buffer_.clear();
727
728
  metadata_pieces_received_.clear();
729
+ metadata_pieces_requested_.clear();
728
730
  peer_metadata_size_.clear();
729
731
  peer_ut_metadata_id_.clear();
730
732
 
@@ -778,8 +780,9 @@ void Torrent::tick() {
778
780
  //=============================================================================
779
781
 
780
782
  void Torrent::on_peer_connected(BtPeerConnection* peer) {
781
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected, has_metadata=" +
782
- (has_metadata_unlocked() ? "true" : "false"));
783
+ LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected" +
784
+ (peer->peer_client_name().empty() ? "" : " [" + peer->peer_client_name() + "]") +
785
+ ", has_metadata=" + (has_metadata_unlocked() ? "true" : "false"));
783
786
 
784
787
  // Send extension handshake if peer supports it
785
788
  if (peer->peer_extensions().extension_protocol) {
@@ -1506,11 +1509,15 @@ void Torrent::request_metadata(BtPeerConnection* peer) {
1506
1509
  if (metadata_buffer_.empty()) {
1507
1510
  metadata_buffer_.resize(metadata_size);
1508
1511
  metadata_pieces_received_.resize(num_pieces, false);
1512
+ metadata_pieces_requested_.resize(num_pieces, false);
1509
1513
  }
1514
+
1515
+ // Request pieces we don't have yet (pipeline up to 4 requests)
1516
+ int requests_sent = 0;
1517
+ const int max_pending_requests = 4;
1510
1518
 
1511
- // Request pieces we don't have yet
1512
1519
  for (uint32_t piece = 0; piece < num_pieces; ++piece) {
1513
- if (!metadata_pieces_received_[piece]) {
1520
+ if (!metadata_pieces_received_[piece] && !metadata_pieces_requested_[piece]) {
1514
1521
  // Create request message
1515
1522
  BencodeValue req = BencodeValue::create_dict();
1516
1523
  req["msg_type"] = BencodeValue(static_cast<int64_t>(0)); // Request
@@ -1518,10 +1525,11 @@ void Torrent::request_metadata(BtPeerConnection* peer) {
1518
1525
 
1519
1526
  auto req_payload = req.encode();
1520
1527
  peer->send_extended(peer_ut_id, req_payload);
1528
+ metadata_pieces_requested_[piece] = true;
1521
1529
 
1522
1530
  LOG_DEBUG("Torrent", "Requesting metadata piece " + std::to_string(piece) +
1523
1531
  " from " + peer->ip());
1524
- break; // Request one piece at a time for now
1532
+ if (++requests_sent >= max_pending_requests) break;
1525
1533
  }
1526
1534
  }
1527
1535
  }
@@ -1580,6 +1588,7 @@ void Torrent::on_metadata_message(BtPeerConnection* peer,
1580
1588
 
1581
1589
  if (piece < metadata_pieces_received_.size()) {
1582
1590
  metadata_pieces_received_[piece] = true;
1591
+ metadata_pieces_requested_[piece] = false;
1583
1592
  }
1584
1593
 
1585
1594
  LOG_DEBUG("Torrent", "Received metadata piece " + std::to_string(piece) +
@@ -1647,6 +1656,7 @@ void Torrent::on_metadata_complete() {
1647
1656
  // Clear metadata tracking data
1648
1657
  metadata_buffer_.clear();
1649
1658
  metadata_pieces_received_.clear();
1659
+ metadata_pieces_requested_.clear();
1650
1660
  peer_metadata_size_.clear();
1651
1661
  peer_ut_metadata_id_.clear();
1652
1662
 
@@ -1672,6 +1682,7 @@ void Torrent::on_metadata_complete() {
1672
1682
  // Could retry from other peers
1673
1683
  metadata_buffer_.clear();
1674
1684
  metadata_pieces_received_.clear();
1685
+ metadata_pieces_requested_.clear();
1675
1686
  }
1676
1687
  }
1677
1688
 
@@ -631,6 +631,7 @@ private:
631
631
  // Metadata exchange (for magnet links)
632
632
  std::vector<uint8_t> metadata_buffer_;
633
633
  std::vector<bool> metadata_pieces_received_;
634
+ std::vector<bool> metadata_pieces_requested_;
634
635
  std::unordered_map<BtPeerConnection*, size_t> peer_metadata_size_;
635
636
  std::unordered_map<BtPeerConnection*, uint8_t> peer_ut_metadata_id_;
636
637
  std::chrono::steady_clock::time_point metadata_download_started_;
@@ -10,6 +10,8 @@
10
10
 
11
11
  #include <array>
12
12
  #include <cstdint>
13
+ #include <cstring>
14
+ #include <cstdio>
13
15
  #include <string>
14
16
  #include <random>
15
17
  #include <chrono>
@@ -270,39 +272,304 @@ inline bool is_zero_hash(const BtInfoHash& hash) {
270
272
  }
271
273
 
272
274
  //=============================================================================
273
- // Client ID Parsing
275
+ // Client ID Parsing (BEP 20)
274
276
  //=============================================================================
275
277
 
278
+ namespace detail {
279
+
280
+ /**
281
+ * @brief Decode a version digit from peer ID (supports 0-9 and A-Z)
282
+ */
283
+ inline int decode_version_digit(uint8_t c) {
284
+ if (c >= '0' && c <= '9') return c - '0';
285
+ if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
286
+ if (c >= 'a' && c <= 'z') return c - 'a' + 10;
287
+ return 0;
288
+ }
289
+
276
290
  /**
277
- * @brief Extract client name from peer ID (BEP 20)
291
+ * @brief Azureus-style client name lookup table
292
+ * Based on BEP 20 and libtorrent's identify_client implementation
293
+ */
294
+ inline const char* lookup_az_client(const char* code) {
295
+ // Sorted alphabetically by 2-char code for binary search potential
296
+ // Using linear search for simplicity - table is small enough
297
+ struct Entry { const char* code; const char* name; };
298
+ static const Entry entries[] = {
299
+ {"7T", "aTorrent"},
300
+ {"AB", "AnyEvent BitTorrent"},
301
+ {"AG", "Ares"},
302
+ {"AR", "Arctic Torrent"},
303
+ {"AT", "Artemis"},
304
+ {"AV", "Avicora"},
305
+ {"AX", "BitPump"},
306
+ {"AZ", "Azureus"},
307
+ {"A~", "Ares"},
308
+ {"BB", "BitBuddy"},
309
+ {"BC", "BitComet"},
310
+ {"BE", "baretorrent"},
311
+ {"BF", "Bitflu"},
312
+ {"BG", "BTG"},
313
+ {"BI", "BiglyBT"},
314
+ {"BL", "BitBlinder"},
315
+ {"BP", "BitTorrent Pro"},
316
+ {"BR", "BitRocket"},
317
+ {"BS", "BTSlave"},
318
+ {"BT", "BitTorrent"},
319
+ {"BU", "BigUp"},
320
+ {"BW", "BitWombat"},
321
+ {"BX", "BittorrentX"},
322
+ {"CD", "Enhanced CTorrent"},
323
+ {"CT", "CTorrent"},
324
+ {"DE", "Deluge"},
325
+ {"DP", "Propagate Data Client"},
326
+ {"EB", "EBit"},
327
+ {"ES", "electric sheep"},
328
+ {"FC", "FileCroc"},
329
+ {"FT", "FoxTorrent"},
330
+ {"FW", "FrostWire"},
331
+ {"FX", "Freebox BitTorrent"},
332
+ {"GS", "GSTorrent"},
333
+ {"HK", "Hekate"},
334
+ {"HL", "Halite"},
335
+ {"HN", "Hydranode"},
336
+ {"IL", "iLivid"},
337
+ {"KC", "Koinonein"},
338
+ {"KG", "KGet"},
339
+ {"KT", "KTorrent"},
340
+ {"LC", "LeechCraft"},
341
+ {"LH", "LH-ABC"},
342
+ {"LK", "Linkage"},
343
+ {"LP", "lphant"},
344
+ {"LR", "librats"},
345
+ {"LT", "libtorrent"},
346
+ {"LW", "Limewire"},
347
+ {"ML", "MLDonkey"},
348
+ {"MO", "Mono Torrent"},
349
+ {"MP", "MooPolice"},
350
+ {"MR", "Miro"},
351
+ {"MT", "Moonlight Torrent"},
352
+ {"NX", "Net Transport"},
353
+ {"OS", "OneSwarm"},
354
+ {"OT", "OmegaTorrent"},
355
+ {"PD", "Pando"},
356
+ {"QD", "QQDownload"},
357
+ {"QT", "Qt 4"},
358
+ {"RT", "Retriever"},
359
+ {"RZ", "RezTorrent"},
360
+ {"SB", "Swiftbit"},
361
+ {"SD", "Xunlei"},
362
+ {"SK", "spark"},
363
+ {"SN", "ShareNet"},
364
+ {"SS", "SwarmScope"},
365
+ {"ST", "SymTorrent"},
366
+ {"SZ", "Shareaza"},
367
+ {"S~", "Shareaza (beta)"},
368
+ {"TB", "Torch"},
369
+ {"TL", "Tribler"},
370
+ {"TN", "Torrent.NET"},
371
+ {"TR", "Transmission"},
372
+ {"TS", "TorrentStorm"},
373
+ {"TT", "TuoTu"},
374
+ {"UL", "uLeecher"},
375
+ {"UM", "uTorrent Mac"},
376
+ {"UT", "uTorrent"},
377
+ {"VG", "Vagaa"},
378
+ {"WT", "BitLet"},
379
+ {"WY", "FireTorrent"},
380
+ {"XF", "Xfplay"},
381
+ {"XL", "Xunlei"},
382
+ {"XS", "XSwifter"},
383
+ {"XT", "XanTorrent"},
384
+ {"XX", "Xtorrent"},
385
+ {"ZO", "Zona"},
386
+ {"ZT", "ZipTorrent"},
387
+ {"lt", "rTorrent"},
388
+ {"pX", "pHoeniX"},
389
+ {"qB", "qBittorrent"},
390
+ {"st", "SharkTorrent"},
391
+ };
392
+
393
+ for (const auto& e : entries) {
394
+ if (e.code[0] == code[0] && e.code[1] == code[1]) {
395
+ return e.name;
396
+ }
397
+ }
398
+ return nullptr;
399
+ }
400
+
401
+ /**
402
+ * @brief Generic (non-standard) client name lookup
403
+ */
404
+ inline const char* lookup_generic_client(const uint8_t* id) {
405
+ struct Entry { int offset; const char* pattern; const char* name; };
406
+ static const Entry entries[] = {
407
+ {0, "Deadman Walking-", "Deadman"},
408
+ {5, "Azureus", "Azureus 2.0.3.2"},
409
+ {0, "DansClient", "XanTorrent"},
410
+ {4, "btfans", "SimpleBT"},
411
+ {0, "PRC.P---", "Bittorrent Plus! II"},
412
+ {0, "P87.P---", "Bittorrent Plus!"},
413
+ {0, "S587Plus", "Bittorrent Plus!"},
414
+ {0, "martini", "Martini Man"},
415
+ {0, "Plus---", "Bittorrent Plus"},
416
+ {0, "turbobt", "TurboBT"},
417
+ {0, "a00---0", "Swarmy"},
418
+ {0, "a02---0", "Swarmy"},
419
+ {0, "T00---0", "Teeweety"},
420
+ {0, "BTDWV-", "Deadman Walking"},
421
+ {2, "BS", "BitSpirit"},
422
+ {0, "-SP", "BitSpirit 3.6"},
423
+ {0, "Pando-", "Pando"},
424
+ {0, "LIME", "LimeWire"},
425
+ {0, "btuga", "BTugaXP"},
426
+ {0, "oernu", "BTugaXP"},
427
+ {0, "Mbrst", "Burst!"},
428
+ {0, "PEERAPP", "PeerApp"},
429
+ {0, "Plus", "Plus!"},
430
+ {0, "-Qt-", "Qt"},
431
+ {0, "exbc", "BitComet"},
432
+ {0, "DNA", "BitTorrent DNA"},
433
+ {0, "-G3", "G3 Torrent"},
434
+ {0, "-FG", "FlashGet"},
435
+ {0, "-ML", "MLdonkey"},
436
+ {0, "-MG", "Media Get"},
437
+ {0, "XBT", "XBT"},
438
+ {0, "OP", "Opera"},
439
+ {2, "RS", "Rufus"},
440
+ {0, "AZ2500BT", "BitTyrant"},
441
+ {0, "btpd/", "BitTorrent Protocol Daemon"},
442
+ {0, "TIX", "Tixati"},
443
+ {0, "QVOD", "Qvod"},
444
+ };
445
+
446
+ for (const auto& e : entries) {
447
+ const char* p = e.pattern;
448
+ const char* s = reinterpret_cast<const char*>(id) + e.offset;
449
+ size_t len = std::strlen(p);
450
+ if (e.offset + len <= BT_PEER_ID_SIZE && std::memcmp(s, p, len) == 0) {
451
+ return e.name;
452
+ }
453
+ }
454
+ return nullptr;
455
+ }
456
+
457
+ } // namespace detail
458
+
459
+ /**
460
+ * @brief Identify BitTorrent client from peer ID (BEP 20)
278
461
  *
279
- * Attempts to identify the client from the peer ID prefix
462
+ * Supports three encoding styles:
463
+ * - Azureus-style: -XX1234-xxxxxxxxxxxx
464
+ * - Shadow-style: Xyyy--xxxxxxxxxxxxxx
465
+ * - Mainline-style: M1-2-3--xxxxxxxxxxxx
466
+ * Also recognizes many non-standard encodings.
280
467
  *
281
- * @param id Peer ID to parse
282
- * @return Client name or "Unknown"
468
+ * @param id 20-byte peer ID
469
+ * @return Human-readable client name with version, or "Unknown [...]"
283
470
  */
284
471
  inline std::string identify_client(const PeerID& id) {
472
+ // Check if all zeros
473
+ bool all_zero = true;
474
+ for (uint8_t b : id) { if (b != 0) { all_zero = false; break; } }
475
+ if (all_zero) return "Unknown";
476
+
477
+ // Check non-standard encodings first
478
+ const char* generic = detail::lookup_generic_client(id.data());
479
+ if (generic) return generic;
480
+
481
+ // Check Bits on Wheels special case
482
+ if (id[0] == '-' && id[1] == 'B' && id[2] == 'O' && id[3] == 'W' && id[7] == '-') {
483
+ return "Bits on Wheels " + std::string(reinterpret_cast<const char*>(id.data()) + 4, 3);
484
+ }
485
+
285
486
  // Azureus-style: -XX1234-
286
- if (id[0] == '-' && id[7] == '-') {
287
- char client[3] = {static_cast<char>(id[1]), static_cast<char>(id[2]), '\0'};
288
- std::string client_code(client);
487
+ if (id[0] == '-' && id[7] == '-' &&
488
+ (id[1] >= 'A' || id[1] >= 'a') &&
489
+ id[3] >= '0' && id[4] >= '0' && id[5] >= '0' && id[6] >= '0') {
490
+
491
+ char code[3] = {static_cast<char>(id[1]), static_cast<char>(id[2]), '\0'};
492
+
493
+ int v1 = detail::decode_version_digit(id[3]);
494
+ int v2 = detail::decode_version_digit(id[4]);
495
+ int v3 = detail::decode_version_digit(id[5]);
496
+ int v4 = detail::decode_version_digit(id[6]);
497
+
498
+ const char* name = detail::lookup_az_client(code);
499
+ std::string client_name = name ? name : std::string("Unknown (") + code + ")";
289
500
 
290
- if (client_code == "LR") return "librats";
291
- if (client_code == "LT") return "libtorrent";
292
- if (client_code == "qB") return "qBittorrent";
293
- if (client_code == "DE") return "Deluge";
294
- if (client_code == "TR") return "Transmission";
295
- if (client_code == "UT") return "uTorrent";
296
- if (client_code == "AZ") return "Azureus/Vuze";
297
- if (client_code == "BT") return "BitTorrent";
501
+ std::string version = std::to_string(v1) + "." + std::to_string(v2) + "." + std::to_string(v3);
502
+ if (v4 != 0) {
503
+ version += "." + std::to_string(v4);
504
+ }
505
+
506
+ return client_name + " " + version;
507
+ }
508
+
509
+ // Shadow-style: first char is client, next 3 are version
510
+ if ((id[0] >= 'A' && id[0] <= 'Z') || (id[0] >= 'a' && id[0] <= 'z')) {
511
+ // Check for shadow format: X + 3 version bytes + "--"
512
+ if (id[4] == '-' && id[5] == '-') {
513
+ if (id[1] >= '0' && id[2] >= '0' && id[3] >= '0') {
514
+ char c = static_cast<char>(id[0]);
515
+ const char* name = nullptr;
516
+ switch (c) {
517
+ case 'A': name = "ABC"; break;
518
+ case 'M': name = "Mainline"; break;
519
+ case 'O': name = "Osprey Permaseed"; break;
520
+ case 'Q': name = "BTQueue"; break;
521
+ case 'R': name = "Tribler"; break;
522
+ case 'S': name = "Shadow"; break;
523
+ case 'T': name = "BitTornado"; break;
524
+ case 'U': name = "UPnP"; break;
525
+ default: break;
526
+ }
527
+ if (name) {
528
+ int v1 = detail::decode_version_digit(id[1]);
529
+ int v2 = detail::decode_version_digit(id[2]);
530
+ int v3 = detail::decode_version_digit(id[3]);
531
+ return std::string(name) + " " + std::to_string(v1) + "." +
532
+ std::to_string(v2) + "." + std::to_string(v3);
533
+ }
534
+ }
535
+ }
298
536
 
299
- return std::string("Unknown (") + client_code + ")";
537
+ // Mainline-style: M1-2-3--
538
+ char ids[21];
539
+ std::memcpy(ids, id.data(), 20);
540
+ ids[20] = '\0';
541
+ char name_ch = '\0';
542
+ int v1 = 0, v2 = 0, v3 = 0;
543
+ if (std::sscanf(ids, "%c%3d-%3d-%3d--", &name_ch, &v1, &v2, &v3) == 4 &&
544
+ name_ch >= 32 && name_ch < 127) {
545
+ const char* name = nullptr;
546
+ switch (name_ch) {
547
+ case 'M': name = "Mainline"; break;
548
+ default: break;
549
+ }
550
+ if (name) {
551
+ return std::string(name) + " " + std::to_string(v1) + "." +
552
+ std::to_string(v2) + "." + std::to_string(v3);
553
+ }
554
+ }
300
555
  }
301
556
 
302
- // Shadow-style: first byte is client ID
303
- // Not as common, but still used
557
+ // All zeros in first 12 bytes - special cases
558
+ bool first_12_zero = true;
559
+ for (int i = 0; i < 12; ++i) { if (id[i] != 0) { first_12_zero = false; break; } }
560
+ if (first_12_zero) {
561
+ if (id[12] == 0x97) return "Experimental 3.2.1b2";
562
+ if (id[12] == 0x00) return "Experimental 3.1";
563
+ return "Generic";
564
+ }
304
565
 
305
- return "Unknown";
566
+ // Unknown - show printable representation
567
+ std::string unknown("Unknown [");
568
+ for (uint8_t c : id) {
569
+ unknown += (c >= 32 && c < 127) ? static_cast<char>(c) : '.';
570
+ }
571
+ unknown += "]";
572
+ return unknown;
306
573
  }
307
574
 
308
575
  //=============================================================================
@@ -62,8 +62,19 @@ bool DhtClient::start() {
62
62
  }
63
63
 
64
64
  socket_ = create_udp_socket(port_, bind_address_);
65
+ // Fallback to free port
66
+ if (!is_valid_socket(socket_) && port_ > 0) {
67
+ // Requested port is not available, try ephemeral port as fallback
68
+ int original_port = port_;
69
+ LOG_DHT_WARN("UDP port " << original_port << " is not available, falling back to ephemeral port");
70
+ socket_ = create_udp_socket(0, bind_address_);
71
+ if (is_valid_socket(socket_)) {
72
+ port_ = get_bound_port(socket_);
73
+ LOG_DHT_INFO("Fell back from port " << original_port << " to ephemeral port " << port_);
74
+ }
75
+ }
65
76
  if (!is_valid_socket(socket_)) {
66
- LOG_DHT_ERROR("Failed to create dual-stack UDP socket");
77
+ LOG_DHT_ERROR("Failed to create dual-stack UDP socket on port " << port_);
67
78
  return false;
68
79
  }
69
80
 
@@ -380,7 +391,7 @@ void DhtClient::network_loop() {
380
391
 
381
392
  while (running_) {
382
393
  Peer sender;
383
- auto data = receive_udp_data(socket_, 1500, sender); // MTU size
394
+ auto data = receive_udp_data(socket_, 1500, sender, -1); // MTU size, blocking
384
395
 
385
396
  if (!data.empty()) {
386
397
  LOG_DHT_DEBUG("Received " << data.size() << " bytes from " << sender.ip << ":" << sender.port);
@@ -937,7 +948,8 @@ void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer
937
948
  }
938
949
 
939
950
  // Determine the actual port (BEP 5: implied_port support)
940
- uint16_t peer_port = message.port;
951
+ // If implied_port is set, use the UDP source port instead of the explicit port field
952
+ uint16_t peer_port = message.implied_port ? sender.port : message.port;
941
953
 
942
954
  // Store the peer announcement
943
955
  Peer announcing_peer(sender.ip, peer_port);
@@ -1072,7 +1084,7 @@ bool DhtClient::send_krpc_message(const KrpcMessage& message, const Peer& peer)
1072
1084
  }
1073
1085
 
1074
1086
  LOG_DHT_DEBUG("Sending KRPC message (" << data.size() << " bytes) to " << peer.ip << ":" << peer.port);
1075
- int result = send_udp_data(socket_, data, peer);
1087
+ int result = send_udp_data(socket_, data, peer.ip, peer.port);
1076
1088
 
1077
1089
  if (result > 0) {
1078
1090
  LOG_DHT_DEBUG("Successfully sent KRPC message to " << peer.ip << ":" << peer.port);
@@ -1101,9 +1113,9 @@ void DhtClient::send_krpc_get_peers(const Peer& peer, const InfoHash& info_hash)
1101
1113
  send_krpc_message(message, peer);
1102
1114
  }
1103
1115
 
1104
- void DhtClient::send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token) {
1116
+ void DhtClient::send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token, bool implied_port) {
1105
1117
  std::string transaction_id = KrpcProtocol::generate_transaction_id();
1106
- auto message = KrpcProtocol::create_announce_peer_query(transaction_id, node_id_, info_hash, port, token);
1118
+ auto message = KrpcProtocol::create_announce_peer_query(transaction_id, node_id_, info_hash, port, token, implied_port);
1107
1119
  send_krpc_message(message, peer);
1108
1120
  }
1109
1121
 
@@ -2138,13 +2150,19 @@ void DhtClient::send_announce_to_closest_nodes(PendingSearch& search) {
2138
2150
 
2139
2151
  LOG_DHT_INFO("Announcing to " << announce_targets.size() << " closest nodes with tokens");
2140
2152
 
2153
+ // BEP 5: Use implied_port when the announce port is the same as the DHT port.
2154
+ // This tells the receiving node to use the UDP source port from the packet,
2155
+ // which is more accurate when behind NAT.
2156
+ bool use_implied_port = (search.announce_port == port_);
2157
+
2141
2158
  // Send announce_peer to each target
2142
2159
  for (const auto& [node, token] : announce_targets) {
2143
2160
  LOG_DHT_DEBUG("Sending announce_peer to node " << node_id_to_hex(node.id)
2144
2161
  << " at " << node.peer.ip << ":" << node.peer.port
2145
- << " with token (distance: " << get_bucket_index(node.id) << ")");
2162
+ << " with token (distance: " << get_bucket_index(node.id) << ")"
2163
+ << (use_implied_port ? " [implied_port]" : ""));
2146
2164
 
2147
- send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token);
2165
+ send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token, use_implied_port);
2148
2166
  }
2149
2167
 
2150
2168
  LOG_DHT_INFO("Announce completed: sent announce_peer to " << announce_targets.size()
@@ -541,7 +541,7 @@ private:
541
541
  void send_krpc_ping(const Peer& peer);
542
542
  void send_krpc_find_node(const Peer& peer, const NodeId& target);
543
543
  void send_krpc_get_peers(const Peer& peer, const InfoHash& info_hash);
544
- void send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token);
544
+ void send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token, bool implied_port = false);
545
545
 
546
546
  void add_node(const DhtNode& node, bool confirmed = true, bool no_verify = false);
547
547
  std::vector<DhtNode> find_closest_nodes(const NodeId& target, size_t count = K_BUCKET_SIZE);
@@ -281,7 +281,7 @@ bool IceManager::ensure_socket() {
281
281
  return false;
282
282
  }
283
283
 
284
- local_port_ = static_cast<uint16_t>(get_ephemeral_port(socket_));
284
+ local_port_ = static_cast<uint16_t>(get_bound_port(socket_));
285
285
  LOG_ICE_DEBUG("Created ICE socket on port " << local_port_);
286
286
  return true;
287
287
  }