diff options
author | heinrich5991 <heinrich5991@gmail.com> | 2018-10-06 12:43:51 +0200 |
---|---|---|
committer | heinrich5991 <heinrich5991@gmail.com> | 2018-10-12 22:09:24 +0200 |
commit | aababc63eeeee1bc41672502ca6c7a1dd9f61d94 (patch) | |
tree | d098df2f76ea965eadf48de9dcba00e57766831c | |
parent | ee2afdac33f43d96a457bcf692a817489d6f896e (diff) |
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.
-rw-r--r-- | CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/engine/server/server.cpp | 9 | ||||
-rw-r--r-- | src/engine/server/server.h | 2 | ||||
-rw-r--r-- | src/engine/shared/config_variables.h | 2 | ||||
-rw-r--r-- | src/engine/shared/network.cpp | 10 | ||||
-rw-r--r-- | src/engine/shared/network.h | 22 | ||||
-rw-r--r-- | src/engine/shared/network_client.cpp | 2 | ||||
-rw-r--r-- | src/engine/shared/network_conn.cpp | 26 | ||||
-rw-r--r-- | src/engine/shared/network_console.cpp | 2 | ||||
-rw-r--r-- | src/engine/shared/network_server.cpp | 127 | ||||
-rw-r--r-- | src/engine/shared/network_server_hack.cpp | 89 |
11 files changed, 267 insertions, 25 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a8fb99e0..45db40b8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 14778e8f5..bd03a08ee 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -686,10 +686,10 @@ 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; @@ -697,6 +697,11 @@ int CServer::NewClientCallback(int ClientID, void *pUser) 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; } diff --git a/src/engine/server/server.h b/src/engine/server/server.h index c3c1794dc..355882ad2 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -193,7 +193,7 @@ public: 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); diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index e62ddf37b..3da55d41b 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -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") diff --git a/src/engine/shared/network.cpp b/src/engine/shared/network.cpp index 6a2eb921c..54d8add15 100644 --- a/src/engine/shared/network.cpp +++ b/src/engine/shared/network.cpp @@ -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 { diff --git a/src/engine/shared/network.h b/src/engine/shared/network.h index 90cc1076b..c265c1ab8 100644 --- a/src/engine/shared/network.h +++ b/src/engine/shared/network.h @@ -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 @@ public: 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 @@ public: 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 diff --git a/src/engine/shared/network_client.cpp b/src/engine/shared/network_client.cpp index eea63d28f..d7ca73d22 100644 --- a/src/engine/shared/network_client.cpp +++ b/src/engine/shared/network_client.cpp @@ -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); + } } } } diff --git a/src/engine/shared/network_conn.cpp b/src/engine/shared/network_conn.cpp index 0af9d48d5..1c38c7001 100644 --- a/src/engine/shared/network_conn.cpp +++ b/src/engine/shared/network_conn.cpp @@ -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) diff --git a/src/engine/shared/network_console.cpp b/src/engine/shared/network_console.cpp index ded83f683..db8104b52 100644 --- a/src/engine/shared/network_console.cpp +++ b/src/engine/shared/network_console.cpp @@ -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; diff --git a/src/engine/shared/network_server.cpp b/src/engine/shared/network_server.cpp index e12de47ad..877a25a07 100644 --- a/src/engine/shared/network_server.cpp +++ b/src/engine/shared/network_server.cpp @@ -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) diff --git a/src/engine/shared/network_server_hack.cpp b/src/engine/shared/network_server_hack.cpp new file mode 100644 index 000000000..01c137d0a --- /dev/null +++ b/src/engine/shared/network_server_hack.cpp @@ -0,0 +1,89 @@ +#include "network.h" + +#include "packer.h" +#include "protocol.h" + + +unsigned DeriveLegacyToken(unsigned Token) +{ + // Clear the highest bit to get rid of negative numbers. + Token &= ~0x80000000; + if(Token < 2) + { + Token += 2; + } + return Token; +} + +static void AddChunk(CNetPacketConstruct *pPacket, int Sequence, const void *pData, int DataSize) +{ + dbg_assert(pPacket->m_DataSize + NET_MAX_CHUNKHEADERSIZE + DataSize <= (int)sizeof(pPacket->m_aChunkData), "too much data"); + + CNetChunkHeader Header; + Header.m_Flags = Sequence >= 0 ? NET_CHUNKFLAG_VITAL : 0; + Header.m_Size = DataSize; + Header.m_Sequence = Sequence >= 0 ? Sequence : 0; + + unsigned char *pPacketData = &pPacket->m_aChunkData[pPacket->m_DataSize]; + const unsigned char *pChunkStart = pPacketData; + pPacketData = Header.Pack(pPacketData); + pPacket->m_DataSize += pPacketData - pChunkStart; + mem_copy(pPacketData, pData, DataSize); + pPacket->m_DataSize += DataSize; + pPacket->m_NumChunks++; +} + +static int System(int MsgID) +{ + return (MsgID << 1) | 1; +} + +void ConstructLegacyHandshake(CNetPacketConstruct *pPacket1, CNetPacketConstruct *pPacket2, unsigned LegacyToken) +{ + pPacket1->m_Flags = NET_PACKETFLAG_CONTROL; + pPacket1->m_Ack = 0; + pPacket1->m_NumChunks = 0; + pPacket1->m_DataSize = 1; + pPacket1->m_aChunkData[0] = NET_CTRLMSG_CONNECTACCEPT; + + pPacket2->m_Flags = 0; + pPacket2->m_Ack = 0; + pPacket2->m_NumChunks = 0; + pPacket2->m_DataSize = 0; + + CPacker Packer; + + Packer.Reset(); + Packer.AddInt(System(NETMSG_MAP_CHANGE)); + Packer.AddString("dm1", 0); + Packer.AddInt(0xf2159e6e); + Packer.AddInt(5805); + AddChunk(pPacket2, 1, Packer.Data(), Packer.Size()); + + Packer.Reset(); + Packer.AddInt(System(NETMSG_CON_READY)); + AddChunk(pPacket2, 2, Packer.Data(), Packer.Size()); + + for(int i = -2; i <= 0; i++) + { + Packer.Reset(); + Packer.AddInt(System(NETMSG_SNAPEMPTY)); + Packer.AddInt(LegacyToken + i); + Packer.AddInt(i == -2 ? LegacyToken + i + 1 : 1); + AddChunk(pPacket2, -1, Packer.Data(), Packer.Size()); + } +} + +bool DecodeLegacyHandshake(const void *pData, int DataSize, unsigned *pLegacyToken) +{ + CUnpacker Unpacker; + Unpacker.Reset(pData, DataSize); + int MsgID = Unpacker.GetInt(); + int Token = Unpacker.GetInt(); + if(Unpacker.Error() || MsgID != System(NETMSG_INPUT)) + { + return false; + } + *pLegacyToken = Token; + return true; +} |