| /* |
| * Copyright (C) 2004-2023 Savoir-faire Linux Inc. |
| * |
| * This program is free software: you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation, either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program. If not, see <https://www.gnu.org/licenses/>. |
| */ |
| #include "upnp/upnp_context.h" |
| #include "protocol/upnp_protocol.h" |
| |
| #if HAVE_LIBNATPMP |
| #include "protocol/natpmp/nat_pmp.h" |
| #endif |
| #if HAVE_LIBUPNP |
| #include "protocol/pupnp/pupnp.h" |
| #endif |
| #include <asio.hpp> |
| #include <asio/steady_timer.hpp> |
| #if __has_include(<fmt/std.h>) |
| #include <fmt/std.h> |
| #else |
| #include <fmt/ostream.h> |
| #endif |
| #include <fmt/chrono.h> |
| |
| namespace dhtnet { |
| namespace upnp { |
| |
| constexpr static auto MAPPING_RENEWAL_THROTTLING_DELAY = std::chrono::seconds(10); |
| constexpr static int MAX_REQUEST_RETRIES = 20; |
| constexpr static int MAX_REQUEST_REMOVE_COUNT = 10; // TODO: increase? |
| |
| constexpr static uint16_t UPNP_TCP_PORT_MIN {10000}; |
| constexpr static uint16_t UPNP_TCP_PORT_MAX {UPNP_TCP_PORT_MIN + 5000}; |
| constexpr static uint16_t UPNP_UDP_PORT_MIN {20000}; |
| constexpr static uint16_t UPNP_UDP_PORT_MAX {UPNP_UDP_PORT_MIN + 5000}; |
| |
| UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, const std::shared_ptr<dht::log::Logger>& logger) |
| : ctx(createIoContext(ioContext, logger)) |
| , logger_(logger) |
| , mappingRenewalTimer_(*ctx) |
| , renewalSchedulingTimer_(*ctx) |
| , syncTimer_(*ctx) |
| , connectivityChangedTimer_(*ctx) |
| { |
| if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this)); |
| |
| // Set port ranges |
| portRange_.emplace(PortType::TCP, std::make_pair(UPNP_TCP_PORT_MIN, UPNP_TCP_PORT_MAX)); |
| portRange_.emplace(PortType::UDP, std::make_pair(UPNP_UDP_PORT_MIN, UPNP_UDP_PORT_MAX)); |
| |
| ctx->post([this] { init(); }); |
| } |
| |
| std::shared_ptr<asio::io_context> |
| UPnPContext::createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger) { |
| if (ctx) { |
| return ctx; |
| } else { |
| if (logger) logger->debug("UPnPContext: starting dedicated io_context thread"); |
| auto ioCtx = std::make_shared<asio::io_context>(); |
| ioContextRunner_ = std::make_unique<std::thread>([ioCtx, l=logger]() { |
| try { |
| auto work = asio::make_work_guard(*ioCtx); |
| ioCtx->run(); |
| } catch (const std::exception& ex) { |
| if (l) l->error("Unexpected io_context thread exception: {}", ex.what()); |
| } |
| }); |
| return ioCtx; |
| } |
| } |
| |
| void |
| UPnPContext::shutdown(std::condition_variable& cv) |
| { |
| if (logger_) logger_->debug("Shutdown UPnPContext instance [{}]", fmt::ptr(this)); |
| |
| stopUpnp(true); |
| |
| for (auto const& [_, proto] : protocolList_) { |
| proto->terminate(); |
| } |
| |
| std::lock_guard lock(mappingMutex_); |
| mappingList_->clear(); |
| controllerList_.clear(); |
| protocolList_.clear(); |
| shutdownComplete_ = true; |
| if (shutdownTimedOut_) { |
| // If we timed out in shutdown(), then calling notify_one is not necessary, |
| // and doing so anyway can cause bugs, see: |
| // https://git.jami.net/savoirfairelinux/dhtnet/-/issues/28 |
| return; |
| } |
| cv.notify_one(); |
| } |
| |
| void |
| UPnPContext::shutdown() |
| { |
| std::unique_lock lk(mappingMutex_); |
| std::condition_variable cv; |
| |
| ctx->post([&, this] { shutdown(cv); }); |
| |
| if (logger_) logger_->debug("Waiting for shutdown ..."); |
| |
| if (cv.wait_for(lk, std::chrono::seconds(30), [this] { return shutdownComplete_; })) { |
| if (logger_) logger_->debug("Shutdown completed"); |
| } else { |
| if (logger_) logger_->error("Shutdown timed out"); |
| shutdownTimedOut_ = true; |
| } |
| // NOTE: It's important to unlock mappingMutex_ here, otherwise we get a |
| // deadlock when the call to cv.wait_for() above times out before we return |
| // from proto->terminate() in shutdown(cv). |
| lk.unlock(); |
| |
| if (ioContextRunner_) { |
| if (logger_) logger_->debug("UPnPContext: stopping io_context thread {}", fmt::ptr(this)); |
| ctx->stop(); |
| ioContextRunner_->join(); |
| ioContextRunner_.reset(); |
| if (logger_) logger_->debug("UPnPContext: stopping io_context thread - finished {}", fmt::ptr(this)); |
| } |
| } |
| |
| UPnPContext::~UPnPContext() |
| { |
| if (logger_) logger_->debug("UPnPContext instance [{}] destroyed", fmt::ptr(this)); |
| } |
| |
| void |
| UPnPContext::init() |
| { |
| #if HAVE_LIBNATPMP |
| auto natPmp = std::make_shared<NatPmp>(ctx, logger_); |
| natPmp->setObserver(this); |
| protocolList_.emplace(NatProtocolType::NAT_PMP, std::move(natPmp)); |
| #endif |
| |
| #if HAVE_LIBUPNP |
| auto pupnp = std::make_shared<PUPnP>(ctx, logger_); |
| pupnp->setObserver(this); |
| protocolList_.emplace(NatProtocolType::PUPNP, std::move(pupnp)); |
| #endif |
| } |
| |
| void |
| UPnPContext::startUpnp() |
| { |
| assert(not controllerList_.empty()); |
| |
| if (logger_) logger_->debug("Starting UPNP context"); |
| |
| // Request a new IGD search. |
| for (auto const& [_, protocol] : protocolList_) { |
| ctx->dispatch([p=protocol] { p->searchForIgd(); }); |
| } |
| |
| started_ = true; |
| } |
| |
| void |
| UPnPContext::stopUpnp(bool forceRelease) |
| { |
| if (logger_) logger_->debug("Stopping UPnP context"); |
| |
| connectivityChangedTimer_.cancel(); |
| mappingRenewalTimer_.cancel(); |
| renewalSchedulingTimer_.cancel(); |
| syncTimer_.cancel(); |
| syncRequested_ = false; |
| |
| // Clear all current mappings |
| |
| // Use a temporary list to avoid processing the mappings while holding the lock. |
| std::list<Mapping::sharedPtr_t> toRemoveList; |
| { |
| std::lock_guard lock(mappingMutex_); |
| |
| PortType types[2] {PortType::TCP, PortType::UDP}; |
| for (auto& type : types) { |
| const auto& mappingList = getMappingList(type); |
| for (const auto& [_, map] : mappingList) { |
| toRemoveList.emplace_back(map); |
| } |
| } |
| // Invalidate the current IGD. |
| currentIgd_.reset(); |
| } |
| for (auto const& map : toRemoveList) { |
| requestRemoveMapping(map); |
| |
| if (map->getAutoUpdate() && !forceRelease) { |
| // Set the mapping's state to PENDING so that it |
| // gets recreated if we restart UPnP later. |
| map->setState(MappingState::PENDING); |
| } else { |
| unregisterMapping(map, true); |
| } |
| } |
| |
| // Clear all current IGDs. |
| for (auto const& [_, protocol] : protocolList_) { |
| ctx->dispatch([p=protocol]{ p->clearIgds(); }); |
| } |
| |
| started_ = false; |
| } |
| |
| uint16_t |
| UPnPContext::generateRandomPort(PortType type) |
| { |
| auto minPort = type == PortType::TCP ? UPNP_TCP_PORT_MIN : UPNP_UDP_PORT_MIN; |
| auto maxPort = type == PortType::TCP ? UPNP_TCP_PORT_MAX : UPNP_UDP_PORT_MAX; |
| |
| // Seed the generator. |
| static std::mt19937 gen(dht::crypto::getSeededRandomEngine()); |
| // Define the range. |
| std::uniform_int_distribution<uint16_t> dist(minPort, maxPort); |
| return dist(gen); |
| } |
| |
| void |
| UPnPContext::connectivityChanged() |
| { |
| // Debounce the connectivity change notification. |
| connectivityChangedTimer_.expires_after(std::chrono::milliseconds(50)); |
| connectivityChangedTimer_.async_wait(std::bind(&UPnPContext::_connectivityChanged, this, std::placeholders::_1)); |
| } |
| |
| void |
| UPnPContext::_connectivityChanged(const asio::error_code& ec) |
| { |
| if (ec == asio::error::operation_aborted) |
| return; |
| |
| auto hostAddr = ip_utils::getLocalAddr(AF_INET); |
| |
| if (logger_) logger_->debug("Connectivity change check: host address {}", hostAddr.toString()); |
| |
| auto restartUpnp = false; |
| |
| // On reception of "connectivity change" notification, the UPNP search |
| // will be restarted if either there is no valid IGD, or the IGD address |
| // changed. |
| |
| if (not isReady()) { |
| restartUpnp = true; |
| } else { |
| // Check if the host address changed. |
| for (auto const& [_, protocol] : protocolList_) { |
| if (protocol->isReady() and hostAddr != protocol->getHostAddress()) { |
| if (logger_) logger_->warn("Host address changed from {} to {}", |
| protocol->getHostAddress().toString(), |
| hostAddr.toString()); |
| protocol->clearIgds(); |
| restartUpnp = true; |
| break; |
| } |
| } |
| } |
| |
| // We have at least one valid IGD and the host address did |
| // not change, so no need to restart. |
| if (not restartUpnp) { |
| return; |
| } |
| |
| // No registered controller. A new search will be performed when |
| // a controller is registered. |
| if (controllerList_.empty()) |
| return; |
| |
| if (logger_) logger_->debug("Connectivity changed. Clear the IGDs and restart"); |
| |
| stopUpnp(); |
| startUpnp(); |
| } |
| |
| void |
| UPnPContext::setPublicAddress(const IpAddr& addr) |
| { |
| if (not addr) |
| return; |
| |
| std::lock_guard lock(publicAddressMutex_); |
| if (knownPublicAddress_ != addr) { |
| knownPublicAddress_ = std::move(addr); |
| if (logger_) logger_->debug("Setting the known public address to {}", addr.toString()); |
| } |
| } |
| |
| bool |
| UPnPContext::isReady() const |
| { |
| std::lock_guard lock(mappingMutex_); |
| return currentIgd_ ? true : false; |
| } |
| |
| IpAddr |
| UPnPContext::getExternalIP() const |
| { |
| std::lock_guard lock(mappingMutex_); |
| if (currentIgd_) |
| return currentIgd_->getPublicIp(); |
| return {}; |
| } |
| |
| Mapping::sharedPtr_t |
| UPnPContext::reserveMapping(Mapping& requestedMap) |
| { |
| auto desiredPort = requestedMap.getExternalPort(); |
| |
| if (desiredPort == 0) { |
| if (logger_) logger_->debug("Desired port is not set, will provide the first available port for [{}]", |
| requestedMap.getTypeStr()); |
| } else { |
| if (logger_) logger_->debug("Try to find mapping for port {:d} [{}]", desiredPort, requestedMap.getTypeStr()); |
| } |
| |
| Mapping::sharedPtr_t mapRes; |
| |
| { |
| std::lock_guard lock(mappingMutex_); |
| const auto& mappingList = getMappingList(requestedMap.getType()); |
| |
| // We try to provide a mapping in "OPEN" state. If not found, |
| // we provide any available mapping. In this case, it's up to |
| // the caller to use it or not. |
| for (auto const& [_, map] : mappingList) { |
| // If the desired port is null, we pick the first available port. |
| if (map->isValid() and (desiredPort == 0 or map->getExternalPort() == desiredPort) |
| and map->isAvailable()) { |
| // Considere the first available mapping regardless of its |
| // state. A mapping with OPEN state will be used if found. |
| if (not mapRes) |
| mapRes = map; |
| |
| if (map->getState() == MappingState::OPEN) { |
| // Found an "OPEN" mapping. We are done. |
| mapRes = map; |
| break; |
| } |
| } |
| } |
| } |
| |
| // Create a mapping if none was available. |
| if (not mapRes) { |
| mapRes = registerMapping(requestedMap); |
| } |
| |
| if (mapRes) { |
| // Make the mapping unavailable |
| mapRes->setAvailable(false); |
| // Copy attributes. |
| mapRes->setNotifyCallback(requestedMap.getNotifyCallback()); |
| mapRes->enableAutoUpdate(requestedMap.getAutoUpdate()); |
| // Notify the listener. |
| if (auto cb = mapRes->getNotifyCallback()) |
| cb(mapRes); |
| } |
| |
| enforceAvailableMappingsLimits(); |
| |
| return mapRes; |
| } |
| |
| void |
| UPnPContext::releaseMapping(const Mapping& map) |
| { |
| ctx->dispatch([this, map] { |
| if (shutdownComplete_) |
| return; |
| auto mapPtr = getMappingWithKey(map.getMapKey()); |
| |
| if (not mapPtr) { |
| // Might happen if the mapping failed or was never granted. |
| if (logger_) logger_->debug("Mapping {} does not exist or was already removed", map.toString()); |
| return; |
| } |
| |
| if (mapPtr->isAvailable()) { |
| if (logger_) logger_->warn("Trying to release an unused mapping {}", mapPtr->toString()); |
| return; |
| } |
| |
| // Remove it. |
| requestRemoveMapping(mapPtr); |
| unregisterMapping(mapPtr, true); |
| enforceAvailableMappingsLimits(); |
| }); |
| } |
| |
| void |
| UPnPContext::registerController(void* controller) |
| { |
| { |
| std::lock_guard lock(mappingMutex_); |
| if (shutdownComplete_) { |
| if (logger_) logger_->warn("UPnPContext already shut down"); |
| return; |
| } |
| auto ret = controllerList_.emplace(controller); |
| if (not ret.second) { |
| if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller)); |
| return; |
| } |
| } |
| |
| if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller)); |
| if (not started_) |
| startUpnp(); |
| } |
| |
| void |
| UPnPContext::unregisterController(void* controller) |
| { |
| if (shutdownComplete_) |
| return; |
| std::unique_lock lock(mappingMutex_); |
| if (controllerList_.erase(controller) == 1) { |
| if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller)); |
| } else { |
| if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller)); |
| } |
| |
| if (controllerList_.empty()) { |
| lock.unlock(); |
| stopUpnp(); |
| } |
| } |
| |
| std::vector<IGDInfo> |
| UPnPContext::getIgdsInfo() const |
| { |
| std::vector<IGDInfo> igdInfoList; |
| |
| for (const auto& [_, protocol] : protocolList_) { |
| for (auto& igd : protocol->getIgdList()) { |
| IGDInfo info; |
| info.uid = igd->getUID(); |
| info.localIp = igd->getLocalIp(); |
| info.publicIp = igd->getPublicIp(); |
| info.mappingInfoList = protocol->getMappingsInfo(igd); |
| |
| igdInfoList.push_back(std::move(info)); |
| } |
| } |
| |
| return igdInfoList; |
| } |
| |
| // TODO: refactor this function so that it can never fail unless there are literally no ports available |
| uint16_t |
| UPnPContext::getAvailablePortNumber(PortType type) |
| { |
| // Only return an available random port. No actual |
| // reservation is made here. |
| |
| std::lock_guard lock(mappingMutex_); |
| const auto& mappingList = getMappingList(type); |
| int tryCount = 0; |
| while (tryCount++ < MAX_REQUEST_RETRIES) { |
| uint16_t port = generateRandomPort(type); |
| Mapping map(type, port, port); |
| if (mappingList.find(map.getMapKey()) == mappingList.end()) |
| return port; |
| } |
| |
| // Very unlikely to get here. |
| if (logger_) logger_->error("Could not find an available port after %i trials", MAX_REQUEST_RETRIES); |
| return 0; |
| } |
| |
| void |
| UPnPContext::requestMapping(const Mapping::sharedPtr_t& map) |
| { |
| assert(map); |
| auto const& igd = getCurrentIgd(); |
| // We must have at least a valid IGD pointer if we get here. |
| // Note that this method is called only if there was a valid IGD, but |
| // because the processing is asynchronous, there may no longer |
| // be one by the time this code executes. |
| if (not igd) { |
| if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available", |
| map->toString()); |
| return; |
| } |
| |
| map->setIgd(igd); |
| |
| if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]", |
| map->toString(), |
| igd->getProtocolName(), |
| igd->toString()); |
| |
| updateMappingState(map, MappingState::IN_PROGRESS); |
| |
| auto const& protocol = protocolList_.at(igd->getProtocol()); |
| protocol->requestMappingAdd(*map); |
| } |
| |
| void |
| UPnPContext::provisionNewMappings(PortType type, int portCount) |
| { |
| if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type)); |
| |
| while (portCount > 0) { |
| auto port = getAvailablePortNumber(type); |
| if (port > 0) { |
| // Found an available port number |
| portCount--; |
| Mapping map(type, port, port, true); |
| registerMapping(map); |
| } else { |
| // Very unlikely to get here! |
| if (logger_) logger_->error("Cannot provision port: no available port number"); |
| } |
| } |
| } |
| |
| void |
| UPnPContext::deleteUnneededMappings(PortType type, int portCount) |
| { |
| if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type)); |
| |
| std::lock_guard lock(mappingMutex_); |
| auto& mappingList = getMappingList(type); |
| |
| for (auto it = mappingList.begin(); it != mappingList.end();) { |
| auto map = it->second; |
| assert(map); |
| |
| if (not map->isAvailable()) { |
| it++; |
| continue; |
| } |
| |
| if (map->getState() == MappingState::OPEN and portCount > 0) { |
| // Close portCount mappings in "OPEN" state. |
| requestRemoveMapping(map); |
| it = mappingList.erase(it); |
| portCount--; |
| } else if (map->getState() != MappingState::OPEN) { |
| // If this methods is called, it means there are more open |
| // mappings than required. So, all mappings in a state other |
| // than "OPEN" state (typically in in-progress state) will |
| // be deleted as well. |
| it = mappingList.erase(it); |
| } else { |
| it++; |
| } |
| } |
| } |
| |
| void |
| UPnPContext::updateCurrentIgd() |
| { |
| std::lock_guard lock(mappingMutex_); |
| if (currentIgd_ and currentIgd_->isValid()) { |
| if (logger_) logger_->debug("Current IGD is still valid, no need to update"); |
| return; |
| } |
| |
| // Reset and search for the best IGD. |
| currentIgd_.reset(); |
| |
| for (auto const& [_, protocol] : protocolList_) { |
| if (protocol->isReady()) { |
| auto igdList = protocol->getIgdList(); |
| assert(not igdList.empty()); |
| auto const& igd = igdList.front(); |
| if (not igd->isValid()) |
| continue; |
| |
| // Prefer NAT-PMP over PUPNP. |
| if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP) |
| continue; |
| |
| // Update. |
| currentIgd_ = igd; |
| } |
| } |
| |
| if (currentIgd_ and currentIgd_->isValid()) { |
| if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ", |
| currentIgd_->getProtocolName(), |
| currentIgd_->getUID(), |
| currentIgd_->toString()); |
| } else { |
| if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found"); |
| } |
| } |
| |
| std::shared_ptr<IGD> |
| UPnPContext::getCurrentIgd() const |
| { |
| return currentIgd_; |
| } |
| |
| void |
| UPnPContext::enforceAvailableMappingsLimits() |
| { |
| for (auto type : {PortType::TCP, PortType::UDP}) { |
| int pendingCount = 0; |
| int inProgressCount = 0; |
| int openCount = 0; |
| { |
| std::lock_guard lock(mappingMutex_); |
| const auto& mappingList = getMappingList(type); |
| for (const auto& [_, mapping] : mappingList) { |
| if (!mapping->isAvailable()) |
| continue; |
| switch (mapping->getState()) { |
| case MappingState::PENDING: |
| pendingCount++; |
| break; |
| case MappingState::IN_PROGRESS: |
| inProgressCount++; |
| break; |
| case MappingState::OPEN: |
| openCount++; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| int availableCount = openCount + pendingCount + inProgressCount; |
| if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)", |
| Mapping::getTypeStr(type), |
| availableCount, |
| openCount, |
| pendingCount, |
| inProgressCount); |
| |
| int minAvailableMappings = getMinAvailableMappings(type); |
| if (minAvailableMappings > availableCount) { |
| provisionNewMappings(type, minAvailableMappings - availableCount); |
| continue; |
| } |
| |
| int maxAvailableMappings = getMaxAvailableMappings(type); |
| if (openCount > maxAvailableMappings) { |
| deleteUnneededMappings(type, openCount - maxAvailableMappings); |
| } |
| } |
| } |
| |
| void |
| UPnPContext::renewMappings() |
| { |
| if (!started_) |
| return; |
| |
| const auto& igd = getCurrentIgd(); |
| if (!igd) { |
| if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available"); |
| return; |
| } |
| |
| auto now = sys_clock::now(); |
| auto nextRenewalTime = sys_clock::time_point::max(); |
| |
| std::vector<Mapping::sharedPtr_t> toRenew; |
| int toRenewLaterCount = 0; |
| |
| for (auto type : {PortType::TCP, PortType::UDP}) { |
| std::lock_guard lock(mappingMutex_); |
| const auto& mappingList = getMappingList(type); |
| for (const auto& [_, map] : mappingList) { |
| if (not map->isValid()) |
| continue; |
| if (map->getState() != MappingState::OPEN) |
| continue; |
| |
| auto mapRenewalTime = map->getRenewalTime(); |
| if (now >= mapRenewalTime) { |
| toRenew.emplace_back(map); |
| } else if (mapRenewalTime < sys_clock::time_point::max()) { |
| toRenewLaterCount++; |
| if (mapRenewalTime < nextRenewalTime) |
| nextRenewalTime = map->getRenewalTime(); |
| } |
| |
| } |
| } |
| |
| if (!toRenew.empty()) { |
| if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size()); |
| } |
| for (const auto& map : toRenew) { |
| const auto& protocol = protocolList_.at(map->getIgd()->getProtocol()); |
| protocol->requestMappingRenew(*map); |
| } |
| if (toRenewLaterCount > 0) { |
| nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY; |
| if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})", |
| toRenewLaterCount, |
| fmt::localtime(sys_clock::to_time_t(nextRenewalTime))); |
| mappingRenewalTimer_.expires_at(nextRenewalTime); |
| mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) { |
| if (ec != asio::error::operation_aborted) |
| renewMappings(); |
| }); |
| } |
| } |
| |
| void |
| UPnPContext::scheduleMappingsRenewal() |
| { |
| // Debounce the scheduling function so that it doesn't get called multiple |
| // times when several mappings are added or renewed in rapid succession. |
| renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500)); |
| renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) { |
| if (ec != asio::error::operation_aborted) |
| _scheduleMappingsRenewal(); |
| }); |
| } |
| |
| void |
| UPnPContext::_scheduleMappingsRenewal() |
| { |
| if (!started_) |
| return; |
| |
| sys_clock::time_point nextRenewalTime = sys_clock::time_point::max(); |
| for (auto type : {PortType::TCP, PortType::UDP}) { |
| std::lock_guard lock(mappingMutex_); |
| const auto& mappingList = getMappingList(type); |
| for (const auto& [_, map] : mappingList) { |
| if (map->getState() == MappingState::OPEN && |
| map->getRenewalTime() < nextRenewalTime) |
| nextRenewalTime = map->getRenewalTime(); |
| } |
| } |
| if (nextRenewalTime == sys_clock::time_point::max()) |
| return; |
| |
| // Add a small delay so that we don't have to call renewMappings multiple |
| // times in a row (and iterate over the whole list of mappings each time) |
| // when multiple mappings have almost the same renewal time. |
| nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY; |
| if (nextRenewalTime == mappingRenewalTimer_.expiry()) |
| return; |
| |
| if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}", |
| fmt::localtime(sys_clock::to_time_t(nextRenewalTime))); |
| mappingRenewalTimer_.expires_at(nextRenewalTime); |
| mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) { |
| if (ec != asio::error::operation_aborted) |
| renewMappings(); |
| }); |
| } |
| |
| void |
| UPnPContext::syncLocalMappingListWithIgd() |
| { |
| std::lock_guard lock(syncMutex_); |
| if (syncRequested_) |
| return; |
| |
| syncRequested_ = true; |
| syncTimer_.expires_after(std::chrono::minutes(5)); |
| syncTimer_.async_wait([this](asio::error_code const& ec) { |
| if (ec != asio::error::operation_aborted) |
| _syncLocalMappingListWithIgd(); |
| }); |
| } |
| |
| void |
| UPnPContext::_syncLocalMappingListWithIgd() |
| { |
| { |
| std::lock_guard lock(syncMutex_); |
| syncRequested_ = false; |
| } |
| const auto& igd = getCurrentIgd(); |
| if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) { |
| return; |
| } |
| auto pupnp = protocolList_.at(NatProtocolType::PUPNP); |
| if (!pupnp->isReady()) |
| return; |
| |
| if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]", |
| igd->toString()); |
| auto remoteMapList = pupnp->getMappingsListByDescr(igd, |
| Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX); |
| bool requestsInProgress = false; |
| // Use a temporary list to avoid processing mappings while holding the lock. |
| std::list<Mapping::sharedPtr_t> toRemoveFromLocalList; |
| for (auto type: {PortType::TCP, PortType::UDP}) { |
| std::lock_guard lock(mappingMutex_); |
| for (auto& [_, map] : getMappingList(type)) { |
| if (map->getProtocol() != NatProtocolType::PUPNP) { |
| continue; |
| } |
| switch (map->getState()) { |
| case MappingState::PENDING: |
| case MappingState::IN_PROGRESS: |
| requestsInProgress = true; |
| break; |
| case MappingState::OPEN: { |
| auto it = remoteMapList.find(map->getMapKey()); |
| if (it == remoteMapList.end()) { |
| if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the " |
| "remote list. Removing from local list.", |
| map->toString(), |
| igd->toString()); |
| toRemoveFromLocalList.emplace_back(map); |
| } else { |
| auto oldExpiryTime = map->getExpiryTime(); |
| auto newExpiryTime = it->second.getExpiryTime(); |
| // The value of newExpiryTime is based on the mapping's "lease duration" that we got from |
| // the IGD, which is supposed to be (according to the UPnP specification) the number of |
| // seconds remaining before the mapping expires. In practice, the duration values returned |
| // by some routers are only precise to the hour (i.e. they're always multiples of 3600). This |
| // means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case. |
| // In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to |
| // push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin. |
| if (newExpiryTime < oldExpiryTime || |
| newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) { |
| auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2; |
| map->setRenewalTime(newRenewalTime); |
| map->setExpiryTime(newExpiryTime); |
| } |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| } |
| scheduleMappingsRenewal(); |
| |
| for (auto const& map : toRemoveFromLocalList) { |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| } |
| if (!toRemoveFromLocalList.empty()) |
| enforceAvailableMappingsLimits(); |
| |
| if (requestsInProgress) { |
| // It's unlikely that there will be requests in progress when this function is |
| // called, but if there are, that suggests that we are dealing with a slow |
| // router, so we return early instead of sending additional deletion requests |
| // (which aren't essential and could end up "competing" with higher-priority |
| // creation/renewal requests). |
| return; |
| } |
| // Use a temporary list to avoid processing mappings while holding the lock. |
| std::list<Mapping> toRemoveFromIgd; |
| { |
| std::lock_guard lock(mappingMutex_); |
| |
| for (auto const& [_, map] : remoteMapList) { |
| const auto& mappingList = getMappingList(map.getType()); |
| auto it = mappingList.find(map.getMapKey()); |
| if (it == mappingList.end()) { |
| // Not present, request mapping remove. |
| toRemoveFromIgd.emplace_back(std::move(map)); |
| // Make only few remove requests at once. |
| if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT) |
| break; |
| } |
| } |
| } |
| |
| for (const auto& map : toRemoveFromIgd) { |
| pupnp->requestMappingRemove(map); |
| } |
| |
| } |
| |
| void |
| UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd) |
| { |
| // Use temporary list to avoid holding the lock while |
| // processing the mapping list. |
| std::list<Mapping::sharedPtr_t> toRemoveList; |
| { |
| std::lock_guard lock(mappingMutex_); |
| |
| PortType types[2] {PortType::TCP, PortType::UDP}; |
| for (auto& type : types) { |
| const auto& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| if (map->getIgd() == igd) |
| toRemoveList.emplace_back(map); |
| } |
| } |
| } |
| |
| for (auto const& map : toRemoveList) { |
| if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])", |
| map->toString(), |
| igd->toString(), |
| igd->getProtocolName()); |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| } |
| } |
| |
| void |
| UPnPContext::processPendingRequests() |
| { |
| // This list holds the mappings to be requested. This is |
| // needed to avoid performing the requests while holding |
| // the lock. |
| std::list<Mapping::sharedPtr_t> requestsList; |
| |
| // Populate the list of requests to perform. |
| { |
| std::lock_guard lock(mappingMutex_); |
| PortType typeArray[2] {PortType::TCP, PortType::UDP}; |
| |
| for (auto type : typeArray) { |
| const auto& mappingList = getMappingList(type); |
| for (const auto& [_, map] : mappingList) { |
| if (map->getState() == MappingState::PENDING) { |
| if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}", |
| map->toString()); |
| requestsList.emplace_back(map); |
| } |
| } |
| } |
| } |
| |
| // Process the pending requests. |
| for (auto const& map : requestsList) { |
| requestMapping(map); |
| } |
| } |
| |
| void |
| UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event) |
| { |
| assert(igd); |
| |
| char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED" |
| : event == UpnpIgdEvent::REMOVED ? "REMOVED" |
| : "INVALID"; |
| |
| auto const& igdLocalAddr = igd->getLocalIp(); |
| auto protocolName = igd->getProtocolName(); |
| |
| if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]", |
| igd->getUID(), |
| igd->toString(), |
| protocolName, |
| IgdState); |
| |
| if (not igdLocalAddr) { |
| if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring", |
| protocolName, |
| igd->getUID(), |
| igd->toString()); |
| return; |
| } |
| |
| if (not igd->getPublicIp()) { |
| if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring", |
| protocolName, |
| igd->getUID(), |
| igd->toString()); |
| return; |
| } |
| |
| { |
| std::lock_guard lock(publicAddressMutex_); |
| if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) { |
| if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]." |
| " The mapped addresses might not be reachable", |
| protocolName, |
| igd->getPublicIp().toString(), |
| knownPublicAddress_.toString()); |
| } |
| } |
| |
| if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) { |
| if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list", |
| igd->getUID(), |
| igd->toString(), |
| protocolName, |
| IgdState); |
| |
| pruneMappingsWithInvalidIgds(igd); |
| } |
| |
| updateCurrentIgd(); |
| if (isReady()) { |
| processPendingRequests(); |
| enforceAvailableMappingsLimits(); |
| } |
| } |
| |
| void |
| UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes) |
| { |
| // Check if we have a pending request for this response. |
| auto map = getMappingWithKey(mapRes.getMapKey()); |
| if (not map) { |
| // We may receive a response for a canceled request. Just ignore it. |
| if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match", |
| mapRes.toString(), |
| igd->toString(), |
| mapRes.getProtocolName()); |
| return; |
| } |
| |
| // The mapping request is new and successful. Update. |
| map->setIgd(igd); |
| map->setInternalAddress(mapRes.getInternalAddress()); |
| map->setExternalPort(mapRes.getExternalPort()); |
| map->setRenewalTime(mapRes.getRenewalTime()); |
| map->setExpiryTime(mapRes.getExpiryTime()); |
| // Update the state and report to the owner. |
| updateMappingState(map, MappingState::OPEN); |
| scheduleMappingsRenewal(); |
| |
| if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed", |
| map->toString(), |
| igd->toString(), |
| map->getProtocolName()); |
| |
| // Call setValid() to reset the errors counter. We need |
| // to reset the counter on each successful response. |
| igd->setValid(true); |
| if (igd->getProtocol() == NatProtocolType::PUPNP) |
| syncLocalMappingListWithIgd(); |
| } |
| |
| void |
| UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map) |
| { |
| auto mapPtr = getMappingWithKey(map.getMapKey()); |
| |
| if (not mapPtr) { |
| if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list", |
| map.toString(), |
| igd->toString(), |
| map.getProtocolName()); |
| return; |
| } |
| if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) { |
| if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state", |
| mapPtr->toString(), |
| igd->toString(), |
| mapPtr->getProtocolName()); |
| return; |
| } |
| |
| mapPtr->setRenewalTime(map.getRenewalTime()); |
| mapPtr->setExpiryTime(map.getExpiryTime()); |
| scheduleMappingsRenewal(); |
| if (igd->getProtocol() == NatProtocolType::PUPNP) |
| syncLocalMappingListWithIgd(); |
| } |
| |
| void |
| UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map) |
| { |
| if (not map or not map->isValid()) { |
| // Silently ignore if the mapping is invalid |
| return; |
| } |
| auto protocol = protocolList_.at(map->getIgd()->getProtocol()); |
| protocol->requestMappingRemove(*map); |
| } |
| |
| void |
| UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes) |
| { |
| if (not mapRes.isValid()) |
| return; |
| |
| auto map = getMappingWithKey(mapRes.getMapKey()); |
| // Notify the listener. |
| if (map and map->getNotifyCallback()) |
| map->getNotifyCallback()(map); |
| } |
| |
| Mapping::sharedPtr_t |
| UPnPContext::registerMapping(Mapping& map) |
| { |
| if (map.getExternalPort() == 0) { |
| // JAMI_DBG("Port number not set. Will set a random port number"); |
| auto port = getAvailablePortNumber(map.getType()); |
| map.setExternalPort(port); |
| map.setInternalPort(port); |
| } |
| |
| // Newly added mapping must be in pending state by default. |
| map.setState(MappingState::PENDING); |
| |
| Mapping::sharedPtr_t mapPtr; |
| |
| { |
| std::lock_guard lock(mappingMutex_); |
| auto& mappingList = getMappingList(map.getType()); |
| |
| auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map)); |
| if (not ret.second) { |
| if (logger_) logger_->warn("Mapping request for {} already added!", map.toString()); |
| return {}; |
| } |
| mapPtr = ret.first->second; |
| assert(mapPtr); |
| } |
| |
| // No available IGD. The pending mapping requests will be processed |
| // when an IGD becomes available |
| if (not isReady()) { |
| if (logger_) logger_->warn("No IGD available. Mapping will be requested when an IGD becomes available"); |
| } else { |
| requestMapping(mapPtr); |
| } |
| |
| return mapPtr; |
| } |
| |
| void |
| UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate) |
| { |
| if (not map) { |
| return; |
| } |
| |
| if (map->getAutoUpdate() && !ignoreAutoUpdate) { |
| if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested", |
| map->toString()); |
| |
| Mapping newMapping(map->getType()); |
| newMapping.enableAutoUpdate(true); |
| newMapping.setNotifyCallback(map->getNotifyCallback()); |
| reserveMapping(newMapping); |
| |
| // TODO: figure out if this line is actually necessary |
| // (See https://review.jami.net/c/jami-daemon/+/16940) |
| map->setNotifyCallback(nullptr); |
| } |
| std::lock_guard lock(mappingMutex_); |
| auto& mappingList = getMappingList(map->getType()); |
| |
| if (mappingList.erase(map->getMapKey()) == 1) { |
| if (logger_) logger_->debug("Unregistered mapping {}", map->toString()); |
| } else { |
| // The mapping may already be un-registered. Just ignore it. |
| if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match", |
| map->toString(), |
| map->getProtocolName()); |
| } |
| } |
| |
| std::map<Mapping::key_t, Mapping::sharedPtr_t>& |
| UPnPContext::getMappingList(PortType type) |
| { |
| unsigned typeIdx = type == PortType::TCP ? 0 : 1; |
| return mappingList_[typeIdx]; |
| } |
| |
| Mapping::sharedPtr_t |
| UPnPContext::getMappingWithKey(Mapping::key_t key) |
| { |
| std::lock_guard lock(mappingMutex_); |
| auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key)); |
| auto it = mappingList.find(key); |
| if (it == mappingList.end()) |
| return nullptr; |
| return it->second; |
| } |
| |
| void |
| UPnPContext::onMappingRequestFailed(const Mapping& mapRes) |
| { |
| auto igd = mapRes.getIgd(); |
| auto const& map = getMappingWithKey(mapRes.getMapKey()); |
| if (not map) { |
| // We may receive a response for a removed request. Just ignore it. |
| if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match", |
| mapRes.toString(), |
| igd->toString()); |
| return; |
| } |
| |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| |
| if (logger_) logger_->warn("Request for mapping {} on IGD {} failed", |
| map->toString(), |
| igd->toString()); |
| |
| enforceAvailableMappingsLimits(); |
| } |
| |
| void |
| UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify) |
| { |
| assert(map); |
| |
| // Ignore if the state did not change. |
| if (newState == map->getState()) { |
| return; |
| } |
| |
| // Update the state. |
| map->setState(newState); |
| |
| // Notify the listener if set. |
| if (notify and map->getNotifyCallback()) |
| map->getNotifyCallback()(map); |
| } |
| |
| } // namespace upnp |
| } // namespace dhtnet |