ddnet/src/engine/shared/network_server.cpp
heinrich5991 6b65ccb945 Add HTTP masterserver registering and HTTP masterserver
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.
2022-05-20 08:58:32 +02:00

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