blob: cf9b6ed3ddd501bb1b389225247b5464966f1e4e [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
Adrien Béraudc36965c2023-08-17 21:50:27 -0400402 // Remove it.
403 requestRemoveMapping(mapPtr);
Amna6f458612024-08-26 13:49:27 -0400404 unregisterMapping(mapPtr, true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400405 enforceAvailableMappingsLimits();
Adrien Béraudc36965c2023-08-17 21:50:27 -0400406 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400407}
408
409void
410UPnPContext::registerController(void* controller)
411{
412 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500413 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400414 if (shutdownComplete_) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400415 if (logger_) logger_->warn("UPnPContext already shut down");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400416 return;
417 }
Adrien Béraudc36965c2023-08-17 21:50:27 -0400418 auto ret = controllerList_.emplace(controller);
419 if (not ret.second) {
420 if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller));
421 return;
422 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400423 }
424
Adrien Berauda8731ac2023-08-17 12:19:39 -0400425 if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400426 if (not started_)
427 startUpnp();
428}
429
430void
431UPnPContext::unregisterController(void* controller)
432{
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500433 if (shutdownComplete_)
434 return;
Adrien Béraud024c46f2024-03-02 23:53:18 -0500435 std::unique_lock lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400436 if (controllerList_.erase(controller) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400437 if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400438 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400439 if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400440 }
441
442 if (controllerList_.empty()) {
Adrien Béraudc36965c2023-08-17 21:50:27 -0400443 lock.unlock();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400444 stopUpnp();
445 }
446}
447
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400448std::vector<IGDInfo>
449UPnPContext::getIgdsInfo() const
450{
451 std::vector<IGDInfo> igdInfoList;
452
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400453 for (const auto& [_, protocol] : protocolList_) {
454 for (auto& igd : protocol->getIgdList()) {
455 IGDInfo info;
456 info.uid = igd->getUID();
457 info.localIp = igd->getLocalIp();
458 info.publicIp = igd->getPublicIp();
459 info.mappingInfoList = protocol->getMappingsInfo(igd);
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400460
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400461 igdInfoList.push_back(std::move(info));
462 }
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400463 }
464
465 return igdInfoList;
466}
467
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400468// 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 -0400469uint16_t
470UPnPContext::getAvailablePortNumber(PortType type)
471{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400472 // Only return an available random port. No actual
Adrien Béraud612b55b2023-05-29 10:42:04 -0400473 // reservation is made here.
474
Adrien Béraud024c46f2024-03-02 23:53:18 -0500475 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400476 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400477 int tryCount = 0;
478 while (tryCount++ < MAX_REQUEST_RETRIES) {
479 uint16_t port = generateRandomPort(type);
480 Mapping map(type, port, port);
481 if (mappingList.find(map.getMapKey()) == mappingList.end())
482 return port;
483 }
484
485 // Very unlikely to get here.
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -0400486 if (logger_) logger_->error("Could not find an available port after {} trials", MAX_REQUEST_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400487 return 0;
488}
489
490void
491UPnPContext::requestMapping(const Mapping::sharedPtr_t& map)
492{
493 assert(map);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400494 auto const& igd = getCurrentIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400495 // We must have at least a valid IGD pointer if we get here.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400496 // Note that this method is called only if there was a valid IGD, but
497 // because the processing is asynchronous, there may no longer
498 // be one by the time this code executes.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400499 if (not igd) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400500 if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available",
501 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400502 return;
503 }
504
505 map->setIgd(igd);
506
Adrien Berauda8731ac2023-08-17 12:19:39 -0400507 if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]",
508 map->toString(),
509 igd->getProtocolName(),
510 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400511
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400512 updateMappingState(map, MappingState::IN_PROGRESS);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400513
514 auto const& protocol = protocolList_.at(igd->getProtocol());
515 protocol->requestMappingAdd(*map);
516}
517
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400518void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400519UPnPContext::provisionNewMappings(PortType type, int portCount)
520{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400521 if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400522
Adrien Béraud612b55b2023-05-29 10:42:04 -0400523 while (portCount > 0) {
524 auto port = getAvailablePortNumber(type);
525 if (port > 0) {
526 // Found an available port number
527 portCount--;
528 Mapping map(type, port, port, true);
529 registerMapping(map);
530 } else {
531 // Very unlikely to get here!
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400532 if (logger_) logger_->error("Cannot provision port: no available port number");
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -0400533 return;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400534 }
535 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400536}
537
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400538void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400539UPnPContext::deleteUnneededMappings(PortType type, int portCount)
540{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400541 if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400542
Adrien Béraud024c46f2024-03-02 23:53:18 -0500543 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400544 auto& mappingList = getMappingList(type);
545
546 for (auto it = mappingList.begin(); it != mappingList.end();) {
547 auto map = it->second;
548 assert(map);
549
550 if (not map->isAvailable()) {
551 it++;
552 continue;
553 }
554
555 if (map->getState() == MappingState::OPEN and portCount > 0) {
556 // Close portCount mappings in "OPEN" state.
557 requestRemoveMapping(map);
Adrien Béraud370257c2023-08-15 20:53:09 -0400558 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400559 portCount--;
560 } else if (map->getState() != MappingState::OPEN) {
561 // If this methods is called, it means there are more open
562 // mappings than required. So, all mappings in a state other
563 // than "OPEN" state (typically in in-progress state) will
564 // be deleted as well.
Adrien Béraud370257c2023-08-15 20:53:09 -0400565 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400566 } else {
567 it++;
568 }
569 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400570}
571
572void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400573UPnPContext::updateCurrentIgd()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400574{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400575 std::lock_guard lock(mappingMutex_);
576 if (currentIgd_ and currentIgd_->isValid()) {
577 if (logger_) logger_->debug("Current IGD is still valid, no need to update");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400578 return;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400579 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400580
581 // Reset and search for the best IGD.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400582 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400583
584 for (auto const& [_, protocol] : protocolList_) {
585 if (protocol->isReady()) {
586 auto igdList = protocol->getIgdList();
587 assert(not igdList.empty());
588 auto const& igd = igdList.front();
589 if (not igd->isValid())
590 continue;
591
592 // Prefer NAT-PMP over PUPNP.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400593 if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400594 continue;
595
596 // Update.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400597 currentIgd_ = igd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400598 }
599 }
600
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400601 if (currentIgd_ and currentIgd_->isValid()) {
602 if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ",
603 currentIgd_->getProtocolName(),
604 currentIgd_->getUID(),
605 currentIgd_->toString());
606 } else {
607 if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400608 }
609}
610
611std::shared_ptr<IGD>
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400612UPnPContext::getCurrentIgd() const
Adrien Béraud612b55b2023-05-29 10:42:04 -0400613{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400614 return currentIgd_;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400615}
616
617void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400618UPnPContext::enforceAvailableMappingsLimits()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400619{
Amna8905f902024-09-10 17:35:15 -0400620 // If there is no valid IGD, do nothing.
621 if (!isReady())
622 return;
623
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400624 for (auto type : {PortType::TCP, PortType::UDP}) {
625 int pendingCount = 0;
626 int inProgressCount = 0;
627 int openCount = 0;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400628 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500629 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400630 const auto& mappingList = getMappingList(type);
631 for (const auto& [_, mapping] : mappingList) {
632 if (!mapping->isAvailable())
Adrien Béraud612b55b2023-05-29 10:42:04 -0400633 continue;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400634 switch (mapping->getState()) {
635 case MappingState::PENDING:
636 pendingCount++;
637 break;
638 case MappingState::IN_PROGRESS:
639 inProgressCount++;
640 break;
641 case MappingState::OPEN:
642 openCount++;
643 break;
644 default:
645 break;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400646 }
647 }
648 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400649 int availableCount = openCount + pendingCount + inProgressCount;
650 if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)",
651 Mapping::getTypeStr(type),
652 availableCount,
653 openCount,
654 pendingCount,
655 inProgressCount);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400656
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400657 int minAvailableMappings = getMinAvailableMappings(type);
658 if (minAvailableMappings > availableCount) {
659 provisionNewMappings(type, minAvailableMappings - availableCount);
660 continue;
661 }
662
663 int maxAvailableMappings = getMaxAvailableMappings(type);
664 if (openCount > maxAvailableMappings) {
665 deleteUnneededMappings(type, openCount - maxAvailableMappings);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400666 }
667 }
668}
669
670void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400671UPnPContext::renewMappings()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400672{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400673 if (!started_)
674 return;
675
676 const auto& igd = getCurrentIgd();
677 if (!igd) {
678 if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available");
679 return;
680 }
681
682 auto now = sys_clock::now();
683 auto nextRenewalTime = sys_clock::time_point::max();
684
685 std::vector<Mapping::sharedPtr_t> toRenew;
686 int toRenewLaterCount = 0;
687
688 for (auto type : {PortType::TCP, PortType::UDP}) {
689 std::lock_guard lock(mappingMutex_);
690 const auto& mappingList = getMappingList(type);
691 for (const auto& [_, map] : mappingList) {
692 if (not map->isValid())
693 continue;
694 if (map->getState() != MappingState::OPEN)
695 continue;
696
697 auto mapRenewalTime = map->getRenewalTime();
698 if (now >= mapRenewalTime) {
699 toRenew.emplace_back(map);
700 } else if (mapRenewalTime < sys_clock::time_point::max()) {
701 toRenewLaterCount++;
702 if (mapRenewalTime < nextRenewalTime)
703 nextRenewalTime = map->getRenewalTime();
704 }
705
706 }
707 }
708
709 if (!toRenew.empty()) {
710 if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size());
711 }
712 for (const auto& map : toRenew) {
713 const auto& protocol = protocolList_.at(map->getIgd()->getProtocol());
714 protocol->requestMappingRenew(*map);
715 }
716 if (toRenewLaterCount > 0) {
717 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
718 if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})",
719 toRenewLaterCount,
720 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
721 mappingRenewalTimer_.expires_at(nextRenewalTime);
722 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
723 if (ec != asio::error::operation_aborted)
724 renewMappings();
725 });
726 }
727}
728
729void
730UPnPContext::scheduleMappingsRenewal()
731{
732 // Debounce the scheduling function so that it doesn't get called multiple
733 // times when several mappings are added or renewed in rapid succession.
734 renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500));
735 renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) {
736 if (ec != asio::error::operation_aborted)
737 _scheduleMappingsRenewal();
738 });
739}
740
741void
742UPnPContext::_scheduleMappingsRenewal()
743{
744 if (!started_)
745 return;
746
747 sys_clock::time_point nextRenewalTime = sys_clock::time_point::max();
748 for (auto type : {PortType::TCP, PortType::UDP}) {
749 std::lock_guard lock(mappingMutex_);
750 const auto& mappingList = getMappingList(type);
751 for (const auto& [_, map] : mappingList) {
752 if (map->getState() == MappingState::OPEN &&
753 map->getRenewalTime() < nextRenewalTime)
754 nextRenewalTime = map->getRenewalTime();
755 }
756 }
757 if (nextRenewalTime == sys_clock::time_point::max())
758 return;
759
760 // Add a small delay so that we don't have to call renewMappings multiple
761 // times in a row (and iterate over the whole list of mappings each time)
762 // when multiple mappings have almost the same renewal time.
763 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
764 if (nextRenewalTime == mappingRenewalTimer_.expiry())
765 return;
766
767 if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}",
768 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
769 mappingRenewalTimer_.expires_at(nextRenewalTime);
770 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
771 if (ec != asio::error::operation_aborted)
772 renewMappings();
773 });
774}
775
776void
777UPnPContext::syncLocalMappingListWithIgd()
778{
779 std::lock_guard lock(syncMutex_);
780 if (syncRequested_)
781 return;
782
783 syncRequested_ = true;
784 syncTimer_.expires_after(std::chrono::minutes(5));
785 syncTimer_.async_wait([this](asio::error_code const& ec) {
786 if (ec != asio::error::operation_aborted)
787 _syncLocalMappingListWithIgd();
788 });
789}
790
791void
792UPnPContext::_syncLocalMappingListWithIgd()
793{
794 {
795 std::lock_guard lock(syncMutex_);
796 syncRequested_ = false;
797 }
798 const auto& igd = getCurrentIgd();
799 if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) {
800 return;
801 }
802 auto pupnp = protocolList_.at(NatProtocolType::PUPNP);
803 if (!pupnp->isReady())
804 return;
805
806 if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]",
807 igd->toString());
808 auto remoteMapList = pupnp->getMappingsListByDescr(igd,
809 Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
810 bool requestsInProgress = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400811 // Use a temporary list to avoid processing mappings while holding the lock.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400812 std::list<Mapping::sharedPtr_t> toRemoveFromLocalList;
813 for (auto type: {PortType::TCP, PortType::UDP}) {
814 std::lock_guard lock(mappingMutex_);
815 for (auto& [_, map] : getMappingList(type)) {
816 if (map->getProtocol() != NatProtocolType::PUPNP) {
817 continue;
818 }
819 switch (map->getState()) {
820 case MappingState::PENDING:
821 case MappingState::IN_PROGRESS:
822 requestsInProgress = true;
823 break;
824 case MappingState::OPEN: {
825 auto it = remoteMapList.find(map->getMapKey());
826 if (it == remoteMapList.end()) {
827 if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
828 "remote list. Removing from local list.",
829 map->toString(),
830 igd->toString());
831 toRemoveFromLocalList.emplace_back(map);
832 } else {
833 auto oldExpiryTime = map->getExpiryTime();
834 auto newExpiryTime = it->second.getExpiryTime();
835 // The value of newExpiryTime is based on the mapping's "lease duration" that we got from
836 // the IGD, which is supposed to be (according to the UPnP specification) the number of
837 // seconds remaining before the mapping expires. In practice, the duration values returned
838 // by some routers are only precise to the hour (i.e. they're always multiples of 3600). This
839 // means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case.
840 // In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to
841 // push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin.
842 if (newExpiryTime < oldExpiryTime ||
843 newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) {
844 auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2;
845 map->setRenewalTime(newRenewalTime);
846 map->setExpiryTime(newExpiryTime);
847 }
848 }
849 break;
850 }
851 default:
852 break;
853 }
854 }
855 }
856 scheduleMappingsRenewal();
857
858 for (auto const& map : toRemoveFromLocalList) {
859 updateMappingState(map, MappingState::FAILED);
860 unregisterMapping(map);
861 }
862 if (!toRemoveFromLocalList.empty())
863 enforceAvailableMappingsLimits();
864
865 if (requestsInProgress) {
866 // It's unlikely that there will be requests in progress when this function is
867 // called, but if there are, that suggests that we are dealing with a slow
868 // router, so we return early instead of sending additional deletion requests
869 // (which aren't essential and could end up "competing" with higher-priority
870 // creation/renewal requests).
871 return;
872 }
873 // Use a temporary list to avoid processing mappings while holding the lock.
874 std::list<Mapping> toRemoveFromIgd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400875 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500876 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400877
878 for (auto const& [_, map] : remoteMapList) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400879 const auto& mappingList = getMappingList(map.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400880 auto it = mappingList.find(map.getMapKey());
881 if (it == mappingList.end()) {
882 // Not present, request mapping remove.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400883 toRemoveFromIgd.emplace_back(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400884 // Make only few remove requests at once.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400885 if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400886 break;
887 }
888 }
889 }
890
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400891 for (const auto& map : toRemoveFromIgd) {
892 pupnp->requestMappingRemove(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400893 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400894
Adrien Béraud612b55b2023-05-29 10:42:04 -0400895}
896
897void
898UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
899{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400900 // Use temporary list to avoid holding the lock while
901 // processing the mapping list.
902 std::list<Mapping::sharedPtr_t> toRemoveList;
903 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500904 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400905
906 PortType types[2] {PortType::TCP, PortType::UDP};
907 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400908 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400909 for (auto const& [_, map] : mappingList) {
910 if (map->getIgd() == igd)
911 toRemoveList.emplace_back(map);
912 }
913 }
914 }
915
916 for (auto const& map : toRemoveList) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400917 if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
918 map->toString(),
919 igd->toString(),
920 igd->getProtocolName());
Adrien Béraud56740312023-08-23 08:38:28 -0400921 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400922 unregisterMapping(map);
923 }
924}
925
926void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400927UPnPContext::processPendingRequests()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400928{
929 // This list holds the mappings to be requested. This is
930 // needed to avoid performing the requests while holding
931 // the lock.
932 std::list<Mapping::sharedPtr_t> requestsList;
933
934 // Populate the list of requests to perform.
935 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500936 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400937 PortType typeArray[2] {PortType::TCP, PortType::UDP};
938
939 for (auto type : typeArray) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400940 const auto& mappingList = getMappingList(type);
941 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400942 if (map->getState() == MappingState::PENDING) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400943 if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
944 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400945 requestsList.emplace_back(map);
946 }
947 }
948 }
949 }
950
951 // Process the pending requests.
952 for (auto const& map : requestsList) {
953 requestMapping(map);
954 }
955}
956
957void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400958UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
959{
960 assert(igd);
961
Adrien Béraud612b55b2023-05-29 10:42:04 -0400962 char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
963 : event == UpnpIgdEvent::REMOVED ? "REMOVED"
964 : "INVALID";
965
966 auto const& igdLocalAddr = igd->getLocalIp();
967 auto protocolName = igd->getProtocolName();
968
Adrien Berauda8731ac2023-08-17 12:19:39 -0400969 if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
970 igd->getUID(),
971 igd->toString(),
972 protocolName,
973 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400974
Adrien Béraud612b55b2023-05-29 10:42:04 -0400975 if (not igdLocalAddr) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400976 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
977 protocolName,
978 igd->getUID(),
979 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400980 return;
981 }
982
983 if (not igd->getPublicIp()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400984 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
985 protocolName,
986 igd->getUID(),
987 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400988 return;
989 }
990
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400991 {
992 std::lock_guard lock(publicAddressMutex_);
993 if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
994 if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
995 " The mapped addresses might not be reachable",
996 protocolName,
997 igd->getPublicIp().toString(),
998 knownPublicAddress_.toString());
999 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001000 }
1001
Adrien Béraud612b55b2023-05-29 10:42:04 -04001002 if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001003 if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
1004 igd->getUID(),
1005 igd->toString(),
1006 protocolName,
1007 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001008
1009 pruneMappingsWithInvalidIgds(igd);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001010 }
1011
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001012 updateCurrentIgd();
1013 if (isReady()) {
1014 processPendingRequests();
1015 enforceAvailableMappingsLimits();
1016 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001017}
1018
1019void
1020UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1021{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001022 // Check if we have a pending request for this response.
1023 auto map = getMappingWithKey(mapRes.getMapKey());
1024 if (not map) {
1025 // We may receive a response for a canceled request. Just ignore it.
Adrien Berauda8731ac2023-08-17 12:19:39 -04001026 if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
1027 mapRes.toString(),
1028 igd->toString(),
1029 mapRes.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001030 return;
1031 }
1032
1033 // The mapping request is new and successful. Update.
1034 map->setIgd(igd);
1035 map->setInternalAddress(mapRes.getInternalAddress());
1036 map->setExternalPort(mapRes.getExternalPort());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001037 map->setRenewalTime(mapRes.getRenewalTime());
1038 map->setExpiryTime(mapRes.getExpiryTime());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001039 // Update the state and report to the owner.
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001040 updateMappingState(map, MappingState::OPEN);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001041 scheduleMappingsRenewal();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001042
Adrien Berauda8731ac2023-08-17 12:19:39 -04001043 if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
1044 map->toString(),
1045 igd->toString(),
1046 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001047
1048 // Call setValid() to reset the errors counter. We need
1049 // to reset the counter on each successful response.
1050 igd->setValid(true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001051 if (igd->getProtocol() == NatProtocolType::PUPNP)
1052 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001053}
1054
Adrien Béraud612b55b2023-05-29 10:42:04 -04001055void
1056UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
1057{
1058 auto mapPtr = getMappingWithKey(map.getMapKey());
1059
1060 if (not mapPtr) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001061 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
1062 map.toString(),
1063 igd->toString(),
1064 map.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001065 return;
1066 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001067 if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001068 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
1069 mapPtr->toString(),
1070 igd->toString(),
1071 mapPtr->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001072 return;
1073 }
1074
1075 mapPtr->setRenewalTime(map.getRenewalTime());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001076 mapPtr->setExpiryTime(map.getExpiryTime());
1077 scheduleMappingsRenewal();
1078 if (igd->getProtocol() == NatProtocolType::PUPNP)
1079 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001080}
Adrien Béraud612b55b2023-05-29 10:42:04 -04001081
1082void
1083UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
1084{
Adrien Béraud370257c2023-08-15 20:53:09 -04001085 if (not map or not map->isValid()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001086 // Silently ignore if the mapping is invalid
1087 return;
1088 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001089 auto protocol = protocolList_.at(map->getIgd()->getProtocol());
1090 protocol->requestMappingRemove(*map);
1091}
1092
1093void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001094UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1095{
1096 if (not mapRes.isValid())
1097 return;
1098
Adrien Béraud612b55b2023-05-29 10:42:04 -04001099 auto map = getMappingWithKey(mapRes.getMapKey());
1100 // Notify the listener.
1101 if (map and map->getNotifyCallback())
1102 map->getNotifyCallback()(map);
1103}
1104
Amna0d215232024-08-27 17:57:45 -04001105void
1106UPnPContext::onIgdDiscoveryStarted()
1107{
1108 std::lock_guard lock(igdDiscoveryMutex_);
1109 igdDiscoveryInProgress_ = true;
1110 if (logger_) logger_->debug("IGD Discovery started");
1111 igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
1112 igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
1113 if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
1114 _endIgdDiscovery();
1115 }
1116 });
1117}
1118
1119void
1120UPnPContext::_endIgdDiscovery()
1121{
1122 std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
1123 igdDiscoveryInProgress_ = false;
1124 if (logger_) logger_->debug("IGD Discovery ended");
1125 if (isReady()) {
1126 return;
1127 }
1128 // if there is no valid IGD, the pending mapping requests will be changed to failed
1129 std::lock_guard lockMappings_(mappingMutex_);
1130 PortType types[2] {PortType::TCP, PortType::UDP};
1131 for (auto& type : types) {
1132 const auto& mappingList = getMappingList(type);
1133 for (auto const& [_, map] : mappingList) {
1134 updateMappingState(map, MappingState::FAILED);
1135 // Do not unregister the mapping, it's up to the controller to decide. It will be unregistered when the controller releases it.
1136 // unregisterMapping(map) here will cause a deadlock because of the lock on mappingMutex_.
1137 if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1138 map->toString());
1139 }
1140 }
1141}
1142
1143void
1144UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
1145{
1146 std::lock_guard lock(igdDiscoveryMutex_);
1147 igdDiscoveryTimeout_ = timeout;
1148}
1149
Adrien Béraud612b55b2023-05-29 10:42:04 -04001150Mapping::sharedPtr_t
1151UPnPContext::registerMapping(Mapping& map)
1152{
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001153 Mapping::sharedPtr_t mapPtr;
1154
Adrien Béraud612b55b2023-05-29 10:42:04 -04001155 if (map.getExternalPort() == 0) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001156 auto port = getAvailablePortNumber(map.getType());
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001157 if (port == 0) {
1158 if (logger_) logger_->error("Unable to register mapping: no available port number");
1159 return mapPtr;
1160 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001161 map.setExternalPort(port);
1162 map.setInternalPort(port);
1163 }
1164
1165 // Newly added mapping must be in pending state by default.
1166 map.setState(MappingState::PENDING);
1167
Adrien Béraud612b55b2023-05-29 10:42:04 -04001168 {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001169 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001170 auto& mappingList = getMappingList(map.getType());
1171
1172 auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
1173 if (not ret.second) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001174 if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001175 return {};
1176 }
1177 mapPtr = ret.first->second;
1178 assert(mapPtr);
1179 }
1180
Adrien Béraud612b55b2023-05-29 10:42:04 -04001181 if (not isReady()) {
Amna0d215232024-08-27 17:57:45 -04001182 // There is no valid IGD available
1183 std::lock_guard lock(igdDiscoveryMutex_);
1184 // IGD discovery is in progress, the mapping request will be made once an IGD becomes available
1185 if (igdDiscoveryInProgress_) {
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001186 if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
Amna0d215232024-08-27 17:57:45 -04001187 map.toString());
1188 } else {
1189 // it's not in the IGD discovery phase, the mapping request will fail
1190 if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1191 map.toString());
1192 updateMappingState(mapPtr, MappingState::FAILED);
1193 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001194 } else {
Amna0d215232024-08-27 17:57:45 -04001195 // There is a valid IGD available, request the mapping.
Adrien Béraud612b55b2023-05-29 10:42:04 -04001196 requestMapping(mapPtr);
1197 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001198 return mapPtr;
1199}
1200
Adrien Béraud612b55b2023-05-29 10:42:04 -04001201void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001202UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate)
Adrien Béraud612b55b2023-05-29 10:42:04 -04001203{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001204 if (not map) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001205 return;
1206 }
1207
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001208 if (map->getAutoUpdate() && !ignoreAutoUpdate) {
1209 if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
1210 map->toString());
1211
1212 Mapping newMapping(map->getType());
1213 newMapping.enableAutoUpdate(true);
1214 newMapping.setNotifyCallback(map->getNotifyCallback());
1215 reserveMapping(newMapping);
1216
1217 // TODO: figure out if this line is actually necessary
1218 // (See https://review.jami.net/c/jami-daemon/+/16940)
1219 map->setNotifyCallback(nullptr);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001220 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001221 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001222 auto& mappingList = getMappingList(map->getType());
1223
1224 if (mappingList.erase(map->getMapKey()) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001225 if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001226 } else {
1227 // The mapping may already be un-registered. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001228 if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001229 map->toString(),
1230 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001231 }
1232}
1233
1234std::map<Mapping::key_t, Mapping::sharedPtr_t>&
1235UPnPContext::getMappingList(PortType type)
1236{
1237 unsigned typeIdx = type == PortType::TCP ? 0 : 1;
1238 return mappingList_[typeIdx];
1239}
1240
1241Mapping::sharedPtr_t
1242UPnPContext::getMappingWithKey(Mapping::key_t key)
1243{
Adrien Béraud024c46f2024-03-02 23:53:18 -05001244 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001245 auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
1246 auto it = mappingList.find(key);
1247 if (it == mappingList.end())
1248 return nullptr;
1249 return it->second;
1250}
1251
1252void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001253UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
1254{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001255 auto igd = mapRes.getIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001256 auto const& map = getMappingWithKey(mapRes.getMapKey());
1257 if (not map) {
1258 // We may receive a response for a removed request. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001259 if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
1260 mapRes.toString(),
1261 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001262 return;
1263 }
1264
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001265 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001266 unregisterMapping(map);
1267
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001268 if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001269 map->toString(),
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001270 igd->toString());
1271
1272 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001273}
1274
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001275void
1276UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
1277{
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001278 assert(map);
1279
1280 // Ignore if the state did not change.
1281 if (newState == map->getState()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001282 return;
1283 }
1284
1285 // Update the state.
1286 map->setState(newState);
1287
1288 // Notify the listener if set.
1289 if (notify and map->getNotifyCallback())
1290 map->getNotifyCallback()(map);
1291}
1292
Adrien Béraud612b55b2023-05-29 10:42:04 -04001293} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001294} // namespace dhtnet