librats 0.8.0 → 0.9.1

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.
@@ -1166,185 +1166,88 @@ public:
1166
1166
  // =========================================================================
1167
1167
  // File Transfer API
1168
1168
  // =========================================================================
1169
-
1169
+ //
1170
+ // Streams files and directory trees to connected peers. A transfer is
1171
+ // offered to the peer, who accepts (choosing a destination) or rejects it.
1172
+ // See file_transfer.h for the FileTransferManager that implements it.
1173
+
1170
1174
  /**
1171
- * Get the file transfer manager instance
1172
- * @return Reference to the file transfer manager
1175
+ * Get the file transfer manager instance.
1173
1176
  */
1174
1177
  FileTransferManager& get_file_transfer_manager();
1175
-
1178
+
1176
1179
  /**
1177
- * Check if file transfer is available
1178
- * @return true if file transfer manager is initialized
1180
+ * Check whether the file transfer manager is initialized.
1179
1181
  */
1180
1182
  bool is_file_transfer_available() const;
1181
-
1182
- // Sending and Requesting
1183
+
1183
1184
  /**
1184
- * Send a file to a peer
1185
- * @param peer_id Target peer ID
1186
- * @param file_path Local file path to send
1187
- * @param remote_filename Optional remote filename (default: use local name)
1188
- * @return Transfer ID if successful, empty string if failed
1185
+ * Send a file to a peer.
1186
+ * @param peer_id Target peer ID
1187
+ * @param file_path Local file to send
1188
+ * @param remote_filename Optional name to present to the peer
1189
+ * @return Transfer ID, or empty string on immediate failure
1189
1190
  */
1190
- std::string send_file(const std::string& peer_id, const std::string& file_path,
1191
- const std::string& remote_filename = "");
1192
-
1191
+ std::string send_file(const std::string& peer_id, const std::string& file_path,
1192
+ const std::string& remote_filename = "");
1193
+
1193
1194
  /**
1194
- * Send an entire directory to a peer
1195
- * @param peer_id Target peer ID
1196
- * @param directory_path Local directory path to send
1197
- * @param remote_directory_name Optional remote directory name
1198
- * @param recursive Whether to include subdirectories (default: true)
1199
- * @return Transfer ID if successful, empty string if failed
1195
+ * Send a directory tree (recursively) to a peer.
1196
+ * @param peer_id Target peer ID
1197
+ * @param directory_path Local directory to send
1198
+ * @param remote_name Optional name to present to the peer
1199
+ * @return Transfer ID, or empty string on immediate failure
1200
1200
  */
1201
1201
  std::string send_directory(const std::string& peer_id, const std::string& directory_path,
1202
- const std::string& remote_directory_name = "", bool recursive = true);
1203
-
1204
- /**
1205
- * Request a file from a remote peer
1206
- * @param peer_id Target peer ID
1207
- * @param remote_file_path Path to file on remote peer
1208
- * @param local_path Local path where file should be saved
1209
- * @return Transfer ID if successful, empty string if failed
1210
- */
1211
- std::string request_file(const std::string& peer_id, const std::string& remote_file_path,
1212
- const std::string& local_path);
1213
-
1214
- /**
1215
- * Request a directory from a remote peer
1216
- * @param peer_id Target peer ID
1217
- * @param remote_directory_path Path to directory on remote peer
1218
- * @param local_directory_path Local path where directory should be saved
1219
- * @param recursive Whether to include subdirectories (default: true)
1220
- * @return Transfer ID if successful, empty string if failed
1221
- */
1222
- std::string request_directory(const std::string& peer_id, const std::string& remote_directory_path,
1223
- const std::string& local_directory_path, bool recursive = true);
1224
-
1225
- // Accept/Reject Operations
1202
+ const std::string& remote_name = "");
1203
+
1226
1204
  /**
1227
- * Accept an incoming file transfer
1228
- * @param transfer_id Transfer identifier from request
1229
- * @param local_path Local path where file should be saved
1230
- * @return true if accepted successfully
1205
+ * Accept an incoming transfer. For a file, local_path is the destination
1206
+ * file path; for a directory, it is the destination directory.
1231
1207
  */
1232
1208
  bool accept_file_transfer(const std::string& transfer_id, const std::string& local_path);
1233
-
1209
+
1234
1210
  /**
1235
- * Reject an incoming file transfer
1236
- * @param transfer_id Transfer identifier from request
1237
- * @param reason Optional reason for rejection
1238
- * @return true if rejected successfully
1211
+ * Reject an incoming transfer.
1239
1212
  */
1240
1213
  bool reject_file_transfer(const std::string& transfer_id, const std::string& reason = "");
1241
-
1242
- /**
1243
- * Accept an incoming directory transfer
1244
- * @param transfer_id Transfer identifier from request
1245
- * @param local_path Local path where directory should be saved
1246
- * @return true if accepted successfully
1247
- */
1248
- bool accept_directory_transfer(const std::string& transfer_id, const std::string& local_path);
1249
-
1250
- /**
1251
- * Reject an incoming directory transfer
1252
- * @param transfer_id Transfer identifier from request
1253
- * @param reason Optional reason for rejection
1254
- * @return true if rejected successfully
1255
- */
1256
- bool reject_directory_transfer(const std::string& transfer_id, const std::string& reason = "");
1257
-
1258
- // Transfer Control
1259
- /**
1260
- * Pause an active file transfer
1261
- * @param transfer_id Transfer to pause
1262
- * @return true if paused successfully
1263
- */
1214
+
1215
+ /** Pause an active transfer (either direction). */
1264
1216
  bool pause_file_transfer(const std::string& transfer_id);
1265
-
1266
- /**
1267
- * Resume a paused file transfer
1268
- * @param transfer_id Transfer to resume
1269
- * @return true if resumed successfully
1270
- */
1217
+
1218
+ /** Resume a paused transfer. */
1271
1219
  bool resume_file_transfer(const std::string& transfer_id);
1272
-
1273
- /**
1274
- * Cancel an active or paused file transfer
1275
- * @param transfer_id Transfer to cancel
1276
- * @return true if cancelled successfully
1277
- */
1220
+
1221
+ /** Cancel an active or paused transfer. */
1278
1222
  bool cancel_file_transfer(const std::string& transfer_id);
1279
-
1280
- // Information and Monitoring
1281
- /**
1282
- * Get file transfer progress information
1283
- * @param transfer_id Transfer to query
1284
- * @return Progress information or nullptr if not found
1285
- */
1223
+
1224
+ /** Get a progress snapshot, or nullptr if the transfer is unknown. */
1286
1225
  std::shared_ptr<FileTransferProgress> get_file_transfer_progress(const std::string& transfer_id) const;
1287
-
1288
- /**
1289
- * Get all active file transfers
1290
- * @return Vector of transfer progress objects
1291
- */
1226
+
1227
+ /** Get progress snapshots for all non-finished transfers. */
1292
1228
  std::vector<std::shared_ptr<FileTransferProgress>> get_active_file_transfers() const;
1293
-
1294
- /**
1295
- * Get file transfer statistics
1296
- * @return JSON object with transfer statistics
1297
- */
1229
+
1230
+ /** Get aggregate file transfer statistics as JSON. */
1298
1231
  nlohmann::json get_file_transfer_statistics() const;
1299
-
1300
- /**
1301
- * Set file transfer configuration
1302
- * @param config Transfer configuration settings
1303
- */
1232
+
1233
+ /** Replace the file transfer configuration. */
1304
1234
  void set_file_transfer_config(const FileTransferConfig& config);
1305
-
1306
- /**
1307
- * Get current file transfer configuration
1308
- * @return Current configuration settings
1309
- */
1235
+
1236
+ /** Get the current file transfer configuration. */
1310
1237
  FileTransferConfig get_file_transfer_config() const;
1311
-
1312
- // Event Handlers
1313
- /**
1314
- * Set file transfer progress callback
1315
- * @param callback Function to call with progress updates
1316
- */
1317
- void on_file_transfer_progress(FileTransferProgressCallback callback);
1318
-
1319
- /**
1320
- * Set file transfer completion callback
1321
- * @param callback Function to call when transfers complete
1322
- */
1323
- void on_file_transfer_completed(FileTransferCompletedCallback callback);
1324
-
1325
- /**
1326
- * Set incoming file transfer request callback
1327
- * @param callback Function to call when receiving transfer requests
1328
- */
1329
- void on_file_transfer_request(FileTransferRequestCallback callback);
1330
-
1331
- /**
1332
- * Set directory transfer progress callback
1333
- * @param callback Function to call with directory transfer progress
1334
- */
1335
- void on_directory_transfer_progress(DirectoryTransferProgressCallback callback);
1336
-
1337
- /**
1338
- * Set file request callback (called when receiving file requests)
1339
- * @param callback Function to call when receiving file requests
1340
- */
1341
- void on_file_request(FileRequestCallback callback);
1342
-
1238
+
1239
+ /** Set the progress callback (fires for both directions). */
1240
+ void on_file_transfer_progress(TransferProgressCallback callback);
1241
+
1242
+ /** Set the completion callback (fires once per transfer). */
1243
+ void on_file_transfer_completed(TransferCompletedCallback callback);
1244
+
1343
1245
  /**
1344
- * Set directory request callback (called when receiving directory requests)
1345
- * @param callback Function to call when receiving directory requests
1246
+ * Set the incoming-offer callback. The handler should call
1247
+ * accept_file_transfer()/reject_file_transfer(). Without it, offers are
1248
+ * auto-rejected.
1346
1249
  */
1347
- void on_directory_request(DirectoryRequestCallback callback);
1250
+ void on_file_transfer_request(TransferOfferCallback callback);
1348
1251
 
1349
1252
  // =========================================================================
1350
1253
  // ICE (NAT Traversal) API
@@ -2059,9 +1962,13 @@ private:
2059
1962
  JsonDataCallback json_data_callback_;
2060
1963
  DisconnectCallback disconnect_callback_;
2061
1964
 
2062
- // DHT client for peer discovery
1965
+ // DHT clients for peer discovery. IPv4 and IPv6 are separate Kademlia networks
1966
+ // (BEP 32), so each family runs its own client. dht_client_ (IPv4) is also the one
1967
+ // shared with the BitTorrent subsystem; dht_client_v6_ is created best-effort when
1968
+ // IPv6 is available.
2063
1969
  std::unique_ptr<DhtClient> dht_client_;
2064
-
1970
+ std::unique_ptr<DhtClient> dht_client_v6_;
1971
+
2065
1972
  // mDNS client for local network discovery
2066
1973
  std::unique_ptr<MdnsClient> mdns_client_;
2067
1974
  std::function<void(const std::string&, int, const std::string&)> mdns_callback_;
@@ -679,42 +679,36 @@ char* rats_send_directory(rats_client_t handle, const char* peer_id, const char*
679
679
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
680
680
 
681
681
  std::string remote_name = remote_directory_name ? std::string(remote_directory_name) : std::string("");
682
- std::string transfer_id = wrap->client->send_directory(std::string(peer_id), std::string(directory_path), remote_name, recursive != 0);
683
-
682
+ (void)recursive; // directory transfers are always recursive
683
+ std::string transfer_id = wrap->client->send_directory(std::string(peer_id), std::string(directory_path), remote_name);
684
+
684
685
  return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
685
686
  }
686
687
 
688
+ // Pull-style requests are not supported in this version; transfers are push-only.
687
689
  char* rats_request_file(rats_client_t handle, const char* peer_id, const char* remote_file_path, const char* local_path) {
688
- if (!handle || !peer_id || !remote_file_path || !local_path) return nullptr;
689
- rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
690
-
691
- std::string transfer_id = wrap->client->request_file(std::string(peer_id), std::string(remote_file_path), std::string(local_path));
692
-
693
- return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
690
+ (void)handle; (void)peer_id; (void)remote_file_path; (void)local_path;
691
+ return nullptr;
694
692
  }
695
693
 
696
694
  char* rats_request_directory(rats_client_t handle, const char* peer_id, const char* remote_directory_path, const char* local_directory_path, int recursive) {
697
- if (!handle || !peer_id || !remote_directory_path || !local_directory_path) return nullptr;
698
- rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
699
-
700
- std::string transfer_id = wrap->client->request_directory(std::string(peer_id), std::string(remote_directory_path), std::string(local_directory_path), recursive != 0);
701
-
702
- return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
695
+ (void)handle; (void)peer_id; (void)remote_directory_path; (void)local_directory_path; (void)recursive;
696
+ return nullptr;
703
697
  }
704
698
 
705
699
  rats_error_t rats_accept_directory_transfer(rats_client_t handle, const char* transfer_id, const char* local_path) {
706
700
  if (!handle || !transfer_id || !local_path) return RATS_ERROR_INVALID_PARAMETER;
707
701
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
708
-
709
- return wrap->client->accept_directory_transfer(std::string(transfer_id), std::string(local_path)) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
702
+
703
+ return wrap->client->accept_file_transfer(std::string(transfer_id), std::string(local_path)) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
710
704
  }
711
705
 
712
706
  rats_error_t rats_reject_directory_transfer(rats_client_t handle, const char* transfer_id, const char* reason) {
713
707
  if (!handle || !transfer_id) return RATS_ERROR_INVALID_PARAMETER;
714
708
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
715
-
709
+
716
710
  std::string reject_reason = reason ? std::string(reason) : std::string("");
717
- return wrap->client->reject_directory_transfer(std::string(transfer_id), reject_reason) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
711
+ return wrap->client->reject_file_transfer(std::string(transfer_id), reject_reason) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
718
712
  }
719
713
 
720
714
  rats_error_t rats_pause_file_transfer(rats_client_t handle, const char* transfer_id) {
@@ -768,44 +762,33 @@ void rats_set_file_request_callback(rats_client_t handle, rats_file_request_cb c
768
762
  wrap->file_request_ud = user_data;
769
763
 
770
764
  if (cb) {
771
- wrap->client->on_file_request([wrap](const std::string& peer_id, const std::string& file_path, const std::string& transfer_id) {
765
+ // Fires for every incoming offer (file or directory). The C app should
766
+ // then call rats_accept_file_transfer()/rats_reject_file_transfer().
767
+ wrap->client->on_file_transfer_request([wrap](const IncomingTransferOffer& offer) {
772
768
  if (wrap->file_request_cb) {
773
- wrap->file_request_cb(wrap->file_request_ud, peer_id.c_str(), transfer_id.c_str(), file_path.c_str(), "");
769
+ wrap->file_request_cb(wrap->file_request_ud, offer.peer_id.c_str(),
770
+ offer.transfer_id.c_str(), "", offer.name.c_str());
774
771
  }
775
- return true; // Accept the file request
776
772
  });
777
773
  }
778
774
  }
779
775
 
776
+ // Directory offers arrive through the unified file-request callback above;
777
+ // this setter is kept for ABI compatibility and simply records the callback.
780
778
  void rats_set_directory_request_callback(rats_client_t handle, rats_directory_request_cb cb, void* user_data) {
781
779
  if (!handle) return;
782
780
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
783
781
  wrap->directory_request_cb = cb;
784
782
  wrap->directory_request_ud = user_data;
785
-
786
- if (cb) {
787
- wrap->client->on_directory_request([wrap](const std::string& peer_id, const std::string& directory_path, bool recursive, const std::string& transfer_id) {
788
- if (wrap->directory_request_cb) {
789
- wrap->directory_request_cb(wrap->directory_request_ud, peer_id.c_str(), transfer_id.c_str(), directory_path.c_str(), "");
790
- }
791
- return true; // Accept the directory request
792
- });
793
- }
794
783
  }
795
784
 
785
+ // Directory progress arrives through the unified file-progress callback above;
786
+ // this setter is kept for ABI compatibility and simply records the callback.
796
787
  void rats_set_directory_progress_callback(rats_client_t handle, rats_directory_progress_cb cb, void* user_data) {
797
788
  if (!handle) return;
798
789
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
799
790
  wrap->directory_progress_cb = cb;
800
791
  wrap->directory_progress_ud = user_data;
801
-
802
- if (cb) {
803
- wrap->client->on_directory_transfer_progress([wrap](const std::string& transfer_id, const std::string& current_file, uint64_t files_completed, uint64_t total_files, uint64_t bytes_completed, uint64_t total_bytes) {
804
- if (wrap->directory_progress_cb) {
805
- wrap->directory_progress_cb(wrap->directory_progress_ud, transfer_id.c_str(), static_cast<int>(files_completed), static_cast<int>(total_files), current_file.c_str());
806
- }
807
- });
808
- }
809
792
  }
810
793
 
811
794
  // Peer information
@@ -13,94 +13,124 @@ bool RatsClient::start_dht_discovery(int dht_port) {
13
13
  LOG_CLIENT_WARN("DHT discovery is already running");
14
14
  return true;
15
15
  }
16
-
16
+
17
17
  LOG_CLIENT_INFO("Starting DHT discovery on port " << dht_port <<
18
18
  (bind_address_.empty() ? "" : " bound to " + bind_address_));
19
-
20
- dht_client_ = std::make_unique<DhtClient>(dht_port, bind_address_, data_directory_);
19
+
20
+ auto bootstrap_nodes = DhtClient::get_default_bootstrap_nodes();
21
+
22
+ // IPv4 DHT (also shared with the BitTorrent subsystem) - required.
23
+ dht_client_ = std::make_unique<DhtClient>(dht_port, bind_address_, data_directory_, AddressFamily::IPv4);
21
24
  if (!dht_client_->start()) {
22
- LOG_CLIENT_ERROR("Failed to start DHT client");
25
+ LOG_CLIENT_ERROR("Failed to start IPv4 DHT client");
23
26
  dht_client_.reset();
24
27
  return false;
25
28
  }
26
-
27
- // Bootstrap with default nodes
28
- auto bootstrap_nodes = DhtClient::get_default_bootstrap_nodes();
29
29
  if (!dht_client_->bootstrap(bootstrap_nodes)) {
30
- LOG_CLIENT_WARN("Failed to bootstrap DHT");
30
+ LOG_CLIENT_WARN("Failed to bootstrap IPv4 DHT");
31
31
  }
32
-
33
- // Start automatic peer discovery
32
+
33
+ // IPv6 DHT - best effort. Shares the same port via a V6ONLY socket. If the host has
34
+ // no usable IPv6 stack, socket creation fails and we simply run IPv4-only.
35
+ dht_client_v6_ = std::make_unique<DhtClient>(dht_port, bind_address_, data_directory_, AddressFamily::IPv6);
36
+ if (dht_client_v6_->start()) {
37
+ if (!dht_client_v6_->bootstrap(bootstrap_nodes)) {
38
+ LOG_CLIENT_WARN("Failed to bootstrap IPv6 DHT");
39
+ }
40
+ LOG_CLIENT_INFO("IPv6 DHT client started");
41
+ } else {
42
+ LOG_CLIENT_INFO("IPv6 DHT unavailable on this host, running IPv4-only");
43
+ dht_client_v6_.reset();
44
+ }
45
+
46
+ // Start automatic peer discovery (this thread also performs the best-effort STUN probe
47
+ // that derives our BEP 42 node ID — see automatic_discovery_loop).
34
48
  start_automatic_peer_discovery();
35
-
49
+
36
50
  LOG_CLIENT_INFO("DHT discovery started successfully");
37
51
  return true;
38
52
  }
39
53
 
40
54
  void RatsClient::stop_dht_discovery() {
41
- if (!dht_client_) {
55
+ if (!dht_client_ && !dht_client_v6_) {
42
56
  return;
43
57
  }
44
-
58
+
45
59
  LOG_CLIENT_INFO("Stopping DHT discovery");
46
-
60
+
47
61
  // Stop automatic peer discovery
48
62
  stop_automatic_peer_discovery();
49
-
50
- dht_client_->stop();
51
- dht_client_.reset();
63
+
64
+ if (dht_client_) {
65
+ dht_client_->stop();
66
+ dht_client_.reset();
67
+ }
68
+ if (dht_client_v6_) {
69
+ dht_client_v6_->stop();
70
+ dht_client_v6_.reset();
71
+ }
52
72
  LOG_CLIENT_INFO("DHT discovery stopped");
53
73
  }
54
74
 
55
75
  bool RatsClient::find_peers_by_hash(const std::string& content_hash, std::function<void(const std::vector<std::string>&)> callback) {
56
- if (!dht_client_ || !dht_client_->is_running()) {
76
+ if (!is_dht_running()) {
57
77
  LOG_CLIENT_ERROR("DHT client not running");
58
78
  return false;
59
79
  }
60
-
80
+
61
81
  if (content_hash.length() != CONTENT_HASH_HEX_LENGTH) {
62
82
  LOG_CLIENT_ERROR("Invalid content hash length: " << content_hash.length() << " (expected " << CONTENT_HASH_HEX_LENGTH << ")");
63
83
  return false;
64
84
  }
65
-
85
+
66
86
  LOG_CLIENT_INFO("Finding peers for content hash: " << content_hash);
67
-
87
+
68
88
  InfoHash info_hash = hex_to_node_id(content_hash);
69
-
70
- return dht_client_->find_peers(info_hash, [this, callback](const std::vector<Peer>& peers, const InfoHash& info_hash) {
71
- // Convert Peer to string addresses for callback
89
+
90
+ // Shared wrapper: discovered peers (IPv4 or IPv6) are delivered through the same callback.
91
+ PeerDiscoveryCallback peer_callback = [callback](const std::vector<Peer>& peers, const InfoHash&) {
72
92
  std::vector<std::string> peer_addresses;
93
+ peer_addresses.reserve(peers.size());
73
94
  for (const auto& peer : peers) {
74
95
  peer_addresses.emplace_back(peer.ip + ":" + std::to_string(peer.port));
75
96
  }
76
-
77
97
  if (callback) {
78
98
  callback(peer_addresses);
79
99
  }
80
- });
100
+ };
101
+
102
+ // Fan out to both Kademlia networks; succeed if at least one accepts the search.
103
+ bool started = false;
104
+ if (dht_client_ && dht_client_->is_running()) {
105
+ started |= dht_client_->find_peers(info_hash, peer_callback);
106
+ }
107
+ if (dht_client_v6_ && dht_client_v6_->is_running()) {
108
+ started |= dht_client_v6_->find_peers(info_hash, peer_callback);
109
+ }
110
+ return started;
81
111
  }
82
112
 
83
113
  bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port,
84
114
  std::function<void(const std::vector<std::string>&)> callback) {
85
- if (!dht_client_ || !dht_client_->is_running()) {
115
+ if (!is_dht_running()) {
86
116
  LOG_CLIENT_ERROR("DHT client not running");
87
117
  return false;
88
118
  }
89
-
119
+
90
120
  if (content_hash.length() != CONTENT_HASH_HEX_LENGTH) {
91
121
  LOG_CLIENT_ERROR("Invalid content hash length: " << content_hash.length() << " (expected " << CONTENT_HASH_HEX_LENGTH << ")");
92
122
  return false;
93
123
  }
94
-
124
+
95
125
  if (port == 0) {
96
126
  port = listen_port_;
97
127
  }
98
-
128
+
99
129
  LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port
100
130
  << (callback ? " with peer callback" : ""));
101
-
131
+
102
132
  InfoHash info_hash = hex_to_node_id(content_hash);
103
-
133
+
104
134
  // Create wrapper callback that converts Peer to string addresses (if callback provided)
105
135
  PeerDiscoveryCallback peer_callback = nullptr;
106
136
  if (callback) {
@@ -113,19 +143,32 @@ bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t por
113
143
  callback(peer_addresses);
114
144
  };
115
145
  }
116
-
117
- return dht_client_->announce_peer(info_hash, port, peer_callback);
146
+
147
+ // Announce on both Kademlia networks so IPv4 and IPv6 peers can both find us.
148
+ bool started = false;
149
+ if (dht_client_ && dht_client_->is_running()) {
150
+ started |= dht_client_->announce_peer(info_hash, port, peer_callback);
151
+ }
152
+ if (dht_client_v6_ && dht_client_v6_->is_running()) {
153
+ started |= dht_client_v6_->announce_peer(info_hash, port, peer_callback);
154
+ }
155
+ return started;
118
156
  }
119
157
 
120
158
  bool RatsClient::is_dht_running() const {
121
- return dht_client_ && dht_client_->is_running();
159
+ return (dht_client_ && dht_client_->is_running()) ||
160
+ (dht_client_v6_ && dht_client_v6_->is_running());
122
161
  }
123
162
 
124
163
  size_t RatsClient::get_dht_routing_table_size() const {
125
- if (!dht_client_) {
126
- return 0;
164
+ size_t total = 0;
165
+ if (dht_client_) {
166
+ total += dht_client_->get_routing_table_size();
167
+ }
168
+ if (dht_client_v6_) {
169
+ total += dht_client_v6_->get_routing_table_size();
127
170
  }
128
- return dht_client_->get_routing_table_size();
171
+ return total;
129
172
  }
130
173
 
131
174
  void RatsClient::handle_dht_peer_discovery(const std::vector<Peer>& peers, const InfoHash& info_hash) {
@@ -219,6 +262,27 @@ void RatsClient::automatic_discovery_loop() {
219
262
  }
220
263
  }
221
264
 
265
+ // Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it before
266
+ // we announce, so we announce under the correct ID. This runs on the discovery thread (not
267
+ // a standalone one) on purpose: stop_dht_discovery() joins this thread via
268
+ // stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so set_external_ip()
269
+ // can never touch a destroyed DhtClient. It complements the in-DHT "ip"-field voting, which
270
+ // keeps the node ID correct if the address changes or STUN is unavailable.
271
+ if (auto_discovery_running_.load() && running_.load()) {
272
+ auto mapped = discover_public_address("stun.l.google.com", 19302, 4000);
273
+ if (mapped) {
274
+ // set_external_ip validates the address family, so feeding both instances is safe:
275
+ // only the matching-family DHT adopts it. (A default IPv4 STUN server yields an IPv4
276
+ // address; the IPv6 node ID is learned organically from IPv6 DHT responses.)
277
+ if (dht_client_) {
278
+ dht_client_->set_external_ip(mapped->address);
279
+ }
280
+ if (dht_client_v6_) {
281
+ dht_client_v6_->set_external_ip(mapped->address);
282
+ }
283
+ }
284
+ }
285
+
222
286
  // Announce immediately - this also discovers peers during traversal
223
287
  announce_rats_peer();
224
288
 
@@ -261,7 +325,11 @@ void RatsClient::announce_rats_peer() {
261
325
 
262
326
  InfoHash info_hash = hex_to_node_id(discovery_hash);
263
327
 
264
- if (dht_client_->is_announce_active(info_hash)) {
328
+ // Skip only if every running DHT network already has this announce in flight.
329
+ // (announce_peer() also dedups internally, so this is just to avoid redundant work.)
330
+ bool v4_busy = !dht_client_ || !dht_client_->is_running() || dht_client_->is_announce_active(info_hash);
331
+ bool v6_busy = !dht_client_v6_ || !dht_client_v6_->is_running() || dht_client_v6_->is_announce_active(info_hash);
332
+ if (v4_busy && v6_busy) {
265
333
  LOG_CLIENT_WARN("Announce already in progress for info hash: " << node_id_to_hex(info_hash));
266
334
  return;
267
335
  }