From 2034136560319805f79bffca7102fc0488c83009 Mon Sep 17 00:00:00 2001 From: wraitii Date: Sun, 16 May 2021 15:34:38 +0000 Subject: [PATCH] Implement a workaround for routers without NAT loopback. This allows joining a lobby game hosted on the same network (behind the same NAT gateway). This is relatively primitive to keep things simple: if the server and the client have the same public IP, it is assumed that they are on the same network and the client instead requests the local IP. Differential Revision: https://code.wildfiregames.com/D3944 This was SVN commit r25448. --- .../public/gui/gamesetup_mp/gamesetup_mp.js | 1 + source/lobby/IXmppClient.h | 2 +- source/lobby/StanzaExtensions.cpp | 6 ++ source/lobby/StanzaExtensions.h | 3 +- source/lobby/XmppClient.cpp | 27 ++++++-- source/lobby/XmppClient.h | 2 +- source/network/NetClient.cpp | 13 +++- source/network/NetServer.cpp | 12 +++- source/network/NetServer.h | 11 ++++ source/network/StunClient.cpp | 35 ++++++++++- source/network/StunClient.h | 8 ++- .../network/scripting/JSInterface_Network.cpp | 2 +- source/network/tests/test_StunClient.h | 63 +++++++++++++++++++ 13 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 source/network/tests/test_StunClient.h diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js index 125ccf0199..11132396a3 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -165,6 +165,7 @@ function getConnectionFailReason(reason) 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 }); diff --git a/source/lobby/IXmppClient.h b/source/lobby/IXmppClient.h index 9bd2a4d497..574d97a89f 100644 --- a/source/lobby/IXmppClient.h +++ b/source/lobby/IXmppClient.h @@ -40,7 +40,7 @@ public: 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; diff --git a/source/lobby/StanzaExtensions.cpp b/source/lobby/StanzaExtensions.cpp index 985b1effe2..ed6957152c 100644 --- a/source/lobby/StanzaExtensions.cpp +++ b/source/lobby/StanzaExtensions.cpp @@ -300,6 +300,9 @@ ConnectionData::ConnectionData(const glooxwrapper::Tag* tag) 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 @@ ConnectionData::ConnectionData(const glooxwrapper::Tag* tag) 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 @@ glooxwrapper::Tag* ConnectionData::tag() const 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()) diff --git a/source/lobby/StanzaExtensions.h b/source/lobby/StanzaExtensions.h index 7304488d61..e8115572be 100644 --- a/source/lobby/StanzaExtensions.h +++ b/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 @@ public: glooxwrapper::string m_Ip; glooxwrapper::string m_Port; + glooxwrapper::string m_IsLocalIP; glooxwrapper::string m_UseSTUN; glooxwrapper::string m_Password; glooxwrapper::string m_Error; diff --git a/source/lobby/XmppClient.cpp b/source/lobby/XmppClient.cpp index 19fdae406e..ec966e13e6 100644 --- a/source/lobby/XmppClient.cpp +++ b/source/lobby/XmppClient.cpp @@ -370,12 +370,13 @@ void XmppClient::SendIqGetProfile(const std::string& player) /** * 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 @@ bool XmppClient::handleIq(const glooxwrapper::IQ& iq) 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 @@ bool XmppClient::handleIq(const glooxwrapper::IQ& iq) 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); diff --git a/source/lobby/XmppClient.h b/source/lobby/XmppClient.h index a782c767f5..ac52386cbc 100644 --- a/source/lobby/XmppClient.h +++ b/source/lobby/XmppClient.h @@ -86,7 +86,7 @@ public: 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); diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 1098bb3b00..49b7fb6c2c 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -260,7 +260,8 @@ bool CNetClient::TryToConnect(const CStr& hostJID) } StunClient::StunEndpoint stunEndpoint; - if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint)) + CStr ip; + if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint, ip)) { PushGuiMessage( "type", "netstatus", @@ -269,6 +270,16 @@ bool CNetClient::TryToConnect(const CStr& hostJID) 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); diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index d614cf8286..8c27229fc7 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -1640,14 +1640,22 @@ bool CNetServer::SetupConnection(const u16 port) 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) diff --git a/source/network/NetServer.h b/source/network/NetServer.h index c474295547..6d15b12148 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -153,10 +153,21 @@ public: 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 diff --git a/source/network/StunClient.cpp b/source/network/StunClient.cpp index 90ec38f377..dc18ec0e61 100644 --- a/source/network/StunClient.cpp +++ b/source/network/StunClient.cpp @@ -371,7 +371,7 @@ bool STUNRequestAndResponse(ENetHost& transactionHost) 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 @@ bool StunClient::FindStunEndpointHost(CStr8& ip, u16& port) 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 @@ bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::Stu 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 @@ void StunClient::SendHolePunchingMessages(ENetHost& enetClient, const std::strin 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; +} diff --git a/source/network/StunClient.h b/source/network/StunClient.h index baf5dd26b1..538be42aee 100644 --- a/source/network/StunClient.h +++ b/source/network/StunClient.h @@ -37,9 +37,15 @@ void SendStunRequest(ENetHost& transactionHost, u32 targetIp, u16 targetPort); 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 diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index 5a5bc91151..6564aef125 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -191,7 +191,7 @@ void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const C 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() diff --git a/source/network/tests/test_StunClient.h b/source/network/tests/test_StunClient.h new file mode 100644 index 0000000000..5c33879053 --- /dev/null +++ b/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"); + } +};