librats 0.9.0 → 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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file natpmp.cpp
|
|
3
|
+
* @brief NAT-PMP (RFC 6886) client implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#include "natpmp.h"
|
|
7
|
+
#include "network_utils.h"
|
|
8
|
+
#include "logger.h"
|
|
9
|
+
|
|
10
|
+
#include <cstring>
|
|
11
|
+
|
|
12
|
+
#define LOG_NATPMP_DEBUG(message) LOG_DEBUG("natpmp", message)
|
|
13
|
+
#define LOG_NATPMP_INFO(message) LOG_INFO("natpmp", message)
|
|
14
|
+
#define LOG_NATPMP_WARN(message) LOG_WARN("natpmp", message)
|
|
15
|
+
#define LOG_NATPMP_ERROR(message) LOG_ERROR("natpmp", message)
|
|
16
|
+
|
|
17
|
+
namespace librats {
|
|
18
|
+
|
|
19
|
+
namespace {
|
|
20
|
+
|
|
21
|
+
// NAT-PMP opcodes
|
|
22
|
+
constexpr uint8_t OP_EXTERNAL_IP = 0;
|
|
23
|
+
constexpr uint8_t OP_MAP_UDP = 1;
|
|
24
|
+
constexpr uint8_t OP_MAP_TCP = 2;
|
|
25
|
+
constexpr uint8_t OP_RESPONSE_BIT = 0x80;
|
|
26
|
+
|
|
27
|
+
// Retransmission schedule (ms). RFC 6886 doubles from 250ms; we cap retries to
|
|
28
|
+
// stay responsive while still tolerating a couple of dropped UDP packets.
|
|
29
|
+
const int kRetryTimeouts[] = { 250, 500, 1000 };
|
|
30
|
+
|
|
31
|
+
void put_u16(std::vector<uint8_t>& buf, uint16_t v) {
|
|
32
|
+
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
|
33
|
+
buf.push_back(static_cast<uint8_t>(v & 0xFF));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
void put_u32(std::vector<uint8_t>& buf, uint32_t v) {
|
|
37
|
+
buf.push_back(static_cast<uint8_t>((v >> 24) & 0xFF));
|
|
38
|
+
buf.push_back(static_cast<uint8_t>((v >> 16) & 0xFF));
|
|
39
|
+
buf.push_back(static_cast<uint8_t>((v >> 8) & 0xFF));
|
|
40
|
+
buf.push_back(static_cast<uint8_t>(v & 0xFF));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
uint16_t get_u16(const uint8_t* p) {
|
|
44
|
+
return static_cast<uint16_t>((static_cast<uint16_t>(p[0]) << 8) | p[1]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
uint32_t get_u32(const uint8_t* p) {
|
|
48
|
+
return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
|
|
49
|
+
(static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
uint8_t map_opcode(PortMapProtocol p) {
|
|
53
|
+
return p == PortMapProtocol::UDP ? OP_MAP_UDP : OP_MAP_TCP;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Translate a NAT-PMP result code into a readable message (RFC 6886 §3.5).
|
|
57
|
+
const char* result_message(uint16_t code) {
|
|
58
|
+
switch (code) {
|
|
59
|
+
case 0: return "success";
|
|
60
|
+
case 1: return "unsupported version";
|
|
61
|
+
case 2: return "not authorized / refused";
|
|
62
|
+
case 3: return "network failure";
|
|
63
|
+
case 4: return "out of resources";
|
|
64
|
+
case 5: return "unsupported opcode";
|
|
65
|
+
default: return "unknown error";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} // anonymous namespace
|
|
70
|
+
|
|
71
|
+
NatPmpClient::NatPmpClient() = default;
|
|
72
|
+
|
|
73
|
+
NatPmpClient::~NatPmpClient() {
|
|
74
|
+
stop();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void NatPmpClient::add_mapping(PortMapProtocol protocol, uint16_t internal_port, uint16_t external_port) {
|
|
78
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
79
|
+
for (const auto& m : mappings_) {
|
|
80
|
+
if (m.protocol == protocol && m.internal_port == internal_port) {
|
|
81
|
+
return; // already registered
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
Mapping m;
|
|
85
|
+
m.protocol = protocol;
|
|
86
|
+
m.internal_port = internal_port;
|
|
87
|
+
m.external_port = external_port == 0 ? internal_port : external_port;
|
|
88
|
+
mappings_.push_back(m);
|
|
89
|
+
LOG_NATPMP_DEBUG("Registered mapping " << to_string(protocol) << " internal=" << internal_port
|
|
90
|
+
<< " external=" << m.external_port);
|
|
91
|
+
// If the worker is already running, wake it up to install the new mapping.
|
|
92
|
+
// wake_worker_ must be set under cv_mutex_ (the mutex the worker waits on) so
|
|
93
|
+
// the notification can't be lost in the gap between the worker evaluating its
|
|
94
|
+
// wait predicate and actually blocking.
|
|
95
|
+
if (running_.load()) {
|
|
96
|
+
{
|
|
97
|
+
std::lock_guard<std::mutex> lk(cv_mutex_);
|
|
98
|
+
wake_worker_ = true;
|
|
99
|
+
}
|
|
100
|
+
cv_.notify_all();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::string NatPmpClient::external_ip() const {
|
|
105
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
106
|
+
return external_ip_;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bool NatPmpClient::start() {
|
|
110
|
+
if (running_.exchange(true)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
stop_requested_.store(false);
|
|
114
|
+
worker_ = std::thread(&NatPmpClient::worker_loop, this);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void NatPmpClient::stop() {
|
|
119
|
+
// Guard idempotency on stop_requested_, NOT running_: the worker clears
|
|
120
|
+
// running_ itself when gateway discovery fails (see worker_loop), so gating
|
|
121
|
+
// the join on running_ would skip it and leave the thread joinable —
|
|
122
|
+
// destroying it then calls std::terminate ("terminate called without an
|
|
123
|
+
// active exception").
|
|
124
|
+
if (stop_requested_.exchange(true)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
{
|
|
128
|
+
// Take cv_mutex_ before notifying so a worker about to sleep can't miss the
|
|
129
|
+
// stop request (same lost-wakeup hazard as add_mapping).
|
|
130
|
+
std::lock_guard<std::mutex> lk(cv_mutex_);
|
|
131
|
+
wake_worker_ = true;
|
|
132
|
+
}
|
|
133
|
+
cv_.notify_all();
|
|
134
|
+
wakeup_.signal(); // unblock an in-flight gateway receive so the join is immediate
|
|
135
|
+
if (worker_.joinable()) {
|
|
136
|
+
worker_.join();
|
|
137
|
+
}
|
|
138
|
+
running_.store(false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void NatPmpClient::notify(const Mapping& m, bool success, const std::string& error) {
|
|
142
|
+
if (!callback_) return;
|
|
143
|
+
PortMapResult r;
|
|
144
|
+
r.transport = PortMapTransport::NatPMP;
|
|
145
|
+
r.protocol = m.protocol;
|
|
146
|
+
r.success = success;
|
|
147
|
+
r.internal_port = m.internal_port;
|
|
148
|
+
r.external_port = m.external_port;
|
|
149
|
+
{
|
|
150
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
151
|
+
r.external_ip = external_ip_;
|
|
152
|
+
}
|
|
153
|
+
r.error = error;
|
|
154
|
+
callback_(r);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bool NatPmpClient::ensure_gateway() {
|
|
158
|
+
std::vector<std::string> candidates;
|
|
159
|
+
if (!forced_gateway_.empty()) {
|
|
160
|
+
candidates.push_back(forced_gateway_);
|
|
161
|
+
} else {
|
|
162
|
+
candidates = network_utils::get_default_gateways();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (candidates.empty()) {
|
|
166
|
+
LOG_NATPMP_WARN("No gateway candidates found for NAT-PMP");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Probe each candidate with an external-IP request; the first that answers
|
|
171
|
+
// becomes our gateway. If none answer, fall back to the first candidate so
|
|
172
|
+
// we still attempt a mapping (some routers only reply to MAP requests).
|
|
173
|
+
for (const auto& gw : candidates) {
|
|
174
|
+
socket_t sock = create_udp_socket(0, "", AddressFamily::IPv4);
|
|
175
|
+
if (!is_valid_socket(sock)) continue;
|
|
176
|
+
|
|
177
|
+
std::vector<uint8_t> req = { NATPMP_VERSION, OP_EXTERNAL_IP };
|
|
178
|
+
bool answered = false;
|
|
179
|
+
for (int timeout : kRetryTimeouts) {
|
|
180
|
+
if (stop_requested_.load()) { close_socket(sock); return false; }
|
|
181
|
+
if (send_udp_data(sock, req, gw, NATPMP_PORT, AddressFamily::IPv4) < 0) break;
|
|
182
|
+
Peer from;
|
|
183
|
+
auto resp = receive_udp_data(sock, 64, from, timeout, wakeup_.fd());
|
|
184
|
+
if (resp.size() >= 12 && from.ip == gw && resp[0] == NATPMP_VERSION &&
|
|
185
|
+
resp[1] == (OP_EXTERNAL_IP | OP_RESPONSE_BIT)) {
|
|
186
|
+
uint16_t result = get_u16(&resp[2]);
|
|
187
|
+
if (result == 0) {
|
|
188
|
+
char ip[INET_ADDRSTRLEN];
|
|
189
|
+
struct in_addr addr;
|
|
190
|
+
std::memcpy(&addr, &resp[8], 4);
|
|
191
|
+
if (inet_ntop(AF_INET, &addr, ip, sizeof(ip))) {
|
|
192
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
193
|
+
external_ip_ = ip;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
answered = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
close_socket(sock);
|
|
201
|
+
|
|
202
|
+
if (answered) {
|
|
203
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
204
|
+
gateway_ = gw;
|
|
205
|
+
LOG_NATPMP_INFO("NAT-PMP gateway responding at " << gw
|
|
206
|
+
<< (external_ip_.empty() ? "" : " (external IP " + external_ip_ + ")"));
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Nobody answered the external-IP probe — keep the first candidate to try MAP.
|
|
212
|
+
{
|
|
213
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
214
|
+
gateway_ = candidates.front();
|
|
215
|
+
}
|
|
216
|
+
LOG_NATPMP_DEBUG("No NAT-PMP external-IP reply; will still try MAP against " << candidates.front());
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
bool NatPmpClient::request_external_ip(socket_t sock) {
|
|
221
|
+
std::string gw;
|
|
222
|
+
{
|
|
223
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
224
|
+
gw = gateway_;
|
|
225
|
+
}
|
|
226
|
+
if (gw.empty()) return false;
|
|
227
|
+
|
|
228
|
+
std::vector<uint8_t> req = { NATPMP_VERSION, OP_EXTERNAL_IP };
|
|
229
|
+
for (int timeout : kRetryTimeouts) {
|
|
230
|
+
if (stop_requested_.load()) return false;
|
|
231
|
+
if (send_udp_data(sock, req, gw, NATPMP_PORT, AddressFamily::IPv4) < 0) return false;
|
|
232
|
+
Peer from;
|
|
233
|
+
auto resp = receive_udp_data(sock, 64, from, timeout, wakeup_.fd());
|
|
234
|
+
if (resp.size() >= 12 && from.ip == gw && resp[0] == NATPMP_VERSION &&
|
|
235
|
+
resp[1] == (OP_EXTERNAL_IP | OP_RESPONSE_BIT) && get_u16(&resp[2]) == 0) {
|
|
236
|
+
char ip[INET_ADDRSTRLEN];
|
|
237
|
+
struct in_addr addr;
|
|
238
|
+
std::memcpy(&addr, &resp[8], 4);
|
|
239
|
+
if (inet_ntop(AF_INET, &addr, ip, sizeof(ip))) {
|
|
240
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
241
|
+
external_ip_ = ip;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
bool NatPmpClient::send_map_request(socket_t sock, Mapping& m, bool remove) {
|
|
250
|
+
std::string gw;
|
|
251
|
+
{
|
|
252
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
253
|
+
gw = gateway_;
|
|
254
|
+
}
|
|
255
|
+
if (gw.empty()) return false;
|
|
256
|
+
|
|
257
|
+
const uint32_t lifetime = remove ? 0 : lease_duration_;
|
|
258
|
+
const uint16_t requested_external = remove ? 0 : m.external_port;
|
|
259
|
+
|
|
260
|
+
std::vector<uint8_t> req;
|
|
261
|
+
req.push_back(NATPMP_VERSION);
|
|
262
|
+
req.push_back(map_opcode(m.protocol));
|
|
263
|
+
put_u16(req, 0); // reserved
|
|
264
|
+
put_u16(req, m.internal_port);
|
|
265
|
+
put_u16(req, requested_external);
|
|
266
|
+
put_u32(req, lifetime);
|
|
267
|
+
|
|
268
|
+
if (remove) {
|
|
269
|
+
// Best-effort teardown. A NAT-PMP delete is just a MAP request with lifetime 0;
|
|
270
|
+
// we deliberately do NOT wait for the reply. stop() has already signalled the
|
|
271
|
+
// wakeup pipe so the worker exits promptly (tests churn through open/close
|
|
272
|
+
// cycles): a blocking receive here would either return instantly (confirming
|
|
273
|
+
// nothing) or, if the pipe were drained, stall for the full retransmit timeout
|
|
274
|
+
// when the gateway is silent. So we send the datagram twice — to ride out UDP
|
|
275
|
+
// loss — and return immediately. The lease caps the mapping lifetime as a
|
|
276
|
+
// backstop if both packets are lost.
|
|
277
|
+
bool sent = false;
|
|
278
|
+
for (int i = 0; i < 2; ++i) {
|
|
279
|
+
if (send_udp_data(sock, req, gw, NATPMP_PORT, AddressFamily::IPv4) >= 0) sent = true;
|
|
280
|
+
}
|
|
281
|
+
if (sent) {
|
|
282
|
+
LOG_NATPMP_INFO("NAT-PMP delete request sent (best-effort) for "
|
|
283
|
+
<< to_string(m.protocol) << " port " << m.internal_port);
|
|
284
|
+
}
|
|
285
|
+
return sent;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const uint8_t expected_opcode = static_cast<uint8_t>(map_opcode(m.protocol) | OP_RESPONSE_BIT);
|
|
289
|
+
|
|
290
|
+
for (int timeout : kRetryTimeouts) {
|
|
291
|
+
if (stop_requested_.load()) return false;
|
|
292
|
+
if (send_udp_data(sock, req, gw, NATPMP_PORT, AddressFamily::IPv4) < 0) return false;
|
|
293
|
+
|
|
294
|
+
Peer from;
|
|
295
|
+
auto resp = receive_udp_data(sock, 64, from, timeout, wakeup_.fd());
|
|
296
|
+
if (resp.size() < 16 || from.ip != gw) continue;
|
|
297
|
+
if (resp[0] != NATPMP_VERSION || resp[1] != expected_opcode) continue;
|
|
298
|
+
|
|
299
|
+
uint16_t result = get_u16(&resp[2]);
|
|
300
|
+
uint16_t resp_internal = get_u16(&resp[8]);
|
|
301
|
+
if (resp_internal != m.internal_port) continue; // not our mapping
|
|
302
|
+
|
|
303
|
+
if (result != 0) {
|
|
304
|
+
std::string err = result_message(result);
|
|
305
|
+
LOG_NATPMP_WARN("NAT-PMP map " << to_string(m.protocol) << " port " << m.internal_port
|
|
306
|
+
<< " failed: " << err);
|
|
307
|
+
notify(m, false, err);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
uint16_t mapped_external = get_u16(&resp[10]);
|
|
312
|
+
uint32_t granted_lifetime = get_u32(&resp[12]);
|
|
313
|
+
|
|
314
|
+
m.external_port = mapped_external;
|
|
315
|
+
m.active = true;
|
|
316
|
+
// Refresh at half the granted lifetime (RFC 6886 §3.3.1 recommendation).
|
|
317
|
+
uint32_t refresh = granted_lifetime > 0 ? granted_lifetime / 2 : lease_duration_ / 2;
|
|
318
|
+
if (refresh < 30) refresh = 30;
|
|
319
|
+
m.expires = std::chrono::steady_clock::now() + std::chrono::seconds(refresh);
|
|
320
|
+
|
|
321
|
+
LOG_NATPMP_INFO("NAT-PMP mapped " << to_string(m.protocol) << " internal " << m.internal_port
|
|
322
|
+
<< " -> external " << mapped_external << " (lease " << granted_lifetime << "s)");
|
|
323
|
+
notify(m, true, "");
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
LOG_NATPMP_DEBUG("NAT-PMP map request timed out for port " << m.internal_port);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void NatPmpClient::remove_all_mappings() {
|
|
332
|
+
socket_t sock = create_udp_socket(0, "", AddressFamily::IPv4);
|
|
333
|
+
if (!is_valid_socket(sock)) return;
|
|
334
|
+
std::vector<Mapping> snapshot;
|
|
335
|
+
{
|
|
336
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
337
|
+
snapshot = mappings_;
|
|
338
|
+
}
|
|
339
|
+
for (auto& m : snapshot) {
|
|
340
|
+
if (m.active) {
|
|
341
|
+
send_map_request(sock, m, /*remove=*/true);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
close_socket(sock);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
void NatPmpClient::worker_loop() {
|
|
348
|
+
LOG_NATPMP_DEBUG("NAT-PMP worker started");
|
|
349
|
+
|
|
350
|
+
if (!ensure_gateway()) {
|
|
351
|
+
LOG_NATPMP_WARN("NAT-PMP disabled: no usable gateway");
|
|
352
|
+
running_.store(false);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
while (!stop_requested_.load()) {
|
|
357
|
+
socket_t sock = create_udp_socket(0, "", AddressFamily::IPv4);
|
|
358
|
+
if (!is_valid_socket(sock)) {
|
|
359
|
+
LOG_NATPMP_ERROR("Failed to create NAT-PMP UDP socket");
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
request_external_ip(sock);
|
|
364
|
+
|
|
365
|
+
// Install / refresh any mapping that needs it.
|
|
366
|
+
auto now = std::chrono::steady_clock::now();
|
|
367
|
+
std::vector<size_t> indices;
|
|
368
|
+
{
|
|
369
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
370
|
+
for (size_t i = 0; i < mappings_.size(); ++i) {
|
|
371
|
+
if (!mappings_[i].active || mappings_[i].expires <= now) {
|
|
372
|
+
indices.push_back(i);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (size_t idx : indices) {
|
|
377
|
+
if (stop_requested_.load()) break;
|
|
378
|
+
Mapping local;
|
|
379
|
+
{
|
|
380
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
381
|
+
if (idx >= mappings_.size()) continue;
|
|
382
|
+
local = mappings_[idx];
|
|
383
|
+
}
|
|
384
|
+
bool ok = send_map_request(sock, local, /*remove=*/false);
|
|
385
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
386
|
+
if (idx < mappings_.size() && ok) {
|
|
387
|
+
mappings_[idx].external_port = local.external_port;
|
|
388
|
+
mappings_[idx].active = local.active;
|
|
389
|
+
mappings_[idx].expires = local.expires;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
close_socket(sock);
|
|
394
|
+
|
|
395
|
+
// Sleep until the soonest refresh is due (or a default poll interval).
|
|
396
|
+
auto next_wake = std::chrono::steady_clock::now() + std::chrono::seconds(lease_duration_ / 2);
|
|
397
|
+
{
|
|
398
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
399
|
+
for (const auto& m : mappings_) {
|
|
400
|
+
if (m.active && m.expires < next_wake) {
|
|
401
|
+
next_wake = m.expires;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
std::unique_lock<std::mutex> lk(cv_mutex_);
|
|
407
|
+
cv_.wait_until(lk, next_wake, [this] { return stop_requested_.load() || wake_worker_; });
|
|
408
|
+
wake_worker_ = false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Clean up mappings on the way out.
|
|
412
|
+
remove_all_mappings();
|
|
413
|
+
LOG_NATPMP_DEBUG("NAT-PMP worker stopped");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} // namespace librats
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file natpmp.h
|
|
5
|
+
* @brief NAT-PMP (NAT Port Mapping Protocol, RFC 6886) client
|
|
6
|
+
*
|
|
7
|
+
* NAT-PMP is the simple, Apple-originated alternative to UPnP for asking a home
|
|
8
|
+
* router to forward an external port to a host on the LAN. It is a tiny binary
|
|
9
|
+
* UDP protocol spoken directly to the default gateway on port 5351.
|
|
10
|
+
*
|
|
11
|
+
* @ref NatPmpClient discovers the gateway (or uses one supplied by the caller),
|
|
12
|
+
* requests the public IPv4 address, installs the requested port mappings and
|
|
13
|
+
* keeps them alive by renewing each lease before it expires, all on a dedicated
|
|
14
|
+
* background thread. Results are delivered through a @ref PortMapCallback.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* @code
|
|
18
|
+
* NatPmpClient natpmp;
|
|
19
|
+
* natpmp.set_callback([](const PortMapResult& r) { ... });
|
|
20
|
+
* natpmp.add_mapping(PortMapProtocol::TCP, listen_port);
|
|
21
|
+
* natpmp.start(); // discovers gateway and maps in the background
|
|
22
|
+
* ...
|
|
23
|
+
* natpmp.stop(); // removes the mappings and joins the worker
|
|
24
|
+
* @endcode
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
#include "port_mapping.h"
|
|
28
|
+
#include "socket.h"
|
|
29
|
+
#include "wakeup_pipe.h"
|
|
30
|
+
|
|
31
|
+
#include <atomic>
|
|
32
|
+
#include <condition_variable>
|
|
33
|
+
#include <mutex>
|
|
34
|
+
#include <string>
|
|
35
|
+
#include <thread>
|
|
36
|
+
#include <vector>
|
|
37
|
+
#include <chrono>
|
|
38
|
+
|
|
39
|
+
namespace librats {
|
|
40
|
+
|
|
41
|
+
/// Default NAT-PMP / PCP server port (the gateway listens here).
|
|
42
|
+
constexpr uint16_t NATPMP_PORT = 5351;
|
|
43
|
+
|
|
44
|
+
/// NAT-PMP protocol version byte (RFC 6886).
|
|
45
|
+
constexpr uint8_t NATPMP_VERSION = 0;
|
|
46
|
+
|
|
47
|
+
/// Default requested lease lifetime, in seconds.
|
|
48
|
+
constexpr uint32_t NATPMP_DEFAULT_LIFETIME = 3600;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* NAT-PMP client. Thread-safe public API; all network activity happens on an
|
|
52
|
+
* internal worker thread started by @ref start().
|
|
53
|
+
*/
|
|
54
|
+
class NatPmpClient {
|
|
55
|
+
public:
|
|
56
|
+
NatPmpClient();
|
|
57
|
+
~NatPmpClient();
|
|
58
|
+
|
|
59
|
+
NatPmpClient(const NatPmpClient&) = delete;
|
|
60
|
+
NatPmpClient& operator=(const NatPmpClient&) = delete;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a mapping to install once the client starts (or immediately, if it
|
|
64
|
+
* is already running). External port defaults to the internal port.
|
|
65
|
+
* @param protocol TCP or UDP
|
|
66
|
+
* @param internal_port Local port to expose
|
|
67
|
+
* @param external_port Suggested public port (0 = same as internal)
|
|
68
|
+
*/
|
|
69
|
+
void add_mapping(PortMapProtocol protocol, uint16_t internal_port, uint16_t external_port = 0);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set the requested lease lifetime in seconds (default 3600). Must be called
|
|
73
|
+
* before start() to take effect on the initial mapping.
|
|
74
|
+
*/
|
|
75
|
+
void set_lease_duration(uint32_t seconds) { lease_duration_ = seconds == 0 ? NATPMP_DEFAULT_LIFETIME : seconds; }
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Override gateway auto-detection with an explicit gateway IPv4 address.
|
|
79
|
+
* Pass an empty string to restore auto-detection.
|
|
80
|
+
*/
|
|
81
|
+
void set_gateway(const std::string& gateway_ip) { forced_gateway_ = gateway_ip; }
|
|
82
|
+
|
|
83
|
+
/// Set the result callback (invoked from the worker thread).
|
|
84
|
+
void set_callback(PortMapCallback cb) { callback_ = std::move(cb); }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Start the background worker: discover the gateway, request the external IP
|
|
88
|
+
* and install/refresh all registered mappings.
|
|
89
|
+
* @return false if already running
|
|
90
|
+
*/
|
|
91
|
+
bool start();
|
|
92
|
+
|
|
93
|
+
/// Remove all installed mappings (best-effort) and stop the worker thread.
|
|
94
|
+
void stop();
|
|
95
|
+
|
|
96
|
+
/// Whether the worker thread is currently running.
|
|
97
|
+
bool is_running() const { return running_.load(); }
|
|
98
|
+
|
|
99
|
+
/// Discovered public IPv4 address, or empty if unknown.
|
|
100
|
+
std::string external_ip() const;
|
|
101
|
+
|
|
102
|
+
private:
|
|
103
|
+
struct Mapping {
|
|
104
|
+
PortMapProtocol protocol;
|
|
105
|
+
uint16_t internal_port;
|
|
106
|
+
uint16_t external_port; // suggested then assigned
|
|
107
|
+
bool active = false;
|
|
108
|
+
std::chrono::steady_clock::time_point expires{};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
void worker_loop();
|
|
112
|
+
bool ensure_gateway(); // pick a responding gateway
|
|
113
|
+
bool request_external_ip(socket_t sock);
|
|
114
|
+
bool send_map_request(socket_t sock, Mapping& m, bool remove);
|
|
115
|
+
void remove_all_mappings(); // sends delete requests for active mappings
|
|
116
|
+
void notify(const Mapping& m, bool success, const std::string& error);
|
|
117
|
+
|
|
118
|
+
std::string forced_gateway_;
|
|
119
|
+
std::string gateway_;
|
|
120
|
+
std::string external_ip_;
|
|
121
|
+
uint32_t lease_duration_ = NATPMP_DEFAULT_LIFETIME;
|
|
122
|
+
|
|
123
|
+
PortMapCallback callback_;
|
|
124
|
+
|
|
125
|
+
mutable std::mutex mutex_; // guards mappings_, gateway_, external_ip_
|
|
126
|
+
std::vector<Mapping> mappings_;
|
|
127
|
+
|
|
128
|
+
std::atomic<bool> running_{false};
|
|
129
|
+
std::atomic<bool> stop_requested_{false};
|
|
130
|
+
std::condition_variable cv_;
|
|
131
|
+
std::mutex cv_mutex_;
|
|
132
|
+
// Set under cv_mutex_ to break the refresh sleep early when a mapping is added
|
|
133
|
+
// or stop() is requested. Guarding it with the same mutex the worker waits on
|
|
134
|
+
// is what makes the wakeup race-free (no lost notifications).
|
|
135
|
+
bool wake_worker_ = false;
|
|
136
|
+
WakeupPipe wakeup_; // interrupts blocking gateway receives on stop()
|
|
137
|
+
std::thread worker_;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
} // namespace librats
|