engine: server: add challenge to A2S_GOLDSRC_RULES and A2S_GOLDSRC_PLAYERS

This commit is contained in:
Alibek Omarov
2026-07-22 10:03:03 +05:00
parent 3a15357dc8
commit e09773c912
4 changed files with 82 additions and 17 deletions

View File

@@ -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. 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. 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.

View File

@@ -542,6 +542,8 @@ void SV_UpdateServerInfo( void );
void SV_EndRedirect( host_redirect_t *rd ); void SV_EndRedirect( host_redirect_t *rd );
void SV_RejectConnection( netadr_t from, const char *fmt, ... ) FORMAT_CHECK( 2 ); void SV_RejectConnection( netadr_t from, const char *fmt, ... ) FORMAT_CHECK( 2 );
void SV_GetPlayerCount( int *clients, int *bots ); 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 ) static inline qboolean SV_HavePassword( void )
{ {
@@ -701,6 +703,6 @@ int SV_LightForEntity( edict_t *pEdict );
// //
// sv_query.c // 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 #endif//SERVER_H

View File

@@ -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; 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; qboolean error = false;
uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS ); 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 ) if( error )
return; return;
@@ -213,15 +245,7 @@ Make sure connecting client is not spoofing
*/ */
static int SV_CheckChallenge( netadr_t from, int challenge ) static int SV_CheckChallenge( netadr_t from, int challenge )
{ {
qboolean error = false; if( SV_ValidateChallenge( from, challenge ))
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 )
return true; return true;
SV_RejectConnection( from, "no challenge for your address\n" ); 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. // `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 ) 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 )) else if( !Q_strcmp( pcmd, A2A_INFO ))
{ {

View File

@@ -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 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 )) 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] ) else switch( c[0] )
{ {
case A2S_GOLDSRC_RULES: case A2S_GOLDSRC_RULES:
SV_SourceQuery_Rules( from ); if( SV_SourceQuery_CheckChallenge( from, msg ))
SV_SourceQuery_Rules( from );
break; break;
case A2S_GOLDSRC_PLAYERS: case A2S_GOLDSRC_PLAYERS:
SV_SourceQuery_Players( from ); if( SV_SourceQuery_CheckChallenge( from, msg ))
SV_SourceQuery_Players( from );
break; break;
} }
} }