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