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,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file upnp.cpp
|
|
3
|
+
* @brief UPnP IGD port mapping client implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#include "upnp.h"
|
|
7
|
+
#include "socket.h"
|
|
8
|
+
#include "logger.h"
|
|
9
|
+
|
|
10
|
+
#include <cstring>
|
|
11
|
+
#include <cstdio>
|
|
12
|
+
#include <cstdlib>
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <cctype>
|
|
15
|
+
#include <random>
|
|
16
|
+
#include <sstream>
|
|
17
|
+
|
|
18
|
+
#define LOG_UPNP_DEBUG(message) LOG_DEBUG("upnp", message)
|
|
19
|
+
#define LOG_UPNP_INFO(message) LOG_INFO("upnp", message)
|
|
20
|
+
#define LOG_UPNP_WARN(message) LOG_WARN("upnp", message)
|
|
21
|
+
#define LOG_UPNP_ERROR(message) LOG_ERROR("upnp", message)
|
|
22
|
+
|
|
23
|
+
namespace librats {
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
// Service types we know how to drive, in preference order.
|
|
28
|
+
const char* kWantedServices[] = {
|
|
29
|
+
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
|
30
|
+
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
31
|
+
"urn:schemas-upnp-org:service:WANPPPConnection:1",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
std::string to_lower(std::string s) {
|
|
35
|
+
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Case-insensitive search for `needle` in `haystack` starting at `from`.
|
|
40
|
+
size_t ifind(const std::string& haystack, const std::string& needle, size_t from = 0) {
|
|
41
|
+
auto it = std::search(haystack.begin() + from, haystack.end(), needle.begin(), needle.end(),
|
|
42
|
+
[](char a, char b) { return std::tolower((unsigned char)a) == std::tolower((unsigned char)b); });
|
|
43
|
+
if (it == haystack.end()) return std::string::npos;
|
|
44
|
+
return static_cast<size_t>(it - haystack.begin());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Determine which local IPv4 address the OS would use to reach `dest_ip`.
|
|
48
|
+
std::string local_ip_for_destination(const std::string& dest_ip, uint16_t dest_port) {
|
|
49
|
+
socket_t s = ::socket(AF_INET, SOCK_DGRAM, 0);
|
|
50
|
+
if (!is_valid_socket(s)) return "";
|
|
51
|
+
struct sockaddr_in dest;
|
|
52
|
+
std::memset(&dest, 0, sizeof(dest));
|
|
53
|
+
dest.sin_family = AF_INET;
|
|
54
|
+
dest.sin_port = htons(dest_port);
|
|
55
|
+
inet_pton(AF_INET, dest_ip.c_str(), &dest.sin_addr);
|
|
56
|
+
|
|
57
|
+
std::string result;
|
|
58
|
+
if (::connect(s, reinterpret_cast<struct sockaddr*>(&dest), sizeof(dest)) == 0) {
|
|
59
|
+
struct sockaddr_in local;
|
|
60
|
+
socklen_t len = sizeof(local);
|
|
61
|
+
if (::getsockname(s, reinterpret_cast<struct sockaddr*>(&local), &len) == 0) {
|
|
62
|
+
char ip[INET_ADDRSTRLEN];
|
|
63
|
+
if (inet_ntop(AF_INET, &local.sin_addr, ip, sizeof(ip))) {
|
|
64
|
+
result = ip;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
close_socket(s);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Perform a blocking HTTP/1.1 request (Connection: close) and return the body.
|
|
73
|
+
// `extra_headers` must each end with CRLF. Returns false on transport failure.
|
|
74
|
+
bool http_request(const std::string& host, uint16_t port, const std::string& method,
|
|
75
|
+
const std::string& path, const std::string& extra_headers,
|
|
76
|
+
const std::string& body, int& status_code, std::string& response_body) {
|
|
77
|
+
socket_t sock = create_tcp_client(host, port, 10000);
|
|
78
|
+
if (!is_valid_socket(sock)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
std::ostringstream req;
|
|
83
|
+
req << method << " " << path << " HTTP/1.1\r\n"
|
|
84
|
+
<< "Host: " << host << ":" << port << "\r\n"
|
|
85
|
+
<< "Connection: close\r\n"
|
|
86
|
+
<< extra_headers;
|
|
87
|
+
if (!body.empty()) {
|
|
88
|
+
req << "Content-Length: " << body.size() << "\r\n";
|
|
89
|
+
}
|
|
90
|
+
req << "\r\n" << body;
|
|
91
|
+
|
|
92
|
+
if (send_tcp_string(sock, req.str()) < 0) {
|
|
93
|
+
close_socket(sock);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
std::string raw;
|
|
98
|
+
while (true) {
|
|
99
|
+
auto chunk = receive_tcp_data(sock, 4096);
|
|
100
|
+
if (chunk.empty()) break;
|
|
101
|
+
raw.append(reinterpret_cast<const char*>(chunk.data()), chunk.size());
|
|
102
|
+
if (raw.size() > 256 * 1024) break; // sanity cap for IGD descriptions
|
|
103
|
+
}
|
|
104
|
+
close_socket(sock);
|
|
105
|
+
|
|
106
|
+
if (raw.empty()) return false;
|
|
107
|
+
|
|
108
|
+
// Parse status line
|
|
109
|
+
status_code = 0;
|
|
110
|
+
size_t sp = raw.find(' ');
|
|
111
|
+
if (sp != std::string::npos) {
|
|
112
|
+
status_code = std::atoi(raw.substr(sp + 1, 4).c_str());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
size_t header_end = raw.find("\r\n\r\n");
|
|
116
|
+
if (header_end == std::string::npos) {
|
|
117
|
+
response_body = "";
|
|
118
|
+
} else {
|
|
119
|
+
response_body = raw.substr(header_end + 4);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} // anonymous namespace
|
|
125
|
+
|
|
126
|
+
namespace upnp_detail {
|
|
127
|
+
|
|
128
|
+
std::string extract_xml_tag(const std::string& xml, const std::string& tag, size_t from) {
|
|
129
|
+
std::string open = "<" + tag;
|
|
130
|
+
size_t s = ifind(xml, open, from);
|
|
131
|
+
if (s == std::string::npos) return "";
|
|
132
|
+
size_t gt = xml.find('>', s);
|
|
133
|
+
if (gt == std::string::npos) return "";
|
|
134
|
+
std::string close = "</" + tag + ">";
|
|
135
|
+
size_t e = ifind(xml, close, gt + 1);
|
|
136
|
+
if (e == std::string::npos) return "";
|
|
137
|
+
std::string value = xml.substr(gt + 1, e - gt - 1);
|
|
138
|
+
// trim whitespace
|
|
139
|
+
size_t b = value.find_first_not_of(" \t\r\n");
|
|
140
|
+
size_t en = value.find_last_not_of(" \t\r\n");
|
|
141
|
+
if (b == std::string::npos) return "";
|
|
142
|
+
return value.substr(b, en - b + 1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bool parse_http_url(const std::string& url, std::string& host, uint16_t& port, std::string& path) {
|
|
146
|
+
std::string lower = to_lower(url);
|
|
147
|
+
const std::string prefix = "http://";
|
|
148
|
+
if (lower.compare(0, prefix.size(), prefix) != 0) return false;
|
|
149
|
+
size_t host_start = prefix.size();
|
|
150
|
+
size_t path_start = url.find('/', host_start);
|
|
151
|
+
std::string authority = (path_start == std::string::npos)
|
|
152
|
+
? url.substr(host_start)
|
|
153
|
+
: url.substr(host_start, path_start - host_start);
|
|
154
|
+
path = (path_start == std::string::npos) ? "/" : url.substr(path_start);
|
|
155
|
+
size_t colon = authority.find(':');
|
|
156
|
+
if (colon == std::string::npos) {
|
|
157
|
+
host = authority;
|
|
158
|
+
port = 80;
|
|
159
|
+
} else {
|
|
160
|
+
host = authority.substr(0, colon);
|
|
161
|
+
port = static_cast<uint16_t>(std::atoi(authority.substr(colon + 1).c_str()));
|
|
162
|
+
if (port == 0) port = 80;
|
|
163
|
+
}
|
|
164
|
+
return !host.empty();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
std::string resolve_control_url(std::string control_url, std::string url_base,
|
|
168
|
+
const std::string& desc_host, uint16_t desc_port) {
|
|
169
|
+
if (control_url.empty()) return "";
|
|
170
|
+
|
|
171
|
+
// Already absolute.
|
|
172
|
+
if (to_lower(control_url).compare(0, 7, "http://") == 0) {
|
|
173
|
+
return control_url;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Ensure a leading slash so we can append to an authority.
|
|
177
|
+
if (control_url.front() != '/') control_url = "/" + control_url;
|
|
178
|
+
|
|
179
|
+
if (!url_base.empty()) {
|
|
180
|
+
// <URLBase> is typically http://host:port[/]; drop a trailing slash so we
|
|
181
|
+
// don't produce a doubled "//" when joining with the rooted control path.
|
|
182
|
+
if (url_base.back() == '/') url_base.pop_back();
|
|
183
|
+
return url_base + control_url;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fall back to the host the description itself was fetched from.
|
|
187
|
+
return "http://" + desc_host + ":" + std::to_string(desc_port) + control_url;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
} // namespace upnp_detail
|
|
191
|
+
|
|
192
|
+
UpnpClient::UpnpClient() = default;
|
|
193
|
+
|
|
194
|
+
UpnpClient::~UpnpClient() {
|
|
195
|
+
stop();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void UpnpClient::add_mapping(PortMapProtocol protocol, uint16_t internal_port, uint16_t external_port,
|
|
199
|
+
const std::string& description) {
|
|
200
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
201
|
+
for (const auto& m : mappings_) {
|
|
202
|
+
if (m.protocol == protocol && m.internal_port == internal_port) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
Mapping m;
|
|
207
|
+
m.protocol = protocol;
|
|
208
|
+
m.internal_port = internal_port;
|
|
209
|
+
m.external_port = external_port == 0 ? internal_port : external_port;
|
|
210
|
+
m.description = description;
|
|
211
|
+
mappings_.push_back(m);
|
|
212
|
+
LOG_UPNP_DEBUG("Registered mapping " << to_string(protocol) << " internal=" << internal_port
|
|
213
|
+
<< " external=" << m.external_port);
|
|
214
|
+
// Wake the worker to install the new mapping immediately. wake_worker_ must be
|
|
215
|
+
// set under cv_mutex_ (the mutex the worker waits on) so the notification can't
|
|
216
|
+
// be lost in the gap between the worker evaluating its predicate and blocking.
|
|
217
|
+
if (running_.load()) {
|
|
218
|
+
{
|
|
219
|
+
std::lock_guard<std::mutex> lk(cv_mutex_);
|
|
220
|
+
wake_worker_ = true;
|
|
221
|
+
}
|
|
222
|
+
cv_.notify_all();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
std::string UpnpClient::external_ip() const {
|
|
227
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
228
|
+
return external_ip_;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
bool UpnpClient::start() {
|
|
232
|
+
if (running_.exchange(true)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
stop_requested_.store(false);
|
|
236
|
+
worker_ = std::thread(&UpnpClient::worker_loop, this);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
void UpnpClient::stop() {
|
|
241
|
+
// Guard idempotency on stop_requested_, NOT running_: the worker clears
|
|
242
|
+
// running_ itself when discovery fails (see worker_loop), so gating the join
|
|
243
|
+
// on running_ would skip it and leave the thread joinable — destroying it
|
|
244
|
+
// then calls std::terminate ("terminate called without an active exception").
|
|
245
|
+
if (stop_requested_.exchange(true)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
{
|
|
249
|
+
// Take cv_mutex_ before notifying so a worker about to sleep can't miss the
|
|
250
|
+
// stop request (same lost-wakeup hazard as add_mapping).
|
|
251
|
+
std::lock_guard<std::mutex> lk(cv_mutex_);
|
|
252
|
+
wake_worker_ = true;
|
|
253
|
+
}
|
|
254
|
+
cv_.notify_all();
|
|
255
|
+
wakeup_.signal(); // unblock an in-flight SSDP receive so the join is immediate
|
|
256
|
+
if (worker_.joinable()) {
|
|
257
|
+
worker_.join();
|
|
258
|
+
}
|
|
259
|
+
running_.store(false);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
void UpnpClient::notify(const Mapping& m, bool success, const std::string& error) {
|
|
263
|
+
if (!callback_) return;
|
|
264
|
+
PortMapResult r;
|
|
265
|
+
r.transport = PortMapTransport::UPnP;
|
|
266
|
+
r.protocol = m.protocol;
|
|
267
|
+
r.success = success;
|
|
268
|
+
r.internal_port = m.internal_port;
|
|
269
|
+
r.external_port = m.external_port;
|
|
270
|
+
{
|
|
271
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
272
|
+
r.external_ip = external_ip_;
|
|
273
|
+
}
|
|
274
|
+
r.error = error;
|
|
275
|
+
callback_(r);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
bool UpnpClient::discover_device(Device& out) {
|
|
279
|
+
socket_t sock = create_udp_socket(0, "", AddressFamily::IPv4);
|
|
280
|
+
if (!is_valid_socket(sock)) {
|
|
281
|
+
LOG_UPNP_ERROR("Failed to create SSDP socket");
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// SSDP M-SEARCH. MX=2 asks devices to spread responses over up to 2 seconds.
|
|
286
|
+
// Search for several targets: IGD v1/v2 directly, and the generic root-device
|
|
287
|
+
// target (some routers only answer the latter). We then validate the WAN
|
|
288
|
+
// service from each device description, so over-broad replies are harmless.
|
|
289
|
+
static const char* kSearchTargets[] = {
|
|
290
|
+
"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
291
|
+
"urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
292
|
+
"upnp:rootdevice",
|
|
293
|
+
};
|
|
294
|
+
std::vector<std::vector<uint8_t>> payloads;
|
|
295
|
+
for (const char* st : kSearchTargets) {
|
|
296
|
+
std::string msearch =
|
|
297
|
+
"M-SEARCH * HTTP/1.1\r\n"
|
|
298
|
+
"HOST: 239.255.255.250:1900\r\n"
|
|
299
|
+
"MAN: \"ssdp:discover\"\r\n"
|
|
300
|
+
"MX: 2\r\n"
|
|
301
|
+
"ST: " + std::string(st) + "\r\n"
|
|
302
|
+
"\r\n";
|
|
303
|
+
payloads.emplace_back(msearch.begin(), msearch.end());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
bool found = false;
|
|
307
|
+
std::vector<std::string> tried; // device descriptions already fetched this run
|
|
308
|
+
// Send a few bursts (UDP is lossy) and harvest responses for a few seconds.
|
|
309
|
+
for (int attempt = 0; attempt < 3 && !found && !stop_requested_.load(); ++attempt) {
|
|
310
|
+
for (const auto& payload : payloads) {
|
|
311
|
+
send_udp_data(sock, payload, SSDP_MULTICAST_ADDR, SSDP_PORT, AddressFamily::IPv4);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
315
|
+
while (std::chrono::steady_clock::now() < deadline && !stop_requested_.load()) {
|
|
316
|
+
Peer from;
|
|
317
|
+
auto resp = receive_udp_data(sock, 2048, from, 1000, wakeup_.fd());
|
|
318
|
+
if (resp.empty()) continue;
|
|
319
|
+
|
|
320
|
+
std::string text(reinterpret_cast<const char*>(resp.data()), resp.size());
|
|
321
|
+
size_t loc = ifind(text, "location:");
|
|
322
|
+
if (loc == std::string::npos) continue;
|
|
323
|
+
size_t value_start = loc + std::string("location:").size();
|
|
324
|
+
size_t line_end = text.find("\r\n", value_start);
|
|
325
|
+
std::string location = text.substr(value_start, line_end - value_start);
|
|
326
|
+
// trim
|
|
327
|
+
size_t b = location.find_first_not_of(" \t");
|
|
328
|
+
size_t e = location.find_last_not_of(" \t\r\n");
|
|
329
|
+
if (b == std::string::npos) continue;
|
|
330
|
+
location = location.substr(b, e - b + 1);
|
|
331
|
+
|
|
332
|
+
// The broadened search may surface non-IGD root devices and duplicate
|
|
333
|
+
// replies; fetch each unique description at most once.
|
|
334
|
+
if (std::find(tried.begin(), tried.end(), location) != tried.end()) continue;
|
|
335
|
+
tried.push_back(location);
|
|
336
|
+
|
|
337
|
+
std::string local_ip = local_ip_for_destination(from.ip, from.port);
|
|
338
|
+
LOG_UPNP_DEBUG("SSDP reply from " << from.ip << " location=" << location);
|
|
339
|
+
if (fetch_description(location, local_ip, out)) {
|
|
340
|
+
found = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
close_socket(sock);
|
|
347
|
+
return found;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
bool UpnpClient::fetch_description(const std::string& location, const std::string& local_ip, Device& out) {
|
|
351
|
+
std::string host, path;
|
|
352
|
+
uint16_t port = 0;
|
|
353
|
+
if (!upnp_detail::parse_http_url(location, host, port, path)) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
int status = 0;
|
|
358
|
+
std::string body;
|
|
359
|
+
if (!http_request(host, port, "GET", path, "", "", status, body) || status != 200) {
|
|
360
|
+
LOG_UPNP_DEBUG("Failed to fetch device description from " << location << " (status " << status << ")");
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Find a usable WAN connection service and its control URL.
|
|
365
|
+
std::string service_type, control_url;
|
|
366
|
+
size_t pos = 0;
|
|
367
|
+
while (true) {
|
|
368
|
+
size_t svc_start = ifind(body, "<service", pos);
|
|
369
|
+
if (svc_start == std::string::npos) break;
|
|
370
|
+
size_t svc_end = ifind(body, "</service>", svc_start);
|
|
371
|
+
if (svc_end == std::string::npos) break;
|
|
372
|
+
std::string segment = body.substr(svc_start, svc_end - svc_start);
|
|
373
|
+
std::string type = upnp_detail::extract_xml_tag(segment, "serviceType");
|
|
374
|
+
for (const char* wanted : kWantedServices) {
|
|
375
|
+
if (to_lower(type) == to_lower(wanted)) {
|
|
376
|
+
std::string ctrl = upnp_detail::extract_xml_tag(segment, "controlURL");
|
|
377
|
+
if (!ctrl.empty()) {
|
|
378
|
+
service_type = type;
|
|
379
|
+
control_url = ctrl;
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!service_type.empty()) break;
|
|
385
|
+
pos = svc_end + 1;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (service_type.empty() || control_url.empty()) {
|
|
389
|
+
LOG_UPNP_DEBUG("No WAN connection service found at " << location);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Resolve control URL (absolute, root-relative or relative to <URLBase>).
|
|
394
|
+
std::string control_absolute = upnp_detail::resolve_control_url(
|
|
395
|
+
control_url, upnp_detail::extract_xml_tag(body, "URLBase"), host, port);
|
|
396
|
+
|
|
397
|
+
std::string c_host, c_path;
|
|
398
|
+
uint16_t c_port = 0;
|
|
399
|
+
if (!upnp_detail::parse_http_url(control_absolute, c_host, c_port, c_path)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
out.control_url = control_absolute;
|
|
404
|
+
out.service_type = service_type;
|
|
405
|
+
out.control_host = c_host;
|
|
406
|
+
out.control_port = c_port;
|
|
407
|
+
out.control_path = c_path;
|
|
408
|
+
out.local_ip = !local_ip.empty() ? local_ip : local_ip_for_destination(c_host, c_port);
|
|
409
|
+
|
|
410
|
+
LOG_UPNP_INFO("Found UPnP IGD: service=" << service_type << " control=" << control_absolute
|
|
411
|
+
<< " localIP=" << out.local_ip);
|
|
412
|
+
return out.valid();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
bool UpnpClient::soap_action(const Device& dev, const std::string& action,
|
|
416
|
+
const std::string& body_args, std::string& response_body,
|
|
417
|
+
int* upnp_error) {
|
|
418
|
+
if (upnp_error) *upnp_error = 0;
|
|
419
|
+
|
|
420
|
+
std::ostringstream soap;
|
|
421
|
+
soap << "<?xml version=\"1.0\"?>\r\n"
|
|
422
|
+
<< "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
|
423
|
+
<< "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
424
|
+
<< "<s:Body><u:" << action << " xmlns:u=\"" << dev.service_type << "\">"
|
|
425
|
+
<< body_args
|
|
426
|
+
<< "</u:" << action << "></s:Body></s:Envelope>";
|
|
427
|
+
std::string body = soap.str();
|
|
428
|
+
|
|
429
|
+
std::ostringstream headers;
|
|
430
|
+
headers << "Content-Type: text/xml; charset=\"utf-8\"\r\n"
|
|
431
|
+
<< "SOAPAction: \"" << dev.service_type << "#" << action << "\"\r\n";
|
|
432
|
+
|
|
433
|
+
int status = 0;
|
|
434
|
+
if (!http_request(dev.control_host, dev.control_port, "POST", dev.control_path,
|
|
435
|
+
headers.str(), body, status, response_body)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// UPnP faults are carried in a SOAP body that typically rides on HTTP 500, but
|
|
440
|
+
// some routers answer 200 with a fault too. Parse the body regardless of status.
|
|
441
|
+
std::string err_code = upnp_detail::extract_xml_tag(response_body, "errorCode");
|
|
442
|
+
if (!err_code.empty()) {
|
|
443
|
+
int code = std::atoi(err_code.c_str());
|
|
444
|
+
if (upnp_error) *upnp_error = code;
|
|
445
|
+
LOG_UPNP_DEBUG("SOAP " << action << " returned status " << status
|
|
446
|
+
<< " upnp errorCode " << code);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (status != 200) {
|
|
451
|
+
LOG_UPNP_DEBUG("SOAP " << action << " returned status " << status);
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
bool UpnpClient::add_port_mapping(const Device& dev, Mapping& m) {
|
|
458
|
+
// A few routers reject the requested external port (718 conflict / 501 action
|
|
459
|
+
// failed) or only accept permanent leases (725). Mirror libtorrent: retry with
|
|
460
|
+
// a fresh random external port on conflict, and drop to a permanent lease on
|
|
461
|
+
// 725, instead of giving up on the first error.
|
|
462
|
+
static constexpr int kMaxAttempts = 6;
|
|
463
|
+
int last_error = 0;
|
|
464
|
+
|
|
465
|
+
for (int attempt = 0; attempt < kMaxAttempts && !stop_requested_.load(); ++attempt) {
|
|
466
|
+
const uint32_t lease = permanent_lease_only_ ? 0 : lease_duration_;
|
|
467
|
+
|
|
468
|
+
std::ostringstream args;
|
|
469
|
+
args << "<NewRemoteHost></NewRemoteHost>"
|
|
470
|
+
<< "<NewExternalPort>" << m.external_port << "</NewExternalPort>"
|
|
471
|
+
<< "<NewProtocol>" << to_string(m.protocol) << "</NewProtocol>"
|
|
472
|
+
<< "<NewInternalPort>" << m.internal_port << "</NewInternalPort>"
|
|
473
|
+
<< "<NewInternalClient>" << dev.local_ip << "</NewInternalClient>"
|
|
474
|
+
<< "<NewEnabled>1</NewEnabled>"
|
|
475
|
+
<< "<NewPortMappingDescription>" << m.description << "</NewPortMappingDescription>"
|
|
476
|
+
<< "<NewLeaseDuration>" << lease << "</NewLeaseDuration>";
|
|
477
|
+
|
|
478
|
+
std::string resp;
|
|
479
|
+
int upnp_error = 0;
|
|
480
|
+
if (soap_action(dev, "AddPortMapping", args.str(), resp, &upnp_error)) {
|
|
481
|
+
m.active = true;
|
|
482
|
+
uint32_t refresh = lease > 0 ? lease / 2 : 1800;
|
|
483
|
+
if (refresh < 60) refresh = 60;
|
|
484
|
+
m.expires = std::chrono::steady_clock::now() + std::chrono::seconds(refresh);
|
|
485
|
+
|
|
486
|
+
LOG_UPNP_INFO("UPnP mapped " << to_string(m.protocol) << " external " << m.external_port
|
|
487
|
+
<< " -> " << dev.local_ip << ":" << m.internal_port
|
|
488
|
+
<< " (lease " << lease << "s)");
|
|
489
|
+
notify(m, true, "");
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
last_error = upnp_error;
|
|
494
|
+
|
|
495
|
+
if (upnp_error == 725 && !permanent_lease_only_) {
|
|
496
|
+
// IGD only supports permanent leases: switch and retry immediately.
|
|
497
|
+
LOG_UPNP_DEBUG("IGD supports permanent leases only; retrying without a lease");
|
|
498
|
+
permanent_lease_only_ = true;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (upnp_error == 718 || upnp_error == 501) {
|
|
503
|
+
// External port conflicts with an existing mapping (some routers report
|
|
504
|
+
// 501 Action Failed instead): pick another port and retry.
|
|
505
|
+
static std::mt19937 rng(std::random_device{}());
|
|
506
|
+
std::uniform_int_distribution<int> dist(49152, 65535);
|
|
507
|
+
uint16_t new_port = static_cast<uint16_t>(dist(rng));
|
|
508
|
+
LOG_UPNP_DEBUG("External port " << m.external_port << " conflicts (error " << upnp_error
|
|
509
|
+
<< "); retrying with " << new_port);
|
|
510
|
+
m.external_port = new_port;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Any other error is not retryable.
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
std::string err = "AddPortMapping failed";
|
|
519
|
+
if (last_error) err += " (UPnP error " + std::to_string(last_error) + ")";
|
|
520
|
+
LOG_UPNP_WARN("UPnP " << err << " for " << to_string(m.protocol) << " port " << m.internal_port);
|
|
521
|
+
notify(m, false, err);
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
bool UpnpClient::delete_port_mapping(const Device& dev, const Mapping& m) {
|
|
526
|
+
std::ostringstream args;
|
|
527
|
+
args << "<NewRemoteHost></NewRemoteHost>"
|
|
528
|
+
<< "<NewExternalPort>" << m.external_port << "</NewExternalPort>"
|
|
529
|
+
<< "<NewProtocol>" << to_string(m.protocol) << "</NewProtocol>";
|
|
530
|
+
std::string resp;
|
|
531
|
+
bool ok = soap_action(dev, "DeletePortMapping", args.str(), resp);
|
|
532
|
+
if (ok) {
|
|
533
|
+
LOG_UPNP_INFO("UPnP removed mapping " << to_string(m.protocol) << " external " << m.external_port);
|
|
534
|
+
}
|
|
535
|
+
return ok;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
bool UpnpClient::query_external_ip(const Device& dev) {
|
|
539
|
+
std::string resp;
|
|
540
|
+
if (!soap_action(dev, "GetExternalIPAddress", "", resp)) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
std::string ip = upnp_detail::extract_xml_tag(resp, "NewExternalIPAddress");
|
|
544
|
+
if (!ip.empty()) {
|
|
545
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
546
|
+
external_ip_ = ip;
|
|
547
|
+
LOG_UPNP_INFO("UPnP external IP: " << ip);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
void UpnpClient::remove_all_mappings(const Device& dev) {
|
|
554
|
+
std::vector<Mapping> snapshot;
|
|
555
|
+
{
|
|
556
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
557
|
+
snapshot = mappings_;
|
|
558
|
+
}
|
|
559
|
+
for (const auto& m : snapshot) {
|
|
560
|
+
if (m.active) {
|
|
561
|
+
delete_port_mapping(dev, m);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
void UpnpClient::worker_loop() {
|
|
567
|
+
LOG_UPNP_DEBUG("UPnP worker started");
|
|
568
|
+
|
|
569
|
+
Device dev;
|
|
570
|
+
if (!discover_device(dev)) {
|
|
571
|
+
LOG_UPNP_WARN("UPnP disabled: no Internet Gateway Device found");
|
|
572
|
+
running_.store(false);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
{
|
|
577
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
578
|
+
device_ = dev;
|
|
579
|
+
device_found_ = true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
query_external_ip(dev);
|
|
583
|
+
|
|
584
|
+
while (!stop_requested_.load()) {
|
|
585
|
+
auto now = std::chrono::steady_clock::now();
|
|
586
|
+
|
|
587
|
+
// Install / refresh mappings that are due.
|
|
588
|
+
std::vector<size_t> indices;
|
|
589
|
+
{
|
|
590
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
591
|
+
for (size_t i = 0; i < mappings_.size(); ++i) {
|
|
592
|
+
if (!mappings_[i].active || mappings_[i].expires <= now) {
|
|
593
|
+
indices.push_back(i);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (size_t idx : indices) {
|
|
598
|
+
if (stop_requested_.load()) break;
|
|
599
|
+
Mapping local;
|
|
600
|
+
{
|
|
601
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
602
|
+
if (idx >= mappings_.size()) continue;
|
|
603
|
+
local = mappings_[idx];
|
|
604
|
+
}
|
|
605
|
+
bool ok = add_port_mapping(dev, local);
|
|
606
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
607
|
+
if (idx < mappings_.size() && ok) {
|
|
608
|
+
mappings_[idx].active = local.active;
|
|
609
|
+
mappings_[idx].expires = local.expires;
|
|
610
|
+
mappings_[idx].external_port = local.external_port;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Sleep until the soonest refresh (default: lease/2).
|
|
615
|
+
auto next_wake = std::chrono::steady_clock::now() +
|
|
616
|
+
std::chrono::seconds(lease_duration_ > 0 ? lease_duration_ / 2 : 1800);
|
|
617
|
+
{
|
|
618
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
619
|
+
for (const auto& m : mappings_) {
|
|
620
|
+
if (m.active && m.expires < next_wake) {
|
|
621
|
+
next_wake = m.expires;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
std::unique_lock<std::mutex> lk(cv_mutex_);
|
|
627
|
+
cv_.wait_until(lk, next_wake, [this] { return stop_requested_.load() || wake_worker_; });
|
|
628
|
+
wake_worker_ = false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
remove_all_mappings(dev);
|
|
632
|
+
LOG_UPNP_DEBUG("UPnP worker stopped");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
} // namespace librats
|