From 0ed32b48c3c28e66eced8dc802085e30af95c2b0 Mon Sep 17 00:00:00 2001 From: past-due <30942300+past-due@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:07:16 -0400 Subject: [PATCH] PortMappingManager: Split implementations to separate files --- lib/netplay/port_mapping_manager.cpp | 691 +----------------- lib/netplay/port_mapping_manager.h | 7 +- .../port_mapping_manager_impl_libplum.cpp | 142 ++++ .../port_mapping_manager_impl_libplum.h | 39 + .../port_mapping_manager_impl_miniupnpc.cpp | 503 +++++++++++++ .../port_mapping_manager_impl_miniupnpc.h | 79 ++ 6 files changed, 781 insertions(+), 680 deletions(-) create mode 100644 lib/netplay/port_mapping_manager_impl_libplum.cpp create mode 100644 lib/netplay/port_mapping_manager_impl_libplum.h create mode 100644 lib/netplay/port_mapping_manager_impl_miniupnpc.cpp create mode 100644 lib/netplay/port_mapping_manager_impl_miniupnpc.h diff --git a/lib/netplay/port_mapping_manager.cpp b/lib/netplay/port_mapping_manager.cpp index 643cf8a366a..f4161ab2b00 100644 --- a/lib/netplay/port_mapping_manager.cpp +++ b/lib/netplay/port_mapping_manager.cpp @@ -26,685 +26,26 @@ #include "port_mapping_manager.h" -#include +#include "port_mapping_manager_impl_libplum.h" +#include "port_mapping_manager_impl_miniupnpc.h" + +#define WZ_PORT_MAPPING_ID_INVALID -1 PortMappingImpl::~PortMappingImpl() { // no-op } -// MARK: - Libplum implementation - -#include - -void PlumMappingCallbackSuccess(int mappingId, const std::string& externalHost, uint16_t externalPort) -{ - PortMappingManager::instance().resolve_success_fromimpl(PortMappingImpl::Type::Libplum, mappingId, externalHost, externalPort); -} - -void PlumMappingCallbackFailure(int mappingId) -{ - PortMappingManager::instance().resolve_failure_fromimpl(PortMappingImpl::Type::Libplum, mappingId, PortMappingDiscoveryStatus::FAILURE); -} - -namespace -{ - -// This function is run in its own thread managed by LibPLum! Do not call any non-threadsafe functions! -void PlumMappingCallback(int mappingId, plum_state_t state, const plum_mapping_t* mapping) -{ - debug(LOG_NET, "LibPlum cb: Port mapping %d: state=%d\n", mappingId, (int)state); - switch (state) { - case PLUM_STATE_SUCCESS: - debug(LOG_NET, "Mapping %d: success, internal=%hu, external=%s:%hu\n", mappingId, mapping->internal_port, - mapping->external_host, mapping->external_port); - PlumMappingCallbackSuccess(mappingId, mapping->external_host, mapping->external_port); - break; - - case PLUM_STATE_FAILURE: - debug(LOG_NET, "Mapping %d: failed\n", mappingId); - PlumMappingCallbackFailure(mappingId); - break; - - default: - break; - } -} - -void PlumLogCallback(plum_log_level_t level, const char* message) -{ - code_part wz_log_level = LOG_NET; - switch (level) - { - case PLUM_LOG_LEVEL_FATAL: - case PLUM_LOG_LEVEL_ERROR: - case PLUM_LOG_LEVEL_WARN: - wz_log_level = LOG_INFO; - break; - default: - break; - } - if (enabled_debug[wz_log_level]) - { - std::string msg = (message) ? message : ""; - wzAsyncExecOnMainThread([wz_log_level, msg]() { - _debug(__LINE__, wz_log_level, "PlumLogCallback", "%s", msg.c_str()); - }); - } -} - -} // anonymous namespace - -class PortMappingImpl_LibPlum : public PortMappingImpl -{ -public: - virtual ~PortMappingImpl_LibPlum(); - virtual bool init() override; - virtual bool shutdown() override; - virtual int create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) override; - virtual bool destroy_port_mapping(int mappingId) override; - virtual PortMappingImpl::Type get_impl_type() const override; - virtual int get_discovery_timeout_seconds() const override; -private: - bool m_isInit = false; -}; - -PortMappingImpl_LibPlum::~PortMappingImpl_LibPlum() -{ - try - { - shutdown(); - } - catch(...) // Don't let any exceptions escape the dtor. - {} -} - -PortMappingImpl::Type PortMappingImpl_LibPlum::get_impl_type() const -{ - return PortMappingImpl::Type::Libplum; -} - -int PortMappingImpl_LibPlum::get_discovery_timeout_seconds() const -{ - return 10; // 10 seconds for Libplum, to ensure we don't wait too long before falling-back to Miniupnpc -} +// MARK: - General callback helpers -bool PortMappingImpl_LibPlum::init() +void PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type type, int mappingId, const std::string& externalHost, uint16_t externalPort) { - plum_config_t config; - memset(&config, 0, sizeof(config)); - config.log_level = PLUM_LOG_LEVEL_INFO; - config.log_callback = &PlumLogCallback; - auto res = plum_init(&config); - m_isInit = (res == PLUM_ERR_SUCCESS); - return res == PLUM_ERR_SUCCESS; + PortMappingManager::instance().resolve_success_fromimpl(type, mappingId, externalHost, externalPort); } -bool PortMappingImpl_LibPlum::shutdown() +void PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type type, int mappingId) { - if (!m_isInit) - { - return false; - } - plum_cleanup(); - m_isInit = false; - return true; -} - -int PortMappingImpl_LibPlum::create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) -{ - plum_mapping_t m; - memset(&m, 0, sizeof(m)); - m.protocol = PLUM_IP_PROTOCOL_TCP; - m.internal_port = port; - m.external_port = port; // suggest an external port the same as the internal port (the router may decide otherwise) - - auto mappingId = plum_create_mapping(&m, PlumMappingCallback); - return mappingId; -} - -bool PortMappingImpl_LibPlum::destroy_port_mapping(int mappingId) -{ - const auto result = plum_destroy_mapping(mappingId); - return result == PLUM_ERR_SUCCESS; -} - -// MARK: - Minupnpc implementation - -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER) -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wpedantic" -#endif -#include -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER) -# pragma GCC diagnostic pop -#endif -#include - -// Enforce minimum MINIUPNPC_API_VERSION -#if !defined(MINIUPNPC_API_VERSION) || (MINIUPNPC_API_VERSION < 9) - #error lib/netplay requires MINIUPNPC_API_VERSION >= 9 -#endif - -void _MiniupnpcLogCallback(int line, code_part part, const char *function, const char* message) -{ - std::string msg = (message) ? message : ""; - wzAsyncExecOnMainThread([line, part, function, msg]() { - _debug(line, part, function, "%s", msg.c_str()); - }); -} -#define MiniupnpcLogCallback(part, msg) do { if (enabled_debug[part]) _MiniupnpcLogCallback(__LINE__, part, __FUNCTION__, msg); } while(0) - -class PortMappingImpl_Miniupnpc : public PortMappingImpl -{ -public: - virtual ~PortMappingImpl_Miniupnpc(); - virtual bool init() override; - virtual bool shutdown() override; - virtual int create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) override; - virtual bool destroy_port_mapping(int mappingId) override; - virtual PortMappingImpl::Type get_impl_type() const override; - virtual int get_discovery_timeout_seconds() const override; - -private: - enum class DiscoveryStatus - { - UPNP_SUCCESS, - UPNP_ERROR_DEVICE_NOT_FOUND, - UPNP_ERROR_CONTROL_NOT_AVAILABLE - }; - struct DiscoveryResults - { - struct UPNPUrls urls; - struct IGDdatas data; - char lanaddr[64] = {}; - }; - static DiscoveryStatus upnp_discover(DiscoveryResults& output); - - struct upnp_map_output - { - bool success = false; - std::string externalHost; - uint16_t externalPort = 0; - }; - static upnp_map_output upnp_add_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port); - static bool upnp_remove_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port); - static void miniupnpc_background_thread(std::shared_ptr pInstance); -private: - bool m_isInit = false; - - enum class WzMiniupnpc_State - { - Destroyed = 0, - Pending = 1, - Success = 2, - Failure = 3, - Destroying = 4 - }; - - struct MappingInfo - { - uint16_t port = 0; - PortMappingInternetProtocol protocol = PortMappingInternetProtocol::IPV4; - WzMiniupnpc_State state = WzMiniupnpc_State::Destroyed; - - void reset() - { - *this = MappingInfo(); - } - }; - - std::thread thread_; - WZ_SEMAPHORE *threadSemaphore = nullptr; - - // mutex protected data - std::mutex mappings_mtx_; - bool stopRequested_ = false; - optional status_ = nullopt; - std::array mappings_; -}; - -PortMappingImpl_Miniupnpc::~PortMappingImpl_Miniupnpc() -{ - try - { - shutdown(); - } - catch(...) // Don't let any exceptions escape the dtor. - {} -} - -PortMappingImpl::Type PortMappingImpl_Miniupnpc::get_impl_type() const -{ - return PortMappingImpl::Type::Miniupnpc; -} - -int PortMappingImpl_Miniupnpc::get_discovery_timeout_seconds() const -{ - return 20; // Slightly longer timeout for Miniupnpc -} - -const char* WZ_UPNP_GetValidIGD_GetErrStr(int result) -{ - switch (result) - { - case -1: - return "Internal error"; - case 0: - return "No IGD found"; - case 1: - return "A valid connected IGD has been found"; -#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) - case 2: - return "A valid connected IGD has been found but its IP address is reserved (non routable)"; - case 3: - return "A valid IGD has been found but it reported as not connected"; - case 4: - return "A UPnP device has been found but was not recognized as an IGD"; -#else - case 2: - return "A valid IGD has been found but it reported as not connected"; - case 3: - return "A UPnP device has been found but was not recognized as an IGD"; -#endif - default: - return "Unknown error"; - } -} - -// This function is run in its own thread! Do not call any non-threadsafe functions! -PortMappingImpl_Miniupnpc::DiscoveryStatus PortMappingImpl_Miniupnpc::upnp_discover(DiscoveryResults& output) -{ - char buf[512] = {'\0'}; - struct UPNPDev *devlist; - int result = 0; - memset(&output.urls, 0, sizeof(struct UPNPUrls)); - memset(&output.data, 0, sizeof(struct IGDdatas)); - - MiniupnpcLogCallback(LOG_NET, "Searching for UPnP devices for automatic port forwarding..."); -#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 14) - devlist = upnpDiscover(3000, nullptr, nullptr, 0, 0, 2, &result); -#else - devlist = upnpDiscover(3000, nullptr, nullptr, 0, 0, &result); -#endif - MiniupnpcLogCallback(LOG_NET, "UPnP device search finished"); - - if (!devlist) - { - return DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND; - } - - char wanaddr[64] = {}; -#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) - int validIGDResult = UPNP_GetValidIGD(devlist, &output.urls, &output.data, output.lanaddr, sizeof(output.lanaddr), wanaddr, sizeof(wanaddr)); -#else - int validIGDResult = UPNP_GetValidIGD(devlist, &output.urls, &output.data, output.lanaddr, sizeof(output.lanaddr)); -#endif - freeUPNPDevlist(devlist); - - if (validIGDResult == 1) - { - ssprintf(buf, "UPnP IGD device found: %s (LAN address %s)", output.urls.controlURL, output.lanaddr); - MiniupnpcLogCallback(LOG_NET, buf); - } -#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) - else if (validIGDResult == 2) - { - ssprintf(buf, "UPnP found an IGD with a reserved IP address (%s): %s\n", wanaddr, output.urls.controlURL); - MiniupnpcLogCallback(LOG_NET, buf); - } -#endif - else - { - ssprintf(buf, "UPNP_GetValidIGD failed: (%d): %s", validIGDResult, WZ_UPNP_GetValidIGD_GetErrStr(validIGDResult)); - MiniupnpcLogCallback(LOG_NET, buf); - return DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND; - } - - if (!output.urls.controlURL || output.urls.controlURL[0] == '\0') - { - return DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE; - } - else - { - return DiscoveryStatus::UPNP_SUCCESS; - } -} - -bool PortMappingImpl_Miniupnpc::upnp_remove_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port) -{ - char port_str[16]; - char buf[512] = {'\0'}; - - ssprintf(buf, "upnp_remove_redirect(%" PRIu16 ")", port); - MiniupnpcLogCallback(LOG_NET, buf); - - sprintf(port_str, "%" PRIu16, port); - auto result = UPNP_DeletePortMapping(discovery.urls.controlURL, discovery.data.first.servicetype, port_str, "TCP", nullptr); - if (result != 0) - { - ssprintf(buf, "upnp_remove_redirect(%" PRIu16 ") failed with error: %d", port, result); - MiniupnpcLogCallback(LOG_NET, buf); - } - return result == 0; -} - -void MiniupnpcMappingCallbackSuccess(int mappingId, const std::string& externalHost, uint16_t externalPort) -{ - PortMappingManager::instance().resolve_success_fromimpl(PortMappingImpl::Type::Miniupnpc, mappingId, externalHost, externalPort); -} - -void MiniupnpcMappingCallbackFailure(int mappingId) -{ - PortMappingManager::instance().resolve_failure_fromimpl(PortMappingImpl::Type::Miniupnpc, mappingId, PortMappingDiscoveryStatus::FAILURE); -} - -PortMappingImpl_Miniupnpc::upnp_map_output PortMappingImpl_Miniupnpc::upnp_add_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port) -{ - char port_str[16]; - char buf[512] = {'\0'}; - int r; - char externalIPAddress[40] = {}; - - ssprintf(buf, "upnp_add_redirect(%" PRIu16 ")", port); - MiniupnpcLogCallback(LOG_NET, buf); - sprintf(port_str, "%" PRIu16, port); - r = UPNP_AddPortMapping(discovery.urls.controlURL, discovery.data.first.servicetype, - port_str, port_str, discovery.lanaddr, "Warzone 2100", "TCP", nullptr, "0"); // "0" = lease time unlimited - if (r != UPNPCOMMAND_SUCCESS) - { - ssprintf(buf, "Could not open required port (%s) on (%s)", port_str, discovery.lanaddr); - MiniupnpcLogCallback(LOG_NET, buf); - - // Failure - upnp_map_output result; - result.success = false; - return result; - } - - r = UPNP_GetExternalIPAddress(discovery.urls.controlURL, discovery.data.first.servicetype, externalIPAddress); - if (r != UPNPCOMMAND_SUCCESS) - { - // Non-fatal error - ssprintf(buf, "Opened required port (%s), but failed externalIPAddress discovery - proceeding anyway", port_str); - MiniupnpcLogCallback(LOG_NET, buf); - // Zero-out externalIpAddress - externalIPAddress[0] = '\0'; - } - - upnp_map_output result; - result.success = true; - result.externalHost = externalIPAddress; - result.externalPort = port; - return result; -} - -void PortMappingImpl_Miniupnpc::miniupnpc_background_thread(std::shared_ptr pInstanceBase) -{ - auto pInstance = std::dynamic_pointer_cast(pInstanceBase); - - auto doDiscovery = [pInstance](DiscoveryResults& output) -> DiscoveryStatus { - DiscoveryStatus status = upnp_discover(output); - - // Only once that's done do we handle mutex-protected data - switch (status) - { - case DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE: - case DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND: - { - if (status == DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND) - { - MiniupnpcLogCallback(LOG_NET, "UPnP device not found"); - } - else if (status == DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE) - { - MiniupnpcLogCallback(LOG_NET, "controlURL not available, UPnP disabled"); - } - std::vector failedMappingIds; - { - const std::lock_guard guard {pInstance->mappings_mtx_}; - // Store failure status - pInstance->status_ = status; - // Fail any pending requests - for (size_t i = 0; i < pInstance->mappings_.size(); ++i) - { - if (pInstance->mappings_[i].state == WzMiniupnpc_State::Destroyed) - { - continue; - } - pInstance->mappings_[i].state = WzMiniupnpc_State::Destroyed; - failedMappingIds.push_back(i); - } - } - for (auto mappingId : failedMappingIds) - { - MiniupnpcMappingCallbackFailure(mappingId); - } - // exit background thread - return status; - } - case DiscoveryStatus::UPNP_SUCCESS: - // continue to process pending and new mapping requests - break; - } - - // Store discovery success status - { - const std::lock_guard guard {pInstance->mappings_mtx_}; - pInstance->status_ = status; - } - - return status; - }; - - // Call upnp_discover - std::shared_ptr discovery = std::make_shared(); - DiscoveryStatus status = doDiscovery(*discovery); - if (status != DiscoveryStatus::UPNP_SUCCESS) - { - // exit background thread - return; - } - - bool shouldUpdateDiscovery = false; - while (true) - { - wzSemaphoreWait(pInstance->threadSemaphore); // wait until main thread sends tasks to do - - if (shouldUpdateDiscovery) - { - status = doDiscovery(*discovery); - if (status != DiscoveryStatus::UPNP_SUCCESS) - { - // exit background thread - return; - } - shouldUpdateDiscovery = false; - } - - size_t i = 0; - size_t anyMappings = false; - while (true) - { - auto mappingId = i; - MappingInfo mapInfo; - - { - const std::lock_guard guard {pInstance->mappings_mtx_}; - - if (pInstance->stopRequested_) - { - return; - } - - if (i >= pInstance->mappings_.size()) - { - break; - } - - if (pInstance->mappings_[i].state == WzMiniupnpc_State::Destroyed) - { - ++i; - continue; - } - - mapInfo = pInstance->mappings_[i]; - } - - switch (mapInfo.state) - { - case WzMiniupnpc_State::Pending: - { - anyMappings = true; - auto result = upnp_add_redirect(mappingId, *discovery, mapInfo.port); - bool doCallbacks = false; - { - const std::lock_guard guard {pInstance->mappings_mtx_}; - if (pInstance->mappings_[i].state != WzMiniupnpc_State::Destroying) - { - if (result.success) - { - pInstance->mappings_[i].state = WzMiniupnpc_State::Success; - } - else - { - pInstance->mappings_[i].state = WzMiniupnpc_State::Failure; - } - doCallbacks = true; - } - } - if (doCallbacks) - { - if (result.success) - { - MiniupnpcMappingCallbackSuccess(mappingId, result.externalHost, result.externalPort); - } - else - { - MiniupnpcMappingCallbackFailure(mappingId); - } - } - break; - } - case WzMiniupnpc_State::Destroying: - upnp_remove_redirect(mappingId, *discovery, mapInfo.port); - { - const std::lock_guard guard {pInstance->mappings_mtx_}; - pInstance->mappings_[i].reset(); // sets state to WzMiniupnpc_State::Destroyed - } - break; - case WzMiniupnpc_State::Success: - case WzMiniupnpc_State::Failure: - // do nothing - anyMappings = true; - break; - default: - // no-op - break; - } - - ++i; - } - - shouldUpdateDiscovery = !anyMappings; // queue a re-discovery next time a mapping is requested, if all existing mappings have been destroyed - } -} - -bool PortMappingImpl_Miniupnpc::init() -{ - if (m_isInit) - { - return true; - } - stopRequested_ = false; - threadSemaphore = wzSemaphoreCreate(0); - thread_ = std::thread(PortMappingImpl_Miniupnpc::miniupnpc_background_thread, shared_from_this()); - m_isInit = true; - return true; -} - -bool PortMappingImpl_Miniupnpc::shutdown() -{ - if (!m_isInit) - { - return false; - } - - { - const std::lock_guard guard {mappings_mtx_}; - stopRequested_ = true; - } - wzSemaphorePost(threadSemaphore); // Wake up the thread, so it can quit. - if (thread_.joinable()) - { - thread_.join(); - } - wzSemaphoreDestroy(threadSemaphore); - threadSemaphore = nullptr; - m_isInit = false; - return true; -} - -int PortMappingImpl_Miniupnpc::create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) -{ - if (protocol != PortMappingInternetProtocol::IPV4) - { - // currently only supports IPv4 - return -1; - } - - size_t mappingId = 0; - { - const std::lock_guard guard {mappings_mtx_}; - - if (status_.has_value() && status_.value() != DiscoveryStatus::UPNP_SUCCESS) - { - // UPnP not supported - return -1; - } - - // Find first available mapping id - for (size_t i = 0; i <= mappings_.size(); ++i) - { - if (i == mappings_.size()) - { - // no available mapping id - return -1; - } - if (mappings_[i].state == WzMiniupnpc_State::Destroyed) - { - mappingId = i; - break; - } - } - - mappings_[mappingId].protocol = protocol; - mappings_[mappingId].port = port; - mappings_[mappingId].state = WzMiniupnpc_State::Pending; - } - wzSemaphorePost(threadSemaphore); - - return static_cast(mappingId); -} - -bool PortMappingImpl_Miniupnpc::destroy_port_mapping(int mappingId) -{ - { - const std::lock_guard guard {mappings_mtx_}; - if (mappingId < 0 || static_cast(mappingId) >= mappings_.size()) - { - return false; - } - - if (mappings_[mappingId].state == WzMiniupnpc_State::Destroyed) - { - return true; - } - - mappings_[mappingId].state = WzMiniupnpc_State::Destroying; - } - wzSemaphorePost(threadSemaphore); - return true; + PortMappingManager::instance().resolve_failure_fromimpl(type, mappingId, PortMappingDiscoveryStatus::FAILURE); } // MARK: - PortMappingManager @@ -716,7 +57,7 @@ PortMappingAsyncRequestOpaqueBase::~PortMappingAsyncRequestOpaqueBase() { } PortMappingManager::PortMappingAsyncRequest::PortMappingAsyncRequest() - : mappingId(PLUM_ERR_INVALID) + : mappingId(WZ_PORT_MAPPING_ID_INVALID) {} PortMappingDiscoveryStatus PortMappingManager::PortMappingAsyncRequest::status() const @@ -756,10 +97,10 @@ void PortMappingManager::PortMappingAsyncRequest::set_port_mapping_in_progress(T bool PortMappingManager::PortMappingAsyncRequest::destroy_mapping() { // Destroy the internal mapping. - if (mappingId != PLUM_ERR_INVALID) + if (mappingId != WZ_PORT_MAPPING_ID_INVALID) { const auto result = impl->destroy_port_mapping(mappingId); - mappingId = PLUM_ERR_INVALID; + mappingId = WZ_PORT_MAPPING_ID_INVALID; s = PortMappingDiscoveryStatus::NOT_STARTED; return result; } @@ -917,7 +258,7 @@ bool PortMappingManager::destroy_port_mapping(const PortMappingAsyncRequestHandl auto h = std::dynamic_pointer_cast(p); ASSERT_OR_RETURN(false, h != nullptr, "Invalid request handle"); const std::lock_guard guard {mtx_}; - if (h->mappingId == PLUM_ERR_INVALID) + if (h->mappingId == WZ_PORT_MAPPING_ID_INVALID) { return true; } @@ -941,7 +282,7 @@ bool PortMappingManager::destroy_port_mapping(const PortMappingAsyncRequestHandl auto wasFailedMapping = h->failed(); // Destroy the internal mapping. auto result = h->destroy_mapping(); - h->mappingId = PLUM_ERR_INVALID; + h->mappingId = WZ_PORT_MAPPING_ID_INVALID; return (wasFailedMapping) ? true : result; } @@ -1114,10 +455,10 @@ void PortMappingManager::resolve_success_fromimpl(PortMappingImpl::Type type, in } if (!resolve_success_internal(req, externalIp, externalPort)) { - if (req->mappingId != PLUM_ERR_INVALID) + if (req->mappingId != WZ_PORT_MAPPING_ID_INVALID) { req->destroy_mapping(); - req->mappingId = PLUM_ERR_INVALID; + req->mappingId = WZ_PORT_MAPPING_ID_INVALID; } } } diff --git a/lib/netplay/port_mapping_manager.h b/lib/netplay/port_mapping_manager.h index e3b75ec95b9..ace6514eee9 100644 --- a/lib/netplay/port_mapping_manager.h +++ b/lib/netplay/port_mapping_manager.h @@ -183,11 +183,8 @@ class PortMappingManager protected: // Only intended to be called by plum callback handlers: - friend void PlumMappingCallbackSuccess(int mappingId, const std::string& externalHost, uint16_t externalPort); - friend void PlumMappingCallbackFailure(int mappingId); - - friend void MiniupnpcMappingCallbackSuccess(int mappingId, const std::string& externalHost, uint16_t externalPort); - friend void MiniupnpcMappingCallbackFailure(int mappingId); + friend void PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type type, int mappingId, const std::string& externalHost, uint16_t externalPort); + friend void PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type type, int mappingId); // Set discovery status to `SUCCESS`. // The method saves the discovered IP address + port combination inside the diff --git a/lib/netplay/port_mapping_manager_impl_libplum.cpp b/lib/netplay/port_mapping_manager_impl_libplum.cpp new file mode 100644 index 00000000000..f73ae5c6ec9 --- /dev/null +++ b/lib/netplay/port_mapping_manager_impl_libplum.cpp @@ -0,0 +1,142 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2024 Warzone 2100 Project + + Warzone 2100 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 2 of the License, or + (at your option) any later version. + + Warzone 2100 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 Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** + * @file port_mapping_manager_impl_libplum.cpp + * libplum-based port mapping implementation + */ + +#include "lib/framework/wzapp.h" +#include "lib/framework/debug.h" + +#include "port_mapping_manager_impl_libplum.h" + +#include + +void PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type type, int mappingId, const std::string& externalHost, uint16_t externalPort); +void PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type type, int mappingId); + +// MARK: - Libplum implementation + +namespace +{ + +// This function is run in its own thread managed by LibPLum! Do not call any non-threadsafe functions! +void PlumMappingCallback(int mappingId, plum_state_t state, const plum_mapping_t* mapping) +{ + debug(LOG_NET, "LibPlum cb: Port mapping %d: state=%d\n", mappingId, (int)state); + switch (state) { + case PLUM_STATE_SUCCESS: + debug(LOG_NET, "Mapping %d: success, internal=%hu, external=%s:%hu\n", mappingId, mapping->internal_port, + mapping->external_host, mapping->external_port); + PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type::Libplum, mappingId, mapping->external_host, mapping->external_port); + break; + + case PLUM_STATE_FAILURE: + debug(LOG_NET, "Mapping %d: failed\n", mappingId); + PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type::Libplum, mappingId); + break; + + default: + break; + } +} + +void PlumLogCallback(plum_log_level_t level, const char* message) +{ + code_part wz_log_level = LOG_NET; + switch (level) + { + case PLUM_LOG_LEVEL_FATAL: + case PLUM_LOG_LEVEL_ERROR: + case PLUM_LOG_LEVEL_WARN: + wz_log_level = LOG_INFO; + break; + default: + break; + } + if (enabled_debug[wz_log_level]) + { + std::string msg = (message) ? message : ""; + wzAsyncExecOnMainThread([wz_log_level, msg]() { + _debug(__LINE__, wz_log_level, "PlumLogCallback", "%s", msg.c_str()); + }); + } +} + +} // anonymous namespace + +PortMappingImpl_LibPlum::~PortMappingImpl_LibPlum() +{ + try + { + shutdown(); + } + catch(...) // Don't let any exceptions escape the dtor. + {} +} + +PortMappingImpl::Type PortMappingImpl_LibPlum::get_impl_type() const +{ + return PortMappingImpl::Type::Libplum; +} + +int PortMappingImpl_LibPlum::get_discovery_timeout_seconds() const +{ + return 10; // 10 seconds for Libplum, to ensure we don't wait too long before falling-back to Miniupnpc +} + +bool PortMappingImpl_LibPlum::init() +{ + plum_config_t config; + memset(&config, 0, sizeof(config)); + config.log_level = PLUM_LOG_LEVEL_INFO; + config.log_callback = &PlumLogCallback; + auto res = plum_init(&config); + m_isInit = (res == PLUM_ERR_SUCCESS); + return res == PLUM_ERR_SUCCESS; +} + +bool PortMappingImpl_LibPlum::shutdown() +{ + if (!m_isInit) + { + return false; + } + plum_cleanup(); + m_isInit = false; + return true; +} + +int PortMappingImpl_LibPlum::create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) +{ + plum_mapping_t m; + memset(&m, 0, sizeof(m)); + m.protocol = PLUM_IP_PROTOCOL_TCP; + m.internal_port = port; + m.external_port = port; // suggest an external port the same as the internal port (the router may decide otherwise) + + auto mappingId = plum_create_mapping(&m, PlumMappingCallback); + return mappingId; +} + +bool PortMappingImpl_LibPlum::destroy_port_mapping(int mappingId) +{ + const auto result = plum_destroy_mapping(mappingId); + return result == PLUM_ERR_SUCCESS; +} diff --git a/lib/netplay/port_mapping_manager_impl_libplum.h b/lib/netplay/port_mapping_manager_impl_libplum.h new file mode 100644 index 00000000000..53a9d5b656b --- /dev/null +++ b/lib/netplay/port_mapping_manager_impl_libplum.h @@ -0,0 +1,39 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2024 Warzone 2100 Project + + Warzone 2100 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 2 of the License, or + (at your option) any later version. + + Warzone 2100 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 Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** + * @file port_mapping_manager_impl_libplum.h + */ +#pragma once + +#include "port_mapping_manager.h" + +class PortMappingImpl_LibPlum : public PortMappingImpl +{ +public: + virtual ~PortMappingImpl_LibPlum(); + virtual bool init() override; + virtual bool shutdown() override; + virtual int create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) override; + virtual bool destroy_port_mapping(int mappingId) override; + virtual PortMappingImpl::Type get_impl_type() const override; + virtual int get_discovery_timeout_seconds() const override; +private: + bool m_isInit = false; +}; + diff --git a/lib/netplay/port_mapping_manager_impl_miniupnpc.cpp b/lib/netplay/port_mapping_manager_impl_miniupnpc.cpp new file mode 100644 index 00000000000..0b01f5dca83 --- /dev/null +++ b/lib/netplay/port_mapping_manager_impl_miniupnpc.cpp @@ -0,0 +1,503 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2024 Warzone 2100 Project + + Warzone 2100 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 2 of the License, or + (at your option) any later version. + + Warzone 2100 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 Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** + * @file port_mapping_manager_impl_miniupnpc.cpp + * miniupnpc-based port mapping implementation + */ + +#include "lib/framework/wzapp.h" +#include "lib/framework/debug.h" + +#include "port_mapping_manager_impl_miniupnpc.h" + +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wpedantic" +#endif +#include +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER) +# pragma GCC diagnostic pop +#endif +#include + +// Enforce minimum MINIUPNPC_API_VERSION +#if !defined(MINIUPNPC_API_VERSION) || (MINIUPNPC_API_VERSION < 9) + #error lib/netplay requires MINIUPNPC_API_VERSION >= 9 +#endif + +void PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type type, int mappingId, const std::string& externalHost, uint16_t externalPort); +void PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type type, int mappingId); + +// MARK: - Minupnpc implementation + +void _MiniupnpcLogCallback(int line, code_part part, const char *function, const char* message) +{ + std::string msg = (message) ? message : ""; + wzAsyncExecOnMainThread([line, part, function, msg]() { + _debug(line, part, function, "%s", msg.c_str()); + }); +} +#define MiniupnpcLogCallback(part, msg) do { if (enabled_debug[part]) _MiniupnpcLogCallback(__LINE__, part, __FUNCTION__, msg); } while(0) + +PortMappingImpl_Miniupnpc::~PortMappingImpl_Miniupnpc() +{ + try + { + shutdown(); + } + catch(...) // Don't let any exceptions escape the dtor. + {} +} + +PortMappingImpl::Type PortMappingImpl_Miniupnpc::get_impl_type() const +{ + return PortMappingImpl::Type::Miniupnpc; +} + +int PortMappingImpl_Miniupnpc::get_discovery_timeout_seconds() const +{ + return 20; // Slightly longer timeout for Miniupnpc +} + +const char* WZ_UPNP_GetValidIGD_GetErrStr(int result) +{ + switch (result) + { + case -1: + return "Internal error"; + case 0: + return "No IGD found"; + case 1: + return "A valid connected IGD has been found"; +#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) + case 2: + return "A valid connected IGD has been found but its IP address is reserved (non routable)"; + case 3: + return "A valid IGD has been found but it reported as not connected"; + case 4: + return "A UPnP device has been found but was not recognized as an IGD"; +#else + case 2: + return "A valid IGD has been found but it reported as not connected"; + case 3: + return "A UPnP device has been found but was not recognized as an IGD"; +#endif + default: + return "Unknown error"; + } +} + +struct DiscoveryResults +{ + struct UPNPUrls urls; + struct IGDdatas data; + char lanaddr[64] = {}; +}; + +// This function is run in its own thread! Do not call any non-threadsafe functions! +static PortMappingImpl_Miniupnpc::DiscoveryStatus upnp_discover(DiscoveryResults& output) +{ + char buf[512] = {'\0'}; + struct UPNPDev *devlist; + int result = 0; + memset(&output.urls, 0, sizeof(struct UPNPUrls)); + memset(&output.data, 0, sizeof(struct IGDdatas)); + + MiniupnpcLogCallback(LOG_NET, "Searching for UPnP devices for automatic port forwarding..."); +#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 14) + devlist = upnpDiscover(3000, nullptr, nullptr, 0, 0, 2, &result); +#else + devlist = upnpDiscover(3000, nullptr, nullptr, 0, 0, &result); +#endif + MiniupnpcLogCallback(LOG_NET, "UPnP device search finished"); + + if (!devlist) + { + return PortMappingImpl_Miniupnpc::DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND; + } + + char wanaddr[64] = {}; +#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) + int validIGDResult = UPNP_GetValidIGD(devlist, &output.urls, &output.data, output.lanaddr, sizeof(output.lanaddr), wanaddr, sizeof(wanaddr)); +#else + int validIGDResult = UPNP_GetValidIGD(devlist, &output.urls, &output.data, output.lanaddr, sizeof(output.lanaddr)); +#endif + freeUPNPDevlist(devlist); + + if (validIGDResult == 1) + { + ssprintf(buf, "UPnP IGD device found: %s (LAN address %s)", output.urls.controlURL, output.lanaddr); + MiniupnpcLogCallback(LOG_NET, buf); + } +#if defined(MINIUPNPC_API_VERSION) && (MINIUPNPC_API_VERSION >= 18) + else if (validIGDResult == 2) + { + ssprintf(buf, "UPnP found an IGD with a reserved IP address (%s): %s\n", wanaddr, output.urls.controlURL); + MiniupnpcLogCallback(LOG_NET, buf); + } +#endif + else + { + ssprintf(buf, "UPNP_GetValidIGD failed: (%d): %s", validIGDResult, WZ_UPNP_GetValidIGD_GetErrStr(validIGDResult)); + MiniupnpcLogCallback(LOG_NET, buf); + return PortMappingImpl_Miniupnpc::DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND; + } + + if (!output.urls.controlURL || output.urls.controlURL[0] == '\0') + { + return PortMappingImpl_Miniupnpc::DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE; + } + else + { + return PortMappingImpl_Miniupnpc::DiscoveryStatus::UPNP_SUCCESS; + } +} + +static bool upnp_remove_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port) +{ + char port_str[16]; + char buf[512] = {'\0'}; + + ssprintf(buf, "upnp_remove_redirect(%" PRIu16 ")", port); + MiniupnpcLogCallback(LOG_NET, buf); + + sprintf(port_str, "%" PRIu16, port); + auto result = UPNP_DeletePortMapping(discovery.urls.controlURL, discovery.data.first.servicetype, port_str, "TCP", nullptr); + if (result != 0) + { + ssprintf(buf, "upnp_remove_redirect(%" PRIu16 ") failed with error: %d", port, result); + MiniupnpcLogCallback(LOG_NET, buf); + } + return result == 0; +} + +struct upnp_map_output +{ + bool success = false; + std::string externalHost; + uint16_t externalPort = 0; +}; + +static upnp_map_output upnp_add_redirect(int mappingId, const DiscoveryResults& discovery, uint16_t port) +{ + char port_str[16]; + char buf[512] = {'\0'}; + int r; + char externalIPAddress[40] = {}; + + ssprintf(buf, "upnp_add_redirect(%" PRIu16 ")", port); + MiniupnpcLogCallback(LOG_NET, buf); + sprintf(port_str, "%" PRIu16, port); + r = UPNP_AddPortMapping(discovery.urls.controlURL, discovery.data.first.servicetype, + port_str, port_str, discovery.lanaddr, "Warzone 2100", "TCP", nullptr, "0"); // "0" = lease time unlimited + if (r != UPNPCOMMAND_SUCCESS) + { + ssprintf(buf, "Could not open required port (%s) on (%s)", port_str, discovery.lanaddr); + MiniupnpcLogCallback(LOG_NET, buf); + + // Failure + upnp_map_output result; + result.success = false; + return result; + } + + r = UPNP_GetExternalIPAddress(discovery.urls.controlURL, discovery.data.first.servicetype, externalIPAddress); + if (r != UPNPCOMMAND_SUCCESS) + { + // Non-fatal error + ssprintf(buf, "Opened required port (%s), but failed externalIPAddress discovery - proceeding anyway", port_str); + MiniupnpcLogCallback(LOG_NET, buf); + // Zero-out externalIpAddress + externalIPAddress[0] = '\0'; + } + + upnp_map_output result; + result.success = true; + result.externalHost = externalIPAddress; + result.externalPort = port; + return result; +} + +void PortMappingImpl_Miniupnpc::miniupnpc_background_thread(std::shared_ptr pInstanceBase) +{ + auto pInstance = std::dynamic_pointer_cast(pInstanceBase); + + auto doDiscovery = [pInstance](DiscoveryResults& output) -> DiscoveryStatus { + DiscoveryStatus status = upnp_discover(output); + + // Only once that's done do we handle mutex-protected data + switch (status) + { + case DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE: + case DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND: + { + if (status == DiscoveryStatus::UPNP_ERROR_DEVICE_NOT_FOUND) + { + MiniupnpcLogCallback(LOG_NET, "UPnP device not found"); + } + else if (status == DiscoveryStatus::UPNP_ERROR_CONTROL_NOT_AVAILABLE) + { + MiniupnpcLogCallback(LOG_NET, "controlURL not available, UPnP disabled"); + } + std::vector failedMappingIds; + { + const std::lock_guard guard {pInstance->mappings_mtx_}; + // Store failure status + pInstance->status_ = status; + // Fail any pending requests + for (size_t i = 0; i < pInstance->mappings_.size(); ++i) + { + if (pInstance->mappings_[i].state == WzMiniupnpc_State::Destroyed) + { + continue; + } + pInstance->mappings_[i].state = WzMiniupnpc_State::Destroyed; + failedMappingIds.push_back(i); + } + } + for (auto mappingId : failedMappingIds) + { + PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type::Miniupnpc, mappingId); + } + // exit background thread + return status; + } + case DiscoveryStatus::UPNP_SUCCESS: + // continue to process pending and new mapping requests + break; + } + + // Store discovery success status + { + const std::lock_guard guard {pInstance->mappings_mtx_}; + pInstance->status_ = status; + } + + return status; + }; + + // Call upnp_discover + std::shared_ptr discovery = std::make_shared(); + DiscoveryStatus status = doDiscovery(*discovery); + if (status != DiscoveryStatus::UPNP_SUCCESS) + { + // exit background thread + return; + } + + bool shouldUpdateDiscovery = false; + while (true) + { + wzSemaphoreWait(pInstance->threadSemaphore); // wait until main thread sends tasks to do + + if (shouldUpdateDiscovery) + { + status = doDiscovery(*discovery); + if (status != DiscoveryStatus::UPNP_SUCCESS) + { + // exit background thread + return; + } + shouldUpdateDiscovery = false; + } + + size_t i = 0; + size_t anyMappings = false; + while (true) + { + auto mappingId = i; + MappingInfo mapInfo; + + { + const std::lock_guard guard {pInstance->mappings_mtx_}; + + if (pInstance->stopRequested_) + { + return; + } + + if (i >= pInstance->mappings_.size()) + { + break; + } + + if (pInstance->mappings_[i].state == WzMiniupnpc_State::Destroyed) + { + ++i; + continue; + } + + mapInfo = pInstance->mappings_[i]; + } + + switch (mapInfo.state) + { + case WzMiniupnpc_State::Pending: + { + anyMappings = true; + auto result = upnp_add_redirect(mappingId, *discovery, mapInfo.port); + bool doCallbacks = false; + { + const std::lock_guard guard {pInstance->mappings_mtx_}; + if (pInstance->mappings_[i].state != WzMiniupnpc_State::Destroying) + { + if (result.success) + { + pInstance->mappings_[i].state = WzMiniupnpc_State::Success; + } + else + { + pInstance->mappings_[i].state = WzMiniupnpc_State::Failure; + } + doCallbacks = true; + } + } + if (doCallbacks) + { + if (result.success) + { + PortMappingImpl_MappingCallbackSuccess(PortMappingImpl::Type::Miniupnpc, mappingId, result.externalHost, result.externalPort); + } + else + { + PortMappingImpl_MappingCallbackFailure(PortMappingImpl::Type::Miniupnpc, mappingId); + } + } + break; + } + case WzMiniupnpc_State::Destroying: + upnp_remove_redirect(mappingId, *discovery, mapInfo.port); + { + const std::lock_guard guard {pInstance->mappings_mtx_}; + pInstance->mappings_[i].reset(); // sets state to WzMiniupnpc_State::Destroyed + } + break; + case WzMiniupnpc_State::Success: + case WzMiniupnpc_State::Failure: + // do nothing + anyMappings = true; + break; + default: + // no-op + break; + } + + ++i; + } + + shouldUpdateDiscovery = !anyMappings; // queue a re-discovery next time a mapping is requested, if all existing mappings have been destroyed + } +} + +bool PortMappingImpl_Miniupnpc::init() +{ + if (m_isInit) + { + return true; + } + stopRequested_ = false; + threadSemaphore = wzSemaphoreCreate(0); + thread_ = std::thread(PortMappingImpl_Miniupnpc::miniupnpc_background_thread, shared_from_this()); + m_isInit = true; + return true; +} + +bool PortMappingImpl_Miniupnpc::shutdown() +{ + if (!m_isInit) + { + return false; + } + + { + const std::lock_guard guard {mappings_mtx_}; + stopRequested_ = true; + } + wzSemaphorePost(threadSemaphore); // Wake up the thread, so it can quit. + if (thread_.joinable()) + { + thread_.join(); + } + wzSemaphoreDestroy(threadSemaphore); + threadSemaphore = nullptr; + m_isInit = false; + return true; +} + +int PortMappingImpl_Miniupnpc::create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) +{ + if (protocol != PortMappingInternetProtocol::IPV4) + { + // currently only supports IPv4 + return -1; + } + + size_t mappingId = 0; + { + const std::lock_guard guard {mappings_mtx_}; + + if (status_.has_value() && status_.value() != DiscoveryStatus::UPNP_SUCCESS) + { + // UPnP not supported + return -1; + } + + // Find first available mapping id + for (size_t i = 0; i <= mappings_.size(); ++i) + { + if (i == mappings_.size()) + { + // no available mapping id + return -1; + } + if (mappings_[i].state == WzMiniupnpc_State::Destroyed) + { + mappingId = i; + break; + } + } + + mappings_[mappingId].protocol = protocol; + mappings_[mappingId].port = port; + mappings_[mappingId].state = WzMiniupnpc_State::Pending; + } + wzSemaphorePost(threadSemaphore); + + return static_cast(mappingId); +} + +bool PortMappingImpl_Miniupnpc::destroy_port_mapping(int mappingId) +{ + { + const std::lock_guard guard {mappings_mtx_}; + if (mappingId < 0 || static_cast(mappingId) >= mappings_.size()) + { + return false; + } + + if (mappings_[mappingId].state == WzMiniupnpc_State::Destroyed) + { + return true; + } + + mappings_[mappingId].state = WzMiniupnpc_State::Destroying; + } + wzSemaphorePost(threadSemaphore); + return true; +} diff --git a/lib/netplay/port_mapping_manager_impl_miniupnpc.h b/lib/netplay/port_mapping_manager_impl_miniupnpc.h new file mode 100644 index 00000000000..8960db5bc8e --- /dev/null +++ b/lib/netplay/port_mapping_manager_impl_miniupnpc.h @@ -0,0 +1,79 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2024 Warzone 2100 Project + + Warzone 2100 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 2 of the License, or + (at your option) any later version. + + Warzone 2100 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 Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** + * @file port_mapping_manager_impl_miniupnpc.h + */ +#pragma once + +#include "port_mapping_manager.h" + +#include + +class PortMappingImpl_Miniupnpc : public PortMappingImpl +{ +public: + virtual ~PortMappingImpl_Miniupnpc(); + virtual bool init() override; + virtual bool shutdown() override; + virtual int create_port_mapping(uint16_t port, PortMappingInternetProtocol protocol) override; + virtual bool destroy_port_mapping(int mappingId) override; + virtual PortMappingImpl::Type get_impl_type() const override; + virtual int get_discovery_timeout_seconds() const override; + + enum class DiscoveryStatus + { + UPNP_SUCCESS, + UPNP_ERROR_DEVICE_NOT_FOUND, + UPNP_ERROR_CONTROL_NOT_AVAILABLE + }; +private: + static void miniupnpc_background_thread(std::shared_ptr pInstance); +private: + bool m_isInit = false; + + enum class WzMiniupnpc_State + { + Destroyed = 0, + Pending = 1, + Success = 2, + Failure = 3, + Destroying = 4 + }; + + struct MappingInfo + { + uint16_t port = 0; + PortMappingInternetProtocol protocol = PortMappingInternetProtocol::IPV4; + WzMiniupnpc_State state = WzMiniupnpc_State::Destroyed; + + void reset() + { + *this = MappingInfo(); + } + }; + + std::thread thread_; + WZ_SEMAPHORE *threadSemaphore = nullptr; + + // mutex protected data + std::mutex mappings_mtx_; + bool stopRequested_ = false; + optional status_ = nullopt; + std::array mappings_; +};