librats 0.9.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -212,6 +214,14 @@ set(LIBRARY_SOURCES
212
214
  src/ice.cpp
213
215
  src/ice.h
214
216
  src/librats_ice.cpp
217
+
218
+ # Automatic port forwarding (UPnP IGD / NAT-PMP)
219
+ src/port_mapping.h
220
+ src/upnp.cpp
221
+ src/upnp.h
222
+ src/natpmp.cpp
223
+ src/natpmp.h
224
+ src/librats_portmap.cpp
215
225
  )
216
226
 
217
227
  # Add BitTorrent sources if RATS_SEARCH_FEATURES is enabled
@@ -458,6 +468,10 @@ if(RATS_BUILD_TESTS)
458
468
  tests/test_stun.cpp
459
469
  tests/test_turn.cpp
460
470
  tests/test_ice.cpp
471
+ # Automatic port forwarding tests (UPnP/NAT-PMP)
472
+ tests/test_portmap.cpp
473
+ # Network change detection tests
474
+ tests/test_network_monitor.cpp
461
475
  # I/O poller abstraction (epoll/kqueue/IOCP)
462
476
  tests/test_io_poller.cpp
463
477
  # Buffer utilities for file transfer and bittorrent
@@ -1258,38 +1258,9 @@ bool bep42_prefix(const std::string& ip, uint8_t seed, uint8_t prefix[3]) {
1258
1258
  } // namespace
1259
1259
 
1260
1260
  bool DhtClient::is_public_address(const std::string& ip) {
1261
- if (ip.empty()) return false;
1262
-
1263
- if (network_utils::is_valid_ipv6(ip)) {
1264
- struct in6_addr a;
1265
- if (inet_pton(AF_INET6, ip.c_str(), &a) != 1) return false;
1266
- const uint8_t* b = a.s6_addr;
1267
- bool all_zero = true;
1268
- for (int i = 0; i < 16; ++i) { if (b[i]) { all_zero = false; break; } }
1269
- if (all_zero) return false; // :: (unspecified)
1270
- bool loopback = (b[15] == 1);
1271
- for (int i = 0; i < 15; ++i) { if (b[i]) { loopback = false; break; } }
1272
- if (loopback) return false; // ::1
1273
- if ((b[0] & 0xfe) == 0xfc) return false; // fc00::/7 unique local
1274
- if (b[0] == 0xfe && (b[1] & 0xc0) == 0x80) return false; // fe80::/10 link-local
1275
- if (b[0] == 0xff) return false; // ff00::/8 multicast
1276
- return true;
1277
- }
1278
-
1279
- struct in_addr a;
1280
- if (inet_pton(AF_INET, ip.c_str(), &a) != 1) return false;
1281
- uint32_t h = ntohl(a.s_addr);
1282
- uint8_t o1 = static_cast<uint8_t>((h >> 24) & 0xff);
1283
- uint8_t o2 = static_cast<uint8_t>((h >> 16) & 0xff);
1284
- if (o1 == 0) return false; // 0.0.0.0/8
1285
- if (o1 == 127) return false; // loopback
1286
- if (o1 == 10) return false; // 10.0.0.0/8
1287
- if (o1 == 172 && o2 >= 16 && o2 <= 31) return false; // 172.16.0.0/12
1288
- if (o1 == 192 && o2 == 168) return false; // 192.168.0.0/16
1289
- if (o1 == 169 && o2 == 254) return false; // 169.254.0.0/16 link-local
1290
- if (o1 == 100 && o2 >= 64 && o2 <= 127) return false; // 100.64.0.0/10 CGNAT
1291
- if (o1 >= 224) return false; // multicast / reserved
1292
- return true;
1261
+ // Shared with automatic port forwarding; the canonical classifier lives in
1262
+ // network_utils so both subsystems agree on what "publicly routable" means.
1263
+ return network_utils::is_public_ip(ip);
1293
1264
  }
1294
1265
 
1295
1266
  bool DhtClient::generate_node_id_from_ip(const std::string& ip, NodeId& out, std::mt19937& gen) {
@@ -298,7 +298,15 @@ public:
298
298
  * @return true if running, false otherwise
299
299
  */
300
300
  bool is_running() const { return running_; }
301
-
301
+
302
+ /**
303
+ * Actual bound UDP port. May differ from the requested port if it was taken and
304
+ * the client fell back to an ephemeral port (see start()). Use this — not the
305
+ * requested port — when forwarding the DHT port through a router.
306
+ * @return bound port, or 0 if not started
307
+ */
308
+ uint16_t get_port() const { return static_cast<uint16_t>(port_); }
309
+
302
310
  #ifdef RATS_SEARCH_FEATURES
303
311
  // ============================================================================
304
312
  // SPIDER MODE - Aggressive node discovery and announce collection
@@ -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>
@@ -166,7 +167,15 @@ bool RatsClient::start() {
166
167
  if (gossipsub_ && !gossipsub_->start()) {
167
168
  LOG_CLIENT_WARN("Failed to start GossipSub - continuing without it");
168
169
  }
169
-
170
+
171
+ // Start automatic port forwarding (UPnP/NAT-PMP) for the bound listen port.
172
+ // No-op when disabled; runs discovery/mapping on its own background threads.
173
+ start_port_mapping();
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
+
170
179
  LOG_CLIENT_INFO("RatsClient started successfully on port " << listen_port_);
171
180
 
172
181
  // Attempt to reconnect to saved peers
@@ -197,7 +206,16 @@ void RatsClient::stop() {
197
206
  }
198
207
 
199
208
  LOG_CLIENT_INFO("Stopping RatsClient");
200
-
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
+
215
+ // Remove port mappings and stop UPnP/NAT-PMP backends before tearing down
216
+ // sockets (best-effort cleanup so we don't leave stale router mappings).
217
+ stop_port_mapping();
218
+
201
219
  // Stop GossipSub (can broadcast stop message)
202
220
  if (gossipsub_) {
203
221
  gossipsub_->stop();
@@ -1379,22 +1397,34 @@ static constexpr std::array<std::string_view,5> localhost_addrs{"127.0.0.1", "::
1379
1397
 
1380
1398
  // Local interface address blocking methods
1381
1399
  void RatsClient::initialize_local_addresses() {
1382
- LOG_CLIENT_INFO("Initializing local interface addresses for connection blocking");
1383
-
1384
1400
  std::lock_guard<std::mutex> lock(local_addresses_mutex_);
1385
-
1386
- // 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.
1387
1408
  auto addrs = network_utils::get_local_interface_addresses();
1388
- local_interface_addresses_.insert(addrs.begin(), addrs.end());
1389
-
1390
- // 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
+ }
1391
1419
  for (const auto& addr : localhost_addrs) {
1392
1420
  local_interface_addresses_.emplace(std::string(addr));
1393
1421
  }
1394
-
1395
- 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)");
1396
1426
  for (const auto& addr : local_interface_addresses_) {
1397
- LOG_CLIENT_INFO(" - " << addr);
1427
+ LOG_CLIENT_DEBUG(" - " << addr);
1398
1428
  }
1399
1429
  }
1400
1430
 
@@ -9,6 +9,8 @@
9
9
  #include "file_transfer.h" // File transfer functionality
10
10
  #include "noise.h" // Noise Protocol encryption
11
11
  #include "ice.h" // ICE-lite NAT traversal
12
+ #include "upnp.h" // UPnP IGD automatic port forwarding
13
+ #include "natpmp.h" // NAT-PMP automatic port forwarding
12
14
  #include "io_poller.h" // Platform-optimal I/O multiplexing
13
15
  #include "receive_buffer.h" // Efficient receive buffer for async I/O
14
16
  #include "chained_send_buffer.h" // Zero-copy chained send buffer
@@ -36,6 +38,11 @@
36
38
 
37
39
  namespace librats {
38
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
+
39
46
  /**
40
47
  * PeerIOContext - Per-peer async I/O buffers and framing state
41
48
  *
@@ -280,6 +287,10 @@ public:
280
287
  using DisconnectCallback = std::function<void(socket_t, const std::string&)>;
281
288
  using MessageCallback = std::function<void(const std::string&, const nlohmann::json&)>;
282
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)>;
283
294
 
284
295
  // =========================================================================
285
296
  // Constructor and Destructor
@@ -1432,6 +1443,79 @@ public:
1432
1443
  */
1433
1444
  void restart_ice();
1434
1445
 
1446
+ // =========================================================================
1447
+ // Automatic Port Forwarding API (UPnP IGD + NAT-PMP)
1448
+ // =========================================================================
1449
+ //
1450
+ // When enabled (the default), RatsClient asks the home router to forward the
1451
+ // TCP listen port on startup, using UPnP and NAT-PMP in parallel (whichever
1452
+ // the router supports wins). Mappings are refreshed automatically and removed
1453
+ // on stop(). This lets peers behind a NAT accept inbound connections without
1454
+ // manual router configuration.
1455
+
1456
+ /**
1457
+ * Enable or disable automatic port forwarding. If toggled while running, the
1458
+ * port mapping backends are started or stopped immediately. The setting is
1459
+ * persisted to config.json.
1460
+ */
1461
+ void set_port_mapping_enabled(bool enabled);
1462
+
1463
+ /// Whether automatic port forwarding is currently enabled.
1464
+ bool is_port_mapping_enabled() const;
1465
+
1466
+ /// Replace the full port mapping configuration (takes effect on next start).
1467
+ void set_port_mapping_config(const PortMappingConfig& config);
1468
+
1469
+ /// Get the current port mapping configuration.
1470
+ PortMappingConfig get_port_mapping_config() const;
1471
+
1472
+ /**
1473
+ * Request an additional port mapping beyond the automatic listen-port mapping
1474
+ * (e.g. a DHT UDP port). Has effect only while port mapping is enabled.
1475
+ */
1476
+ void add_port_mapping(PortMapProtocol protocol, uint16_t port);
1477
+
1478
+ /**
1479
+ * Get the public (external) address discovered by the port mapping backends.
1480
+ * @return {external_ip, external_port} if a mapping is active, otherwise nullopt
1481
+ */
1482
+ std::optional<std::pair<std::string, uint16_t>> get_mapped_public_address() const;
1483
+
1484
+ /**
1485
+ * Register a callback fired whenever a port mapping is established, refreshed,
1486
+ * removed or fails (invoked from a backend worker thread).
1487
+ */
1488
+ void on_port_mapping(PortMapCallback callback);
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
+
1435
1519
  #ifdef RATS_STORAGE
1436
1520
  // =========================================================================
1437
1521
  // Distributed Storage API (requires RATS_STORAGE)
@@ -1918,6 +2002,11 @@ private:
1918
2002
  // 6. io_mutex_ (I/O poller and send buffer access)
1919
2003
  // 7. message_handlers_mutex_ (Message handler registration)
1920
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.
1921
2010
  // =========================================================================
1922
2011
 
1923
2012
  // [1] Configuration persistence (protected by config_mutex_)
@@ -1942,7 +2031,12 @@ private:
1942
2031
  // [4] Local interface address blocking (protected by local_addresses_mutex_)
1943
2032
  mutable std::mutex local_addresses_mutex_; // [4] Protects local interface addresses
1944
2033
  std::unordered_set<std::string> local_interface_addresses_;
1945
-
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
+
1946
2040
  // [5] Organized peer management using RatsPeer struct (protected by peers_mutex_)
1947
2041
  mutable std::mutex peers_mutex_; // [5] Protects peer data (most frequently locked)
1948
2042
  std::unordered_map<std::string, RatsPeer> peers_; // keyed by peer_id
@@ -1981,7 +2075,57 @@ private:
1981
2075
 
1982
2076
  // ICE manager for NAT traversal
1983
2077
  std::unique_ptr<IceManager> ice_manager_;
1984
-
2078
+
2079
+ // Automatic port forwarding (UPnP IGD + NAT-PMP). Implemented in librats_portmap.cpp.
2080
+ mutable std::mutex port_mapping_mutex_; // guards the fields below
2081
+ PortMappingConfig port_mapping_config_;
2082
+ std::unique_ptr<UpnpClient> upnp_client_;
2083
+ std::unique_ptr<NatPmpClient> natpmp_client_;
2084
+ PortMapCallback port_mapping_callback_;
2085
+ // Public address discovered by the backends. The external port is tracked per
2086
+ // protocol (like libtorrent's per-listen-socket tcp/udp port mappings): the TCP
2087
+ // mapping forwards the peer listen port, the UDP mapping forwards the DHT port.
2088
+ // A single field would let the two backend callbacks clobber each other.
2089
+ std::string mapped_external_ip_;
2090
+ uint16_t mapped_external_tcp_port_ = 0; // public port for the TCP peer-listen port
2091
+ uint16_t mapped_external_udp_port_ = 0; // public port for the UDP DHT port
2092
+ // Set once we warn that the gateway's reported external IP is itself private
2093
+ // (double-NAT), so the warning isn't repeated on every lease refresh.
2094
+ bool double_nat_warning_logged_ = false;
2095
+
2096
+ // Start/stop the port mapping backends (no-ops if disabled). Called from
2097
+ // start()/stop(); safe to call repeatedly.
2098
+ void start_port_mapping();
2099
+ void stop_port_mapping();
2100
+ void handle_port_mapping_result(const PortMapResult& result);
2101
+ // Public TCP port to advertise to peers/DHT: the mapped external port once a
2102
+ // TCP mapping is established, otherwise the local listen port.
2103
+ uint16_t get_advertised_port() const;
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
+
1985
2129
  #ifdef RATS_STORAGE
1986
2130
  // Distributed storage manager (optional, requires RATS_STORAGE)
1987
2131
  std::unique_ptr<StorageManager> storage_manager_;
@@ -43,6 +43,19 @@ bool RatsClient::start_dht_discovery(int dht_port) {
43
43
  dht_client_v6_.reset();
44
44
  }
45
45
 
46
+ // Forward the DHT's UDP port through the router, in addition to the TCP peer
47
+ // port mapped at start(). Like libtorrent (which maps both the TCP listen port
48
+ // and the UDP DHT/uTP port per listen socket), this makes the node reachable by
49
+ // inbound DHT traffic behind NAT. No-op when port mapping is disabled; the
50
+ // mapping uses external==internal so outgoing queries and inbound packets share
51
+ // one public port. The backends wake their workers to install it immediately.
52
+ // Use the *actual* bound port: start() may have fallen back to an ephemeral one
53
+ // when the requested dht_port was taken.
54
+ uint16_t bound_dht_port = dht_client_->get_port();
55
+ if (bound_dht_port != 0) {
56
+ add_port_mapping(PortMapProtocol::UDP, bound_dht_port);
57
+ }
58
+
46
59
  // Start automatic peer discovery (this thread also performs the best-effort STUN probe
47
60
  // that derives our BEP 42 node ID — see automatic_discovery_loop).
48
61
  start_automatic_peer_discovery();
@@ -321,8 +334,14 @@ void RatsClient::announce_rats_peer() {
321
334
  }
322
335
 
323
336
  std::string discovery_hash = get_discovery_hash();
324
- LOG_CLIENT_INFO("Announcing peer for discovery hash: " << discovery_hash << " on port " << listen_port_);
325
-
337
+
338
+ // Advertise the mapped public TCP port when UPnP/NAT-PMP has established one,
339
+ // so WAN peers connect to (external_ip, external_port) rather than our NATed
340
+ // local listen port. Falls back to listen_port_ when no mapping is active.
341
+ // (Mirrors libtorrent feeding the port-mapping result into what it announces.)
342
+ uint16_t announce_port = get_advertised_port();
343
+ LOG_CLIENT_INFO("Announcing peer for discovery hash: " << discovery_hash << " on port " << announce_port);
344
+
326
345
  InfoHash info_hash = hex_to_node_id(discovery_hash);
327
346
 
328
347
  // Skip only if every running DHT network already has this announce in flight.
@@ -336,7 +355,7 @@ void RatsClient::announce_rats_peer() {
336
355
 
337
356
  // Use announce with callback - combines announce and find_peers in one traversal
338
357
  // Peers discovered during traversal will be returned through the callback
339
- if (announce_for_hash(discovery_hash, listen_port_, [this, info_hash](const std::vector<std::string>& peer_addresses) {
358
+ if (announce_for_hash(discovery_hash, announce_port, [this, info_hash](const std::vector<std::string>& peer_addresses) {
340
359
  LOG_CLIENT_INFO("Announce discovered " << peer_addresses.size() << " peers during traversal");
341
360
 
342
361
  // Convert peer addresses to Peer objects for handle_dht_peer_discovery()
@@ -79,8 +79,14 @@ bool RatsClient::load_configuration() {
79
79
  if (config.contains("encryption_enabled")) {
80
80
  encryption_enabled_ = config.value("encryption_enabled", true);
81
81
  }
82
-
83
-
82
+
83
+ // Load automatic port forwarding preference
84
+ if (config.contains("port_mapping_enabled")) {
85
+ std::lock_guard<std::mutex> pm_lock(port_mapping_mutex_);
86
+ port_mapping_config_.enabled = config.value("port_mapping_enabled", true);
87
+ }
88
+
89
+
84
90
  // Update last_updated timestamp
85
91
  auto now = std::chrono::high_resolution_clock::now();
86
92
  auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
@@ -119,6 +125,10 @@ bool RatsClient::save_configuration() {
119
125
  config["listen_port"] = listen_port_;
120
126
  config["max_peers"] = max_peers_;
121
127
  config["encryption_enabled"] = encryption_enabled_;
128
+ {
129
+ std::lock_guard<std::mutex> pm_lock(port_mapping_mutex_);
130
+ config["port_mapping_enabled"] = port_mapping_config_.enabled;
131
+ }
122
132
  // TODO: Re-add when implementing new Noise protocol
123
133
  // config["encryption_key"] = get_encryption_key();
124
134