| /* |
| * Copyright (C) 2004-2023 Savoir-faire Linux Inc. |
| * |
| * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> |
| * Author: Eden Abitbol <eden.abitbol@savoirfairelinux.com> |
| * Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com> |
| * |
| * 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, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include "upnp_context.h" |
| |
| namespace jami { |
| namespace upnp { |
| |
| constexpr static auto MAP_UPDATE_INTERVAL = std::chrono::seconds(30); |
| constexpr static int MAX_REQUEST_RETRIES = 20; |
| constexpr static int MAX_REQUEST_REMOVE_COUNT = 5; |
| |
| 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() |
| { |
| JAMI_DBG("Creating UPnPContext instance [%p]", 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)); |
| |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this] { init(); }); |
| return; |
| } |
| } |
| |
| std::shared_ptr<UPnPContext> |
| UPnPContext::getUPnPContext() |
| { |
| // This is the unique shared instance (singleton) of UPnPContext class. |
| static auto context = std::make_shared<UPnPContext>(); |
| return context; |
| } |
| |
| void |
| UPnPContext::shutdown(std::condition_variable& cv) |
| { |
| JAMI_DBG("Shutdown UPnPContext instance [%p]", this); |
| |
| stopUpnp(true); |
| |
| for (auto const& [_, proto] : protocolList_) { |
| proto->terminate(); |
| } |
| |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| mappingList_->clear(); |
| if (mappingListUpdateTimer_) |
| mappingListUpdateTimer_->cancel(); |
| controllerList_.clear(); |
| protocolList_.clear(); |
| shutdownComplete_ = true; |
| cv.notify_one(); |
| } |
| } |
| |
| void |
| UPnPContext::shutdown() |
| { |
| std::unique_lock<std::mutex> lk(mappingMutex_); |
| std::condition_variable cv; |
| |
| runOnUpnpContextQueue([&, this] { shutdown(cv); }); |
| |
| JAMI_DBG("Waiting for shutdown ..."); |
| |
| if (cv.wait_for(lk, std::chrono::seconds(30), [this] { return shutdownComplete_; })) { |
| JAMI_DBG("Shutdown completed"); |
| } else { |
| JAMI_ERR("Shutdown timed-out"); |
| } |
| } |
| |
| UPnPContext::~UPnPContext() |
| { |
| JAMI_DBG("UPnPContext instance [%p] destroyed", this); |
| } |
| |
| void |
| UPnPContext::init() |
| { |
| threadId_ = getCurrentThread(); |
| CHECK_VALID_THREAD(); |
| |
| #if HAVE_LIBNATPMP |
| auto natPmp = std::make_shared<NatPmp>(); |
| natPmp->setObserver(this); |
| protocolList_.emplace(NatProtocolType::NAT_PMP, std::move(natPmp)); |
| #endif |
| |
| #if HAVE_LIBUPNP |
| auto pupnp = std::make_shared<PUPnP>(); |
| pupnp->setObserver(this); |
| protocolList_.emplace(NatProtocolType::PUPNP, std::move(pupnp)); |
| #endif |
| } |
| |
| void |
| UPnPContext::startUpnp() |
| { |
| assert(not controllerList_.empty()); |
| |
| CHECK_VALID_THREAD(); |
| |
| JAMI_DBG("Starting UPNP context"); |
| |
| // Request a new IGD search. |
| for (auto const& [_, protocol] : protocolList_) { |
| protocol->searchForIgd(); |
| } |
| |
| started_ = true; |
| } |
| |
| void |
| UPnPContext::stopUpnp(bool forceRelease) |
| { |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, forceRelease] { stopUpnp(forceRelease); }); |
| return; |
| } |
| |
| JAMI_DBG("Stopping UPNP context"); |
| |
| // Clear all current mappings if any. |
| |
| // Use a temporary list to avoid processing the mapping |
| // list while holding the lock. |
| std::list<Mapping::sharedPtr_t> toRemoveList; |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| |
| PortType types[2] {PortType::TCP, PortType::UDP}; |
| for (auto& type : types) { |
| auto& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| toRemoveList.emplace_back(map); |
| } |
| } |
| // Invalidate the current IGDs. |
| preferredIgd_.reset(); |
| validIgdList_.clear(); |
| } |
| for (auto const& map : toRemoveList) { |
| requestRemoveMapping(map); |
| |
| // Notify is not needed in updateMappingState when |
| // shutting down (hence set it to false). NotifyCallback |
| // would trigger a new SIP registration and create a |
| // false registered state upon program close. |
| // It's handled by upper layers. |
| |
| updateMappingState(map, MappingState::FAILED, false); |
| // We dont remove mappings with auto-update enabled, |
| // unless forceRelease is true. |
| if (not map->getAutoUpdate() or forceRelease) { |
| map->enableAutoUpdate(false); |
| unregisterMapping(map); |
| } |
| } |
| |
| // Clear all current IGDs. |
| for (auto const& [_, protocol] : protocolList_) { |
| protocol->clearIgds(); |
| } |
| |
| started_ = false; |
| } |
| |
| uint16_t |
| UPnPContext::generateRandomPort(PortType type, bool mustBeEven) |
| { |
| 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; |
| |
| if (minPort >= maxPort) { |
| JAMI_ERR("Max port number (%i) must be greater than min port number (%i)", maxPort, minPort); |
| // Must be called with valid range. |
| assert(false); |
| } |
| |
| int fact = mustBeEven ? 2 : 1; |
| if (mustBeEven) { |
| minPort /= fact; |
| maxPort /= fact; |
| } |
| |
| // 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) * fact; |
| } |
| |
| void |
| UPnPContext::connectivityChanged() |
| { |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this] { connectivityChanged(); }); |
| return; |
| } |
| |
| auto hostAddr = ip_utils::getLocalAddr(AF_INET); |
| |
| JAMI_DBG("Connectivity change check: host address %s", hostAddr.toString().c_str()); |
| |
| 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()) { |
| JAMI_WARN("Host address changed from %s to %s", |
| protocol->getHostAddress().toString().c_str(), |
| hostAddr.toString().c_str()); |
| 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; |
| |
| JAMI_DBG("Connectivity changed. Clear the IGDs and restart"); |
| |
| stopUpnp(); |
| startUpnp(); |
| |
| // Mapping with auto update enabled must be processed first. |
| processMappingWithAutoUpdate(); |
| } |
| |
| void |
| UPnPContext::setPublicAddress(const IpAddr& addr) |
| { |
| if (not addr) |
| return; |
| |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| if (knownPublicAddress_ != addr) { |
| knownPublicAddress_ = std::move(addr); |
| JAMI_DBG("Setting the known public address to %s", addr.toString().c_str()); |
| } |
| } |
| |
| bool |
| UPnPContext::isReady() const |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| return not validIgdList_.empty(); |
| } |
| |
| IpAddr |
| UPnPContext::getExternalIP() const |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| // Return the first IGD Ip available. |
| if (not validIgdList_.empty()) { |
| return (*validIgdList_.begin())->getPublicIp(); |
| } |
| return {}; |
| } |
| |
| Mapping::sharedPtr_t |
| UPnPContext::reserveMapping(Mapping& requestedMap) |
| { |
| auto desiredPort = requestedMap.getExternalPort(); |
| |
| if (desiredPort == 0) { |
| JAMI_DBG("Desired port is not set, will provide the first available port for [%s]", |
| requestedMap.getTypeStr()); |
| } else { |
| JAMI_DBG("Try to find mapping for port %i [%s]", desiredPort, requestedMap.getTypeStr()); |
| } |
| |
| Mapping::sharedPtr_t mapRes; |
| |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| 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) { |
| JAMI_WARN("Did not find any available mapping. Will request one now"); |
| 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); |
| } |
| |
| updateMappingList(true); |
| |
| return mapRes; |
| } |
| |
| void |
| UPnPContext::releaseMapping(const Mapping& map) |
| { |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, map] { releaseMapping(map); }); |
| return; |
| } |
| |
| auto mapPtr = getMappingWithKey(map.getMapKey()); |
| |
| if (not mapPtr) { |
| // Might happen if the mapping failed or was never granted. |
| JAMI_DBG("Mapping %s does not exist or was already removed", map.toString().c_str()); |
| return; |
| } |
| |
| if (mapPtr->isAvailable()) { |
| JAMI_WARN("Trying to release an unused mapping %s", mapPtr->toString().c_str()); |
| return; |
| } |
| |
| // Remove it. |
| requestRemoveMapping(mapPtr); |
| unregisterMapping(mapPtr); |
| } |
| |
| void |
| UPnPContext::registerController(void* controller) |
| { |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| if (shutdownComplete_) { |
| JAMI_WARN("UPnPContext already shut down"); |
| return; |
| } |
| } |
| |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, controller] { registerController(controller); }); |
| return; |
| } |
| |
| auto ret = controllerList_.emplace(controller); |
| if (not ret.second) { |
| JAMI_WARN("Controller %p is already registered", controller); |
| return; |
| } |
| |
| JAMI_DBG("Successfully registered controller %p", controller); |
| if (not started_) |
| startUpnp(); |
| } |
| |
| void |
| UPnPContext::unregisterController(void* controller) |
| { |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, controller] { unregisterController(controller); }); |
| return; |
| } |
| |
| if (controllerList_.erase(controller) == 1) { |
| JAMI_DBG("Successfully unregistered controller %p", controller); |
| } else { |
| JAMI_DBG("Controller %p was already removed", controller); |
| } |
| |
| if (controllerList_.empty()) { |
| stopUpnp(); |
| } |
| } |
| |
| uint16_t |
| UPnPContext::getAvailablePortNumber(PortType type) |
| { |
| // Only return an availalable random port. No actual |
| // reservation is made here. |
| |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| 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. |
| JAMI_ERR("Could not find an available port after %i trials", MAX_REQUEST_RETRIES); |
| return 0; |
| } |
| |
| void |
| UPnPContext::requestMapping(const Mapping::sharedPtr_t& map) |
| { |
| assert(map); |
| |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, map] { requestMapping(map); }); |
| return; |
| } |
| |
| auto const& igd = getPreferredIgd(); |
| // We must have at least a valid IGD pointer if we get here. |
| // Not this method is called only if there were a valid IGD, however, |
| // because the processing is asynchronous, it's possible that the IGD |
| // was invalidated when the this code executed. |
| if (not igd) { |
| JAMI_DBG("No valid IGDs available"); |
| return; |
| } |
| |
| map->setIgd(igd); |
| |
| JAMI_DBG("Request mapping %s using protocol [%s] IGD [%s]", |
| map->toString().c_str(), |
| igd->getProtocolName(), |
| igd->toString().c_str()); |
| |
| if (map->getState() != MappingState::IN_PROGRESS) |
| updateMappingState(map, MappingState::IN_PROGRESS); |
| |
| auto const& protocol = protocolList_.at(igd->getProtocol()); |
| protocol->requestMappingAdd(*map); |
| } |
| |
| bool |
| UPnPContext::provisionNewMappings(PortType type, int portCount) |
| { |
| JAMI_DBG("Provision %i new mappings of type [%s]", portCount, Mapping::getTypeStr(type)); |
| |
| assert(portCount > 0); |
| |
| 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! |
| JAMI_ERR("Can not find any available port to provision!"); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool |
| UPnPContext::deleteUnneededMappings(PortType type, int portCount) |
| { |
| JAMI_DBG("Remove %i unneeded mapping of type [%s]", portCount, Mapping::getTypeStr(type)); |
| |
| assert(portCount > 0); |
| |
| CHECK_VALID_THREAD(); |
| |
| std::lock_guard<std::mutex> 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 = unregisterMapping(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 = unregisterMapping(it); |
| } else { |
| it++; |
| } |
| } |
| |
| return true; |
| } |
| |
| void |
| UPnPContext::updatePreferredIgd() |
| { |
| CHECK_VALID_THREAD(); |
| |
| if (preferredIgd_ and preferredIgd_->isValid()) |
| return; |
| |
| // Reset and search for the best IGD. |
| preferredIgd_.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 (preferredIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP) |
| continue; |
| |
| // Update. |
| preferredIgd_ = igd; |
| } |
| } |
| |
| if (preferredIgd_ and preferredIgd_->isValid()) { |
| JAMI_DBG("Preferred IGD updated to [%s] IGD [%s %s] ", |
| preferredIgd_->getProtocolName(), |
| preferredIgd_->getUID().c_str(), |
| preferredIgd_->toString().c_str()); |
| } |
| } |
| |
| std::shared_ptr<IGD> |
| UPnPContext::getPreferredIgd() const |
| { |
| CHECK_VALID_THREAD(); |
| |
| return preferredIgd_; |
| } |
| |
| void |
| UPnPContext::updateMappingList(bool async) |
| { |
| // Run async if requested. |
| if (async) { |
| runOnUpnpContextQueue([this] { updateMappingList(false); }); |
| return; |
| } |
| |
| CHECK_VALID_THREAD(); |
| |
| // Update the preferred IGD. |
| updatePreferredIgd(); |
| |
| if (mappingListUpdateTimer_) { |
| mappingListUpdateTimer_->cancel(); |
| mappingListUpdateTimer_ = {}; |
| } |
| |
| // Skip if no controller registered. |
| if (controllerList_.empty()) |
| return; |
| |
| // Cancel the current timer (if any) and re-schedule. |
| std::shared_ptr<IGD> prefIgd = getPreferredIgd(); |
| if (not prefIgd) { |
| JAMI_DBG("UPNP/NAT-PMP enabled, but no valid IGDs available"); |
| // No valid IGD. Nothing to do. |
| return; |
| } |
| |
| mappingListUpdateTimer_ = getScheduler()->scheduleIn([this] { updateMappingList(false); }, |
| MAP_UPDATE_INTERVAL); |
| |
| // Process pending requests if any. |
| processPendingRequests(prefIgd); |
| |
| // Make new requests for mappings that failed and have |
| // the auto-update option enabled. |
| processMappingWithAutoUpdate(); |
| |
| PortType typeArray[2] = {PortType::TCP, PortType::UDP}; |
| |
| for (auto idx : {0, 1}) { |
| auto type = typeArray[idx]; |
| |
| MappingStatus status; |
| getMappingStatus(type, status); |
| |
| JAMI_DBG("Mapping status [%s] - overall %i: %i open (%i ready + %i in use), %i pending, %i " |
| "in-progress, %i failed", |
| Mapping::getTypeStr(type), |
| status.sum(), |
| status.openCount_, |
| status.readyCount_, |
| status.openCount_ - status.readyCount_, |
| status.pendingCount_, |
| status.inProgressCount_, |
| status.failedCount_); |
| |
| if (status.failedCount_ > 0) { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto const& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| if (map->getState() == MappingState::FAILED) { |
| JAMI_DBG("Mapping status [%s] - Available [%s]", |
| map->toString(true).c_str(), |
| map->isAvailable() ? "YES" : "NO"); |
| } |
| } |
| } |
| |
| int toRequestCount = (int) minOpenPortLimit_[idx] |
| - (int) (status.readyCount_ + status.inProgressCount_ |
| + status.pendingCount_); |
| |
| // Provision/release mappings accordingly. |
| if (toRequestCount > 0) { |
| // Take into account the request in-progress when making |
| // requests for new mappings. |
| provisionNewMappings(type, toRequestCount); |
| } else if (status.readyCount_ > maxOpenPortLimit_[idx]) { |
| deleteUnneededMappings(type, status.readyCount_ - maxOpenPortLimit_[idx]); |
| } |
| } |
| |
| // Prune the mapping list if needed |
| if (protocolList_.at(NatProtocolType::PUPNP)->isReady()) { |
| #if HAVE_LIBNATPMP |
| // Dont perform if NAT-PMP is valid. |
| if (not protocolList_.at(NatProtocolType::NAT_PMP)->isReady()) |
| #endif |
| { |
| pruneMappingList(); |
| } |
| } |
| |
| #if HAVE_LIBNATPMP |
| // Renew nat-pmp allocations |
| if (protocolList_.at(NatProtocolType::NAT_PMP)->isReady()) |
| renewAllocations(); |
| #endif |
| } |
| |
| void |
| UPnPContext::pruneMappingList() |
| { |
| CHECK_VALID_THREAD(); |
| |
| MappingStatus status; |
| getMappingStatus(status); |
| |
| // Do not prune the list if there are pending/in-progress requests. |
| if (status.inProgressCount_ != 0 or status.pendingCount_ != 0) { |
| return; |
| } |
| |
| auto const& igd = getPreferredIgd(); |
| if (not igd or igd->getProtocol() != NatProtocolType::PUPNP) { |
| return; |
| } |
| auto protocol = protocolList_.at(NatProtocolType::PUPNP); |
| |
| auto remoteMapList = protocol->getMappingsListByDescr(igd, |
| Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX); |
| if (remoteMapList.empty()) { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| if (not getMappingList(PortType::TCP).empty() or getMappingList(PortType::TCP).empty()) { |
| JAMI_WARN("We have provisionned mappings but the PUPNP IGD returned an empty list!"); |
| } |
| } |
| |
| pruneUnMatchedMappings(igd, remoteMapList); |
| pruneUnTrackedMappings(igd, remoteMapList); |
| } |
| |
| void |
| UPnPContext::pruneUnMatchedMappings(const std::shared_ptr<IGD>& igd, |
| const std::map<Mapping::key_t, Mapping>& remoteMapList) |
| { |
| // Check/synchronize local mapping list with the list |
| // returned by the IGD. |
| |
| PortType types[2] {PortType::TCP, PortType::UDP}; |
| |
| for (auto& type : types) { |
| // Use a temporary list to avoid processing mappings while holding the lock. |
| std::list<Mapping::sharedPtr_t> toRemoveList; |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| // Only check mappings allocated by UPNP protocol. |
| if (map->getProtocol() != NatProtocolType::PUPNP) { |
| continue; |
| } |
| // Set mapping as failed if not found in the list |
| // returned by the IGD. |
| if (map->getState() == MappingState::OPEN |
| and remoteMapList.find(map->getMapKey()) == remoteMapList.end()) { |
| toRemoveList.emplace_back(map); |
| |
| JAMI_WARN("Mapping %s (IGD %s) marked as \"OPEN\" but not found in the " |
| "remote list. Mark as failed!", |
| map->toString().c_str(), |
| igd->toString().c_str()); |
| } |
| } |
| } |
| |
| for (auto const& map : toRemoveList) { |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| } |
| } |
| } |
| |
| void |
| UPnPContext::pruneUnTrackedMappings(const std::shared_ptr<IGD>& igd, |
| const std::map<Mapping::key_t, Mapping>& remoteMapList) |
| { |
| // Use a temporary list to avoid processing mappings while holding the lock. |
| std::list<Mapping> toRemoveList; |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| |
| for (auto const& [_, map] : remoteMapList) { |
| // Must has valid IGD pointer and use UPNP protocol. |
| assert(map.getIgd()); |
| assert(map.getIgd()->getProtocol() == NatProtocolType::PUPNP); |
| auto& mappingList = getMappingList(map.getType()); |
| auto it = mappingList.find(map.getMapKey()); |
| if (it == mappingList.end()) { |
| // Not present, request mapping remove. |
| toRemoveList.emplace_back(std::move(map)); |
| // Make only few remove requests at once. |
| if (toRemoveList.size() >= MAX_REQUEST_REMOVE_COUNT) |
| break; |
| } |
| } |
| } |
| |
| // Remove un-tracked mappings. |
| auto protocol = protocolList_.at(NatProtocolType::PUPNP); |
| for (auto const& map : toRemoveList) { |
| protocol->requestMappingRemove(map); |
| } |
| } |
| |
| void |
| UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd) |
| { |
| CHECK_VALID_THREAD(); |
| |
| // Use temporary list to avoid holding the lock while |
| // processing the mapping list. |
| std::list<Mapping::sharedPtr_t> toRemoveList; |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| |
| PortType types[2] {PortType::TCP, PortType::UDP}; |
| for (auto& type : types) { |
| auto& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| if (map->getIgd() == igd) |
| toRemoveList.emplace_back(map); |
| } |
| } |
| } |
| |
| for (auto const& map : toRemoveList) { |
| JAMI_DBG("Remove mapping %s (has an invalid IGD %s [%s])", |
| map->toString().c_str(), |
| igd->toString().c_str(), |
| igd->getProtocolName()); |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| } |
| } |
| |
| void |
| UPnPContext::processPendingRequests(const std::shared_ptr<IGD>& igd) |
| { |
| // 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<std::mutex> lock(mappingMutex_); |
| PortType typeArray[2] {PortType::TCP, PortType::UDP}; |
| |
| for (auto type : typeArray) { |
| auto& mappingList = getMappingList(type); |
| for (auto& [_, map] : mappingList) { |
| if (map->getState() == MappingState::PENDING) { |
| JAMI_DBG("Send pending request for mapping %s to IGD %s", |
| map->toString().c_str(), |
| igd->toString().c_str()); |
| requestsList.emplace_back(map); |
| } |
| } |
| } |
| } |
| |
| // Process the pending requests. |
| for (auto const& map : requestsList) { |
| requestMapping(map); |
| } |
| } |
| |
| void |
| UPnPContext::processMappingWithAutoUpdate() |
| { |
| // 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 for mappings with auto-update enabled. |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| PortType typeArray[2] {PortType::TCP, PortType::UDP}; |
| |
| for (auto type : typeArray) { |
| auto& mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| if (map->getState() == MappingState::FAILED and map->getAutoUpdate()) { |
| requestsList.emplace_back(map); |
| } |
| } |
| } |
| } |
| |
| for (auto const& oldMap : requestsList) { |
| // Request a new mapping if auto-update is enabled. |
| JAMI_DBG("Mapping %s has auto-update enabled, a new mapping will be requested", |
| oldMap->toString().c_str()); |
| |
| // Reserve a new mapping. |
| Mapping newMapping(oldMap->getType()); |
| newMapping.enableAutoUpdate(true); |
| newMapping.setNotifyCallback(oldMap->getNotifyCallback()); |
| |
| auto const& mapPtr = reserveMapping(newMapping); |
| assert(mapPtr); |
| |
| // Release the old one. |
| oldMap->setAvailable(true); |
| oldMap->enableAutoUpdate(false); |
| oldMap->setNotifyCallback(nullptr); |
| unregisterMapping(oldMap); |
| } |
| } |
| |
| void |
| UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event) |
| { |
| assert(igd); |
| |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, igd, event] { onIgdUpdated(igd, event); }); |
| return; |
| } |
| |
| // Reset to start search for a new best IGD. |
| preferredIgd_.reset(); |
| |
| char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED" |
| : event == UpnpIgdEvent::REMOVED ? "REMOVED" |
| : "INVALID"; |
| |
| auto const& igdLocalAddr = igd->getLocalIp(); |
| auto protocolName = igd->getProtocolName(); |
| |
| JAMI_DBG("New event for IGD [%s %s] [%s]: [%s]", |
| igd->getUID().c_str(), |
| igd->toString().c_str(), |
| protocolName, |
| IgdState); |
| |
| // Check if the IGD has valid addresses. |
| if (not igdLocalAddr) { |
| JAMI_WARN("[%s] IGD has an invalid local address", protocolName); |
| return; |
| } |
| |
| if (not igd->getPublicIp()) { |
| JAMI_WARN("[%s] IGD has an invalid public address", protocolName); |
| return; |
| } |
| |
| if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) { |
| JAMI_WARN("[%s] IGD external address [%s] does not match known public address [%s]." |
| " The mapped addresses might not be reachable", |
| protocolName, |
| igd->getPublicIp().toString().c_str(), |
| knownPublicAddress_.toString().c_str()); |
| } |
| |
| // The IGD was removed or is invalid. |
| if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) { |
| JAMI_WARN("State of IGD [%s %s] [%s] changed to [%s]. Pruning the mapping list", |
| igd->getUID().c_str(), |
| igd->toString().c_str(), |
| protocolName, |
| IgdState); |
| |
| pruneMappingsWithInvalidIgds(igd); |
| |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| validIgdList_.erase(igd); |
| return; |
| } |
| |
| // Update the IGD list. |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto ret = validIgdList_.emplace(igd); |
| if (ret.second) { |
| JAMI_DBG("IGD [%s] on address %s was added. Will process any pending requests", |
| protocolName, |
| igdLocalAddr.toString(true, true).c_str()); |
| } else { |
| // Already in the list. |
| JAMI_ERR("IGD [%s] on address %s already in the list", |
| protocolName, |
| igdLocalAddr.toString(true, true).c_str()); |
| return; |
| } |
| } |
| |
| // Update the provisionned mappings. |
| updateMappingList(false); |
| } |
| |
| void |
| UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes) |
| { |
| CHECK_VALID_THREAD(); |
| |
| // 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. |
| JAMI_DBG("Response for mapping %s [IGD %s] [%s] does not have a local match", |
| mapRes.toString().c_str(), |
| igd->toString().c_str(), |
| mapRes.getProtocolName()); |
| return; |
| } |
| |
| // The mapping request is new and successful. Update. |
| map->setIgd(igd); |
| map->setInternalAddress(mapRes.getInternalAddress()); |
| map->setExternalPort(mapRes.getExternalPort()); |
| |
| // Update the state and report to the owner. |
| updateMappingState(map, MappingState::OPEN); |
| |
| JAMI_DBG("Mapping %s (on IGD %s [%s]) successfully performed", |
| map->toString().c_str(), |
| igd->toString().c_str(), |
| map->getProtocolName()); |
| |
| // Call setValid() to reset the errors counter. We need |
| // to reset the counter on each successful response. |
| igd->setValid(true); |
| } |
| |
| #if HAVE_LIBNATPMP |
| void |
| UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map) |
| { |
| auto mapPtr = getMappingWithKey(map.getMapKey()); |
| |
| if (not mapPtr) { |
| // We may receive a notification for a canceled request. Ignore it. |
| JAMI_WARN("Renewed mapping %s from IGD %s [%s] does not have a match in local list", |
| map.toString().c_str(), |
| igd->toString().c_str(), |
| map.getProtocolName()); |
| return; |
| } |
| if (mapPtr->getProtocol() != NatProtocolType::NAT_PMP or not mapPtr->isValid() |
| or mapPtr->getState() != MappingState::OPEN) { |
| JAMI_WARN("Renewed mapping %s from IGD %s [%s] is in unexpected state", |
| mapPtr->toString().c_str(), |
| igd->toString().c_str(), |
| mapPtr->getProtocolName()); |
| return; |
| } |
| |
| mapPtr->setRenewalTime(map.getRenewalTime()); |
| } |
| #endif |
| |
| void |
| UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map) |
| { |
| CHECK_VALID_THREAD(); |
| |
| if (not map) { |
| JAMI_ERR("Mapping shared pointer is null!"); |
| return; |
| } |
| |
| if (not map->isValid()) { |
| // Silently ignore if the mapping is invalid |
| return; |
| } |
| |
| auto protocol = protocolList_.at(map->getIgd()->getProtocol()); |
| protocol->requestMappingRemove(*map); |
| } |
| |
| void |
| UPnPContext::deleteAllMappings(PortType type) |
| { |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, type] { deleteAllMappings(type); }); |
| return; |
| } |
| |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto& mappingList = getMappingList(type); |
| |
| for (auto const& [_, map] : mappingList) { |
| requestRemoveMapping(map); |
| } |
| } |
| |
| void |
| UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes) |
| { |
| if (not mapRes.isValid()) |
| return; |
| |
| if (not isValidThread()) { |
| runOnUpnpContextQueue([this, igd, mapRes] { onMappingRemoved(igd, mapRes); }); |
| 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<std::mutex> lock(mappingMutex_); |
| auto& mappingList = getMappingList(map.getType()); |
| |
| auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map)); |
| if (not ret.second) { |
| JAMI_WARN("Mapping request for %s already added!", map.toString().c_str()); |
| return {}; |
| } |
| mapPtr = ret.first->second; |
| assert(mapPtr); |
| } |
| |
| // No available IGD. The pending mapping requests will be processed |
| // when a IGD becomes available (in onIgdAdded() method). |
| if (not isReady()) { |
| JAMI_WARN("No IGD available. Mapping will be requested when an IGD becomes available"); |
| } else { |
| requestMapping(mapPtr); |
| } |
| |
| return mapPtr; |
| } |
| |
| std::map<Mapping::key_t, Mapping::sharedPtr_t>::iterator |
| UPnPContext::unregisterMapping(std::map<Mapping::key_t, Mapping::sharedPtr_t>::iterator it) |
| { |
| assert(it->second); |
| |
| CHECK_VALID_THREAD(); |
| auto descr = it->second->toString(); |
| auto& mappingList = getMappingList(it->second->getType()); |
| auto ret = mappingList.erase(it); |
| |
| return ret; |
| } |
| |
| void |
| UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map) |
| { |
| CHECK_VALID_THREAD(); |
| |
| if (not map) { |
| JAMI_ERR("Mapping pointer is null"); |
| return; |
| } |
| |
| if (map->getAutoUpdate()) { |
| // Dont unregister mappings with auto-update enabled. |
| return; |
| } |
| auto& mappingList = getMappingList(map->getType()); |
| |
| if (mappingList.erase(map->getMapKey()) == 1) { |
| JAMI_DBG("Unregistered mapping %s", map->toString().c_str()); |
| } else { |
| // The mapping may already be un-registered. Just ignore it. |
| JAMI_DBG("Mapping %s [%s] does not have a local match", |
| map->toString().c_str(), |
| 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<std::mutex> 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::getMappingStatus(PortType type, MappingStatus& status) |
| { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto& mappingList = getMappingList(type); |
| |
| for (auto const& [_, map] : mappingList) { |
| switch (map->getState()) { |
| case MappingState::PENDING: { |
| status.pendingCount_++; |
| break; |
| } |
| case MappingState::IN_PROGRESS: { |
| status.inProgressCount_++; |
| break; |
| } |
| case MappingState::FAILED: { |
| status.failedCount_++; |
| break; |
| } |
| case MappingState::OPEN: { |
| status.openCount_++; |
| if (map->isAvailable()) |
| status.readyCount_++; |
| break; |
| } |
| |
| default: |
| // Must not get here. |
| assert(false); |
| break; |
| } |
| } |
| } |
| |
| void |
| UPnPContext::getMappingStatus(MappingStatus& status) |
| { |
| getMappingStatus(PortType::TCP, status); |
| getMappingStatus(PortType::UDP, status); |
| } |
| |
| void |
| UPnPContext::onMappingRequestFailed(const Mapping& mapRes) |
| { |
| CHECK_VALID_THREAD(); |
| |
| auto const& map = getMappingWithKey(mapRes.getMapKey()); |
| if (not map) { |
| // We may receive a response for a removed request. Just ignore it. |
| JAMI_DBG("Mapping %s [IGD %s] does not have a local match", |
| mapRes.toString().c_str(), |
| mapRes.getProtocolName()); |
| return; |
| } |
| |
| auto igd = map->getIgd(); |
| if (not igd) { |
| JAMI_ERR("IGD pointer is null"); |
| return; |
| } |
| |
| updateMappingState(map, MappingState::FAILED); |
| unregisterMapping(map); |
| |
| JAMI_WARN("Mapping request for %s failed on IGD %s [%s]", |
| map->toString().c_str(), |
| igd->toString().c_str(), |
| igd->getProtocolName()); |
| } |
| |
| void |
| UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify) |
| { |
| CHECK_VALID_THREAD(); |
| |
| assert(map); |
| |
| // Ignore if the state did not change. |
| if (newState == map->getState()) { |
| JAMI_DBG("Mapping %s already in state %s", map->toString().c_str(), map->getStateStr()); |
| return; |
| } |
| |
| // Update the state. |
| map->setState(newState); |
| |
| // Notify the listener if set. |
| if (notify and map->getNotifyCallback()) |
| map->getNotifyCallback()(map); |
| } |
| |
| #if HAVE_LIBNATPMP |
| void |
| UPnPContext::renewAllocations() |
| { |
| CHECK_VALID_THREAD(); |
| |
| // Check if the we have valid PMP IGD. |
| auto pmpProto = protocolList_.at(NatProtocolType::NAT_PMP); |
| |
| auto now = sys_clock::now(); |
| std::vector<Mapping::sharedPtr_t> toRenew; |
| |
| for (auto type : {PortType::TCP, PortType::UDP}) { |
| std::lock_guard<std::mutex> lock(mappingMutex_); |
| auto mappingList = getMappingList(type); |
| for (auto const& [_, map] : mappingList) { |
| if (not map->isValid()) |
| continue; |
| if (map->getProtocol() != NatProtocolType::NAT_PMP) |
| continue; |
| if (map->getState() != MappingState::OPEN) |
| continue; |
| if (now < map->getRenewalTime()) |
| continue; |
| |
| toRenew.emplace_back(map); |
| } |
| } |
| |
| // Quit if there are no mapping to renew |
| if (toRenew.empty()) |
| return; |
| |
| for (auto const& map : toRenew) { |
| pmpProto->requestMappingRenew(*map); |
| } |
| } |
| #endif |
| |
| } // namespace upnp |
| } // namespace jami |