librats 0.9.2 → 1.0.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.
@@ -132,6 +132,8 @@ set(LIBRARY_SOURCES
132
132
  src/socket.h
133
133
  src/network_utils.cpp
134
134
  src/network_utils.h
135
+ src/network_monitor.cpp
136
+ src/network_monitor.h
135
137
  src/dht.cpp
136
138
  src/dht.h
137
139
  src/bencode.cpp
@@ -468,6 +470,8 @@ if(RATS_BUILD_TESTS)
468
470
  tests/test_ice.cpp
469
471
  # Automatic port forwarding tests (UPnP/NAT-PMP)
470
472
  tests/test_portmap.cpp
473
+ # Network change detection tests
474
+ tests/test_network_monitor.cpp
471
475
  # I/O poller abstraction (epoll/kqueue/IOCP)
472
476
  tests/test_io_poller.cpp
473
477
  # Buffer utilities for file transfer and bittorrent
@@ -2894,7 +2894,30 @@ bool DhtClient::load_routing_table() {
2894
2894
  LOG_DHT_WARN("Unsupported routing table version: " << version);
2895
2895
  return false;
2896
2896
  }
2897
-
2897
+
2898
+ // Restore our own node ID so it stays stable across restarts (matches libtorrent's
2899
+ // calculate_node_id). The external address is not known yet at load time, which is
2900
+ // libtorrent's "external_address.is_unspecified()" case -> reuse the saved ID as long
2901
+ // as it is valid (non-zero). If the external IP is later discovered and the restored
2902
+ // ID is no longer valid for it (BEP 42), set_external_ip() regenerates it then.
2903
+ // Restored before bucketing the loaded nodes below, since get_bucket_index() is
2904
+ // computed relative to node_id_.
2905
+ if (routing_data.contains("node_id")) {
2906
+ try {
2907
+ NodeId saved_id = hex_to_node_id(routing_data["node_id"].get<std::string>());
2908
+ bool all_zeros = std::all_of(saved_id.begin(), saved_id.end(),
2909
+ [](uint8_t b) { return b == 0; });
2910
+ if (!all_zeros) {
2911
+ node_id_ = saved_id;
2912
+ LOG_DHT_INFO("Restored DHT node ID from disk: " << node_id_to_hex(node_id_));
2913
+ } else {
2914
+ LOG_DHT_WARN("Saved node ID is invalid (zero/malformed), keeping generated one");
2915
+ }
2916
+ } catch (const std::exception& e) {
2917
+ LOG_DHT_WARN("Failed to restore saved node ID, keeping generated one: " << e.what());
2918
+ }
2919
+ }
2920
+
2898
2921
  // Load nodes
2899
2922
  const auto& nodes_array = routing_data["nodes"];
2900
2923
  size_t loaded_count = 0;
@@ -1,6 +1,7 @@
1
1
  #include "librats.h"
2
2
  #include "os.h"
3
3
  #include "network_utils.h"
4
+ #include "network_monitor.h" // complete type for unique_ptr<NetworkMonitor> member
4
5
  #include "version.h"
5
6
  #include <algorithm>
6
7
  #include <array>
@@ -171,6 +172,10 @@ bool RatsClient::start() {
171
172
  // No-op when disabled; runs discovery/mapping on its own background threads.
172
173
  start_port_mapping();
173
174
 
175
+ // React to host network changes (IP/interface/route): renew port mappings,
176
+ // re-discover the public address and re-announce. No-op when disabled.
177
+ start_network_monitor();
178
+
174
179
  LOG_CLIENT_INFO("RatsClient started successfully on port " << listen_port_);
175
180
 
176
181
  // Attempt to reconnect to saved peers
@@ -202,6 +207,11 @@ void RatsClient::stop() {
202
207
 
203
208
  LOG_CLIENT_INFO("Stopping RatsClient");
204
209
 
210
+ // Stop network-change detection FIRST and join its recovery worker: that
211
+ // worker touches the port-mapping backends and the DHT clients, both torn
212
+ // down below, so it must not be running past this point.
213
+ stop_network_monitor();
214
+
205
215
  // Remove port mappings and stop UPnP/NAT-PMP backends before tearing down
206
216
  // sockets (best-effort cleanup so we don't leave stale router mappings).
207
217
  stop_port_mapping();
@@ -1387,22 +1397,34 @@ static constexpr std::array<std::string_view,5> localhost_addrs{"127.0.0.1", "::
1387
1397
 
1388
1398
  // Local interface address blocking methods
1389
1399
  void RatsClient::initialize_local_addresses() {
1390
- LOG_CLIENT_INFO("Initializing local interface addresses for connection blocking");
1391
-
1392
1400
  std::lock_guard<std::mutex> lock(local_addresses_mutex_);
1393
-
1394
- // Get all local interface addresses using network_utils
1401
+
1402
+ // (Re)enumerate the host's interface addresses. This is also called on every
1403
+ // network change, so it must be a diff, not a blind insert: drop auto-detected
1404
+ // addresses that have disappeared (a removed IP must stop being treated as
1405
+ // "ourselves"), while preserving localhost entries and any externally-
1406
+ // discovered / manually-ignored addresses (STUN reflexive, mapped external IP,
1407
+ // user add_ignored_address()), which live in the same set.
1395
1408
  auto addrs = network_utils::get_local_interface_addresses();
1396
- local_interface_addresses_.insert(addrs.begin(), addrs.end());
1397
-
1398
- // Add common localhost addresses
1409
+ std::unordered_set<std::string> new_auto(addrs.begin(), addrs.end());
1410
+
1411
+ for (const auto& old : auto_interface_addresses_) {
1412
+ if (new_auto.find(old) == new_auto.end()) {
1413
+ local_interface_addresses_.erase(old);
1414
+ }
1415
+ }
1416
+ for (const auto& addr : addrs) {
1417
+ local_interface_addresses_.insert(addr);
1418
+ }
1399
1419
  for (const auto& addr : localhost_addrs) {
1400
1420
  local_interface_addresses_.emplace(std::string(addr));
1401
1421
  }
1402
-
1403
- LOG_CLIENT_INFO("Found " << local_interface_addresses_.size() << " local addresses to block:");
1422
+ auto_interface_addresses_ = std::move(new_auto);
1423
+
1424
+ LOG_CLIENT_INFO("Local interface addresses: " << local_interface_addresses_.size()
1425
+ << " blocked (" << addrs.size() << " from interfaces)");
1404
1426
  for (const auto& addr : local_interface_addresses_) {
1405
- LOG_CLIENT_INFO(" - " << addr);
1427
+ LOG_CLIENT_DEBUG(" - " << addr);
1406
1428
  }
1407
1429
  }
1408
1430
 
@@ -38,6 +38,11 @@
38
38
 
39
39
  namespace librats {
40
40
 
41
+ // Detects host network configuration changes (defined in network_monitor.h).
42
+ // Forward-declared here; only used via unique_ptr, so RatsClient's destructor
43
+ // (defined in librats.cpp, which includes network_monitor.h) sees the full type.
44
+ class NetworkMonitor;
45
+
41
46
  /**
42
47
  * PeerIOContext - Per-peer async I/O buffers and framing state
43
48
  *
@@ -282,6 +287,10 @@ public:
282
287
  using DisconnectCallback = std::function<void(socket_t, const std::string&)>;
283
288
  using MessageCallback = std::function<void(const std::string&, const nlohmann::json&)>;
284
289
  using SendCallback = std::function<void(bool, const std::string&)>;
290
+ // Fired when the host's network configuration changes (interface up/down, IP
291
+ // added/removed, default route change). The argument is the new full list of
292
+ // local interface addresses. See on_network_changed().
293
+ using NetworkChangeCallback = std::function<void(const std::vector<std::string>& local_addresses)>;
285
294
 
286
295
  // =========================================================================
287
296
  // Constructor and Destructor
@@ -1478,6 +1487,35 @@ public:
1478
1487
  */
1479
1488
  void on_port_mapping(PortMapCallback callback);
1480
1489
 
1490
+ // =========================================================================
1491
+ // Network change detection
1492
+ // =========================================================================
1493
+ //
1494
+ // A long-lived node must notice when the host's connectivity changes (a new
1495
+ // interface, an IP added/removed, the default route flipping between Wi-Fi
1496
+ // and cellular, dock/undock, VPN up/down, wake-from-sleep) and recover:
1497
+ // renew router port mappings, re-discover its public address via STUN, and
1498
+ // re-announce to the DHT. Otherwise it keeps advertising a stale endpoint
1499
+ // until the next periodic refresh. Enabled by default. Implemented in
1500
+ // librats_portmap.cpp on top of the platform-specific NetworkMonitor.
1501
+
1502
+ /**
1503
+ * Enable or disable automatic reaction to host network changes. Enabled by
1504
+ * default. Can be called before or after start(): toggling while running
1505
+ * starts/stops the monitor immediately. Not persisted.
1506
+ */
1507
+ void set_network_change_detection_enabled(bool enabled);
1508
+ bool is_network_change_detection_enabled() const;
1509
+
1510
+ /**
1511
+ * Register a callback fired (debounced) whenever the set of local interface
1512
+ * addresses changes. The argument is the new full list of local addresses.
1513
+ * Invoked from the monitor's worker thread, so keep the handler quick. This
1514
+ * is in addition to — not a replacement for — the built-in recovery
1515
+ * (port re-mapping, STUN re-discovery, DHT re-announce).
1516
+ */
1517
+ void on_network_changed(NetworkChangeCallback callback);
1518
+
1481
1519
  #ifdef RATS_STORAGE
1482
1520
  // =========================================================================
1483
1521
  // Distributed Storage API (requires RATS_STORAGE)
@@ -1964,6 +2002,11 @@ private:
1964
2002
  // 6. io_mutex_ (I/O poller and send buffer access)
1965
2003
  // 7. message_handlers_mutex_ (Message handler registration)
1966
2004
  // 8. reconnect_mutex_ (Reconnection queue management)
2005
+ // 9. port_mapping_mutex_ (UPnP/NAT-PMP backends and mapped address)
2006
+ // 10. network_monitor_mutex_ / network_recovery_mutex_ (network-change state)
2007
+ //
2008
+ // (9) and (10) are leaf locks: they are never held while acquiring any lock
2009
+ // above, so they impose no additional ordering constraints.
1967
2010
  // =========================================================================
1968
2011
 
1969
2012
  // [1] Configuration persistence (protected by config_mutex_)
@@ -1988,7 +2031,12 @@ private:
1988
2031
  // [4] Local interface address blocking (protected by local_addresses_mutex_)
1989
2032
  mutable std::mutex local_addresses_mutex_; // [4] Protects local interface addresses
1990
2033
  std::unordered_set<std::string> local_interface_addresses_;
1991
-
2034
+ // Subset of local_interface_addresses_ that came from interface enumeration.
2035
+ // Tracked separately so a network-change refresh can drop only stale auto
2036
+ // entries without evicting localhost or externally-discovered addresses
2037
+ // (STUN reflexive, mapped external IP, user add_ignored_address()).
2038
+ std::unordered_set<std::string> auto_interface_addresses_;
2039
+
1992
2040
  // [5] Organized peer management using RatsPeer struct (protected by peers_mutex_)
1993
2041
  mutable std::mutex peers_mutex_; // [5] Protects peer data (most frequently locked)
1994
2042
  std::unordered_map<std::string, RatsPeer> peers_; // keyed by peer_id
@@ -2054,6 +2102,30 @@ private:
2054
2102
  // TCP mapping is established, otherwise the local listen port.
2055
2103
  uint16_t get_advertised_port() const;
2056
2104
 
2105
+ // Network change detection (implemented in librats_portmap.cpp). Monitor runs
2106
+ // a platform watcher; on a real address-set change it refreshes the self-
2107
+ // address set inline and wakes the recovery worker, which re-maps ports,
2108
+ // re-discovers the public IP and re-announces. These mutexes are last in the
2109
+ // lock order (after [8]) and are never held while calling into other
2110
+ // subsystems, so they introduce no new ordering constraints.
2111
+ std::unique_ptr<NetworkMonitor> network_monitor_;
2112
+ bool network_change_detection_enabled_ = true; // start the monitor on start()
2113
+ mutable std::mutex network_monitor_mutex_; // guards the user callback below
2114
+ NetworkChangeCallback network_change_callback_;
2115
+ // Dedicated recovery worker: serialises the slow recovery work (STUN can
2116
+ // block for seconds) off the monitor thread and coalesces rapid changes.
2117
+ std::thread network_recovery_thread_;
2118
+ std::mutex network_recovery_mutex_;
2119
+ std::condition_variable network_recovery_cv_;
2120
+ bool network_recovery_pending_ = false;
2121
+ bool network_recovery_stop_ = false;
2122
+
2123
+ void start_network_monitor();
2124
+ void stop_network_monitor();
2125
+ void network_recovery_loop();
2126
+ void handle_network_change(const std::vector<std::string>& current_addresses);
2127
+ void recover_after_network_change();
2128
+
2057
2129
  #ifdef RATS_STORAGE
2058
2130
  // Distributed storage manager (optional, requires RATS_STORAGE)
2059
2131
  std::unique_ptr<StorageManager> storage_manager_;
@@ -265,22 +265,19 @@ std::chrono::seconds RatsClient::calculate_discovery_interval() const {
265
265
 
266
266
  void RatsClient::automatic_discovery_loop() {
267
267
  LOG_CLIENT_INFO("Automatic peer discovery loop started");
268
-
269
- // Initial delay to let DHT bootstrap
270
- {
271
- std::unique_lock<std::mutex> lock(shutdown_mutex_);
272
- if (shutdown_cv_.wait_for(lock, std::chrono::seconds(INITIAL_DISCOVERY_DELAY_SECONDS), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
273
- LOG_CLIENT_INFO("Automatic peer discovery loop stopped during initial delay");
274
- return;
275
- }
276
- }
277
268
 
278
- // Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it before
279
- // we announce, so we announce under the correct ID. This runs on the discovery thread (not
280
- // a standalone one) on purpose: stop_dht_discovery() joins this thread via
281
- // stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so set_external_ip()
282
- // can never touch a destroyed DhtClient. It complements the in-DHT "ip"-field voting, which
283
- // keeps the node ID correct if the address changes or STUN is unavailable.
269
+ // Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it BEFORE
270
+ // the initial bootstrap delay below. The DHT starts receiving KRPC responses (each carrying
271
+ // our address in the "ip" field) immediately on bootstrap, and that in-DHT vote can reach
272
+ // EXTERNAL_IP_VOTE_THRESHOLD within a few seconds. Probing STUN first lets set_external_ip()
273
+ // adopt the address and regenerate the node ID up front, so the DHT bootstraps under the
274
+ // correct ID and the ip-field voting silently confirms the already-adopted address instead
275
+ // of racing it to consensus. The voting remains the fallback when STUN is unavailable or the
276
+ // address later changes.
277
+ //
278
+ // This runs on the discovery thread (not a standalone one) on purpose: stop_dht_discovery()
279
+ // joins this thread via stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so
280
+ // set_external_ip() can never touch a destroyed DhtClient.
284
281
  if (auto_discovery_running_.load() && running_.load()) {
285
282
  auto mapped = discover_public_address("stun.l.google.com", 19302, 4000);
286
283
  if (mapped) {
@@ -296,6 +293,15 @@ void RatsClient::automatic_discovery_loop() {
296
293
  }
297
294
  }
298
295
 
296
+ // Initial delay to let DHT bootstrap
297
+ {
298
+ std::unique_lock<std::mutex> lock(shutdown_mutex_);
299
+ if (shutdown_cv_.wait_for(lock, std::chrono::seconds(INITIAL_DISCOVERY_DELAY_SECONDS), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
300
+ LOG_CLIENT_INFO("Automatic peer discovery loop stopped during initial delay");
301
+ return;
302
+ }
303
+ }
304
+
299
305
  // Announce immediately - this also discovers peers during traversal
300
306
  announce_rats_peer();
301
307
 
@@ -9,7 +9,9 @@
9
9
  */
10
10
 
11
11
  #include "librats.h"
12
+ #include "librats_log_macros.h"
12
13
  #include "network_utils.h"
14
+ #include "network_monitor.h"
13
15
  #include "logger.h"
14
16
 
15
17
  namespace librats {
@@ -241,4 +243,177 @@ void RatsClient::stop_port_mapping() {
241
243
  if (natpmp) natpmp->stop();
242
244
  }
243
245
 
246
+ // ============================================================================
247
+ // Network change detection
248
+ // ============================================================================
249
+
250
+ void RatsClient::set_network_change_detection_enabled(bool enabled) {
251
+ bool was_enabled;
252
+ {
253
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
254
+ was_enabled = network_change_detection_enabled_;
255
+ network_change_detection_enabled_ = enabled;
256
+ }
257
+ if (was_enabled == enabled) {
258
+ return;
259
+ }
260
+ LOG_INFO("netmon", "Network change detection " << (enabled ? "enabled" : "disabled"));
261
+ if (running_.load()) {
262
+ if (enabled) {
263
+ start_network_monitor();
264
+ } else {
265
+ stop_network_monitor();
266
+ }
267
+ }
268
+ }
269
+
270
+ bool RatsClient::is_network_change_detection_enabled() const {
271
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
272
+ return network_change_detection_enabled_;
273
+ }
274
+
275
+ void RatsClient::on_network_changed(NetworkChangeCallback callback) {
276
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
277
+ network_change_callback_ = std::move(callback);
278
+ }
279
+
280
+ void RatsClient::start_network_monitor() {
281
+ {
282
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
283
+ if (!network_change_detection_enabled_ || network_monitor_) {
284
+ return; // disabled or already running
285
+ }
286
+ }
287
+
288
+ // Spin up the recovery worker before the monitor so it can never miss the
289
+ // first wake-up.
290
+ {
291
+ std::lock_guard<std::mutex> lock(network_recovery_mutex_);
292
+ network_recovery_stop_ = false;
293
+ network_recovery_pending_ = false;
294
+ }
295
+ network_recovery_thread_ = std::thread([this]() { network_recovery_loop(); });
296
+
297
+ auto monitor = std::make_unique<NetworkMonitor>();
298
+ monitor->start([this](const std::vector<std::string>& addrs) {
299
+ handle_network_change(addrs);
300
+ });
301
+ {
302
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
303
+ network_monitor_ = std::move(monitor);
304
+ }
305
+ LOG_INFO("netmon", "Network change detection active");
306
+ }
307
+
308
+ void RatsClient::stop_network_monitor() {
309
+ // Stop the OS watcher first so no further change events are queued.
310
+ std::unique_ptr<NetworkMonitor> monitor;
311
+ {
312
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
313
+ monitor = std::move(network_monitor_);
314
+ }
315
+ if (monitor) {
316
+ monitor->stop(); // joins the monitor's worker thread
317
+ }
318
+
319
+ // Then wake and join the recovery worker. Done after the monitor is down so
320
+ // no new recovery can be scheduled while we shut it down.
321
+ {
322
+ std::lock_guard<std::mutex> lock(network_recovery_mutex_);
323
+ network_recovery_stop_ = true;
324
+ }
325
+ network_recovery_cv_.notify_all();
326
+ if (network_recovery_thread_.joinable()) {
327
+ network_recovery_thread_.join();
328
+ }
329
+ }
330
+
331
+ void RatsClient::network_recovery_loop() {
332
+ while (true) {
333
+ {
334
+ std::unique_lock<std::mutex> lock(network_recovery_mutex_);
335
+ network_recovery_cv_.wait(lock, [this]() {
336
+ return network_recovery_pending_ || network_recovery_stop_;
337
+ });
338
+ if (network_recovery_stop_) {
339
+ break;
340
+ }
341
+ network_recovery_pending_ = false;
342
+ }
343
+ recover_after_network_change();
344
+ }
345
+ }
346
+
347
+ void RatsClient::handle_network_change(const std::vector<std::string>& current_addresses) {
348
+ if (!running_.load()) {
349
+ return;
350
+ }
351
+ LOG_CLIENT_INFO("Network change detected (" << current_addresses.size()
352
+ << " local address(es)); refreshing network state");
353
+
354
+ // Cheap and synchronous: refresh the self-address set so a freshly added
355
+ // local address isn't misjudged as a remote peer, and a removed one stops
356
+ // being blocked. (Diff-based; preserves STUN/mapped/ignored entries.)
357
+ initialize_local_addresses();
358
+
359
+ // Notify the application.
360
+ NetworkChangeCallback cb;
361
+ {
362
+ std::lock_guard<std::mutex> lock(network_monitor_mutex_);
363
+ cb = network_change_callback_;
364
+ }
365
+ if (cb) {
366
+ cb(current_addresses);
367
+ }
368
+
369
+ // Hand the slow recovery (port re-mapping + STUN + re-announce) to the
370
+ // dedicated worker so we don't block the monitor thread. Coalesced: if a
371
+ // recovery is already running, this just marks another pass is needed.
372
+ {
373
+ std::lock_guard<std::mutex> lock(network_recovery_mutex_);
374
+ network_recovery_pending_ = true;
375
+ }
376
+ network_recovery_cv_.notify_all();
377
+ }
378
+
379
+ void RatsClient::recover_after_network_change() {
380
+ if (!running_.load()) {
381
+ return;
382
+ }
383
+ LOG_CLIENT_INFO("Recovering after network change: renewing port mappings and re-announcing");
384
+
385
+ // 1. Renew router port mappings. Our LAN IP and/or the gateway likely
386
+ // changed, so existing UPnP/NAT-PMP leases are stale or aimed at the wrong
387
+ // internal address. Tear down and re-run discovery from scratch.
388
+ if (is_port_mapping_enabled()) {
389
+ stop_port_mapping();
390
+ if (running_.load()) {
391
+ start_port_mapping();
392
+ }
393
+ }
394
+
395
+ if (!running_.load()) {
396
+ return;
397
+ }
398
+
399
+ // 2. Re-discover our public address via STUN, update the BEP 42 node IDs and
400
+ // re-announce so the DHT advertises our current reachable endpoint rather
401
+ // than the one from the previous network. This runs on the recovery
402
+ // thread, which stop() joins before the DHT clients are destroyed, so the
403
+ // set_external_ip()/announce calls can never touch a freed DhtClient.
404
+ if (is_dht_running()) {
405
+ auto mapped = discover_public_address("stun.l.google.com", 19302, 4000);
406
+ if (mapped) {
407
+ if (dht_client_) dht_client_->set_external_ip(mapped->address);
408
+ if (dht_client_v6_) dht_client_v6_->set_external_ip(mapped->address);
409
+ LOG_CLIENT_INFO("Public address after network change: " << mapped->address);
410
+ } else {
411
+ LOG_CLIENT_DEBUG("STUN public address discovery failed after network change");
412
+ }
413
+ if (running_.load()) {
414
+ announce_rats_peer();
415
+ }
416
+ }
417
+ }
418
+
244
419
  } // namespace librats
@@ -0,0 +1,367 @@
1
+ /**
2
+ * @file network_monitor.cpp
3
+ * @brief Platform backends for NetworkMonitor (see network_monitor.h).
4
+ */
5
+
6
+ // socket.h pulls in winsock2.h/ws2tcpip.h first on Windows (must precede the
7
+ // iphlpapi / windows headers below to avoid the classic winsock2/windows.h clash).
8
+ #include "socket.h"
9
+ #include "network_monitor.h"
10
+ #include "network_utils.h"
11
+ #include "logger.h"
12
+
13
+ #include <algorithm>
14
+
15
+ #ifdef _WIN32
16
+ #include <iphlpapi.h>
17
+ #include <netioapi.h>
18
+ #elif defined(__linux__)
19
+ #include <sys/socket.h>
20
+ #include <linux/netlink.h>
21
+ #include <linux/rtnetlink.h>
22
+ #include <unistd.h>
23
+ #include <poll.h>
24
+ #include <cstring>
25
+ #include <cerrno>
26
+ #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
27
+ defined(__OpenBSD__) || defined(__DragonFly__)
28
+ #define RATS_MONITOR_BSD_ROUTES 1
29
+ #include <sys/types.h>
30
+ #include <sys/socket.h>
31
+ #include <net/route.h>
32
+ #include <net/if.h>
33
+ #include <unistd.h>
34
+ #include <poll.h>
35
+ #include <cerrno>
36
+ #endif
37
+
38
+ #define LOG_NETMON_DEBUG(message) LOG_DEBUG("netmon", message)
39
+ #define LOG_NETMON_INFO(message) LOG_INFO("netmon", message)
40
+ #define LOG_NETMON_WARN(message) LOG_WARN("netmon", message)
41
+ #define LOG_NETMON_ERROR(message) LOG_ERROR("netmon", message)
42
+
43
+ namespace librats {
44
+
45
+ namespace {
46
+
47
+ std::vector<std::string> snapshot_addresses() {
48
+ auto addrs = network_utils::get_local_interface_addresses();
49
+ std::sort(addrs.begin(), addrs.end());
50
+ addrs.erase(std::unique(addrs.begin(), addrs.end()), addrs.end());
51
+ return addrs;
52
+ }
53
+
54
+ } // namespace
55
+
56
+ // ============================================================================
57
+ // Platform backend state
58
+ // ============================================================================
59
+
60
+ struct NetworkMonitor::Impl {
61
+ #ifdef _WIN32
62
+ HANDLE handle = nullptr;
63
+ #elif defined(__linux__) || defined(RATS_MONITOR_BSD_ROUTES)
64
+ int fd = -1; // netlink (Linux) or PF_ROUTE (BSD) socket
65
+ int stop_pipe[2] = {-1, -1};
66
+ std::thread reader;
67
+ #endif
68
+ };
69
+
70
+ #ifdef _WIN32
71
+ // NotifyUnicastIpAddressChange invokes this from an OS worker thread on any
72
+ // unicast address add/remove/change. Windows gives no usable detail here, so we
73
+ // just trigger a re-enumeration (matches the cross-platform "something changed"
74
+ // contract). Must be __stdcall (WINAPI) to match PUNICAST_IPADDRESS_CHANGE_CALLBACK.
75
+ static void WINAPI rats_ip_change_cb(void* ctx, MIB_UNICASTIPADDRESS_ROW* /*row*/,
76
+ MIB_NOTIFICATION_TYPE /*type*/) {
77
+ auto* self = static_cast<NetworkMonitor*>(ctx);
78
+ if (self) self->check_now();
79
+ }
80
+ #endif
81
+
82
+ // ============================================================================
83
+ // Lifecycle
84
+ // ============================================================================
85
+
86
+ NetworkMonitor::NetworkMonitor() = default;
87
+
88
+ NetworkMonitor::~NetworkMonitor() {
89
+ stop();
90
+ }
91
+
92
+ bool NetworkMonitor::start(ChangeCallback on_change) {
93
+ if (running_.exchange(true)) {
94
+ return event_backend_active_;
95
+ }
96
+ on_change_ = std::move(on_change);
97
+ impl_ = std::make_unique<Impl>();
98
+ last_addresses_ = snapshot_addresses();
99
+
100
+ event_backend_active_ = backend_start();
101
+ LOG_NETMON_INFO("Network monitor started ("
102
+ << (event_backend_active_ ? "event-driven" : "polling")
103
+ << ", " << last_addresses_.size() << " local address(es))");
104
+
105
+ worker_ = std::thread([this]() { worker_loop(); });
106
+ return event_backend_active_;
107
+ }
108
+
109
+ void NetworkMonitor::stop() {
110
+ if (!running_.exchange(false)) {
111
+ return;
112
+ }
113
+ cv_.notify_all(); // wake the worker out of its wait
114
+ backend_stop(); // stop OS notifications / join the reader thread
115
+ if (worker_.joinable()) {
116
+ worker_.join();
117
+ }
118
+ impl_.reset();
119
+ event_backend_active_ = false;
120
+ LOG_NETMON_INFO("Network monitor stopped");
121
+ }
122
+
123
+ void NetworkMonitor::check_now() {
124
+ if (!running_.load()) return;
125
+ {
126
+ std::lock_guard<std::mutex> lock(mutex_);
127
+ change_pending_ = true;
128
+ }
129
+ cv_.notify_all();
130
+ }
131
+
132
+ // ============================================================================
133
+ // Worker: debounce + diff + dispatch
134
+ // ============================================================================
135
+
136
+ void NetworkMonitor::worker_loop() {
137
+ // When push notifications are active, the long interval is just a safety net
138
+ // for events the OS might drop (e.g. across suspend/resume). Without them,
139
+ // this interval is the actual detection latency.
140
+ const auto poll_interval = event_backend_active_
141
+ ? std::chrono::milliseconds(30000)
142
+ : std::chrono::milliseconds(5000);
143
+
144
+ while (running_.load()) {
145
+ bool was_event = false;
146
+ {
147
+ std::unique_lock<std::mutex> lock(mutex_);
148
+ cv_.wait_for(lock, poll_interval,
149
+ [this]() { return !running_.load() || change_pending_; });
150
+ if (!running_.load()) break;
151
+
152
+ was_event = change_pending_;
153
+ change_pending_ = false;
154
+
155
+ if (was_event) {
156
+ // Coalesce the burst: wait out a quiet debounce window (only a
157
+ // stop interrupts it), then drop any events that arrived during it.
158
+ cv_.wait_for(lock, debounce_, [this]() { return !running_.load(); });
159
+ if (!running_.load()) break;
160
+ change_pending_ = false;
161
+ }
162
+ }
163
+
164
+ auto current = snapshot_addresses();
165
+ if (current != last_addresses_) {
166
+ LOG_NETMON_INFO("Local address set changed (" << last_addresses_.size()
167
+ << " -> " << current.size() << ")");
168
+ last_addresses_ = current;
169
+ if (on_change_) {
170
+ on_change_(current);
171
+ }
172
+ } else if (was_event) {
173
+ LOG_NETMON_DEBUG("Network event with no effective address change; ignored");
174
+ }
175
+ }
176
+ }
177
+
178
+ // ============================================================================
179
+ // Windows backend: NotifyUnicastIpAddressChange
180
+ // ============================================================================
181
+ #ifdef _WIN32
182
+
183
+ bool NetworkMonitor::backend_start() {
184
+ DWORD rv = NotifyUnicastIpAddressChange(AF_UNSPEC, &rats_ip_change_cb, this,
185
+ FALSE, &impl_->handle);
186
+ if (rv != NO_ERROR) {
187
+ LOG_NETMON_WARN("NotifyUnicastIpAddressChange failed (" << rv
188
+ << "); falling back to polling");
189
+ impl_->handle = nullptr;
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+
195
+ void NetworkMonitor::backend_stop() {
196
+ if (impl_ && impl_->handle != nullptr) {
197
+ // Cancels and waits for any in-flight callback to return, so no callback
198
+ // can run against a half-destroyed monitor afterwards.
199
+ CancelMibChangeNotify2(impl_->handle);
200
+ impl_->handle = nullptr;
201
+ }
202
+ }
203
+
204
+ // ============================================================================
205
+ // Linux backend: NETLINK_ROUTE socket
206
+ // ============================================================================
207
+ #elif defined(__linux__)
208
+
209
+ bool NetworkMonitor::backend_start() {
210
+ int fd = ::socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
211
+ if (fd < 0) {
212
+ LOG_NETMON_WARN("netlink socket() failed (" << errno << "); falling back to polling");
213
+ return false;
214
+ }
215
+
216
+ sockaddr_nl addr{};
217
+ addr.nl_family = AF_NETLINK;
218
+ addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR
219
+ | RTMGRP_LINK | RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE;
220
+ if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
221
+ LOG_NETMON_WARN("netlink bind() failed (" << errno << "); falling back to polling");
222
+ ::close(fd);
223
+ return false;
224
+ }
225
+
226
+ if (::pipe(impl_->stop_pipe) < 0) {
227
+ ::close(fd);
228
+ return false;
229
+ }
230
+ impl_->fd = fd;
231
+
232
+ impl_->reader = std::thread([this]() {
233
+ char buf[4096];
234
+ struct pollfd fds[2];
235
+ fds[0].fd = impl_->fd; fds[0].events = POLLIN;
236
+ fds[1].fd = impl_->stop_pipe[0]; fds[1].events = POLLIN;
237
+
238
+ while (running_.load()) {
239
+ int pr = ::poll(fds, 2, -1);
240
+ if (pr < 0) {
241
+ if (errno == EINTR) continue;
242
+ break;
243
+ }
244
+ if (fds[1].revents & POLLIN) break; // stop requested
245
+ if (!(fds[0].revents & POLLIN)) continue;
246
+
247
+ ssize_t len = ::recv(impl_->fd, buf, sizeof(buf), 0);
248
+ if (len <= 0) {
249
+ // ENOBUFS just means we missed messages under load — treat as a change.
250
+ if (len < 0 && errno == ENOBUFS) { check_now(); continue; }
251
+ if (len < 0 && errno == EINTR) continue;
252
+ break;
253
+ }
254
+
255
+ bool pertinent = false;
256
+ for (auto* nh = reinterpret_cast<nlmsghdr*>(buf);
257
+ NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
258
+ switch (nh->nlmsg_type) {
259
+ case RTM_NEWADDR: case RTM_DELADDR:
260
+ case RTM_NEWLINK: case RTM_DELLINK:
261
+ case RTM_NEWROUTE: case RTM_DELROUTE:
262
+ pertinent = true;
263
+ break;
264
+ default:
265
+ break;
266
+ }
267
+ }
268
+ if (pertinent) check_now();
269
+ }
270
+ });
271
+ return true;
272
+ }
273
+
274
+ void NetworkMonitor::backend_stop() {
275
+ if (!impl_) return;
276
+ if (impl_->stop_pipe[1] >= 0) {
277
+ char b = 1;
278
+ ssize_t n = ::write(impl_->stop_pipe[1], &b, 1);
279
+ (void)n;
280
+ }
281
+ if (impl_->reader.joinable()) impl_->reader.join();
282
+ if (impl_->fd >= 0) { ::close(impl_->fd); impl_->fd = -1; }
283
+ if (impl_->stop_pipe[0] >= 0) { ::close(impl_->stop_pipe[0]); impl_->stop_pipe[0] = -1; }
284
+ if (impl_->stop_pipe[1] >= 0) { ::close(impl_->stop_pipe[1]); impl_->stop_pipe[1] = -1; }
285
+ }
286
+
287
+ // ============================================================================
288
+ // macOS / BSD backend: PF_ROUTE routing socket
289
+ // ============================================================================
290
+ #elif defined(RATS_MONITOR_BSD_ROUTES)
291
+
292
+ bool NetworkMonitor::backend_start() {
293
+ int fd = ::socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC);
294
+ if (fd < 0) {
295
+ LOG_NETMON_WARN("PF_ROUTE socket() failed (" << errno << "); falling back to polling");
296
+ return false;
297
+ }
298
+ if (::pipe(impl_->stop_pipe) < 0) {
299
+ ::close(fd);
300
+ return false;
301
+ }
302
+ impl_->fd = fd;
303
+
304
+ impl_->reader = std::thread([this]() {
305
+ char buf[2048];
306
+ struct pollfd fds[2];
307
+ fds[0].fd = impl_->fd; fds[0].events = POLLIN;
308
+ fds[1].fd = impl_->stop_pipe[0]; fds[1].events = POLLIN;
309
+
310
+ while (running_.load()) {
311
+ int pr = ::poll(fds, 2, -1);
312
+ if (pr < 0) {
313
+ if (errno == EINTR) continue;
314
+ break;
315
+ }
316
+ if (fds[1].revents & POLLIN) break; // stop requested
317
+ if (!(fds[0].revents & POLLIN)) continue;
318
+
319
+ ssize_t len = ::read(impl_->fd, buf, sizeof(buf));
320
+ if (len <= 0) {
321
+ if (len < 0 && errno == EINTR) continue;
322
+ break;
323
+ }
324
+ if (static_cast<size_t>(len) < sizeof(rt_msghdr)) continue;
325
+
326
+ auto* rtm = reinterpret_cast<rt_msghdr*>(buf);
327
+ switch (rtm->rtm_type) {
328
+ case RTM_NEWADDR: case RTM_DELADDR:
329
+ case RTM_IFINFO:
330
+ #ifdef RTM_IFANNOUNCE
331
+ case RTM_IFANNOUNCE:
332
+ #endif
333
+ case RTM_ADD: case RTM_DELETE: case RTM_CHANGE:
334
+ check_now();
335
+ break;
336
+ default:
337
+ break;
338
+ }
339
+ }
340
+ });
341
+ return true;
342
+ }
343
+
344
+ void NetworkMonitor::backend_stop() {
345
+ if (!impl_) return;
346
+ if (impl_->stop_pipe[1] >= 0) {
347
+ char b = 1;
348
+ ssize_t n = ::write(impl_->stop_pipe[1], &b, 1);
349
+ (void)n;
350
+ }
351
+ if (impl_->reader.joinable()) impl_->reader.join();
352
+ if (impl_->fd >= 0) { ::close(impl_->fd); impl_->fd = -1; }
353
+ if (impl_->stop_pipe[0] >= 0) { ::close(impl_->stop_pipe[0]); impl_->stop_pipe[0] = -1; }
354
+ if (impl_->stop_pipe[1] >= 0) { ::close(impl_->stop_pipe[1]); impl_->stop_pipe[1] = -1; }
355
+ }
356
+
357
+ // ============================================================================
358
+ // Fallback backend: polling only (worker_loop diffs on its poll interval)
359
+ // ============================================================================
360
+ #else
361
+
362
+ bool NetworkMonitor::backend_start() { return false; }
363
+ void NetworkMonitor::backend_stop() {}
364
+
365
+ #endif
366
+
367
+ } // namespace librats
@@ -0,0 +1,104 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file network_monitor.h
5
+ * @brief Detects host network configuration changes (IP / interface / route).
6
+ *
7
+ * When the machine's connectivity changes — a new interface comes up, an IP
8
+ * address is added or removed, the default route flips (Wi-Fi <-> cellular,
9
+ * dock/undock, VPN up/down, wake-from-sleep) — a long-lived P2P node must react:
10
+ * re-create router port mappings, re-discover its public address via STUN, and
11
+ * re-announce to the DHT. Otherwise it keeps advertising a stale, unreachable
12
+ * endpoint until the next periodic refresh.
13
+ *
14
+ * NetworkMonitor provides an event-driven signal for exactly that. The design
15
+ * mirrors libtorrent's aux::ip_notifier but is adapted to librats' own threading
16
+ * model (no boost::asio): each platform backend only signals "something changed",
17
+ * and the monitor itself re-enumerates the local interface addresses and invokes
18
+ * the callback ONLY when the address set actually differs. Bursts of OS events
19
+ * (a single interface transition typically emits several) are coalesced with a
20
+ * short debounce window.
21
+ *
22
+ * Platform backends:
23
+ * - Windows: NotifyUnicastIpAddressChange() (iphlpapi)
24
+ * - Linux: NETLINK_ROUTE socket, RTMGRP_*_IFADDR / *_LINK groups
25
+ * - macOS/BSD: PF_ROUTE routing socket
26
+ * - otherwise: periodic polling of the interface address list
27
+ *
28
+ * The callback runs on the monitor's worker thread, so it must not block for
29
+ * long; offload slow recovery work (STUN, port mapping) to another thread.
30
+ */
31
+
32
+ #include <atomic>
33
+ #include <chrono>
34
+ #include <condition_variable>
35
+ #include <functional>
36
+ #include <memory>
37
+ #include <mutex>
38
+ #include <string>
39
+ #include <thread>
40
+ #include <vector>
41
+
42
+ namespace librats {
43
+
44
+ class NetworkMonitor {
45
+ public:
46
+ /// Invoked (debounced) whenever the set of local interface addresses changes.
47
+ /// @param current_addresses the new, full list of local interface addresses.
48
+ using ChangeCallback = std::function<void(const std::vector<std::string>& current_addresses)>;
49
+
50
+ NetworkMonitor();
51
+ ~NetworkMonitor();
52
+
53
+ NetworkMonitor(const NetworkMonitor&) = delete;
54
+ NetworkMonitor& operator=(const NetworkMonitor&) = delete;
55
+
56
+ /**
57
+ * Start monitoring. The callback fires on each detected change (after the
58
+ * debounce window) on the monitor's worker thread.
59
+ *
60
+ * @return true if an OS push-notification backend is active; false if the
61
+ * monitor fell back to periodic polling. The monitor works either
62
+ * way, so the return value is informational only.
63
+ */
64
+ bool start(ChangeCallback on_change);
65
+
66
+ /// Stop monitoring and join the worker thread. Idempotent.
67
+ void stop();
68
+
69
+ bool is_running() const { return running_.load(); }
70
+
71
+ /// Whether OS push notifications (vs. polling) are in use. Valid after start().
72
+ bool is_event_driven() const { return event_backend_active_; }
73
+
74
+ /**
75
+ * Request an immediate re-check, coalesced/debounced exactly like a real OS
76
+ * event. Thread-safe. Useful to call after the device wakes from sleep, or
77
+ * from the platform backends themselves. A no-op if not running.
78
+ */
79
+ void check_now();
80
+
81
+ /// Override the debounce window used to coalesce event bursts (default 2s).
82
+ void set_debounce(std::chrono::milliseconds d) { debounce_ = d; }
83
+
84
+ private:
85
+ void worker_loop();
86
+ bool backend_start(); // set up the OS notifier; returns true if event-driven
87
+ void backend_stop(); // tear the OS notifier / reader thread down
88
+
89
+ ChangeCallback on_change_;
90
+ std::atomic<bool> running_{false};
91
+ bool change_pending_ = false; // guarded by mutex_
92
+ bool event_backend_active_ = false; // true once an OS backend is confirmed up
93
+ std::thread worker_;
94
+ std::mutex mutex_;
95
+ std::condition_variable cv_;
96
+ std::chrono::milliseconds debounce_{2000};
97
+ std::vector<std::string> last_addresses_;
98
+
99
+ // Platform-specific backend state (fds, OS handles, reader thread).
100
+ struct Impl;
101
+ std::unique_ptr<Impl> impl_;
102
+ };
103
+
104
+ } // namespace librats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "0.9.2",
3
+ "version": "1.0.1",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",