blob: cffab9987df22b665fa806369adcf2f45630a913 [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 */
Morteza Namvar5f639522023-07-04 17:08:58 -040017#include "upnp/upnp_context.h"
Adrien Béraud25c30c42023-07-05 13:46:54 -040018#include "protocol/upnp_protocol.h"
19
Adrien Béraud370257c2023-08-15 20:53:09 -040020#if HAVE_LIBNATPMP
21#include "protocol/natpmp/nat_pmp.h"
22#endif
23#if HAVE_LIBUPNP
24#include "protocol/pupnp/pupnp.h"
25#endif
Amna7cd813c2023-10-02 18:15:47 -040026#include <asio.hpp>
Morteza Namvar5f639522023-07-04 17:08:58 -040027#include <asio/steady_timer.hpp>
Adrien Béraud9d350962023-07-13 15:36:32 -040028#if __has_include(<fmt/std.h>)
Adrien Béraud25c30c42023-07-05 13:46:54 -040029#include <fmt/std.h>
Adrien Béraud9d350962023-07-13 15:36:32 -040030#else
31#include <fmt/ostream.h>
32#endif
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -040033#include <fmt/chrono.h>
Adrien Béraud612b55b2023-05-29 10:42:04 -040034
Adrien Béraud1ae60aa2023-07-07 09:55:09 -040035namespace dhtnet {
Adrien Béraud612b55b2023-05-29 10:42:04 -040036namespace upnp {
37
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -040038constexpr static auto MAPPING_RENEWAL_THROTTLING_DELAY = std::chrono::seconds(10);
Adrien Béraud612b55b2023-05-29 10:42:04 -040039constexpr static int MAX_REQUEST_RETRIES = 20;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -040040constexpr static int MAX_REQUEST_REMOVE_COUNT = 10; // TODO: increase?
Adrien Béraud612b55b2023-05-29 10:42:04 -040041
42constexpr static uint16_t UPNP_TCP_PORT_MIN {10000};
43constexpr static uint16_t UPNP_TCP_PORT_MAX {UPNP_TCP_PORT_MIN + 5000};
44constexpr static uint16_t UPNP_UDP_PORT_MIN {20000};
45constexpr static uint16_t UPNP_UDP_PORT_MAX {UPNP_UDP_PORT_MIN + 5000};
46
Sébastien Blin55abf072023-07-19 10:21:21 -040047UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, const std::shared_ptr<dht::log::Logger>& logger)
Adrien Béraudc36965c2023-08-17 21:50:27 -040048 : ctx(createIoContext(ioContext, logger))
Adrien Béraud91fd4b62023-08-29 20:50:01 -040049 , logger_(logger)
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -040050 , mappingRenewalTimer_(*ctx)
51 , renewalSchedulingTimer_(*ctx)
52 , syncTimer_(*ctx)
Adrien Béraud95219ef2023-08-17 21:55:37 -040053 , connectivityChangedTimer_(*ctx)
Amna0d215232024-08-27 17:57:45 -040054 , igdDiscoveryTimer_(*ctx)
55
Adrien Béraud612b55b2023-05-29 10:42:04 -040056{
Adrien Beraud3bd61c92023-08-17 16:57:37 -040057 if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -040058
59 // Set port ranges
60 portRange_.emplace(PortType::TCP, std::make_pair(UPNP_TCP_PORT_MIN, UPNP_TCP_PORT_MAX));
61 portRange_.emplace(PortType::UDP, std::make_pair(UPNP_UDP_PORT_MIN, UPNP_UDP_PORT_MAX));
62
Adrien Béraud370257c2023-08-15 20:53:09 -040063 ctx->post([this] { init(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -040064}
65
Adrien Béraudb04fbd72023-08-17 19:56:11 -040066std::shared_ptr<asio::io_context>
67UPnPContext::createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger) {
68 if (ctx) {
69 return ctx;
70 } else {
71 if (logger) logger->debug("UPnPContext: starting dedicated io_context thread");
72 auto ioCtx = std::make_shared<asio::io_context>();
73 ioContextRunner_ = std::make_unique<std::thread>([ioCtx, l=logger]() {
74 try {
75 auto work = asio::make_work_guard(*ioCtx);
76 ioCtx->run();
77 } catch (const std::exception& ex) {
78 if (l) l->error("Unexpected io_context thread exception: {}", ex.what());
79 }
80 });
81 return ioCtx;
82 }
83}
84
Adrien Béraud612b55b2023-05-29 10:42:04 -040085void
86UPnPContext::shutdown(std::condition_variable& cv)
87{
Adrien Beraud3bd61c92023-08-17 16:57:37 -040088 if (logger_) logger_->debug("Shutdown UPnPContext instance [{}]", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -040089
90 stopUpnp(true);
91
92 for (auto const& [_, proto] : protocolList_) {
93 proto->terminate();
94 }
95
Adrien Béraud024c46f2024-03-02 23:53:18 -050096 std::lock_guard lock(mappingMutex_);
Adrien Béraud91fd4b62023-08-29 20:50:01 -040097 mappingList_->clear();
Adrien Béraud91fd4b62023-08-29 20:50:01 -040098 controllerList_.clear();
99 protocolList_.clear();
100 shutdownComplete_ = true;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400101 if (shutdownTimedOut_) {
102 // If we timed out in shutdown(), then calling notify_one is not necessary,
103 // and doing so anyway can cause bugs, see:
104 // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/28
105 return;
106 }
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400107 cv.notify_one();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400108}
109
110void
111UPnPContext::shutdown()
112{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500113 std::unique_lock lk(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400114 std::condition_variable cv;
115
Adrien Béraud370257c2023-08-15 20:53:09 -0400116 ctx->post([&, this] { shutdown(cv); });
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500117
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400118 if (logger_) logger_->debug("Waiting for shutdown ...");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400119
120 if (cv.wait_for(lk, std::chrono::seconds(30), [this] { return shutdownComplete_; })) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400121 if (logger_) logger_->debug("Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400122 } else {
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400123 if (logger_) logger_->error("Shutdown timed out");
124 shutdownTimedOut_ = true;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400125 }
François-Simon Fauteux-Chapleau648907c2024-02-06 15:16:48 -0500126 // NOTE: It's important to unlock mappingMutex_ here, otherwise we get a
127 // deadlock when the call to cv.wait_for() above times out before we return
128 // from proto->terminate() in shutdown(cv).
129 lk.unlock();
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400130
131 if (ioContextRunner_) {
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500132 if (logger_) logger_->debug("UPnPContext: stopping io_context thread {}", fmt::ptr(this));
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400133 ctx->stop();
134 ioContextRunner_->join();
135 ioContextRunner_.reset();
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500136 if (logger_) logger_->debug("UPnPContext: stopping io_context thread - finished {}", fmt::ptr(this));
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400137 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400138}
139
140UPnPContext::~UPnPContext()
141{
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400142 if (logger_) logger_->debug("UPnPContext instance [{}] destroyed", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400143}
144
145void
146UPnPContext::init()
147{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400148#if HAVE_LIBNATPMP
Adrien Béraud370257c2023-08-15 20:53:09 -0400149 auto natPmp = std::make_shared<NatPmp>(ctx, logger_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400150 natPmp->setObserver(this);
151 protocolList_.emplace(NatProtocolType::NAT_PMP, std::move(natPmp));
152#endif
153
154#if HAVE_LIBUPNP
Adrien Béraud370257c2023-08-15 20:53:09 -0400155 auto pupnp = std::make_shared<PUPnP>(ctx, logger_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400156 pupnp->setObserver(this);
157 protocolList_.emplace(NatProtocolType::PUPNP, std::move(pupnp));
158#endif
159}
160
161void
162UPnPContext::startUpnp()
163{
164 assert(not controllerList_.empty());
165
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400166 if (logger_) logger_->debug("Starting UPNP context");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400167
168 // Request a new IGD search.
169 for (auto const& [_, protocol] : protocolList_) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400170 ctx->dispatch([p=protocol] { p->searchForIgd(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400171 }
172
173 started_ = true;
174}
175
176void
177UPnPContext::stopUpnp(bool forceRelease)
178{
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400179 if (logger_) logger_->debug("Stopping UPnP context");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400180
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400181 connectivityChangedTimer_.cancel();
182 mappingRenewalTimer_.cancel();
183 renewalSchedulingTimer_.cancel();
184 syncTimer_.cancel();
185 syncRequested_ = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400186
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400187 // Clear all current mappings
188
189 // Use a temporary list to avoid processing the mappings while holding the lock.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400190 std::list<Mapping::sharedPtr_t> toRemoveList;
191 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500192 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400193
194 PortType types[2] {PortType::TCP, PortType::UDP};
195 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400196 const auto& mappingList = getMappingList(type);
197 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400198 toRemoveList.emplace_back(map);
199 }
200 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400201 // Invalidate the current IGD.
202 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400203 }
204 for (auto const& map : toRemoveList) {
205 requestRemoveMapping(map);
206
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400207 if (map->getAutoUpdate() && !forceRelease) {
208 // Set the mapping's state to PENDING so that it
209 // gets recreated if we restart UPnP later.
210 map->setState(MappingState::PENDING);
211 } else {
212 unregisterMapping(map, true);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400213 }
214 }
215
216 // Clear all current IGDs.
217 for (auto const& [_, protocol] : protocolList_) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400218 ctx->dispatch([p=protocol]{ p->clearIgds(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400219 }
220
221 started_ = false;
222}
223
224uint16_t
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400225UPnPContext::generateRandomPort(PortType type)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400226{
227 auto minPort = type == PortType::TCP ? UPNP_TCP_PORT_MIN : UPNP_UDP_PORT_MIN;
228 auto maxPort = type == PortType::TCP ? UPNP_TCP_PORT_MAX : UPNP_UDP_PORT_MAX;
229
Adrien Béraud612b55b2023-05-29 10:42:04 -0400230 // Seed the generator.
231 static std::mt19937 gen(dht::crypto::getSeededRandomEngine());
232 // Define the range.
233 std::uniform_int_distribution<uint16_t> dist(minPort, maxPort);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400234 return dist(gen);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400235}
236
237void
238UPnPContext::connectivityChanged()
239{
Adrien Béraudc36965c2023-08-17 21:50:27 -0400240 // Debounce the connectivity change notification.
241 connectivityChangedTimer_.expires_after(std::chrono::milliseconds(50));
242 connectivityChangedTimer_.async_wait(std::bind(&UPnPContext::_connectivityChanged, this, std::placeholders::_1));
243}
244
245void
246UPnPContext::_connectivityChanged(const asio::error_code& ec)
247{
248 if (ec == asio::error::operation_aborted)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400249 return;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400250
251 auto hostAddr = ip_utils::getLocalAddr(AF_INET);
252
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400253 if (logger_) logger_->debug("Connectivity change check: host address {}", hostAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400254
255 auto restartUpnp = false;
256
257 // On reception of "connectivity change" notification, the UPNP search
258 // will be restarted if either there is no valid IGD, or the IGD address
259 // changed.
260
261 if (not isReady()) {
262 restartUpnp = true;
263 } else {
264 // Check if the host address changed.
265 for (auto const& [_, protocol] : protocolList_) {
266 if (protocol->isReady() and hostAddr != protocol->getHostAddress()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400267 if (logger_) logger_->warn("Host address changed from {} to {}",
268 protocol->getHostAddress().toString(),
269 hostAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400270 protocol->clearIgds();
271 restartUpnp = true;
272 break;
273 }
274 }
275 }
276
277 // We have at least one valid IGD and the host address did
278 // not change, so no need to restart.
279 if (not restartUpnp) {
280 return;
281 }
282
283 // No registered controller. A new search will be performed when
284 // a controller is registered.
285 if (controllerList_.empty())
286 return;
287
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400288 if (logger_) logger_->debug("Connectivity changed. Clear the IGDs and restart");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400289
290 stopUpnp();
291 startUpnp();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400292}
293
294void
295UPnPContext::setPublicAddress(const IpAddr& addr)
296{
297 if (not addr)
298 return;
299
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400300 std::lock_guard lock(publicAddressMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400301 if (knownPublicAddress_ != addr) {
302 knownPublicAddress_ = std::move(addr);
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400303 if (logger_) logger_->debug("Setting the known public address to {}", addr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400304 }
305}
306
307bool
308UPnPContext::isReady() const
309{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500310 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400311 return currentIgd_ ? true : false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400312}
313
314IpAddr
315UPnPContext::getExternalIP() const
316{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500317 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400318 if (currentIgd_)
319 return currentIgd_->getPublicIp();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400320 return {};
321}
322
323Mapping::sharedPtr_t
324UPnPContext::reserveMapping(Mapping& requestedMap)
325{
326 auto desiredPort = requestedMap.getExternalPort();
327
328 if (desiredPort == 0) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400329 if (logger_) logger_->debug("Desired port is not set, will provide the first available port for [{}]",
330 requestedMap.getTypeStr());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400331 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400332 if (logger_) logger_->debug("Try to find mapping for port {:d} [{}]", desiredPort, requestedMap.getTypeStr());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400333 }
334
335 Mapping::sharedPtr_t mapRes;
336
337 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500338 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400339 const auto& mappingList = getMappingList(requestedMap.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400340
341 // We try to provide a mapping in "OPEN" state. If not found,
342 // we provide any available mapping. In this case, it's up to
343 // the caller to use it or not.
344 for (auto const& [_, map] : mappingList) {
345 // If the desired port is null, we pick the first available port.
346 if (map->isValid() and (desiredPort == 0 or map->getExternalPort() == desiredPort)
347 and map->isAvailable()) {
348 // Considere the first available mapping regardless of its
349 // state. A mapping with OPEN state will be used if found.
350 if (not mapRes)
351 mapRes = map;
352
353 if (map->getState() == MappingState::OPEN) {
354 // Found an "OPEN" mapping. We are done.
355 mapRes = map;
356 break;
357 }
358 }
359 }
360 }
361
362 // Create a mapping if none was available.
363 if (not mapRes) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400364 mapRes = registerMapping(requestedMap);
365 }
366
367 if (mapRes) {
368 // Make the mapping unavailable
369 mapRes->setAvailable(false);
370 // Copy attributes.
371 mapRes->setNotifyCallback(requestedMap.getNotifyCallback());
372 mapRes->enableAutoUpdate(requestedMap.getAutoUpdate());
373 // Notify the listener.
374 if (auto cb = mapRes->getNotifyCallback())
375 cb(mapRes);
376 }
377
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400378 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400379
380 return mapRes;
381}
382
383void
384UPnPContext::releaseMapping(const Mapping& map)
385{
Adrien Béraudc36965c2023-08-17 21:50:27 -0400386 ctx->dispatch([this, map] {
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500387 if (shutdownComplete_)
388 return;
Adrien Béraudc36965c2023-08-17 21:50:27 -0400389 auto mapPtr = getMappingWithKey(map.getMapKey());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400390
Adrien Béraudc36965c2023-08-17 21:50:27 -0400391 if (not mapPtr) {
392 // Might happen if the mapping failed or was never granted.
393 if (logger_) logger_->debug("Mapping {} does not exist or was already removed", map.toString());
394 return;
395 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400396
Adrien Béraudc36965c2023-08-17 21:50:27 -0400397 if (mapPtr->isAvailable()) {
398 if (logger_) logger_->warn("Trying to release an unused mapping {}", mapPtr->toString());
399 return;
400 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400401
Amna7333b1f2024-08-27 10:58:34 -0400402 // Reset the mapping options: disable auto-update and remove the notify callback.
403 // This is important because the mapping will be available again and can be reused
404 // by another (or the same) controller which may have different preferences.
405 // The notify callback is also removed to avoid calling it when the mapping is not used anymore.
406 mapPtr->setNotifyCallback(nullptr);
407 mapPtr->enableAutoUpdate(false);
408 mapPtr->setAvailable(true);
409 if (logger_) logger_->debug("Mapping {} released", mapPtr->toString());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400410 enforceAvailableMappingsLimits();
Adrien Béraudc36965c2023-08-17 21:50:27 -0400411 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400412}
413
414void
415UPnPContext::registerController(void* controller)
416{
417 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500418 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400419 if (shutdownComplete_) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400420 if (logger_) logger_->warn("UPnPContext already shut down");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400421 return;
422 }
Adrien Béraudc36965c2023-08-17 21:50:27 -0400423 auto ret = controllerList_.emplace(controller);
424 if (not ret.second) {
425 if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller));
426 return;
427 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400428 }
429
Adrien Berauda8731ac2023-08-17 12:19:39 -0400430 if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400431 if (not started_)
432 startUpnp();
433}
434
435void
436UPnPContext::unregisterController(void* controller)
437{
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500438 if (shutdownComplete_)
439 return;
Adrien Béraud024c46f2024-03-02 23:53:18 -0500440 std::unique_lock lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400441 if (controllerList_.erase(controller) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400442 if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400443 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400444 if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400445 }
446
447 if (controllerList_.empty()) {
Adrien Béraudc36965c2023-08-17 21:50:27 -0400448 lock.unlock();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400449 stopUpnp();
450 }
451}
452
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400453std::vector<IGDInfo>
454UPnPContext::getIgdsInfo() const
455{
456 std::vector<IGDInfo> igdInfoList;
457
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400458 for (const auto& [_, protocol] : protocolList_) {
459 for (auto& igd : protocol->getIgdList()) {
460 IGDInfo info;
461 info.uid = igd->getUID();
462 info.localIp = igd->getLocalIp();
463 info.publicIp = igd->getPublicIp();
464 info.mappingInfoList = protocol->getMappingsInfo(igd);
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400465
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400466 igdInfoList.push_back(std::move(info));
467 }
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400468 }
469
470 return igdInfoList;
471}
472
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400473// TODO: refactor this function so that it can never fail unless there are literally no ports available
Adrien Béraud612b55b2023-05-29 10:42:04 -0400474uint16_t
475UPnPContext::getAvailablePortNumber(PortType type)
476{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400477 // Only return an available random port. No actual
Adrien Béraud612b55b2023-05-29 10:42:04 -0400478 // reservation is made here.
479
Adrien Béraud024c46f2024-03-02 23:53:18 -0500480 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400481 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400482 int tryCount = 0;
483 while (tryCount++ < MAX_REQUEST_RETRIES) {
484 uint16_t port = generateRandomPort(type);
485 Mapping map(type, port, port);
486 if (mappingList.find(map.getMapKey()) == mappingList.end())
487 return port;
488 }
489
490 // Very unlikely to get here.
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -0400491 if (logger_) logger_->error("Could not find an available port after {} trials", MAX_REQUEST_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400492 return 0;
493}
494
495void
496UPnPContext::requestMapping(const Mapping::sharedPtr_t& map)
497{
498 assert(map);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400499 auto const& igd = getCurrentIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400500 // We must have at least a valid IGD pointer if we get here.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400501 // Note that this method is called only if there was a valid IGD, but
502 // because the processing is asynchronous, there may no longer
503 // be one by the time this code executes.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400504 if (not igd) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400505 if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available",
506 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400507 return;
508 }
509
510 map->setIgd(igd);
511
Adrien Berauda8731ac2023-08-17 12:19:39 -0400512 if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]",
513 map->toString(),
514 igd->getProtocolName(),
515 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400516
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400517 updateMappingState(map, MappingState::IN_PROGRESS);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400518
519 auto const& protocol = protocolList_.at(igd->getProtocol());
520 protocol->requestMappingAdd(*map);
521}
522
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400523void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400524UPnPContext::provisionNewMappings(PortType type, int portCount)
525{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400526 if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400527
Adrien Béraud612b55b2023-05-29 10:42:04 -0400528 while (portCount > 0) {
529 auto port = getAvailablePortNumber(type);
530 if (port > 0) {
531 // Found an available port number
532 portCount--;
533 Mapping map(type, port, port, true);
534 registerMapping(map);
535 } else {
536 // Very unlikely to get here!
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400537 if (logger_) logger_->error("Cannot provision port: no available port number");
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -0400538 return;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400539 }
540 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400541}
542
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400543void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400544UPnPContext::deleteUnneededMappings(PortType type, int portCount)
545{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400546 if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400547
Adrien Béraud024c46f2024-03-02 23:53:18 -0500548 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400549 auto& mappingList = getMappingList(type);
550
551 for (auto it = mappingList.begin(); it != mappingList.end();) {
552 auto map = it->second;
553 assert(map);
554
555 if (not map->isAvailable()) {
556 it++;
557 continue;
558 }
559
560 if (map->getState() == MappingState::OPEN and portCount > 0) {
561 // Close portCount mappings in "OPEN" state.
562 requestRemoveMapping(map);
Adrien Béraud370257c2023-08-15 20:53:09 -0400563 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400564 portCount--;
565 } else if (map->getState() != MappingState::OPEN) {
566 // If this methods is called, it means there are more open
567 // mappings than required. So, all mappings in a state other
568 // than "OPEN" state (typically in in-progress state) will
569 // be deleted as well.
Adrien Béraud370257c2023-08-15 20:53:09 -0400570 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400571 } else {
572 it++;
573 }
574 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400575}
576
577void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400578UPnPContext::updateCurrentIgd()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400579{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400580 std::lock_guard lock(mappingMutex_);
581 if (currentIgd_ and currentIgd_->isValid()) {
582 if (logger_) logger_->debug("Current IGD is still valid, no need to update");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400583 return;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400584 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400585
586 // Reset and search for the best IGD.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400587 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400588
589 for (auto const& [_, protocol] : protocolList_) {
590 if (protocol->isReady()) {
591 auto igdList = protocol->getIgdList();
592 assert(not igdList.empty());
593 auto const& igd = igdList.front();
594 if (not igd->isValid())
595 continue;
596
597 // Prefer NAT-PMP over PUPNP.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400598 if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400599 continue;
600
601 // Update.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400602 currentIgd_ = igd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400603 }
604 }
605
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400606 if (currentIgd_ and currentIgd_->isValid()) {
607 if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ",
608 currentIgd_->getProtocolName(),
609 currentIgd_->getUID(),
610 currentIgd_->toString());
611 } else {
612 if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400613 }
614}
615
616std::shared_ptr<IGD>
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400617UPnPContext::getCurrentIgd() const
Adrien Béraud612b55b2023-05-29 10:42:04 -0400618{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400619 return currentIgd_;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400620}
621
622void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400623UPnPContext::enforceAvailableMappingsLimits()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400624{
Amna8905f902024-09-10 17:35:15 -0400625 // If there is no valid IGD, do nothing.
626 if (!isReady())
627 return;
Amna7333b1f2024-08-27 10:58:34 -0400628
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400629 for (auto type : {PortType::TCP, PortType::UDP}) {
630 int pendingCount = 0;
631 int inProgressCount = 0;
632 int openCount = 0;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400633 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500634 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400635 const auto& mappingList = getMappingList(type);
636 for (const auto& [_, mapping] : mappingList) {
637 if (!mapping->isAvailable())
Adrien Béraud612b55b2023-05-29 10:42:04 -0400638 continue;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400639 switch (mapping->getState()) {
640 case MappingState::PENDING:
641 pendingCount++;
642 break;
643 case MappingState::IN_PROGRESS:
644 inProgressCount++;
645 break;
646 case MappingState::OPEN:
647 openCount++;
648 break;
649 default:
650 break;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400651 }
652 }
653 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400654 int availableCount = openCount + pendingCount + inProgressCount;
655 if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)",
656 Mapping::getTypeStr(type),
657 availableCount,
658 openCount,
659 pendingCount,
660 inProgressCount);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400661
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400662 int minAvailableMappings = getMinAvailableMappings(type);
663 if (minAvailableMappings > availableCount) {
664 provisionNewMappings(type, minAvailableMappings - availableCount);
665 continue;
666 }
667
668 int maxAvailableMappings = getMaxAvailableMappings(type);
669 if (openCount > maxAvailableMappings) {
670 deleteUnneededMappings(type, openCount - maxAvailableMappings);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400671 }
672 }
673}
674
675void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400676UPnPContext::renewMappings()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400677{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400678 if (!started_)
679 return;
680
681 const auto& igd = getCurrentIgd();
682 if (!igd) {
683 if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available");
684 return;
685 }
686
687 auto now = sys_clock::now();
688 auto nextRenewalTime = sys_clock::time_point::max();
689
690 std::vector<Mapping::sharedPtr_t> toRenew;
691 int toRenewLaterCount = 0;
692
693 for (auto type : {PortType::TCP, PortType::UDP}) {
694 std::lock_guard lock(mappingMutex_);
695 const auto& mappingList = getMappingList(type);
696 for (const auto& [_, map] : mappingList) {
697 if (not map->isValid())
698 continue;
699 if (map->getState() != MappingState::OPEN)
700 continue;
701
702 auto mapRenewalTime = map->getRenewalTime();
703 if (now >= mapRenewalTime) {
704 toRenew.emplace_back(map);
705 } else if (mapRenewalTime < sys_clock::time_point::max()) {
706 toRenewLaterCount++;
707 if (mapRenewalTime < nextRenewalTime)
708 nextRenewalTime = map->getRenewalTime();
709 }
710
711 }
712 }
713
714 if (!toRenew.empty()) {
715 if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size());
716 }
717 for (const auto& map : toRenew) {
718 const auto& protocol = protocolList_.at(map->getIgd()->getProtocol());
719 protocol->requestMappingRenew(*map);
720 }
721 if (toRenewLaterCount > 0) {
722 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
723 if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})",
724 toRenewLaterCount,
725 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
726 mappingRenewalTimer_.expires_at(nextRenewalTime);
727 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
728 if (ec != asio::error::operation_aborted)
729 renewMappings();
730 });
731 }
732}
733
734void
735UPnPContext::scheduleMappingsRenewal()
736{
737 // Debounce the scheduling function so that it doesn't get called multiple
738 // times when several mappings are added or renewed in rapid succession.
739 renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500));
740 renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) {
741 if (ec != asio::error::operation_aborted)
742 _scheduleMappingsRenewal();
743 });
744}
745
746void
747UPnPContext::_scheduleMappingsRenewal()
748{
749 if (!started_)
750 return;
751
752 sys_clock::time_point nextRenewalTime = sys_clock::time_point::max();
753 for (auto type : {PortType::TCP, PortType::UDP}) {
754 std::lock_guard lock(mappingMutex_);
755 const auto& mappingList = getMappingList(type);
756 for (const auto& [_, map] : mappingList) {
757 if (map->getState() == MappingState::OPEN &&
758 map->getRenewalTime() < nextRenewalTime)
759 nextRenewalTime = map->getRenewalTime();
760 }
761 }
762 if (nextRenewalTime == sys_clock::time_point::max())
763 return;
764
765 // Add a small delay so that we don't have to call renewMappings multiple
766 // times in a row (and iterate over the whole list of mappings each time)
767 // when multiple mappings have almost the same renewal time.
768 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
769 if (nextRenewalTime == mappingRenewalTimer_.expiry())
770 return;
771
772 if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}",
773 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
774 mappingRenewalTimer_.expires_at(nextRenewalTime);
775 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
776 if (ec != asio::error::operation_aborted)
777 renewMappings();
778 });
779}
780
781void
782UPnPContext::syncLocalMappingListWithIgd()
783{
784 std::lock_guard lock(syncMutex_);
785 if (syncRequested_)
786 return;
787
788 syncRequested_ = true;
789 syncTimer_.expires_after(std::chrono::minutes(5));
790 syncTimer_.async_wait([this](asio::error_code const& ec) {
791 if (ec != asio::error::operation_aborted)
792 _syncLocalMappingListWithIgd();
793 });
794}
795
796void
797UPnPContext::_syncLocalMappingListWithIgd()
798{
799 {
800 std::lock_guard lock(syncMutex_);
801 syncRequested_ = false;
802 }
803 const auto& igd = getCurrentIgd();
804 if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) {
805 return;
806 }
807 auto pupnp = protocolList_.at(NatProtocolType::PUPNP);
808 if (!pupnp->isReady())
809 return;
810
811 if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]",
812 igd->toString());
813 auto remoteMapList = pupnp->getMappingsListByDescr(igd,
814 Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
815 bool requestsInProgress = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400816 // Use a temporary list to avoid processing mappings while holding the lock.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400817 std::list<Mapping::sharedPtr_t> toRemoveFromLocalList;
818 for (auto type: {PortType::TCP, PortType::UDP}) {
819 std::lock_guard lock(mappingMutex_);
820 for (auto& [_, map] : getMappingList(type)) {
821 if (map->getProtocol() != NatProtocolType::PUPNP) {
822 continue;
823 }
824 switch (map->getState()) {
825 case MappingState::PENDING:
826 case MappingState::IN_PROGRESS:
827 requestsInProgress = true;
828 break;
829 case MappingState::OPEN: {
830 auto it = remoteMapList.find(map->getMapKey());
831 if (it == remoteMapList.end()) {
832 if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
833 "remote list. Removing from local list.",
834 map->toString(),
835 igd->toString());
836 toRemoveFromLocalList.emplace_back(map);
837 } else {
838 auto oldExpiryTime = map->getExpiryTime();
839 auto newExpiryTime = it->second.getExpiryTime();
840 // The value of newExpiryTime is based on the mapping's "lease duration" that we got from
841 // the IGD, which is supposed to be (according to the UPnP specification) the number of
842 // seconds remaining before the mapping expires. In practice, the duration values returned
843 // by some routers are only precise to the hour (i.e. they're always multiples of 3600). This
844 // means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case.
845 // In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to
846 // push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin.
847 if (newExpiryTime < oldExpiryTime ||
848 newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) {
849 auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2;
850 map->setRenewalTime(newRenewalTime);
851 map->setExpiryTime(newExpiryTime);
852 }
853 }
854 break;
855 }
856 default:
857 break;
858 }
859 }
860 }
861 scheduleMappingsRenewal();
862
863 for (auto const& map : toRemoveFromLocalList) {
864 updateMappingState(map, MappingState::FAILED);
865 unregisterMapping(map);
866 }
867 if (!toRemoveFromLocalList.empty())
868 enforceAvailableMappingsLimits();
869
870 if (requestsInProgress) {
871 // It's unlikely that there will be requests in progress when this function is
872 // called, but if there are, that suggests that we are dealing with a slow
873 // router, so we return early instead of sending additional deletion requests
874 // (which aren't essential and could end up "competing" with higher-priority
875 // creation/renewal requests).
876 return;
877 }
878 // Use a temporary list to avoid processing mappings while holding the lock.
879 std::list<Mapping> toRemoveFromIgd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400880 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500881 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400882
883 for (auto const& [_, map] : remoteMapList) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400884 const auto& mappingList = getMappingList(map.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400885 auto it = mappingList.find(map.getMapKey());
886 if (it == mappingList.end()) {
887 // Not present, request mapping remove.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400888 toRemoveFromIgd.emplace_back(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400889 // Make only few remove requests at once.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400890 if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400891 break;
892 }
893 }
894 }
895
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400896 for (const auto& map : toRemoveFromIgd) {
897 pupnp->requestMappingRemove(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400898 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400899
Adrien Béraud612b55b2023-05-29 10:42:04 -0400900}
901
902void
903UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
904{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400905 // Use temporary list to avoid holding the lock while
906 // processing the mapping list.
907 std::list<Mapping::sharedPtr_t> toRemoveList;
908 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500909 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400910
911 PortType types[2] {PortType::TCP, PortType::UDP};
912 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400913 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400914 for (auto const& [_, map] : mappingList) {
915 if (map->getIgd() == igd)
916 toRemoveList.emplace_back(map);
917 }
918 }
919 }
920
921 for (auto const& map : toRemoveList) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400922 if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
923 map->toString(),
924 igd->toString(),
925 igd->getProtocolName());
Adrien Béraud56740312023-08-23 08:38:28 -0400926 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400927 unregisterMapping(map);
928 }
929}
930
931void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400932UPnPContext::processPendingRequests()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400933{
934 // This list holds the mappings to be requested. This is
935 // needed to avoid performing the requests while holding
936 // the lock.
937 std::list<Mapping::sharedPtr_t> requestsList;
938
939 // Populate the list of requests to perform.
940 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500941 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400942 PortType typeArray[2] {PortType::TCP, PortType::UDP};
943
944 for (auto type : typeArray) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400945 const auto& mappingList = getMappingList(type);
946 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400947 if (map->getState() == MappingState::PENDING) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400948 if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
949 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400950 requestsList.emplace_back(map);
951 }
952 }
953 }
954 }
955
956 // Process the pending requests.
957 for (auto const& map : requestsList) {
958 requestMapping(map);
959 }
960}
961
962void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400963UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
964{
965 assert(igd);
966
Adrien Béraud612b55b2023-05-29 10:42:04 -0400967 char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
968 : event == UpnpIgdEvent::REMOVED ? "REMOVED"
969 : "INVALID";
970
971 auto const& igdLocalAddr = igd->getLocalIp();
972 auto protocolName = igd->getProtocolName();
973
Adrien Berauda8731ac2023-08-17 12:19:39 -0400974 if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
975 igd->getUID(),
976 igd->toString(),
977 protocolName,
978 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400979
Adrien Béraud612b55b2023-05-29 10:42:04 -0400980 if (not igdLocalAddr) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400981 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
982 protocolName,
983 igd->getUID(),
984 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400985 return;
986 }
987
988 if (not igd->getPublicIp()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400989 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
990 protocolName,
991 igd->getUID(),
992 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400993 return;
994 }
995
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400996 {
997 std::lock_guard lock(publicAddressMutex_);
998 if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
999 if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
1000 " The mapped addresses might not be reachable",
1001 protocolName,
1002 igd->getPublicIp().toString(),
1003 knownPublicAddress_.toString());
1004 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001005 }
1006
Adrien Béraud612b55b2023-05-29 10:42:04 -04001007 if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001008 if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
1009 igd->getUID(),
1010 igd->toString(),
1011 protocolName,
1012 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001013
1014 pruneMappingsWithInvalidIgds(igd);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001015 }
1016
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001017 updateCurrentIgd();
1018 if (isReady()) {
1019 processPendingRequests();
1020 enforceAvailableMappingsLimits();
1021 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001022}
1023
1024void
1025UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1026{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001027 // Check if we have a pending request for this response.
1028 auto map = getMappingWithKey(mapRes.getMapKey());
1029 if (not map) {
1030 // We may receive a response for a canceled request. Just ignore it.
Adrien Berauda8731ac2023-08-17 12:19:39 -04001031 if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
1032 mapRes.toString(),
1033 igd->toString(),
1034 mapRes.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001035 return;
1036 }
1037
1038 // The mapping request is new and successful. Update.
1039 map->setIgd(igd);
1040 map->setInternalAddress(mapRes.getInternalAddress());
1041 map->setExternalPort(mapRes.getExternalPort());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001042 map->setRenewalTime(mapRes.getRenewalTime());
1043 map->setExpiryTime(mapRes.getExpiryTime());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001044 // Update the state and report to the owner.
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001045 updateMappingState(map, MappingState::OPEN);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001046 scheduleMappingsRenewal();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001047
Adrien Berauda8731ac2023-08-17 12:19:39 -04001048 if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
1049 map->toString(),
1050 igd->toString(),
1051 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001052
1053 // Call setValid() to reset the errors counter. We need
1054 // to reset the counter on each successful response.
1055 igd->setValid(true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001056 if (igd->getProtocol() == NatProtocolType::PUPNP)
1057 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001058}
1059
Adrien Béraud612b55b2023-05-29 10:42:04 -04001060void
1061UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
1062{
1063 auto mapPtr = getMappingWithKey(map.getMapKey());
1064
1065 if (not mapPtr) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001066 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
1067 map.toString(),
1068 igd->toString(),
1069 map.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001070 return;
1071 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001072 if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001073 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
1074 mapPtr->toString(),
1075 igd->toString(),
1076 mapPtr->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001077 return;
1078 }
1079
1080 mapPtr->setRenewalTime(map.getRenewalTime());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001081 mapPtr->setExpiryTime(map.getExpiryTime());
1082 scheduleMappingsRenewal();
1083 if (igd->getProtocol() == NatProtocolType::PUPNP)
1084 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001085}
Adrien Béraud612b55b2023-05-29 10:42:04 -04001086
1087void
1088UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
1089{
Adrien Béraud370257c2023-08-15 20:53:09 -04001090 if (not map or not map->isValid()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001091 // Silently ignore if the mapping is invalid
1092 return;
1093 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001094 auto protocol = protocolList_.at(map->getIgd()->getProtocol());
1095 protocol->requestMappingRemove(*map);
1096}
1097
1098void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001099UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1100{
1101 if (not mapRes.isValid())
1102 return;
1103
Adrien Béraud612b55b2023-05-29 10:42:04 -04001104 auto map = getMappingWithKey(mapRes.getMapKey());
1105 // Notify the listener.
1106 if (map and map->getNotifyCallback())
1107 map->getNotifyCallback()(map);
1108}
1109
Amna0d215232024-08-27 17:57:45 -04001110void
1111UPnPContext::onIgdDiscoveryStarted()
1112{
1113 std::lock_guard lock(igdDiscoveryMutex_);
1114 igdDiscoveryInProgress_ = true;
1115 if (logger_) logger_->debug("IGD Discovery started");
1116 igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
1117 igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
1118 if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
1119 _endIgdDiscovery();
1120 }
1121 });
1122}
1123
1124void
1125UPnPContext::_endIgdDiscovery()
1126{
1127 std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
1128 igdDiscoveryInProgress_ = false;
1129 if (logger_) logger_->debug("IGD Discovery ended");
1130 if (isReady()) {
1131 return;
1132 }
1133 // if there is no valid IGD, the pending mapping requests will be changed to failed
1134 std::lock_guard lockMappings_(mappingMutex_);
1135 PortType types[2] {PortType::TCP, PortType::UDP};
1136 for (auto& type : types) {
1137 const auto& mappingList = getMappingList(type);
1138 for (auto const& [_, map] : mappingList) {
1139 updateMappingState(map, MappingState::FAILED);
1140 // Do not unregister the mapping, it's up to the controller to decide. It will be unregistered when the controller releases it.
1141 // unregisterMapping(map) here will cause a deadlock because of the lock on mappingMutex_.
1142 if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1143 map->toString());
1144 }
1145 }
1146}
1147
1148void
1149UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
1150{
1151 std::lock_guard lock(igdDiscoveryMutex_);
1152 igdDiscoveryTimeout_ = timeout;
1153}
1154
Adrien Béraud612b55b2023-05-29 10:42:04 -04001155Mapping::sharedPtr_t
1156UPnPContext::registerMapping(Mapping& map)
1157{
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001158 Mapping::sharedPtr_t mapPtr;
1159
Adrien Béraud612b55b2023-05-29 10:42:04 -04001160 if (map.getExternalPort() == 0) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001161 auto port = getAvailablePortNumber(map.getType());
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001162 if (port == 0) {
1163 if (logger_) logger_->error("Unable to register mapping: no available port number");
1164 return mapPtr;
1165 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001166 map.setExternalPort(port);
1167 map.setInternalPort(port);
1168 }
1169
1170 // Newly added mapping must be in pending state by default.
1171 map.setState(MappingState::PENDING);
1172
Adrien Béraud612b55b2023-05-29 10:42:04 -04001173 {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001174 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001175 auto& mappingList = getMappingList(map.getType());
1176
1177 auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
1178 if (not ret.second) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001179 if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001180 return {};
1181 }
1182 mapPtr = ret.first->second;
1183 assert(mapPtr);
1184 }
1185
Adrien Béraud612b55b2023-05-29 10:42:04 -04001186 if (not isReady()) {
Amna0d215232024-08-27 17:57:45 -04001187 // There is no valid IGD available
1188 std::lock_guard lock(igdDiscoveryMutex_);
1189 // IGD discovery is in progress, the mapping request will be made once an IGD becomes available
1190 if (igdDiscoveryInProgress_) {
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001191 if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
Amna0d215232024-08-27 17:57:45 -04001192 map.toString());
1193 } else {
1194 // it's not in the IGD discovery phase, the mapping request will fail
1195 if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1196 map.toString());
1197 updateMappingState(mapPtr, MappingState::FAILED);
1198 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001199 } else {
Amna0d215232024-08-27 17:57:45 -04001200 // There is a valid IGD available, request the mapping.
Adrien Béraud612b55b2023-05-29 10:42:04 -04001201 requestMapping(mapPtr);
1202 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001203 return mapPtr;
1204}
1205
Adrien Béraud612b55b2023-05-29 10:42:04 -04001206void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001207UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate)
Adrien Béraud612b55b2023-05-29 10:42:04 -04001208{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001209 if (not map) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001210 return;
1211 }
1212
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001213 if (map->getAutoUpdate() && !ignoreAutoUpdate) {
1214 if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
1215 map->toString());
1216
1217 Mapping newMapping(map->getType());
1218 newMapping.enableAutoUpdate(true);
1219 newMapping.setNotifyCallback(map->getNotifyCallback());
1220 reserveMapping(newMapping);
1221
1222 // TODO: figure out if this line is actually necessary
1223 // (See https://review.jami.net/c/jami-daemon/+/16940)
1224 map->setNotifyCallback(nullptr);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001225 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001226 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001227 auto& mappingList = getMappingList(map->getType());
1228
1229 if (mappingList.erase(map->getMapKey()) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001230 if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001231 } else {
1232 // The mapping may already be un-registered. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001233 if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001234 map->toString(),
1235 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001236 }
1237}
1238
1239std::map<Mapping::key_t, Mapping::sharedPtr_t>&
1240UPnPContext::getMappingList(PortType type)
1241{
1242 unsigned typeIdx = type == PortType::TCP ? 0 : 1;
1243 return mappingList_[typeIdx];
1244}
1245
1246Mapping::sharedPtr_t
1247UPnPContext::getMappingWithKey(Mapping::key_t key)
1248{
Adrien Béraud024c46f2024-03-02 23:53:18 -05001249 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001250 auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
1251 auto it = mappingList.find(key);
1252 if (it == mappingList.end())
1253 return nullptr;
1254 return it->second;
1255}
1256
1257void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001258UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
1259{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001260 auto igd = mapRes.getIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001261 auto const& map = getMappingWithKey(mapRes.getMapKey());
1262 if (not map) {
1263 // We may receive a response for a removed request. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001264 if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
1265 mapRes.toString(),
1266 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001267 return;
1268 }
1269
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001270 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001271 unregisterMapping(map);
1272
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001273 if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001274 map->toString(),
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001275 igd->toString());
1276
1277 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001278}
1279
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001280void
1281UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
1282{
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001283 assert(map);
1284
1285 // Ignore if the state did not change.
1286 if (newState == map->getState()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001287 return;
1288 }
1289
1290 // Update the state.
1291 map->setState(newState);
1292
1293 // Notify the listener if set.
1294 if (notify and map->getNotifyCallback())
1295 map->getNotifyCallback()(map);
1296}
1297
Adrien Béraud612b55b2023-05-29 10:42:04 -04001298} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001299} // namespace dhtnet