blob: e832c65e2ffe7bf3930f016d0407373fe922cd9c [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)
Adrien Béraud612b55b2023-05-29 10:42:04 -040054{
Adrien Beraud3bd61c92023-08-17 16:57:37 -040055 if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -040056
57 // Set port ranges
58 portRange_.emplace(PortType::TCP, std::make_pair(UPNP_TCP_PORT_MIN, UPNP_TCP_PORT_MAX));
59 portRange_.emplace(PortType::UDP, std::make_pair(UPNP_UDP_PORT_MIN, UPNP_UDP_PORT_MAX));
60
Adrien Béraud370257c2023-08-15 20:53:09 -040061 ctx->post([this] { init(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -040062}
63
Adrien Béraudb04fbd72023-08-17 19:56:11 -040064std::shared_ptr<asio::io_context>
65UPnPContext::createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger) {
66 if (ctx) {
67 return ctx;
68 } else {
69 if (logger) logger->debug("UPnPContext: starting dedicated io_context thread");
70 auto ioCtx = std::make_shared<asio::io_context>();
71 ioContextRunner_ = std::make_unique<std::thread>([ioCtx, l=logger]() {
72 try {
73 auto work = asio::make_work_guard(*ioCtx);
74 ioCtx->run();
75 } catch (const std::exception& ex) {
76 if (l) l->error("Unexpected io_context thread exception: {}", ex.what());
77 }
78 });
79 return ioCtx;
80 }
81}
82
Adrien Béraud612b55b2023-05-29 10:42:04 -040083void
84UPnPContext::shutdown(std::condition_variable& cv)
85{
Adrien Beraud3bd61c92023-08-17 16:57:37 -040086 if (logger_) logger_->debug("Shutdown UPnPContext instance [{}]", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -040087
88 stopUpnp(true);
89
90 for (auto const& [_, proto] : protocolList_) {
91 proto->terminate();
92 }
93
Adrien Béraud024c46f2024-03-02 23:53:18 -050094 std::lock_guard lock(mappingMutex_);
Adrien Béraud91fd4b62023-08-29 20:50:01 -040095 mappingList_->clear();
Adrien Béraud91fd4b62023-08-29 20:50:01 -040096 controllerList_.clear();
97 protocolList_.clear();
98 shutdownComplete_ = true;
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -040099 if (shutdownTimedOut_) {
100 // If we timed out in shutdown(), then calling notify_one is not necessary,
101 // and doing so anyway can cause bugs, see:
102 // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/28
103 return;
104 }
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400105 cv.notify_one();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400106}
107
108void
109UPnPContext::shutdown()
110{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500111 std::unique_lock lk(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400112 std::condition_variable cv;
113
Adrien Béraud370257c2023-08-15 20:53:09 -0400114 ctx->post([&, this] { shutdown(cv); });
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500115
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400116 if (logger_) logger_->debug("Waiting for shutdown ...");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400117
118 if (cv.wait_for(lk, std::chrono::seconds(30), [this] { return shutdownComplete_; })) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400119 if (logger_) logger_->debug("Shutdown completed");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400120 } else {
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400121 if (logger_) logger_->error("Shutdown timed out");
122 shutdownTimedOut_ = true;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400123 }
François-Simon Fauteux-Chapleau648907c2024-02-06 15:16:48 -0500124 // NOTE: It's important to unlock mappingMutex_ here, otherwise we get a
125 // deadlock when the call to cv.wait_for() above times out before we return
126 // from proto->terminate() in shutdown(cv).
127 lk.unlock();
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400128
129 if (ioContextRunner_) {
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500130 if (logger_) logger_->debug("UPnPContext: stopping io_context thread {}", fmt::ptr(this));
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400131 ctx->stop();
132 ioContextRunner_->join();
133 ioContextRunner_.reset();
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500134 if (logger_) logger_->debug("UPnPContext: stopping io_context thread - finished {}", fmt::ptr(this));
Adrien Béraud91fd4b62023-08-29 20:50:01 -0400135 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400136}
137
138UPnPContext::~UPnPContext()
139{
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400140 if (logger_) logger_->debug("UPnPContext instance [{}] destroyed", fmt::ptr(this));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400141}
142
143void
144UPnPContext::init()
145{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400146#if HAVE_LIBNATPMP
Adrien Béraud370257c2023-08-15 20:53:09 -0400147 auto natPmp = std::make_shared<NatPmp>(ctx, logger_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400148 natPmp->setObserver(this);
149 protocolList_.emplace(NatProtocolType::NAT_PMP, std::move(natPmp));
150#endif
151
152#if HAVE_LIBUPNP
Adrien Béraud370257c2023-08-15 20:53:09 -0400153 auto pupnp = std::make_shared<PUPnP>(ctx, logger_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400154 pupnp->setObserver(this);
155 protocolList_.emplace(NatProtocolType::PUPNP, std::move(pupnp));
156#endif
157}
158
159void
160UPnPContext::startUpnp()
161{
162 assert(not controllerList_.empty());
163
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400164 if (logger_) logger_->debug("Starting UPNP context");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400165
166 // Request a new IGD search.
167 for (auto const& [_, protocol] : protocolList_) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400168 ctx->dispatch([p=protocol] { p->searchForIgd(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400169 }
170
171 started_ = true;
172}
173
174void
175UPnPContext::stopUpnp(bool forceRelease)
176{
François-Simon Fauteux-Chapleau808db4f2024-04-19 11:39:47 -0400177 if (logger_) logger_->debug("Stopping UPnP context");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400178
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400179 connectivityChangedTimer_.cancel();
180 mappingRenewalTimer_.cancel();
181 renewalSchedulingTimer_.cancel();
182 syncTimer_.cancel();
183 syncRequested_ = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400184
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400185 // Clear all current mappings
186
187 // Use a temporary list to avoid processing the mappings while holding the lock.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400188 std::list<Mapping::sharedPtr_t> toRemoveList;
189 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500190 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400191
192 PortType types[2] {PortType::TCP, PortType::UDP};
193 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400194 const auto& mappingList = getMappingList(type);
195 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400196 toRemoveList.emplace_back(map);
197 }
198 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400199 // Invalidate the current IGD.
200 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400201 }
202 for (auto const& map : toRemoveList) {
203 requestRemoveMapping(map);
204
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400205 if (map->getAutoUpdate() && !forceRelease) {
206 // Set the mapping's state to PENDING so that it
207 // gets recreated if we restart UPnP later.
208 map->setState(MappingState::PENDING);
209 } else {
210 unregisterMapping(map, true);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400211 }
212 }
213
214 // Clear all current IGDs.
215 for (auto const& [_, protocol] : protocolList_) {
Adrien Béraud370257c2023-08-15 20:53:09 -0400216 ctx->dispatch([p=protocol]{ p->clearIgds(); });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400217 }
218
219 started_ = false;
220}
221
222uint16_t
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400223UPnPContext::generateRandomPort(PortType type)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400224{
225 auto minPort = type == PortType::TCP ? UPNP_TCP_PORT_MIN : UPNP_UDP_PORT_MIN;
226 auto maxPort = type == PortType::TCP ? UPNP_TCP_PORT_MAX : UPNP_UDP_PORT_MAX;
227
Adrien Béraud612b55b2023-05-29 10:42:04 -0400228 // Seed the generator.
229 static std::mt19937 gen(dht::crypto::getSeededRandomEngine());
230 // Define the range.
231 std::uniform_int_distribution<uint16_t> dist(minPort, maxPort);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400232 return dist(gen);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400233}
234
235void
236UPnPContext::connectivityChanged()
237{
Adrien Béraudc36965c2023-08-17 21:50:27 -0400238 // Debounce the connectivity change notification.
239 connectivityChangedTimer_.expires_after(std::chrono::milliseconds(50));
240 connectivityChangedTimer_.async_wait(std::bind(&UPnPContext::_connectivityChanged, this, std::placeholders::_1));
241}
242
243void
244UPnPContext::_connectivityChanged(const asio::error_code& ec)
245{
246 if (ec == asio::error::operation_aborted)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400247 return;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400248
249 auto hostAddr = ip_utils::getLocalAddr(AF_INET);
250
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400251 if (logger_) logger_->debug("Connectivity change check: host address {}", hostAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400252
253 auto restartUpnp = false;
254
255 // On reception of "connectivity change" notification, the UPNP search
256 // will be restarted if either there is no valid IGD, or the IGD address
257 // changed.
258
259 if (not isReady()) {
260 restartUpnp = true;
261 } else {
262 // Check if the host address changed.
263 for (auto const& [_, protocol] : protocolList_) {
264 if (protocol->isReady() and hostAddr != protocol->getHostAddress()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400265 if (logger_) logger_->warn("Host address changed from {} to {}",
266 protocol->getHostAddress().toString(),
267 hostAddr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400268 protocol->clearIgds();
269 restartUpnp = true;
270 break;
271 }
272 }
273 }
274
275 // We have at least one valid IGD and the host address did
276 // not change, so no need to restart.
277 if (not restartUpnp) {
278 return;
279 }
280
281 // No registered controller. A new search will be performed when
282 // a controller is registered.
283 if (controllerList_.empty())
284 return;
285
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400286 if (logger_) logger_->debug("Connectivity changed. Clear the IGDs and restart");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400287
288 stopUpnp();
289 startUpnp();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400290}
291
292void
293UPnPContext::setPublicAddress(const IpAddr& addr)
294{
295 if (not addr)
296 return;
297
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400298 std::lock_guard lock(publicAddressMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400299 if (knownPublicAddress_ != addr) {
300 knownPublicAddress_ = std::move(addr);
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400301 if (logger_) logger_->debug("Setting the known public address to {}", addr.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400302 }
303}
304
305bool
306UPnPContext::isReady() const
307{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500308 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400309 return currentIgd_ ? true : false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400310}
311
312IpAddr
313UPnPContext::getExternalIP() const
314{
Adrien Béraud024c46f2024-03-02 23:53:18 -0500315 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400316 if (currentIgd_)
317 return currentIgd_->getPublicIp();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400318 return {};
319}
320
321Mapping::sharedPtr_t
322UPnPContext::reserveMapping(Mapping& requestedMap)
323{
324 auto desiredPort = requestedMap.getExternalPort();
325
326 if (desiredPort == 0) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400327 if (logger_) logger_->debug("Desired port is not set, will provide the first available port for [{}]",
328 requestedMap.getTypeStr());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400329 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400330 if (logger_) logger_->debug("Try to find mapping for port {:d} [{}]", desiredPort, requestedMap.getTypeStr());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400331 }
332
333 Mapping::sharedPtr_t mapRes;
334
335 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500336 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400337 const auto& mappingList = getMappingList(requestedMap.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400338
339 // We try to provide a mapping in "OPEN" state. If not found,
340 // we provide any available mapping. In this case, it's up to
341 // the caller to use it or not.
342 for (auto const& [_, map] : mappingList) {
343 // If the desired port is null, we pick the first available port.
344 if (map->isValid() and (desiredPort == 0 or map->getExternalPort() == desiredPort)
345 and map->isAvailable()) {
346 // Considere the first available mapping regardless of its
347 // state. A mapping with OPEN state will be used if found.
348 if (not mapRes)
349 mapRes = map;
350
351 if (map->getState() == MappingState::OPEN) {
352 // Found an "OPEN" mapping. We are done.
353 mapRes = map;
354 break;
355 }
356 }
357 }
358 }
359
360 // Create a mapping if none was available.
361 if (not mapRes) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400362 mapRes = registerMapping(requestedMap);
363 }
364
365 if (mapRes) {
366 // Make the mapping unavailable
367 mapRes->setAvailable(false);
368 // Copy attributes.
369 mapRes->setNotifyCallback(requestedMap.getNotifyCallback());
370 mapRes->enableAutoUpdate(requestedMap.getAutoUpdate());
371 // Notify the listener.
372 if (auto cb = mapRes->getNotifyCallback())
373 cb(mapRes);
374 }
375
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400376 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400377
378 return mapRes;
379}
380
381void
382UPnPContext::releaseMapping(const Mapping& map)
383{
Adrien Béraudc36965c2023-08-17 21:50:27 -0400384 ctx->dispatch([this, map] {
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500385 if (shutdownComplete_)
386 return;
Adrien Béraudc36965c2023-08-17 21:50:27 -0400387 auto mapPtr = getMappingWithKey(map.getMapKey());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400388
Adrien Béraudc36965c2023-08-17 21:50:27 -0400389 if (not mapPtr) {
390 // Might happen if the mapping failed or was never granted.
391 if (logger_) logger_->debug("Mapping {} does not exist or was already removed", map.toString());
392 return;
393 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400394
Adrien Béraudc36965c2023-08-17 21:50:27 -0400395 if (mapPtr->isAvailable()) {
396 if (logger_) logger_->warn("Trying to release an unused mapping {}", mapPtr->toString());
397 return;
398 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400399
Adrien Béraudc36965c2023-08-17 21:50:27 -0400400 // Remove it.
401 requestRemoveMapping(mapPtr);
Amna6f458612024-08-26 13:49:27 -0400402 unregisterMapping(mapPtr, true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400403 enforceAvailableMappingsLimits();
Adrien Béraudc36965c2023-08-17 21:50:27 -0400404 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400405}
406
407void
408UPnPContext::registerController(void* controller)
409{
410 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500411 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400412 if (shutdownComplete_) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400413 if (logger_) logger_->warn("UPnPContext already shut down");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400414 return;
415 }
Adrien Béraudc36965c2023-08-17 21:50:27 -0400416 auto ret = controllerList_.emplace(controller);
417 if (not ret.second) {
418 if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller));
419 return;
420 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400421 }
422
Adrien Berauda8731ac2023-08-17 12:19:39 -0400423 if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400424 if (not started_)
425 startUpnp();
426}
427
428void
429UPnPContext::unregisterController(void* controller)
430{
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500431 if (shutdownComplete_)
432 return;
Adrien Béraud024c46f2024-03-02 23:53:18 -0500433 std::unique_lock lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400434 if (controllerList_.erase(controller) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400435 if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400436 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400437 if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400438 }
439
440 if (controllerList_.empty()) {
Adrien Béraudc36965c2023-08-17 21:50:27 -0400441 lock.unlock();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400442 stopUpnp();
443 }
444}
445
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400446std::vector<IGDInfo>
447UPnPContext::getIgdsInfo() const
448{
449 std::vector<IGDInfo> igdInfoList;
450
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400451 for (const auto& [_, protocol] : protocolList_) {
452 for (auto& igd : protocol->getIgdList()) {
453 IGDInfo info;
454 info.uid = igd->getUID();
455 info.localIp = igd->getLocalIp();
456 info.publicIp = igd->getPublicIp();
457 info.mappingInfoList = protocol->getMappingsInfo(igd);
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400458
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400459 igdInfoList.push_back(std::move(info));
460 }
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400461 }
462
463 return igdInfoList;
464}
465
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400466// 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 -0400467uint16_t
468UPnPContext::getAvailablePortNumber(PortType type)
469{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400470 // Only return an available random port. No actual
Adrien Béraud612b55b2023-05-29 10:42:04 -0400471 // reservation is made here.
472
Adrien Béraud024c46f2024-03-02 23:53:18 -0500473 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400474 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400475 int tryCount = 0;
476 while (tryCount++ < MAX_REQUEST_RETRIES) {
477 uint16_t port = generateRandomPort(type);
478 Mapping map(type, port, port);
479 if (mappingList.find(map.getMapKey()) == mappingList.end())
480 return port;
481 }
482
483 // Very unlikely to get here.
Adrien Berauda8731ac2023-08-17 12:19:39 -0400484 if (logger_) logger_->error("Could not find an available port after %i trials", MAX_REQUEST_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400485 return 0;
486}
487
488void
489UPnPContext::requestMapping(const Mapping::sharedPtr_t& map)
490{
491 assert(map);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400492 auto const& igd = getCurrentIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400493 // We must have at least a valid IGD pointer if we get here.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400494 // Note that this method is called only if there was a valid IGD, but
495 // because the processing is asynchronous, there may no longer
496 // be one by the time this code executes.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400497 if (not igd) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400498 if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available",
499 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400500 return;
501 }
502
503 map->setIgd(igd);
504
Adrien Berauda8731ac2023-08-17 12:19:39 -0400505 if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]",
506 map->toString(),
507 igd->getProtocolName(),
508 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400509
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400510 updateMappingState(map, MappingState::IN_PROGRESS);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400511
512 auto const& protocol = protocolList_.at(igd->getProtocol());
513 protocol->requestMappingAdd(*map);
514}
515
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400516void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400517UPnPContext::provisionNewMappings(PortType type, int portCount)
518{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400519 if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400520
Adrien Béraud612b55b2023-05-29 10:42:04 -0400521 while (portCount > 0) {
522 auto port = getAvailablePortNumber(type);
523 if (port > 0) {
524 // Found an available port number
525 portCount--;
526 Mapping map(type, port, port, true);
527 registerMapping(map);
528 } else {
529 // Very unlikely to get here!
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400530 if (logger_) logger_->error("Cannot provision port: no available port number");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400531 }
532 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400533}
534
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400535void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400536UPnPContext::deleteUnneededMappings(PortType type, int portCount)
537{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400538 if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400539
Adrien Béraud024c46f2024-03-02 23:53:18 -0500540 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400541 auto& mappingList = getMappingList(type);
542
543 for (auto it = mappingList.begin(); it != mappingList.end();) {
544 auto map = it->second;
545 assert(map);
546
547 if (not map->isAvailable()) {
548 it++;
549 continue;
550 }
551
552 if (map->getState() == MappingState::OPEN and portCount > 0) {
553 // Close portCount mappings in "OPEN" state.
554 requestRemoveMapping(map);
Adrien Béraud370257c2023-08-15 20:53:09 -0400555 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400556 portCount--;
557 } else if (map->getState() != MappingState::OPEN) {
558 // If this methods is called, it means there are more open
559 // mappings than required. So, all mappings in a state other
560 // than "OPEN" state (typically in in-progress state) will
561 // be deleted as well.
Adrien Béraud370257c2023-08-15 20:53:09 -0400562 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400563 } else {
564 it++;
565 }
566 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400567}
568
569void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400570UPnPContext::updateCurrentIgd()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400571{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400572 std::lock_guard lock(mappingMutex_);
573 if (currentIgd_ and currentIgd_->isValid()) {
574 if (logger_) logger_->debug("Current IGD is still valid, no need to update");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400575 return;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400576 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400577
578 // Reset and search for the best IGD.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400579 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400580
581 for (auto const& [_, protocol] : protocolList_) {
582 if (protocol->isReady()) {
583 auto igdList = protocol->getIgdList();
584 assert(not igdList.empty());
585 auto const& igd = igdList.front();
586 if (not igd->isValid())
587 continue;
588
589 // Prefer NAT-PMP over PUPNP.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400590 if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400591 continue;
592
593 // Update.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400594 currentIgd_ = igd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400595 }
596 }
597
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400598 if (currentIgd_ and currentIgd_->isValid()) {
599 if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ",
600 currentIgd_->getProtocolName(),
601 currentIgd_->getUID(),
602 currentIgd_->toString());
603 } else {
604 if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400605 }
606}
607
608std::shared_ptr<IGD>
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400609UPnPContext::getCurrentIgd() const
Adrien Béraud612b55b2023-05-29 10:42:04 -0400610{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400611 return currentIgd_;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400612}
613
614void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400615UPnPContext::enforceAvailableMappingsLimits()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400616{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400617 for (auto type : {PortType::TCP, PortType::UDP}) {
618 int pendingCount = 0;
619 int inProgressCount = 0;
620 int openCount = 0;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400621 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500622 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400623 const auto& mappingList = getMappingList(type);
624 for (const auto& [_, mapping] : mappingList) {
625 if (!mapping->isAvailable())
Adrien Béraud612b55b2023-05-29 10:42:04 -0400626 continue;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400627 switch (mapping->getState()) {
628 case MappingState::PENDING:
629 pendingCount++;
630 break;
631 case MappingState::IN_PROGRESS:
632 inProgressCount++;
633 break;
634 case MappingState::OPEN:
635 openCount++;
636 break;
637 default:
638 break;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400639 }
640 }
641 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400642 int availableCount = openCount + pendingCount + inProgressCount;
643 if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)",
644 Mapping::getTypeStr(type),
645 availableCount,
646 openCount,
647 pendingCount,
648 inProgressCount);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400649
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400650 int minAvailableMappings = getMinAvailableMappings(type);
651 if (minAvailableMappings > availableCount) {
652 provisionNewMappings(type, minAvailableMappings - availableCount);
653 continue;
654 }
655
656 int maxAvailableMappings = getMaxAvailableMappings(type);
657 if (openCount > maxAvailableMappings) {
658 deleteUnneededMappings(type, openCount - maxAvailableMappings);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400659 }
660 }
661}
662
663void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400664UPnPContext::renewMappings()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400665{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400666 if (!started_)
667 return;
668
669 const auto& igd = getCurrentIgd();
670 if (!igd) {
671 if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available");
672 return;
673 }
674
675 auto now = sys_clock::now();
676 auto nextRenewalTime = sys_clock::time_point::max();
677
678 std::vector<Mapping::sharedPtr_t> toRenew;
679 int toRenewLaterCount = 0;
680
681 for (auto type : {PortType::TCP, PortType::UDP}) {
682 std::lock_guard lock(mappingMutex_);
683 const auto& mappingList = getMappingList(type);
684 for (const auto& [_, map] : mappingList) {
685 if (not map->isValid())
686 continue;
687 if (map->getState() != MappingState::OPEN)
688 continue;
689
690 auto mapRenewalTime = map->getRenewalTime();
691 if (now >= mapRenewalTime) {
692 toRenew.emplace_back(map);
693 } else if (mapRenewalTime < sys_clock::time_point::max()) {
694 toRenewLaterCount++;
695 if (mapRenewalTime < nextRenewalTime)
696 nextRenewalTime = map->getRenewalTime();
697 }
698
699 }
700 }
701
702 if (!toRenew.empty()) {
703 if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size());
704 }
705 for (const auto& map : toRenew) {
706 const auto& protocol = protocolList_.at(map->getIgd()->getProtocol());
707 protocol->requestMappingRenew(*map);
708 }
709 if (toRenewLaterCount > 0) {
710 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
711 if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})",
712 toRenewLaterCount,
713 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
714 mappingRenewalTimer_.expires_at(nextRenewalTime);
715 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
716 if (ec != asio::error::operation_aborted)
717 renewMappings();
718 });
719 }
720}
721
722void
723UPnPContext::scheduleMappingsRenewal()
724{
725 // Debounce the scheduling function so that it doesn't get called multiple
726 // times when several mappings are added or renewed in rapid succession.
727 renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500));
728 renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) {
729 if (ec != asio::error::operation_aborted)
730 _scheduleMappingsRenewal();
731 });
732}
733
734void
735UPnPContext::_scheduleMappingsRenewal()
736{
737 if (!started_)
738 return;
739
740 sys_clock::time_point nextRenewalTime = sys_clock::time_point::max();
741 for (auto type : {PortType::TCP, PortType::UDP}) {
742 std::lock_guard lock(mappingMutex_);
743 const auto& mappingList = getMappingList(type);
744 for (const auto& [_, map] : mappingList) {
745 if (map->getState() == MappingState::OPEN &&
746 map->getRenewalTime() < nextRenewalTime)
747 nextRenewalTime = map->getRenewalTime();
748 }
749 }
750 if (nextRenewalTime == sys_clock::time_point::max())
751 return;
752
753 // Add a small delay so that we don't have to call renewMappings multiple
754 // times in a row (and iterate over the whole list of mappings each time)
755 // when multiple mappings have almost the same renewal time.
756 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
757 if (nextRenewalTime == mappingRenewalTimer_.expiry())
758 return;
759
760 if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}",
761 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
762 mappingRenewalTimer_.expires_at(nextRenewalTime);
763 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
764 if (ec != asio::error::operation_aborted)
765 renewMappings();
766 });
767}
768
769void
770UPnPContext::syncLocalMappingListWithIgd()
771{
772 std::lock_guard lock(syncMutex_);
773 if (syncRequested_)
774 return;
775
776 syncRequested_ = true;
777 syncTimer_.expires_after(std::chrono::minutes(5));
778 syncTimer_.async_wait([this](asio::error_code const& ec) {
779 if (ec != asio::error::operation_aborted)
780 _syncLocalMappingListWithIgd();
781 });
782}
783
784void
785UPnPContext::_syncLocalMappingListWithIgd()
786{
787 {
788 std::lock_guard lock(syncMutex_);
789 syncRequested_ = false;
790 }
791 const auto& igd = getCurrentIgd();
792 if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) {
793 return;
794 }
795 auto pupnp = protocolList_.at(NatProtocolType::PUPNP);
796 if (!pupnp->isReady())
797 return;
798
799 if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]",
800 igd->toString());
801 auto remoteMapList = pupnp->getMappingsListByDescr(igd,
802 Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
803 bool requestsInProgress = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400804 // Use a temporary list to avoid processing mappings while holding the lock.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400805 std::list<Mapping::sharedPtr_t> toRemoveFromLocalList;
806 for (auto type: {PortType::TCP, PortType::UDP}) {
807 std::lock_guard lock(mappingMutex_);
808 for (auto& [_, map] : getMappingList(type)) {
809 if (map->getProtocol() != NatProtocolType::PUPNP) {
810 continue;
811 }
812 switch (map->getState()) {
813 case MappingState::PENDING:
814 case MappingState::IN_PROGRESS:
815 requestsInProgress = true;
816 break;
817 case MappingState::OPEN: {
818 auto it = remoteMapList.find(map->getMapKey());
819 if (it == remoteMapList.end()) {
820 if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
821 "remote list. Removing from local list.",
822 map->toString(),
823 igd->toString());
824 toRemoveFromLocalList.emplace_back(map);
825 } else {
826 auto oldExpiryTime = map->getExpiryTime();
827 auto newExpiryTime = it->second.getExpiryTime();
828 // The value of newExpiryTime is based on the mapping's "lease duration" that we got from
829 // the IGD, which is supposed to be (according to the UPnP specification) the number of
830 // seconds remaining before the mapping expires. In practice, the duration values returned
831 // by some routers are only precise to the hour (i.e. they're always multiples of 3600). This
832 // means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case.
833 // In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to
834 // push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin.
835 if (newExpiryTime < oldExpiryTime ||
836 newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) {
837 auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2;
838 map->setRenewalTime(newRenewalTime);
839 map->setExpiryTime(newExpiryTime);
840 }
841 }
842 break;
843 }
844 default:
845 break;
846 }
847 }
848 }
849 scheduleMappingsRenewal();
850
851 for (auto const& map : toRemoveFromLocalList) {
852 updateMappingState(map, MappingState::FAILED);
853 unregisterMapping(map);
854 }
855 if (!toRemoveFromLocalList.empty())
856 enforceAvailableMappingsLimits();
857
858 if (requestsInProgress) {
859 // It's unlikely that there will be requests in progress when this function is
860 // called, but if there are, that suggests that we are dealing with a slow
861 // router, so we return early instead of sending additional deletion requests
862 // (which aren't essential and could end up "competing" with higher-priority
863 // creation/renewal requests).
864 return;
865 }
866 // Use a temporary list to avoid processing mappings while holding the lock.
867 std::list<Mapping> toRemoveFromIgd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400868 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500869 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400870
871 for (auto const& [_, map] : remoteMapList) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400872 const auto& mappingList = getMappingList(map.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400873 auto it = mappingList.find(map.getMapKey());
874 if (it == mappingList.end()) {
875 // Not present, request mapping remove.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400876 toRemoveFromIgd.emplace_back(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400877 // Make only few remove requests at once.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400878 if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400879 break;
880 }
881 }
882 }
883
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400884 for (const auto& map : toRemoveFromIgd) {
885 pupnp->requestMappingRemove(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400886 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400887
Adrien Béraud612b55b2023-05-29 10:42:04 -0400888}
889
890void
891UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
892{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400893 // Use temporary list to avoid holding the lock while
894 // processing the mapping list.
895 std::list<Mapping::sharedPtr_t> toRemoveList;
896 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500897 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400898
899 PortType types[2] {PortType::TCP, PortType::UDP};
900 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400901 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400902 for (auto const& [_, map] : mappingList) {
903 if (map->getIgd() == igd)
904 toRemoveList.emplace_back(map);
905 }
906 }
907 }
908
909 for (auto const& map : toRemoveList) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400910 if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
911 map->toString(),
912 igd->toString(),
913 igd->getProtocolName());
Adrien Béraud56740312023-08-23 08:38:28 -0400914 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400915 unregisterMapping(map);
916 }
917}
918
919void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400920UPnPContext::processPendingRequests()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400921{
922 // This list holds the mappings to be requested. This is
923 // needed to avoid performing the requests while holding
924 // the lock.
925 std::list<Mapping::sharedPtr_t> requestsList;
926
927 // Populate the list of requests to perform.
928 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500929 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400930 PortType typeArray[2] {PortType::TCP, PortType::UDP};
931
932 for (auto type : typeArray) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400933 const auto& mappingList = getMappingList(type);
934 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400935 if (map->getState() == MappingState::PENDING) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400936 if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
937 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400938 requestsList.emplace_back(map);
939 }
940 }
941 }
942 }
943
944 // Process the pending requests.
945 for (auto const& map : requestsList) {
946 requestMapping(map);
947 }
948}
949
950void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400951UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
952{
953 assert(igd);
954
Adrien Béraud612b55b2023-05-29 10:42:04 -0400955 char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
956 : event == UpnpIgdEvent::REMOVED ? "REMOVED"
957 : "INVALID";
958
959 auto const& igdLocalAddr = igd->getLocalIp();
960 auto protocolName = igd->getProtocolName();
961
Adrien Berauda8731ac2023-08-17 12:19:39 -0400962 if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
963 igd->getUID(),
964 igd->toString(),
965 protocolName,
966 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400967
Adrien Béraud612b55b2023-05-29 10:42:04 -0400968 if (not igdLocalAddr) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400969 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
970 protocolName,
971 igd->getUID(),
972 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400973 return;
974 }
975
976 if (not igd->getPublicIp()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400977 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
978 protocolName,
979 igd->getUID(),
980 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400981 return;
982 }
983
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400984 {
985 std::lock_guard lock(publicAddressMutex_);
986 if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
987 if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
988 " The mapped addresses might not be reachable",
989 protocolName,
990 igd->getPublicIp().toString(),
991 knownPublicAddress_.toString());
992 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400993 }
994
Adrien Béraud612b55b2023-05-29 10:42:04 -0400995 if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400996 if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
997 igd->getUID(),
998 igd->toString(),
999 protocolName,
1000 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001001
1002 pruneMappingsWithInvalidIgds(igd);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001003 }
1004
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001005 updateCurrentIgd();
1006 if (isReady()) {
1007 processPendingRequests();
1008 enforceAvailableMappingsLimits();
1009 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001010}
1011
1012void
1013UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1014{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001015 // Check if we have a pending request for this response.
1016 auto map = getMappingWithKey(mapRes.getMapKey());
1017 if (not map) {
1018 // We may receive a response for a canceled request. Just ignore it.
Adrien Berauda8731ac2023-08-17 12:19:39 -04001019 if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
1020 mapRes.toString(),
1021 igd->toString(),
1022 mapRes.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001023 return;
1024 }
1025
1026 // The mapping request is new and successful. Update.
1027 map->setIgd(igd);
1028 map->setInternalAddress(mapRes.getInternalAddress());
1029 map->setExternalPort(mapRes.getExternalPort());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001030 map->setRenewalTime(mapRes.getRenewalTime());
1031 map->setExpiryTime(mapRes.getExpiryTime());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001032 // Update the state and report to the owner.
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001033 updateMappingState(map, MappingState::OPEN);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001034 scheduleMappingsRenewal();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001035
Adrien Berauda8731ac2023-08-17 12:19:39 -04001036 if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
1037 map->toString(),
1038 igd->toString(),
1039 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001040
1041 // Call setValid() to reset the errors counter. We need
1042 // to reset the counter on each successful response.
1043 igd->setValid(true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001044 if (igd->getProtocol() == NatProtocolType::PUPNP)
1045 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001046}
1047
Adrien Béraud612b55b2023-05-29 10:42:04 -04001048void
1049UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
1050{
1051 auto mapPtr = getMappingWithKey(map.getMapKey());
1052
1053 if (not mapPtr) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001054 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
1055 map.toString(),
1056 igd->toString(),
1057 map.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001058 return;
1059 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001060 if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001061 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
1062 mapPtr->toString(),
1063 igd->toString(),
1064 mapPtr->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001065 return;
1066 }
1067
1068 mapPtr->setRenewalTime(map.getRenewalTime());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001069 mapPtr->setExpiryTime(map.getExpiryTime());
1070 scheduleMappingsRenewal();
1071 if (igd->getProtocol() == NatProtocolType::PUPNP)
1072 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001073}
Adrien Béraud612b55b2023-05-29 10:42:04 -04001074
1075void
1076UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
1077{
Adrien Béraud370257c2023-08-15 20:53:09 -04001078 if (not map or not map->isValid()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001079 // Silently ignore if the mapping is invalid
1080 return;
1081 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001082 auto protocol = protocolList_.at(map->getIgd()->getProtocol());
1083 protocol->requestMappingRemove(*map);
1084}
1085
1086void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001087UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1088{
1089 if (not mapRes.isValid())
1090 return;
1091
Adrien Béraud612b55b2023-05-29 10:42:04 -04001092 auto map = getMappingWithKey(mapRes.getMapKey());
1093 // Notify the listener.
1094 if (map and map->getNotifyCallback())
1095 map->getNotifyCallback()(map);
1096}
1097
1098Mapping::sharedPtr_t
1099UPnPContext::registerMapping(Mapping& map)
1100{
1101 if (map.getExternalPort() == 0) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001102 // JAMI_DBG("Port number not set. Will set a random port number");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001103 auto port = getAvailablePortNumber(map.getType());
1104 map.setExternalPort(port);
1105 map.setInternalPort(port);
1106 }
1107
1108 // Newly added mapping must be in pending state by default.
1109 map.setState(MappingState::PENDING);
1110
1111 Mapping::sharedPtr_t mapPtr;
1112
1113 {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001114 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001115 auto& mappingList = getMappingList(map.getType());
1116
1117 auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
1118 if (not ret.second) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001119 if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001120 return {};
1121 }
1122 mapPtr = ret.first->second;
1123 assert(mapPtr);
1124 }
1125
1126 // No available IGD. The pending mapping requests will be processed
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001127 // when an IGD becomes available
Adrien Béraud612b55b2023-05-29 10:42:04 -04001128 if (not isReady()) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001129 if (logger_) logger_->warn("No IGD available. Mapping will be requested when an IGD becomes available");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001130 } else {
1131 requestMapping(mapPtr);
1132 }
1133
1134 return mapPtr;
1135}
1136
Adrien Béraud612b55b2023-05-29 10:42:04 -04001137void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001138UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate)
Adrien Béraud612b55b2023-05-29 10:42:04 -04001139{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001140 if (not map) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001141 return;
1142 }
1143
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001144 if (map->getAutoUpdate() && !ignoreAutoUpdate) {
1145 if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
1146 map->toString());
1147
1148 Mapping newMapping(map->getType());
1149 newMapping.enableAutoUpdate(true);
1150 newMapping.setNotifyCallback(map->getNotifyCallback());
1151 reserveMapping(newMapping);
1152
1153 // TODO: figure out if this line is actually necessary
1154 // (See https://review.jami.net/c/jami-daemon/+/16940)
1155 map->setNotifyCallback(nullptr);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001156 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001157 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001158 auto& mappingList = getMappingList(map->getType());
1159
1160 if (mappingList.erase(map->getMapKey()) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001161 if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001162 } else {
1163 // The mapping may already be un-registered. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001164 if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001165 map->toString(),
1166 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001167 }
1168}
1169
1170std::map<Mapping::key_t, Mapping::sharedPtr_t>&
1171UPnPContext::getMappingList(PortType type)
1172{
1173 unsigned typeIdx = type == PortType::TCP ? 0 : 1;
1174 return mappingList_[typeIdx];
1175}
1176
1177Mapping::sharedPtr_t
1178UPnPContext::getMappingWithKey(Mapping::key_t key)
1179{
Adrien Béraud024c46f2024-03-02 23:53:18 -05001180 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001181 auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
1182 auto it = mappingList.find(key);
1183 if (it == mappingList.end())
1184 return nullptr;
1185 return it->second;
1186}
1187
1188void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001189UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
1190{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001191 auto igd = mapRes.getIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001192 auto const& map = getMappingWithKey(mapRes.getMapKey());
1193 if (not map) {
1194 // We may receive a response for a removed request. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001195 if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
1196 mapRes.toString(),
1197 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001198 return;
1199 }
1200
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001201 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001202 unregisterMapping(map);
1203
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001204 if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001205 map->toString(),
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001206 igd->toString());
1207
1208 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001209}
1210
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001211void
1212UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
1213{
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001214 assert(map);
1215
1216 // Ignore if the state did not change.
1217 if (newState == map->getState()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001218 return;
1219 }
1220
1221 // Update the state.
1222 map->setState(newState);
1223
1224 // Notify the listener if set.
1225 if (notify and map->getNotifyCallback())
1226 map->getNotifyCallback()(map);
1227}
1228
Adrien Béraud612b55b2023-05-29 10:42:04 -04001229} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001230} // namespace dhtnet