diff --git a/src/ice_transport.cpp b/src/ice_transport.cpp index 50b35e0fc8cb2a950f769c8fc4a4ab6be257a94c..d1b45bb5cee1f08ebf14fc70766eff16241d7bbb 100644 --- a/src/ice_transport.cpp +++ b/src/ice_transport.cpp @@ -788,7 +788,7 @@ IceTransport::Impl::requestUpnpMappings() // Request upnp mapping for each component. for (unsigned compId = 1; compId <= component_count_; compId++) { // Set port number to 0 to get any available port. - Mapping requestedMap {0, 0, portType}; + Mapping requestedMap(portType); // Request the mapping Mapping::sharedPtr_t mapPtr = upnp_->reserveMapping(requestedMap); diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 316dd6aea51571e947c3aba71a9292ea3704f523..008938f92a882339e84b259d91b71b969095b7d9 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -1786,8 +1786,6 @@ JamiAccount::registerAsyncOps() upnpCtrl_->releaseMapping(dhtUpnpMapping_); } - dhtUpnpMapping_ = upnp::Mapping {0, 0, upnp::PortType::UDP}; - dhtUpnpMapping_.enableAutoUpdate(true); // Set the notify callback. @@ -1805,7 +1803,8 @@ JamiAccount::registerAsyncOps() if (*update) { // Check if we need to update the mapping and the registration. - if (dhtMap != *mapRes or dhtMap.getState() != mapRes->getState()) { + if (dhtMap.getMapKey() != mapRes->getMapKey() + or dhtMap.getState() != mapRes->getState()) { // The connectivity must be restarted, if either: // - the state changed to "OPEN", // - the state changed to "FAILED" and the mapping was in use. @@ -1813,7 +1812,7 @@ JamiAccount::registerAsyncOps() or (mapRes->getState() == upnp::MappingState::FAILED and dhtMap.getState() == upnp::MappingState::OPEN)) { // Update the mapping and restart the registration. - dhtMap = upnp::Mapping {*mapRes}; + dhtMap.updateFrom(mapRes); JAMI_WARN("[Account %s] Allocated port changed to %u. Restarting the " "registration", @@ -1824,14 +1823,14 @@ JamiAccount::registerAsyncOps() } else { // Only update the mapping. - dhtMap = upnp::Mapping {*mapRes}; + dhtMap.updateFrom(mapRes); } } } else { *update = true; // Set connection info and load the account. if (mapRes->getState() == upnp::MappingState::OPEN) { - dhtMap = upnp::Mapping {*mapRes}; + dhtMap.updateFrom(mapRes); JAMI_DBG("[Account %s] Mapping %s successfully allocated: starting the DHT", accId.c_str(), dhtMap.toString().c_str()); @@ -2566,7 +2565,6 @@ JamiAccount::doUnregister(std::function<void(bool)> released_cb) // Release current upnp mapping if any. if (upnpCtrl_ and dhtUpnpMapping_.isValid()) { upnpCtrl_->releaseMapping(dhtUpnpMapping_); - dhtUpnpMapping_ = upnp::Mapping {}; } lock.unlock(); diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 376534f49be6cd4d3b5bc8efb4b6a05232ae0fa0..fde37c8f33dc407a0f3f9462759ffe3a45d534dc 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -638,7 +638,7 @@ private: } /* Current UPNP mapping */ - upnp::Mapping dhtUpnpMapping_ {}; + upnp::Mapping dhtUpnpMapping_ {upnp::PortType::UDP}; bool dhtPeerDiscovery_ {false}; diff --git a/src/sip/sipaccount.cpp b/src/sip/sipaccount.cpp index 1375ccd441461376c97ebbd54abc5b7ad5606755..c0653039f4ed407e676de946d56ac5cf104608c6 100644 --- a/src/sip/sipaccount.cpp +++ b/src/sip/sipaccount.cpp @@ -738,7 +738,7 @@ SIPAccount::getVolatileAccountDetails() const bool SIPAccount::mapPortUPnP() { - upnp::Mapping map {publishedPort_, localPort_, upnp::PortType::UDP}; + upnp::Mapping map(upnp::PortType::UDP, publishedPort_, localPort_); map.setNotifyCallback([w = weak()](upnp::Mapping::sharedPtr_t mapRes) { if (auto accPtr = w.lock()) { auto oldPort = static_cast<in_port_t>(accPtr->publishedPortUsed_); diff --git a/src/upnp/protocol/igd.cpp b/src/upnp/protocol/igd.cpp index 4c625cbe920e07822279dc52f368052fb63af05c..8159a89367bea4d1a8ea72d67a5d3ab5df3785b8 100644 --- a/src/upnp/protocol/igd.cpp +++ b/src/upnp/protocol/igd.cpp @@ -27,7 +27,6 @@ namespace upnp { IGD::IGD(NatProtocolType proto) : protocol_(proto) - , valid_(true) {} bool @@ -66,5 +65,11 @@ IGD::incrementErrorsCounter() return true; } +int +IGD::getErrorsCount() const +{ + return errorsCounter_.load(); +} + } // namespace upnp } // namespace jami \ No newline at end of file diff --git a/src/upnp/protocol/igd.h b/src/upnp/protocol/igd.h index e52a1d283771af34491c59a51ee03c6b655f782c..ceb523bcbe6b2f8caf5baa231eed69660fbeac8d 100644 --- a/src/upnp/protocol/igd.h +++ b/src/upnp/protocol/igd.h @@ -44,7 +44,7 @@ class IGD { public: // Max error before moving the IGD to invalid state. - constexpr static int MAX_ERRORS_COUNT = 20; + constexpr static int MAX_ERRORS_COUNT = 10; IGD(NatProtocolType prot); virtual ~IGD() = default; @@ -93,10 +93,11 @@ public: void setValid(bool valid); bool isValid() const { return valid_; } bool incrementErrorsCounter(); + int getErrorsCount() const; protected: const NatProtocolType protocol_ {NatProtocolType::UNKNOWN}; - std::atomic_bool valid_ {true}; + std::atomic_bool valid_ {false}; std::atomic<int> errorsCounter_ {0}; mutable std::mutex mutex_; diff --git a/src/upnp/protocol/mapping.cpp b/src/upnp/protocol/mapping.cpp index 0ede1470729c7df0354ff0e0d1a7307822fd51db..58a5a367f5594b7f894d9a7d44993d1c0caada20 100644 --- a/src/upnp/protocol/mapping.cpp +++ b/src/upnp/protocol/mapping.cpp @@ -25,16 +25,16 @@ namespace jami { namespace upnp { -Mapping::Mapping(uint16_t portExternal, uint16_t portInternal, PortType type, bool available) - : internalAddr_() - , internalPort_(portInternal) +Mapping::Mapping(PortType type, uint16_t portExternal, uint16_t portInternal, bool available) + : type_(type) , externalPort_(portExternal) - , type_(type) + , internalPort_(portInternal) + , internalAddr_() , igd_() , available_(available) , state_(MappingState::PENDING) , notifyCb_(nullptr) - , timeoutTimer_(nullptr) + , timeoutTimer_() , autoUpdate_(false) #if HAVE_LIBNATPMP , renewalTime_(sys_clock::now()) @@ -53,75 +53,28 @@ Mapping::Mapping(const Mapping& other) available_ = other.available_; state_ = other.state_; notifyCb_ = other.notifyCb_; - timeoutTimer_ = other.timeoutTimer_; autoUpdate_ = other.autoUpdate_; #if HAVE_LIBNATPMP renewalTime_ = other.renewalTime_; #endif } -Mapping& -Mapping::operator=(Mapping&& other) noexcept -{ - std::lock_guard<std::mutex> lock(mutex_); - - if (this != &other) { - internalAddr_ = other.internalAddr_; - internalPort_ = other.internalPort_; - externalPort_ = other.externalPort_; - type_ = other.type_; - igd_ = other.igd_; - other.igd_.reset(); - available_ = other.available_; - other.available_ = false; - state_ = other.state_; - other.state_ = MappingState::PENDING; - notifyCb_ = std::move(other.notifyCb_); - other.notifyCb_ = nullptr; - timeoutTimer_ = std::move(other.timeoutTimer_); - other.timeoutTimer_ = nullptr; - autoUpdate_ = other.autoUpdate_; - other.autoUpdate_ = false; -#if HAVE_LIBNATPMP - renewalTime_ = other.renewalTime_; -#endif - } - return *this; -} - -bool -Mapping::operator==(const Mapping& other) const noexcept -{ - std::lock_guard<std::mutex> lock(mutex_); - - if (internalAddr_ != other.internalAddr_) - return false; - if (internalPort_ != other.internalPort_) - return false; - if (externalPort_ != other.externalPort_) - return false; - if (type_ != other.type_) - return false; - if (igd_ != other.igd_) - return false; - return true; -} - -bool -Mapping::operator!=(const Mapping& other) const noexcept +void +Mapping::updateFrom(const Mapping::sharedPtr_t& other) { - return not(*this == other); + updateFrom(*other); } void Mapping::updateFrom(const Mapping& other) { - if (internalPort_ != other.internalPort_ or type_ != other.type_) { - JAMI_ERR("The source and destination mappings must match (same type and port)"); - assert(false); + if (type_ != other.type_) { + JAMI_ERR("The source and destination types must match"); + return; } internalAddr_ = std::move(other.internalAddr_); + internalPort_ = other.internalPort_; externalPort_ = other.externalPort_; igd_ = other.igd_; state_ = other.state_; @@ -165,15 +118,17 @@ Mapping::getStateStr() const } std::string -Mapping::toString(bool addState) const +Mapping::toString(bool extraInfo) const { std::lock_guard<std::mutex> lock(mutex_); std::ostringstream descr; descr << UPNP_MAPPING_DESCRIPTION_PREFIX << "-" << getTypeStr(type_); descr << ":" << std::to_string(internalPort_); - if (addState) - descr << " (state=" << getStateStr(state_) << ")"; + if (extraInfo) { + descr << " (state=" << getStateStr(state_) + << ", auto-update=" << (autoUpdate_ ? "YES" : "NO") << ")"; + } return descr.str(); } @@ -231,9 +186,9 @@ Mapping::cancelTimeoutTimer() { std::lock_guard<std::mutex> lock(mutex_); - if (timeoutTimer_ != nullptr) { + if (timeoutTimer_) { timeoutTimer_->cancel(); - timeoutTimer_ = nullptr; + timeoutTimer_.reset(); } } diff --git a/src/upnp/protocol/mapping.h b/src/upnp/protocol/mapping.h index 694784e5b2221c22a19192e0e66314c0c4861334..43885917609960a3d39348d72bee2f021a21e5e9 100644 --- a/src/upnp/protocol/mapping.h +++ b/src/upnp/protocol/mapping.h @@ -61,19 +61,18 @@ public: static constexpr char const* MAPPING_STATE_STR[4] {"PENDING", "IN_PROGRESS", "FAILED", "OPEN"}; static constexpr char const* UPNP_MAPPING_DESCRIPTION_PREFIX {"JAMI"}; - Mapping(uint16_t portExternal = 0, + Mapping(PortType type, + uint16_t portExternal = 0, uint16_t portInternal = 0, - PortType type = PortType::UDP, bool available = true); Mapping(const Mapping& other); Mapping(Mapping&& other) = delete; ~Mapping() = default; - Mapping& operator=(Mapping&& other) noexcept; - bool operator==(const Mapping& other) const noexcept; - bool operator!=(const Mapping& other) const noexcept; - // Delete operators with confusing semantic. + Mapping& operator=(Mapping&& other) = delete; + bool operator==(const Mapping& other) = delete; + bool operator!=(const Mapping& other) = delete; bool operator<(const Mapping& other) = delete; bool operator>(const Mapping& other) = delete; bool operator<=(const Mapping& other) = delete; @@ -82,6 +81,7 @@ public: inline explicit operator bool() const { return isValid(); } void updateFrom(const Mapping& other); + void updateFrom(const Mapping::sharedPtr_t& other); std::string getExternalAddress() const; uint16_t getExternalPort() const; std::string getExternalPortStr() const; @@ -101,7 +101,7 @@ public: { return MAPPING_STATE_STR[static_cast<int>(state)]; } - std::string toString(bool addState = false) const; + std::string toString(bool extraInfo = false) const; bool isValid() const; bool hasValidHostAddress() const; bool hasPublicAddress() const; @@ -131,10 +131,10 @@ private: #endif mutable std::mutex mutex_; - std::string internalAddr_; - uint16_t internalPort_ {0}; + PortType type_ {PortType::UDP}; uint16_t externalPort_ {0}; - PortType type_; + uint16_t internalPort_ {0}; + std::string internalAddr_; // Protocol and std::shared_ptr<IGD> igd_; // Track if the mapping is available to use. @@ -142,7 +142,7 @@ private: // Track the state of the mapping MappingState state_; NotifyCallback notifyCb_; - std::shared_ptr<Task> timeoutTimer_; + std::shared_ptr<Task> timeoutTimer_ {}; // If true, a new mapping will be requested on behave of the mapping // owner when the mapping state changes from "OPEN" to "FAILED". bool autoUpdate_; diff --git a/src/upnp/protocol/natpmp/nat_pmp.cpp b/src/upnp/protocol/natpmp/nat_pmp.cpp index 6a58e7af12ff69ee6452875f0bdf4372dfac09a2..eab6049fe32e650ce29a3dc209b0b41fbf12df99 100644 --- a/src/upnp/protocol/natpmp/nat_pmp.cpp +++ b/src/upnp/protocol/natpmp/nat_pmp.cpp @@ -225,10 +225,14 @@ NatPmp::requestMappingAdd(const std::shared_ptr<IGD>& igd, const Mapping& mappin void NatPmp::requestMappingRenew(const Mapping& mapping) { - assert(mapping.getIgd()); - // Process on nat-pmp thread. getNatpmpScheduler()->run([this, mapping] { + if (not mapping.getIgd() or not mapping.getIgd()->isValid()) { + JAMI_WARN("NAT-PMP: Mapping %s has an invalid IGD. Ignoring.", + mapping.toString().c_str()); + return; + } + JAMI_DBG("NAT-PMP: Renew mapping %s on %s", mapping.toString().c_str(), mapping.getIgd()->getLocalIp().toString().c_str()); @@ -335,9 +339,7 @@ NatPmp::addPortMapping(const std::shared_ptr<IGD>& igdIn, Mapping& mapping, bool return; } - Mapping mapToAdd(mapping.getExternalPort(), - mapping.getInternalPort(), - mapping.getType() == PortType::UDP ? upnp::PortType::UDP : upnp::PortType::TCP); + Mapping mapToAdd(mapping); mapToAdd.setInternalAddress(getHostAddress().toString()); mapToAdd.setIgd(igd_); @@ -345,9 +347,9 @@ NatPmp::addPortMapping(const std::shared_ptr<IGD>& igdIn, Mapping& mapping, bool int err = sendMappingRequest(mapping, lifetime); if (err < 0) { - JAMI_ERR("NAT-PMP: Add mapping request failed with error %s %i", - getNatPmpErrorStr(err), - errno); + JAMI_WARN("NAT-PMP: Add mapping request failed with error %s %i", + getNatPmpErrorStr(err), + errno); if (isErrorFatal(err)) { // Fatal error, increment the counter. @@ -400,10 +402,7 @@ NatPmp::removePortMapping(Mapping& mapping) return; } - Mapping mapToRemove(mapping.getExternalPort(), - mapping.getInternalPort(), - mapping.getType() == PortType::UDP ? upnp::PortType::UDP - : upnp::PortType::TCP); + Mapping mapToRemove(mapping); uint32_t lifetime = 0; int err = sendMappingRequest(mapping, lifetime); diff --git a/src/upnp/protocol/pupnp/pupnp.cpp b/src/upnp/protocol/pupnp/pupnp.cpp index cf6334101cca85ec35c5515381d4c3d3e044fa0c..0b81a8c0992f3a1ad565501cb274d7d146d5b550 100644 --- a/src/upnp/protocol/pupnp/pupnp.cpp +++ b/src/upnp/protocol/pupnp/pupnp.cpp @@ -163,15 +163,10 @@ PUPnP::PUPnP() PUPnP::~PUPnP() { - JAMI_DBG("PUPnP: Destroying instance %p", this); // Clear all the lists. { std::lock_guard<std::mutex> lock(validIgdListMutex_); std::lock_guard<std::mutex> lk(ctrlptMutex_); - for (auto const& igd : validIgdList_) { - if (igd->isValid()) - deleteMappingsByDescription(igd, Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX); - } validIgdList_.clear(); clientRegistered_ = false; UpnpUnRegisterClient(ctrlptHandle_); @@ -191,6 +186,8 @@ PUPnP::~PUPnP() pupnpCv_.notify_all(); if (pupnpThread_.joinable()) pupnpThread_.join(); + + JAMI_DBG("PUPnP: Instance [%p] destroyed", this); } bool @@ -467,6 +464,9 @@ PUPnP::validateIgd(const IGDInfo& info) } } + // We have a valid IGD + igd_candidate->setValid(true); + JAMI_DBG("PUPnP: Added a new IGD [%s] to the list of valid IGDs", igd_candidate->getUID().c_str()); @@ -583,7 +583,7 @@ PUPnP::processAddMapAction(const std::string& ctrlURL, uint16_t iPort, PortType portType) { - Mapping mapToAdd(ePort, iPort, portType); + Mapping mapToAdd(portType, ePort, iPort); JAMI_DBG("PUPnP: Opened port %s", mapToAdd.toString().c_str()); @@ -615,7 +615,7 @@ PUPnP::processRemoveMapAction(const std::string& ctrlURL, uint16_t iPort, PortType portType) { - Mapping mapToRemove {ePort, iPort, portType}; + Mapping mapToRemove(portType, ePort, iPort); { std::lock_guard<std::mutex> lock(validIgdListMutex_); for (auto const& it : validIgdList_) { @@ -1324,11 +1324,11 @@ PUPnP::getMappingsListByDescr(const std::shared_ptr<IGD>& igd, const std::string } std::transform(transport.begin(), transport.end(), transport.begin(), ::toupper); + PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP; uint16_t ePort = static_cast<uint16_t>(std::stoi(port_external)); uint16_t iPort = static_cast<uint16_t>(std::stoi(port_internal)); - PortType type = transport.find("TCP") != std::string::npos ? PortType::TCP : PortType::UDP; - auto map = Mapping(ePort, iPort, type); + Mapping map(type, ePort, iPort); map.setIgd(igd); mapList.emplace(map.getMapKey(), std::move(map)); diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp index 6973540f59cc14521c8d10aaa7391de306c70a3a..e52f86bc1024df8b837c090da3519fd16d077f0c 100644 --- a/src/upnp/upnp_context.cpp +++ b/src/upnp/upnp_context.cpp @@ -24,6 +24,17 @@ namespace jami { namespace upnp { +constexpr static auto NAT_MAP_REQUEST_TIMEOUT_UNIT = std::chrono::seconds(1); +constexpr static auto PUPNP_MAP_REQUEST_TIMEOUT_UNIT = std::chrono::seconds(5); +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}; + std::shared_ptr<UPnPContext> UPnPContext::getUPnPContext() { @@ -34,6 +45,8 @@ UPnPContext::getUPnPContext() 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)); @@ -46,10 +59,14 @@ UPnPContext::UPnPContext() UPnPContext::~UPnPContext() { + stopUpnp(true); + + std::lock_guard<std::mutex> lock(mappingMutex_); + mappingList_->clear(); mappingListUpdateTimer_->cancel(); + controllerList_.clear(); - deleteAllMappings(PortType::UDP); - deleteAllMappings(PortType::TCP); + JAMI_DBG("UPnPContext instance [%p] destroyed", this); } void @@ -72,7 +89,7 @@ UPnPContext::init() } void -UPnPContext::StartUpnp() +UPnPContext::startUpnp() { assert(not controllerList_.empty()); @@ -89,14 +106,19 @@ UPnPContext::StartUpnp() } void -UPnPContext::StopUpnp() +UPnPContext::stopUpnp(bool forceRelease) { - CHECK_VALID_THREAD(); + if (not isValidThread()) { + runOnUpnpContextThread([this] { stopUpnp(); }); + return; + } JAMI_DBG("Stoping UPNP context"); - // Use temporary list to avoid holding the lock while - // processing the mapping list. + // 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_); @@ -113,11 +135,17 @@ UPnPContext::StopUpnp() } for (auto const& map : toRemoveList) { - map->cancelTimeoutTimer(); + requestRemoveMapping(map); updateMappingState(map, MappingState::FAILED); - unregisterMapping(map); + // 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(); } @@ -126,24 +154,27 @@ UPnPContext::StopUpnp() } uint16_t -UPnPContext::generateRandomPort(uint16_t min, uint16_t max, bool mustBeEven) +UPnPContext::generateRandomPort(PortType type, bool mustBeEven) { - if (min >= max) { - JAMI_ERR("Max port number (%i) must be greater than min port number (%i)", max, min); + 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) { - min /= fact; - max /= fact; + minPort /= fact; + maxPort /= fact; } // Seed the generator. static std::mt19937 gen(dht::crypto::getSeededRandomEngine()); // Define the range. - std::uniform_int_distribution<uint16_t> dist(min, max); + std::uniform_int_distribution<uint16_t> dist(minPort, maxPort); return dist(gen) * fact; } @@ -162,8 +193,8 @@ UPnPContext::connectivityChanged() JAMI_DBG("Connectivity changed. Reset IGDs and restart."); - StopUpnp(); - StartUpnp(); + stopUpnp(); + startUpnp(); processMappingWithAutoUpdate(); } @@ -271,7 +302,7 @@ UPnPContext::releaseMapping(const Mapping& map) auto mapPtr = getMappingWithKey(map.getMapKey()); if (not mapPtr) { - // Might happen if the mapping failed + // 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; } @@ -282,7 +313,7 @@ UPnPContext::releaseMapping(const Mapping& map) } // Remove it. - deleteMapping(mapPtr); + requestRemoveMapping(mapPtr); unregisterMapping(mapPtr); } @@ -302,7 +333,7 @@ UPnPContext::registerController(void* controller) JAMI_DBG("Successfully registered controller %p", this); if (not started_) - StartUpnp(); + startUpnp(); } void @@ -320,31 +351,21 @@ UPnPContext::unregisterController(void* controller) } if (controllerList_.empty()) - StopUpnp(); + stopUpnp(); } uint16_t -UPnPContext::getAvailablePortNumber(PortType type, uint16_t minPort, uint16_t maxPort) +UPnPContext::getAvailablePortNumber(PortType type) { // Only return an availalable random port. No actual // reservation is made here. - if (minPort > maxPort) { - JAMI_ERR("Min port %u can not be greater than max port %u", minPort, maxPort); - return 0; - } - - if (minPort == 0) - minPort = portRange_[type].first; - if (maxPort == 0) - maxPort = portRange_[type].second; - std::lock_guard<std::mutex> lock(mappingMutex_); auto& mappingList = getMappingList(type); int tryCount = 0; while (tryCount++ < MAX_REQUEST_RETRIES) { - uint16_t port = generateRandomPort(minPort, maxPort); - Mapping map {port, port, type}; + uint16_t port = generateRandomPort(type); + Mapping map(type, port, port); if (mappingList.find(map.getMapKey()) == mappingList.end()) return port; } @@ -355,17 +376,29 @@ UPnPContext::getAvailablePortNumber(PortType type, uint16_t minPort, uint16_t ma } void -UPnPContext::requestMapping(const std::shared_ptr<IGD>& igd, const Mapping::sharedPtr_t& map) +UPnPContext::requestMapping(const Mapping::sharedPtr_t& map) { assert(map); if (not isValidThread()) { - runOnUpnpContextThread([this, igd, map] { requestMapping(igd, map); }); + runOnUpnpContextThread([this, map] { requestMapping(map); }); return; } CHECK_VALID_THREAD(); + 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 %s]", map->toString().c_str(), igd->getProtocolName(), @@ -373,61 +406,32 @@ UPnPContext::requestMapping(const std::shared_ptr<IGD>& igd, const Mapping::shar igd->getLocalIp().toString().c_str()); // Register mapping timeout callback and update the state if needed. - registerAddMappingTimeout(igd, map); + registerAddMappingTimeout(map); if (map->getState() != MappingState::IN_PROGRESS) updateMappingState(map, MappingState::IN_PROGRESS); // Request the mapping. - auto const& protocol = protocolList_.at(igd->getProtocol()); - protocol->requestMappingAdd(igd, *map); -} - -void -UPnPContext::requestMappingOnValidIgds(const Mapping::sharedPtr_t& map) -{ - if (not isValidThread()) { - runOnUpnpContextThread([this, map] { requestMappingOnValidIgds(map); }); - return; - } - - CHECK_VALID_THREAD(); - - if (not isReady()) { - JAMI_WARN("Can not request mapping, no valid IGD available!"); + if (not igd) { + JAMI_ERR("No valid IGD!"); return; } - - // Set to PUPNP by default. - auto proto = protocolList_.at(NatProtocolType::PUPNP); - -#if HAVE_LIBNATPMP - // Prefere NAT-PMP if available. - if (protocolList_.at(NatProtocolType::NAT_PMP)->isReady()) - proto = protocolList_.at(NatProtocolType::NAT_PMP); -#endif - - // Request the mappings - if (proto->isReady()) { - std::list<std::shared_ptr<IGD>> igdList; - proto->getIgdList(igdList); - for (auto const& igd : igdList) - requestMapping(igd, map); - } + auto const& protocol = protocolList_.at(igd->getProtocol()); + protocol->requestMappingAdd(igd, *map); } bool -UPnPContext::provisionNewMappings(PortType type, int portCount, uint16_t minPort, uint16_t maxPort) +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, minPort, maxPort); + auto port = getAvailablePortNumber(type); if (port > 0) { // Found an available port number portCount--; - Mapping map {port, port, type, true}; + Mapping map(type, port, port, true); registerMapping(map); } else { // Very unlikely to get here! @@ -462,7 +466,7 @@ UPnPContext::deleteUnneededMappings(PortType type, int portCount) if (map->getState() == MappingState::OPEN and portCount > 0) { // Close portCount mappings in "OPEN" state. - deleteMapping(map); + requestRemoveMapping(map); it = unregisterMapping(it); portCount--; } else if (map->getState() != MappingState::OPEN) { @@ -479,6 +483,29 @@ UPnPContext::deleteUnneededMappings(PortType type, int portCount) return true; } +std::shared_ptr<IGD> +UPnPContext::getPreferredIgd() const +{ + CHECK_VALID_THREAD(); + + std::shared_ptr<IGD> prefIgd; + for (auto const& [_, protocol] : protocolList_) { + if (protocol->isReady()) { + std::list<std::shared_ptr<IGD>> igdList; + protocol->getIgdList(igdList); + assert(not igdList.empty()); + auto const& igd = igdList.front(); + + // Perefer IGD with the lowest error count, if equal, prefer NAT-PMP. + if (not prefIgd or igd->getErrorsCount() < prefIgd->getErrorsCount() + or protocol->getProtocol() == NatProtocolType::NAT_PMP) { + prefIgd = igd; + } + } + } + return prefIgd; +} + void UPnPContext::updateMappingList(bool async) { @@ -499,19 +526,8 @@ UPnPContext::updateMappingList(bool async) mappingListUpdateTimer_->cancel(); mappingListUpdateTimer_ = getScheduler()->scheduleIn([this] { updateMappingList(false); }, MAP_UPDATE_INTERVAL); - std::shared_ptr<IGD> prefIgd; - for (auto const& [_, protocol] : protocolList_) { - if (protocol->isReady()) { - std::list<std::shared_ptr<IGD>> igdList; - protocol->getIgdList(igdList); - assert(not igdList.empty()); - auto const& igd = igdList.front(); - // Perefer NAT-PMP if available. - if (not prefIgd or protocol->getProtocol() == NatProtocolType::NAT_PMP) - prefIgd = igd; - } - } + std::shared_ptr<IGD> prefIgd = getPreferredIgd(); if (not prefIgd) { JAMI_DBG("UPNP/NAT-PMP enabled, but no valid IGDs available"); @@ -605,25 +621,24 @@ UPnPContext::pruneMappingList() MappingStatus status; getMappingStatus(status); - // Prune the list if there are pending/in-progress requests. - if (status.inProgressCount_ == 0 and status.pendingCount_ == 0) { - std::list<std::shared_ptr<IGD>> igdsList; - - // Performed only with PUPNP protocol. - auto protocol = protocolList_.at(NatProtocolType::PUPNP); - protocol->getIgdList(igdsList); + // Do not prune the list if there are pending/in-progress requests. + if (status.inProgressCount_ != 0 or status.pendingCount_ != 0) { + return; + } - for (auto const& igd : igdsList) { - auto remoteMapList - = protocol->getMappingsListByDescr(igd, Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX); - if (remoteMapList.empty()) - continue; + auto const& igd = getPreferredIgd(); + if (not igd or igd->getProtocol() != NatProtocolType::PUPNP) { + return; + } + auto protocol = protocolList_.at(NatProtocolType::PUPNP); - pruneUnMatchedMappings(igd, remoteMapList); + auto remoteMapList = protocol->getMappingsListByDescr(igd, + Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX); + if (remoteMapList.empty()) + return; - pruneUnTrackedMappings(igd, remoteMapList); - } - } + pruneUnMatchedMappings(igd, remoteMapList); + pruneUnTrackedMappings(igd, remoteMapList); } void @@ -677,7 +692,8 @@ UPnPContext::pruneUnTrackedMappings(const std::shared_ptr<IGD>& igd, std::lock_guard<std::mutex> lock(mappingMutex_); for (auto const& [_, map] : remoteMapList) { - // Must be UPNP protocol. + // 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()); @@ -705,6 +721,8 @@ UPnPContext::pruneUnTrackedMappings(const std::shared_ptr<IGD>& igd, 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; @@ -747,7 +765,7 @@ UPnPContext::processPendingRequests(const std::shared_ptr<IGD>& igd) for (auto type : typeArray) { auto& mappingList = getMappingList(type); - for (auto const& [_, map] : mappingList) { + for (auto& [_, map] : mappingList) { if (map->getState() == MappingState::PENDING) { JAMI_DBG("Send request for pending mapping %s to IGD %s", map->toString().c_str(), @@ -760,7 +778,7 @@ UPnPContext::processPendingRequests(const std::shared_ptr<IGD>& igd) // Process the pending requests. for (auto const& map : requestsList) { - requestMapping(igd, map); + requestMapping(map); } } @@ -787,21 +805,24 @@ UPnPContext::processMappingWithAutoUpdate() } } - for (auto const& map : requestsList) { + 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", - map->toString().c_str()); + oldMap->toString().c_str()); // Reserve a new mapping. - Mapping newMapping(*map); - newMapping.setExternalPort(0); - newMapping.setInternalPort(0); - registerMapping(newMapping); + Mapping newMapping(oldMap->getType()); + auto const& mapPtr = reserveMapping(newMapping); + assert(mapPtr); + mapPtr->setAvailable(false); + mapPtr->enableAutoUpdate(true); + mapPtr->setNotifyCallback(oldMap->getNotifyCallback()); + // Release the old one. - map->setAvailable(true); - map->enableAutoUpdate(false); - map->setNotifyCallback(nullptr); - unregisterMapping(map); + oldMap->setAvailable(true); + oldMap->enableAutoUpdate(false); + oldMap->setNotifyCallback(nullptr); + unregisterMapping(oldMap); } } @@ -817,9 +838,15 @@ UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event) CHECK_VALID_THREAD(); + 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]", igd->getUID().c_str(), protocolName, IgdState); + // Check if IGD has a valid addresses. if (not igdLocalAddr) { JAMI_WARN("[%s] IGD has an invalid local address", protocolName); @@ -839,18 +866,14 @@ UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event) knownPublicAddress_.toString().c_str()); } - char const* IgdState = event == UpnpIgdEvent::ADDED - ? "ADDED" - : event == UpnpIgdEvent::REMOVED ? "REMOVED" : "INVALID"; - - JAMI_WARN("State of IGD %s %s [%s] changed to [%s]", - igd->getUID().c_str(), - igdLocalAddr.toString(true, true).c_str(), - protocolName, - IgdState); - // 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(), + igdLocalAddr.toString(true, true).c_str(), + protocolName, + IgdState); + pruneMappingsWithInvalidIgds(igd); std::lock_guard<std::mutex> lock(mappingMutex_); @@ -902,13 +925,17 @@ UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapR map->cancelTimeoutTimer(); // Update the mapping. - map->updateFrom(mapRes); + map->setState(mapRes.getState()); + map->setIgd(mapRes.getIgd()); + map->setInternalAddress(mapRes.getInternalAddress()); + map->setExternalPort(mapRes.getExternalPort()); // Clean-up if not valid. if (not map->isValid()) { - JAMI_WARN("Mapping request %s on IGD %s failed or returned an invalid mapping!", + JAMI_WARN("Mapping request %s on IGD %s [%s] failed!", map->toString(true).c_str(), - igd->getLocalIp().toString().c_str()); + igd->getLocalIp().toString().c_str(), + igd->getProtocolName()); updateMappingState(map, MappingState::FAILED); unregisterMapping(map); return; @@ -916,6 +943,7 @@ UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapR // Ignore if already open. if (map->getState() == MappingState::OPEN) { + assert(map->getIgd()); JAMI_DBG("Mapping %s is already open on IGD %s [%s]", map->toString().c_str(), map->getIgd()->getLocalIp().toString().c_str(), @@ -964,17 +992,22 @@ UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& ma #endif void -UPnPContext::deleteMapping(const Mapping::sharedPtr_t& map) +UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map) { CHECK_VALID_THREAD(); if (not map) { - JAMI_ERR("Invalid mapping shared pointer"); + JAMI_ERR("Mapping shared pointer is null!"); return; } - if (not map->getIgd() or not map->getIgd()->isValid()) { - JAMI_WARN("Invalid mapping shared pointer"); + if (not map->isValid()) { + JAMI_WARN("Mapping [%s] is invalid! Ignore.", map->toString().c_str()); + return; + } + + if (not map->getIgd()->isValid()) { + JAMI_WARN("Mapping [%s] has an invalid IGD! Ignore.", map->toString().c_str()); return; } @@ -995,15 +1028,9 @@ UPnPContext::deleteAllMappings(PortType type) std::lock_guard<std::mutex> lock(mappingMutex_); auto& mappingList = getMappingList(type); - for (auto it = mappingList.begin(); it != mappingList.end();) { - auto map = it->second; - it = unregisterMapping(it); - auto protocol = protocolList_.at(map->getIgd()->getProtocol()); - protocol->requestMappingRemove(*map); + for (auto const& [_, map] : mappingList) { + requestRemoveMapping(map); } - - getMappingList(PortType::TCP).clear(); - getMappingList(PortType::UDP).clear(); } void @@ -1043,6 +1070,7 @@ UPnPContext::registerMapping(Mapping& map) { 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()); @@ -1057,7 +1085,7 @@ UPnPContext::registerMapping(Mapping& map) if (not isReady()) { JAMI_WARN("No IGD available. Mapping will be requested when an IGD becomes available"); } else { - requestMappingOnValidIgds(mapPtr); + requestMapping(mapPtr); } return mapPtr; @@ -1072,11 +1100,6 @@ UPnPContext::unregisterMapping(std::map<Mapping::key_t, Mapping::sharedPtr_t>::i auto descr = it->second->toString(); auto& mappingList = getMappingList(it->second->getType()); auto ret = mappingList.erase(it); - if (ret != mappingList.end()) { - JAMI_DBG("Unregister mapping %s succeeded", descr.c_str()); - } else { - JAMI_ERR("Failed to unregister mapping %s", descr.c_str()); - } return ret; } @@ -1084,6 +1107,15 @@ UPnPContext::unregisterMapping(std::map<Mapping::key_t, Mapping::sharedPtr_t>::i void UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map) { + CHECK_VALID_THREAD(); + + if (not map) { + JAMI_ERR("Mapping pointer is null"); + return; + } + + map->cancelTimeoutTimer(); + if (map->getAutoUpdate()) { // Dont unregister mappings with auto-update enabled. return; @@ -1158,14 +1190,19 @@ UPnPContext::getMappingStatus(MappingStatus& status) } void -UPnPContext::registerAddMappingTimeout(const std::shared_ptr<IGD>& igd, - const Mapping::sharedPtr_t& map) +UPnPContext::registerAddMappingTimeout(const Mapping::sharedPtr_t& map) { if (not map) { JAMI_ERR("Invalid mapping pointer"); return; } + auto const& igd = map->getIgd(); + if (not igd) { + JAMI_ERR("Invalid igd pointer"); + return; + } + MappingStatus status; getMappingStatus(status); @@ -1177,17 +1214,31 @@ UPnPContext::registerAddMappingTimeout(const std::shared_ptr<IGD>& igd, // requests plus a bias. // This rule is empirical and based on experimenting with few // implementations. - Manager::instance().scheduler().scheduleIn([this, igd, map] { onRequestTimeOut(igd, map); }, - timeout * (status.inProgressCount_ + 3))); + Manager::instance() + .scheduler() + .scheduleIn([this, key = map->getMapKey()] { onRequestTimeOut(key); }, + timeout * (status.inProgressCount_ + 5))); } void -UPnPContext::onRequestTimeOut(const std::shared_ptr<IGD>& igd, const Mapping::sharedPtr_t& map) +UPnPContext::onRequestTimeOut(Mapping::key_t key) { CHECK_VALID_THREAD(); + auto const& map = getMappingWithKey(key); if (not map) { - JAMI_ERR("Invalid mapping pointer"); + JAMI_ERR("Mapping pointer is null"); + return; + } + + auto igd = map->getIgd(); + if (not igd) { + JAMI_ERR("IGD pointer is null"); + return; + } + + if (not getMappingWithKey(map->getMapKey())) { + JAMI_ERR("Mapping [%s] does not exist", map->toString().c_str()); return; } diff --git a/src/upnp/upnp_context.h b/src/upnp/upnp_context.h index 6b6e0feb5d1bc039564eb537c964a15216028cd4..d6879ff8c9930fd56a147cc315fa7b1737127bdb 100644 --- a/src/upnp/upnp_context.h +++ b/src/upnp/upnp_context.h @@ -66,19 +66,7 @@ namespace upnp { class UPnPContext : public UpnpMappingObserver, protected UpnpThreadUtil { -public: - 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; - private: - constexpr static auto NAT_MAP_REQUEST_TIMEOUT_UNIT = std::chrono::seconds(1); - constexpr static auto PUPNP_MAP_REQUEST_TIMEOUT_UNIT = std::chrono::seconds(5); - 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; - struct MappingStatus { int openCount_ {0}; @@ -129,15 +117,27 @@ public: void unregisterController(void* controller); // Generate random port numbers - static uint16_t generateRandomPort(uint16_t min, uint16_t max, bool mustBeEven = false); + static uint16_t generateRandomPort(PortType type, bool mustBeEven = false); private: // Initialization void init(); - // Start/Stop - void StartUpnp(); - void StopUpnp(); + /** + * @brief start the search for IGDs activate the mapping + * list update. + * + */ + void startUpnp(); + + /** + * @brief Clear all IGDs and release/delete current mappings + * + * @param forceRelease If true, also delete mappings with enabled + * auto-update feature. + * + */ + void stopUpnp(bool forceRelease = false); // Create and register a new mapping. Mapping::sharedPtr_t registerMapping(Mapping& map); @@ -148,22 +148,19 @@ private: void unregisterMapping(const Mapping::sharedPtr_t& map); // Perform the request on the provided IGD. - void requestMapping(const std::shared_ptr<IGD>& igd, const Mapping::sharedPtr_t& map); - - // Perform the request on all available IGDs - void requestMappingOnValidIgds(const Mapping::sharedPtr_t& map); + void requestMapping(const Mapping::sharedPtr_t& map); - // Delete mapping from the list and and send remove request. - void deleteMapping(const Mapping::sharedPtr_t& map); + // Request a mapping remove from the IGD. + void requestRemoveMapping(const Mapping::sharedPtr_t& map); // Remove all mappings of the given type. void deleteAllMappings(PortType type); // Schedule a time-out timer for a in-progress request. - void registerAddMappingTimeout(const std::shared_ptr<IGD>& igd, const Mapping::sharedPtr_t& map); + void registerAddMappingTimeout(const Mapping::sharedPtr_t& map); // Callback invoked when a request times-out - void onRequestTimeOut(const std::shared_ptr<IGD>& igd, const Mapping::sharedPtr_t& map); + void onRequestTimeOut(Mapping::key_t key); // Update the state and notify the listener void updateMappingState(const Mapping::sharedPtr_t& map, @@ -171,16 +168,16 @@ private: bool notify = true); // Provision ports. - uint16_t getAvailablePortNumber(PortType type, uint16_t minPort = 0, uint16_t maxPort = 0); + uint16_t getAvailablePortNumber(PortType type); + + // Get preferred IGD + std::shared_ptr<IGD> getPreferredIgd() const; // Check and prune the mapping list. Called periodically. void updateMappingList(bool async); // Provision (pre-allocate) the requested number of mappings. - bool provisionNewMappings(PortType type, - int portCount, - uint16_t minPort = 0, - uint16_t maxPort = 0); + bool provisionNewMappings(PortType type, int portCount); // Close unused mappings. bool deleteUnneededMappings(PortType type, int portCount); @@ -211,8 +208,15 @@ private: void pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd); - // Get the mapping list + /** + * @brief Get the mapping list + * + * @param type transport type (TCP/UDP) + * @return a reference on the map + * @warning concurrency protection done by the caller + */ std::map<Mapping::key_t, Mapping::sharedPtr_t>& getMappingList(PortType type); + // Get the mapping from the key. Mapping::sharedPtr_t getMappingWithKey(Mapping::key_t key); diff --git a/src/upnp/upnp_control.cpp b/src/upnp/upnp_control.cpp index 5de901b3a0604a9dafe1c87b55750d873ff2cb78..702da5ff0fe8e6b1a9b4a56fa9be8ed4a9fefd25 100644 --- a/src/upnp/upnp_control.cpp +++ b/src/upnp/upnp_control.cpp @@ -76,7 +76,7 @@ Controller::getExternalIP() const Mapping::sharedPtr_t Controller::reserveMapping(uint16_t port, PortType type) { - Mapping map {port, port, type}; + Mapping map(type, port, port); return reserveMapping(map); } @@ -142,12 +142,7 @@ Controller::removeLocalMap(const Mapping& map) uint16_t Controller::generateRandomPort(PortType type) { - if (type == PortType::TCP) - return UPnPContext::generateRandomPort(UPnPContext::UPNP_TCP_PORT_MIN, - UPnPContext::UPNP_TCP_PORT_MAX); - else - return UPnPContext::generateRandomPort(UPnPContext::UPNP_UDP_PORT_MIN, - UPnPContext::UPNP_UDP_PORT_MAX); + return UPnPContext::generateRandomPort(type); } } // namespace upnp diff --git a/src/upnp/upnp_thread_util.h b/src/upnp/upnp_thread_util.h index c1f66fc1953d597b5fef15c6d6c4cdaaec132cea..1fcc3c9ceee5f261bffd321c7821490bcddb9018 100644 --- a/src/upnp/upnp_thread_util.h +++ b/src/upnp/upnp_thread_util.h @@ -10,7 +10,7 @@ #define CHECK_VALID_THREAD() \ if (not isValidThread()) \ JAMI_ERR() << "The calling thread " << getCurrentThread() \ - << "is not the expected thread: " << threadId_; + << " is not the expected thread: " << threadId_; namespace jami { namespace upnp {