librats 0.9.1 → 0.9.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.
- package/native-src/CMakeLists.txt +10 -0
- package/native-src/src/dht.cpp +3 -32
- package/native-src/src/dht.h +9 -1
- package/native-src/src/librats.cpp +10 -2
- package/native-src/src/librats.h +73 -1
- package/native-src/src/librats_discovery.cpp +22 -3
- package/native-src/src/librats_persistence.cpp +12 -2
- package/native-src/src/librats_portmap.cpp +244 -0
- package/native-src/src/natpmp.cpp +416 -0
- package/native-src/src/natpmp.h +140 -0
- package/native-src/src/network_utils.cpp +183 -2
- package/native-src/src/network_utils.h +27 -0
- package/native-src/src/port_mapping.h +78 -0
- package/native-src/src/socket.cpp +26 -6
- package/native-src/src/socket.h +6 -2
- package/native-src/src/upnp.cpp +635 -0
- package/native-src/src/upnp.h +163 -0
- package/native-src/src/wakeup_pipe.h +60 -0
- package/package.json +1 -1
|
@@ -212,6 +212,14 @@ set(LIBRARY_SOURCES
|
|
|
212
212
|
src/ice.cpp
|
|
213
213
|
src/ice.h
|
|
214
214
|
src/librats_ice.cpp
|
|
215
|
+
|
|
216
|
+
# Automatic port forwarding (UPnP IGD / NAT-PMP)
|
|
217
|
+
src/port_mapping.h
|
|
218
|
+
src/upnp.cpp
|
|
219
|
+
src/upnp.h
|
|
220
|
+
src/natpmp.cpp
|
|
221
|
+
src/natpmp.h
|
|
222
|
+
src/librats_portmap.cpp
|
|
215
223
|
)
|
|
216
224
|
|
|
217
225
|
# Add BitTorrent sources if RATS_SEARCH_FEATURES is enabled
|
|
@@ -458,6 +466,8 @@ if(RATS_BUILD_TESTS)
|
|
|
458
466
|
tests/test_stun.cpp
|
|
459
467
|
tests/test_turn.cpp
|
|
460
468
|
tests/test_ice.cpp
|
|
469
|
+
# Automatic port forwarding tests (UPnP/NAT-PMP)
|
|
470
|
+
tests/test_portmap.cpp
|
|
461
471
|
# I/O poller abstraction (epoll/kqueue/IOCP)
|
|
462
472
|
tests/test_io_poller.cpp
|
|
463
473
|
# Buffer utilities for file transfer and bittorrent
|
package/native-src/src/dht.cpp
CHANGED
|
@@ -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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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) {
|
package/native-src/src/dht.h
CHANGED
|
@@ -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
|
|
@@ -166,7 +166,11 @@ bool RatsClient::start() {
|
|
|
166
166
|
if (gossipsub_ && !gossipsub_->start()) {
|
|
167
167
|
LOG_CLIENT_WARN("Failed to start GossipSub - continuing without it");
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
|
|
170
|
+
// Start automatic port forwarding (UPnP/NAT-PMP) for the bound listen port.
|
|
171
|
+
// No-op when disabled; runs discovery/mapping on its own background threads.
|
|
172
|
+
start_port_mapping();
|
|
173
|
+
|
|
170
174
|
LOG_CLIENT_INFO("RatsClient started successfully on port " << listen_port_);
|
|
171
175
|
|
|
172
176
|
// Attempt to reconnect to saved peers
|
|
@@ -197,7 +201,11 @@ void RatsClient::stop() {
|
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
LOG_CLIENT_INFO("Stopping RatsClient");
|
|
200
|
-
|
|
204
|
+
|
|
205
|
+
// Remove port mappings and stop UPnP/NAT-PMP backends before tearing down
|
|
206
|
+
// sockets (best-effort cleanup so we don't leave stale router mappings).
|
|
207
|
+
stop_port_mapping();
|
|
208
|
+
|
|
201
209
|
// Stop GossipSub (can broadcast stop message)
|
|
202
210
|
if (gossipsub_) {
|
|
203
211
|
gossipsub_->stop();
|
package/native-src/src/librats.h
CHANGED
|
@@ -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
|
|
@@ -1432,6 +1434,50 @@ public:
|
|
|
1432
1434
|
*/
|
|
1433
1435
|
void restart_ice();
|
|
1434
1436
|
|
|
1437
|
+
// =========================================================================
|
|
1438
|
+
// Automatic Port Forwarding API (UPnP IGD + NAT-PMP)
|
|
1439
|
+
// =========================================================================
|
|
1440
|
+
//
|
|
1441
|
+
// When enabled (the default), RatsClient asks the home router to forward the
|
|
1442
|
+
// TCP listen port on startup, using UPnP and NAT-PMP in parallel (whichever
|
|
1443
|
+
// the router supports wins). Mappings are refreshed automatically and removed
|
|
1444
|
+
// on stop(). This lets peers behind a NAT accept inbound connections without
|
|
1445
|
+
// manual router configuration.
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Enable or disable automatic port forwarding. If toggled while running, the
|
|
1449
|
+
* port mapping backends are started or stopped immediately. The setting is
|
|
1450
|
+
* persisted to config.json.
|
|
1451
|
+
*/
|
|
1452
|
+
void set_port_mapping_enabled(bool enabled);
|
|
1453
|
+
|
|
1454
|
+
/// Whether automatic port forwarding is currently enabled.
|
|
1455
|
+
bool is_port_mapping_enabled() const;
|
|
1456
|
+
|
|
1457
|
+
/// Replace the full port mapping configuration (takes effect on next start).
|
|
1458
|
+
void set_port_mapping_config(const PortMappingConfig& config);
|
|
1459
|
+
|
|
1460
|
+
/// Get the current port mapping configuration.
|
|
1461
|
+
PortMappingConfig get_port_mapping_config() const;
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Request an additional port mapping beyond the automatic listen-port mapping
|
|
1465
|
+
* (e.g. a DHT UDP port). Has effect only while port mapping is enabled.
|
|
1466
|
+
*/
|
|
1467
|
+
void add_port_mapping(PortMapProtocol protocol, uint16_t port);
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Get the public (external) address discovered by the port mapping backends.
|
|
1471
|
+
* @return {external_ip, external_port} if a mapping is active, otherwise nullopt
|
|
1472
|
+
*/
|
|
1473
|
+
std::optional<std::pair<std::string, uint16_t>> get_mapped_public_address() const;
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Register a callback fired whenever a port mapping is established, refreshed,
|
|
1477
|
+
* removed or fails (invoked from a backend worker thread).
|
|
1478
|
+
*/
|
|
1479
|
+
void on_port_mapping(PortMapCallback callback);
|
|
1480
|
+
|
|
1435
1481
|
#ifdef RATS_STORAGE
|
|
1436
1482
|
// =========================================================================
|
|
1437
1483
|
// Distributed Storage API (requires RATS_STORAGE)
|
|
@@ -1981,7 +2027,33 @@ private:
|
|
|
1981
2027
|
|
|
1982
2028
|
// ICE manager for NAT traversal
|
|
1983
2029
|
std::unique_ptr<IceManager> ice_manager_;
|
|
1984
|
-
|
|
2030
|
+
|
|
2031
|
+
// Automatic port forwarding (UPnP IGD + NAT-PMP). Implemented in librats_portmap.cpp.
|
|
2032
|
+
mutable std::mutex port_mapping_mutex_; // guards the fields below
|
|
2033
|
+
PortMappingConfig port_mapping_config_;
|
|
2034
|
+
std::unique_ptr<UpnpClient> upnp_client_;
|
|
2035
|
+
std::unique_ptr<NatPmpClient> natpmp_client_;
|
|
2036
|
+
PortMapCallback port_mapping_callback_;
|
|
2037
|
+
// Public address discovered by the backends. The external port is tracked per
|
|
2038
|
+
// protocol (like libtorrent's per-listen-socket tcp/udp port mappings): the TCP
|
|
2039
|
+
// mapping forwards the peer listen port, the UDP mapping forwards the DHT port.
|
|
2040
|
+
// A single field would let the two backend callbacks clobber each other.
|
|
2041
|
+
std::string mapped_external_ip_;
|
|
2042
|
+
uint16_t mapped_external_tcp_port_ = 0; // public port for the TCP peer-listen port
|
|
2043
|
+
uint16_t mapped_external_udp_port_ = 0; // public port for the UDP DHT port
|
|
2044
|
+
// Set once we warn that the gateway's reported external IP is itself private
|
|
2045
|
+
// (double-NAT), so the warning isn't repeated on every lease refresh.
|
|
2046
|
+
bool double_nat_warning_logged_ = false;
|
|
2047
|
+
|
|
2048
|
+
// Start/stop the port mapping backends (no-ops if disabled). Called from
|
|
2049
|
+
// start()/stop(); safe to call repeatedly.
|
|
2050
|
+
void start_port_mapping();
|
|
2051
|
+
void stop_port_mapping();
|
|
2052
|
+
void handle_port_mapping_result(const PortMapResult& result);
|
|
2053
|
+
// Public TCP port to advertise to peers/DHT: the mapped external port once a
|
|
2054
|
+
// TCP mapping is established, otherwise the local listen port.
|
|
2055
|
+
uint16_t get_advertised_port() const;
|
|
2056
|
+
|
|
1985
2057
|
#ifdef RATS_STORAGE
|
|
1986
2058
|
// Distributed storage manager (optional, requires RATS_STORAGE)
|
|
1987
2059
|
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
|
-
|
|
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,
|
|
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
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file librats_portmap.cpp
|
|
3
|
+
* @brief Automatic port forwarding (UPnP IGD + NAT-PMP) integration for RatsClient
|
|
4
|
+
*
|
|
5
|
+
* Wires the standalone @ref UpnpClient and @ref NatPmpClient backends into the
|
|
6
|
+
* RatsClient lifecycle. Both run in parallel; whichever the router supports maps
|
|
7
|
+
* the TCP listen port so peers behind NAT can accept inbound connections. The
|
|
8
|
+
* backends are started from RatsClient::start() and torn down in stop().
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include "librats.h"
|
|
12
|
+
#include "network_utils.h"
|
|
13
|
+
#include "logger.h"
|
|
14
|
+
|
|
15
|
+
namespace librats {
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Configuration
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
void RatsClient::set_port_mapping_enabled(bool enabled) {
|
|
22
|
+
bool was_enabled;
|
|
23
|
+
{
|
|
24
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
25
|
+
was_enabled = port_mapping_config_.enabled;
|
|
26
|
+
port_mapping_config_.enabled = enabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (was_enabled != enabled) {
|
|
30
|
+
LOG_INFO("portmap", "Automatic port forwarding " << (enabled ? "enabled" : "disabled"));
|
|
31
|
+
if (running_.load()) {
|
|
32
|
+
if (enabled) {
|
|
33
|
+
start_port_mapping();
|
|
34
|
+
} else {
|
|
35
|
+
stop_port_mapping();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Persist the new preference
|
|
39
|
+
save_configuration();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool RatsClient::is_port_mapping_enabled() const {
|
|
44
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
45
|
+
return port_mapping_config_.enabled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void RatsClient::set_port_mapping_config(const PortMappingConfig& config) {
|
|
49
|
+
{
|
|
50
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
51
|
+
port_mapping_config_ = config;
|
|
52
|
+
}
|
|
53
|
+
LOG_DEBUG("portmap", "Port mapping config updated (upnp=" << config.enable_upnp
|
|
54
|
+
<< " natpmp=" << config.enable_natpmp << " lease=" << config.lease_duration_seconds << ")");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
PortMappingConfig RatsClient::get_port_mapping_config() const {
|
|
58
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
59
|
+
return port_mapping_config_;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void RatsClient::on_port_mapping(PortMapCallback callback) {
|
|
63
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
64
|
+
port_mapping_callback_ = std::move(callback);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
std::optional<std::pair<std::string, uint16_t>> RatsClient::get_mapped_public_address() const {
|
|
68
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
69
|
+
// The "public address" peers should reach us on is the TCP peer-listen mapping.
|
|
70
|
+
if (mapped_external_tcp_port_ == 0 || mapped_external_ip_.empty()) {
|
|
71
|
+
return std::nullopt;
|
|
72
|
+
}
|
|
73
|
+
return std::make_pair(mapped_external_ip_, mapped_external_tcp_port_);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
uint16_t RatsClient::get_advertised_port() const {
|
|
77
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
78
|
+
return mapped_external_tcp_port_ != 0
|
|
79
|
+
? mapped_external_tcp_port_
|
|
80
|
+
: static_cast<uint16_t>(listen_port_);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void RatsClient::add_port_mapping(PortMapProtocol protocol, uint16_t port) {
|
|
84
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
85
|
+
if (upnp_client_) upnp_client_->add_mapping(protocol, port);
|
|
86
|
+
if (natpmp_client_) natpmp_client_->add_mapping(protocol, port);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Result handling
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
void RatsClient::handle_port_mapping_result(const PortMapResult& result) {
|
|
94
|
+
// A gateway whose reported "external" IP is itself private means we're behind a
|
|
95
|
+
// second NAT (double-NAT): the mapping forwards a port on the inner router, but
|
|
96
|
+
// that doesn't make us reachable from the internet, and the inner port won't
|
|
97
|
+
// match whatever the outer NAT assigns. So such a mapping must NOT be treated as
|
|
98
|
+
// a public endpoint — we leave mapped_external_* untouched and let the advertised
|
|
99
|
+
// address fall back to listen_port_ / the STUN-discovered reflexive address.
|
|
100
|
+
// An empty external IP (gateway didn't report one) is "unknown", not private, so
|
|
101
|
+
// we keep the previous best-effort behavior for it.
|
|
102
|
+
const bool ip_is_private = !result.external_ip.empty() && !network_utils::is_public_ip(result.external_ip);
|
|
103
|
+
|
|
104
|
+
PortMapCallback user_cb;
|
|
105
|
+
bool tcp_port_changed = false;
|
|
106
|
+
bool warn_double_nat = false;
|
|
107
|
+
{
|
|
108
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
109
|
+
if (result.success && !ip_is_private) {
|
|
110
|
+
if (!result.external_ip.empty()) {
|
|
111
|
+
mapped_external_ip_ = result.external_ip;
|
|
112
|
+
}
|
|
113
|
+
// Track the external port per protocol so the TCP (peer) and UDP (DHT)
|
|
114
|
+
// mappings don't overwrite each other.
|
|
115
|
+
if (result.protocol == PortMapProtocol::TCP) {
|
|
116
|
+
tcp_port_changed = (mapped_external_tcp_port_ != result.external_port);
|
|
117
|
+
mapped_external_tcp_port_ = result.external_port;
|
|
118
|
+
} else {
|
|
119
|
+
mapped_external_udp_port_ = result.external_port;
|
|
120
|
+
}
|
|
121
|
+
} else if (result.success && ip_is_private && !double_nat_warning_logged_) {
|
|
122
|
+
double_nat_warning_logged_ = true;
|
|
123
|
+
warn_double_nat = true;
|
|
124
|
+
}
|
|
125
|
+
user_cb = port_mapping_callback_;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (result.success && ip_is_private) {
|
|
129
|
+
LOG_INFO("portmap", to_string(result.transport) << " mapped " << to_string(result.protocol)
|
|
130
|
+
<< " port " << result.internal_port << " -> external " << result.external_ip << ":"
|
|
131
|
+
<< result.external_port << " (gateway external IP is private — not a usable public address)");
|
|
132
|
+
if (warn_double_nat) {
|
|
133
|
+
LOG_WARN("portmap", "Gateway reports a private external IP (" << result.external_ip
|
|
134
|
+
<< ") — likely double-NAT. Port forwarding alone won't make this host publicly "
|
|
135
|
+
"reachable; relying on STUN for the public address.");
|
|
136
|
+
}
|
|
137
|
+
} else if (result.success) {
|
|
138
|
+
LOG_INFO("portmap", to_string(result.transport) << " mapped " << to_string(result.protocol)
|
|
139
|
+
<< " port " << result.internal_port << " -> external "
|
|
140
|
+
<< (result.external_ip.empty() ? "?" : result.external_ip) << ":" << result.external_port);
|
|
141
|
+
// Avoid trying to connect to ourselves through the public address. Only a
|
|
142
|
+
// genuinely public IP belongs on the ignore list — a private one could be a
|
|
143
|
+
// real LAN peer we still want to reach.
|
|
144
|
+
if (!result.external_ip.empty()) {
|
|
145
|
+
add_ignored_address(result.external_ip);
|
|
146
|
+
}
|
|
147
|
+
// A new/changed public TCP port means the address peers should reach us on
|
|
148
|
+
// changed: re-announce to the DHT so it advertises the mapped port instead
|
|
149
|
+
// of the (NATed) local listen port. Done outside the lock below.
|
|
150
|
+
} else {
|
|
151
|
+
LOG_DEBUG("portmap", to_string(result.transport) << " mapping failed: " << result.error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Invoke the user callback outside the lock to avoid re-entrancy deadlocks.
|
|
155
|
+
if (user_cb) {
|
|
156
|
+
user_cb(result);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Re-announce with the freshly mapped public port (outside any lock).
|
|
160
|
+
if (tcp_port_changed && is_dht_running()) {
|
|
161
|
+
announce_rats_peer();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Lifecycle
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
void RatsClient::start_port_mapping() {
|
|
170
|
+
PortMappingConfig config;
|
|
171
|
+
int port;
|
|
172
|
+
{
|
|
173
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
174
|
+
config = port_mapping_config_;
|
|
175
|
+
// Already started?
|
|
176
|
+
if (upnp_client_ || natpmp_client_) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
port = listen_port_;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!config.enabled) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (port <= 0) {
|
|
186
|
+
LOG_WARN("portmap", "Skipping port mapping: invalid listen port");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
LOG_INFO("portmap", "Starting automatic port forwarding for TCP port " << port
|
|
191
|
+
<< " (upnp=" << config.enable_upnp << " natpmp=" << config.enable_natpmp << ")");
|
|
192
|
+
|
|
193
|
+
auto callback = [this](const PortMapResult& r) { handle_port_mapping_result(r); };
|
|
194
|
+
|
|
195
|
+
std::unique_ptr<UpnpClient> upnp;
|
|
196
|
+
std::unique_ptr<NatPmpClient> natpmp;
|
|
197
|
+
|
|
198
|
+
if (config.enable_upnp) {
|
|
199
|
+
upnp = std::make_unique<UpnpClient>();
|
|
200
|
+
upnp->set_lease_duration(config.lease_duration_seconds);
|
|
201
|
+
upnp->set_callback(callback);
|
|
202
|
+
upnp->add_mapping(PortMapProtocol::TCP, static_cast<uint16_t>(port), 0, "rats");
|
|
203
|
+
}
|
|
204
|
+
if (config.enable_natpmp) {
|
|
205
|
+
natpmp = std::make_unique<NatPmpClient>();
|
|
206
|
+
natpmp->set_lease_duration(config.lease_duration_seconds);
|
|
207
|
+
natpmp->set_callback(callback);
|
|
208
|
+
natpmp->add_mapping(PortMapProtocol::TCP, static_cast<uint16_t>(port));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Publish the clients before starting their workers so add_port_mapping() can
|
|
212
|
+
// reach them, then kick off discovery.
|
|
213
|
+
{
|
|
214
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
215
|
+
upnp_client_ = std::move(upnp);
|
|
216
|
+
natpmp_client_ = std::move(natpmp);
|
|
217
|
+
}
|
|
218
|
+
{
|
|
219
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
220
|
+
if (upnp_client_) upnp_client_->start();
|
|
221
|
+
if (natpmp_client_) natpmp_client_->start();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void RatsClient::stop_port_mapping() {
|
|
226
|
+
// Move the clients out under the lock, then stop() them outside it: stop()
|
|
227
|
+
// joins worker threads which may call handle_port_mapping_result() (and thus
|
|
228
|
+
// re-acquire port_mapping_mutex_), so holding it here would deadlock.
|
|
229
|
+
std::unique_ptr<UpnpClient> upnp;
|
|
230
|
+
std::unique_ptr<NatPmpClient> natpmp;
|
|
231
|
+
{
|
|
232
|
+
std::lock_guard<std::mutex> lock(port_mapping_mutex_);
|
|
233
|
+
upnp = std::move(upnp_client_);
|
|
234
|
+
natpmp = std::move(natpmp_client_);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (upnp || natpmp) {
|
|
238
|
+
LOG_INFO("portmap", "Removing port mappings and stopping backends");
|
|
239
|
+
}
|
|
240
|
+
if (upnp) upnp->stop();
|
|
241
|
+
if (natpmp) natpmp->stop();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
} // namespace librats
|