blob: 42de87f54b05b1fec20668ad67323c3484446543 [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
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400381// TODO: double-check what the expected behavior is when the mapping has auto-update enabled.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400382void
383UPnPContext::releaseMapping(const Mapping& map)
384{
Adrien Béraudc36965c2023-08-17 21:50:27 -0400385 ctx->dispatch([this, map] {
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500386 if (shutdownComplete_)
387 return;
Adrien Béraudc36965c2023-08-17 21:50:27 -0400388 auto mapPtr = getMappingWithKey(map.getMapKey());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400389
Adrien Béraudc36965c2023-08-17 21:50:27 -0400390 if (not mapPtr) {
391 // Might happen if the mapping failed or was never granted.
392 if (logger_) logger_->debug("Mapping {} does not exist or was already removed", map.toString());
393 return;
394 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400395
Adrien Béraudc36965c2023-08-17 21:50:27 -0400396 if (mapPtr->isAvailable()) {
397 if (logger_) logger_->warn("Trying to release an unused mapping {}", mapPtr->toString());
398 return;
399 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400400
Adrien Béraudc36965c2023-08-17 21:50:27 -0400401 // Remove it.
402 requestRemoveMapping(mapPtr);
403 unregisterMapping(mapPtr);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400404 enforceAvailableMappingsLimits();
Adrien Béraudc36965c2023-08-17 21:50:27 -0400405 });
Adrien Béraud612b55b2023-05-29 10:42:04 -0400406}
407
408void
409UPnPContext::registerController(void* controller)
410{
411 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500412 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400413 if (shutdownComplete_) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400414 if (logger_) logger_->warn("UPnPContext already shut down");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400415 return;
416 }
Adrien Béraudc36965c2023-08-17 21:50:27 -0400417 auto ret = controllerList_.emplace(controller);
418 if (not ret.second) {
419 if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller));
420 return;
421 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400422 }
423
Adrien Berauda8731ac2023-08-17 12:19:39 -0400424 if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400425 if (not started_)
426 startUpnp();
427}
428
429void
430UPnPContext::unregisterController(void* controller)
431{
Sébastien Blin91cda3c2024-01-10 16:21:18 -0500432 if (shutdownComplete_)
433 return;
Adrien Béraud024c46f2024-03-02 23:53:18 -0500434 std::unique_lock lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400435 if (controllerList_.erase(controller) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400436 if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400437 } else {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400438 if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400439 }
440
441 if (controllerList_.empty()) {
Adrien Béraudc36965c2023-08-17 21:50:27 -0400442 lock.unlock();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400443 stopUpnp();
444 }
445}
446
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400447std::vector<IGDInfo>
448UPnPContext::getIgdsInfo() const
449{
450 std::vector<IGDInfo> igdInfoList;
451
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400452 for (const auto& [_, protocol] : protocolList_) {
453 for (auto& igd : protocol->getIgdList()) {
454 IGDInfo info;
455 info.uid = igd->getUID();
456 info.localIp = igd->getLocalIp();
457 info.publicIp = igd->getPublicIp();
458 info.mappingInfoList = protocol->getMappingsInfo(igd);
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400459
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400460 igdInfoList.push_back(std::move(info));
461 }
François-Simon Fauteux-Chapleau826f0ba2024-05-29 15:22:21 -0400462 }
463
464 return igdInfoList;
465}
466
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400467// 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 -0400468uint16_t
469UPnPContext::getAvailablePortNumber(PortType type)
470{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400471 // Only return an available random port. No actual
Adrien Béraud612b55b2023-05-29 10:42:04 -0400472 // reservation is made here.
473
Adrien Béraud024c46f2024-03-02 23:53:18 -0500474 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400475 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400476 int tryCount = 0;
477 while (tryCount++ < MAX_REQUEST_RETRIES) {
478 uint16_t port = generateRandomPort(type);
479 Mapping map(type, port, port);
480 if (mappingList.find(map.getMapKey()) == mappingList.end())
481 return port;
482 }
483
484 // Very unlikely to get here.
Adrien Berauda8731ac2023-08-17 12:19:39 -0400485 if (logger_) logger_->error("Could not find an available port after %i trials", MAX_REQUEST_RETRIES);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400486 return 0;
487}
488
489void
490UPnPContext::requestMapping(const Mapping::sharedPtr_t& map)
491{
492 assert(map);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400493 auto const& igd = getCurrentIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400494 // We must have at least a valid IGD pointer if we get here.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400495 // Note that this method is called only if there was a valid IGD, but
496 // because the processing is asynchronous, there may no longer
497 // be one by the time this code executes.
Adrien Béraud612b55b2023-05-29 10:42:04 -0400498 if (not igd) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400499 if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available",
500 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400501 return;
502 }
503
504 map->setIgd(igd);
505
Adrien Berauda8731ac2023-08-17 12:19:39 -0400506 if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]",
507 map->toString(),
508 igd->getProtocolName(),
509 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400510
Adrien Beraud3bd61c92023-08-17 16:57:37 -0400511 updateMappingState(map, MappingState::IN_PROGRESS);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400512
513 auto const& protocol = protocolList_.at(igd->getProtocol());
514 protocol->requestMappingAdd(*map);
515}
516
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400517void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400518UPnPContext::provisionNewMappings(PortType type, int portCount)
519{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400520 if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400521
Adrien Béraud612b55b2023-05-29 10:42:04 -0400522 while (portCount > 0) {
523 auto port = getAvailablePortNumber(type);
524 if (port > 0) {
525 // Found an available port number
526 portCount--;
527 Mapping map(type, port, port, true);
528 registerMapping(map);
529 } else {
530 // Very unlikely to get here!
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400531 if (logger_) logger_->error("Cannot provision port: no available port number");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400532 }
533 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400534}
535
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400536void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400537UPnPContext::deleteUnneededMappings(PortType type, int portCount)
538{
Adrien Berauda8731ac2023-08-17 12:19:39 -0400539 if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400540
Adrien Béraud024c46f2024-03-02 23:53:18 -0500541 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400542 auto& mappingList = getMappingList(type);
543
544 for (auto it = mappingList.begin(); it != mappingList.end();) {
545 auto map = it->second;
546 assert(map);
547
548 if (not map->isAvailable()) {
549 it++;
550 continue;
551 }
552
553 if (map->getState() == MappingState::OPEN and portCount > 0) {
554 // Close portCount mappings in "OPEN" state.
555 requestRemoveMapping(map);
Adrien Béraud370257c2023-08-15 20:53:09 -0400556 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400557 portCount--;
558 } else if (map->getState() != MappingState::OPEN) {
559 // If this methods is called, it means there are more open
560 // mappings than required. So, all mappings in a state other
561 // than "OPEN" state (typically in in-progress state) will
562 // be deleted as well.
Adrien Béraud370257c2023-08-15 20:53:09 -0400563 it = mappingList.erase(it);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400564 } else {
565 it++;
566 }
567 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400568}
569
570void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400571UPnPContext::updateCurrentIgd()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400572{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400573 std::lock_guard lock(mappingMutex_);
574 if (currentIgd_ and currentIgd_->isValid()) {
575 if (logger_) logger_->debug("Current IGD is still valid, no need to update");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400576 return;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400577 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400578
579 // Reset and search for the best IGD.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400580 currentIgd_.reset();
Adrien Béraud612b55b2023-05-29 10:42:04 -0400581
582 for (auto const& [_, protocol] : protocolList_) {
583 if (protocol->isReady()) {
584 auto igdList = protocol->getIgdList();
585 assert(not igdList.empty());
586 auto const& igd = igdList.front();
587 if (not igd->isValid())
588 continue;
589
590 // Prefer NAT-PMP over PUPNP.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400591 if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400592 continue;
593
594 // Update.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400595 currentIgd_ = igd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400596 }
597 }
598
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400599 if (currentIgd_ and currentIgd_->isValid()) {
600 if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ",
601 currentIgd_->getProtocolName(),
602 currentIgd_->getUID(),
603 currentIgd_->toString());
604 } else {
605 if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found");
Adrien Béraud612b55b2023-05-29 10:42:04 -0400606 }
607}
608
609std::shared_ptr<IGD>
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400610UPnPContext::getCurrentIgd() const
Adrien Béraud612b55b2023-05-29 10:42:04 -0400611{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400612 return currentIgd_;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400613}
614
615void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400616UPnPContext::enforceAvailableMappingsLimits()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400617{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400618 for (auto type : {PortType::TCP, PortType::UDP}) {
619 int pendingCount = 0;
620 int inProgressCount = 0;
621 int openCount = 0;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400622 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500623 std::lock_guard lock(mappingMutex_);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400624 const auto& mappingList = getMappingList(type);
625 for (const auto& [_, mapping] : mappingList) {
626 if (!mapping->isAvailable())
Adrien Béraud612b55b2023-05-29 10:42:04 -0400627 continue;
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400628 switch (mapping->getState()) {
629 case MappingState::PENDING:
630 pendingCount++;
631 break;
632 case MappingState::IN_PROGRESS:
633 inProgressCount++;
634 break;
635 case MappingState::OPEN:
636 openCount++;
637 break;
638 default:
639 break;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400640 }
641 }
642 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400643 int availableCount = openCount + pendingCount + inProgressCount;
644 if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)",
645 Mapping::getTypeStr(type),
646 availableCount,
647 openCount,
648 pendingCount,
649 inProgressCount);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400650
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400651 int minAvailableMappings = getMinAvailableMappings(type);
652 if (minAvailableMappings > availableCount) {
653 provisionNewMappings(type, minAvailableMappings - availableCount);
654 continue;
655 }
656
657 int maxAvailableMappings = getMaxAvailableMappings(type);
658 if (openCount > maxAvailableMappings) {
659 deleteUnneededMappings(type, openCount - maxAvailableMappings);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400660 }
661 }
662}
663
664void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400665UPnPContext::renewMappings()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400666{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400667 if (!started_)
668 return;
669
670 const auto& igd = getCurrentIgd();
671 if (!igd) {
672 if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available");
673 return;
674 }
675
676 auto now = sys_clock::now();
677 auto nextRenewalTime = sys_clock::time_point::max();
678
679 std::vector<Mapping::sharedPtr_t> toRenew;
680 int toRenewLaterCount = 0;
681
682 for (auto type : {PortType::TCP, PortType::UDP}) {
683 std::lock_guard lock(mappingMutex_);
684 const auto& mappingList = getMappingList(type);
685 for (const auto& [_, map] : mappingList) {
686 if (not map->isValid())
687 continue;
688 if (map->getState() != MappingState::OPEN)
689 continue;
690
691 auto mapRenewalTime = map->getRenewalTime();
692 if (now >= mapRenewalTime) {
693 toRenew.emplace_back(map);
694 } else if (mapRenewalTime < sys_clock::time_point::max()) {
695 toRenewLaterCount++;
696 if (mapRenewalTime < nextRenewalTime)
697 nextRenewalTime = map->getRenewalTime();
698 }
699
700 }
701 }
702
703 if (!toRenew.empty()) {
704 if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size());
705 }
706 for (const auto& map : toRenew) {
707 const auto& protocol = protocolList_.at(map->getIgd()->getProtocol());
708 protocol->requestMappingRenew(*map);
709 }
710 if (toRenewLaterCount > 0) {
711 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
712 if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})",
713 toRenewLaterCount,
714 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
715 mappingRenewalTimer_.expires_at(nextRenewalTime);
716 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
717 if (ec != asio::error::operation_aborted)
718 renewMappings();
719 });
720 }
721}
722
723void
724UPnPContext::scheduleMappingsRenewal()
725{
726 // Debounce the scheduling function so that it doesn't get called multiple
727 // times when several mappings are added or renewed in rapid succession.
728 renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500));
729 renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) {
730 if (ec != asio::error::operation_aborted)
731 _scheduleMappingsRenewal();
732 });
733}
734
735void
736UPnPContext::_scheduleMappingsRenewal()
737{
738 if (!started_)
739 return;
740
741 sys_clock::time_point nextRenewalTime = sys_clock::time_point::max();
742 for (auto type : {PortType::TCP, PortType::UDP}) {
743 std::lock_guard lock(mappingMutex_);
744 const auto& mappingList = getMappingList(type);
745 for (const auto& [_, map] : mappingList) {
746 if (map->getState() == MappingState::OPEN &&
747 map->getRenewalTime() < nextRenewalTime)
748 nextRenewalTime = map->getRenewalTime();
749 }
750 }
751 if (nextRenewalTime == sys_clock::time_point::max())
752 return;
753
754 // Add a small delay so that we don't have to call renewMappings multiple
755 // times in a row (and iterate over the whole list of mappings each time)
756 // when multiple mappings have almost the same renewal time.
757 nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
758 if (nextRenewalTime == mappingRenewalTimer_.expiry())
759 return;
760
761 if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}",
762 fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
763 mappingRenewalTimer_.expires_at(nextRenewalTime);
764 mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
765 if (ec != asio::error::operation_aborted)
766 renewMappings();
767 });
768}
769
770void
771UPnPContext::syncLocalMappingListWithIgd()
772{
773 std::lock_guard lock(syncMutex_);
774 if (syncRequested_)
775 return;
776
777 syncRequested_ = true;
778 syncTimer_.expires_after(std::chrono::minutes(5));
779 syncTimer_.async_wait([this](asio::error_code const& ec) {
780 if (ec != asio::error::operation_aborted)
781 _syncLocalMappingListWithIgd();
782 });
783}
784
785void
786UPnPContext::_syncLocalMappingListWithIgd()
787{
788 {
789 std::lock_guard lock(syncMutex_);
790 syncRequested_ = false;
791 }
792 const auto& igd = getCurrentIgd();
793 if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) {
794 return;
795 }
796 auto pupnp = protocolList_.at(NatProtocolType::PUPNP);
797 if (!pupnp->isReady())
798 return;
799
800 if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]",
801 igd->toString());
802 auto remoteMapList = pupnp->getMappingsListByDescr(igd,
803 Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
804 bool requestsInProgress = false;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400805 // Use a temporary list to avoid processing mappings while holding the lock.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400806 std::list<Mapping::sharedPtr_t> toRemoveFromLocalList;
807 for (auto type: {PortType::TCP, PortType::UDP}) {
808 std::lock_guard lock(mappingMutex_);
809 for (auto& [_, map] : getMappingList(type)) {
810 if (map->getProtocol() != NatProtocolType::PUPNP) {
811 continue;
812 }
813 switch (map->getState()) {
814 case MappingState::PENDING:
815 case MappingState::IN_PROGRESS:
816 requestsInProgress = true;
817 break;
818 case MappingState::OPEN: {
819 auto it = remoteMapList.find(map->getMapKey());
820 if (it == remoteMapList.end()) {
821 if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
822 "remote list. Removing from local list.",
823 map->toString(),
824 igd->toString());
825 toRemoveFromLocalList.emplace_back(map);
826 } else {
827 auto oldExpiryTime = map->getExpiryTime();
828 auto newExpiryTime = it->second.getExpiryTime();
829 // The value of newExpiryTime is based on the mapping's "lease duration" that we got from
830 // the IGD, which is supposed to be (according to the UPnP specification) the number of
831 // seconds remaining before the mapping expires. In practice, the duration values returned
832 // by some routers are only precise to the hour (i.e. they're always multiples of 3600). This
833 // means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case.
834 // In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to
835 // push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin.
836 if (newExpiryTime < oldExpiryTime ||
837 newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) {
838 auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2;
839 map->setRenewalTime(newRenewalTime);
840 map->setExpiryTime(newExpiryTime);
841 }
842 }
843 break;
844 }
845 default:
846 break;
847 }
848 }
849 }
850 scheduleMappingsRenewal();
851
852 for (auto const& map : toRemoveFromLocalList) {
853 updateMappingState(map, MappingState::FAILED);
854 unregisterMapping(map);
855 }
856 if (!toRemoveFromLocalList.empty())
857 enforceAvailableMappingsLimits();
858
859 if (requestsInProgress) {
860 // It's unlikely that there will be requests in progress when this function is
861 // called, but if there are, that suggests that we are dealing with a slow
862 // router, so we return early instead of sending additional deletion requests
863 // (which aren't essential and could end up "competing" with higher-priority
864 // creation/renewal requests).
865 return;
866 }
867 // Use a temporary list to avoid processing mappings while holding the lock.
868 std::list<Mapping> toRemoveFromIgd;
Adrien Béraud612b55b2023-05-29 10:42:04 -0400869 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500870 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400871
872 for (auto const& [_, map] : remoteMapList) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400873 const auto& mappingList = getMappingList(map.getType());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400874 auto it = mappingList.find(map.getMapKey());
875 if (it == mappingList.end()) {
876 // Not present, request mapping remove.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400877 toRemoveFromIgd.emplace_back(std::move(map));
Adrien Béraud612b55b2023-05-29 10:42:04 -0400878 // Make only few remove requests at once.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400879 if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
Adrien Béraud612b55b2023-05-29 10:42:04 -0400880 break;
881 }
882 }
883 }
884
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400885 for (const auto& map : toRemoveFromIgd) {
886 pupnp->requestMappingRemove(map);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400887 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400888
Adrien Béraud612b55b2023-05-29 10:42:04 -0400889}
890
891void
892UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
893{
Adrien Béraud612b55b2023-05-29 10:42:04 -0400894 // Use temporary list to avoid holding the lock while
895 // processing the mapping list.
896 std::list<Mapping::sharedPtr_t> toRemoveList;
897 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500898 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400899
900 PortType types[2] {PortType::TCP, PortType::UDP};
901 for (auto& type : types) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400902 const auto& mappingList = getMappingList(type);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400903 for (auto const& [_, map] : mappingList) {
904 if (map->getIgd() == igd)
905 toRemoveList.emplace_back(map);
906 }
907 }
908 }
909
910 for (auto const& map : toRemoveList) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400911 if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
912 map->toString(),
913 igd->toString(),
914 igd->getProtocolName());
Adrien Béraud56740312023-08-23 08:38:28 -0400915 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400916 unregisterMapping(map);
917 }
918}
919
920void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400921UPnPContext::processPendingRequests()
Adrien Béraud612b55b2023-05-29 10:42:04 -0400922{
923 // This list holds the mappings to be requested. This is
924 // needed to avoid performing the requests while holding
925 // the lock.
926 std::list<Mapping::sharedPtr_t> requestsList;
927
928 // Populate the list of requests to perform.
929 {
Adrien Béraud024c46f2024-03-02 23:53:18 -0500930 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400931 PortType typeArray[2] {PortType::TCP, PortType::UDP};
932
933 for (auto type : typeArray) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400934 const auto& mappingList = getMappingList(type);
935 for (const auto& [_, map] : mappingList) {
Adrien Béraud612b55b2023-05-29 10:42:04 -0400936 if (map->getState() == MappingState::PENDING) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400937 if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
938 map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400939 requestsList.emplace_back(map);
940 }
941 }
942 }
943 }
944
945 // Process the pending requests.
946 for (auto const& map : requestsList) {
947 requestMapping(map);
948 }
949}
950
951void
Adrien Béraud612b55b2023-05-29 10:42:04 -0400952UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
953{
954 assert(igd);
955
Adrien Béraud612b55b2023-05-29 10:42:04 -0400956 char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
957 : event == UpnpIgdEvent::REMOVED ? "REMOVED"
958 : "INVALID";
959
960 auto const& igdLocalAddr = igd->getLocalIp();
961 auto protocolName = igd->getProtocolName();
962
Adrien Berauda8731ac2023-08-17 12:19:39 -0400963 if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
964 igd->getUID(),
965 igd->toString(),
966 protocolName,
967 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -0400968
Adrien Béraud612b55b2023-05-29 10:42:04 -0400969 if (not igdLocalAddr) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400970 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
971 protocolName,
972 igd->getUID(),
973 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400974 return;
975 }
976
977 if (not igd->getPublicIp()) {
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400978 if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
979 protocolName,
980 igd->getUID(),
981 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -0400982 return;
983 }
984
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -0400985 {
986 std::lock_guard lock(publicAddressMutex_);
987 if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
988 if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
989 " The mapped addresses might not be reachable",
990 protocolName,
991 igd->getPublicIp().toString(),
992 knownPublicAddress_.toString());
993 }
Adrien Béraud612b55b2023-05-29 10:42:04 -0400994 }
995
Adrien Béraud612b55b2023-05-29 10:42:04 -0400996 if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
Adrien Berauda8731ac2023-08-17 12:19:39 -0400997 if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
998 igd->getUID(),
999 igd->toString(),
1000 protocolName,
1001 IgdState);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001002
1003 pruneMappingsWithInvalidIgds(igd);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001004 }
1005
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001006 updateCurrentIgd();
1007 if (isReady()) {
1008 processPendingRequests();
1009 enforceAvailableMappingsLimits();
1010 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001011}
1012
1013void
1014UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1015{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001016 // Check if we have a pending request for this response.
1017 auto map = getMappingWithKey(mapRes.getMapKey());
1018 if (not map) {
1019 // We may receive a response for a canceled request. Just ignore it.
Adrien Berauda8731ac2023-08-17 12:19:39 -04001020 if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
1021 mapRes.toString(),
1022 igd->toString(),
1023 mapRes.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001024 return;
1025 }
1026
1027 // The mapping request is new and successful. Update.
1028 map->setIgd(igd);
1029 map->setInternalAddress(mapRes.getInternalAddress());
1030 map->setExternalPort(mapRes.getExternalPort());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001031 map->setRenewalTime(mapRes.getRenewalTime());
1032 map->setExpiryTime(mapRes.getExpiryTime());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001033 // Update the state and report to the owner.
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001034 updateMappingState(map, MappingState::OPEN);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001035 scheduleMappingsRenewal();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001036
Adrien Berauda8731ac2023-08-17 12:19:39 -04001037 if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
1038 map->toString(),
1039 igd->toString(),
1040 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001041
1042 // Call setValid() to reset the errors counter. We need
1043 // to reset the counter on each successful response.
1044 igd->setValid(true);
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001045 if (igd->getProtocol() == NatProtocolType::PUPNP)
1046 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001047}
1048
Adrien Béraud612b55b2023-05-29 10:42:04 -04001049void
1050UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
1051{
1052 auto mapPtr = getMappingWithKey(map.getMapKey());
1053
1054 if (not mapPtr) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001055 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
1056 map.toString(),
1057 igd->toString(),
1058 map.getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001059 return;
1060 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001061 if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001062 if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
1063 mapPtr->toString(),
1064 igd->toString(),
1065 mapPtr->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001066 return;
1067 }
1068
1069 mapPtr->setRenewalTime(map.getRenewalTime());
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001070 mapPtr->setExpiryTime(map.getExpiryTime());
1071 scheduleMappingsRenewal();
1072 if (igd->getProtocol() == NatProtocolType::PUPNP)
1073 syncLocalMappingListWithIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001074}
Adrien Béraud612b55b2023-05-29 10:42:04 -04001075
1076void
1077UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
1078{
Adrien Béraud370257c2023-08-15 20:53:09 -04001079 if (not map or not map->isValid()) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001080 // Silently ignore if the mapping is invalid
1081 return;
1082 }
Adrien Béraud612b55b2023-05-29 10:42:04 -04001083 auto protocol = protocolList_.at(map->getIgd()->getProtocol());
1084 protocol->requestMappingRemove(*map);
1085}
1086
1087void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001088UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
1089{
1090 if (not mapRes.isValid())
1091 return;
1092
Adrien Béraud612b55b2023-05-29 10:42:04 -04001093 auto map = getMappingWithKey(mapRes.getMapKey());
1094 // Notify the listener.
1095 if (map and map->getNotifyCallback())
1096 map->getNotifyCallback()(map);
1097}
1098
1099Mapping::sharedPtr_t
1100UPnPContext::registerMapping(Mapping& map)
1101{
1102 if (map.getExternalPort() == 0) {
Morteza Namvar5f639522023-07-04 17:08:58 -04001103 // JAMI_DBG("Port number not set. Will set a random port number");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001104 auto port = getAvailablePortNumber(map.getType());
1105 map.setExternalPort(port);
1106 map.setInternalPort(port);
1107 }
1108
1109 // Newly added mapping must be in pending state by default.
1110 map.setState(MappingState::PENDING);
1111
1112 Mapping::sharedPtr_t mapPtr;
1113
1114 {
Adrien Béraud024c46f2024-03-02 23:53:18 -05001115 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001116 auto& mappingList = getMappingList(map.getType());
1117
1118 auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
1119 if (not ret.second) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001120 if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001121 return {};
1122 }
1123 mapPtr = ret.first->second;
1124 assert(mapPtr);
1125 }
1126
1127 // No available IGD. The pending mapping requests will be processed
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001128 // when an IGD becomes available
Adrien Béraud612b55b2023-05-29 10:42:04 -04001129 if (not isReady()) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001130 if (logger_) logger_->warn("No IGD available. Mapping will be requested when an IGD becomes available");
Adrien Béraud612b55b2023-05-29 10:42:04 -04001131 } else {
1132 requestMapping(mapPtr);
1133 }
1134
1135 return mapPtr;
1136}
1137
Adrien Béraud612b55b2023-05-29 10:42:04 -04001138void
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001139UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate)
Adrien Béraud612b55b2023-05-29 10:42:04 -04001140{
Adrien Béraud612b55b2023-05-29 10:42:04 -04001141 if (not map) {
Adrien Béraud612b55b2023-05-29 10:42:04 -04001142 return;
1143 }
1144
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001145 if (map->getAutoUpdate() && !ignoreAutoUpdate) {
1146 if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
1147 map->toString());
1148
1149 Mapping newMapping(map->getType());
1150 newMapping.enableAutoUpdate(true);
1151 newMapping.setNotifyCallback(map->getNotifyCallback());
1152 reserveMapping(newMapping);
1153
1154 // TODO: figure out if this line is actually necessary
1155 // (See https://review.jami.net/c/jami-daemon/+/16940)
1156 map->setNotifyCallback(nullptr);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001157 }
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001158 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001159 auto& mappingList = getMappingList(map->getType());
1160
1161 if (mappingList.erase(map->getMapKey()) == 1) {
Adrien Berauda8731ac2023-08-17 12:19:39 -04001162 if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001163 } else {
1164 // The mapping may already be un-registered. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001165 if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001166 map->toString(),
1167 map->getProtocolName());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001168 }
1169}
1170
1171std::map<Mapping::key_t, Mapping::sharedPtr_t>&
1172UPnPContext::getMappingList(PortType type)
1173{
1174 unsigned typeIdx = type == PortType::TCP ? 0 : 1;
1175 return mappingList_[typeIdx];
1176}
1177
1178Mapping::sharedPtr_t
1179UPnPContext::getMappingWithKey(Mapping::key_t key)
1180{
Adrien Béraud024c46f2024-03-02 23:53:18 -05001181 std::lock_guard lock(mappingMutex_);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001182 auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
1183 auto it = mappingList.find(key);
1184 if (it == mappingList.end())
1185 return nullptr;
1186 return it->second;
1187}
1188
1189void
Adrien Béraud612b55b2023-05-29 10:42:04 -04001190UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
1191{
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001192 auto igd = mapRes.getIgd();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001193 auto const& map = getMappingWithKey(mapRes.getMapKey());
1194 if (not map) {
1195 // We may receive a response for a removed request. Just ignore it.
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001196 if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
1197 mapRes.toString(),
1198 igd->toString());
Adrien Béraud612b55b2023-05-29 10:42:04 -04001199 return;
1200 }
1201
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001202 updateMappingState(map, MappingState::FAILED);
Adrien Béraud612b55b2023-05-29 10:42:04 -04001203 unregisterMapping(map);
1204
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001205 if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
Adrien Berauda8731ac2023-08-17 12:19:39 -04001206 map->toString(),
François-Simon Fauteux-Chapleaufd29c1d2024-05-30 16:48:26 -04001207 igd->toString());
1208
1209 enforceAvailableMappingsLimits();
Adrien Béraud612b55b2023-05-29 10:42:04 -04001210}
1211
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001212void
1213UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
1214{
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001215 assert(map);
1216
1217 // Ignore if the state did not change.
1218 if (newState == map->getState()) {
Adrien Beraud3bd61c92023-08-17 16:57:37 -04001219 return;
1220 }
1221
1222 // Update the state.
1223 map->setState(newState);
1224
1225 // Notify the listener if set.
1226 if (notify and map->getNotifyCallback())
1227 map->getNotifyCallback()(map);
1228}
1229
Adrien Béraud612b55b2023-05-29 10:42:04 -04001230} // namespace upnp
Sébastien Blin464bdff2023-07-19 08:02:53 -04001231} // namespace dhtnet