diff --git a/configure.ac b/configure.ac
index 10e7970da289b3ec8e2d07ff5df498aef1c881fb..ab744a105d252114acb1495a5c790809d201991f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -531,6 +531,20 @@ AS_IF([test "x$with_upnp" = "xyes"],
          AC_DEFINE([HAVE_LIBUPNP], 0, [Define if you have libupnp])])
       ])
 
+# LIBNATPMP
+dnl check for libnatpmp
+AC_ARG_WITH([natpmp], [AS_HELP_STRING([--without-natpmp],
+            [disable support for NAT-PMP])], [], [with_natpmp=yes])
+
+AS_IF([test "x$with_natpmp" != xno],
+        [AC_CHECK_HEADER([natpmp.h],
+            AC_CHECK_LIB([natpmp], [initnatpmp], [], [with_natpmp=no]),
+            [AC_MSG_WARN([Unable to find the libnatpmp headers (you may need to install the dev package). You may use --without-natpmp to compile without NAT-PMP support.]);
+             with_natpmp=no])
+        ],[])
+
+AC_DEFINE_UNQUOTED([HAVE_LIBNATPMP], `if test "x$with_natpmp" != xno; then echo 1; else echo 0; fi`, [Define if you have libnatpmp])
+
 AC_DEFINE_UNQUOTED([HAVE_SHM], `if test -z "${HAVE_LINUX_TRUE}" && test -z "${HAVE_ANDROID_FALSE}" ; then echo 1; else echo 0; fi`,
 	[Define if you have shared memory support])
 
diff --git a/contrib/src/natpmp/SHA512SUMS b/contrib/src/natpmp/SHA512SUMS
new file mode 100644
index 0000000000000000000000000000000000000000..956a4e3cd0e0b5c63bfb954a67de4d1e6f9ff8d0
--- /dev/null
+++ b/contrib/src/natpmp/SHA512SUMS
@@ -0,0 +1 @@
+e50b1f68ce9254bb2f068ddc37417a3c417b80f7b3fb3d84e3e9af4a144d89e204ab993b54c01657335e855d0124a8fcbbf96ce78db7b9ae0b03b6eb79de2e09  libnatpmp-20150609.tar.gz
diff --git a/contrib/src/natpmp/rules.mak b/contrib/src/natpmp/rules.mak
new file mode 100644
index 0000000000000000000000000000000000000000..49ddcb006cb57005560838125889afe126553c98
--- /dev/null
+++ b/contrib/src/natpmp/rules.mak
@@ -0,0 +1,21 @@
+# libnatpmp
+NATPMP_VERSION := 20150609
+NATPMP_URL := http://miniupnp.free.fr/files/download.php?file=libnatpmp-$(NATPMP_VERSION).tar.gz
+
+PKGS += natpmp
+ifeq ($(call need_pkg,'libnatpmp'),)
+PKGS_FOUND += natpmp
+endif
+
+$(TARBALLS)/libnatpmp-$(NATPMP_VERSION).tar.gz:
+	$(call download,$(NATPMP_URL))
+
+.sum-natpmp: libnatpmp-$(NATPMP_VERSION).tar.gz
+
+natpmp: libnatpmp-$(NATPMP_VERSION).tar.gz .sum-natpmp
+	$(UNPACK)
+	$(MOVE)
+
+.natpmp: natpmp
+	cd $< && $(MAKE) INSTALLPREFIX="$(PREFIX)" install
+	touch $@
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
index 8c95f4ab116e0bf8b72c18f054fcc4a01b9e7a4c..ace784f2c440977da2945b99a18e25c2b8f80260 100644
--- a/src/upnp/upnp_context.cpp
+++ b/src/upnp/upnp_context.cpp
@@ -38,6 +38,10 @@
 #include <upnp/upnptools.h>
 #endif
 
+#if HAVE_LIBNATPMP
+#include <natpmp.h>
+#endif
+
 #include "logger.h"
 #include "ip_utils.h"
 #include "upnp_igd.h"
@@ -61,6 +65,20 @@ getUPnPContext()
     return context;
 }
 
+/* UPnP error codes */
+constexpr static int INVALID_ARGS = 402;
+constexpr static int ARRAY_IDX_INVALID = 713;
+constexpr static int CONFLICT_IN_MAPPING = 718;
+
+/* max number of times to retry mapping if it fails due to conflict;
+ * there isn't much logic in picking this number... ideally not many ports should
+ * be mapped in a system, so a few number of random port retries should work;
+ * a high number of retries would indicate there might be some kind of bug or else
+ * incompatibility with the router; we use it to prevent an infinite loop of
+ * retrying to map the entry
+ */
+constexpr static unsigned MAX_RETRIES = 20;
+
 #if HAVE_LIBUPNP
 
 /* UPnP IGD definitions */
@@ -71,23 +89,10 @@ constexpr static const char * UPNP_WANCON_DEVICE = "urn:schemas-upnp-org:device:
 constexpr static const char * UPNP_WANIP_SERVICE = "urn:schemas-upnp-org:service:WANIPConnection:1";
 constexpr static const char * UPNP_WANPPP_SERVICE = "urn:schemas-upnp-org:service:WANPPPConnection:1";
 
-/* UPnP error codes */
-constexpr static int          INVALID_ARGS = 402;
 constexpr static const char * INVALID_ARGS_STR = "402";
-constexpr static int          ARRAY_IDX_INVALID = 713;
 constexpr static const char * ARRAY_IDX_INVALID_STR = "713";
-constexpr static int          CONFLICT_IN_MAPPING = 718;
 constexpr static const char * CONFLICT_IN_MAPPING_STR = "718";
 
-/* max number of times to retry mapping if it fails due to conflict;
- * there isn't much logic in picking this number... ideally not many ports should
- * be mapped in a system, so a few number of random port retries should work;
- * a high number of retries would indicate there might be some kind of bug or else
- * incompatibility with the router; we use it to prevent an infinite loop of
- * retrying to map the entry
- */
-constexpr static unsigned MAX_RETRIES = 20;
-
 /*
  * Local prototypes
  */
@@ -96,18 +101,68 @@ static std::string get_first_doc_item(IXML_Document*, const char*);
 static std::string get_first_element_item(IXML_Element*, const char*);
 static void checkResponseError(IXML_Document*);
 
-static int
-cp_callback(Upnp_EventType event_type, void* event, void* user_data)
-{
-    if (auto upnpContext = static_cast<UPnPContext*>(user_data))
-        return upnpContext->handleUPnPEvents(event_type, event);
+#else
 
-    RING_WARN("UPnP callback without UPnPContext");
-    return 0;
-}
+constexpr static int UPNP_E_SUCCESS = 0;
+
+#endif // HAVE_LIBUPNP
 
 UPnPContext::UPnPContext()
 {
+#if HAVE_LIBNATPMP
+    pmpThread_ = std::thread([this]() {
+        PMPIGD* pmp_igd = new PMPIGD();
+        natpmp_t natpmp;
+        bool found {false};
+
+        while (pmpRun_) {
+            if (initnatpmp(&natpmp, 0, 0) < 0) {
+                RING_ERR("NAT-PMP: can't initialize libnatpmp");
+                std::unique_lock<std::mutex> lk(pmpMutex_);
+                pmpCv_.wait_for(lk, std::chrono::minutes(1));
+            } else {
+                RING_DBG("NAT-PMP: initialized");
+                break;
+            }
+        }
+
+        while (pmpRun_) {
+            std::unique_lock<std::mutex> lk(pmpMutex_);
+            pmpCv_.wait_until(lk, pmp_igd->getRenewalTime(), [&] {
+                return not pmpRun_ or pmp_igd->getRenewalTime() <= clock::now();
+            });
+            if (not pmpRun_) break;
+
+            auto now = clock::now();
+
+            if (pmp_igd->renewal_ < now) {
+                PMPsearchForIGD(pmp_igd, natpmp, found);
+            }
+            if (found) {
+                if (pmp_igd->clearAll_) {
+                    PMPdeleteAllPortMapping(*pmp_igd, natpmp, NATPMP_PROTOCOL_UDP);
+                    PMPdeleteAllPortMapping(*pmp_igd, natpmp, NATPMP_PROTOCOL_TCP);
+                    pmp_igd->clearAll_ = false;
+                    pmp_igd->toRemove_.clear();
+                } else if (not pmp_igd->toRemove_.empty()) {
+                    for (auto& m : pmp_igd->toRemove_)
+                        PMPaddPortMapping(*pmp_igd, natpmp, m, true);
+                    pmp_igd->toRemove_.clear();
+                }
+                auto mapping = pmp_igd->getNextMappingToRenew();
+                if (mapping and mapping->renewal_ < now)
+                    PMPaddPortMapping(*pmp_igd, natpmp, *mapping);
+            }
+        }
+        if (not found) delete pmp_igd;
+        closenatpmp(&natpmp);
+        RING_DBG("NAT-PMP: ended");
+    });
+    clientRegistered_ = true;
+#endif
+
+#if HAVE_LIBUPNP
+
     int upnp_err;
     char* ip_address = nullptr;
     unsigned short port = 0;
@@ -155,19 +210,39 @@ UPnPContext::UPnPContext()
      * we will probably receive their advertisements either way
      */
     searchForIGD();
+#endif
 }
 
 UPnPContext::~UPnPContext()
 {
     /* make sure everything is unregistered, freed, and UpnpFinish() is called */
-
     {
         std::lock_guard<std::mutex> lock(validIGDMutex_);
         for( auto const &it : validIGDs_) {
-            removeMappingsByLocalIPAndDescription(it.second.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+#if HAVE_LIBUPNP
+            if (auto igd = dynamic_cast<UPnPIGD*>(it.second.get()))
+                removeMappingsByLocalIPAndDescription(*igd, Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+#endif
+#if HAVE_LIBNATPMP
+            if (auto pmp = dynamic_cast<PMPIGD*>(it.second.get())) {
+                {
+                    std::lock_guard<std::mutex> lk(pmpMutex_);
+                    pmp->clearAll();
+                }
+                pmpCv_.notify_all();
+            }
+#endif
         }
     }
 
+#if HAVE_LIBNATPMP
+    pmpRun_ = false;
+    pmpCv_.notify_all();
+    if (pmpThread_.joinable())
+        pmpThread_.join();
+#endif
+
+#if HAVE_LIBUPNP
     if (clientRegistered_)
         UpnpUnRegisterClient( ctrlptHandle_ );
 
@@ -177,21 +252,7 @@ UPnPContext::~UPnPContext()
 #ifndef _WIN32
     UpnpFinish();
 #endif
-}
-
-void
-UPnPContext::searchForIGD()
-{
-    if (not clientRegistered_) {
-        RING_WARN("UPnP: Control Point not registered");
-        return;
-    }
-
-    /* send out search for both types, as some routers may possibly only reply to one */
-    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
-    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
-    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
-    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
+#endif
 }
 
 bool
@@ -239,7 +300,11 @@ UPnPContext::chooseIGD_unlocked() const
 {
     if (validIGDs_.empty())
         return nullptr;
-    return validIGDs_.begin()->second.get();
+    auto natpmp = validIGDs_.find("NATPMP");
+    if (natpmp == validIGDs_.end())
+        return validIGDs_.begin()->second.get();
+    else
+        return natpmp->second.get();
 }
 
 /**
@@ -284,11 +349,21 @@ UPnPContext::addMapping(IGD* igd,
     }
 
     /* mapping doesn't exist, so try to add it */
-    RING_DBG("UPnP: adding port mapping : %s", mapping.toString().c_str());
+    RING_DBG("adding port mapping : %s", mapping.toString().c_str());
 
-    if(addPortMapping(igd, mapping, upnp_error)) {
+#if HAVE_LIBUPNP
+    auto upnp = dynamic_cast<const UPnPIGD*>(igd);
+    if (not upnp or addPortMapping(*upnp, mapping, upnp_error))
+#endif
+    {
         /* success; add it to global list */
         globalMappings->emplace(port_external, std::move(GlobalMapping{mapping}));
+#if HAVE_LIBNATPMP
+#if HAVE_LIBUPNP
+        if (not upnp)
+#endif
+            pmpCv_.notify_all();
+#endif
         return mapping;
     }
     return {};
@@ -304,17 +379,17 @@ generateRandomPort()
     /* define the range */
     static std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX);
 
-    return dist(gen);;
+    return dist(gen);
 }
 
 /**
  * chooses a random port that is not yet used by the daemon for UPnP
  */
 uint16_t
-UPnPContext::chooseRandomPort(const IGD* igd, PortType type)
+UPnPContext::chooseRandomPort(const IGD& igd, PortType type)
 {
     auto globalMappings = type == PortType::UDP ?
-                          &igd->udpMappings : &igd->tcpMappings;
+                          &igd.udpMappings : &igd.tcpMappings;
 
     uint16_t port = generateRandomPort();
 
@@ -363,7 +438,7 @@ UPnPContext::addAnyMapping(uint16_t port_desired,
         auto iter = globalMappings->find(port_desired);
         if (iter != globalMappings->end()) {
             /* port already used, we need a unique port */
-            port_desired = chooseRandomPort(igd, type);
+            port_desired = chooseRandomPort(*igd, type);
         }
     }
 
@@ -388,7 +463,7 @@ UPnPContext::addAnyMapping(uint16_t port_desired,
         RING_DBG("UPnP: mapping failed (conflicting entry? err = %d), trying with a different port.",
                  upnp_error);
         /* TODO: make sure we don't try sellecting the same random port twice if it fails ? */
-        port_desired = chooseRandomPort(igd, type);
+        port_desired = chooseRandomPort(*igd, type);
         if (use_same_port)
             port_local = port_desired;
         mapping = addMapping(igd, port_desired, port_local, type, &upnp_error);
@@ -423,21 +498,33 @@ UPnPContext::removeMapping(const Mapping& mapping)
     auto iter = globalMappings->find(mapping.getPortExternal());
     if ( iter != globalMappings->end() ) {
         /* make sure its the same mapping */
-        GlobalMapping *global_mapping = &iter->second;
-        if (mapping == *global_mapping ) {
+        GlobalMapping& global_mapping = iter->second;
+        if (mapping == global_mapping ) {
             /* now check the users */
-            if (global_mapping->users > 1) {
+            if (global_mapping.users > 1) {
                 /* more than one user, simply decrement the number */
-                --(global_mapping->users);
+                --(global_mapping.users);
                 RING_DBG("UPnP: decrementing users of mapping: %s, %d users remaining",
-                         mapping.toString().c_str(), global_mapping->users);
+                         mapping.toString().c_str(), global_mapping.users);
             } else {
                 /* no other users, can delete */
                 RING_DBG("UPnP: removing port mapping : %s",
                          mapping.toString().c_str());
-                deletePortMapping(igd,
-                                  mapping.getPortExternalStr(),
-                                  mapping.getTypeStr());
+#if HAVE_LIBUPNP
+                if (auto upnp = dynamic_cast<UPnPIGD*>(igd))
+                    deletePortMapping(*upnp,
+                                      mapping.getPortExternalStr(),
+                                      mapping.getTypeStr());
+#endif
+#if HAVE_LIBNATPMP
+                if (auto pmp = dynamic_cast<PMPIGD*>(igd)) {
+                    {
+                        std::lock_guard<std::mutex> lk(pmpMutex_);
+                        pmp->toRemove_.emplace_back(std::move(global_mapping));
+                    }
+                    pmpCv_.notify_all();
+                }
+#endif
                 globalMappings->erase(iter);
             }
         } else {
@@ -478,6 +565,117 @@ UPnPContext::getExternalIP() const
     return {};
 }
 
+#if HAVE_LIBNATPMP
+
+void
+UPnPContext::PMPsearchForIGD(PMPIGD* pmp_igd, natpmp_t& natpmp, bool& found)
+{
+    if (sendpublicaddressrequest(&natpmp) < 0) {
+        RING_ERR("NAT-PMP: can't send request");
+        pmp_igd->renewal_ = clock::now() + std::chrono::minutes(1);
+        return;
+    }
+
+    while (pmpRun_) {
+        natpmpresp_t response;
+        std::this_thread::sleep_for(std::chrono::milliseconds(2));
+        auto r = readnatpmpresponseorretry(&natpmp, &response);
+        if (r < 0 && r != NATPMP_TRYAGAIN) {
+            RING_WARN("NAT-PMP: can't find device");
+            pmp_igd->renewal_ = clock::now() + std::chrono::minutes(5);
+            break;
+        }
+        else if (r != NATPMP_TRYAGAIN) {
+            pmp_igd->localIp = ip_utils::getLocalAddr(AF_INET);
+            pmp_igd->publicIp = IpAddr(response.pnu.publicaddress.addr);
+            if (not found) {
+                found = true;
+                RING_DBG("NAT-PMP: found new device");
+                RING_DBG("NAT-PMP: got external IP: %s", pmp_igd->publicIp.toString().c_str());
+                {
+                    std::lock_guard<std::mutex> lock(validIGDMutex_);
+                    validIGDs_.emplace("NATPMP", std::unique_ptr<IGD>(pmp_igd));
+                    validIGDCondVar_.notify_all();
+                    for (const auto& l : igdListeners_)
+                        l.second();
+                }
+            }
+            pmp_igd->renewal_ = clock::now() + std::chrono::minutes(1);
+            break;
+        }
+    }
+}
+
+void
+UPnPContext::PMPaddPortMapping(const PMPIGD& /*pmp_igd*/, natpmp_t& natpmp, GlobalMapping& mapping, bool remove) const
+{
+    if (sendnewportmappingrequest(&natpmp,
+                                  mapping.getType() == PortType::UDP ? NATPMP_PROTOCOL_UDP : NATPMP_PROTOCOL_TCP,
+                                  mapping.getPortInternal(),
+                                  mapping.getPortExternal(), remove ? 0 : 3600) < 0) {
+        RING_ERR("NAT-PMP: can't send port mapping request");
+        mapping.renewal_ = clock::now() + std::chrono::minutes(1);
+        return;
+    }
+    RING_DBG("NAT-PMP: sent port mapping %srequest", remove ? "removal " : "");
+    while (pmpRun_) {
+        natpmpresp_t response;
+        std::this_thread::sleep_for(std::chrono::milliseconds(2));
+        auto r = readnatpmpresponseorretry(&natpmp, &response);
+        if (r < 0 && r != NATPMP_TRYAGAIN) {
+            RING_ERR("NAT-PMP: can't %sregister port mapping", remove ? "un" : "");
+            break;
+        }
+        else if (r != NATPMP_TRYAGAIN) {
+            mapping.renewal_ = clock::now()
+                             + std::chrono::seconds(response.pnu.newportmapping.lifetime/2);
+            break;
+        }
+    }
+}
+
+void
+UPnPContext::PMPdeleteAllPortMapping(const PMPIGD& /*pmp_igd*/, natpmp_t& natpmp, int proto) const
+{
+    if (sendnewportmappingrequest(&natpmp, proto, 0, 0, 0) < 0) {
+        RING_ERR("NAT-PMP: can't send all port mapping removal request");
+        return;
+    }
+    RING_DBG("NAT-PMP: sent all port mapping removal request");
+    while (pmpRun_) {
+        natpmpresp_t response;
+        std::this_thread::sleep_for(std::chrono::milliseconds(2));
+        auto r = readnatpmpresponseorretry(&natpmp, &response);
+        if (r < 0 && r != NATPMP_TRYAGAIN) {
+            RING_ERR("NAT-PMP: can't remove all port mappings");
+            break;
+        }
+        else if (r != NATPMP_TRYAGAIN) {
+            break;
+        }
+    }
+}
+
+#endif /* HAVE_LIBNATPMP */
+
+
+#if HAVE_LIBUPNP
+
+void
+UPnPContext::searchForIGD()
+{
+    if (not clientRegistered_) {
+        RING_WARN("UPnP: Control Point not registered");
+        return;
+    }
+
+    /* send out search for both types, as some routers may possibly only reply to one */
+    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
+    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
+    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
+    UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
+}
+
 /**
  * Parses the device description and adds desired devices to
  * relevant lists
@@ -528,7 +726,7 @@ UPnPContext::parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event)
         }
     }
 
-    std::unique_ptr<IGD> new_igd;
+    std::unique_ptr<UPnPIGD> new_igd;
     int upnp_err;
 
     std::string friendlyName = get_first_doc_item(doc, "friendlyName");
@@ -616,9 +814,9 @@ UPnPContext::parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event)
                     if (not (serviceId.empty() and controlURL.empty() and eventSubURL.empty()) ) {
                         /* RING_DBG("UPnP: got service info from device:\n\tserviceType: %s\n\tserviceID: %s\n\tcontrolURL: %s\n\teventSubURL: %s",
                                  serviceType.c_str(), serviceId.c_str(), controlURL.c_str(), eventSubURL.c_str()); */
-                        new_igd.reset(new IGD(UDN, baseURL, friendlyName, serviceType, serviceId, controlURL, eventSubURL));
-                        if (isIGDConnected(new_igd.get())) {
-                            new_igd->publicIp = getExternalIP(new_igd.get());
+                        new_igd.reset(new UPnPIGD(UDN, baseURL, friendlyName, serviceType, serviceId, controlURL, eventSubURL));
+                        if (isIGDConnected(*new_igd)) {
+                            new_igd->publicIp = getExternalIP(*new_igd);
                             if (new_igd->publicIp) {
                                 RING_DBG("UPnP: got external IP: %s", new_igd->publicIp.toString().c_str());
                                 new_igd->localIp = ip_utils::getLocalAddr(pj_AF_INET());
@@ -646,7 +844,7 @@ UPnPContext::parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event)
         {
             std::lock_guard<std::mutex> lock(validIGDMutex_);
             /* delete all RING mappings first */
-            removeMappingsByLocalIPAndDescription(new_igd.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+            removeMappingsByLocalIPAndDescription(*new_igd, Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
             validIGDs_.emplace(UDN, std::move(new_igd));
             validIGDCondVar_.notify_all();
             for (const auto& l : igdListeners_)
@@ -697,6 +895,17 @@ get_first_element_item(IXML_Element* element, const char* item)
     }
     return ret;
 }
+
+int
+UPnPContext::cp_callback(Upnp_EventType event_type, void* event, void* user_data)
+{
+    if (auto upnpContext = static_cast<UPnPContext*>(user_data))
+        return upnpContext->handleUPnPEvents(event_type, event);
+
+    RING_WARN("UPnP callback without UPnPContext");
+    return 0;
+}
+
 int
 UPnPContext::handleUPnPEvents(Upnp_EventType event_type, void* event)
 {
@@ -866,22 +1075,22 @@ checkResponseError(IXML_Document* doc)
 }
 
 bool
-UPnPContext::isIGDConnected(const IGD* igd)
+UPnPContext::isIGDConnected(const UPnPIGD& igd)
 {
     bool connected = false;
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
-    action.reset(UpnpMakeAction("GetStatusInfo", igd->getServiceType().c_str(), 0, nullptr));
+    action.reset(UpnpMakeAction("GetStatusInfo", igd.getServiceType().c_str(), 0, nullptr));
 
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
     IXML_Document* response_ptr = nullptr;
-    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
-                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd.getControlURL().c_str(),
+                                  igd.getServiceType().c_str(), nullptr, action.get(), &response_ptr);
     response.reset(response_ptr);
     checkResponseError(response.get());
     if( upnp_err != UPNP_E_SUCCESS) {
         /* TODO: if failed, should we chck if the igd is disconnected? */
         RING_WARN("UPnP: Failed to get GetStatusInfo from: %s, %d: %s",
-                  igd->getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
+                  igd.getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
 
         return false;
     }
@@ -899,21 +1108,21 @@ UPnPContext::isIGDConnected(const IGD* igd)
 }
 
 IpAddr
-UPnPContext::getExternalIP(const IGD* igd)
+UPnPContext::getExternalIP(const UPnPIGD& igd)
 {
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
-    action.reset(UpnpMakeAction("GetExternalIPAddress", igd->getServiceType().c_str(), 0, nullptr));
+    action.reset(UpnpMakeAction("GetExternalIPAddress", igd.getServiceType().c_str(), 0, nullptr));
 
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
     IXML_Document* response_ptr = nullptr;
-    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
-                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd.getControlURL().c_str(),
+                                  igd.getServiceType().c_str(), nullptr, action.get(), &response_ptr);
     response.reset(response_ptr);
     checkResponseError(response.get());
     if( upnp_err != UPNP_E_SUCCESS) {
         /* TODO: if failed, should we chck if the igd is disconnected? */
         RING_WARN("UPnP: Failed to get GetExternalIPAddress from: %s, %d: %s",
-                  igd->getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
+                  igd.getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
         return {};
     }
 
@@ -922,15 +1131,15 @@ UPnPContext::getExternalIP(const IGD* igd)
 }
 
 void
-UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::string& description)
+UPnPContext::removeMappingsByLocalIPAndDescription(const UPnPIGD& igd, const std::string& description)
 {
-    if (!igd->localIp) {
+    if (!igd.localIp) {
         RING_DBG("UPnP: cannot determine local IP in function removeMappingsByLocalIPAndDescription()");
         return;
     }
 
     RING_DBG("UPnP: removing all port mappings with description: \"%s\" and local ip: %s",
-             description.c_str(), igd->localIp.toString().c_str());
+             description.c_str(), igd.localIp.toString().c_str());
 
     int entry_idx = 0;
     bool done = false;
@@ -938,19 +1147,19 @@ UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::st
     do {
         std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
         IXML_Document* action_ptr = nullptr;
-        UpnpAddToAction(&action_ptr, "GetGenericPortMappingEntry", igd->getServiceType().c_str(),
+        UpnpAddToAction(&action_ptr, "GetGenericPortMappingEntry", igd.getServiceType().c_str(),
                         "NewPortMappingIndex", ring::to_string(entry_idx).c_str());
         action.reset(action_ptr);
 
         std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
         IXML_Document* response_ptr = nullptr;
-        int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
-                                      igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+        int upnp_err = UpnpSendAction(ctrlptHandle_, igd.getControlURL().c_str(),
+                                      igd.getServiceType().c_str(), nullptr, action.get(), &response_ptr);
         response.reset(response_ptr);
         if( not response and upnp_err != UPNP_E_SUCCESS) {
             /* TODO: if failed, should we chck if the igd is disconnected? */
             RING_WARN("UPnP: Failed to get GetGenericPortMappingEntry from: %s, %d: %s",
-                      igd->getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
+                      igd.getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
             return;
         }
 
@@ -963,7 +1172,7 @@ UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::st
             std::string client_ip = get_first_doc_item(response.get(), "NewInternalClient");
 
             /* check if same IP and description */
-            if (IpAddr(client_ip) == igd->localIp and desc_actual.compare(description) == 0) {
+            if (IpAddr(client_ip) == igd.localIp and desc_actual.compare(description) == 0) {
                 /* get the rest of the needed parameters */
                 std::string port_internal = get_first_doc_item(response.get(), "NewInternalPort");
                 std::string port_external = get_first_doc_item(response.get(), "NewExternalPort");
@@ -996,28 +1205,28 @@ UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::st
 }
 
 bool
-UPnPContext::deletePortMapping(const IGD* igd, const std::string& port_external, const std::string& protocol)
+UPnPContext::deletePortMapping(const UPnPIGD& igd, const std::string& port_external, const std::string& protocol)
 {
     std::string action_name{"DeletePortMapping"};
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
     IXML_Document* action_ptr = nullptr;
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewRemoteHost", "");
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewExternalPort", port_external.c_str());
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewProtocol", protocol.c_str());
     action.reset(action_ptr);
 
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
     IXML_Document* response_ptr = nullptr;
-    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
-                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd.getControlURL().c_str(),
+                                  igd.getServiceType().c_str(), nullptr, action.get(), &response_ptr);
     response.reset(response_ptr);
     if( upnp_err != UPNP_E_SUCCESS) {
         /* TODO: if failed, should we check if the igd is disconnected? */
         RING_WARN("UPnP: Failed to get %s from: %s, %d: %s", action_name.c_str(),
-                  igd->getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
+                  igd.getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
         return false;
     }
     /* check if there is an error code */
@@ -1032,41 +1241,41 @@ UPnPContext::deletePortMapping(const IGD* igd, const std::string& port_external,
 }
 
 bool
-UPnPContext::addPortMapping(const IGD* igd, const Mapping& mapping, int* error_code)
+UPnPContext::addPortMapping(const UPnPIGD& igd, const Mapping& mapping, int* error_code)
 {
     *error_code = UPNP_E_SUCCESS;
 
     std::string action_name{"AddPortMapping"};
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
     IXML_Document* action_ptr = nullptr;
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewRemoteHost", "");
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewExternalPort", mapping.getPortExternalStr().c_str());
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewProtocol", mapping.getTypeStr().c_str());
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewInternalPort", mapping.getPortInternalStr().c_str());
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
-                    "NewInternalClient", igd->localIp.toString().c_str());
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
+                    "NewInternalClient", igd.localIp.toString().c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewEnabled", "1");
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewPortMappingDescription", mapping.getDescription().c_str());
     /* for now assume lease duration is always infinite */
-    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd.getServiceType().c_str(),
                     "NewLeaseDuration", "0");
     action.reset(action_ptr);
 
     std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
     IXML_Document* response_ptr = nullptr;
-    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
-                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd.getControlURL().c_str(),
+                                  igd.getServiceType().c_str(), nullptr, action.get(), &response_ptr);
     response.reset(response_ptr);
     if( not response and upnp_err != UPNP_E_SUCCESS) {
         /* TODO: if failed, should we chck if the igd is disconnected? */
         RING_WARN("UPnP: Failed to %s from: %s, %d: %s", action_name.c_str(),
-                  igd->getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
+                  igd.getServiceType().c_str(), upnp_err, UpnpGetErrorMessage(upnp_err));
         *error_code = -1; /* make sure to -1 since we didn't get a response */
         return false;
     }
diff --git a/src/upnp/upnp_context.h b/src/upnp/upnp_context.h
index 72d06b75eafdcb2871f38755105c9e5d217512e0..3ad8285655ada1304cdb48de90018b793cd27e64 100644
--- a/src/upnp/upnp_context.h
+++ b/src/upnp/upnp_context.h
@@ -18,8 +18,7 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
-#ifndef UPNP_CONTEXT_H_
-#define UPNP_CONTEXT_H_
+#pragma once
 
 #ifdef HAVE_CONFIG_H
 #include "config.h"
@@ -32,6 +31,7 @@
 #include <condition_variable>
 #include <chrono>
 #include <atomic>
+#include <thread>
 
 #if HAVE_LIBUPNP
 #ifdef _WIN32
@@ -43,6 +43,10 @@
 #include <upnp/upnptools.h>
 #endif
 
+#if HAVE_LIBNATPMP
+#include <natpmp.h>
+#endif
+
 #include "noncopyable.h"
 #include "upnp_igd.h"
 
@@ -56,7 +60,6 @@ class UPnPContext {
 public:
     constexpr static unsigned SEARCH_TIMEOUT {30};
 
-#if HAVE_LIBUPNP
     UPnPContext();
     ~UPnPContext();
 
@@ -104,20 +107,62 @@ public:
      */
     IpAddr getLocalIP() const;
 
+private:
+    NON_COPYABLE(UPnPContext);
+
+    std::atomic_bool clientRegistered_ {false};
+
     /**
-     * callback function for the UPnP client (control point)
-     * all UPnP events received by the client are processed here
+     * map of valid IGDs - IGDs which have the correct services and are connected
+     * to some external network (have an external IP)
+     *
+     * the UDN string is used to uniquely identify the IGD
+     *
+     * the mutex is used to access these lists and IGDs in a thread-safe manner
      */
-    int handleUPnPEvents(Upnp_EventType event_type, void* event);
+    std::map<std::string, std::unique_ptr<IGD>> validIGDs_;
+    mutable std::mutex validIGDMutex_;
+    std::condition_variable validIGDCondVar_;
 
-#else
-    /* use default constructor and destructor */
-    UPnPContext() = default;
-    ~UPnPContext() = default;
-#endif
+    /**
+     * Map of valid IGD listeners.
+     */
+    std::map<size_t, IGDFoundCallback> igdListeners_;
+
+    /**
+     * Last provided token for valid IGD listeners.
+     * 0 is the invalid token.
+     */
+    size_t listenerToken_ {0};
+
+    /**
+     * chooses the IGD to use (currently selects the first one in the map)
+     * assumes you already have a lock on igd_mutex_
+     */
+    IGD* chooseIGD_unlocked() const;
 
-private:
-    NON_COPYABLE(UPnPContext);
+
+    /* tries to add mapping, assumes you alreayd have lock on igd_mutex_ */
+    Mapping addMapping(IGD* igd,
+                       uint16_t port_external,
+                       uint16_t port_internal,
+                       PortType type,
+                       int *upnp_error);
+
+    uint16_t chooseRandomPort(const IGD& igd, PortType type);
+
+#if HAVE_LIBNATPMP
+
+    std::thread pmpThread_;
+    std::mutex pmpMutex_;
+    std::condition_variable pmpCv_;
+    std::atomic_bool pmpRun_ {true};
+
+    void PMPsearchForIGD(PMPIGD* pmp_igd, natpmp_t& natpmp, bool& found);
+    void PMPaddPortMapping(const PMPIGD& pmp_igd, natpmp_t& natpmp, GlobalMapping& mapping, bool remove=false) const;
+    void PMPdeleteAllPortMapping(const PMPIGD& pmp_igd, natpmp_t& natpmp, int proto) const;
+
+#endif
 
 #if HAVE_LIBUPNP
 
@@ -150,40 +195,18 @@ private:
     UpnpDevice_Handle deviceHandle_ {-1};
 
     /**
-     * keep track if we've successfully registered
-     * a client and/ore device
+     * keep track if we've successfully registered a device
      */
-    std::atomic_bool clientRegistered_ {false};
     bool deviceRegistered_ {false};
 
-    /**
-     * map of valid IGDs - IGDs which have the correct services and are connected
-     * to some external network (have an external IP)
-     *
-     * the UDN string is used to uniquely identify the IGD
-     *
-     * the mutex is used to access these lists and IGDs in a thread-safe manner
-     */
-    std::map<std::string, std::unique_ptr<IGD>> validIGDs_;
-    mutable std::mutex validIGDMutex_;
-    std::condition_variable validIGDCondVar_;
-
-    /**
-     * Map of valid IGD listeners.
-     */
-    std::map<size_t, IGDFoundCallback> igdListeners_;
+    static int cp_callback(Upnp_EventType event_type, void* event, void* user_data);
 
     /**
-     * Last provided token for valid IGD listeners.
-     * 0 is the invalid token.
+     * callback function for the UPnP client (control point)
+     * all UPnP events received by the client are processed here
      */
-    size_t listenerToken_ {0};
+    int handleUPnPEvents(Upnp_EventType event_type, void* event);
 
-    /**
-     * chooses the IGD to use (currently selects the first one in the map)
-     * assumes you already have a lock on igd_mutex_
-     */
-    IGD* chooseIGD_unlocked() const;
 
     /* sends out async search for IGD */
     void searchForIGD();
@@ -196,33 +219,24 @@ private:
 
     void parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event);
 
-    /* tries to add mapping, assumes you alreayd have lock on igd_mutex_ */
-    Mapping addMapping(IGD* igd,
-                       uint16_t port_external,
-                       uint16_t port_internal,
-                       PortType type,
-                       int *upnp_error);
-
-    uint16_t chooseRandomPort(const IGD* igd, PortType type);
 
     /* these functions directly create UPnP actions
      * and make synchronous UPnP control point calls
      * they assume you have a lock on the igd_mutex_ */
-    bool isIGDConnected(const IGD* igd);
+    bool isIGDConnected(const UPnPIGD& igd);
 
-    IpAddr getExternalIP(const IGD* igd);
+    IpAddr getExternalIP(const UPnPIGD& igd);
 
-    void removeMappingsByLocalIPAndDescription(const IGD* igd,
+    void removeMappingsByLocalIPAndDescription(const UPnPIGD& igd,
                                                const std::string& description);
 
-    bool deletePortMapping(const IGD* igd,
+    bool deletePortMapping(const UPnPIGD& igd,
                            const std::string& port_external,
                            const std::string& protocol);
 
-    bool addPortMapping(const IGD* igd,
+    bool addPortMapping(const UPnPIGD& igd,
                         const Mapping& mapping,
                         int* error_code);
-
 #endif /* HAVE_LIBUPNP */
 
 };
@@ -236,5 +250,3 @@ private:
 std::shared_ptr<UPnPContext> getUPnPContext();
 
 }} // namespace ring::upnp
-
-#endif /* UPNP_CONTEXT_H_ */
diff --git a/src/upnp/upnp_control.cpp b/src/upnp/upnp_control.cpp
index 4e3be9d60481ba39dadbfd817ed73aafa87485d8..d6b06f345cee130e1116231c668eb0a2f0017730 100644
--- a/src/upnp/upnp_control.cpp
+++ b/src/upnp/upnp_control.cpp
@@ -46,19 +46,14 @@ Controller::~Controller()
 {
     /* remove all mappings */
     removeMappings();
-#if HAVE_LIBUPNP
     if (listToken_ and upnpContext_)
         upnpContext_->removeIGDListener(listToken_);
-#endif
 }
 
 bool
 Controller::hasValidIGD(std::chrono::seconds timeout)
 {
-#if HAVE_LIBUPNP
     return upnpContext_ and upnpContext_->hasValidIGD(timeout);
-#endif
-    return false;
 }
 
 void
@@ -66,11 +61,9 @@ Controller::setIGDListener(IGDFoundCallback&& cb)
 {
     if (not upnpContext_)
         return;
-#if HAVE_LIBUPNP
     if (listToken_)
         upnpContext_->removeIGDListener(listToken_);
     listToken_ = cb ? upnpContext_->addIGDListener(std::move(cb)) : 0;
-#endif
 }
 
 bool
@@ -81,7 +74,6 @@ Controller::addAnyMapping(uint16_t port_desired,
                           bool unique,
                           uint16_t *port_used)
 {
-#if HAVE_LIBUPNP
     if (not upnpContext_)
         return false;
 
@@ -97,7 +89,6 @@ Controller::addAnyMapping(uint16_t port_desired,
         instanceMappings.emplace(usedPort, std::move(mapping));
         return true;
     }
-#endif
     return false;
 }
 
@@ -113,7 +104,6 @@ Controller::addAnyMapping(uint16_t port_desired,
 
 void
 Controller::removeMappings(PortType type) {
-#if HAVE_LIBUPNP
     if (not upnpContext_)
         return;
 
@@ -123,34 +113,28 @@ Controller::removeMappings(PortType type) {
         upnpContext_->removeMapping(mapping);
         iter = instanceMappings.erase(iter);
     }
-#endif
 }
+
 void
 Controller::removeMappings()
 {
-#if HAVE_LIBUPNP
     removeMappings(PortType::UDP);
     removeMappings(PortType::TCP);
-#endif
 }
 
 IpAddr
 Controller::getLocalIP() const
 {
-#if HAVE_LIBUPNP
     if (upnpContext_)
         return upnpContext_->getLocalIP();
-#endif
     return {}; //  empty address
 }
 
 IpAddr
 Controller::getExternalIP() const
 {
-#if HAVE_LIBUPNP
     if (upnpContext_)
         return upnpContext_->getExternalIP();
-#endif
     return {}; //  empty address
 }
 
diff --git a/src/upnp/upnp_igd.h b/src/upnp/upnp_igd.h
index 3fdce01678488ddf30e5c79658de74fd0c010c2a..6a65de3f361ac9370a45bfef2cb8f96e845f86e5 100644
--- a/src/upnp/upnp_igd.h
+++ b/src/upnp/upnp_igd.h
@@ -18,12 +18,16 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
-#ifndef UPNP_IGD_H_
-#define UPNP_IGD_H_
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
 
 #include <string>
 #include <map>
 #include <functional>
+#include <chrono>
 
 #include "noncopyable.h"
 #include "ip_utils.h"
@@ -63,13 +67,13 @@ public:
     friend bool operator== (const Mapping& cRedir1, const Mapping& cRedir2);
     friend bool operator!= (const Mapping& cRedir1, const Mapping& cRedir2);
 
-    uint16_t      getPortExternal()    const { return port_external_; };
-    std::string   getPortExternalStr() const { return ring::to_string(port_external_); };
-    uint16_t      getPortInternal()    const { return port_internal_; };
-    std::string   getPortInternalStr() const { return ring::to_string(port_internal_); };
-    PortType      getType()            const { return type_; };
+    uint16_t      getPortExternal()    const { return port_external_; }
+    std::string   getPortExternalStr() const { return ring::to_string(port_external_); }
+    uint16_t      getPortInternal()    const { return port_internal_; }
+    std::string   getPortInternalStr() const { return ring::to_string(port_internal_); }
+    PortType      getType()            const { return type_; }
     std::string   getTypeStr()         const { return type_ == PortType::UDP ? "UDP" : "TCP"; }
-    std::string   getDescription()     const { return description_; };
+    std::string   getDescription()     const { return description_; }
 
     std::string toString() const {
         return getPortExternalStr() + ":" + getPortInternalStr() + ", " + getTypeStr();
@@ -83,6 +87,11 @@ public:
         return isValid();
     }
 
+#if HAVE_LIBNATPMP
+    std::chrono::system_clock::time_point renewal_ {std::chrono::system_clock::time_point::min()};
+    bool remove {false};
+#endif
+
 private:
     NON_COPYABLE(Mapping);
 
@@ -123,7 +132,6 @@ using IGDFoundCallback = std::function<void()>;
 /* defines a UPnP capable Internet Gateway Device (a router) */
 class IGD {
 public:
-
     /* device address seen by IGD */
     IpAddr localIp;
 
@@ -136,7 +144,22 @@ public:
 
     /* constructors */
     IGD() {}
-    IGD(std::string UDN,
+
+    /* move constructor and operator */
+    IGD(IGD&&) = default;
+    IGD& operator=(IGD&&) = default;
+
+    virtual ~IGD() = default;
+
+private:
+    NON_COPYABLE(IGD);
+};
+
+#if HAVE_LIBUPNP
+
+class UPnPIGD : public IGD {
+public:
+    UPnPIGD(std::string UDN,
         std::string baseURL,
         std::string friendlyName,
         std::string serviceType,
@@ -152,12 +175,6 @@ public:
         , eventSubURL_(eventSubURL)
         {}
 
-    /* move constructor and operator */
-    IGD(IGD&&) = default;
-    IGD& operator=(IGD&&) = default;
-
-    ~IGD() = default;
-
     const std::string& getUDN() const { return UDN_; };
     const std::string& getBaseURL() const { return baseURL_; };
     const std::string& getFriendlyName() const { return friendlyName_; };
@@ -167,8 +184,6 @@ public:
     const std::string& getEventSubURL() const { return eventSubURL_; };
 
 private:
-    NON_COPYABLE(IGD);
-
     /* root device info */
     std::string UDN_ {}; /* used to uniquely identify this UPnP device */
     std::string baseURL_ {};
@@ -179,9 +194,47 @@ private:
     std::string serviceId_ {};
     std::string controlURL_ {};
     std::string eventSubURL_ {};
+};
+
+#endif
+
+#if HAVE_LIBNATPMP
+
+using clock = std::chrono::system_clock;
+using time_point = clock::time_point;
+
+class PMPIGD : public IGD {
+public:
+
+    void clearAll() {
+        toRemove_.clear();
+        udpMappings.clear();
+        tcpMappings.clear();
+        clearAll_ = true;
+    }
+
+    GlobalMapping* getNextMappingToRenew() const {
+        const GlobalMapping* mapping {nullptr};
+        for (const auto& m : udpMappings)
+            if (!mapping or m.second.renewal_ < mapping->renewal_)
+                mapping = &m.second;
+        for (const auto& m : tcpMappings)
+            if (!mapping or m.second.renewal_ < mapping->renewal_)
+                mapping = &m.second;
+        return (GlobalMapping*)mapping;
+    }
 
+    time_point getRenewalTime() const {
+        const auto next = getNextMappingToRenew();
+        auto nextTime = std::min(renewal_, next ? next->renewal_ : time_point::max());
+        return toRemove_.empty() ? nextTime : std::min(nextTime, time_point::min());
+    }
+
+    time_point renewal_ {time_point::min()};
+    std::vector<GlobalMapping> toRemove_ {};
+    bool clearAll_ {false};
 };
 
-}} // namespace ring::upnp
+#endif
 
-#endif /* UPNP_IGD_H_ */
+}} // namespace ring::upnp