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