Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -165,6 +165,7 @@ case "not_server": return translate("Server is not running."); case "invalid_password": return translate("Password is invalid."); case "banned": return translate("You have been banned."); + case "local_ip_failed": return translate("Failed to get local IP of the server (it was assumed to be on the same network)."); default: warn("Unknown connection failure reason: " + reason); return sprintf(translate("\\[Invalid value %(reason)s]"), { "reason": reason }); Index: source/lobby/IXmppClient.h =================================================================== --- source/lobby/IXmppClient.h +++ source/lobby/IXmppClient.h @@ -39,7 +39,7 @@ virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; - virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password) = 0; + virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP) = 0; virtual void SendIqUnregisterGame() = 0; virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0; virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0; Index: source/lobby/XmppClient.h =================================================================== --- source/lobby/XmppClient.h +++ source/lobby/XmppClient.h @@ -86,7 +86,7 @@ void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data); - void SendIqGetConnectionData(const std::string& jid, const std::string& password); + void SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP); void SendIqUnregisterGame(); void SendIqChangeStateGame(const std::string& nbp, const std::string& players); void SendIqLobbyAuth(const std::string& to, const std::string& token); Index: source/lobby/XmppClient.cpp =================================================================== --- source/lobby/XmppClient.cpp +++ source/lobby/XmppClient.cpp @@ -370,12 +370,13 @@ /** * Request the Connection data (ip, port...) from the server. */ -void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password) +void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP) { glooxwrapper::JID targetJID(jid); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Password = password; + connectionData->m_UseSTUN = localIP ? "0" : "1"; glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID()); iq.addExtension(connectionData); m_connectionDataJid = iq.from().full(); @@ -1000,9 +1001,24 @@ glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); - connectionData->m_Ip = g_NetServer->GetPublicIp();; - connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort()); - connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : ""; + if (cd->m_UseSTUN == "1") + { + connectionData->m_Ip = g_NetServer->GetPublicIp(); + connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort()); + connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : ""; + } + else + { + CStr ip; + if (StunClient::FindLocalIP(ip)) + { + connectionData->m_Ip = ip; + connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort()); + connectionData->m_UseSTUN = ""; + } + else + connectionData->m_Error = "local_ip_failed"; + } response.addExtension(connectionData); Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -259,7 +259,8 @@ } StunClient::StunEndpoint stunEndpoint; - if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint)) + CStr ip; + if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint, ip)) { PushGuiMessage( "type", "netstatus", @@ -268,6 +269,16 @@ return false; } + // If the host is on the same network, we risk failing to connect + // on routers that don't support NAT hairpinning/NAT loopback. + // To work around that, send again a connection data request, but for internal IP this time. + if (ip == m_ServerAddress) + { + g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, true); + // Return true anyways - we're on a success path here. + return true; + } + g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID); SDL_Delay(1000); Index: source/network/StunClient.h =================================================================== --- source/network/StunClient.h +++ source/network/StunClient.h @@ -37,9 +37,15 @@ bool FindStunEndpointHost(CStr8& ip, u16& port); -bool FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint); +bool FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint, CStr8& ip); void SendHolePunchingMessages(ENetHost& enetClient, const std::string& serverAddress, u16 serverPort); + +/** + * Return the local IP. + * Technically not a STUN method, but convenient to define here. + */ +bool FindLocalIP(CStr8& ip); } #endif // STUNCLIENT_H Index: source/network/StunClient.cpp =================================================================== --- source/network/StunClient.cpp +++ source/network/StunClient.cpp @@ -31,6 +31,7 @@ # include # include #else +# include # include # include #endif @@ -369,7 +370,7 @@ ParseStunResponse(buffer); } -bool StunClient::FindStunEndpointHost(CStr8& ip, u16& port) +bool StunClient::FindStunEndpointHost(CStr& ip, u16& port) { ENetAddress hostAddr{ENET_HOST_ANY, static_cast(port)}; ENetHost* transactionHost = enet_host_create(&hostAddr, 1, 1, 0, 0); @@ -392,7 +393,7 @@ return result == 0; } -bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint) +bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint, CStr& ip) { if (!STUNRequestAndResponse(transactionHost)) return false; @@ -403,6 +404,7 @@ addr.host = ntohl(m_IP); enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); + ip = ipStr; stunEndpoint.ip = m_IP; stunEndpoint.port = m_Port; @@ -426,3 +428,64 @@ usleep(delay * 1000); } } + +bool StunClient::FindLocalIP(CStr& ip) +{ +#ifdef WIN32 +#define CLOSESOCKET closesocket +#elif OS_UNIX +#define CLOSESOCKET close +#else + // Not a vital component of the game, warn and carry on. +# warning "FindLocalIP not defined on this system." + UNUSED2(ip); + return false; +#endif + + // Step 0: open an ipv4 UDP socket. + int udp_socket = socket(PF_INET, SOCK_DGRAM, 0); + if (udp_socket == -1) + return false; + + // Step 1: connect() to an arbitrary address. + // As a UDP socket, this simply sets the default destination/reception address (no outgoing traffic) + // and binds the socket to INADDR_ANY on a non-specified port. + // This makes it possible to use this even without an internet connection. + // NB: the assumption is that this non-specific 'entrant' address will match what 0 A.D. + // uses locally, and so we can reuse it for the actual net traffic. + // I don't believe there is actually a strong guarantee that this is the case, since users could + // probably set things up with port-forwarding, but those users probably would know how to make it work + // anyways, so this is likely a "good enough" default. + sockaddr_in loopback; + memset(&loopback, 0, sizeof(loopback)); + loopback.sin_family = AF_INET; + loopback.sin_addr.s_addr = 1337; // Non-specific IP. The only restriction is that it's not localhost. + loopback.sin_port = htons(9); // UDP debug port (this also doesn't matter). + + if (connect(udp_socket, reinterpret_cast(&loopback), sizeof(loopback)) == -1) + { + CLOSESOCKET(udp_socket); + return false; + } + + // Step 2: get the local network name of socket, i.e. the local IP. + socklen_t addrlen = sizeof(loopback); + if (getsockname(udp_socket, reinterpret_cast(&loopback), &addrlen) == -1) + { + CLOSESOCKET(udp_socket); + return false; + } + + CLOSESOCKET(udp_socket); + + // Step 3: Convert to human-readable string via inet_ntop (which is also IPV6 ready). + // This could also be done via enet_address_get_host_ip like above, + // but since this doesn't use enet sockets, it seemed more natural to use something standard. + char buf[INET_ADDRSTRLEN]; + if (inet_ntop(AF_INET, &loopback.sin_addr, buf, INET_ADDRSTRLEN) == nullptr) + return false; + + ip = buf; + + return true; +} Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -190,7 +190,7 @@ g_NetClient->SetUserName(playerName); g_NetClient->SetHostJID(hostJID); g_NetClient->SetGamePassword(hashedPass); - g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str()); + g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str(), false); } void DisconnectNetworkGame() Index: source/network/tests/test_StunClient.h =================================================================== --- /dev/null +++ source/network/tests/test_StunClient.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "network/StunClient.h" + +#include "lib/external_libraries/enet.h" +#include "ps/CStr.h" + +class TestStunClient : public CxxTest::TestSuite +{ +public: + void setUp() + { + // Sets networking up in a cross-platform manner. + enet_initialize(); + } + + void tearDown() + { + enet_deinitialize(); + } + + void test_local_ip() + { + CStr ip; + TS_ASSERT(StunClient::FindLocalIP(ip)); + // Quick validation that this looks like a valid IP address. + if (ip.size() < 8 || ip.size() > 15) + { + TS_FAIL("StunClient::FindLocalIP did not return a valid IPV4 address."); + return; + } + int dots = 0; + for (char c : ip) + { + if (c == '.') + ++dots; + else if (c < '0' && c > '9') + { + TS_FAIL("StunClient::FindLocalIP did not return a valid IPV4 address."); + return; + } + } + if (dots != 3) + TS_FAIL("StunClient::FindLocalIP did not return a valid IPV4 address."); + } +};