mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Current issue with the lobby, is that we make ips of hosts public for anyone to read. This patch consists of 3 parts. 1.) Removing ips and ports from lobby javascript 2.) Removing need of script on the server to attach public ips to game stanza by asking the host using xmppclient as proxy. 3.) Implementing password protected matches, to deny this information to not trusted players. Further description: Do not send ports and stunip to the bots. Removed from stanza. Do not send ip to the lobby. Removed from mapping gamelist from backend to gui (still on the backend side, because it is done by script on 0ad server). Get ip and ports on request when trying to connect. On the host side, ask stun server what is host's public ip and remember it. On the client side, send iq through xmppclient to the hosting player and ask for ip, port and if Stun is used, then if answer is success, continue with connecting, else fail. Add optional password for matches. Add password required identifier to the stanza. Allow host to setup password for the match. Hash it on the host side and store inside Netserver. If no password is given, matches will behave as it is not required. On the client side, if password for the match is required, show additional window before trying to connect and ask for password, then hash it and send with iq request for ip, port and stun. Server will answer with ip, port and stun only if passwords matches, else will asnwer with error string. Some security: Passwords are hashed before sending, so it is not easy to guess what users typed. (per wraitii) Hashes are using different salt as lobby hashing and not using usernames as salt (as that is not doable), so they are different even typing the same password as for the lobby account. Client remembers which user was asked for connection data and iq's id of request. If answer doesn't match these things, it is ignored. (thnx user1) Every request for connection data is logged with hostname of the requester to the mainlog file (no ips). If user gets iq to send connection data and is not hosting the match, will respond with error string "not_server". If server gets iq::result with connection data, request is ignored. Differential revision: D3184 Reviewed by: @wraitii Comments by: @Stan, @bb, @Imarok, @vladislavbelov Tested in lobby This was SVN commit r24728.
435 lines
10 KiB
C++
435 lines
10 KiB
C++
/* Copyright (C) 2021 Wildfire Games.
|
|
* Copyright (C) 2013-2016 SuperTuxKart-Team.
|
|
* 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 "precompiled.h"
|
|
|
|
#include "StunClient.h"
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <sys/types.h>
|
|
#ifdef WIN32
|
|
# include <winsock2.h>
|
|
# include <ws2tcpip.h>
|
|
#else
|
|
# include <sys/socket.h>
|
|
# include <netdb.h>
|
|
#endif
|
|
|
|
#include <vector>
|
|
|
|
#include "lib/external_libraries/enet.h"
|
|
|
|
#if OS_WIN
|
|
#include "lib/sysdep/os/win/wposix/wtime.h"
|
|
#endif
|
|
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/ConfigDB.h"
|
|
#include "ps/CStr.h"
|
|
|
|
unsigned int m_StunServerIP;
|
|
int m_StunServerPort;
|
|
|
|
/**
|
|
* These constants are defined in Section 6 of RFC 5389.
|
|
*/
|
|
const u32 m_MagicCookie = 0x2112A442;
|
|
const u32 m_MethodTypeBinding = 0x0001;
|
|
const u32 m_BindingSuccessResponse = 0x0101;
|
|
|
|
/**
|
|
* Bit determining whether comprehension of an attribute is optional.
|
|
* Described in Section 15 of RFC 5389.
|
|
*/
|
|
const u16 m_ComprehensionOptional = 0x1 << 15;
|
|
|
|
/**
|
|
* Bit determining whether the bit was assigned by IETF Review.
|
|
* Described in section 18.1. of RFC 5389.
|
|
*/
|
|
const u16 m_IETFReview = 0x1 << 14;
|
|
|
|
/**
|
|
* These constants are defined in Section 15.1 of RFC 5389.
|
|
*/
|
|
const u8 m_IPAddressFamilyIPv4 = 0x01;
|
|
|
|
/**
|
|
* These constants are defined in Section 18.2 of RFC 5389.
|
|
*/
|
|
const u16 m_AttrTypeMappedAddress = 0x001;
|
|
const u16 m_AttrTypeXORMappedAddress = 0x0020;
|
|
|
|
/**
|
|
* Described in section 3 of RFC 5389.
|
|
*/
|
|
u8 m_TransactionID[12];
|
|
|
|
/**
|
|
* Discovered STUN endpoint
|
|
*/
|
|
u32 m_IP;
|
|
u16 m_Port;
|
|
|
|
void AddUInt16(std::vector<u8>& buffer, const u16 value)
|
|
{
|
|
buffer.push_back((value >> 8) & 0xff);
|
|
buffer.push_back(value & 0xff);
|
|
}
|
|
|
|
void AddUInt32(std::vector<u8>& buffer, const u32 value)
|
|
{
|
|
buffer.push_back((value >> 24) & 0xff);
|
|
buffer.push_back((value >> 16) & 0xff);
|
|
buffer.push_back((value >> 8) & 0xff);
|
|
buffer.push_back( value & 0xff);
|
|
}
|
|
|
|
template<typename T, size_t n>
|
|
bool GetFromBuffer(const std::vector<u8>& buffer, u32& offset, T& result)
|
|
{
|
|
if (offset + n > buffer.size())
|
|
return false;
|
|
|
|
int a = n;
|
|
offset += n;
|
|
while (a--)
|
|
{
|
|
// Prevent shift count overflow if the type is u8
|
|
if (n > 1)
|
|
result <<= 8;
|
|
|
|
result += buffer[offset - 1 - a];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Creates a STUN request and sends it to a STUN server.
|
|
* The request is sent through transactionHost, from which the answer
|
|
* will be retrieved by ReceiveStunResponse and interpreted by ParseStunResponse.
|
|
*/
|
|
bool CreateStunRequest(ENetHost& transactionHost)
|
|
{
|
|
CStr server_name;
|
|
CFG_GET_VAL("lobby.stun.server", server_name);
|
|
CFG_GET_VAL("lobby.stun.port", m_StunServerPort);
|
|
|
|
debug_printf("GetPublicAddress: Using STUN server %s:%d\n", server_name.c_str(), m_StunServerPort);
|
|
|
|
addrinfo hints;
|
|
addrinfo* res;
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
// Resolve the stun server name so we can send it a STUN request
|
|
int status = getaddrinfo(server_name.c_str(), nullptr, &hints, &res);
|
|
if (status != 0)
|
|
{
|
|
#ifdef UNICODE
|
|
LOGERROR("GetPublicAddress: Error in getaddrinfo: %s", utf8_from_wstring(gai_strerror(status)));
|
|
#else
|
|
LOGERROR("GetPublicAddress: Error in getaddrinfo: %s", gai_strerror(status));
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
ENSURE(res);
|
|
|
|
// Documentation says it points to "one or more addrinfo structures"
|
|
sockaddr_in* current_interface = reinterpret_cast<sockaddr_in*>(res->ai_addr);
|
|
m_StunServerIP = ntohl(current_interface->sin_addr.s_addr);
|
|
|
|
StunClient::SendStunRequest(transactionHost, m_StunServerIP, m_StunServerPort);
|
|
|
|
freeaddrinfo(res);
|
|
return true;
|
|
}
|
|
|
|
void StunClient::SendStunRequest(ENetHost& transactionHost, u32 targetIp, u16 targetPort)
|
|
{
|
|
std::vector<u8> buffer;
|
|
AddUInt16(buffer, m_MethodTypeBinding);
|
|
AddUInt16(buffer, 0); // length
|
|
AddUInt32(buffer, m_MagicCookie);
|
|
|
|
for (std::size_t i = 0; i < sizeof(m_TransactionID); ++i)
|
|
{
|
|
u8 random_byte = rand() % 256;
|
|
buffer.push_back(random_byte);
|
|
m_TransactionID[i] = random_byte;
|
|
}
|
|
|
|
sockaddr_in to;
|
|
int to_len = sizeof(to);
|
|
memset(&to, 0, to_len);
|
|
|
|
to.sin_family = AF_INET;
|
|
to.sin_port = htons(targetPort);
|
|
to.sin_addr.s_addr = htonl(targetIp);
|
|
|
|
sendto(
|
|
transactionHost.socket,
|
|
reinterpret_cast<char*>(buffer.data()),
|
|
static_cast<int>(buffer.size()),
|
|
0,
|
|
reinterpret_cast<sockaddr*>(&to),
|
|
to_len);
|
|
}
|
|
|
|
/**
|
|
* Gets the response from the STUN server and checks it for its validity.
|
|
*/
|
|
bool ReceiveStunResponse(ENetHost& transactionHost, std::vector<u8>& buffer)
|
|
{
|
|
// TransportAddress sender;
|
|
const int LEN = 2048;
|
|
char input_buffer[LEN];
|
|
|
|
memset(input_buffer, 0, LEN);
|
|
|
|
sockaddr_in addr;
|
|
socklen_t from_len = sizeof(addr);
|
|
|
|
int len = recvfrom(transactionHost.socket, input_buffer, LEN, 0, reinterpret_cast<sockaddr*>(&addr), &from_len);
|
|
|
|
int delay = 200;
|
|
CFG_GET_VAL("lobby.stun.delay", delay);
|
|
|
|
// Wait to receive the message because enet sockets are non-blocking
|
|
const int max_tries = 5;
|
|
for (int count = 0; len < 0 && (count < max_tries || max_tries == -1); ++count)
|
|
{
|
|
usleep(delay * 1000);
|
|
len = recvfrom(transactionHost.socket, input_buffer, LEN, 0, reinterpret_cast<sockaddr*>(&addr), &from_len);
|
|
}
|
|
|
|
if (len < 0)
|
|
{
|
|
LOGERROR("GetPublicAddress: recvfrom error (%d): %s", errno, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
u32 sender_ip = ntohl(static_cast<u32>(addr.sin_addr.s_addr));
|
|
u16 sender_port = ntohs(addr.sin_port);
|
|
|
|
if (sender_ip != m_StunServerIP)
|
|
LOGERROR("GetPublicAddress: Received stun response from different address: %d:%d (%d.%d.%d.%d:%d) %s",
|
|
addr.sin_addr.s_addr,
|
|
addr.sin_port,
|
|
(sender_ip >> 24) & 0xff,
|
|
(sender_ip >> 16) & 0xff,
|
|
(sender_ip >> 8) & 0xff,
|
|
(sender_ip >> 0) & 0xff,
|
|
sender_port,
|
|
input_buffer);
|
|
|
|
// Convert to network string.
|
|
buffer.resize(len);
|
|
memcpy(buffer.data(), reinterpret_cast<u8*>(input_buffer), len);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseStunResponse(const std::vector<u8>& buffer)
|
|
{
|
|
u32 offset = 0;
|
|
|
|
u16 responseType = 0;
|
|
if (!GetFromBuffer<u16, 2>(buffer, offset, responseType) || responseType != m_BindingSuccessResponse)
|
|
{
|
|
LOGERROR("STUN response isn't a binding success response");
|
|
return false;
|
|
}
|
|
|
|
// Ignore message size
|
|
offset += 2;
|
|
|
|
u32 cookie = 0;
|
|
if (!GetFromBuffer<u32, 4>(buffer, offset, cookie) || cookie != m_MagicCookie)
|
|
{
|
|
LOGERROR("STUN response doesn't contain the magic cookie");
|
|
return false;
|
|
}
|
|
|
|
for (std::size_t i = 0; i < sizeof(m_TransactionID); ++i)
|
|
{
|
|
u8 transactionChar = 0;
|
|
if (!GetFromBuffer<u8, 1>(buffer, offset, transactionChar) || transactionChar != m_TransactionID[i])
|
|
{
|
|
LOGERROR("STUN response doesn't contain the transaction ID");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
while (offset < buffer.size())
|
|
{
|
|
u16 type = 0;
|
|
u16 size = 0;
|
|
if (!GetFromBuffer<u16, 2>(buffer, offset, type) ||
|
|
!GetFromBuffer<u16, 2>(buffer, offset, size))
|
|
{
|
|
LOGERROR("STUN response contains invalid attribute");
|
|
return false;
|
|
}
|
|
|
|
// The first two bits are irrelevant to the type
|
|
type &= ~(m_ComprehensionOptional | m_IETFReview);
|
|
|
|
switch (type)
|
|
{
|
|
case m_AttrTypeMappedAddress:
|
|
case m_AttrTypeXORMappedAddress:
|
|
{
|
|
if (size != 8)
|
|
{
|
|
LOGERROR("Invalid STUN Mapped Address length");
|
|
return false;
|
|
}
|
|
|
|
// Ignore the first byte as mentioned in Section 15.1 of RFC 5389.
|
|
++offset;
|
|
|
|
u8 ipFamily = 0;
|
|
if (!GetFromBuffer<u8, 1>(buffer, offset, ipFamily) || ipFamily != m_IPAddressFamilyIPv4)
|
|
{
|
|
LOGERROR("Unsupported address family, IPv4 is expected");
|
|
return false;
|
|
}
|
|
|
|
u16 port = 0;
|
|
u32 ip = 0;
|
|
if (!GetFromBuffer<u16, 2>(buffer, offset, port) ||
|
|
!GetFromBuffer<u32, 4>(buffer, offset, ip))
|
|
{
|
|
LOGERROR("Mapped address doesn't contain IP and port");
|
|
return false;
|
|
}
|
|
|
|
// Obfuscation is described in Section 15.2 of RFC 5389.
|
|
if (type == m_AttrTypeXORMappedAddress)
|
|
{
|
|
port ^= m_MagicCookie >> 16;
|
|
ip ^= m_MagicCookie;
|
|
}
|
|
|
|
m_Port = port;
|
|
m_IP = ip;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
// We don't care about other attributes at all
|
|
|
|
// Skip attribute
|
|
offset += size;
|
|
|
|
// Skip padding
|
|
int padding = size % 4;
|
|
if (padding)
|
|
offset += 4 - padding;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool STUNRequestAndResponse(ENetHost& transactionHost)
|
|
{
|
|
if (!CreateStunRequest(transactionHost))
|
|
return false;
|
|
|
|
std::vector<u8> buffer;
|
|
return ReceiveStunResponse(transactionHost, buffer) &&
|
|
ParseStunResponse(buffer);
|
|
}
|
|
|
|
bool StunClient::GetPublicIp(CStr8& ip, u16 port)
|
|
{
|
|
return FindStunEndpointHost(ip, port);
|
|
}
|
|
|
|
bool StunClient::FindStunEndpointHost(CStr8& ip, u16& port)
|
|
{
|
|
ENetAddress hostAddr{ENET_HOST_ANY, static_cast<u16>(port)};
|
|
ENetHost* transactionHost = enet_host_create(&hostAddr, 1, 1, 0, 0);
|
|
if (!transactionHost)
|
|
return false;
|
|
|
|
bool success = STUNRequestAndResponse(*transactionHost);
|
|
enet_host_destroy(transactionHost);
|
|
if (!success)
|
|
return false;
|
|
|
|
// Convert m_IP to string
|
|
char ipStr[256] = "(error)";
|
|
ENetAddress addr;
|
|
addr.host = ntohl(m_IP);
|
|
int result = enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
|
|
|
|
ip = ipStr;
|
|
port = m_Port;
|
|
return result == 0;
|
|
}
|
|
|
|
bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint)
|
|
{
|
|
if (!STUNRequestAndResponse(transactionHost))
|
|
return false;
|
|
|
|
// Convert m_IP to string
|
|
char ipStr[256] = "(error)";
|
|
ENetAddress addr;
|
|
addr.host = ntohl(m_IP);
|
|
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
|
|
|
|
stunEndpoint.ip = m_IP;
|
|
stunEndpoint.port = m_Port;
|
|
|
|
return true;
|
|
}
|
|
|
|
void StunClient::SendHolePunchingMessages(ENetHost& enetClient, const std::string& serverAddress, u16 serverPort)
|
|
{
|
|
// Convert ip string to int64
|
|
ENetAddress addr;
|
|
addr.port = serverPort;
|
|
enet_address_set_host(&addr, serverAddress.c_str());
|
|
|
|
int delay = 200;
|
|
CFG_GET_VAL("lobby.stun.delay", delay);
|
|
|
|
// Send an UDP message from enet host to ip:port
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
StunClient::SendStunRequest(enetClient, htonl(addr.host), serverPort);
|
|
usleep(delay * 1000);
|
|
}
|
|
}
|