Permalink
Browse files

Add a backward compatible handshake

This work is basically on eeeee's handshake developed for DDRace.

It works by realizing that the client will send back tick numbers sent
in snapshots. The client puts these into a field named "last acked
snapshot", aka the last snapshot the client saw (used for delta
compression). This can be abused for a challenge-response handshake.

For legacy clients (detected by a short `CTRLMSG_CONNECT` message), the
idea is, upon reception of the `CTRLMSG_CONNECT` packet, to send a
`CTRLMSG_CONNECTACCEPT` to fake accepting the connection, then send a
packet containing all of the following: the rest of the initial
connection build up (`MAP_CHANGE` to the standard dm1 map, `CON_READY`)
and three empty snapshots (`SNAPEMPTY`) with the desired challenge. Due
to client-side constraints, the token must be between 2 and `MAX_INT`.
This lowers the security by roughly one bit to around 31 bits.

If the `CTRLMSG_CONNECTACCEPT` message gets through to the client, but
the other packet does not, the client is stuck. It won't receive any
more packets from the server. If the client does not have the standard
dm1 map, it will crash, since it accepts the `CON_READY` message from
the server despite not having any map data.

No data is saved until this point.

When one receives an `INPUT` packet by a previously unknown client, the
server checks whether it contains a correct token, and if it does,
accept the new client. The client has received two vital messages from
the server so far, so it expects the next sequence number to be 3. The
client has sent an unknown amount of vital messages (might be a custom
client) so we don't know what ack numbers it wants to see. We just treat
the first vital chunk we receive as the new ack number. If we miss a
packet due to that, the handshake will be broken and the client will be
stuck. We send a `MAP_CHANGE` to the current map of the server and
continue normally.

Due to the large difference between packet sizes sent by the client to
packets sent by the server, this legacy handshake is prone to reflection
attacks due to IP spoofing. Rate limiting should be added.
  • Loading branch information...
heinrich5991 committed Oct 6, 2018
1 parent ee2afda commit aababc63eeeee1bc41672502ca6c7a1dd9f61d94
@@ -507,6 +507,7 @@ set_glob(ENGINE_SHARED GLOB src/engine/shared
network_console.cpp
network_console_conn.cpp
network_server.cpp
network_server_hack.cpp
packer.cpp
packer.h
protocol.h
@@ -686,17 +686,22 @@ void CServer::DoSnapshot()
}
int CServer::NewClientCallback(int ClientID, void *pUser)
int CServer::NewClientCallback(int ClientID, bool Legacy, void *pUser)
{
CServer *pThis = (CServer *)pUser;
pThis->m_aClients[ClientID].m_State = CClient::STATE_AUTH;
pThis->m_aClients[ClientID].m_State = !Legacy ? CClient::STATE_AUTH : CClient::STATE_CONNECTING;
pThis->m_aClients[ClientID].m_aName[0] = 0;
pThis->m_aClients[ClientID].m_aClan[0] = 0;
pThis->m_aClients[ClientID].m_Country = -1;
pThis->m_aClients[ClientID].m_Authed = AUTHED_NO;
pThis->m_aClients[ClientID].m_AuthTries = 0;
pThis->m_aClients[ClientID].m_pRconCmdToSend = 0;
pThis->m_aClients[ClientID].Reset();
if(Legacy)
{
pThis->SendMap(ClientID);
}
return 0;
}
@@ -193,7 +193,7 @@ class CServer : public IServer
void DoSnapshot();
static int NewClientCallback(int ClientID, void *pUser);
static int NewClientCallback(int ClientID, bool Legacy, void *pUser);
static int DelClientCallback(int ClientID, const char *pReason, void *pUser);
void SendMap(int ClientID);
@@ -83,7 +83,7 @@ MACRO_CONFIG_STR(SvName, sv_name, 128, "unnamed server", CFGFLAG_SERVER, "Server
MACRO_CONFIG_STR(Bindaddr, bindaddr, 128, "", CFGFLAG_CLIENT|CFGFLAG_SERVER|CFGFLAG_MASTER, "Address to bind the client/server to")
MACRO_CONFIG_INT(SvPort, sv_port, 8303, 0, 0, CFGFLAG_SERVER, "Port to use for the server")
MACRO_CONFIG_INT(SvExternalPort, sv_external_port, 0, 0, 0, CFGFLAG_SERVER, "External port to report to the master servers")
MACRO_CONFIG_INT(SvAllowOldClients, sv_allow_old_clients, 1, 0, 1, CFGFLAG_SERVER, "Allow clients to connect that do not support the anti-spoof protocol")
MACRO_CONFIG_INT(SvAllowOldClients, sv_allow_old_clients, 1, 0, 1, CFGFLAG_SERVER, "Allow clients to connect that do not support the anti-spoof protocol (this presents a DoS risk)")
MACRO_CONFIG_STR(SvMap, sv_map, 128, "dm1", CFGFLAG_SERVER, "Map to use on the server")
MACRO_CONFIG_INT(SvMaxClients, sv_max_clients, 8, 1, MAX_CLIENTS, CFGFLAG_SERVER, "Maximum number of clients that are allowed on a server")
MACRO_CONFIG_INT(SvMaxClientsPerIP, sv_max_clients_per_ip, 4, 1, MAX_CLIENTS, CFGFLAG_SERVER, "Maximum number of clients with the same IP that can connect to the server")
@@ -58,10 +58,16 @@ int CNetRecvUnpacker::FetchChunk(CNetChunk *pChunk)
// handle sequence stuff
if(m_pConnection && (Header.m_Flags&NET_CHUNKFLAG_VITAL))
{
if(Header.m_Sequence == (m_pConnection->m_Ack+1)%NET_MAX_SEQUENCE)
if(m_pConnection->m_UnknownAck || Header.m_Sequence == (m_pConnection->m_Ack+1)%NET_MAX_SEQUENCE)
{
// in case we're in the backward compatibility
// path, we don't know the client's sequence
// number, so we can't decide whether this one
// is correct. but now we know.
m_pConnection->m_UnknownAck = false;
// in sequence
m_pConnection->m_Ack = (m_pConnection->m_Ack+1)%NET_MAX_SEQUENCE;
m_pConnection->m_Ack = Header.m_Sequence;
}
else
{
@@ -3,6 +3,8 @@
#ifndef ENGINE_SHARED_NETWORK_H
#define ENGINE_SHARED_NETWORK_H
#include <base/system.h>
#include "ringbuffer.h"
#include "huffman.h"
@@ -79,12 +81,15 @@ enum
NET_CONN_BUFFERSIZE=1024*32,
NET_COMPATIBILITY_SEQ=2,
NET_ENUM_TERMINATOR
};
typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser);
typedef int (*NETFUNC_NEWCLIENT)(int ClientID, void *pUser);
typedef int (*NETFUNC_NEWCLIENT)(int ClientID, bool Legacy, void *pUser);
typedef int (*NETFUNC_NEWCLIENT_CON)(int ClientID, void *pUser);
struct CNetChunk
{
@@ -140,6 +145,7 @@ class CNetConnection
friend class CNetRecvUnpacker;
private:
unsigned short m_Sequence;
bool m_UnknownAck; // ack not known due to the backward compatibility hack
unsigned short m_Ack;
unsigned short m_PeerAck;
unsigned m_State;
@@ -179,6 +185,7 @@ class CNetConnection
void Init(NETSOCKET Socket, bool BlockCloseMsg);
int Connect(NETADDR *pAddr);
int Accept(NETADDR *pAddr, unsigned Token);
int AcceptLegacy(NETADDR *pAddr);
void Disconnect(const char *pReason);
int Update();
@@ -279,6 +286,10 @@ class CNetServer
unsigned GetToken(const NETADDR &Addr, int SaltIndex) const;
bool IsCorrectToken(const NETADDR &Addr, unsigned Token) const;
unsigned GetLegacyToken(const NETADDR &Addr) const;
unsigned GetLegacyToken(const NETADDR &Addr, int SaltIndex) const;
bool IsCorrectLegacyToken(const NETADDR &Addr, unsigned LegacyToken) const;
public:
int SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser);
@@ -316,14 +327,14 @@ class CNetConsole
class CNetBan *m_pNetBan;
CSlot m_aSlots[NET_MAX_CONSOLE_CLIENTS];
NETFUNC_NEWCLIENT m_pfnNewClient;
NETFUNC_NEWCLIENT_CON m_pfnNewClient;
NETFUNC_DELCLIENT m_pfnDelClient;
void *m_UserPtr;
CNetRecvUnpacker m_RecvUnpacker;
public:
void SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser);
void SetCallbacks(NETFUNC_NEWCLIENT_CON pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser);
//
bool Open(NETADDR BindAddr, class CNetBan *pNetBan, int Flags);
@@ -378,6 +389,11 @@ class CNetClient
const char *ErrorString();
};
// backward compatibility hack
unsigned DeriveLegacyToken(unsigned Token);
void ConstructLegacyHandshake(CNetPacketConstruct *pPacket1, CNetPacketConstruct *pPacket2, unsigned LegacyToken);
bool DecodeLegacyHandshake(const void *pData, int DataSize, unsigned *pLegacyToken);
// TODO: both, fix these. This feels like a junk class for stuff that doesn't fit anywere
@@ -85,7 +85,9 @@ int CNetClient::Recv(CNetChunk *pChunk)
{
if(m_Connection.State() != NET_CONNSTATE_OFFLINE && m_Connection.State() != NET_CONNSTATE_ERROR && net_addr_comp(m_Connection.PeerAddress(), &Addr) == 0
&& m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr))
{
m_RecvUnpacker.Start(&Addr, &m_Connection, 0);
}
}
}
}
@@ -12,6 +12,7 @@ void CNetConnection::ResetStats()
void CNetConnection::Reset()
{
m_Sequence = 0;
m_UnknownAck = false;
m_Ack = 0;
m_PeerAck = 0;
m_RemoteClosed = 0;
@@ -200,6 +201,7 @@ int CNetConnection::Accept(NETADDR *pAddr, unsigned Token)
m_PeerAddr = *pAddr;
mem_zero(m_ErrorString, sizeof(m_ErrorString));
m_State = NET_CONNSTATE_ONLINE;
m_LastRecvTime = time_get();
m_Token = Token;
if(g_Config.m_Debug)
{
@@ -208,6 +210,30 @@ int CNetConnection::Accept(NETADDR *pAddr, unsigned Token)
return 0;
}
int CNetConnection::AcceptLegacy(NETADDR *pAddr)
{
if(State() != NET_CONNSTATE_OFFLINE)
return -1;
// init connection
Reset();
m_PeerAddr = *pAddr;
mem_zero(m_ErrorString, sizeof(m_ErrorString));
m_State = NET_CONNSTATE_ONLINE;
m_LastRecvTime = time_get();
m_Token = 0;
m_UseToken = false;
m_UnknownAck = true;
m_Sequence = NET_COMPATIBILITY_SEQ;
if(g_Config.m_Debug)
{
dbg_msg("connection", "legacy connecting online");
}
return 0;
}
void CNetConnection::Disconnect(const char *pReason)
{
if(State() == NET_CONNSTATE_OFFLINE)
@@ -31,7 +31,7 @@ bool CNetConsole::Open(NETADDR BindAddr, CNetBan *pNetBan, int Flags)
return true;
}
void CNetConsole::SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
void CNetConsole::SetCallbacks(NETFUNC_NEWCLIENT_CON pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
{
m_pfnNewClient = pfnNewClient;
m_pfnDelClient = pfnDelClient;
@@ -128,6 +128,30 @@ bool CNetServer::IsCorrectToken(const NETADDR &Addr, unsigned Token) const
return false;
}
unsigned CNetServer::GetLegacyToken(const NETADDR &Addr) const
{
return GetLegacyToken(Addr, m_CurrentSalt);
}
unsigned CNetServer::GetLegacyToken(const NETADDR &Addr, int SaltIndex) const
{
return DeriveLegacyToken(GetToken(Addr, SaltIndex));
}
bool CNetServer::IsCorrectLegacyToken(const NETADDR &Addr, unsigned LegacyToken) const
{
for(unsigned i = 0; i < sizeof(m_aaSalts) / sizeof(m_aaSalts[0]); i++)
{
if(GetLegacyToken(Addr, i) == LegacyToken)
{
return true;
}
}
return false;
}
/*
TODO: chopp up this function into smaller working parts
*/
@@ -201,19 +225,44 @@ int CNetServer::Recv(CNetChunk *pChunk)
{
continue; // silent ignore.. we got this client already
}
if(m_RecvUnpacker.m_Data.m_DataSize < 1+512)
if(m_RecvUnpacker.m_Data.m_DataSize >= 1+512)
{
unsigned MyToken = GetToken(Addr);
unsigned char aConnectAccept[4];
uint32_to_be(&aConnectAccept[0], MyToken);
CNetBase::SendControlMsg(m_Socket, &Addr, 0, true, Token, NET_CTRLMSG_CONNECTACCEPT, aConnectAccept, sizeof(aConnectAccept));
if(g_Config.m_Debug)
{
dbg_msg("netserver", "dropping short connect packet, size=%d", m_RecvUnpacker.m_Data.m_DataSize);
dbg_msg("netserver", "got connect, sending connect+accept challenge");
}
continue;
}
// the legacy handshake doesn't support
// passwords, allowing the legacy
// handshake to function while a
// password is set would let these
// clients bypass the password check.
else if(g_Config.m_SvAllowOldClients && !g_Config.m_Password[0])
{
CNetPacketConstruct aPackets[2];
unsigned MyToken = GetToken(Addr);
unsigned char aConnectAccept[4];
uint32_to_be(&aConnectAccept[0], MyToken);
CNetBase::SendControlMsg(m_Socket, &Addr, 0, true, Token, NET_CTRLMSG_CONNECTACCEPT, aConnectAccept, sizeof(aConnectAccept));
unsigned LegacyToken = GetLegacyToken(Addr);
ConstructLegacyHandshake(&aPackets[0], &aPackets[1], LegacyToken);
for(int i = 0; i < 2; i++)
{
CNetBase::SendPacket(m_Socket, &Addr, &aPackets[i]);
}
if(g_Config.m_Debug)
{
dbg_msg("netserver", "got legacy connect, sending legacy challenge");
}
}
else
{
if(g_Config.m_Debug)
{
dbg_msg("netserver", "dropping short connect packet, size=%d", m_RecvUnpacker.m_Data.m_DataSize);
}
}
}
else
{
@@ -222,11 +271,46 @@ int CNetServer::Recv(CNetChunk *pChunk)
{
if(!UseToken || !IsCorrectToken(Addr, Token))
{
if(g_Config.m_Debug)
if(!UseToken && g_Config.m_SvAllowOldClients)
{
dbg_msg("netserver", "dropping packet with missing/invalid token, present=%d token=%08x", (int)UseToken, Token);
m_RecvUnpacker.Start(&Addr, 0, -1);
CNetChunk Chunk;
unsigned LegacyToken;
bool Correct = false;
while(m_RecvUnpacker.FetchChunk(&Chunk))
{
if(DecodeLegacyHandshake(Chunk.m_pData, Chunk.m_DataSize, &LegacyToken))
{
if(IsCorrectLegacyToken(Addr, LegacyToken))
{
Correct = true;
break;
}
}
}
m_RecvUnpacker.Clear();
if(!Correct)
{
continue;
}
// if we find a correct token, fall through to
// the other connection handling below.
}
else
{
if(g_Config.m_Debug)
{
if(!UseToken)
{
dbg_msg("netserver", "dropping packet with missing token");
}
else
{
dbg_msg("netserver", "dropping packet with invalid token, token=%08x", (int)UseToken, Token);
}
}
continue;
}
continue;
}
// only allow a specific number of players with the same ip
NETADDR ThisAddr = Addr, OtherAddr;
@@ -265,16 +349,29 @@ int CNetServer::Recv(CNetChunk *pChunk)
{
const char aFullMsg[] = "This server is full";
CNetBase::SendControlMsg(m_Socket, &Addr, 0, UseToken, Token, NET_CTRLMSG_CLOSE, aFullMsg, sizeof(aFullMsg));
continue;
}
ClientID = EmptySlot;
if(UseToken)
{
m_aSlots[ClientID].m_Connection.Accept(&Addr, Token);
}
else
{
m_aSlots[EmptySlot].m_Connection.Accept(&Addr, Token);
m_aSlots[EmptySlot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr);
if(m_pfnNewClient)
m_pfnNewClient(EmptySlot, m_UserPtr);
m_aSlots[ClientID].m_Connection.AcceptLegacy(&Addr);
}
if(m_pfnNewClient)
{
m_pfnNewClient(ClientID, !UseToken, m_UserPtr);
}
if(!UseToken)
{
// Do not process the packet furtherly if it comes from a legacy handshake.
continue;
}
m_aSlots[ClientID].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr);
}
else
if(m_aSlots[ClientID].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr))
{
// normal packet
if(m_RecvUnpacker.m_Data.m_DataSize)
Oops, something went wrong.

0 comments on commit aababc6

Please sign in to comment.