diff --git a/Documentation/protocol/02-connectionless.md b/Documentation/protocol/02-connectionless.md index 73185b53..7d3638c7 100644 --- a/Documentation/protocol/02-connectionless.md +++ b/Documentation/protocol/02-connectionless.md @@ -138,7 +138,7 @@ These match Source Engine Query messages used in GoldSrc and Source engine and i Unlike other connectionless messages these are binary: integers are little-endian, floats are 32-bit IEEE 754 and strings are null-terminated. -`A2S_GOLDSRC_PLAYERS` and `A2S_GOLDSRC_RULES` requests carry a 32-bit challenge, `A2S_GOLDSRC_INFO` may have one appended after the query string. When no challenge is known yet, `\xff\xff\xff\xff` is sent in it's place. The server may answer with a challenge instead of the data: byte `A` (`0x41`) followed by a 32-bit challenge value, 9 bytes total with the header. The request is then repeated with the received challenge. Don't confuse this message with textual `S2C_GOLDSRC_CHALLENGE` used during connection, they only share the first byte. Xash servers respond to queries immediately and never send query challenges, GoldSrc servers require them since the 2020 update. +`A2S_GOLDSRC_PLAYERS` and `A2S_GOLDSRC_RULES` requests carry a 32-bit challenge, `A2S_GOLDSRC_INFO` may have one appended after the query string. When no challenge is known yet, `\xff\xff\xff\xff` is sent in it's place. The server may answer with a challenge instead of the data: byte `A` (`0x41`) followed by a 32-bit challenge value, 9 bytes total with the header. The request is then repeated with the received challenge. Don't confuse this message with textual `S2C_GOLDSRC_CHALLENGE` used during connection, they only share the first byte. Responses that don't fit into a single packet (usually rules) come from GoldSrc servers split into fragments. Such packets start with `\xfe\xff\xff\xff` instead of `\xff\xff\xff\xff`, followed by a 32-bit sequence number, same in all fragments of one response, and a byte with total fragment count in the lower 4 bits and this fragment's index in the upper 4 bits. Fragment payloads concatenated in index order form a normal `\xff\xff\xff\xff` message. Xash servers never split query responses and send them as single datagrams. diff --git a/engine/server/server.h b/engine/server/server.h index 482746f8..cb7469ed 100644 --- a/engine/server/server.h +++ b/engine/server/server.h @@ -542,6 +542,8 @@ void SV_UpdateServerInfo( void ); void SV_EndRedirect( host_redirect_t *rd ); void SV_RejectConnection( netadr_t from, const char *fmt, ... ) FORMAT_CHECK( 2 ); void SV_GetPlayerCount( int *clients, int *bots ); +int SV_CreateChallenge( netadr_t from, qboolean *error ); +qboolean SV_ValidateChallenge( netadr_t from, int challenge ); static inline qboolean SV_HavePassword( void ) { @@ -701,6 +703,6 @@ int SV_LightForEntity( edict_t *pEdict ); // // sv_query.c // -void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from ); +void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from, sizebuf_t *msg ); #endif//SERVER_H diff --git a/engine/server/sv_client.c b/engine/server/sv_client.c index de44f9b2..a46c829a 100644 --- a/engine/server/sv_client.c +++ b/engine/server/sv_client.c @@ -109,11 +109,43 @@ static int SV_GetChallenge( netadr_t from, uint32_t time_window, qboolean *error return digest[0] | digest[1] << 8 | digest[2] << 16 | digest[3] << 24; } -static void SV_SendChallenge( netadr_t from, qboolean skip_bandwidth_test ) +/* +================= +SV_CreateChallenge +================= +*/ +int SV_CreateChallenge( netadr_t from, qboolean *error ) +{ + uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS ); + + return SV_GetChallenge( from, time_window, error ); +} + +/* +================= +SV_ValidateChallenge +================= +*/ +qboolean SV_ValidateChallenge( netadr_t from, int challenge ) { qboolean error = false; uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS ); - int challenge = SV_GetChallenge( from, time_window, &error ); + + // accept the current window and the previous one so challenges issued just + // before a window boundary remain valid for the full expected lifetime + if( SV_GetChallenge( from, time_window, &error ) == challenge && !error ) + return true; + + if( SV_GetChallenge( from, time_window - 1, &error ) == challenge && !error ) + return true; + + return false; +} + +static void SV_SendChallenge( netadr_t from, qboolean skip_bandwidth_test ) +{ + qboolean error = false; + int challenge = SV_CreateChallenge( from, &error ); if( error ) return; @@ -213,15 +245,7 @@ Make sure connecting client is not spoofing */ static int SV_CheckChallenge( netadr_t from, int challenge ) { - qboolean error = false; - uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS ); - - // accept the current window and the previous one so challenges issued just - // before a window boundary remain valid for the full expected lifetime - if( SV_GetChallenge( from, time_window, &error ) == challenge && !error ) - return true; - - if( SV_GetChallenge( from, time_window - 1, &error ) == challenge && !error ) + if( SV_ValidateChallenge( from, challenge )) return true; SV_RejectConnection( from, "no challenge for your address\n" ); @@ -3138,7 +3162,7 @@ void SV_ConnectionlessPacket( netadr_t from, sizebuf_t *msg ) // `pcmd` points only to the first word from the query string. if( !Q_strcmp( args, A2S_GOLDSRC_INFO ) || pcmd[0] == A2S_GOLDSRC_PLAYERS || pcmd[0] == A2S_GOLDSRC_RULES ) { - SV_SourceQuery_HandleConnnectionlessPacket( args, from ); + SV_SourceQuery_HandleConnnectionlessPacket( args, from, msg ); } else if( !Q_strcmp( pcmd, A2A_INFO )) { diff --git a/engine/server/sv_query.c b/engine/server/sv_query.c index 7e05f0b6..0bf59ee9 100644 --- a/engine/server/sv_query.c +++ b/engine/server/sv_query.c @@ -165,12 +165,49 @@ static void SV_SourceQuery_Players( netadr_t from ) } } +/* +================== +SV_SourceQuery_CheckChallenge + +Validates the challenge that follows the query type byte. +On missing, mismatched or explicitly requested (-1) challenge, +replies with a new challenge for this address. +================== +*/ +static qboolean SV_SourceQuery_CheckChallenge( netadr_t from, sizebuf_t *msg ) +{ + qboolean error = false; + + MSG_SeekToBit( msg, ( sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET ); + int challenge = MSG_ReadLong( msg ); + + if( !MSG_CheckOverflow( msg ) && challenge != -1 && SV_ValidateChallenge( from, challenge )) + return true; + + challenge = SV_CreateChallenge( from, &error ); + if( error ) + return false; + + sizebuf_t buf; + char answer[16]; + + MSG_Init( &buf, "QueryChallenge", answer, sizeof( answer )); + + MSG_WriteDword( &buf, 0xFFFFFFFFU ); + MSG_WriteByte( &buf, S2C_GOLDSRC_CHALLENGE[0] ); + MSG_WriteLong( &buf, challenge ); + + NET_SendPacket( NS_SERVER, MSG_GetNumBytesWritten( &buf ), MSG_GetData( &buf ), from ); + + return false; +} + /* ================== SV_SourceQuery_HandleConnnectionlessPacket ================== */ -void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from ) +void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from, sizebuf_t *msg ) { if( !Q_strcmp( c, A2S_GOLDSRC_INFO )) { @@ -179,10 +216,12 @@ void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from ) else switch( c[0] ) { case A2S_GOLDSRC_RULES: - SV_SourceQuery_Rules( from ); + if( SV_SourceQuery_CheckChallenge( from, msg )) + SV_SourceQuery_Rules( from ); break; case A2S_GOLDSRC_PLAYERS: - SV_SourceQuery_Players( from ); + if( SV_SourceQuery_CheckChallenge( from, msg )) + SV_SourceQuery_Players( from ); break; } }