add initial project structure
Change-Id: I6a3fb080ff623b312e42d71754480a7ce00b81a0
diff --git a/src/upnp/protocol/natpmp/nat_pmp.cpp b/src/upnp/protocol/natpmp/nat_pmp.cpp
new file mode 100644
index 0000000..21f11ee
--- /dev/null
+++ b/src/upnp/protocol/natpmp/nat_pmp.cpp
@@ -0,0 +1,775 @@
+/*
+ * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
+ *
+ * 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 "nat_pmp.h"
+
+#if HAVE_LIBNATPMP
+
+namespace jami {
+namespace upnp {
+
+NatPmp::NatPmp()
+{
+ JAMI_DBG("NAT-PMP: Instance [%p] created", this);
+ runOnNatPmpQueue([this] {
+ threadId_ = getCurrentThread();
+ igd_ = std::make_shared<PMPIGD>();
+ });
+}
+
+NatPmp::~NatPmp()
+{
+ JAMI_DBG("NAT-PMP: Instance [%p] destroyed", this);
+}
+
+void
+NatPmp::initNatPmp()
+{
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak()] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->initNatPmp();
+ }
+ });
+ return;
+ }
+
+ initialized_ = false;
+
+ {
+ std::lock_guard<std::mutex> lock(natpmpMutex_);
+ hostAddress_ = ip_utils::getLocalAddr(AF_INET);
+ }
+
+ // Local address must be valid.
+ if (not getHostAddress() or getHostAddress().isLoopback()) {
+ JAMI_WARN("NAT-PMP: Does not have a valid local address!");
+ return;
+ }
+
+ assert(igd_);
+ if (igd_->isValid()) {
+ igd_->setValid(false);
+ processIgdUpdate(UpnpIgdEvent::REMOVED);
+ }
+
+ igd_->setLocalIp(IpAddr());
+ igd_->setPublicIp(IpAddr());
+ igd_->setUID("");
+
+ JAMI_DBG("NAT-PMP: Trying to initialize IGD");
+
+ int err = initnatpmp(&natpmpHdl_, 0, 0);
+
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Initializing IGD using default gateway failed!");
+ const auto& localGw = ip_utils::getLocalGateway();
+ if (not localGw) {
+ JAMI_WARN("NAT-PMP: Couldn't find valid gateway on local host");
+ err = NATPMP_ERR_CANNOTGETGATEWAY;
+ } else {
+ JAMI_WARN("NAT-PMP: Trying to initialize using detected gateway %s",
+ localGw.toString().c_str());
+
+ struct in_addr inaddr;
+ inet_pton(AF_INET, localGw.toString().c_str(), &inaddr);
+ err = initnatpmp(&natpmpHdl_, 1, inaddr.s_addr);
+ }
+ }
+
+ if (err < 0) {
+ JAMI_ERR("NAT-PMP: Can't initialize libnatpmp -> %s", getNatPmpErrorStr(err));
+ return;
+ }
+
+ char addrbuf[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &natpmpHdl_.gateway, addrbuf, sizeof(addrbuf));
+ IpAddr igdAddr(addrbuf);
+ JAMI_DBG("NAT-PMP: Initialized on gateway %s", igdAddr.toString().c_str());
+
+ // Set the local (gateway) address.
+ igd_->setLocalIp(igdAddr);
+ // NAT-PMP protocol does not have UID, but we will set generic
+ // one debugging purposes.
+ igd_->setUID("NAT-PMP Gateway");
+
+ // Search and set the public address.
+ getIgdPublicAddress();
+
+ // Update and notify.
+ if (igd_->isValid()) {
+ initialized_ = true;
+ processIgdUpdate(UpnpIgdEvent::ADDED);
+ };
+}
+
+void
+NatPmp::setObserver(UpnpMappingObserver* obs)
+{
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak(), obs] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->setObserver(obs);
+ }
+ });
+ return;
+ }
+
+ JAMI_DBG("NAT-PMP: Setting observer to %p", obs);
+
+ observer_ = obs;
+}
+
+void
+NatPmp::terminate(std::condition_variable& cv)
+{
+ initialized_ = false;
+ observer_ = nullptr;
+
+ {
+ std::lock_guard<std::mutex> lock(natpmpMutex_);
+ shutdownComplete_ = true;
+ cv.notify_one();
+ }
+}
+
+void
+NatPmp::terminate()
+{
+ std::unique_lock<std::mutex> lk(natpmpMutex_);
+ std::condition_variable cv {};
+
+ runOnNatPmpQueue([w = weak(), &cv = cv] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->terminate(cv);
+ }
+ });
+
+ if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
+ JAMI_DBG("NAT-PMP: Shutdown completed");
+ } else {
+ JAMI_ERR("NAT-PMP: Shutdown timed-out");
+ }
+}
+
+const IpAddr
+NatPmp::getHostAddress() const
+{
+ std::lock_guard<std::mutex> lock(natpmpMutex_);
+ return hostAddress_;
+}
+
+void
+NatPmp::clearIgds()
+{
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak()] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->clearIgds();
+ }
+ });
+ return;
+ }
+
+ bool do_close = false;
+
+ if (igd_) {
+ if (igd_->isValid()) {
+ do_close = true;
+ }
+ igd_->setValid(false);
+ }
+
+ initialized_ = false;
+ if (searchForIgdTimer_)
+ searchForIgdTimer_->cancel();
+
+ igdSearchCounter_ = 0;
+
+ if (do_close) {
+ closenatpmp(&natpmpHdl_);
+ memset(&natpmpHdl_, 0, sizeof(natpmpHdl_));
+ }
+}
+
+void
+NatPmp::searchForIgd()
+{
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak()] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->searchForIgd();
+ }
+ });
+ return;
+ }
+
+ if (not initialized_) {
+ initNatPmp();
+ }
+
+ // Schedule a retry in case init failed.
+ if (not initialized_) {
+ if (igdSearchCounter_++ < MAX_RESTART_SEARCH_RETRIES) {
+ JAMI_DBG("NAT-PMP: Start search for IGDs. Attempt %i", igdSearchCounter_);
+
+ // Cancel the current timer (if any) and re-schedule.
+ if (searchForIgdTimer_)
+ searchForIgdTimer_->cancel();
+
+ searchForIgdTimer_ = getNatpmpScheduler()->scheduleIn([this] { searchForIgd(); },
+ NATPMP_SEARCH_RETRY_UNIT
+ * igdSearchCounter_);
+ } else {
+ JAMI_WARN("NAT-PMP: Setup failed after %u trials. NAT-PMP will be disabled!",
+ MAX_RESTART_SEARCH_RETRIES);
+ }
+ }
+}
+
+std::list<std::shared_ptr<IGD>>
+NatPmp::getIgdList() const
+{
+ std::lock_guard<std::mutex> lock(natpmpMutex_);
+ std::list<std::shared_ptr<IGD>> igdList;
+ if (igd_->isValid())
+ igdList.emplace_back(igd_);
+ return igdList;
+}
+
+bool
+NatPmp::isReady() const
+{
+ if (observer_ == nullptr) {
+ JAMI_ERR("NAT-PMP: the observer is not set!");
+ return false;
+ }
+
+ // Must at least have a valid local address.
+ if (not getHostAddress() or getHostAddress().isLoopback())
+ return false;
+
+ return igd_ and igd_->isValid();
+}
+
+void
+NatPmp::incrementErrorsCounter(const std::shared_ptr<IGD>& igdIn)
+{
+ if (not validIgdInstance(igdIn)) {
+ return;
+ }
+
+ if (not igd_->isValid()) {
+ // Already invalid. Nothing to do.
+ return;
+ }
+
+ if (not igd_->incrementErrorsCounter()) {
+ // Disable this IGD.
+ igd_->setValid(false);
+ // Notify the listener.
+ JAMI_WARN("NAT-PMP: No more valid IGD!");
+
+ processIgdUpdate(UpnpIgdEvent::INVALID_STATE);
+ }
+}
+
+void
+NatPmp::requestMappingAdd(const Mapping& mapping)
+{
+ // Process on nat-pmp thread.
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak(), mapping] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->requestMappingAdd(mapping);
+ }
+ });
+ return;
+ }
+
+ Mapping map(mapping);
+ assert(map.getIgd());
+ auto err = addPortMapping(map);
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Request for mapping %s on %s failed with error %i: %s",
+ map.toString().c_str(),
+ igd_->toString().c_str(),
+ err,
+ getNatPmpErrorStr(err));
+
+ if (isErrorFatal(err)) {
+ // Fatal error, increment the counter.
+ incrementErrorsCounter(igd_);
+ }
+ // Notify the listener.
+ processMappingRequestFailed(std::move(map));
+ } else {
+ JAMI_DBG("NAT-PMP: Request for mapping %s on %s succeeded",
+ map.toString().c_str(),
+ igd_->toString().c_str());
+ // Notify the listener.
+ processMappingAdded(std::move(map));
+ }
+}
+
+void
+NatPmp::requestMappingRenew(const Mapping& mapping)
+{
+ // Process on nat-pmp thread.
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak(), mapping] {
+ if (auto pmpThis = w.lock()) {
+ pmpThis->requestMappingRenew(mapping);
+ }
+ });
+ return;
+ }
+
+ Mapping map(mapping);
+ auto err = addPortMapping(map);
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Renewal request for mapping %s on %s failed with error %i: %s",
+ map.toString().c_str(),
+ igd_->toString().c_str(),
+ err,
+ getNatPmpErrorStr(err));
+ // Notify the listener.
+ processMappingRequestFailed(std::move(map));
+
+ if (isErrorFatal(err)) {
+ // Fatal error, increment the counter.
+ incrementErrorsCounter(igd_);
+ }
+ } else {
+ JAMI_DBG("NAT-PMP: Renewal request for mapping %s on %s succeeded",
+ map.toString().c_str(),
+ igd_->toString().c_str());
+ // Notify the listener.
+ processMappingRenewed(map);
+ }
+}
+
+int
+NatPmp::readResponse(natpmp_t& handle, natpmpresp_t& response)
+{
+ int err = 0;
+ unsigned readRetriesCounter = 0;
+
+ while (true) {
+ if (readRetriesCounter++ > MAX_READ_RETRIES) {
+ err = NATPMP_ERR_SOCKETERROR;
+ break;
+ }
+
+ fd_set fds;
+ struct timeval timeout;
+ FD_ZERO(&fds);
+ FD_SET(handle.s, &fds);
+ getnatpmprequesttimeout(&handle, &timeout);
+ // Wait for data.
+ if (select(FD_SETSIZE, &fds, NULL, NULL, &timeout) == -1) {
+ err = NATPMP_ERR_SOCKETERROR;
+ break;
+ }
+
+ // Read the data.
+ err = readnatpmpresponseorretry(&handle, &response);
+
+ if (err == NATPMP_TRYAGAIN) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT_BEFORE_READ_RETRY));
+ } else {
+ break;
+ }
+ }
+
+ return err;
+}
+
+int
+NatPmp::sendMappingRequest(const Mapping& mapping, uint32_t& lifetime)
+{
+ CHECK_VALID_THREAD();
+
+ int err = sendnewportmappingrequest(&natpmpHdl_,
+ mapping.getType() == PortType::UDP ? NATPMP_PROTOCOL_UDP
+ : NATPMP_PROTOCOL_TCP,
+ mapping.getInternalPort(),
+ mapping.getExternalPort(),
+ lifetime);
+
+ if (err < 0) {
+ JAMI_ERR("NAT-PMP: Send mapping request failed with error %s %i",
+ getNatPmpErrorStr(err),
+ errno);
+ return err;
+ }
+
+ unsigned readRetriesCounter = 0;
+
+ while (readRetriesCounter++ < MAX_READ_RETRIES) {
+ // Read the response
+ natpmpresp_t response;
+ err = readResponse(natpmpHdl_, response);
+
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Read response on IGD %s failed with error %s",
+ igd_->toString().c_str(),
+ getNatPmpErrorStr(err));
+ } else if (response.type != NATPMP_RESPTYPE_TCPPORTMAPPING
+ and response.type != NATPMP_RESPTYPE_UDPPORTMAPPING) {
+ JAMI_ERR("NAT-PMP: Unexpected response type (%i) for mapping %s from IGD %s.",
+ response.type,
+ mapping.toString().c_str(),
+ igd_->toString().c_str());
+ // Try to read again.
+ continue;
+ }
+
+ lifetime = response.pnu.newportmapping.lifetime;
+ // Done.
+ break;
+ }
+
+ return err;
+}
+
+int
+NatPmp::addPortMapping(Mapping& mapping)
+{
+ auto const& igdIn = mapping.getIgd();
+ assert(igdIn);
+ assert(igdIn->getProtocol() == NatProtocolType::NAT_PMP);
+
+ if (not igdIn->isValid() or not validIgdInstance(igdIn)) {
+ mapping.setState(MappingState::FAILED);
+ return NATPMP_ERR_INVALIDARGS;
+ }
+
+ mapping.setInternalAddress(getHostAddress().toString());
+
+ uint32_t lifetime = MAPPING_ALLOCATION_LIFETIME;
+ int err = sendMappingRequest(mapping, lifetime);
+
+ if (err < 0) {
+ mapping.setState(MappingState::FAILED);
+ return err;
+ }
+
+ // Set the renewal time and update.
+ mapping.setRenewalTime(sys_clock::now() + std::chrono::seconds(lifetime * 4 / 5));
+ mapping.setState(MappingState::OPEN);
+
+ return 0;
+}
+
+void
+NatPmp::requestMappingRemove(const Mapping& mapping)
+{
+ // Process on nat-pmp thread.
+ if (not isValidThread()) {
+ runOnNatPmpQueue([w = weak(), mapping] {
+ if (auto pmpThis = w.lock()) {
+ Mapping map {mapping};
+ pmpThis->removePortMapping(map);
+ }
+ });
+ return;
+ }
+}
+
+void
+NatPmp::removePortMapping(Mapping& mapping)
+{
+ auto igdIn = mapping.getIgd();
+ assert(igdIn);
+ if (not igdIn->isValid()) {
+ return;
+ }
+
+ if (not validIgdInstance(igdIn)) {
+ return;
+ }
+
+ Mapping mapToRemove(mapping);
+
+ uint32_t lifetime = 0;
+ int err = sendMappingRequest(mapping, lifetime);
+
+ if (err < 0) {
+ // Nothing to do if the request fails, just log the error.
+ JAMI_WARN("NAT-PMP: Send remove request failed with error %s. Ignoring",
+ getNatPmpErrorStr(err));
+ }
+
+ // Update and notify the listener.
+ mapToRemove.setState(MappingState::FAILED);
+ processMappingRemoved(std::move(mapToRemove));
+}
+
+void
+NatPmp::getIgdPublicAddress()
+{
+ CHECK_VALID_THREAD();
+
+ // Set the public address for this IGD if it does not
+ // have one already.
+ if (igd_->getPublicIp()) {
+ JAMI_WARN("NAT-PMP: IGD %s already have a public address (%s)",
+ igd_->toString().c_str(),
+ igd_->getPublicIp().toString().c_str());
+ return;
+ }
+ assert(igd_->getProtocol() == NatProtocolType::NAT_PMP);
+
+ int err = sendpublicaddressrequest(&natpmpHdl_);
+
+ if (err < 0) {
+ JAMI_ERR("NAT-PMP: send public address request on IGD %s failed with error: %s",
+ igd_->toString().c_str(),
+ getNatPmpErrorStr(err));
+
+ if (isErrorFatal(err)) {
+ // Fatal error, increment the counter.
+ incrementErrorsCounter(igd_);
+ }
+ return;
+ }
+
+ natpmpresp_t response;
+ err = readResponse(natpmpHdl_, response);
+
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Read response on IGD %s failed - %s",
+ igd_->toString().c_str(),
+ getNatPmpErrorStr(err));
+ return;
+ }
+
+ if (response.type != NATPMP_RESPTYPE_PUBLICADDRESS) {
+ JAMI_ERR("NAT-PMP: Unexpected response type (%i) for public address request from IGD %s.",
+ response.type,
+ igd_->toString().c_str());
+ return;
+ }
+
+ IpAddr publicAddr(response.pnu.publicaddress.addr);
+
+ if (not publicAddr) {
+ JAMI_ERR("NAT-PMP: IGD %s returned an invalid public address %s",
+ igd_->toString().c_str(),
+ publicAddr.toString().c_str());
+ }
+
+ // Update.
+ igd_->setPublicIp(publicAddr);
+ igd_->setValid(true);
+
+ JAMI_DBG("NAT-PMP: Setting IGD %s public address to %s",
+ igd_->toString().c_str(),
+ igd_->getPublicIp().toString().c_str());
+}
+
+void
+NatPmp::removeAllMappings()
+{
+ CHECK_VALID_THREAD();
+
+ JAMI_WARN("NAT-PMP: Send request to close all existing mappings to IGD %s",
+ igd_->toString().c_str());
+
+ int err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_TCP, 0, 0, 0);
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Send close all TCP mappings request failed with error %s",
+ getNatPmpErrorStr(err));
+ }
+ err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_UDP, 0, 0, 0);
+ if (err < 0) {
+ JAMI_WARN("NAT-PMP: Send close all UDP mappings request failed with error %s",
+ getNatPmpErrorStr(err));
+ }
+}
+
+const char*
+NatPmp::getNatPmpErrorStr(int errorCode) const
+{
+#ifdef ENABLE_STRNATPMPERR
+ return strnatpmperr(errorCode);
+#else
+ switch (errorCode) {
+ case NATPMP_ERR_INVALIDARGS:
+ return "INVALIDARGS";
+ break;
+ case NATPMP_ERR_SOCKETERROR:
+ return "SOCKETERROR";
+ break;
+ case NATPMP_ERR_CANNOTGETGATEWAY:
+ return "CANNOTGETGATEWAY";
+ break;
+ case NATPMP_ERR_CLOSEERR:
+ return "CLOSEERR";
+ break;
+ case NATPMP_ERR_RECVFROM:
+ return "RECVFROM";
+ break;
+ case NATPMP_ERR_NOPENDINGREQ:
+ return "NOPENDINGREQ";
+ break;
+ case NATPMP_ERR_NOGATEWAYSUPPORT:
+ return "NOGATEWAYSUPPORT";
+ break;
+ case NATPMP_ERR_CONNECTERR:
+ return "CONNECTERR";
+ break;
+ case NATPMP_ERR_WRONGPACKETSOURCE:
+ return "WRONGPACKETSOURCE";
+ break;
+ case NATPMP_ERR_SENDERR:
+ return "SENDERR";
+ break;
+ case NATPMP_ERR_FCNTLERROR:
+ return "FCNTLERROR";
+ break;
+ case NATPMP_ERR_GETTIMEOFDAYERR:
+ return "GETTIMEOFDAYERR";
+ break;
+ case NATPMP_ERR_UNSUPPORTEDVERSION:
+ return "UNSUPPORTEDVERSION";
+ break;
+ case NATPMP_ERR_UNSUPPORTEDOPCODE:
+ return "UNSUPPORTEDOPCODE";
+ break;
+ case NATPMP_ERR_UNDEFINEDERROR:
+ return "UNDEFINEDERROR";
+ break;
+ case NATPMP_ERR_NOTAUTHORIZED:
+ return "NOTAUTHORIZED";
+ break;
+ case NATPMP_ERR_NETWORKFAILURE:
+ return "NETWORKFAILURE";
+ break;
+ case NATPMP_ERR_OUTOFRESOURCES:
+ return "OUTOFRESOURCES";
+ break;
+ case NATPMP_TRYAGAIN:
+ return "TRYAGAIN";
+ break;
+ default:
+ return "UNKNOWNERR";
+ break;
+ }
+#endif
+}
+
+bool
+NatPmp::isErrorFatal(int error)
+{
+ switch (error) {
+ case NATPMP_ERR_INVALIDARGS:
+ case NATPMP_ERR_SOCKETERROR:
+ case NATPMP_ERR_CANNOTGETGATEWAY:
+ case NATPMP_ERR_CLOSEERR:
+ case NATPMP_ERR_RECVFROM:
+ case NATPMP_ERR_NOGATEWAYSUPPORT:
+ case NATPMP_ERR_CONNECTERR:
+ case NATPMP_ERR_SENDERR:
+ case NATPMP_ERR_UNDEFINEDERROR:
+ case NATPMP_ERR_UNSUPPORTEDVERSION:
+ case NATPMP_ERR_UNSUPPORTEDOPCODE:
+ case NATPMP_ERR_NOTAUTHORIZED:
+ case NATPMP_ERR_NETWORKFAILURE:
+ case NATPMP_ERR_OUTOFRESOURCES:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool
+NatPmp::validIgdInstance(const std::shared_ptr<IGD>& igdIn)
+{
+ if (igd_.get() != igdIn.get()) {
+ JAMI_ERR("NAT-PMP: IGD (%s) does not match local instance (%s)",
+ igdIn->toString().c_str(),
+ igd_->toString().c_str());
+ return false;
+ }
+
+ return true;
+}
+
+void
+NatPmp::processIgdUpdate(UpnpIgdEvent event)
+{
+ if (igd_->isValid()) {
+ // Remove all current mappings if any.
+ removeAllMappings();
+ }
+
+ if (observer_ == nullptr)
+ return;
+ // Process the response on the context thread.
+ runOnUpnpContextQueue([obs = observer_, igd = igd_, event] { obs->onIgdUpdated(igd, event); });
+}
+
+void
+NatPmp::processMappingAdded(const Mapping& map)
+{
+ if (observer_ == nullptr)
+ return;
+
+ // Process the response on the context thread.
+ runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingAdded(igd, map); });
+}
+
+void
+NatPmp::processMappingRequestFailed(const Mapping& map)
+{
+ if (observer_ == nullptr)
+ return;
+
+ // Process the response on the context thread.
+ runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRequestFailed(map); });
+}
+
+void
+NatPmp::processMappingRenewed(const Mapping& map)
+{
+ if (observer_ == nullptr)
+ return;
+
+ // Process the response on the context thread.
+ runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRenewed(igd, map); });
+}
+
+void
+NatPmp::processMappingRemoved(const Mapping& map)
+{
+ if (observer_ == nullptr)
+ return;
+
+ // Process the response on the context thread.
+ runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRemoved(igd, map); });
+}
+
+} // namespace upnp
+} // namespace jami
+
+#endif //-- #if HAVE_LIBNATPMP
diff --git a/src/upnp/protocol/natpmp/nat_pmp.h b/src/upnp/protocol/natpmp/nat_pmp.h
new file mode 100644
index 0000000..68fd28b
--- /dev/null
+++ b/src/upnp/protocol/natpmp/nat_pmp.h
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include "connectivity/upnp/protocol/upnp_protocol.h"
+#include "connectivity/upnp/protocol/igd.h"
+#include "pmp_igd.h"
+
+#include "logger.h"
+#include "connectivity/ip_utils.h"
+#include "noncopyable.h"
+#include "compiler_intrinsics.h"
+
+// uncomment to enable native natpmp error messages
+//#define ENABLE_STRNATPMPERR 1
+#include <natpmp.h>
+
+#include <atomic>
+#include <thread>
+
+namespace jami {
+class IpAddr;
+}
+
+namespace jami {
+namespace upnp {
+
+// Requested lifetime in seconds. The actual lifetime might be different.
+constexpr static unsigned int MAPPING_ALLOCATION_LIFETIME {60 * 60};
+// Max number of IGD search attempts before failure.
+constexpr static unsigned int MAX_RESTART_SEARCH_RETRIES {3};
+// Time-out between two successive read response.
+constexpr static auto TIMEOUT_BEFORE_READ_RETRY {std::chrono::milliseconds(300)};
+// Max number of read attempts before failure.
+constexpr static unsigned int MAX_READ_RETRIES {3};
+// Base unit for the timeout between two successive IGD search.
+constexpr static auto NATPMP_SEARCH_RETRY_UNIT {std::chrono::seconds(10)};
+
+class NatPmp : public UPnPProtocol
+{
+public:
+ NatPmp();
+ ~NatPmp();
+
+ // Set the observer.
+ void setObserver(UpnpMappingObserver* obs) override;
+
+ // Returns the protocol type.
+ NatProtocolType getProtocol() const override { return NatProtocolType::NAT_PMP; }
+
+ // Get protocol type as string.
+ char const* getProtocolName() const override { return "NAT-PMP"; }
+
+ // Notifies a change in network.
+ void clearIgds() override;
+
+ // Renew pmp_igd.
+ void searchForIgd() override;
+
+ // Get the IGD list.
+ std::list<std::shared_ptr<IGD>> getIgdList() const override;
+
+ // Return true if it has at least one valid IGD.
+ bool isReady() const override;
+
+ // Request a new mapping.
+ void requestMappingAdd(const Mapping& mapping) override;
+
+ // Renew an allocated mapping.
+ void requestMappingRenew(const Mapping& mapping) override;
+
+ // Removes a mapping.
+ void requestMappingRemove(const Mapping& mapping) override;
+
+ // Get the host (local) address.
+ const IpAddr getHostAddress() const override;
+
+ // Terminate. Nothing to do here, the clean-up is done when
+ // the IGD is cleared.
+ void terminate() override;
+
+private:
+ NON_COPYABLE(NatPmp);
+
+ std::weak_ptr<NatPmp> weak() { return std::static_pointer_cast<NatPmp>(shared_from_this()); }
+
+ // Helpers to run tasks on NAT-PMP internal execution queue.
+ ScheduledExecutor* getNatpmpScheduler() { return &natpmpScheduler_; }
+ template<typename Callback>
+ void runOnNatPmpQueue(Callback&& cb)
+ {
+ natpmpScheduler_.run([cb = std::forward<Callback>(cb)]() mutable { cb(); });
+ }
+
+ // Helpers to run tasks on UPNP context execution queue.
+ ScheduledExecutor* getUpnContextScheduler() { return UpnpThreadUtil::getScheduler(); }
+
+ void terminate(std::condition_variable& cv);
+
+ void initNatPmp();
+ void getIgdPublicAddress();
+ void removeAllMappings();
+ int readResponse(natpmp_t& handle, natpmpresp_t& response);
+ int sendMappingRequest(const Mapping& mapping, uint32_t& lifetime);
+
+ // Adds a port mapping.
+ int addPortMapping(Mapping& mapping);
+ // Removes a port mapping.
+ void removePortMapping(Mapping& mapping);
+
+ // True if the error is fatal.
+ bool isErrorFatal(int error);
+ // Gets NAT-PMP error code string.
+ const char* getNatPmpErrorStr(int errorCode) const;
+ // Get local getaway.
+ std::unique_ptr<IpAddr> getLocalGateway() const;
+
+ // Helpers to process user's callbacks
+ void processIgdUpdate(UpnpIgdEvent event);
+ void processMappingAdded(const Mapping& map);
+ void processMappingRequestFailed(const Mapping& map);
+ void processMappingRenewed(const Mapping& map);
+ void processMappingRemoved(const Mapping& map);
+
+ // Check if the IGD has a local match
+ bool validIgdInstance(const std::shared_ptr<IGD>& igdIn);
+
+ // Increment errors counter.
+ void incrementErrorsCounter(const std::shared_ptr<IGD>& igd);
+
+ std::atomic_bool initialized_ {false};
+
+ // Data members
+ std::shared_ptr<PMPIGD> igd_;
+ natpmp_t natpmpHdl_;
+ ScheduledExecutor natpmpScheduler_ {"natpmp"};
+ std::shared_ptr<Task> searchForIgdTimer_ {};
+ unsigned int igdSearchCounter_ {0};
+ UpnpMappingObserver* observer_ {nullptr};
+ IpAddr hostAddress_ {};
+
+ // Calls from other threads that does not need synchronous access are
+ // rescheduled on the NatPmp private queue. This will avoid the need to
+ // protect most of the data members of this class.
+ // For some internal members (such as the igd instance and the host
+ // address) that need to be synchronously accessed, are protected by
+ // this mutex.
+ mutable std::mutex natpmpMutex_;
+
+ // Shutdown synchronization
+ bool shutdownComplete_ {false};
+};
+
+} // namespace upnp
+} // namespace jami
diff --git a/src/upnp/protocol/natpmp/pmp_igd.cpp b/src/upnp/protocol/natpmp/pmp_igd.cpp
new file mode 100644
index 0000000..ac8b698
--- /dev/null
+++ b/src/upnp/protocol/natpmp/pmp_igd.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "pmp_igd.h"
+
+#include <algorithm>
+
+namespace jami {
+namespace upnp {
+
+PMPIGD::PMPIGD()
+ : IGD(NatProtocolType::NAT_PMP)
+{}
+
+PMPIGD::PMPIGD(const PMPIGD& other)
+ : PMPIGD()
+{
+ assert(protocol_ == NatProtocolType::NAT_PMP);
+ // protocol_ = other.protocol_;
+ localIp_ = other.localIp_;
+ publicIp_ = other.publicIp_;
+ uid_ = other.uid_;
+}
+
+bool
+PMPIGD::operator==(IGD& other) const
+{
+ return getPublicIp() == other.getPublicIp() and getLocalIp() == other.getLocalIp();
+}
+
+bool
+PMPIGD::operator==(PMPIGD& other) const
+{
+ return getPublicIp() == other.getPublicIp() and getLocalIp() == other.getLocalIp();
+}
+
+const std::string
+PMPIGD::toString() const
+{
+ return getLocalIp().toString();
+}
+
+} // namespace upnp
+} // namespace jami
diff --git a/src/upnp/protocol/natpmp/pmp_igd.h b/src/upnp/protocol/natpmp/pmp_igd.h
new file mode 100644
index 0000000..a70e7ee
--- /dev/null
+++ b/src/upnp/protocol/natpmp/pmp_igd.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include "../igd.h"
+#include "noncopyable.h"
+#include "connectivity/ip_utils.h"
+
+#include <map>
+#include <atomic>
+#include <string>
+#include <chrono>
+#include <functional>
+
+namespace jami {
+namespace upnp {
+
+class PMPIGD : public IGD
+{
+public:
+ PMPIGD();
+ PMPIGD(const PMPIGD&);
+ ~PMPIGD() = default;
+
+ PMPIGD& operator=(PMPIGD&& other) = delete;
+ PMPIGD& operator=(PMPIGD& other) = delete;
+
+ bool operator==(IGD& other) const;
+ bool operator==(PMPIGD& other) const;
+
+ const std::string toString() const override;
+};
+
+} // namespace upnp
+} // namespace jami