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.
This commit is contained in:
wraitii 2021-05-16 15:34:38 +00:00
parent a19dc1717f
commit 2034136560
13 changed files with 170 additions and 15 deletions

View File

@ -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 });

View File

@ -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;

View File

@ -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())

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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<std::mutex> 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)

View File

@ -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

View File

@ -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<u16>(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;
}

View File

@ -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

View File

@ -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()

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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");
}
};