blob: b7ec7c99fa633c77f564f3787fed1716b030cc94 [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 {
Amna18515ae2024-09-11 16:39:11 -0400212 unregisterMapping(map);
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.
Amna18515ae2024-09-11 16:39:11 -0400817 std::list<Mapping::sharedPtr_t> failedMappings;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400818 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 "
Amna18515ae2024-09-11 16:39:11 -0400833 "remote list. Setting state to \"FAILED\".",
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400834 map->toString(),
835 igd->toString());
Amna18515ae2024-09-11 16:39:11 -0400836 failedMappings.emplace_back(map);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400837 } 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
Amna18515ae2024-09-11 16:39:11 -0400863 for (auto const& map : failedMappings) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400864 updateMappingState(map, MappingState::FAILED);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400865 }
Amna18515ae2024-09-11 16:39:11 -0400866 if (!failedMappings.empty())
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400867 enforceAvailableMappingsLimits();
868
869 if (requestsInProgress) {
870 // It's unlikely that there will be requests in progress when this function is
871 // called, but if there are, that suggests that we are dealing with a slow
872 // router, so we return early instead of sending additional deletion requests
873 // (which aren't essential and could end up "competing" with higher-priority
874 // creation/renewal requests).
875 return;
876 }
877 // Use a temporary list to avoid processing mappings while holding the lock.
878 std::list<Mapping> toRemoveFromIgd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400879 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500880 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400881
882 for (auto const& [_, map] : remoteMapList) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400883 const auto& mappingList = getMappingList(map.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400884 auto it = mappingList.find(map.getMapKey());
885 if (it == mappingList.end()) {
886 // Not present, request mapping remove.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400887 toRemoveFromIgd.emplace_back(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400888 // Make only few remove requests at once.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400889 if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400890 break;
891 }
892 }
893 }
894
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400895 for (const auto& map : toRemoveFromIgd) {
896 pupnp->requestMappingRemove(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400897 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400898
Adrien Béraud612b55b2023-05-29 10:42:04 -0400899}
900
901void
902UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
903{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400904 // Use temporary list to avoid holding the lock while
905 // processing the mapping list.
906 std::list<Mapping::sharedPtr_t> toRemoveList;
907 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500908 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400909
910 PortType types[2] {PortType::TCP, PortType::UDP};
911 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400912 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400913 for (auto const& [_, map] : mappingList) {
914 if (map->getIgd() == igd)
915 toRemoveList.emplace_back(map);
916 }
917 }
918 }
919
920 for (auto const& map : toRemoveList) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400921 if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
922 map->toString(),
923 igd->toString(),
924 igd->getProtocolName());
Adrien Béraud56740312023-08-23 08:38:28 -0400925 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400926 }
927}
928
929void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400930UPnPContext::processPendingRequests()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400931{
932 // This list holds the mappings to be requested. This is
933 // needed to avoid performing the requests while holding
934 // the lock.
935 std::list<Mapping::sharedPtr_t> requestsList;
936
937 // Populate the list of requests to perform.
938 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500939 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400940 PortType typeArray[2] {PortType::TCP, PortType::UDP};
941
942 for (auto type : typeArray) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400943 const auto& mappingList = getMappingList(type);
944 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400945 if (map->getState() == MappingState::PENDING) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400946 if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
947 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400948 requestsList.emplace_back(map);
949 }
950 }
951 }
952 }
953
954 // Process the pending requests.
955 for (auto const& map : requestsList) {
956 requestMapping(map);
957 }
958}
959
960void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400961UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
962{
963 assert(igd);
964
Adrien Béraud612b55b2023-05-29 10:42:04 -0400965 char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
966 : event == UpnpIgdEvent::REMOVED ? "REMOVED"
967 : "INVALID";
968
969 auto const& igdLocalAddr = igd->getLocalIp();
970 auto protocolName = igd->getProtocolName();
971
Adrien Berauda8731ac2023-08-17 12:19:39 -0400972 if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
973 igd->getUID(),
974 igd->toString(),
975 protocolName,
976 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400977
Adrien Béraud612b55b2023-05-29 10:42:04 -0400978 if (not igdLocalAddr) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400979 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
980 protocolName,
981 igd->getUID(),
982 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400983 return;
984 }
985
986 if (not igd->getPublicIp()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400987 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
988 protocolName,
989 igd->getUID(),
990 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400991 return;
992 }
993
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400994 {
995 std::lock_guard lock(publicAddressMutex_);
996 if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
997 if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
998 " The mapped addresses might not be reachable",
999 protocolName,
1000 igd->getPublicIp().toString(),
1001 knownPublicAddress_.toString());
1002 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001003 }
1004
Adrien Béraud612b55b2023-05-29 10:42:04 -04001005 if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001006 if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
1007 igd->getUID(),
1008 igd->toString(),
1009 protocolName,
1010 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001011
1012 pruneMappingsWithInvalidIgds(igd);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001013 }
1014
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001015 updateCurrentIgd();
1016 if (isReady()) {
1017 processPendingRequests();
1018 enforceAvailableMappingsLimits();
1019 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001020}
1021
1022void
1023UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1024{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001025 // Check if we have a pending request for this response.
1026 auto map = getMappingWithKey(mapRes.getMapKey());
1027 if (not map) {
1028 // We may receive a response for a canceled request. Just ignore it.
Adrien Berauda8731ac2023-08-17 12:19:39 -04001029 if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
1030 mapRes.toString(),
1031 igd->toString(),
1032 mapRes.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001033 return;
1034 }
1035
1036 // The mapping request is new and successful. Update.
1037 map->setIgd(igd);
1038 map->setInternalAddress(mapRes.getInternalAddress());
1039 map->setExternalPort(mapRes.getExternalPort());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001040 map->setRenewalTime(mapRes.getRenewalTime());
1041 map->setExpiryTime(mapRes.getExpiryTime());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001042 // Update the state and report to the owner.
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001043 updateMappingState(map, MappingState::OPEN);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001044 scheduleMappingsRenewal();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001045
Adrien Berauda8731ac2023-08-17 12:19:39 -04001046 if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
1047 map->toString(),
1048 igd->toString(),
1049 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001050
1051 // Call setValid() to reset the errors counter. We need
1052 // to reset the counter on each successful response.
1053 igd->setValid(true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001054 if (igd->getProtocol() == NatProtocolType::PUPNP)
1055 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001056}
1057
Adrien Béraud612b55b2023-05-29 10:42:04 -04001058void
1059UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
1060{
1061 auto mapPtr = getMappingWithKey(map.getMapKey());
1062
1063 if (not mapPtr) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001064 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
1065 map.toString(),
1066 igd->toString(),
1067 map.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001068 return;
1069 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001070 if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001071 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
1072 mapPtr->toString(),
1073 igd->toString(),
1074 mapPtr->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001075 return;
1076 }
1077
1078 mapPtr->setRenewalTime(map.getRenewalTime());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001079 mapPtr->setExpiryTime(map.getExpiryTime());
1080 scheduleMappingsRenewal();
1081 if (igd->getProtocol() == NatProtocolType::PUPNP)
1082 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001083}
Adrien Béraud612b55b2023-05-29 10:42:04 -04001084
1085void
1086UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
1087{
Adrien Béraud370257c2023-08-15 20:53:09 -04001088 if (not map or not map->isValid()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001089 // Silently ignore if the mapping is invalid
1090 return;
1091 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001092 auto protocol = protocolList_.at(map->getIgd()->getProtocol());
1093 protocol->requestMappingRemove(*map);
1094}
1095
1096void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001097UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1098{
1099 if (not mapRes.isValid())
1100 return;
1101
Adrien Béraud612b55b2023-05-29 10:42:04 -04001102 auto map = getMappingWithKey(mapRes.getMapKey());
1103 // Notify the listener.
1104 if (map and map->getNotifyCallback())
1105 map->getNotifyCallback()(map);
1106}
1107
Amna0d215232024-08-27 17:57:45 -04001108void
1109UPnPContext::onIgdDiscoveryStarted()
1110{
1111 std::lock_guard lock(igdDiscoveryMutex_);
1112 igdDiscoveryInProgress_ = true;
1113 if (logger_) logger_->debug("IGD Discovery started");
1114 igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
1115 igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
1116 if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
1117 _endIgdDiscovery();
1118 }
1119 });
1120}
1121
1122void
1123UPnPContext::_endIgdDiscovery()
1124{
1125 std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
1126 igdDiscoveryInProgress_ = false;
1127 if (logger_) logger_->debug("IGD Discovery ended");
1128 if (isReady()) {
1129 return;
1130 }
Amna18515ae2024-09-11 16:39:11 -04001131
1132 // Use a temporary list to avoid holding the lock while processing the mapping list.
1133 // This is necessary because updateMappingState calls a user-defined callback, which
1134 // in turn could end up calling a function (such as reserveMapping) that would also
1135 // attempt to lock mappingMutex_.
1136 std::list<Mapping::sharedPtr_t> toRemoveList;
1137 {
1138 std::lock_guard lock(mappingMutex_);
1139 PortType types[2] {PortType::TCP, PortType::UDP};
1140 for (auto type : types) {
1141 const auto& mappingList = getMappingList(type);
1142 for (const auto& [_, map] : mappingList) {
1143 toRemoveList.emplace_back(map);
1144 }
Amna0d215232024-08-27 17:57:45 -04001145 }
1146 }
Amna18515ae2024-09-11 16:39:11 -04001147 // We reached the end of the IGD discovery period without finding a valid IGD,
1148 // so all mapping requests are considered to have failed.
1149 for (auto const& map : toRemoveList) {
1150 updateMappingState(map, MappingState::FAILED);
1151 }
Amna0d215232024-08-27 17:57:45 -04001152}
1153
1154void
1155UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
1156{
1157 std::lock_guard lock(igdDiscoveryMutex_);
1158 igdDiscoveryTimeout_ = timeout;
1159}
1160
Adrien Béraud612b55b2023-05-29 10:42:04 -04001161Mapping::sharedPtr_t
1162UPnPContext::registerMapping(Mapping& map)
1163{
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001164 Mapping::sharedPtr_t mapPtr;
1165
Adrien Béraud612b55b2023-05-29 10:42:04 -04001166 if (map.getExternalPort() == 0) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001167 auto port = getAvailablePortNumber(map.getType());
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001168 if (port == 0) {
1169 if (logger_) logger_->error("Unable to register mapping: no available port number");
1170 return mapPtr;
1171 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001172 map.setExternalPort(port);
1173 map.setInternalPort(port);
1174 }
1175
1176 // Newly added mapping must be in pending state by default.
1177 map.setState(MappingState::PENDING);
1178
Adrien Béraud612b55b2023-05-29 10:42:04 -04001179 {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001180 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001181 auto& mappingList = getMappingList(map.getType());
1182
1183 auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
1184 if (not ret.second) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001185 if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001186 return {};
1187 }
1188 mapPtr = ret.first->second;
1189 assert(mapPtr);
1190 }
1191
Adrien Béraud612b55b2023-05-29 10:42:04 -04001192 if (not isReady()) {
Amna0d215232024-08-27 17:57:45 -04001193 // There is no valid IGD available
1194 std::lock_guard lock(igdDiscoveryMutex_);
1195 // IGD discovery is in progress, the mapping request will be made once an IGD becomes available
1196 if (igdDiscoveryInProgress_) {
François-Simon Fauteux-Chapleau872e82f2024-09-09 15:24:05 -04001197 if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
Amna0d215232024-08-27 17:57:45 -04001198 map.toString());
1199 } else {
1200 // it's not in the IGD discovery phase, the mapping request will fail
1201 if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1202 map.toString());
1203 updateMappingState(mapPtr, MappingState::FAILED);
1204 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001205 } else {
Amna0d215232024-08-27 17:57:45 -04001206 // There is a valid IGD available, request the mapping.
Adrien Béraud612b55b2023-05-29 10:42:04 -04001207 requestMapping(mapPtr);
1208 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001209 return mapPtr;
1210}
1211
Adrien Béraud612b55b2023-05-29 10:42:04 -04001212void
Amna18515ae2024-09-11 16:39:11 -04001213UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map)
Adrien Béraud612b55b2023-05-29 10:42:04 -04001214{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001215 if (not map) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001216 return;
1217 }
1218
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001219 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001220 auto& mappingList = getMappingList(map->getType());
1221
1222 if (mappingList.erase(map->getMapKey()) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001223 if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001224 } else {
1225 // The mapping may already be un-registered. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001226 if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001227 map->toString(),
1228 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001229 }
1230}
1231
1232std::map<Mapping::key_t, Mapping::sharedPtr_t>&
1233UPnPContext::getMappingList(PortType type)
1234{
1235 unsigned typeIdx = type == PortType::TCP ? 0 : 1;
1236 return mappingList_[typeIdx];
1237}
1238
1239Mapping::sharedPtr_t
1240UPnPContext::getMappingWithKey(Mapping::key_t key)
1241{
Adrien Béraud024c46f2024-03-02 23:53:18 -05001242 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001243 auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
1244 auto it = mappingList.find(key);
1245 if (it == mappingList.end())
1246 return nullptr;
1247 return it->second;
1248}
1249
1250void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001251UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
1252{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001253 auto igd = mapRes.getIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001254 auto const& map = getMappingWithKey(mapRes.getMapKey());
1255 if (not map) {
1256 // We may receive a response for a removed request. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001257 if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
1258 mapRes.toString(),
1259 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001260 return;
1261 }
1262
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001263 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001264
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001265 if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001266 map->toString(),
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001267 igd->toString());
1268
1269 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001270}
1271
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001272void
1273UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
1274{
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001275 assert(map);
1276
1277 // Ignore if the state did not change.
1278 if (newState == map->getState()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001279 return;
1280 }
1281
1282 // Update the state.
1283 map->setState(newState);
1284
1285 // Notify the listener if set.
1286 if (notify and map->getNotifyCallback())
1287 map->getNotifyCallback()(map);
Amna18515ae2024-09-11 16:39:11 -04001288
1289 if (newState == MappingState::FAILED)
1290 handleFailedMapping(map);
1291
1292}
1293
1294void
1295UPnPContext::handleFailedMapping(const Mapping::sharedPtr_t& map)
1296{
1297 if (!map->getAutoUpdate()) {
1298 // If the mapping is not set to auto-update, it will be unregistered.
1299 unregisterMapping(map);
1300 return;
1301 }
1302
1303 if (isReady()) {
1304 Mapping newMapping(map->getType());
1305 newMapping.enableAutoUpdate(true);
1306 newMapping.setNotifyCallback(map->getNotifyCallback());
1307 reserveMapping(newMapping);
1308 if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
1309 map->toString());
1310
1311 // TODO: figure out if this line is actually necessary
1312 // (See https://review.jami.net/c/jami-daemon/+/16940)
1313 map->setNotifyCallback(nullptr);
1314 } else {
1315 // If there is no valid IGD, the mapping is marked as pending
1316 // and will be requested when an IGD becomes available.
1317 updateMappingState(map, MappingState::PENDING, false);
1318 if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
1319 map->toString());
1320 }
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001321}
1322
Adrien Béraud612b55b2023-05-29 10:42:04 -04001323} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001324} // namespace dhtnet