Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ ps/trunk/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: ps/trunk/source/lobby/IXmppClient.h =================================================================== --- ps/trunk/source/lobby/IXmppClient.h +++ ps/trunk/source/lobby/IXmppClient.h @@ -40,7 +40,7 @@ virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptRequest& rq, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptRequest& rq, 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: ps/trunk/source/lobby/StanzaExtensions.h =================================================================== --- ps/trunk/source/lobby/StanzaExtensions.h +++ ps/trunk/source/lobby/StanzaExtensions.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* 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 @@ -60,6 +60,7 @@ glooxwrapper::string m_Ip; glooxwrapper::string m_Port; + glooxwrapper::string m_IsLocalIP; glooxwrapper::string m_UseSTUN; glooxwrapper::string m_Password; glooxwrapper::string m_Error; Index: ps/trunk/source/lobby/StanzaExtensions.cpp =================================================================== --- ps/trunk/source/lobby/StanzaExtensions.cpp +++ ps/trunk/source/lobby/StanzaExtensions.cpp @@ -300,6 +300,9 @@ const glooxwrapper::Tag* p= tag->findTag_clone("connectiondata/port"); if (p) m_Port = p->cdata(); + const glooxwrapper::Tag* pip = tag->findTag_clone("connectiondata/isLocalIP"); + if (pip) + m_IsLocalIP = pip->cdata(); const glooxwrapper::Tag* s = tag->findTag_clone("connectiondata/useSTUN"); if (s) m_UseSTUN = s->cdata(); @@ -312,6 +315,7 @@ glooxwrapper::Tag::free(c); glooxwrapper::Tag::free(p); + glooxwrapper::Tag::free(pip); glooxwrapper::Tag::free(s); glooxwrapper::Tag::free(pw); glooxwrapper::Tag::free(e); @@ -338,6 +342,8 @@ t->addChild(glooxwrapper::Tag::allocate("ip", m_Ip)); if (!m_Port.empty()) t->addChild(glooxwrapper::Tag::allocate("port", m_Port)); + if (!m_IsLocalIP.empty()) + t->addChild(glooxwrapper::Tag::allocate("isLocalIP", m_IsLocalIP)); if (!m_UseSTUN.empty()) t->addChild(glooxwrapper::Tag::allocate("useSTUN", m_UseSTUN)); if (!m_Password.empty()) Index: ps/trunk/source/lobby/XmppClient.h =================================================================== --- ps/trunk/source/lobby/XmppClient.h +++ ps/trunk/source/lobby/XmppClient.h @@ -86,7 +86,7 @@ void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptRequest& rq, JS::HandleValue data); void SendIqRegisterGame(const ScriptRequest& rq, 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: ps/trunk/source/lobby/XmppClient.cpp =================================================================== --- ps/trunk/source/lobby/XmppClient.cpp +++ ps/trunk/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_IsLocalIP = localIP ? "1" : "0"; glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID()); iq.addExtension(connectionData); m_connectionDataJid = iq.from().full(); @@ -978,7 +979,7 @@ m_client->send(response); return true; } - if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.c_str()), iq.from().username())) + if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.to_string()), iq.from().username())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); @@ -992,9 +993,25 @@ 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_IsLocalIP.to_string() == "0") + { + 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->GetLocalPort()); + connectionData->m_UseSTUN = ""; + } + else + connectionData->m_Error = "local_ip_failed"; + } response.addExtension(connectionData); Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp +++ ps/trunk/source/network/NetClient.cpp @@ -260,7 +260,8 @@ } StunClient::StunEndpoint stunEndpoint; - if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint)) + CStr ip; + if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint, ip)) { PushGuiMessage( "type", "netstatus", @@ -269,6 +270,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: ps/trunk/source/network/NetServer.h =================================================================== --- ps/trunk/source/network/NetServer.h +++ ps/trunk/source/network/NetServer.h @@ -153,11 +153,22 @@ bool GetUseSTUN() const; + /** + * Return the externally accessible IP. + */ CStr GetPublicIp() const; + /** + * Return the externally accessible port. + */ u16 GetPublicPort() const; /** + * Return the serving port on the local machine. + */ + u16 GetLocalPort() const; + + /** * Check if password is valid. If is not, increase number of failed attempts of the lobby user. * This is used without established direct session with the client, to prevent brute force attacks * when guessing password trying to get connection data from the host. Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp +++ ps/trunk/source/network/NetServer.cpp @@ -1640,14 +1640,22 @@ return m_Worker->SetupConnection(port); } +CStr CNetServer::GetPublicIp() const +{ + return m_PublicIp; +} + u16 CNetServer::GetPublicPort() const { return m_PublicPort; } -CStr CNetServer::GetPublicIp() const +u16 CNetServer::GetLocalPort() const { - return m_PublicIp; + std::lock_guard lock(m_Worker->m_WorkerMutex); + if (!m_Worker->m_Host) + return 0; + return m_Worker->m_Host->address.port; } void CNetServer::SetConnectionData(const CStr& ip, const u16 port, bool useSTUN) Index: ps/trunk/source/network/StunClient.h =================================================================== --- ps/trunk/source/network/StunClient.h +++ ps/trunk/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: ps/trunk/source/network/StunClient.cpp =================================================================== --- ps/trunk/source/network/StunClient.cpp +++ ps/trunk/source/network/StunClient.cpp @@ -371,7 +371,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); @@ -394,7 +394,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; @@ -405,6 +405,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; @@ -428,3 +429,33 @@ usleep(delay * 1000); } } + +bool StunClient::FindLocalIP(CStr& ip) +{ + // Open an UDP socket. + ENetSocket socket = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); + + ENetAddress addr; + addr.port = 9; // Use the debug port (which we pick does not matter). + // Connect to a random address. It does not need to be valid, only to not be the loopback address. + if (enet_address_set_host(&addr, "100.0.100.0") == -1) + return false; + + // Connect the socket. Being UDP, there is no actual outgoing traffic, this just binds it + // to a valid port locally, allowing us to get the local IP of the machine. + if (enet_socket_connect(socket, &addr) == -1) + return false; + + // Fetch the local port & IP. + if (enet_socket_get_address(socket, &addr) == -1) + return false; + + // Convert to a human readable string. + char buf[INET_ADDRSTRLEN]; + if (enet_address_get_host_ip(&addr, buf, INET_ADDRSTRLEN) == -1) + return false; + + ip = buf; + + return true; +} Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp =================================================================== --- ps/trunk/source/network/scripting/JSInterface_Network.cpp +++ ps/trunk/source/network/scripting/JSInterface_Network.cpp @@ -191,7 +191,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: ps/trunk/source/network/tests/test_StunClient.h =================================================================== --- ps/trunk/source/network/tests/test_StunClient.h +++ ps/trunk/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: wrong size"); + 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: wrong character"); + return; + } + } + if (dots != 3) + TS_FAIL("StunClient::FindLocalIP did not return a valid IPV4 address: wrong separators"); + } +};