blob: c7e3d6f96c197a852a8a48fd3cbf30689c9e7de8 [file] [log] [blame]
Adrien Béraud612b55b2023-05-29 10:42:04 -04001/*
2 * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
3 *
Adrien Béraudcb753622023-07-17 22:32:49 -04004 * This program is free software: you can redistribute it and/or modify
Adrien Béraud612b55b2023-05-29 10:42:04 -04005 * it under the terms of the GNU General Public License as published by
Adrien Béraudcb753622023-07-17 22:32:49 -04006 * the Free Software Foundation, either version 3 of the License, or
Adrien Béraud612b55b2023-05-29 10:42:04 -04007 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Adrien Béraudcb753622023-07-17 22:32:49 -040011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Adrien Béraud612b55b2023-05-29 10:42:04 -040012 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
Adrien Béraudcb753622023-07-17 22:32:49 -040015 * along with this program. If not, see <https://www.gnu.org/licenses/>.
Adrien Béraud612b55b2023-05-29 10:42:04 -040016 */
Adrien Béraud612b55b2023-05-29 10:42:04 -040017#include "nat_pmp.h"
18
19#if HAVE_LIBNATPMP
Andreas Traczyk8f7e1bd2024-04-04 16:26:58 -040020#ifdef _WIN32
21// On Windows we assume WSAStartup is called during DHT initialization
22#include <winsock2.h>
23#include <ws2tcpip.h>
24#else
Sébastien Blinbaa52d92024-03-25 13:28:30 -040025#include <poll.h>
Andreas Traczyk8f7e1bd2024-04-04 16:26:58 -040026#endif
27
28#ifdef _WIN32
29#define _poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout)
30#else
31#define _poll(fds, nfds, timeout) poll(fds, nfds, timeout)
32#endif
Adrien Béraud612b55b2023-05-29 10:42:04 -040033
Adrien Béraud1ae60aa2023-07-07 09:55:09 -040034namespace dhtnet {
Adrien Béraud612b55b2023-05-29 10:42:04 -040035namespace upnp {
36
Adrien Béraud370257c2023-08-15 20:53:09 -040037NatPmp::NatPmp(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger)
38 : UPnPProtocol(logger), ioContext(ctx), searchForIgdTimer_(*ctx)
Adrien Béraud612b55b2023-05-29 10:42:04 -040039{
Morteza Namvar5f639522023-07-04 17:08:58 -040040 // JAMI_DBG("NAT-PMP: Instance [%p] created", this);
Adrien Béraud370257c2023-08-15 20:53:09 -040041 ioContext->dispatch([this] {
Adrien Béraud612b55b2023-05-29 10:42:04 -040042 igd_ = std::make_shared<PMPIGD>();
43 });
44}
45
46NatPmp::~NatPmp()
47{
Morteza Namvar5f639522023-07-04 17:08:58 -040048 // JAMI_DBG("NAT-PMP: Instance [%p] destroyed", this);
Adrien Béraud612b55b2023-05-29 10:42:04 -040049}
50
51void
52NatPmp::initNatPmp()
53{
Adrien Béraud612b55b2023-05-29 10:42:04 -040054 initialized_ = false;
55
56 {
Adrien Béraud024c46f2024-03-02 23:53:18 -050057 std::lock_guard lock(natpmpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -040058 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
59 }
60
61 // Local address must be valid.
62 if (not getHostAddress() or getHostAddress().isLoopback()) {
Adrien Béraud17765bd2023-08-22 21:02:38 -040063 if (logger_) logger_->warn("NAT-PMP: Does not have a valid local address!");
Adrien Béraud612b55b2023-05-29 10:42:04 -040064 return;
65 }
66
67 assert(igd_);
68 if (igd_->isValid()) {
69 igd_->setValid(false);
70 processIgdUpdate(UpnpIgdEvent::REMOVED);
71 }
72
73 igd_->setLocalIp(IpAddr());
74 igd_->setPublicIp(IpAddr());
75 igd_->setUID("");
76
ovari123a15c6882024-09-17 18:34:20 -040077 if (logger_) logger_->debug("NAT-PMP: Attempting to initialize IGD");
Adrien Béraud612b55b2023-05-29 10:42:04 -040078
79 int err = initnatpmp(&natpmpHdl_, 0, 0);
80
81 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -040082 if (logger_) logger_->warn("NAT-PMP: Initializing IGD using default gateway failed!");
Adrien Béraud612b55b2023-05-29 10:42:04 -040083 const auto& localGw = ip_utils::getLocalGateway();
84 if (not localGw) {
ovari123a15c6882024-09-17 18:34:20 -040085 if (logger_) logger_->warn("NAT-PMP: Unable to find valid gateway on local host");
Adrien Béraud612b55b2023-05-29 10:42:04 -040086 err = NATPMP_ERR_CANNOTGETGATEWAY;
87 } else {
ovari123a15c6882024-09-17 18:34:20 -040088 if (logger_) logger_->warn("NAT-PMP: Attempting to initialize using detected gateway {}",
Adrien Béraud17765bd2023-08-22 21:02:38 -040089 localGw.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -040090 struct in_addr inaddr;
91 inet_pton(AF_INET, localGw.toString().c_str(), &inaddr);
92 err = initnatpmp(&natpmpHdl_, 1, inaddr.s_addr);
93 }
94 }
95
96 if (err < 0) {
ovari123a15c6882024-09-17 18:34:20 -040097 if (logger_) logger_->error("NAT-PMP: Unable to initialize libnatpmp -> {}", getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -040098 return;
99 }
100
101 char addrbuf[INET_ADDRSTRLEN];
102 inet_ntop(AF_INET, &natpmpHdl_.gateway, addrbuf, sizeof(addrbuf));
103 IpAddr igdAddr(addrbuf);
Adrien Béraud17765bd2023-08-22 21:02:38 -0400104 if (logger_) logger_->debug("NAT-PMP: Initialized on gateway {}", igdAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400105
106 // Set the local (gateway) address.
107 igd_->setLocalIp(igdAddr);
108 // NAT-PMP protocol does not have UID, but we will set generic
109 // one debugging purposes.
110 igd_->setUID("NAT-PMP Gateway");
111
112 // Search and set the public address.
113 getIgdPublicAddress();
114
115 // Update and notify.
116 if (igd_->isValid()) {
117 initialized_ = true;
118 processIgdUpdate(UpnpIgdEvent::ADDED);
119 };
120}
121
122void
123NatPmp::setObserver(UpnpMappingObserver* obs)
124{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400125 observer_ = obs;
126}
127
128void
129NatPmp::terminate(std::condition_variable& cv)
130{
Adrien Béraud7a82bee2023-08-30 10:26:45 -0400131 if (logger_) logger_->debug("NAT-PMP: Terminate instance {}", fmt::ptr(this));
132
Adrien Béraud612b55b2023-05-29 10:42:04 -0400133 initialized_ = false;
134 observer_ = nullptr;
135
Adrien Béraud024c46f2024-03-02 23:53:18 -0500136 std::lock_guard lock(natpmpMutex_);
Adrien Béraud7a82bee2023-08-30 10:26:45 -0400137 shutdownComplete_ = true;
138 cv.notify_one();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400139}
140
141void
142NatPmp::terminate()
143{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400144 std::condition_variable cv {};
145
Adrien Béraud370257c2023-08-15 20:53:09 -0400146 ioContext->dispatch([&] {
147 terminate(cv);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400148 });
149
Adrien Béraud024c46f2024-03-02 23:53:18 -0500150 std::unique_lock lk(natpmpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400151 if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400152 if (logger_) logger_->debug("NAT-PMP: Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400153 } else {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400154 if (logger_) logger_->error("NAT-PMP: Shutdown timed-out");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400155 }
156}
157
158const IpAddr
159NatPmp::getHostAddress() const
160{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500161 std::lock_guard lock(natpmpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400162 return hostAddress_;
163}
164
165void
166NatPmp::clearIgds()
167{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400168 bool do_close = false;
169
170 if (igd_) {
171 if (igd_->isValid()) {
172 do_close = true;
173 }
174 igd_->setValid(false);
175 }
176
177 initialized_ = false;
Adrien Béraud370257c2023-08-15 20:53:09 -0400178 searchForIgdTimer_.cancel();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400179
180 igdSearchCounter_ = 0;
181
182 if (do_close) {
183 closenatpmp(&natpmpHdl_);
184 memset(&natpmpHdl_, 0, sizeof(natpmpHdl_));
185 }
186}
187
188void
189NatPmp::searchForIgd()
190{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400191 if (not initialized_) {
Amna0d215232024-08-27 17:57:45 -0400192 observer_->onIgdDiscoveryStarted();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400193 initNatPmp();
194 }
195
196 // Schedule a retry in case init failed.
197 if (not initialized_) {
198 if (igdSearchCounter_++ < MAX_RESTART_SEARCH_RETRIES) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400199 if (logger_) logger_->debug("NAT-PMP: Start search for IGDs. Attempt {}", igdSearchCounter_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400200 // Cancel the current timer (if any) and re-schedule.
Adrien Béraud370257c2023-08-15 20:53:09 -0400201 searchForIgdTimer_.expires_after(NATPMP_SEARCH_RETRY_UNIT * igdSearchCounter_);
Sébastien Blind1e018c2023-09-01 09:15:12 -0400202 searchForIgdTimer_.async_wait([w=weak()](const asio::error_code& ec) {
203 if (!ec) {
204 if (auto shared = w.lock())
205 shared->searchForIgd();
206 }
Adrien Béraud370257c2023-08-15 20:53:09 -0400207 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400208 } else {
ovari123a15c6882024-09-17 18:34:20 -0400209 if (logger_) logger_->warn("NAT-PMP: Setup failed after {} attempts. NAT-PMP will be disabled!",
Adrien Béraud17765bd2023-08-22 21:02:38 -0400210 MAX_RESTART_SEARCH_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400211 }
212 }
213}
214
215std::list<std::shared_ptr<IGD>>
216NatPmp::getIgdList() const
217{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500218 std::lock_guard lock(natpmpMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400219 std::list<std::shared_ptr<IGD>> igdList;
220 if (igd_->isValid())
221 igdList.emplace_back(igd_);
222 return igdList;
223}
224
225bool
226NatPmp::isReady() const
227{
228 if (observer_ == nullptr) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400229 if (logger_) logger_->error("NAT-PMP: the observer is not set!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400230 return false;
231 }
232
233 // Must at least have a valid local address.
234 if (not getHostAddress() or getHostAddress().isLoopback())
235 return false;
236
237 return igd_ and igd_->isValid();
238}
239
240void
241NatPmp::incrementErrorsCounter(const std::shared_ptr<IGD>& igdIn)
242{
243 if (not validIgdInstance(igdIn)) {
244 return;
245 }
246
247 if (not igd_->isValid()) {
248 // Already invalid. Nothing to do.
249 return;
250 }
251
252 if (not igd_->incrementErrorsCounter()) {
253 // Disable this IGD.
254 igd_->setValid(false);
255 // Notify the listener.
Adrien Béraud17765bd2023-08-22 21:02:38 -0400256 if (logger_) logger_->warn("NAT-PMP: No more valid IGD!");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400257
258 processIgdUpdate(UpnpIgdEvent::INVALID_STATE);
259 }
260}
261
262void
263NatPmp::requestMappingAdd(const Mapping& mapping)
264{
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400265 // libnatpmp isn't thread-safe, so we use Asio here to make
266 // sure that all requests are sent from the same thread.
267 ioContext->post([w = weak(), mapping] {
268 auto sthis = w.lock();
269 if (!sthis)
270 return;
271 Mapping map(mapping);
272 assert(map.getIgd());
273 auto err = sthis->addPortMapping(map);
274 if (err < 0) {
275 if (sthis->logger_)
276 sthis->logger_->warn("NAT-PMP: Request for mapping {} on {} failed with error {:d}: {}",
277 map.toString(),
278 sthis->igd_->toString(),
279 err,
280 sthis->getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400281
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400282 if (sthis->isErrorFatal(err)) {
283 // Fatal error, increment the counter.
284 sthis->incrementErrorsCounter(sthis->igd_);
285 }
286 // Notify the listener.
287 sthis->processMappingRequestFailed(std::move(map));
288 } else {
289 if (sthis->logger_)
290 sthis->logger_->debug("NAT-PMP: Request for mapping {:s} on {:s} succeeded",
291 map.toString(),
292 sthis->igd_->toString());
293 // Notify the listener.
294 sthis->processMappingAdded(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400295 }
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400296 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400297}
298
299void
300NatPmp::requestMappingRenew(const Mapping& mapping)
301{
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400302 // libnatpmp isn't thread-safe, so we use Asio here to make
303 // sure that all requests are sent from the same thread.
304 ioContext->post([w = weak(), mapping] {
305 auto sthis = w.lock();
306 if (!sthis)
307 return;
308 Mapping map(mapping);
309 auto err = sthis->addPortMapping(map);
310 if (err < 0) {
311 if (sthis->logger_)
312 sthis->logger_->warn("NAT-PMP: Renewal request for mapping {} on {} failed with error {:d}: {}",
313 map.toString(),
314 sthis->igd_->toString(),
315 err,
316 sthis->getNatPmpErrorStr(err));
317 // Notify the listener.
318 sthis->processMappingRequestFailed(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400319
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400320 if (sthis->isErrorFatal(err)) {
321 // Fatal error, increment the counter.
322 sthis->incrementErrorsCounter(sthis->igd_);
323 }
324 } else {
325 if (sthis->logger_)
326 sthis->logger_->debug("NAT-PMP: Renewal request for mapping {} on {} succeeded",
327 map.toString(),
328 sthis->igd_->toString());
329 // Notify the listener.
330 sthis->processMappingRenewed(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400331 }
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400332 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400333}
334
335int
336NatPmp::readResponse(natpmp_t& handle, natpmpresp_t& response)
337{
338 int err = 0;
339 unsigned readRetriesCounter = 0;
340
341 while (true) {
Sébastien Blin4cf587e2024-03-08 16:00:18 -0500342 struct pollfd fds;
343 fds.fd = handle.s;
344 fds.events = POLLIN;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400345 struct timeval timeout;
Sébastien Blin77331092024-05-17 10:58:23 -0400346 err = getnatpmprequesttimeout(&handle, &timeout);
347 int millis = (timeout.tv_sec * 1000) + (timeout.tv_usec / 1000);
348 // Note, getnatpmprequesttimeout can be negative if previous deadline is passed.
349 if (err != 0 || millis < 0)
350 millis = 50;
Andreas Traczyk8f7e1bd2024-04-04 16:26:58 -0400351
Adrien Béraud612b55b2023-05-29 10:42:04 -0400352 // Wait for data.
Andreas Traczyk8f7e1bd2024-04-04 16:26:58 -0400353 if (_poll(&fds, 1, millis) == -1) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400354 err = NATPMP_ERR_SOCKETERROR;
355 break;
356 }
357
358 // Read the data.
359 err = readnatpmpresponseorretry(&handle, &response);
360
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400361 if (err == NATPMP_TRYAGAIN && readRetriesCounter++ < MAX_READ_RETRIES) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400362 std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT_BEFORE_READ_RETRY));
363 } else {
364 break;
365 }
366 }
367
368 return err;
369}
370
371int
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400372NatPmp::sendMappingRequest(Mapping& mapping, uint32_t& lifetime)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400373{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400374 int err = sendnewportmappingrequest(&natpmpHdl_,
375 mapping.getType() == PortType::UDP ? NATPMP_PROTOCOL_UDP
376 : NATPMP_PROTOCOL_TCP,
377 mapping.getInternalPort(),
378 mapping.getExternalPort(),
379 lifetime);
380
381 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400382 if (logger_) logger_->error("NAT-PMP: Send mapping request failed with error {} {:d}",
383 getNatPmpErrorStr(err),
384 errno);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400385 return err;
386 }
387
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400388 // Read the response
389 natpmpresp_t response;
390 err = readResponse(natpmpHdl_, response);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400391
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400392 if (err < 0) {
393 if (logger_) logger_->warn("NAT-PMP: Read response on IGD {} failed with error {}",
394 igd_->toString(),
395 getNatPmpErrorStr(err));
396 return err;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400397 }
398
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400399 // Even if readResponse returned without error, there is no guarantee that the
400 // response we read is for the mapping we just requested. libnatpmp expects that
401 // after each call to sendnewportmappingrequest, readnatpmpresponseorretry will
402 // be called "as long as it returns NATPMP_TRYAGAIN". Failure to do so (whether
403 // it's because of a bug as in https://git.jami.net/savoirfairelinux/dhtnet/-/issues/33,
404 // or simply because readResponse gave up after MAX_READ_RETRIES attempts) can
405 // result in us reading the response to a previous request.
406 bool responseValid = true;
407
408 if (response.type == NATPMP_RESPTYPE_PUBLICADDRESS) {
409 responseValid = false;
410 if (logger_)
411 logger_->error("NAT-PMP: unexpected response to request for mapping {} from IGD {} [type: PUBLICADDRESS]",
412 mapping.toString(),
413 igd_->toString());
414 } else {
415 // There are only three possible response types in libnatpmp. If it's not
416 // PUBLICADDRESS, then it's either UDPPORTMAPPING or TCPPORTMAPPING.
417 uint16_t expectedType = mapping.getType() == PortType::UDP ? NATPMP_RESPTYPE_UDPPORTMAPPING
418 : NATPMP_RESPTYPE_TCPPORTMAPPING;
419 uint16_t expectedPrivatePort = mapping.getInternalPort();
420 // If the response we got was actually for the mapping we requested, then both the
421 // type and the internal port (called "private port" by libnatpmp) should match.
422 // The other parameters, including the external port, are allowed to differ (see
423 // section 3.3 of the NAT-PMP RFC: https://datatracker.ietf.org/doc/html/rfc6886).
424 if (response.type != expectedType ||
425 response.pnu.newportmapping.privateport != expectedPrivatePort) {
426 responseValid = false;
427 if (logger_)
428 logger_->error("NAT-PMP: unexpected response to request for mapping {} from IGD {}"
429 " [type={}, resultcode={}, privateport={}, mappedpublicport={}, lifetime={}]",
430 mapping.toString(),
431 igd_->toString(),
432 response.type == NATPMP_RESPTYPE_UDPPORTMAPPING ? "UDP" : "TCP",
433 response.resultcode,
434 response.pnu.newportmapping.privateport,
435 response.pnu.newportmapping.mappedpublicport,
436 response.pnu.newportmapping.lifetime);
437 }
438 }
439
440 if (!responseValid) {
441 // Unfortunately, libnatpmp only allows reading one response per request sent; calling
442 // readResponse again at this point would result in a NATPMP_ERR_NOPENDINGREQ error.
ovari123a15c6882024-09-17 18:34:20 -0400443 // Since it is unable to known whether the mapping was actually created or not, we return an
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400444 // error to ensure the caller won't attempt to use a port mapping that doesn't exist.
445 return NATPMP_ERR_INVALIDARGS;
446 }
447
448 uint16_t newExternalPort = response.pnu.newportmapping.mappedpublicport;
449 uint32_t newLifetime = response.pnu.newportmapping.lifetime;
450 if (lifetime > 0) {
451 // We requested the creation/renewal of a mapping and didn't get an error, so at this point
452 // newExternalPort and newLifetime should both be nonzero.
453 if (newExternalPort == 0 || newLifetime == 0) {
454 if (logger_) logger_->error("NAT-PMP: response from IGD {} to request for mapping {}"
455 " indicates that the mapping was deleted [external port: {}, lifetime: {}]",
456 igd_->toString(),
457 mapping.toString(),
458 newExternalPort,
459 newLifetime);
460 return NATPMP_ERR_INVALIDARGS;
461 }
462 }
463
464 // We need to set the mapping's lifetime and external port here because NAT-PMP
465 // doesn't guarantee that the values returned by the IGD are those we requested.
466 lifetime = newLifetime;
467 mapping.setExternalPort(newExternalPort);
468 return 0;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400469}
470
471int
472NatPmp::addPortMapping(Mapping& mapping)
473{
474 auto const& igdIn = mapping.getIgd();
475 assert(igdIn);
476 assert(igdIn->getProtocol() == NatProtocolType::NAT_PMP);
477
478 if (not igdIn->isValid() or not validIgdInstance(igdIn)) {
479 mapping.setState(MappingState::FAILED);
480 return NATPMP_ERR_INVALIDARGS;
481 }
482
483 mapping.setInternalAddress(getHostAddress().toString());
484
485 uint32_t lifetime = MAPPING_ALLOCATION_LIFETIME;
486 int err = sendMappingRequest(mapping, lifetime);
487
488 if (err < 0) {
489 mapping.setState(MappingState::FAILED);
490 return err;
491 }
492
493 // Set the renewal time and update.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400494 mapping.setRenewalTime(sys_clock::now() + std::chrono::seconds(lifetime / 2));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400495 mapping.setState(MappingState::OPEN);
496
497 return 0;
498}
499
500void
501NatPmp::requestMappingRemove(const Mapping& mapping)
502{
Adrien Béraud370257c2023-08-15 20:53:09 -0400503 ioContext->dispatch([w = weak(), mapping] {
504 if (auto pmpThis = w.lock()) {
505 Mapping map {mapping};
506 pmpThis->removePortMapping(map);
507 }
508 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400509}
510
511void
512NatPmp::removePortMapping(Mapping& mapping)
513{
514 auto igdIn = mapping.getIgd();
515 assert(igdIn);
516 if (not igdIn->isValid()) {
517 return;
518 }
519
520 if (not validIgdInstance(igdIn)) {
521 return;
522 }
523
524 Mapping mapToRemove(mapping);
525
526 uint32_t lifetime = 0;
527 int err = sendMappingRequest(mapping, lifetime);
528
529 if (err < 0) {
530 // Nothing to do if the request fails, just log the error.
Adrien Béraud17765bd2023-08-22 21:02:38 -0400531 if (logger_) logger_->warn("NAT-PMP: Send remove request failed with error {}. Ignoring",
532 getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400533 }
534
535 // Update and notify the listener.
536 mapToRemove.setState(MappingState::FAILED);
537 processMappingRemoved(std::move(mapToRemove));
538}
539
540void
541NatPmp::getIgdPublicAddress()
542{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400543 // Set the public address for this IGD if it does not
544 // have one already.
545 if (igd_->getPublicIp()) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400546 if (logger_) logger_->warn("NAT-PMP: IGD {} already have a public address ({})",
547 igd_->toString(),
548 igd_->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400549 return;
550 }
551 assert(igd_->getProtocol() == NatProtocolType::NAT_PMP);
552
553 int err = sendpublicaddressrequest(&natpmpHdl_);
554
555 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400556 if (logger_) logger_->error("NAT-PMP: send public address request on IGD {} failed with error: {}",
557 igd_->toString(),
558 getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400559
560 if (isErrorFatal(err)) {
561 // Fatal error, increment the counter.
562 incrementErrorsCounter(igd_);
563 }
564 return;
565 }
566
567 natpmpresp_t response;
568 err = readResponse(natpmpHdl_, response);
569
570 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400571 if (logger_) logger_->warn("NAT-PMP: Read response on IGD {} failed - {}",
572 igd_->toString(),
573 getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400574 return;
575 }
576
577 if (response.type != NATPMP_RESPTYPE_PUBLICADDRESS) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400578 if (logger_) logger_->error("NAT-PMP: Unexpected response type ({:d}) for public address request from IGD {}.",
579 response.type,
580 igd_->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400581 return;
582 }
583
584 IpAddr publicAddr(response.pnu.publicaddress.addr);
585
586 if (not publicAddr) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400587 if (logger_) logger_->error("NAT-PMP: IGD {} returned an invalid public address {}",
588 igd_->toString(),
589 publicAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400590 }
591
592 // Update.
593 igd_->setPublicIp(publicAddr);
594 igd_->setValid(true);
595
Adrien Béraud17765bd2023-08-22 21:02:38 -0400596 if (logger_) logger_->debug("NAT-PMP: Setting IGD {} public address to {}",
597 igd_->toString(),
598 igd_->getPublicIp().toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400599}
600
601void
602NatPmp::removeAllMappings()
603{
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400604 if (logger_) logger_->debug("NAT-PMP: Send request to close all existing mappings to IGD {}",
Adrien Béraud17765bd2023-08-22 21:02:38 -0400605 igd_->toString().c_str());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400606
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400607 // NOTE: libnatpmp assumes that the response to each request will be read (see
608 // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/33 for more details), so
609 // it's important that we call readResponse after each call to sendnewportmappingrequest
610 // below, even if we don't actually look at the content of the responses.
611 natpmpresp_t response;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400612 int err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_TCP, 0, 0, 0);
613 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400614 if (logger_) logger_->warn("NAT-PMP: Send close all TCP mappings request failed with error {}",
615 getNatPmpErrorStr(err));
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400616 } else {
617 err = readResponse(natpmpHdl_, response);
618 if (err < 0 && logger_)
619 logger_->warn("NAT-PMP: Failed to read response to TCP mappings deletion request: {}",
620 getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400621 }
622 err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_UDP, 0, 0, 0);
623 if (err < 0) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400624 if (logger_) logger_->warn("NAT-PMP: Send close all UDP mappings request failed with error {}",
625 getNatPmpErrorStr(err));
François-Simon Fauteux-Chapleaubd9a2882024-08-30 12:44:32 -0400626 } else {
627 err = readResponse(natpmpHdl_, response);
628 if (err < 0 && logger_)
629 logger_->warn("NAT-PMP: Failed to read response to UDP mappings deletion request: {}",
630 getNatPmpErrorStr(err));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400631 }
632}
633
634const char*
635NatPmp::getNatPmpErrorStr(int errorCode) const
636{
637#ifdef ENABLE_STRNATPMPERR
638 return strnatpmperr(errorCode);
639#else
640 switch (errorCode) {
641 case NATPMP_ERR_INVALIDARGS:
642 return "INVALIDARGS";
643 break;
644 case NATPMP_ERR_SOCKETERROR:
645 return "SOCKETERROR";
646 break;
647 case NATPMP_ERR_CANNOTGETGATEWAY:
648 return "CANNOTGETGATEWAY";
649 break;
650 case NATPMP_ERR_CLOSEERR:
651 return "CLOSEERR";
652 break;
653 case NATPMP_ERR_RECVFROM:
654 return "RECVFROM";
655 break;
656 case NATPMP_ERR_NOPENDINGREQ:
657 return "NOPENDINGREQ";
658 break;
659 case NATPMP_ERR_NOGATEWAYSUPPORT:
660 return "NOGATEWAYSUPPORT";
661 break;
662 case NATPMP_ERR_CONNECTERR:
663 return "CONNECTERR";
664 break;
665 case NATPMP_ERR_WRONGPACKETSOURCE:
666 return "WRONGPACKETSOURCE";
667 break;
668 case NATPMP_ERR_SENDERR:
669 return "SENDERR";
670 break;
671 case NATPMP_ERR_FCNTLERROR:
672 return "FCNTLERROR";
673 break;
674 case NATPMP_ERR_GETTIMEOFDAYERR:
675 return "GETTIMEOFDAYERR";
676 break;
677 case NATPMP_ERR_UNSUPPORTEDVERSION:
678 return "UNSUPPORTEDVERSION";
679 break;
680 case NATPMP_ERR_UNSUPPORTEDOPCODE:
681 return "UNSUPPORTEDOPCODE";
682 break;
683 case NATPMP_ERR_UNDEFINEDERROR:
684 return "UNDEFINEDERROR";
685 break;
686 case NATPMP_ERR_NOTAUTHORIZED:
687 return "NOTAUTHORIZED";
688 break;
689 case NATPMP_ERR_NETWORKFAILURE:
690 return "NETWORKFAILURE";
691 break;
692 case NATPMP_ERR_OUTOFRESOURCES:
693 return "OUTOFRESOURCES";
694 break;
695 case NATPMP_TRYAGAIN:
696 return "TRYAGAIN";
697 break;
698 default:
699 return "UNKNOWNERR";
700 break;
701 }
702#endif
703}
704
705bool
706NatPmp::isErrorFatal(int error)
707{
708 switch (error) {
709 case NATPMP_ERR_INVALIDARGS:
710 case NATPMP_ERR_SOCKETERROR:
711 case NATPMP_ERR_CANNOTGETGATEWAY:
712 case NATPMP_ERR_CLOSEERR:
713 case NATPMP_ERR_RECVFROM:
714 case NATPMP_ERR_NOGATEWAYSUPPORT:
715 case NATPMP_ERR_CONNECTERR:
716 case NATPMP_ERR_SENDERR:
717 case NATPMP_ERR_UNDEFINEDERROR:
718 case NATPMP_ERR_UNSUPPORTEDVERSION:
719 case NATPMP_ERR_UNSUPPORTEDOPCODE:
720 case NATPMP_ERR_NOTAUTHORIZED:
721 case NATPMP_ERR_NETWORKFAILURE:
722 case NATPMP_ERR_OUTOFRESOURCES:
723 return true;
724 default:
725 return false;
726 }
727}
728
729bool
730NatPmp::validIgdInstance(const std::shared_ptr<IGD>& igdIn)
731{
732 if (igd_.get() != igdIn.get()) {
Adrien Béraud17765bd2023-08-22 21:02:38 -0400733 if (logger_) logger_->error("NAT-PMP: IGD ({}) does not match local instance ({})",
734 igdIn->toString(),
735 igd_->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400736 return false;
737 }
738
739 return true;
740}
741
742void
743NatPmp::processIgdUpdate(UpnpIgdEvent event)
744{
745 if (igd_->isValid()) {
746 // Remove all current mappings if any.
747 removeAllMappings();
748 }
749
750 if (observer_ == nullptr)
751 return;
752 // Process the response on the context thread.
Sébastien Blinf6baf4b2024-01-03 15:51:36 -0500753 ioContext->post([w = weak(), event] {
754 if (auto shared = w.lock()) {
755 if (!shared->shutdownComplete_) {
756 shared->observer_->onIgdUpdated(shared->igd_, event);
757 }
758 }
759 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400760}
761
762void
763NatPmp::processMappingAdded(const Mapping& map)
764{
765 if (observer_ == nullptr)
766 return;
767
768 // Process the response on the context thread.
Sébastien Blinf6baf4b2024-01-03 15:51:36 -0500769 ioContext->post([w=weak(), map] {
770 if (auto shared = w.lock()) {
771 if (!shared->shutdownComplete_) {
772 shared->observer_->onMappingAdded(shared->igd_, map);
773 }
774 }
775 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400776}
777
778void
779NatPmp::processMappingRequestFailed(const Mapping& map)
780{
781 if (observer_ == nullptr)
782 return;
783
784 // Process the response on the context thread.
Sébastien Blinf6baf4b2024-01-03 15:51:36 -0500785 ioContext->post([w=weak(), map] {
786 if (auto shared = w.lock()) {
787 if (!shared->shutdownComplete_) {
788 shared->observer_->onMappingRequestFailed(map);
789 }
790 }
791 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400792}
793
794void
795NatPmp::processMappingRenewed(const Mapping& map)
796{
797 if (observer_ == nullptr)
798 return;
799
800 // Process the response on the context thread.
Sébastien Blinf6baf4b2024-01-03 15:51:36 -0500801 ioContext->post([w=weak(), map] {
802 if (auto shared = w.lock()) {
803 if (!shared->shutdownComplete_) {
804 shared->observer_->onMappingRenewed(shared->igd_, map);
805 }
806 }
807 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400808}
809
810void
811NatPmp::processMappingRemoved(const Mapping& map)
812{
813 if (observer_ == nullptr)
814 return;
815
816 // Process the response on the context thread.
Sébastien Blinf6baf4b2024-01-03 15:51:36 -0500817 ioContext->post([w=weak(), map] {
818 if (auto shared = w.lock()) {
819 if (!shared->shutdownComplete_) {
820 shared->observer_->onMappingRemoved(shared->igd_, map);
821 }
822 }
823 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400824}
825
826} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -0400827} // namespace dhtnet
Adrien Béraud612b55b2023-05-29 10:42:04 -0400828
829#endif //-- #if HAVE_LIBNATPMP