mirror of
https://github.com/ddnet/ddnet.git
synced 2024-10-19 23:38:19 +00:00
6b65ccb945
Registering ----------- The idea is that game servers push their server info to the masterservers every 15 seconds or when the server info changes, but not more than once per second. The game servers do not support the old registering protocol anymore, the backward compatibility is handled by the masterserver. The register call is a HTTP POST to a URL like `https://master1.ddnet.tw/ddnet/15/register` and looks like this: ```json POST /ddnet/15/register HTTP/1.1 Address: tw-0.6+udp://connecting-address.invalid:8303 Secret: 81fa3955-6f83-4290-818d-31c0906b1118 Challenge-Secret: 81fa3955-6f83-4290-818d-31c0906b1118:tw0.6/ipv6 Info-Serial: 0 { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } ``` The `Address` header declares that the server wants to register itself as a `tw-0.6+udp` server, i.e. a server speaking a Teeworlds-0.6-compatible protocol. The free-form `Secret` header is used as a server identity, the server list will be deduplicated via this secret. The free-form `Challenge-Secret` is sent back via UDP for a port forward check. This might have security implications as the masterserver can be asked to send a UDP packet containing some user-controlled bytes. This is somewhat mitigated by the fact that it can only go to an attacker-controlled IP address. The `Info-Serial` header is an integer field that should increase each time the server info (in the body) changes. The masterserver uses that field to ensure that it doesn't use old server infos. The body is a free-form JSON object set by the game server. It should contain certain keys in the correct form to be accepted by clients. The body is optional if the masterserver already confirmed the reception of the info with the given `Info-Serial`. Not shown in this payload is the `Connless-Token` header that is used for Teeworlds 0.7 style communication. Also not shown is the `Challenge-Token` that should be included once the server receives the challenge token via UDP. The masterserver responds with a `200 OK` with a body like this: ``` {"status":"success"} ``` The `status` field can be `success` if the server was successfully registered on the masterserver, `need_challenge` if the masterserver wants the correct `Challenge-Token` header before the register process is successful, `need_info` if the server sent an empty body but the masterserver doesn't actually know the server info. It can also be `error` if the request was malformed, only in this case an HTTP status code except `200 OK` is sent. Synchronization --------------- The masterserver keeps state and outputs JSON files every second. ```json { "servers": [ { "addresses": [ "tw-0.6+udp://127.0.0.1:8303", "tw-0.6+udp://[::1]:8303" ], "info_serial": 0, "info": { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } } ] } ``` `servers.json` (or configured by `--out`) is a server list that is compatible with DDNet 15.5+ clients. It is a JSON object containing a single key `servers` with a list of game servers. Each game server is represented by a JSON object with an `addresses` key containing a list of all known addresses of the server and an `info` key containing the free-form server info sent by the game server. The free-form `info` JSON object re-encoded by the master server and thus canonicalized and stripped of any whitespace characters outside strings. ```json { "kind": "mastersrv", "now": 1816002, "secrets": { "tw-0.6+udp://127.0.0.1:8303": { "ping_time": 1811999, "secret": "42d8f991-f2fa-46e5-a9ae-ebcc93846feb" }, "tw-0.6+udp://[::1]:8303": { "ping_time": 1811999, "secret": "42d8f991-f2fa-46e5-a9ae-ebcc93846feb" } }, "servers": { "42d8f991-f2fa-46e5-a9ae-ebcc93846feb": { "info_serial": 0, "info": { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } } } } ``` `--write-dump` outputs a JSON file compatible with `--read-dump-dir`, this can be used to synchronize servers across different masterservers. `--read-dump-dir` is also used to ingest servers from the backward compatibility layer that pings each server for their server info using the old protocol. The `kind` field describe that this is `mastersrv` output and not from a `backcompat`. This is used for prioritizing `mastersrv` information over `backcompat` information. The `now` field contains an integer describing the current time in milliseconds relative an unspecified epoch that is fixed for each JSON file. This is done instead of using the current time as the epoch for better compression of non-changing data. `secrets` is a map from each server address and to a JSON object containing the last ping time (`ping_time`) in milliseconds relative to the same epoch as before, and the server secret (`secret`) that is used to unify server infos from different addresses of the same logical server. `servers` is a map from the aforementioned `secret`s to the corresponding `info_serial` and `info`. ```json [ "tw-0.6+udp://127.0.0.1:8303", "tw-0.6+udp://[::1]:8303" ] ``` `--write-addresses` outputs a JSON file containing all addresses corresponding to servers that are registered to HTTP masterservers. It does not contain the servers that are obtained via backward compatibility measures. This file can be used by an old-style masterserver to also list new-style servers without the game servers having to register there. An implementation of this can be found at https://github.com/heinrich5991/teeworlds/tree/mastersrv_6_backcompat for Teeworlds 0.5/0.6 masterservers and at https://github.com/heinrich5991/teeworlds/tree/mastersrv_7_backcompat for Teeworlds 0.7 masterservers. All these JSON files can be sent over the network in an efficient way using https://github.com/heinrich5991/twmaster-collect. It establishes a zstd-compressed TCP connection authenticated by a string token that is sent in plain-text. It watches the specified file and transmits it every time it changes. Due to the zstd-compression, the data sent over the network is similar to the size of a diff. Implementation -------------- The masterserver implementation was done in Rust. The current gameserver register implementation doesn't support more than one masterserver for registering.
826 lines
24 KiB
C++
826 lines
24 KiB
C++
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
|
|
/* If you are missing that file, acquire a complete release at teeworlds.com. */
|
|
#include <base/hash_ctxt.h>
|
|
#include <base/system.h>
|
|
|
|
#include <engine/console.h>
|
|
|
|
#include "config.h"
|
|
#include "netban.h"
|
|
#include "network.h"
|
|
#include <engine/message.h>
|
|
#include <engine/shared/protocol.h>
|
|
#include <game/generated/protocol.h>
|
|
|
|
const int DummyMapCrc = 0x6c760ac4;
|
|
unsigned char g_aDummyMapData[] = {
|
|
0x44, 0x41, 0x54, 0x41, 0x04, 0x00, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00,
|
|
0x14, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
|
0x01, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
|
|
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
|
0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00,
|
|
0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
|
|
0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0a, 0xc3, 0x52,
|
|
0xff, 0x7f, 0x00, 0x00, 0x20, 0x5c, 0xf6, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x20, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00,
|
|
0x7c, 0x00, 0x00, 0x00, 0x78, 0x9c, 0x63, 0x64, 0x60, 0x60, 0x60, 0x44,
|
|
0xc2, 0x00, 0x00, 0x38, 0x00, 0x05};
|
|
|
|
static SECURITY_TOKEN ToSecurityToken(const unsigned char *pData)
|
|
{
|
|
return (int)pData[0] | (pData[1] << 8) | (pData[2] << 16) | (pData[3] << 24);
|
|
}
|
|
|
|
bool CNetServer::Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int MaxClientsPerIP)
|
|
{
|
|
// zero out the whole structure
|
|
mem_zero(this, sizeof(*this));
|
|
|
|
// open socket
|
|
m_Socket = net_udp_create(BindAddr);
|
|
if(!m_Socket)
|
|
return false;
|
|
|
|
m_Address = BindAddr;
|
|
m_pNetBan = pNetBan;
|
|
|
|
m_MaxClients = clamp(MaxClients, 1, (int)NET_MAX_CLIENTS);
|
|
m_MaxClientsPerIP = MaxClientsPerIP;
|
|
|
|
m_NumConAttempts = 0;
|
|
m_TimeNumConAttempts = time_get();
|
|
|
|
m_VConnNum = 0;
|
|
m_VConnFirst = 0;
|
|
|
|
secure_random_fill(m_aSecurityTokenSeed, sizeof(m_aSecurityTokenSeed));
|
|
|
|
for(auto &Slot : m_aSlots)
|
|
Slot.m_Connection.Init(m_Socket, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
int CNetServer::SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
|
|
{
|
|
m_pfnNewClient = pfnNewClient;
|
|
m_pfnDelClient = pfnDelClient;
|
|
m_pUser = pUser;
|
|
return 0;
|
|
}
|
|
|
|
int CNetServer::SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_NEWCLIENT_NOAUTH pfnNewClientNoAuth, NETFUNC_CLIENTREJOIN pfnClientRejoin, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
|
|
{
|
|
m_pfnNewClient = pfnNewClient;
|
|
m_pfnNewClientNoAuth = pfnNewClientNoAuth;
|
|
m_pfnClientRejoin = pfnClientRejoin;
|
|
m_pfnDelClient = pfnDelClient;
|
|
m_pUser = pUser;
|
|
return 0;
|
|
}
|
|
|
|
int CNetServer::Close()
|
|
{
|
|
if(!m_Socket)
|
|
return 0;
|
|
return net_udp_close(m_Socket);
|
|
}
|
|
|
|
int CNetServer::Drop(int ClientID, const char *pReason)
|
|
{
|
|
// TODO: insert lots of checks here
|
|
/*NETADDR Addr = ClientAddr(ClientID);
|
|
|
|
dbg_msg("net_server", "client dropped. cid=%d ip=%d.%d.%d.%d reason=\"%s\"",
|
|
ClientID,
|
|
Addr.ip[0], Addr.ip[1], Addr.ip[2], Addr.ip[3],
|
|
pReason
|
|
);*/
|
|
if(m_pfnDelClient)
|
|
m_pfnDelClient(ClientID, pReason, m_pUser);
|
|
|
|
m_aSlots[ClientID].m_Connection.Disconnect(pReason);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int CNetServer::Update()
|
|
{
|
|
for(int i = 0; i < MaxClients(); i++)
|
|
{
|
|
m_aSlots[i].m_Connection.Update();
|
|
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ERROR &&
|
|
(!m_aSlots[i].m_Connection.m_TimeoutProtected ||
|
|
!m_aSlots[i].m_Connection.m_TimeoutSituation))
|
|
{
|
|
Drop(i, m_aSlots[i].m_Connection.ErrorString());
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
SECURITY_TOKEN CNetServer::GetGlobalToken()
|
|
{
|
|
static NETADDR NullAddr = {0};
|
|
return GetToken(NullAddr);
|
|
}
|
|
SECURITY_TOKEN CNetServer::GetToken(const NETADDR &Addr)
|
|
{
|
|
SHA256_CTX Sha256;
|
|
sha256_init(&Sha256);
|
|
sha256_update(&Sha256, (unsigned char *)m_aSecurityTokenSeed, sizeof(m_aSecurityTokenSeed));
|
|
sha256_update(&Sha256, (unsigned char *)&Addr, 20); // omit port, bad idea!
|
|
|
|
SECURITY_TOKEN SecurityToken = ToSecurityToken(sha256_finish(&Sha256).data);
|
|
|
|
if(SecurityToken == NET_SECURITY_TOKEN_UNKNOWN ||
|
|
SecurityToken == NET_SECURITY_TOKEN_UNSUPPORTED)
|
|
SecurityToken = 1;
|
|
|
|
return SecurityToken;
|
|
}
|
|
|
|
void CNetServer::SendControl(NETADDR &Addr, int ControlMsg, const void *pExtra, int ExtraSize, SECURITY_TOKEN SecurityToken)
|
|
{
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, ControlMsg, pExtra, ExtraSize, SecurityToken);
|
|
}
|
|
|
|
int CNetServer::NumClientsWithAddr(NETADDR Addr)
|
|
{
|
|
int FoundAddr = 0;
|
|
for(int i = 0; i < MaxClients(); ++i)
|
|
{
|
|
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_OFFLINE ||
|
|
(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ERROR &&
|
|
(!m_aSlots[i].m_Connection.m_TimeoutProtected ||
|
|
!m_aSlots[i].m_Connection.m_TimeoutSituation)))
|
|
continue;
|
|
|
|
if(!net_addr_comp_noport(&Addr, m_aSlots[i].m_Connection.PeerAddress()))
|
|
FoundAddr++;
|
|
}
|
|
|
|
return FoundAddr;
|
|
}
|
|
|
|
bool CNetServer::Connlimit(NETADDR Addr)
|
|
{
|
|
int64_t Now = time_get();
|
|
int Oldest = 0;
|
|
|
|
for(int i = 0; i < NET_CONNLIMIT_IPS; ++i)
|
|
{
|
|
if(!net_addr_comp(&m_aSpamConns[i].m_Addr, &Addr))
|
|
{
|
|
if(m_aSpamConns[i].m_Time > Now - time_freq() * g_Config.m_SvConnlimitTime)
|
|
{
|
|
if(m_aSpamConns[i].m_Conns >= g_Config.m_SvConnlimit)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
m_aSpamConns[i].m_Time = Now;
|
|
m_aSpamConns[i].m_Conns = 0;
|
|
}
|
|
m_aSpamConns[i].m_Conns++;
|
|
return false;
|
|
}
|
|
|
|
if(m_aSpamConns[i].m_Time < m_aSpamConns[Oldest].m_Time)
|
|
Oldest = i;
|
|
}
|
|
|
|
m_aSpamConns[Oldest].m_Addr = Addr;
|
|
m_aSpamConns[Oldest].m_Time = Now;
|
|
m_aSpamConns[Oldest].m_Conns = 1;
|
|
return false;
|
|
}
|
|
|
|
int CNetServer::TryAcceptClient(NETADDR &Addr, SECURITY_TOKEN SecurityToken, bool VanillaAuth, bool Sixup, SECURITY_TOKEN Token)
|
|
{
|
|
if(Sixup && !g_Config.m_SvSixup)
|
|
{
|
|
const char aMsg[] = "0.7 connections are not accepted at this time";
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aMsg, sizeof(aMsg), SecurityToken, Sixup);
|
|
return -1; // failed to add client?
|
|
}
|
|
|
|
if(Connlimit(Addr))
|
|
{
|
|
const char aMsg[] = "Too many connections in a short time";
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aMsg, sizeof(aMsg), SecurityToken, Sixup);
|
|
return -1; // failed to add client
|
|
}
|
|
|
|
// check for sv_max_clients_per_ip
|
|
if(NumClientsWithAddr(Addr) + 1 > m_MaxClientsPerIP)
|
|
{
|
|
char aBuf[128];
|
|
str_format(aBuf, sizeof(aBuf), "Only %d players with the same IP are allowed", m_MaxClientsPerIP);
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aBuf, str_length(aBuf) + 1, SecurityToken, Sixup);
|
|
return -1; // failed to add client
|
|
}
|
|
|
|
int Slot = -1;
|
|
for(int i = 0; i < MaxClients(); i++)
|
|
{
|
|
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_OFFLINE)
|
|
{
|
|
Slot = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(Slot == -1)
|
|
{
|
|
const char aFullMsg[] = "This server is full";
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aFullMsg, sizeof(aFullMsg), SecurityToken, Sixup);
|
|
|
|
return -1; // failed to add client
|
|
}
|
|
|
|
// init connection slot
|
|
m_aSlots[Slot].m_Connection.DirectInit(Addr, SecurityToken, Token, Sixup);
|
|
|
|
if(VanillaAuth)
|
|
{
|
|
// client sequence is unknown if the auth was done
|
|
// connection-less
|
|
m_aSlots[Slot].m_Connection.SetUnknownSeq();
|
|
// correct sequence
|
|
m_aSlots[Slot].m_Connection.SetSequence(6);
|
|
}
|
|
|
|
if(g_Config.m_Debug)
|
|
{
|
|
char aAddrStr[NETADDR_MAXSTRSIZE];
|
|
net_addr_str(&Addr, aAddrStr, sizeof(aAddrStr), true);
|
|
dbg_msg("security", "client accepted %s", aAddrStr);
|
|
}
|
|
|
|
if(VanillaAuth)
|
|
m_pfnNewClientNoAuth(Slot, m_pUser);
|
|
else
|
|
m_pfnNewClient(Slot, m_pUser, Sixup);
|
|
|
|
return Slot; // done
|
|
}
|
|
|
|
void CNetServer::SendMsgs(NETADDR &Addr, const CMsgPacker *apMsgs[], int Num)
|
|
{
|
|
CNetPacketConstruct Construct;
|
|
mem_zero(&Construct, sizeof(Construct));
|
|
unsigned char *pChunkData = &Construct.m_aChunkData[Construct.m_DataSize];
|
|
|
|
for(int i = 0; i < Num; i++)
|
|
{
|
|
const CMsgPacker *pMsg = apMsgs[i];
|
|
CNetChunkHeader Header;
|
|
Header.m_Flags = NET_CHUNKFLAG_VITAL;
|
|
Header.m_Size = pMsg->Size();
|
|
Header.m_Sequence = i + 1;
|
|
pChunkData = Header.Pack(pChunkData);
|
|
mem_copy(pChunkData, pMsg->Data(), pMsg->Size());
|
|
*pChunkData <<= 1;
|
|
*pChunkData |= 1;
|
|
pChunkData += pMsg->Size();
|
|
Construct.m_NumChunks++;
|
|
}
|
|
|
|
//
|
|
Construct.m_DataSize = (int)(pChunkData - Construct.m_aChunkData);
|
|
CNetBase::SendPacket(m_Socket, &Addr, &Construct, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
}
|
|
|
|
// connection-less msg packet without token-support
|
|
void CNetServer::OnPreConnMsg(NETADDR &Addr, CNetPacketConstruct &Packet)
|
|
{
|
|
bool IsCtrl = Packet.m_Flags & NET_PACKETFLAG_CONTROL;
|
|
int CtrlMsg = m_RecvUnpacker.m_Data.m_aChunkData[0];
|
|
|
|
// log flooding
|
|
//TODO: remove
|
|
if(g_Config.m_Debug)
|
|
{
|
|
int64_t Now = time_get();
|
|
|
|
if(Now - m_TimeNumConAttempts > time_freq())
|
|
// reset
|
|
m_NumConAttempts = 0;
|
|
|
|
m_NumConAttempts++;
|
|
|
|
if(m_NumConAttempts > 100)
|
|
{
|
|
dbg_msg("security", "flooding detected");
|
|
|
|
m_TimeNumConAttempts = Now;
|
|
m_NumConAttempts = 0;
|
|
}
|
|
}
|
|
|
|
if(IsCtrl && CtrlMsg == NET_CTRLMSG_CONNECT)
|
|
{
|
|
if(g_Config.m_SvVanillaAntiSpoof && g_Config.m_Password[0] == '\0')
|
|
{
|
|
bool Flooding = false;
|
|
|
|
if(g_Config.m_SvVanConnPerSecond)
|
|
{
|
|
// detect flooding
|
|
Flooding = m_VConnNum > g_Config.m_SvVanConnPerSecond;
|
|
const int64_t Now = time_get();
|
|
|
|
if(Now <= m_VConnFirst + time_freq())
|
|
{
|
|
m_VConnNum++;
|
|
}
|
|
else
|
|
{
|
|
m_VConnNum = 1;
|
|
m_VConnFirst = Now;
|
|
}
|
|
}
|
|
|
|
if(g_Config.m_Debug && Flooding)
|
|
{
|
|
dbg_msg("security", "vanilla connection flooding detected");
|
|
}
|
|
|
|
// simulate accept
|
|
SendControl(Addr, NET_CTRLMSG_CONNECTACCEPT, NULL, 0, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
|
|
// Begin vanilla compatible token handshake
|
|
// The idea is to pack a security token in the gametick
|
|
// parameter of NETMSG_SNAPEMPTY. The Client then will
|
|
// return the token/gametick in NETMSG_INPUT, allowing
|
|
// us to validate the token.
|
|
// https://github.com/eeeee/ddnet/commit/b8e40a244af4e242dc568aa34854c5754c75a39a
|
|
|
|
// Before we can send NETMSG_SNAPEMPTY, the client needs
|
|
// to load a map, otherwise it might crash. The map
|
|
// should be as small as is possible and directly available
|
|
// to the client. Therefore a dummy map is sent in the same
|
|
// packet. To reduce the traffic we'll fallback to a default
|
|
// map if there are too many connection attempts at once.
|
|
|
|
// send mapchange + map data + con_ready + 3 x empty snap (with token)
|
|
CMsgPacker MapChangeMsg(NETMSG_MAP_CHANGE);
|
|
|
|
if(Flooding)
|
|
{
|
|
// Fallback to dm1
|
|
MapChangeMsg.AddString("dm1", 0);
|
|
MapChangeMsg.AddInt(0xf2159e6e);
|
|
MapChangeMsg.AddInt(5805);
|
|
}
|
|
else
|
|
{
|
|
// dummy map
|
|
MapChangeMsg.AddString("dummy", 0);
|
|
MapChangeMsg.AddInt(DummyMapCrc);
|
|
MapChangeMsg.AddInt(sizeof(g_aDummyMapData));
|
|
}
|
|
|
|
CMsgPacker MapDataMsg(NETMSG_MAP_DATA);
|
|
|
|
if(Flooding)
|
|
{
|
|
// send empty map data to keep 0.6.4 support
|
|
MapDataMsg.AddInt(1); // last chunk
|
|
MapDataMsg.AddInt(0); // crc
|
|
MapDataMsg.AddInt(0); // chunk index
|
|
MapDataMsg.AddInt(0); // map size
|
|
MapDataMsg.AddRaw(NULL, 0); // map data
|
|
}
|
|
else
|
|
{
|
|
// send dummy map data
|
|
MapDataMsg.AddInt(1); // last chunk
|
|
MapDataMsg.AddInt(DummyMapCrc); // crc
|
|
MapDataMsg.AddInt(0); // chunk index
|
|
MapDataMsg.AddInt(sizeof(g_aDummyMapData)); // map size
|
|
MapDataMsg.AddRaw(g_aDummyMapData, sizeof(g_aDummyMapData)); // map data
|
|
}
|
|
|
|
CMsgPacker ConReadyMsg(NETMSG_CON_READY);
|
|
|
|
CMsgPacker SnapEmptyMsg(NETMSG_SNAPEMPTY);
|
|
SECURITY_TOKEN SecurityToken = GetVanillaToken(Addr);
|
|
SnapEmptyMsg.AddInt(SecurityToken);
|
|
SnapEmptyMsg.AddInt(SecurityToken + 1);
|
|
|
|
// send all chunks/msgs in one packet
|
|
const CMsgPacker *apMsgs[] = {&MapChangeMsg, &MapDataMsg, &ConReadyMsg,
|
|
&SnapEmptyMsg, &SnapEmptyMsg, &SnapEmptyMsg};
|
|
SendMsgs(Addr, apMsgs, 6);
|
|
}
|
|
else
|
|
{
|
|
// accept client directly
|
|
SendControl(Addr, NET_CTRLMSG_CONNECTACCEPT, NULL, 0, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
|
|
TryAcceptClient(Addr, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
}
|
|
}
|
|
else if(!IsCtrl && g_Config.m_SvVanillaAntiSpoof && g_Config.m_Password[0] == '\0')
|
|
{
|
|
CNetChunkHeader h;
|
|
|
|
unsigned char *pData = Packet.m_aChunkData;
|
|
pData = h.Unpack(pData);
|
|
CUnpacker Unpacker;
|
|
Unpacker.Reset(pData, h.m_Size);
|
|
int Msg = Unpacker.GetInt() >> 1;
|
|
|
|
if(Msg == NETMSG_INPUT)
|
|
{
|
|
SECURITY_TOKEN SecurityToken = Unpacker.GetInt();
|
|
if(SecurityToken == GetVanillaToken(Addr))
|
|
{
|
|
if(g_Config.m_Debug)
|
|
dbg_msg("security", "new client (vanilla handshake)");
|
|
// try to accept client skipping auth state
|
|
TryAcceptClient(Addr, NET_SECURITY_TOKEN_UNSUPPORTED, true);
|
|
}
|
|
else if(g_Config.m_Debug)
|
|
dbg_msg("security", "invalid token (vanilla handshake)");
|
|
}
|
|
else
|
|
{
|
|
if(g_Config.m_Debug)
|
|
{
|
|
dbg_msg("security", "invalid preconn msg %d", Msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CNetServer::OnConnCtrlMsg(NETADDR &Addr, int ClientID, int ControlMsg, const CNetPacketConstruct &Packet)
|
|
{
|
|
if(ControlMsg == NET_CTRLMSG_CONNECT)
|
|
{
|
|
// got connection attempt inside of valid session
|
|
// the client probably wants to reconnect
|
|
bool SupportsToken = Packet.m_DataSize >=
|
|
(int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(SECURITY_TOKEN)) &&
|
|
!mem_comp(&Packet.m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC));
|
|
|
|
if(SupportsToken)
|
|
{
|
|
// response connection request with token
|
|
SECURITY_TOKEN Token = GetToken(Addr);
|
|
SendControl(Addr, NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC), Token);
|
|
}
|
|
|
|
if(g_Config.m_Debug)
|
|
dbg_msg("security", "client %d wants to reconnect", ClientID);
|
|
}
|
|
else if(ControlMsg == NET_CTRLMSG_ACCEPT && Packet.m_DataSize == 1 + sizeof(SECURITY_TOKEN))
|
|
{
|
|
SECURITY_TOKEN Token = ToSecurityToken(&Packet.m_aChunkData[1]);
|
|
if(Token == GetToken(Addr))
|
|
{
|
|
// correct token
|
|
// try to accept client
|
|
if(g_Config.m_Debug)
|
|
dbg_msg("security", "client %d reconnect", ClientID);
|
|
|
|
// reset netconn and process rejoin
|
|
m_aSlots[ClientID].m_Connection.Reset(true);
|
|
m_pfnClientRejoin(ClientID, m_pUser);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CNetServer::OnTokenCtrlMsg(NETADDR &Addr, int ControlMsg, const CNetPacketConstruct &Packet)
|
|
{
|
|
if(ClientExists(Addr))
|
|
return; // silently ignore
|
|
|
|
if(Addr.type == NETTYPE_WEBSOCKET_IPV4)
|
|
{
|
|
// websocket client doesn't send token
|
|
// direct accept
|
|
SendControl(Addr, NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC), NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
TryAcceptClient(Addr, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
}
|
|
else if(ControlMsg == NET_CTRLMSG_CONNECT)
|
|
{
|
|
// response connection request with token
|
|
SECURITY_TOKEN Token = GetToken(Addr);
|
|
SendControl(Addr, NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC), Token);
|
|
}
|
|
else if(ControlMsg == NET_CTRLMSG_ACCEPT)
|
|
{
|
|
SECURITY_TOKEN Token = ToSecurityToken(&Packet.m_aChunkData[1]);
|
|
if(Token == GetToken(Addr))
|
|
{
|
|
// correct token
|
|
// try to accept client
|
|
if(g_Config.m_Debug)
|
|
dbg_msg("security", "new client (ddnet token)");
|
|
TryAcceptClient(Addr, Token);
|
|
}
|
|
else
|
|
{
|
|
// invalid token
|
|
if(g_Config.m_Debug)
|
|
dbg_msg("security", "invalid token");
|
|
}
|
|
}
|
|
}
|
|
|
|
int CNetServer::OnSixupCtrlMsg(NETADDR &Addr, CNetChunk *pChunk, int ControlMsg, const CNetPacketConstruct &Packet, SECURITY_TOKEN &ResponseToken, SECURITY_TOKEN Token)
|
|
{
|
|
if(m_RecvUnpacker.m_Data.m_DataSize < 5 || ClientExists(Addr))
|
|
return 0; // silently ignore
|
|
|
|
mem_copy(&ResponseToken, Packet.m_aChunkData + 1, 4);
|
|
|
|
if(ControlMsg == 5)
|
|
{
|
|
if(m_RecvUnpacker.m_Data.m_DataSize >= 512)
|
|
{
|
|
SendTokenSixup(Addr, ResponseToken);
|
|
return 0;
|
|
}
|
|
|
|
// Is this behaviour safe to rely on?
|
|
pChunk->m_Flags = 0;
|
|
pChunk->m_ClientID = -1;
|
|
pChunk->m_Address = Addr;
|
|
pChunk->m_DataSize = 0;
|
|
return 1;
|
|
}
|
|
else if(ControlMsg == NET_CTRLMSG_CONNECT)
|
|
{
|
|
SECURITY_TOKEN MyToken = GetToken(Addr);
|
|
unsigned char aToken[4];
|
|
mem_copy(aToken, &MyToken, 4);
|
|
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CONNECTACCEPT, aToken, sizeof(aToken), ResponseToken, true);
|
|
if(Token == MyToken)
|
|
TryAcceptClient(Addr, ResponseToken, false, true, Token);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int CNetServer::GetClientSlot(const NETADDR &Addr)
|
|
{
|
|
int Slot = -1;
|
|
|
|
for(int i = 0; i < MaxClients(); i++)
|
|
{
|
|
if(m_aSlots[i].m_Connection.State() != NET_CONNSTATE_OFFLINE &&
|
|
m_aSlots[i].m_Connection.State() != NET_CONNSTATE_ERROR &&
|
|
net_addr_comp(m_aSlots[i].m_Connection.PeerAddress(), &Addr) == 0)
|
|
|
|
{
|
|
Slot = i;
|
|
}
|
|
}
|
|
|
|
return Slot;
|
|
}
|
|
|
|
static bool IsDDNetControlMsg(const CNetPacketConstruct *pPacket)
|
|
{
|
|
if(!(pPacket->m_Flags & NET_PACKETFLAG_CONTROL) || pPacket->m_DataSize < 1)
|
|
{
|
|
return false;
|
|
}
|
|
if(pPacket->m_aChunkData[0] == NET_CTRLMSG_CONNECT && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(SECURITY_TOKEN)) && mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC)) == 0)
|
|
{
|
|
// DDNet CONNECT
|
|
return true;
|
|
}
|
|
if(pPacket->m_aChunkData[0] == NET_CTRLMSG_ACCEPT && pPacket->m_DataSize >= 1 + (int)sizeof(SECURITY_TOKEN))
|
|
{
|
|
// DDNet ACCEPT
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
TODO: chopp up this function into smaller working parts
|
|
*/
|
|
int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken)
|
|
{
|
|
while(true)
|
|
{
|
|
NETADDR Addr;
|
|
|
|
// check for a chunk
|
|
if(m_RecvUnpacker.FetchChunk(pChunk))
|
|
return 1;
|
|
|
|
// TODO: empty the recvinfo
|
|
unsigned char *pData;
|
|
int Bytes = net_udp_recv(m_Socket, &Addr, &pData);
|
|
|
|
// no more packets for now
|
|
if(Bytes <= 0)
|
|
break;
|
|
|
|
// check if we just should drop the packet
|
|
char aBuf[128];
|
|
if(NetBan() && NetBan()->IsBanned(&Addr, aBuf, sizeof(aBuf)))
|
|
{
|
|
// banned, reply with a message
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aBuf, str_length(aBuf) + 1, NET_SECURITY_TOKEN_UNSUPPORTED);
|
|
continue;
|
|
}
|
|
|
|
SECURITY_TOKEN Token;
|
|
bool Sixup = false;
|
|
*pResponseToken = NET_SECURITY_TOKEN_UNKNOWN;
|
|
if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token, pResponseToken) == 0)
|
|
{
|
|
if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONNLESS)
|
|
{
|
|
if(Sixup && Token != GetToken(Addr) && Token != GetGlobalToken())
|
|
continue;
|
|
|
|
pChunk->m_Flags = NETSENDFLAG_CONNLESS;
|
|
pChunk->m_ClientID = -1;
|
|
pChunk->m_Address = Addr;
|
|
pChunk->m_DataSize = m_RecvUnpacker.m_Data.m_DataSize;
|
|
pChunk->m_pData = m_RecvUnpacker.m_Data.m_aChunkData;
|
|
if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_EXTENDED)
|
|
{
|
|
pChunk->m_Flags |= NETSENDFLAG_EXTENDED;
|
|
mem_copy(pChunk->m_aExtraData, m_RecvUnpacker.m_Data.m_aExtraData, sizeof(pChunk->m_aExtraData));
|
|
}
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
// drop invalid ctrl packets
|
|
if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONTROL &&
|
|
m_RecvUnpacker.m_Data.m_DataSize == 0)
|
|
continue;
|
|
|
|
// normal packet, find matching slot
|
|
int Slot = GetClientSlot(Addr);
|
|
|
|
if(!Sixup && Slot != -1 && m_aSlots[Slot].m_Connection.m_Sixup)
|
|
{
|
|
Sixup = true;
|
|
if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token))
|
|
continue;
|
|
}
|
|
|
|
if(Slot != -1)
|
|
{
|
|
// found
|
|
|
|
// control
|
|
if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONTROL)
|
|
OnConnCtrlMsg(Addr, Slot, m_RecvUnpacker.m_Data.m_aChunkData[0], m_RecvUnpacker.m_Data);
|
|
|
|
if(m_aSlots[Slot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token))
|
|
{
|
|
if(m_RecvUnpacker.m_Data.m_DataSize)
|
|
m_RecvUnpacker.Start(&Addr, &m_aSlots[Slot].m_Connection, Slot);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not found, client that wants to connect
|
|
|
|
if(Sixup)
|
|
{
|
|
// got 0.7 control msg
|
|
if(OnSixupCtrlMsg(Addr, pChunk, m_RecvUnpacker.m_Data.m_aChunkData[0], m_RecvUnpacker.m_Data, *pResponseToken, Token) == 1)
|
|
return 1;
|
|
}
|
|
else if(IsDDNetControlMsg(&m_RecvUnpacker.m_Data))
|
|
// got ddnet control msg
|
|
OnTokenCtrlMsg(Addr, m_RecvUnpacker.m_Data.m_aChunkData[0], m_RecvUnpacker.m_Data);
|
|
else
|
|
// got connection-less ctrl or sys msg
|
|
OnPreConnMsg(Addr, m_RecvUnpacker.m_Data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int CNetServer::Send(CNetChunk *pChunk)
|
|
{
|
|
if(pChunk->m_DataSize >= NET_MAX_PAYLOAD)
|
|
{
|
|
dbg_msg("netserver", "packet payload too big. %d. dropping packet", pChunk->m_DataSize);
|
|
return -1;
|
|
}
|
|
|
|
if(pChunk->m_Flags & NETSENDFLAG_CONNLESS)
|
|
{
|
|
// send connectionless packet
|
|
CNetBase::SendPacketConnless(m_Socket, &pChunk->m_Address, pChunk->m_pData, pChunk->m_DataSize,
|
|
pChunk->m_Flags & NETSENDFLAG_EXTENDED, pChunk->m_aExtraData);
|
|
}
|
|
else
|
|
{
|
|
int Flags = 0;
|
|
dbg_assert(pChunk->m_ClientID >= 0, "erroneous client id");
|
|
dbg_assert(pChunk->m_ClientID < MaxClients(), "erroneous client id");
|
|
|
|
if(pChunk->m_Flags & NETSENDFLAG_VITAL)
|
|
Flags = NET_CHUNKFLAG_VITAL;
|
|
|
|
if(m_aSlots[pChunk->m_ClientID].m_Connection.QueueChunk(Flags, pChunk->m_DataSize, pChunk->m_pData) == 0)
|
|
{
|
|
if(pChunk->m_Flags & NETSENDFLAG_FLUSH)
|
|
m_aSlots[pChunk->m_ClientID].m_Connection.Flush();
|
|
}
|
|
else
|
|
{
|
|
//Drop(pChunk->m_ClientID, "Error sending data");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void CNetServer::SendTokenSixup(NETADDR &Addr, SECURITY_TOKEN Token)
|
|
{
|
|
SECURITY_TOKEN MyToken = GetToken(Addr);
|
|
unsigned char aBuf[512] = {};
|
|
mem_copy(aBuf, &MyToken, 4);
|
|
int Size = (Token == NET_SECURITY_TOKEN_UNKNOWN) ? 512 : 4;
|
|
CNetBase::SendControlMsg(m_Socket, &Addr, 0, 5, aBuf, Size, Token, true);
|
|
}
|
|
|
|
int CNetServer::SendConnlessSixup(CNetChunk *pChunk, SECURITY_TOKEN ResponseToken)
|
|
{
|
|
if(pChunk->m_DataSize > NET_MAX_PACKETSIZE - 9)
|
|
return -1;
|
|
|
|
unsigned char aBuffer[NET_MAX_PACKETSIZE];
|
|
aBuffer[0] = NET_PACKETFLAG_CONNLESS << 2 | 1;
|
|
SECURITY_TOKEN Token = GetToken(pChunk->m_Address);
|
|
mem_copy(aBuffer + 1, &ResponseToken, 4);
|
|
mem_copy(aBuffer + 5, &Token, 4);
|
|
mem_copy(aBuffer + 9, pChunk->m_pData, pChunk->m_DataSize);
|
|
net_udp_send(m_Socket, &pChunk->m_Address, aBuffer, pChunk->m_DataSize + 9);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CNetServer::SetMaxClientsPerIP(int Max)
|
|
{
|
|
// clamp
|
|
if(Max < 1)
|
|
Max = 1;
|
|
else if(Max > NET_MAX_CLIENTS)
|
|
Max = NET_MAX_CLIENTS;
|
|
|
|
m_MaxClientsPerIP = Max;
|
|
}
|
|
|
|
bool CNetServer::SetTimedOut(int ClientID, int OrigID)
|
|
{
|
|
if(m_aSlots[ClientID].m_Connection.State() != NET_CONNSTATE_ERROR)
|
|
return false;
|
|
|
|
m_aSlots[ClientID].m_Connection.SetTimedOut(ClientAddr(OrigID), m_aSlots[OrigID].m_Connection.SeqSequence(), m_aSlots[OrigID].m_Connection.AckSequence(), m_aSlots[OrigID].m_Connection.SecurityToken(), m_aSlots[OrigID].m_Connection.ResendBuffer(), m_aSlots[OrigID].m_Connection.m_Sixup);
|
|
m_aSlots[OrigID].m_Connection.Reset();
|
|
return true;
|
|
}
|
|
|
|
void CNetServer::SetTimeoutProtected(int ClientID)
|
|
{
|
|
m_aSlots[ClientID].m_Connection.m_TimeoutProtected = true;
|
|
}
|
|
|
|
int CNetServer::ResetErrorString(int ClientID)
|
|
{
|
|
m_aSlots[ClientID].m_Connection.ResetErrorString();
|
|
return 0;
|
|
}
|
|
|
|
const char *CNetServer::ErrorString(int ClientID)
|
|
{
|
|
return m_aSlots[ClientID].m_Connection.ErrorString();
|
|
}
|