librats 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -501,3 +501,8 @@ if(RATS_BUILD_TESTS)
501
501
  RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin
502
502
  )
503
503
  endif()
504
+
505
+ # Examples (basic examples always built; BT examples require RATS_SEARCH_FEATURES)
506
+ if(RATS_BUILD_EXAMPLES)
507
+ add_subdirectory(examples)
508
+ endif()
@@ -326,7 +326,8 @@ Torrent::Ptr BtClient::add_torrent(const TorrentInfo& info,
326
326
  }
327
327
 
328
328
  Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
329
- const std::string& save_path) {
329
+ const std::string& save_path,
330
+ bool skip_dht_search) {
330
331
  auto info = TorrentInfo::from_magnet(magnet_uri);
331
332
  if (!info) {
332
333
  if (on_alert_) {
@@ -345,7 +346,14 @@ Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
345
346
 
346
347
  // Create torrent without full metadata
347
348
  TorrentConfig torrent_config;
348
- torrent_config.save_path = save_path.empty() ? config_.download_path : save_path;
349
+ // For metadata-only mode (skip_dht_search=true with empty save_path), keep save_path empty
350
+ // Otherwise use default download path if not specified
351
+ if (skip_dht_search && save_path.empty()) {
352
+ // Metadata-only mode - keep empty save_path for proper detection
353
+ torrent_config.save_path = "";
354
+ } else {
355
+ torrent_config.save_path = save_path.empty() ? config_.download_path : save_path;
356
+ }
349
357
  torrent_config.resume_data_path = config_.resume_data_path;
350
358
  torrent_config.max_connections = config_.max_connections_per_torrent;
351
359
  torrent_config.max_uploads = config_.max_uploads;
@@ -362,8 +370,8 @@ Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
362
370
  if (running_) {
363
371
  torrent->start();
364
372
 
365
- // Find peers via DHT for metadata
366
- if (dht_running_) {
373
+ // Find peers via DHT for metadata (skip if using direct peer connection)
374
+ if (dht_running_ && !skip_dht_search) {
367
375
  find_peers_dht(info->info_hash());
368
376
  }
369
377
  }
@@ -891,6 +899,9 @@ void BtClient::announce_torrents_to_trackers() {
891
899
  for (auto& [hash, torrent] : torrents_) {
892
900
  if (!torrent->is_active()) continue;
893
901
 
902
+ // Skip metadata-only torrents (empty save_path means only fetching metadata)
903
+ if (torrent->save_path().empty()) continue;
904
+
894
905
  auto tracker_it = tracker_managers_.find(hash);
895
906
  if (tracker_it == tracker_managers_.end()) continue;
896
907
 
@@ -993,12 +1004,14 @@ void BtClient::tick_loop() {
993
1004
  }
994
1005
 
995
1006
  // Periodic DHT announces (every 15 minutes)
1007
+ // Skip metadata-only torrents (empty save_path) to avoid unnecessary network traffic
996
1008
  auto dht_elapsed = std::chrono::duration_cast<std::chrono::minutes>(
997
1009
  now - last_dht_announce_).count();
998
1010
  if (dht_elapsed >= 15 && dht_running_) {
999
1011
  std::lock_guard<std::mutex> lock(mutex_);
1000
1012
  for (auto& [hash, torrent] : torrents_) {
1001
- if (torrent->is_active()) {
1013
+ // Skip metadata-only torrents (empty save_path means only fetching metadata)
1014
+ if (torrent->is_active() && !torrent->save_path().empty()) {
1002
1015
  announce_to_dht(hash);
1003
1016
  }
1004
1017
  }
@@ -170,10 +170,12 @@ public:
170
170
  *
171
171
  * @param magnet_uri Magnet URI
172
172
  * @param save_path Directory to save files
173
+ * @param skip_dht_search If true, don't search DHT for peers (for metadata-only from specific peer)
173
174
  * @return Torrent pointer, or nullptr on failure
174
175
  */
175
176
  Torrent::Ptr add_magnet(const std::string& magnet_uri,
176
- const std::string& save_path = "");
177
+ const std::string& save_path = "",
178
+ bool skip_dht_search = false);
177
179
 
178
180
  /**
179
181
  * @brief Add a torrent with resume data for fast resume
@@ -69,7 +69,7 @@ bool BtNetworkManager::start() {
69
69
  }
70
70
 
71
71
  // Get actual port (in case 0 was specified)
72
- actual_listen_port_ = static_cast<uint16_t>(get_ephemeral_port(listen_socket_));
72
+ actual_listen_port_ = static_cast<uint16_t>(get_bound_port(listen_socket_));
73
73
  if (actual_listen_port_ == 0) {
74
74
  actual_listen_port_ = config_.listen_port;
75
75
  }
@@ -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()