diff --git a/CMakeLists.txt b/CMakeLists.txt index fad3bb4b3..46a7d6f40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -907,7 +907,7 @@ elseif(UPNP_STYLE STREQUAL "Win32") list(APPEND OC_SYSTEM_SOURCES src/network/C4Network2UPnPWin32.cpp ) -elseif(UPNP_STYLE STREQUAL "libupnp") +elseif(UPNP_STYLE STREQUAL "miniupnpc") list(APPEND OC_SYSTEM_SOURCES src/network/C4Network2UPnPLinux.cpp ) diff --git a/cmake/FindUpnp.cmake b/cmake/FindUpnp.cmake index a661f0d1c..7d0c2fcd4 100644 --- a/cmake/FindUpnp.cmake +++ b/cmake/FindUpnp.cmake @@ -31,22 +31,16 @@ if(WIN32) SET(UPNP_FOUND TRUE) SET(UPNP_STYLE "Win32") else() - find_path(UPNP_INCLUDE_DIR NAMES upnp.h PATH_SUFFIXES upnp) - set(UPNP_NAMES ${UPNP_NAMES} upnp) - set(THREADUTIL_NAMES ${THREADUTIL_NAMES} threadutil) - set(IXML_NAMES ${IXML_NAMES} ixml) + find_path(UPNP_INCLUDE_DIR NAMES miniupnpc.h PATH_SUFFIXES miniupnpc) + set(UPNP_NAMES ${UPNP_NAMES} miniupnpc) find_library(UPNP_LIBRARY NAMES ${UPNP_NAMES}) - find_library(THREADUTIL_LIBRARY NAMES ${THREADUTIL_NAMES}) - find_library(IXML_LIBRARY NAMES ${IXML_NAMES}) - include(FindPackageHandleStandardArgs) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(UPNP DEFAULT_MSG UPNP_LIBRARY THREADUTIL_LIBRARY IXML_LIBRARY UPNP_INCLUDE_DIR) - - if(UPNP_FOUND) - set(UPNP_LIBRARIES ${UPNP_LIBRARY} ${THREADUTIL_LIBRARY} ${IXML_LIBRARY}) + if(UPNP_LIBRARY) + SET(UPNP_FOUND TRUE) + set(UPNP_LIBRARIES ${UPNP_LIBRARY}) set(UPNP_INCLUDE_DIR ${UPNP_INCLUDE_DIR}) - set(UPNP_STYLE "libupnp") + set(UPNP_STYLE "miniupnpc") endif() endif() -mark_as_advanced(UPNP_LIBRARY IXML_LIBRARY UPNP_INCLUDE_DIR) +mark_as_advanced(UPNP_LIBRARY UPNP_INCLUDE_DIR) diff --git a/src/network/C4Network2UPnPLinux.cpp b/src/network/C4Network2UPnPLinux.cpp index 4d61d0044..92b2daeab 100644 --- a/src/network/C4Network2UPnPLinux.cpp +++ b/src/network/C4Network2UPnPLinux.cpp @@ -12,72 +12,21 @@ * To redistribute this file separately, substitute the full license texts * for the above references. */ -/* Linux implementation of a UPnP port mapper (using libupnp) */ +/* Linux implementation of a UPnP port mapper (using miniupnpc) */ #include "C4Include.h" #include "game/C4Application.h" #include "C4Version.h" -#include -#include -#include +#include +#include +#include -#include "network/C4Network2UPnP.h" // must come after upnp.h +#include "network/C4Network2UPnP.h" -namespace -{ - // This attempts to return the local IP address which is used for - // internet connections. It does so by associating a UDP socket to talk to - // 8.8.8.8 (A Google nameserver) and then reading its local address. There - // might be cleverer ways to do this, such as reading the routing table. - std::string GetOutgoingAddress() - { - struct socket_wrapper - { - const int sock; - operator int() const { return sock; } +static const char *description = "OpenClonk"; - socket_wrapper(int domain, int type, int protocol): - sock(socket(domain, type | SOCK_CLOEXEC, protocol)) - { - if(sock == -1) - throw std::runtime_error(std::string("Failed to create a socket: ") + strerror(errno)); - } - - ~socket_wrapper() - { - if(close(sock) != 0) - DebugLogF("Failed to close socket: %s\n", strerror(errno)); - } - }; - - const char* const REMOTE_ADDRESS = "8.8.8.8"; - - struct sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_port = htons(53); // DNS port - if(inet_pton(AF_INET, REMOTE_ADDRESS, &addr.sin_addr) != 1) - throw std::runtime_error(std::string("Failed to convert address text to binary: ") + strerror(errno)); - - socket_wrapper sock(AF_INET, SOCK_DGRAM, 0); - if(connect(sock, reinterpret_cast(&addr), sizeof(addr)) != 0) - throw std::runtime_error(std::string("Failed to set target address on UDP socket: ") + strerror(errno)); - - struct sockaddr_in local_addr; - socklen_t local_addr_len = sizeof(local_addr); - if(getsockname(sock, reinterpret_cast(&local_addr), &local_addr_len) != 0 || local_addr_len > sizeof(local_addr)) - throw std::runtime_error(std::string("Failed to query peer name of UDP socket: ") + strerror(errno)); - - char text_address[INET_ADDRSTRLEN]; - if(inet_ntop(AF_INET, &local_addr.sin_addr, text_address, INET_ADDRSTRLEN) == NULL) - throw std::runtime_error(std::string("Failed to convert binary address to text: ") + strerror(errno)); - - //DebugLogF("Outgoing address: %s", text_address); - return text_address; - } -} - -class C4Network2UPnPP: public C4InteractiveThread::Callback +class C4Network2UPnPP { public: C4Network2UPnPP(); @@ -87,253 +36,98 @@ public: void ClearMappings(); private: - struct IGD { - //std::string DeviceID; - std::string Location; - std::string ServiceType; - }; - struct PortMapping { - std::string external_hostname; uint16_t external_port; - - std::string internal_hostname; uint16_t internal_port; - std::string protocol; }; - struct ActionData { - ActionData(const IGD& igd_, const PortMapping& mapping_): - igd(igd_), mapping(mapping_) {} + void AddPortMapping(const PortMapping& mapping); + void RemovePortMapping(const PortMapping& mapping); - IGD igd; - PortMapping mapping; - }; - - // Main thread notification: - struct Notify { - virtual ~Notify() {} - }; - - struct NotifySearchResult: Notify { - NotifySearchResult(const std::string& device_id, const IGD& igd_): - DeviceId(device_id), igd(igd_) {} - - std::string DeviceId; - IGD igd; - }; - - struct NotifyActionComplete: Notify { - NotifyActionComplete(const ActionData& data, const std::string& action, int err_code): - igd(data.igd), Mapping(data.mapping), Action(action), ErrCode(err_code) {} - - // Mapping than was added or removed - IGD igd; - PortMapping Mapping; - - std::string Action; - int ErrCode; - }; - - virtual void OnThreadEvent(C4InteractiveEventType eEvent, void *pEventData); - - static int Callback_Static(Upnp_EventType EventType, void* Event, void* Cookie); - void OnSearchResult(const std::string& DeviceID, const IGD& igd); - void OnActionComplete(const IGD& igd, const PortMapping& mapping, const std::string& Action, int ErrCode); - - void AddPortMapping(const IGD& igd, const PortMapping& mapping); - void RemovePortMapping(const IGD& igd, const PortMapping& mapping); - - std::string outgoing_address; - std::map igds; std::vector added_mappings; - UpnpClient_Handle upnp_handle; + + bool initialized = false; + char lanaddr[64]; + UPNPDev *devlist = nullptr; + UPNPUrls upnp_urls; + IGDdatas igd_data; }; C4Network2UPnPP::C4Network2UPnPP() { - // Query outgoing network address only once at the beginning. We talk to - // the IGD (if any) via this address. - try + int error, status; + + if ((devlist = upnpDiscover(2000, NULL, NULL, UPNP_LOCAL_PORT_ANY, 0, 2, &error))) { - outgoing_address = GetOutgoingAddress(); - - int res = UpnpInit(outgoing_address.c_str(), 0); - if(res != UPNP_E_SUCCESS) - throw std::runtime_error(std::string("Failed to initialize UPnP: ") + UpnpGetErrorMessage(res)); - - C4InteractiveThread &Thread = Application.InteractiveThread; - Thread.SetCallback(Ev_UPNP_Response, this); - - res = UpnpRegisterClient(C4Network2UPnPP::Callback_Static, this, &upnp_handle); - if(res != UPNP_E_SUCCESS) - throw std::runtime_error(std::string("Failed to register UPnP client: ") + UpnpGetErrorMessage(res)); - - res = UpnpSearchAsync(upnp_handle, 5, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", this); - if(res != UPNP_E_SUCCESS) - throw std::runtime_error(std::string("Failed to search for IGDs: ") + UpnpGetErrorMessage(res)); + if ((status = UPNP_GetValidIGD(devlist, &upnp_urls, &igd_data, lanaddr, sizeof(lanaddr)))) + { + LogF("UPnP: Found IGD %s (status %d)", upnp_urls.controlURL, status); + initialized = true; + } + else + { + Log("UPnP: No IGD found."); + } } - catch(const std::runtime_error& error) + else { - LogF("Failed to initialize UPnP: %s", error.what()); - outgoing_address.clear(); + Log("UPnP: No UPnP device found on the network."); } + } C4Network2UPnPP::~C4Network2UPnPP() { ClearMappings(); - UpnpUnRegisterClient(upnp_handle); - - C4InteractiveThread &Thread = Application.InteractiveThread; - Thread.ClearCallback(Ev_UPNP_Response, this); - - UpnpFinish(); + FreeUPNPUrls(&upnp_urls); + freeUPNPDevlist(devlist); } void C4Network2UPnPP::AddMapping(C4Network2IOProtocol protocol, uint16_t intport, uint16_t extport) { PortMapping mapping; - mapping.external_hostname = ""; mapping.external_port = extport; - mapping.internal_hostname = outgoing_address; mapping.internal_port = intport; mapping.protocol = (protocol == P_TCP ? "TCP" : "UDP"); added_mappings.push_back(mapping); - for(std::map::const_iterator iter = igds.begin(); iter != igds.end(); ++iter) - AddPortMapping(iter->second, mapping); + AddPortMapping(mapping); } void C4Network2UPnPP::ClearMappings() { - for(std::map::const_iterator igd_iter = igds.begin(); igd_iter != igds.end(); ++igd_iter) - for(std::vector::const_iterator mapping_iter = added_mappings.begin(); mapping_iter != added_mappings.end(); ++mapping_iter) - RemovePortMapping(igd_iter->second, *mapping_iter); + for (auto mapping : added_mappings) + RemovePortMapping(mapping); added_mappings.clear(); } -// This function is called asynchronously from a libupnp thread. -// It is not allowed to call back into the library so we queue a function -// to be called by the main thread. The event data is not guaranteed to -// stay alive past this function call so we need to inspect the event type -// and preserve the event data we want to process later. -int C4Network2UPnPP::Callback_Static(Upnp_EventType EventType, void* Event, void* Cookie) +void C4Network2UPnPP::AddPortMapping(const PortMapping& mapping) { - //C4Network2UPnPP* upnp = static_cast(Cookie); + if (!initialized) return; // Catches the case that UPnP initialization failed - switch(EventType) - { - case UPNP_DISCOVERY_SEARCH_RESULT: - { - Upnp_Discovery* discovery = static_cast(Event); + auto eport = std::to_string(mapping.external_port); + auto iport = std::to_string(mapping.internal_port); - IGD igd; - igd.Location = discovery->Location; - igd.ServiceType = discovery->ServiceType; - - Application.InteractiveThread.PushEvent(Ev_UPNP_Response, new NotifySearchResult(discovery->DeviceId, igd)); - } - break; - case UPNP_CONTROL_ACTION_COMPLETE: - { - std::unique_ptr data(static_cast(Cookie)); - Upnp_Action_Complete* complete = static_cast(Event); - std::string action = ixmlNode_getNodeName(ixmlNode_getFirstChild(&complete->ActionRequest->n)); - Application.InteractiveThread.PushEvent(Ev_UPNP_Response, new NotifyActionComplete(*data, action, complete->ErrCode)); - } - break; - default: - Application.InteractiveThread.ThreadLogDebug("Unhandled UPNP event: %d", static_cast(EventType)); - break; - } - - return 0; + int r = UPNP_AddPortMapping(upnp_urls.controlURL, igd_data.first.servicetype, + eport.c_str(), iport.c_str(), lanaddr, description, + mapping.protocol.c_str(), 0, 0); + if (r != UPNPCOMMAND_SUCCESS) + LogF("UPnP: AddPortMapping failed with code %d (%s)", r, strupnperror(r)); } -void C4Network2UPnPP::OnThreadEvent(C4InteractiveEventType eEvent, void *pEventData) +void C4Network2UPnPP::RemovePortMapping(const PortMapping& mapping) { - std::unique_ptr notify(static_cast(pEventData)); + if(!initialized) return; // Catches the case that UPnP initialization failed - // TODO: Should call a virtual method instead of dynamic_casting - NotifySearchResult* notify_search_result = dynamic_cast(notify.get()); - NotifyActionComplete* notify_action_complete = dynamic_cast(notify.get()); + auto eport = std::to_string(mapping.external_port); - if(notify_search_result) - OnSearchResult(notify_search_result->DeviceId, notify_search_result->igd); - if(notify_action_complete) - OnActionComplete(notify_action_complete->igd, notify_action_complete->Mapping, notify_action_complete->Action, notify_action_complete->ErrCode); -} - -void C4Network2UPnPP::OnSearchResult(const std::string& DeviceID, const IGD& igd) -{ - // Make sure we don't find the same device twice - std::map::const_iterator iter = igds.find(DeviceID); - if(iter != igds.end()) return; - - // Add device - igds[DeviceID] = igd; - - // Add all port mappings with this device - for(std::vector::const_iterator mapping_iter = added_mappings.begin(); mapping_iter != added_mappings.end(); ++mapping_iter) - AddPortMapping(igd, *mapping_iter); -} - -void C4Network2UPnPP::OnActionComplete(const IGD& igd, const PortMapping& mapping, const std::string& Action, int ErrCode) -{ - // If adding failed with an error of 718 this means that this external port - // already exists in the port mapping table. Probably this was from some - // previous OpenClonk game. We remove it, and then, after it has been - // successfully removed, we try to add it again. - if(Action == "u:AddPortMapping" && ErrCode == 718) - RemovePortMapping(igd, mapping); - else if(Action == "u:DeletePortMapping" && ErrCode == 0) - AddPortMapping(igd, mapping); - else if(ErrCode != 0) - LogF("UPnP operation %s failed: %s", Action.c_str(), UpnpGetErrorMessage(ErrCode)); -} - -void C4Network2UPnPP::AddPortMapping(const IGD& igd, const PortMapping& mapping) -{ - if(igds.empty()) return; // Catches the case that UPnP initialization failed - - StdStrBuf external_port_buf, internal_port_buf; - external_port_buf.Format("%d", static_cast(mapping.external_port)); - internal_port_buf.Format("%d", static_cast(mapping.internal_port)); - - IXML_Document* action = UpnpMakeAction("AddPortMapping", - igd.ServiceType.c_str(), 8, - "NewRemoteHost", mapping.external_hostname.c_str(), - "NewExternalPort", external_port_buf.getData(), - "NewProtocol", mapping.protocol.c_str(), - "NewInternalPort", internal_port_buf.getData(), - "NewInternalClient", mapping.internal_hostname.c_str(), - "NewEnabled", "1", - "NewPortMappingDescription", C4ENGINECAPTION, - "NewLeaseDuration", "0"); - - UpnpSendActionAsync(upnp_handle, igd.Location.c_str(), igd.ServiceType.c_str(), NULL, action, Callback_Static, new ActionData(igd, mapping)); - ixmlDocument_free(action); -} - -void C4Network2UPnPP::RemovePortMapping(const IGD& igd, const PortMapping& mapping) -{ - if(igds.empty()) return; // Catches the case that UPnP initialization failed - - StdStrBuf external_port_buf; - external_port_buf.Format("%d", static_cast(mapping.external_port)); - - IXML_Document* action = UpnpMakeAction("DeletePortMapping", - igd.ServiceType.c_str(), 3, - "NewRemoteHost", mapping.external_hostname.c_str(), - "NewExternalPort", external_port_buf.getData(), - "NewProtocol", mapping.protocol.c_str()); - - UpnpSendActionAsync(upnp_handle, igd.Location.c_str(), igd.ServiceType.c_str(), NULL, action, Callback_Static, new ActionData(igd, mapping)); - ixmlDocument_free(action); + int r = UPNP_DeletePortMapping(upnp_urls.controlURL, igd_data.first.servicetype, + eport.c_str(), mapping.protocol.c_str(), 0); + if (r != UPNPCOMMAND_SUCCESS) + LogF("UPnP: DeletePortMapping failed with code %d (%s)", r, strupnperror(r)); } C4Network2UPnP::C4Network2UPnP():