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.
@@ -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
@@ -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
@@ -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();
@@ -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
- 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
 
@@ -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