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.
@@ -0,0 +1,419 @@
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 "librats_log_macros.h"
13
+ #include "network_utils.h"
14
+ #include "network_monitor.h"
15
+ #include "logger.h"
16
+
17
+ namespace librats {
18
+
19
+ // ============================================================================
20
+ // Configuration
21
+ // ============================================================================
22
+
23
+ void RatsClient::set_port_mapping_enabled(bool enabled) {
24
+ bool was_enabled;
25
+ {
26
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
27
+ was_enabled = port_mapping_config_.enabled;
28
+ port_mapping_config_.enabled = enabled;
29
+ }
30
+
31
+ if (was_enabled != enabled) {
32
+ LOG_INFO("portmap", "Automatic port forwarding " << (enabled ? "enabled" : "disabled"));
33
+ if (running_.load()) {
34
+ if (enabled) {
35
+ start_port_mapping();
36
+ } else {
37
+ stop_port_mapping();
38
+ }
39
+ }
40
+ // Persist the new preference
41
+ save_configuration();
42
+ }
43
+ }
44
+
45
+ bool RatsClient::is_port_mapping_enabled() const {
46
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
47
+ return port_mapping_config_.enabled;
48
+ }
49
+
50
+ void RatsClient::set_port_mapping_config(const PortMappingConfig& config) {
51
+ {
52
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
53
+ port_mapping_config_ = config;
54
+ }
55
+ LOG_DEBUG("portmap", "Port mapping config updated (upnp=" << config.enable_upnp
56
+ << " natpmp=" << config.enable_natpmp << " lease=" << config.lease_duration_seconds << ")");
57
+ }
58
+
59
+ PortMappingConfig RatsClient::get_port_mapping_config() const {
60
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
61
+ return port_mapping_config_;
62
+ }
63
+
64
+ void RatsClient::on_port_mapping(PortMapCallback callback) {
65
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
66
+ port_mapping_callback_ = std::move(callback);
67
+ }
68
+
69
+ std::optional<std::pair<std::string, uint16_t>> RatsClient::get_mapped_public_address() const {
70
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
71
+ // The "public address" peers should reach us on is the TCP peer-listen mapping.
72
+ if (mapped_external_tcp_port_ == 0 || mapped_external_ip_.empty()) {
73
+ return std::nullopt;
74
+ }
75
+ return std::make_pair(mapped_external_ip_, mapped_external_tcp_port_);
76
+ }
77
+
78
+ uint16_t RatsClient::get_advertised_port() const {
79
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
80
+ return mapped_external_tcp_port_ != 0
81
+ ? mapped_external_tcp_port_
82
+ : static_cast<uint16_t>(listen_port_);
83
+ }
84
+
85
+ void RatsClient::add_port_mapping(PortMapProtocol protocol, uint16_t port) {
86
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
87
+ if (upnp_client_) upnp_client_->add_mapping(protocol, port);
88
+ if (natpmp_client_) natpmp_client_->add_mapping(protocol, port);
89
+ }
90
+
91
+ // ============================================================================
92
+ // Result handling
93
+ // ============================================================================
94
+
95
+ void RatsClient::handle_port_mapping_result(const PortMapResult& result) {
96
+ // A gateway whose reported "external" IP is itself private means we're behind a
97
+ // second NAT (double-NAT): the mapping forwards a port on the inner router, but
98
+ // that doesn't make us reachable from the internet, and the inner port won't
99
+ // match whatever the outer NAT assigns. So such a mapping must NOT be treated as
100
+ // a public endpoint — we leave mapped_external_* untouched and let the advertised
101
+ // address fall back to listen_port_ / the STUN-discovered reflexive address.
102
+ // An empty external IP (gateway didn't report one) is "unknown", not private, so
103
+ // we keep the previous best-effort behavior for it.
104
+ const bool ip_is_private = !result.external_ip.empty() && !network_utils::is_public_ip(result.external_ip);
105
+
106
+ PortMapCallback user_cb;
107
+ bool tcp_port_changed = false;
108
+ bool warn_double_nat = false;
109
+ {
110
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
111
+ if (result.success && !ip_is_private) {
112
+ if (!result.external_ip.empty()) {
113
+ mapped_external_ip_ = result.external_ip;
114
+ }
115
+ // Track the external port per protocol so the TCP (peer) and UDP (DHT)
116
+ // mappings don't overwrite each other.
117
+ if (result.protocol == PortMapProtocol::TCP) {
118
+ tcp_port_changed = (mapped_external_tcp_port_ != result.external_port);
119
+ mapped_external_tcp_port_ = result.external_port;
120
+ } else {
121
+ mapped_external_udp_port_ = result.external_port;
122
+ }
123
+ } else if (result.success && ip_is_private && !double_nat_warning_logged_) {
124
+ double_nat_warning_logged_ = true;
125
+ warn_double_nat = true;
126
+ }
127
+ user_cb = port_mapping_callback_;
128
+ }
129
+
130
+ if (result.success && ip_is_private) {
131
+ LOG_INFO("portmap", to_string(result.transport) << " mapped " << to_string(result.protocol)
132
+ << " port " << result.internal_port << " -> external " << result.external_ip << ":"
133
+ << result.external_port << " (gateway external IP is private — not a usable public address)");
134
+ if (warn_double_nat) {
135
+ LOG_WARN("portmap", "Gateway reports a private external IP (" << result.external_ip
136
+ << ") — likely double-NAT. Port forwarding alone won't make this host publicly "
137
+ "reachable; relying on STUN for the public address.");
138
+ }
139
+ } else if (result.success) {
140
+ LOG_INFO("portmap", to_string(result.transport) << " mapped " << to_string(result.protocol)
141
+ << " port " << result.internal_port << " -> external "
142
+ << (result.external_ip.empty() ? "?" : result.external_ip) << ":" << result.external_port);
143
+ // Avoid trying to connect to ourselves through the public address. Only a
144
+ // genuinely public IP belongs on the ignore list — a private one could be a
145
+ // real LAN peer we still want to reach.
146
+ if (!result.external_ip.empty()) {
147
+ add_ignored_address(result.external_ip);
148
+ }
149
+ // A new/changed public TCP port means the address peers should reach us on
150
+ // changed: re-announce to the DHT so it advertises the mapped port instead
151
+ // of the (NATed) local listen port. Done outside the lock below.
152
+ } else {
153
+ LOG_DEBUG("portmap", to_string(result.transport) << " mapping failed: " << result.error);
154
+ }
155
+
156
+ // Invoke the user callback outside the lock to avoid re-entrancy deadlocks.
157
+ if (user_cb) {
158
+ user_cb(result);
159
+ }
160
+
161
+ // Re-announce with the freshly mapped public port (outside any lock).
162
+ if (tcp_port_changed && is_dht_running()) {
163
+ announce_rats_peer();
164
+ }
165
+ }
166
+
167
+ // ============================================================================
168
+ // Lifecycle
169
+ // ============================================================================
170
+
171
+ void RatsClient::start_port_mapping() {
172
+ PortMappingConfig config;
173
+ int port;
174
+ {
175
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
176
+ config = port_mapping_config_;
177
+ // Already started?
178
+ if (upnp_client_ || natpmp_client_) {
179
+ return;
180
+ }
181
+ port = listen_port_;
182
+ }
183
+
184
+ if (!config.enabled) {
185
+ return;
186
+ }
187
+ if (port <= 0) {
188
+ LOG_WARN("portmap", "Skipping port mapping: invalid listen port");
189
+ return;
190
+ }
191
+
192
+ LOG_INFO("portmap", "Starting automatic port forwarding for TCP port " << port
193
+ << " (upnp=" << config.enable_upnp << " natpmp=" << config.enable_natpmp << ")");
194
+
195
+ auto callback = [this](const PortMapResult& r) { handle_port_mapping_result(r); };
196
+
197
+ std::unique_ptr<UpnpClient> upnp;
198
+ std::unique_ptr<NatPmpClient> natpmp;
199
+
200
+ if (config.enable_upnp) {
201
+ upnp = std::make_unique<UpnpClient>();
202
+ upnp->set_lease_duration(config.lease_duration_seconds);
203
+ upnp->set_callback(callback);
204
+ upnp->add_mapping(PortMapProtocol::TCP, static_cast<uint16_t>(port), 0, "rats");
205
+ }
206
+ if (config.enable_natpmp) {
207
+ natpmp = std::make_unique<NatPmpClient>();
208
+ natpmp->set_lease_duration(config.lease_duration_seconds);
209
+ natpmp->set_callback(callback);
210
+ natpmp->add_mapping(PortMapProtocol::TCP, static_cast<uint16_t>(port));
211
+ }
212
+
213
+ // Publish the clients before starting their workers so add_port_mapping() can
214
+ // reach them, then kick off discovery.
215
+ {
216
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
217
+ upnp_client_ = std::move(upnp);
218
+ natpmp_client_ = std::move(natpmp);
219
+ }
220
+ {
221
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
222
+ if (upnp_client_) upnp_client_->start();
223
+ if (natpmp_client_) natpmp_client_->start();
224
+ }
225
+ }
226
+
227
+ void RatsClient::stop_port_mapping() {
228
+ // Move the clients out under the lock, then stop() them outside it: stop()
229
+ // joins worker threads which may call handle_port_mapping_result() (and thus
230
+ // re-acquire port_mapping_mutex_), so holding it here would deadlock.
231
+ std::unique_ptr<UpnpClient> upnp;
232
+ std::unique_ptr<NatPmpClient> natpmp;
233
+ {
234
+ std::lock_guard<std::mutex> lock(port_mapping_mutex_);
235
+ upnp = std::move(upnp_client_);
236
+ natpmp = std::move(natpmp_client_);
237
+ }
238
+
239
+ if (upnp || natpmp) {
240
+ LOG_INFO("portmap", "Removing port mappings and stopping backends");
241
+ }
242
+ if (upnp) upnp->stop();
243
+ if (natpmp) natpmp->stop();
244
+ }
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
+
419
+ } // namespace librats