add initial project structure
Change-Id: I6a3fb080ff623b312e42d71754480a7ce00b81a0
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
new file mode 100644
index 0000000..ef556f1
--- /dev/null
+++ b/src/upnp/upnp_context.cpp
@@ -0,0 +1,1339 @@
+/*
+ * 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