diff --git a/CMakeLists.txt b/CMakeLists.txt index 897ab9794..80d741dd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ option(WEBSOCKETS "Enable websockets support" OFF) option(MYSQL "Enable mysql support" OFF) option(AUTOUPDATE "Enable the autoupdater" ${AUTOUPDATE_DEFAULT}) option(VIDEORECORDER "Enable video recording support via FFmpeg" OFF) +option(UPNP "Enable UPnP support" OFF) option(ANTIBOT "Enable support for a dynamic anticheat library" OFF) option(CLIENT "Compile client" ON) option(DOWNLOAD_GTEST "Download and compile GTest" ${AUTO_DEPENDENCIES_DEFAULT}) @@ -330,6 +331,9 @@ endif() find_package(Ogg) find_package(Opus) find_package(Opusfile) +if(UPNP) + find_package(Miniupnpc) +endif() find_package(Pnglite) find_package(PythonInterp) find_package(SDL2) @@ -407,6 +411,9 @@ show_dependency_status("Ogg" OGG) show_dependency_status("OpenSSL Crypto" CRYPTO) show_dependency_status("Opus" OPUS) show_dependency_status("Opusfile" OPUSFILE) +if(UPNP) + show_dependency_status("Miniupnpc" MINIUPNPC) +endif() show_dependency_status("Pnglite" PNGLITE) show_dependency_status("PythonInterp" PYTHONINTERP) show_dependency_status("SDL2" SDL2) @@ -434,6 +441,10 @@ if(WEBSOCKETS AND NOT(WEBSOCKETS_FOUND)) message(SEND_ERROR "You must install libwebsockets to compile the DDNet server with websocket support") endif() +if(UPNP AND NOT(MINIUPNPC_FOUND)) + message(SEND_ERROR "You must install miniupnpc to compile the DDNet server with UPnP support") +endif() + if(CLIENT AND NOT(FREETYPE_FOUND)) message(SEND_ERROR "You must install Freetype to compile the DDNet client") endif() @@ -1081,6 +1092,8 @@ set_src(ENGINE_SERVER GLOB src/engine/server sql_server.h sql_string_helpers.cpp sql_string_helpers.h + upnp.cpp + upnp.h ) set_src(GAME_SERVER GLOB_RECURSE src/game/server ddracechat.cpp @@ -1156,6 +1169,7 @@ set(LIBS_SERVER ${LIBS} ${MYSQL_LIBRARIES} ${TARGET_ANTIBOT} + ${MINIUPNPC_LIBRARIES} # Add pthreads (on non-Windows) at the end, so that other libraries can depend # on it. ${CMAKE_THREAD_LIBS_INIT} @@ -1735,6 +1749,10 @@ foreach(target ${TARGETS_OWN}) target_compile_definitions(${target} PRIVATE CONF_WEBSOCKETS) target_include_directories(${target} PRIVATE ${WEBSOCKETS_INCLUDE_DIRS}) endif() + if(UPNP) + target_compile_definitions(${target} PRIVATE CONF_UPNP) + target_include_directories(${target} PRIVATE ${MINIUPNPC_INCLUDE_DIRS}) + endif() if(VIDEORECORDER) target_compile_definitions(${target} PRIVATE CONF_VIDEORECORDER) endif() diff --git a/README.md b/README.md index 2196e1f4e..e7b19eeaa 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ Whether to download and compile GTest. Useful if GTest is not installed and, for * **-DDEV=[ON|OFF]**
Whether to optimize for development, speeding up the compilation process a little. If enabled, don't generate stuff necessary for packaging. Setting to ON will set CMAKE\_BUILD\_TYPE to Debug by default. Default value is OFF. +* **-DUPNP=[ON|OFF]**
+Whether to enable UPnP support for the server. +You need to install `libminiupnpc-dev` on Debian, `miniupnpc` on Arch Linux. + * **-GNinja**
Use the Ninja build system instead of Make. This automatically parallizes the build and is generally faster. Compile with `ninja` instead of `make`. Install Ninja with `sudo apt install ninja-build` on Debian, `sudo pacman -S --needed ninja` on Arch Linux. diff --git a/cmake/FindMiniupnpc.cmake b/cmake/FindMiniupnpc.cmake new file mode 100644 index 000000000..b9ffdd5e2 --- /dev/null +++ b/cmake/FindMiniupnpc.cmake @@ -0,0 +1,33 @@ +if(NOT CMAKE_CROSSCOMPILING) + find_package(PkgConfig QUIET) + pkg_check_modules(PC_MINIUPNPC miniupnpc) +endif() + +set_extra_dirs_lib(MINIUPNPC miniupnpc) +find_library(MINIUPNPC_LIBRARY + NAMES miniupnpc + HINTS ${HINTS_MINIUPNPC_LIBDIR} ${PC_MINIUPNPC_LIBDIR} ${PC_MINIUPNPC_LIBRARY_DIRS} + PATHS ${PATHS_MINIUPNPC_LIBDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} +) +set_extra_dirs_include(MINIUPNPC miniupnpc "${MINIUPNPC_LIBRARY}") +find_path(MINIUPNPC_INCLUDEDIR miniupnpc.h + PATH_SUFFIXES miniupnpc + HINTS ${HINTS_MINIUPNPC_INCLUDEDIR} ${PC_MINIUPNPC_INCLUDEDIR} ${PC_MINIUPNPC_INCLUDE_DIRS} + PATHS ${PATHS_MINIUPNPC_INCLUDEDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Miniupnpc DEFAULT_MSG MINIUPNPC_INCLUDEDIR) + +mark_as_advanced(MINIUPNPC_INCLUDEDIR MINIUPNPC_LIBRARY) + +if(MINIUPNPC_FOUND) + set(MINIUPNPC_INCLUDE_DIRS ${MINIUPNPC_INCLUDEDIR}) + if(MINIUPNPC_LIBRARY) + set(MINIUPNPC_LIBRARIES ${MINIUPNPC_LIBRARY}) + else() + set(MINIUPNPC_LIBRARIES) + endif() +endif() diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index dfca811da..84ebc6196 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -1991,6 +1991,10 @@ int CServer::Run() BindAddr.port = g_Config.m_SvPort; } +#if defined(CONF_UPNP) + m_UPnP.Open(BindAddr); +#endif + if(!m_NetServer.Open(BindAddr, &m_ServerBan, g_Config.m_SvMaxClients, g_Config.m_SvMaxClientsPerIP, 0)) { dbg_msg("server", "couldn't open socket. port %d might already be in use", g_Config.m_SvPort); @@ -2262,6 +2266,10 @@ int CServer::Run() } #endif +#if defined (CONF_UPNP) + m_UPnP.Shutdown(); +#endif + return ErrorShutdown(); } diff --git a/src/engine/server/server.h b/src/engine/server/server.h index 03e785191..38a99ec75 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -29,6 +29,10 @@ #include "authmanager.h" #include "name_ban.h" +#if defined (CONF_UPNP) + #include "upnp.h" +#endif + #if defined (CONF_SQL) #include "sql_connector.h" #include "sql_server.h" @@ -94,6 +98,10 @@ class CServer : public IServer class IStorage *m_pStorage; class IEngineAntibot *m_pAntibot; +#if defined(CONF_UPNP) + CUPnP m_UPnP; +#endif + #if defined(CONF_SQL) lock m_GlobalSqlLock; diff --git a/src/engine/server/upnp.cpp b/src/engine/server/upnp.cpp new file mode 100644 index 000000000..6649d14fd --- /dev/null +++ b/src/engine/server/upnp.cpp @@ -0,0 +1,74 @@ +#ifdef CONF_UPNP + +#include +#include +#include +#include +#include +#include +#include "upnp.h" + +void CUPnP::Open(NETADDR Address) +{ + if(g_Config.m_SvUseUPnP) + { + m_Enabled = false; + m_Addr = Address; + m_UPnPUrls = (struct UPNPUrls *)malloc(sizeof(struct UPNPUrls)); + m_UPnPData = (struct IGDdatas *)malloc(sizeof(struct IGDdatas)); + + char aLanAddr[64]; + char aPort[6]; + int Error; + + m_UPnPDevice = upnpDiscover(2000, NULL, NULL, 0, 0, 2, &Error); + + int Status = UPNP_GetValidIGD(m_UPnPDevice, m_UPnPUrls, m_UPnPData, aLanAddr, sizeof(aLanAddr)); + dbg_msg("upnp", "status=%d, lan_addr=%s", Status, aLanAddr); + + if(Status == 1) + { + m_Enabled = true; + dbg_msg("upnp", "found valid IGD: %s", m_UPnPUrls->controlURL); + str_format(aPort, sizeof(aPort), "%d", m_Addr.port); + Error = UPNP_AddPortMapping(m_UPnPUrls->controlURL, m_UPnPData->first.servicetype, + aPort, aPort, aLanAddr, + "DDNet Server " GAME_RELEASE_VERSION, + "UDP", NULL, "0"); + + if(Error) + dbg_msg("upnp", "failed to map port, error: %s", strupnperror(Error)); + else + dbg_msg("upnp", "successfully mapped port"); + } + else + dbg_msg("upnp", "no valid IGD found, disabled"); + } +} + +void CUPnP::Shutdown() +{ + if(g_Config.m_SvUseUPnP) + { + if(m_Enabled) + { + char aPort[6]; + str_format(aPort, sizeof(aPort), "%d", m_Addr.port); + int Error = UPNP_DeletePortMapping(m_UPnPUrls->controlURL, m_UPnPData->first.servicetype, aPort, "UDP", NULL); + + if(Error != 0) + { + dbg_msg("upnp", "failed to delete port mapping on shutdown: %s", strupnperror(Error)); + } + FreeUPNPUrls(m_UPnPUrls); + freeUPNPDevlist(m_UPnPDevice); + } + free(m_UPnPUrls); + free(m_UPnPData); + m_UPnPUrls = NULL; + m_UPnPData = NULL; + } + +} + +#endif \ No newline at end of file diff --git a/src/engine/server/upnp.h b/src/engine/server/upnp.h new file mode 100644 index 000000000..58a269ea4 --- /dev/null +++ b/src/engine/server/upnp.h @@ -0,0 +1,18 @@ +#ifndef ENGINE_SERVER_UPNP_H +#define ENGINE_SERVER_UPNP_H + +#include +class CUPnP +{ + NETADDR m_Addr; + struct UPNPUrls *m_UPnPUrls; + struct IGDdatas *m_UPnPData; + struct UPNPDev *m_UPnPDevice; + bool m_Enabled; + +public: + void Open(NETADDR Address); + void Shutdown(); +}; + +#endif diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 5386c26b0..beefc6b0a 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -221,6 +221,10 @@ MACRO_CONFIG_STR(SvSqlFailureFile, sv_sql_failure_file, 64, "failed_sql.sql", CF MACRO_CONFIG_INT(SvSqlQueriesDelay, sv_sql_queries_delay, 1, 0, 20, CFGFLAG_SERVER, "Delay in seconds between SQL queries of a single player") #endif +#if defined(CONF_UPNP) +MACRO_CONFIG_INT(SvUseUPnP, sv_use_upnp, 0, 0, 1, CFGFLAG_SERVER, "Enables UPnP support.") +#endif + MACRO_CONFIG_INT(SvDDRaceRules, sv_ddrace_rules, 1, 0, 1, CFGFLAG_SERVER, "Whether the default mod rules are displayed or not") MACRO_CONFIG_STR(SvRulesLine1, sv_rules_line1, 128, "", CFGFLAG_SERVER, "Rules line 1") MACRO_CONFIG_STR(SvRulesLine2, sv_rules_line2, 128, "", CFGFLAG_SERVER, "Rules line 2")