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:
parent
a19dc1717f
commit
2034136560
@ -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 });
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
63
source/network/tests/test_StunClient.h
Normal file
63
source/network/tests/test_StunClient.h
Normal 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");
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user