From 425decdf7e9284d15aa726e3ae96b9942fb0e3ea Mon Sep 17 00:00:00 2001 From: IronClawTrem Date: Sun, 16 Feb 2020 03:40:06 +0000 Subject: create tremded branch --- src/server/sv_main.cpp | 1551 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1551 insertions(+) create mode 100644 src/server/sv_main.cpp (limited to 'src/server/sv_main.cpp') diff --git a/src/server/sv_main.cpp b/src/server/sv_main.cpp new file mode 100644 index 0000000..f5c3b98 --- /dev/null +++ b/src/server/sv_main.cpp @@ -0,0 +1,1551 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2012-2018 ET:Legacy team +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 3 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see + +=========================================================================== +*/ + +#include "server.h" + +#include + +#ifdef USE_VOIP +cvar_t *sv_voip; +cvar_t *sv_voipProtocol; +#endif + +serverStatic_t svs; // persistant server info +server_t sv {}; // local server + +cvar_t *sv_fps = NULL; // time rate for running non-clients +cvar_t *sv_timeout; // seconds without any message +cvar_t *sv_zombietime; // seconds to sink messages after disconnect +cvar_t *sv_rconPassword; // password for remote server commands +cvar_t *sv_privatePassword; // password for the privateClient slots +cvar_t *sv_allowDownload; +cvar_t *sv_maxclients; + +cvar_t *sv_privateClients; // number of clients reserved for password +cvar_t *sv_hostname; +cvar_t *sv_masters[3][MAX_MASTER_SERVERS]; // master server IP addresses +cvar_t *sv_reconnectlimit; // minimum seconds between connect messages +cvar_t *sv_showloss; // report when usercmds are lost +cvar_t *sv_padPackets; // add nop bytes to messages +cvar_t *sv_killserver; // menu system can set to 1 to shut server down +cvar_t *sv_mapname; +cvar_t *sv_mapChecksum; +cvar_t *sv_serverid; +cvar_t *sv_minRate; +cvar_t *sv_maxRate; +cvar_t *sv_dlRate; +cvar_t *sv_minPing; +cvar_t *sv_maxPing; +cvar_t *sv_pure; +cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491) +cvar_t *sv_banFile; + +cvar_t *sv_rsaAuth; + +cvar_t *sv_schachtmeisterPort; + +// server attack protection +cvar_t *sv_protect; // 0 - unprotected + // 1 - ioquake3 method (default) + // 2 - OpenWolf method + // 4 - prints attack info to console (when ioquake3 or OPenWolf method is set) +cvar_t *sv_protectLog; // name of log file + +/* +============================================================================= + +EVENT MESSAGES + +============================================================================= +*/ + +/* +=============== +SV_ExpandNewlines + +Converts newlines to "\n" so a line prints nicer +=============== +*/ +static char *SV_ExpandNewlines( char *in ) { + static char string[1024]; + int l; + + l = 0; + while ( *in && l < sizeof(string) - 3 ) { + if ( *in == '\n' ) { + string[l++] = '\\'; + string[l++] = 'n'; + } else { + string[l++] = *in; + } + in++; + } + string[l] = 0; + + return string; +} + +/* +====================== +SV_ReplacePendingServerCommands + +FIXME: This is ugly +====================== +*/ +#if 0 // unused +static bool SV_ReplacePendingServerCommands( client_t *client, const char *cmd ) +{ + int i, index, csnum1, csnum2; + + for ( i = client->reliableSent+1; i <= client->reliableSequence; i++ ) { + index = i & ( MAX_RELIABLE_COMMANDS - 1 ); + // + if ( !Q_strncmp(cmd, client->reliableCommands[ index ], strlen("cs")) ) + { + sscanf(cmd, "cs %i", &csnum1); + sscanf(client->reliableCommands[ index ], "cs %i", &csnum2); + if ( csnum1 == csnum2 ) + { + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); + return true; + } + } + } + return false; +} +#endif + +/* +====================== +SV_AddServerCommand + +The given command will be transmitted to the client, and is guaranteed to +not have future snapshot_t executed before it is executed +====================== +*/ +void SV_AddServerCommand( client_t *client, const char *cmd ) { + int index, i; + + // this is very ugly but it's also a waste to for instance send multiple config string updates + // for the same config string index in one snapshot +// if ( SV_ReplacePendingServerCommands( client, cmd ) ) { +// return; +// } + + // do not send commands until the gamestate has been sent + if( client->state < CS_PRIMED ) + return; + + client->reliableSequence++; + // if we would be losing an old command that hasn't been acknowledged, + // we must drop the connection + // we check == instead of >= so a broadcast print added by SV_DropClient() + // doesn't cause a recursive drop client + if ( client->reliableSequence - client->reliableAcknowledge == MAX_RELIABLE_COMMANDS + 1 ) { + Com_Printf( "===== pending server commands =====\n" ); + for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) { + Com_Printf( "cmd %5d: %s\n", i, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ); + } + Com_Printf( "cmd %5d: %s\n", i, cmd ); + SV_DropClient( client, "Server command overflow" ); + return; + } + index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); +} + +/* +================= +SV_SendServerCommand + +Sends a reliable command string to be interpreted by +the client game module: "cp", "print", "chat", etc +A NULL client will broadcast to all clients +================= +*/ +void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) { + va_list argptr; + byte message[MAX_MSGLEN]; + client_t *client; + int j; + + va_start(argptr, fmt); + Q_vsnprintf((char*)message, sizeof(message), fmt,argptr); + va_end(argptr); + + // Fix to http://aluigi.altervista.org/adv/q3msgboom-adv.txt + // The actual cause of the bug is probably further downstream + // and should maybe be addressed later, but this certainly + // fixes the problem for now. + // Summary: The bug is that messages longer than 1022 are not + // allowed downstream and there is a buffer overflow issue + // affecting network traffic etc. Therefore, one way to stop + // this from happening is stopping the packet here. Ideally, + // we should increase the size of the downstream message. + if ( strlen ((char *)message) > 1022 ) { + SV_WriteAttackLog( va( "SV_SendServerCommand( %ld, %.20s... ) length %ld > 1022, " + "dropping to avoid server buffer overflow.\n", + cl - svs.clients, message, strlen( (char *)message ) ) ); + SV_WriteAttackLog( va( "Full message: [%s]\n", message ) ); + return; + } + + if ( cl != NULL ) { + SV_AddServerCommand( cl, (char *)message ); + return; + } + + // hack to echo broadcast prints to console + if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) { + Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) ); + } + + // send the data to all relevent clients + for (j = 0, client = svs.clients; j < sv_maxclients->integer ; j++, client++) { + SV_AddServerCommand( client, (char *)message ); + } +} + + +/* +============================================================================== + +MASTER SERVER FUNCTIONS + +============================================================================== +*/ + +/* +================ +SV_MasterHeartbeat + +Send a message to the masters every few minutes to +let it know we are alive, and log information. +We will also have a heartbeat sent when a server +changes from empty to non-empty, and full to non-full, +but not on every player enter or exit. +================ +*/ +#define HEARTBEAT_MSEC 300*1000 +void SV_MasterHeartbeat(const char *message) +{ + static netadr_t adrs[3][MAX_MASTER_SERVERS][2]; // [2] for v4 and v6 address for the same address string. + int a; + int i; + int res; + int netenabled; + int netAlternateProtocols; + + netenabled = Cvar_VariableIntegerValue("net_enabled"); + netAlternateProtocols = Cvar_VariableIntegerValue("net_alternateProtocols"); + + // "dedicated 1" is for lan play, "dedicated 2" is for inet public play + if (!com_dedicated || com_dedicated->integer != 2 || !(netenabled & (NET_ENABLEV4 | NET_ENABLEV6))) + return; // only dedicated servers send heartbeats + + // if not time yet, don't send anything + if ( svs.time < svs.nextHeartbeatTime ) + return; + + svs.nextHeartbeatTime = svs.time + HEARTBEAT_MSEC; + + for (a = 0; a < 3; ++a) + { + // indent + if(a == 0 && (netAlternateProtocols & NET_DISABLEPRIMPROTO)) + continue; + if(a == 1 && !(netAlternateProtocols & NET_ENABLEALT1PROTO)) + continue; + if(a == 2 && !(netAlternateProtocols & NET_ENABLEALT2PROTO)) + continue; + + // send to group masters + for (i = 0; i < MAX_MASTER_SERVERS; i++) + { + if(!sv_masters[a][i]->string[0]) + continue; + + // see if we haven't already resolved the name + // resolving usually causes hitches on win95, so only + // do it when needed + if(sv_masters[a][i]->modified || (adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD)) + { + sv_masters[a][i]->modified = false; + + if(netenabled & NET_ENABLEV4) + { + Com_Printf("Resolving %s (IPv4)\n", sv_masters[a][i]->string); + res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][0], NA_IP); + adrs[a][i][0].alternateProtocol = a; + + if(res == 2) + { + // if no port was specified, use the default master port + adrs[a][i][0].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER); + } + + if(res) + Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][0])); + else + Com_Printf( "%s has no IPv4 address.\n", sv_masters[a][i]->string); + } + + if(netenabled & NET_ENABLEV6) + { + Com_Printf("Resolving %s (IPv6)\n", sv_masters[a][i]->string); + res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][1], NA_IP6); + adrs[a][i][1].alternateProtocol = a; + + if(res == 2) + { + // if no port was specified, use the default master port + adrs[a][i][1].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER); + } + + if(res) + Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][1])); + else + Com_Printf( "%s has no IPv6 address.\n", sv_masters[a][i]->string); + } + + if(adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD) + { + Com_Printf("Couldn't resolve address: %s\n", sv_masters[a][i]->string); + Cvar_Set(sv_masters[a][i]->name, ""); + sv_masters[a][i]->modified = false; + continue; + } + } + + + Com_Printf ("Sending%s heartbeat to %s\n", (a == 2 ? " alternate-2" : a == 1 ? " alternate-1" : ""), sv_masters[a][i]->string ); + + // this command should be changed if the server info / status format + // ever incompatably changes + + if(adrs[a][i][0].type != NA_BAD) + NET_OutOfBandPrint( NS_SERVER, adrs[a][i][0], "heartbeat %s\n", message); + if(adrs[a][i][1].type != NA_BAD) + NET_OutOfBandPrint( NS_SERVER, adrs[a][i][1], "heartbeat %s\n", message); + } + // outdent + } +} + +/* +================= +SV_MasterShutdown + +Informs all masters that this server is going down +================= +*/ +void SV_MasterShutdown( void ) { + // send a heartbeat right now + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + // send it again to minimize chance of drops + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + // when the master tries to poll the server, it won't respond, so + // it will be removed from the list +} + +/* +============================================================================== + +CONNECTIONLESS COMMANDS + +============================================================================== +*/ + +// This is deliberately quite large to make it more of an effort to DoS +#define MAX_BUCKETS 16384 +#define MAX_HASHES 1024 + +static leakyBucket_t buckets[ MAX_BUCKETS ]; +static leakyBucket_t *bucketHashes[ MAX_HASHES ]; +leakyBucket_t outboundLeakyBucket; + +/* +================ +SVC_HashForAddress +================ +*/ +static long SVC_HashForAddress( netadr_t address ) { + byte *ip = NULL; + size_t size = 0; + int i; + long hash = 0; + + switch ( address.type ) { + case NA_IP: ip = address.ip; size = 4; break; + case NA_IP6: ip = address.ip6; size = 16; break; + default: break; + } + + for ( i = 0; i < size; i++ ) { + hash += (long)( ip[ i ] ) * ( i + 119 ); + } + + hash = ( hash ^ ( hash >> 10 ) ^ ( hash >> 20 ) ); + hash &= ( MAX_HASHES - 1 ); + + return hash; +} + +/* +================ +SVC_BucketForAddress + +Find or allocate a bucket for an address +================ +*/ +static leakyBucket_t *SVC_BucketForAddress( netadr_t address, int burst, int period ) { + leakyBucket_t *bucket = NULL; + long hash = SVC_HashForAddress( address ); + int now = Sys_Milliseconds(); + + for ( bucket = bucketHashes[ hash ]; bucket; bucket = bucket->next ) + { + switch ( bucket->type ) + { + case NA_IP: + if ( ::memcmp( bucket->ipv._4, address.ip, 4 ) == 0 ) + return bucket; + break; + + case NA_IP6: + if ( ::memcmp( bucket->ipv._6, address.ip6, 16 ) == 0 ) + return bucket; + break; + + default: + break; + } + } + + for ( int i = 0; i < MAX_BUCKETS; i++ ) + { + int interval; + + bucket = &buckets[ i ]; + interval = now - bucket->lastTime; + + // Reclaim expired buckets + if ( bucket->lastTime > 0 && ( interval > ( burst * period ) || + interval < 0 ) ) { + if ( bucket->prev != NULL ) { + bucket->prev->next = bucket->next; + } else { + bucketHashes[ bucket->hash ] = bucket->next; + } + + if ( bucket->next != NULL ) { + bucket->next->prev = bucket->prev; + } + + ::memset( bucket, 0, sizeof( leakyBucket_t ) ); + } + + if ( bucket->type == NA_BAD ) { + bucket->type = address.type; + switch ( address.type ) { + case NA_IP: ::memcpy( bucket->ipv._4, address.ip, 4 ); break; + case NA_IP6: ::memcpy( bucket->ipv._6, address.ip6, 16 ); break; + default: break; + } + + bucket->lastTime = now; + bucket->burst = 0; + bucket->hash = hash; + + // Add to the head of the relevant hash chain + bucket->next = bucketHashes[ hash ]; + if ( bucketHashes[ hash ] != NULL ) { + bucketHashes[ hash ]->prev = bucket; + } + + bucket->prev = NULL; + bucketHashes[ hash ] = bucket; + + return bucket; + } + } + + // Couldn't allocate a bucket for this address + // Write the info to the attack log since this is relevant information as the system is malfunctioning + SV_WriteAttackLogD(va("SVC_BucketForAddress: Could not allocate a bucket for client from %s\n", NET_AdrToString(address))); + + return NULL; +} + +/* +================ +SVC_RateLimit + * + * @param[in,out] bucket + * @param[in] burst + * @param[in] period + * @return + * + * @note Don't call if sv_protect 1 (SVP_IOQ3) flag is not set! +================ +*/ +bool SVC_RateLimit( leakyBucket_t *bucket, int burst, int period ) +{ + if ( bucket != NULL ) + { + int now = Sys_Milliseconds(); + int interval = now - bucket->lastTime; + int expired = interval / period; + int expiredRemainder = interval % period; + + if ( expired > bucket->burst || interval < 0 ) + { + bucket->burst = 0; + bucket->lastTime = now; + } + else + { + bucket->burst -= expired; + bucket->lastTime = now - expiredRemainder; + } + + if ( bucket->burst < burst ) + { + bucket->burst++; + return false; + } + else + { + SV_WriteAttackLogD(va("SVC_RateLimit: burst limit exceeded for bucket: %i limit: %i\n", bucket->burst, burst)); + } + } + + return true; +} + +/* +================ +SVC_RateLimitAddress + +Rate limit for a particular address +================ +*/ +bool SVC_RateLimitAddress( netadr_t from, int burst, int period ) +{ + leakyBucket_t *bucket = SVC_BucketForAddress( from, burst, period ); + return SVC_RateLimit( bucket, burst, period ); +} + +/* +================ +SVC_Status + +Responds with all the info that qplug or qspy can see about the server +and all connected players. Used for getting detailed information after +the simple info query. +================ +*/ +static void SVC_Status( netadr_t from ) { + char player[1024]; + char status[MAX_MSGLEN]; + int i; + client_t *cl; + playerState_t *ps; + int statusLength; + int playerLength; + char infostring[MAX_INFO_STRING]; + + if (sv_protect->integer & SVP_IOQ3) { + // Prevent using getstatus as an amplifier + if (SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("SVC_Status: rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + // Allow getstatus to be DoSed relatively easily, but prevent + // excess outbound bandwidth usage when being flooded inbound + if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) { + SV_WriteAttackLog("SVC_Status: rate limit exceeded, dropping request\n"); + return; + } + } + + // A maximum challenge length of 128 should be more than plenty. + if (strlen(Cmd_Argv(1)) > 128) { + SV_WriteAttackLog(va("SVC_Status: challenge length exceeded from %s, dropping request\n", NET_AdrToString(from))); + return; + } + + strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) ); + + // echo back the parameter to status. so master servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + if ( from.alternateProtocol != 0 ) + Info_SetValueForKey( infostring, "protocol", from.alternateProtocol == 2 ? "69" : "70" ); + + status[0] = 0; + statusLength = 0; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state >= CS_CONNECTED ) { + ps = SV_GameClientNum( i ); + Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n", + ps->persistant[PERS_SCORE], cl->ping, cl->name); + playerLength = strlen(player); + if (statusLength + playerLength >= sizeof(status) ) { + break; // can't hold any more + } + strcpy (status + statusLength, player); + statusLength += playerLength; + } + } + + NET_OutOfBandPrint( NS_SERVER, from, "statusResponse\n%s\n%s", infostring, status ); +} + +/* +================ +SVC_Info + +Responds with a short info message that should be enough to determine +if a user is interested in a server to do a full status +================ +*/ +void SVC_Info( netadr_t from ) { + int i, count; + const char *gamedir; + char infostring[MAX_INFO_STRING]; + + if (sv_protect->integer & SVP_IOQ3) { + // Prevent using getinfo as an amplifier + if (SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("SVC_Info: rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + // Allow getinfo to be DoSed relatively easily, but prevent + // excess outbound bandwidth usage when being flooded inbound + if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) { + SV_WriteAttackLog("SVC_Info: rate limit exceeded, dropping request\n"); + return; + } + } + + // Check whether Cmd_Argv(1) has a sane length. This was not done in the original Quake3 version which led + // to the Infostring bug discovered by Luigi Auriemma. See http://aluigi.altervista.org/ for the advisory. + // A maximum challenge length of 128 should be more than plenty. + if (strlen(Cmd_Argv(1)) > 128) { + SV_WriteAttackLog(va("SVC_Info: challenge length from %s exceeded, dropping request\n", NET_AdrToString(from))); + return; + } + + // don't count privateclients + count = 0; + for ( i = sv_privateClients->integer ; i < sv_maxclients->integer ; i++ ) { + if ( svs.clients[i].state >= CS_CONNECTED ) { + count++; + } + } + + infostring[0] = 0; + + // echo back the parameter to status. so servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + Info_SetValueForKey( infostring, "protocol", va("%i", from.alternateProtocol == 2 ? 69 : from.alternateProtocol == 1 ? 70 : PROTOCOL_VERSION) ); + Info_SetValueForKey( infostring, "gamename", com_gamename->string ); + Info_SetValueForKey( infostring, "hostname", sv_hostname->string ); + Info_SetValueForKey( infostring, "mapname", sv_mapname->string ); + Info_SetValueForKey( infostring, "clients", va("%i", count) ); + Info_SetValueForKey( infostring, "sv_maxclients", + va("%i", sv_maxclients->integer - sv_privateClients->integer ) ); + Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) ); + +#ifdef USE_VOIP + if (sv_voipProtocol->string && *sv_voipProtocol->string) { + Info_SetValueForKey( infostring, "voip", sv_voipProtocol->string ); + } +#endif + + if( sv_minPing->integer ) { + Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) ); + } + if( sv_maxPing->integer ) { + Info_SetValueForKey( infostring, "maxPing", va("%i", sv_maxPing->integer) ); + } + gamedir = Cvar_VariableString( "fs_game" ); + if( *gamedir ) { + Info_SetValueForKey( infostring, "game", gamedir ); + } + + NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring ); +} + +/* +================ +SVC_FlushRedirect + +================ +*/ +static void SV_FlushRedirect( char *outputbuf ) { + NET_OutOfBandPrint( NS_SERVER, svs.redirectAddress, "print\n%s", outputbuf ); +} + +/** + * @brief DRDoS stands for "Distributed Reflected Denial of Service". + * See here: http://www.lemuria.org/security/application-drdos.html + * + * If the address isn't NA_IP, it's automatically denied. + * + * @return false if we're good. + * otherwise true means we need to block. + * + * @note Don't call this if sv_protect 2 flag is not set! + */ +bool SV_CheckDRDoS(netadr_t from) { + int i; + int globalCount; + int specificCount; + int timeNow; + receipt_t *receipt; + netadr_t exactFrom; + int oldest; + int oldestTime; + static int lastGlobalLogTime = 0; + static int lastSpecificLogTime = 0; + + // Usually the network is smart enough to not allow incoming UDP packets + // with a source address being a spoofed LAN address. Even if that's not + // the case, sending packets to other hosts in the LAN is not a big deal. + // NA_LOOPBACK qualifies as a LAN address. + if (Sys_IsLANAddress(from)) { + return false; + } + + timeNow = svs.time; + exactFrom = from; + + // Time has wrapped + if (lastGlobalLogTime > timeNow || lastSpecificLogTime > timeNow) { + lastGlobalLogTime = 0; + lastSpecificLogTime = 0; + + // just setting time to 1 (cannot be 0 as then globalCount would not be counted) + for (i = 0; i < MAX_INFO_RECEIPTS; i++) { + if (svs.infoReceipts[i].time) { + svs.infoReceipts[i].time = 1; // hack it so we count globalCount correctly + } + } + } + + if (from.type == NA_IP) { + from.ip[3] = 0; // xx.xx.xx.0 + } else { + from.ip6[15] = 0; + } + + // Count receipts in last 2 seconds. + globalCount = 0; + specificCount = 0; + receipt = &svs.infoReceipts[0]; + oldest = 0; + oldestTime = 0x7fffffff; + for (i = 0; i < MAX_INFO_RECEIPTS; i++, receipt++) { + if (receipt->time + 2000 > timeNow) { + if (receipt->time) { + // When the server starts, all receipt times are at zero. Furthermore, + // svs.time is close to zero. We check that the receipt time is already + // set so that during the first two seconds after server starts, queries + // from the master servers don't get ignored. As a consequence a potentially + // unlimited number of getinfo+getstatus responses may be sent during the + // first frame of a server's life. + globalCount++; + } + if (NET_CompareBaseAdr(from, receipt->adr)) { + specificCount++; + } + } + if (receipt->time < oldestTime) { + oldestTime = receipt->time; + oldest = i; + } + } + + if (globalCount == MAX_INFO_RECEIPTS) { // All receipts happened in last 2 seconds. + if (lastGlobalLogTime + 1000 <= timeNow) { // Limit one log every second. + SV_WriteAttackLog("Detected flood of getinfo/getstatus connectionless packets\n"); + lastGlobalLogTime = timeNow; + } + + return true; + } + if (specificCount >= 3) { // Already sent 3 to this IP in last 2 seconds. + if (lastSpecificLogTime + 1000 <= timeNow) { // Limit one log every second. + SV_WriteAttackLog(va("Possible DRDoS attack to address %s, ignoring getinfo/getstatus connectionless packet\n", + NET_AdrToString(exactFrom))); + lastSpecificLogTime = timeNow; + } + + return true; + } + + receipt = &svs.infoReceipts[oldest]; + receipt->adr = from; + receipt->time = timeNow; + return false; +} + +/* +=============== +SVC_RemoteCommand + +An rcon packet arrived from the network. +Shift down the remaining args +Redirect all printfs +=============== +*/ +static void SVC_RemoteCommand( netadr_t from, msg_t *msg ) { + bool valid; + char remaining[1024]; + // TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc. + // (OOB messages are the bottleneck here) +#define SV_OUTPUTBUF_LENGTH (1024 - 16) + char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; + char *cmd_aux; + + // Prevent using rcon as an amplifier and make dictionary attacks impractical + if ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("Bad rcon - rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + if ( !strlen( sv_rconPassword->string ) || + strcmp (Cmd_Argv(1), sv_rconPassword->string) ) { + static leakyBucket_t bucket; + + // Make DoS via rcon impractical + if ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimit(&bucket, 10, 1000)) { + SV_WriteAttackLog("Bad rcon - rate limit exceeded, dropping request\n"); + return; + } + + valid = false; + Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) ); + } else { + valid = true; + Com_Printf ("Rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) ); + SV_WriteAttackLog(va("Rcon from %s: %s\n", NET_AdrToString(from), Cmd_Argv(2))); + } + + // start redirecting all print outputs to the packet + svs.redirectAddress = from; + Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect); + + if ( !strlen( sv_rconPassword->string ) ) { + Com_Printf ("No rconpassword set on the server.\n"); + } else if ( !valid ) { + Com_Printf ("Bad rconpassword.\n"); + SV_WriteAttackLog(va("Bad rconpassword from %s\n", NET_AdrToString(from))); + } else { + remaining[0] = 0; + + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 + // get the command directly, "rcon " to avoid quoting issues + // extract the command by walking + // since the cmd formatting can fuckup (amount of spaces), using a dumb step by step parsing + cmd_aux = Cmd_Cmd(); + cmd_aux+=4; + while(cmd_aux[0]==' ') + cmd_aux++; + while(cmd_aux[0] && cmd_aux[0]!=' ') // password + cmd_aux++; + while(cmd_aux[0]==' ') + cmd_aux++; + + Q_strcat( remaining, sizeof(remaining), cmd_aux); + + Cmd_ExecuteString (remaining); + + } + + Com_EndRedirect (); +} + +static void SVC_SchachtmeisterResponse( netadr_t from ) { + + int tmp[ 4 ]; + + if ( !( from.type == NA_IP && from.ip[0] == 127 ) ) { + return; + } + + if ( Cmd_Argc() >= 2 && sscanf( Cmd_Argv( 1 ), "%i.%i.%i.%i", &tmp[ 0 ], &tmp[ 1 ], &tmp[ 2 ], &tmp[ 3 ] ) == 4 ) { // compatibility with out-of-date crapware conceived in the future + char cmdl[ MAX_STRING_CHARS ]; + Com_sprintf( cmdl, sizeof( cmdl ), "smr ipa %s", Cmd_ArgsFrom( 1 ) ); + Cmd_TokenizeString( cmdl ); + } else { + strcpy( Cmd_Argv( 0 ), "smr" ); + } + + SV_GameCommand(); +} + +/* +================= +SV_ConnectionlessPacket + +A connectionless packet has four leading 0xff +characters to distinguish it from a game channel. +Clients that are in the game can still send +connectionless packets. +================= +*/ +static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { + char *s; + const char *c; + + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // skip the -1 marker + + if (!Q_strncmp("connect", (char *) &msg->data[4], 7)) { + Huff_Decompress(msg, 12); + } + + s = MSG_ReadStringLine( msg ); + Cmd_TokenizeString( s ); + + c = Cmd_Argv(0); + Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c); + + if (!Q_stricmp(c, "getstatus")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SVC_Status( from ); + } else if (!Q_stricmp(c, "getinfo")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SVC_Info( from ); + } else if (!Q_stricmp(c, "getchallenge")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SV_GetChallenge(from); + } else if (!Q_stricmp(c, "connect")) { + SV_DirectConnect( from ); + } else if (!Q_stricmp(c, "rcon")) { + SVC_RemoteCommand( from, msg ); + } else if (!Q_stricmp(c, "disconnect")) { + // if a client starts up a local server, we may see some spurious + // server disconnect messages when their new server sees our final + // sequenced messages to the old client + } else if (!Q_stricmp(c, "sm2reply")) { + SVC_SchachtmeisterResponse( from ); + Com_Printf( "^2response [^7%s^2]\n", s ); + } else { + SV_WriteAttackLog(va("bad connectionless packet from %s:\n%s\n" // changed from Com_DPrintf to print in attack log + , NET_AdrToString(from), s)); // this was never reported to admins before so they might be confused + } // note: if protect log isn't set we do Com_Printf +} + +//============================================================================ + +/* +================= +SV_PacketEvent +================= +*/ +void SV_PacketEvent( netadr_t from, msg_t *msg ) { + int i; + client_t *cl; + int qport; + + // check for connectionless packet (0xffffffff) first + if ( msg->cursize >= 4 && *(int *)msg->data == -1) { + SV_ConnectionlessPacket( from, msg ); + return; + } + + // read the qport out of the message so we can fix up + // stupid address translating routers + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // sequence number + qport = MSG_ReadShort( msg ) & 0xffff; + + // find which client the message is from + for (i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + if (cl->state == CS_FREE) { + continue; + } + if ( !NET_CompareBaseAdr( from, cl->netchan.remoteAddress ) ) { + continue; + } + // it is possible to have multiple clients from a single IP + // address, so they are differentiated by the qport variable + if (cl->netchan.qport != qport) { + continue; + } + + // the IP port can't be used to differentiate them, because + // some address translating routers periodically change UDP + // port assignments + if (cl->netchan.remoteAddress.port != from.port) { + Com_Printf( "SV_PacketEvent: fixing up a translated port\n" ); + cl->netchan.remoteAddress.port = from.port; + } + + // make sure it is a valid, in sequence packet + if (SV_Netchan_Process(cl, msg)) { + // zombie clients still need to do the Netchan_Process + // to make sure they don't need to retransmit the final + // reliable message, but they don't do any other processing + if (cl->state != CS_ZOMBIE) { + cl->lastPacketTime = svs.time; // don't timeout + SV_ExecuteClientMessage( cl, msg ); + } + } + return; + } +} + + +/* +=================== +SV_CalcPings + +Updates the cl->ping variables +=================== +*/ +static void SV_CalcPings( void ) { + int i, j; + client_t *cl; + int total, count; + int delta; + playerState_t *ps; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state != CS_ACTIVE ) { + cl->ping = 999; + continue; + } + if ( !cl->gentity ) { + cl->ping = 999; + continue; + } + + total = 0; + count = 0; + for ( j = 0 ; j < PACKET_BACKUP ; j++ ) { + if ( cl->frames[j].messageAcked <= 0 ) { + continue; + } + delta = cl->frames[j].messageAcked - cl->frames[j].messageSent; + count++; + total += delta; + } + if (!count) { + cl->ping = 999; + } else { + cl->ping = total/count; + if ( cl->ping > 999 ) { + cl->ping = 999; + } + } + + // let the game dll know about the ping + ps = SV_GameClientNum( i ); + ps->ping = cl->ping; + } +} + +/* +================== +SV_CheckTimeouts + +If a packet has not been received from a client for timeout->integer +seconds, drop the conneciton. Server time is used instead of +realtime to avoid dropping the local client while debugging. + +When a client is normally dropped, the client_t goes into a zombie state +for a few seconds to make sure any final reliable message gets resent +if necessary +================== +*/ +static void SV_CheckTimeouts( void ) { + int i; + client_t *cl; + int droppoint; + int zombiepoint; + + droppoint = svs.time - 1000 * sv_timeout->integer; + zombiepoint = svs.time - 1000 * sv_zombietime->integer; + + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + // message times may be wrong across a changelevel + if (cl->lastPacketTime > svs.time) { + cl->lastPacketTime = svs.time; + } + + if (cl->state == CS_ZOMBIE + && cl->lastPacketTime < zombiepoint) { + // using the client id cause the cl->name is empty at this point + Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i ); + cl->state = CS_FREE; // can now be reused + continue; + } + if ( cl->state >= CS_CONNECTED && cl->lastPacketTime < droppoint) { + // wait several frames so a debugger session doesn't + // cause a timeout + if ( ++cl->timeoutCount > 5 ) { + SV_DropClient (cl, "timed out"); + cl->state = CS_FREE; // don't bother with zombie state + } + } else { + cl->timeoutCount = 0; + } + } +} + + +/* +================== +SV_CheckPaused +================== +*/ +static bool SV_CheckPaused( void ) { + int count; + client_t *cl; + int i; + + if ( !cl_paused->integer ) { + return false; + } + + // only pause if there is just a single client connected + count = 0; + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + if ( cl->state >= CS_CONNECTED ) { + count++; + } + } + + if ( count > 1 ) { + // don't pause + if (sv_paused->integer) + Cvar_Set("sv_paused", "0"); + return false; + } + + if (!sv_paused->integer) + Cvar_Set("sv_paused", "1"); + return true; +} + +/* +================== +SV_FrameMsec +Return time in millseconds until processing of the next server frame. +================== +*/ +int SV_FrameMsec() +{ + if(sv_fps) + { + int frameMsec; + + frameMsec = 1000.0f / sv_fps->value; + + if(frameMsec < sv.timeResidual) + return 0; + else + return frameMsec - sv.timeResidual; + } + else + return 1; +} + +#define CPU_USAGE_WARNING 70 +#define FRAME_TIME_WARNING 30 + +/* +================== +SV_Frame + +Player movement occurs as a result of packet events, which +happen before SV_Frame is called +================== +*/ +void SV_Frame( int msec ) { + int frameMsec; + int startTime; + int frameStartTime = 0; + static int start, end; + + start = Sys_Milliseconds(); + svs.stats.idle += ( double )(start - end) / 1000; + + // the menu kills the server with this cvar + if ( sv_killserver->integer ) { + SV_Shutdown ("Server was killed"); + Cvar_Set( "sv_killserver", "0" ); + return; + } + + if (!com_sv_running->integer) + { + // Running as a server, but no map loaded +#ifdef DEDICATED + // Block until something interesting happens + Sys_Sleep(-1); +#endif + + return; + } + + // allow pause if only the local client is connected + if ( SV_CheckPaused() ) { + return; + } + + if (com_dedicated->integer) + { + frameStartTime = Sys_Milliseconds(); + } + + // if it isn't time for the next frame, do nothing + if ( sv_fps->integer < 1 ) { + Cvar_Set( "sv_fps", "10" ); + } + + frameMsec = 1000 / sv_fps->integer * com_timescale->value; + // don't let it scale below 1ms + if(frameMsec < 1) + { + Cvar_Set("timescale", va("%f", sv_fps->integer / 1000.0f)); + frameMsec = 1; + } + + sv.timeResidual += msec; + + // if time is about to hit the 32nd bit, kick all clients + // and clear sv.time, rather + // than checking for negative time wraparound everywhere. + // 2giga-milliseconds = 23 days, so it won't be too often + if ( svs.time > 0x70000000 ) { + SV_Shutdown( "Restarting server due to time wrapping" ); + Cbuf_AddText( va( "map \"%s\"\n", Cvar_VariableString( "mapname" ) ) ); + return; + } + // this can happen considerably earlier when lots of clients play and the map doesn't change + if ( svs.nextSnapshotEntities >= 0x7FFFFFFE - svs.numSnapshotEntities ) { + SV_Shutdown( "Restarting server due to numSnapshotEntities wrapping" ); + Cbuf_AddText( va( "map \"%s\"\n", Cvar_VariableString( "mapname" ) ) ); + return; + } + + if( sv.restartTime && sv.time >= sv.restartTime ) { + sv.restartTime = 0; + Cbuf_AddText( "map_restart 0\n" ); + return; + } + + // update infostrings if anything has been changed + if ( cvar_modifiedFlags & CVAR_SERVERINFO ) { + SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); + cvar_modifiedFlags &= ~CVAR_SERVERINFO; + } + if ( cvar_modifiedFlags & CVAR_SYSTEMINFO ) { + SV_SetConfigstring( CS_SYSTEMINFO, Cvar_InfoString_Big( CVAR_SYSTEMINFO ) ); + cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; + } + + if ( com_speeds->integer ) { + startTime = Sys_Milliseconds (); + } else { + startTime = 0; // quite a compiler warning + } + + // update ping based on the all received frames + SV_CalcPings(); + + // run the game simulation in chunks + while ( sv.timeResidual >= frameMsec ) { + sv.timeResidual -= frameMsec; + svs.time += frameMsec; + sv.time += frameMsec; + + // let everything in the world think and move + VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time); + } + + if ( com_speeds->integer ) { + time_game = Sys_Milliseconds () - startTime; + } + + // check timeouts + SV_CheckTimeouts(); + + // send messages back to the clients + SV_SendClientMessages(); + + // send a heartbeat to the master if needed + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + if (com_dedicated->integer) + { + int frameEndTime = Sys_Milliseconds(); + + svs.totalFrameTime += (frameEndTime - frameStartTime); + + // we may send warnings (similar to watchdog) to the game in case the frametime is unacceptable + //Com_Printf("FRAMETIME frame: %i total %i\n", frameEndTime - frameStartTime, svs.totalFrameTime); + + svs.currentFrameIndex++; + + //if( svs.currentFrameIndex % 50 == 0 ) + // Com_Printf( "currentFrameIndex: %i\n", svs.currentFrameIndex ); + + if (svs.currentFrameIndex == SERVER_PERFORMANCECOUNTER_FRAMES) + { + int averageFrameTime = svs.totalFrameTime / SERVER_PERFORMANCECOUNTER_FRAMES; + + svs.sampleTimes[svs.currentSampleIndex % SERVER_PERFORMANCECOUNTER_SAMPLES] = averageFrameTime; + svs.currentSampleIndex++; + + if (svs.currentSampleIndex > SERVER_PERFORMANCECOUNTER_SAMPLES) + { + int totalTime = 0, i; + + for (i = 0; i < SERVER_PERFORMANCECOUNTER_SAMPLES; i++) + { + totalTime += svs.sampleTimes[i]; + } + + if (!totalTime) + { + totalTime = 1; + } + + averageFrameTime = totalTime / SERVER_PERFORMANCECOUNTER_SAMPLES; + + svs.serverLoad = (int)((averageFrameTime / (float)frameMsec) * 100); + } + + //Com_Printf( "serverload: %i (%i/%i)\n", svs.serverLoad, averageFrameTime, frameMsec ); + + svs.totalFrameTime = 0; + svs.currentFrameIndex = 0; + } + } + else + { + svs.serverLoad = -1; + } + + // collect timing statistics + // - the above 2.60 performance thingy is just inaccurate (30 seconds 'stats') + // to give good warning messages and is only done for dedicated + end = Sys_Milliseconds(); + svs.stats.active += (( double )(end - start)) / 1000; + + if (++svs.stats.count == STATFRAMES) // 5 seconds + { + svs.stats.latched_active = svs.stats.active; + svs.stats.latched_idle = svs.stats.idle; + svs.stats.active = 0; + svs.stats.idle = 0; + svs.stats.count = 0; + + svs.stats.cpu = svs.stats.latched_active + svs.stats.latched_idle; + + if (svs.stats.cpu != 0.f) + { + svs.stats.cpu = 100 * svs.stats.latched_active / svs.stats.cpu; + } + + svs.stats.avg = 1000 * svs.stats.latched_active / STATFRAMES; + + // FIXME: add mail, IRC, player info etc for both warnings + // TODO: inspect/adjust these values and/or add cvars + if (svs.stats.cpu > CPU_USAGE_WARNING) + { + Com_Printf("^3WARNING: Server CPU has reached a critical usage of %i%%\n", (int) svs.stats.cpu); + } + + if (svs.stats.avg > FRAME_TIME_WARNING) + { + Com_Printf("^3WARNING: Average frame time has reached a critical value of %ims\n", (int) svs.stats.avg); + } + } +} + +/* +==================== +SV_RateMsec + +Return the number of msec until another message can be sent to +a client based on its rate settings +==================== +*/ + +#define UDPIP_HEADER_SIZE 28 +#define UDPIP6_HEADER_SIZE 48 + +int SV_RateMsec(client_t *client) +{ + int rate, rateMsec; + int messageSize; + + messageSize = client->netchan.lastSentSize; + rate = client->rate; + + if(sv_maxRate->integer) + { + if(sv_maxRate->integer < 1000) + Cvar_Set( "sv_MaxRate", "1000" ); + if(sv_maxRate->integer < rate) + rate = sv_maxRate->integer; + } + + if(sv_minRate->integer) + { + if(sv_minRate->integer < 1000) + Cvar_Set("sv_minRate", "1000"); + if(sv_minRate->integer > rate) + rate = sv_minRate->integer; + } + + if(client->netchan.remoteAddress.type == NA_IP6) + messageSize += UDPIP6_HEADER_SIZE; + else + messageSize += UDPIP_HEADER_SIZE; + + rate = (int)(rate * com_timescale->value); + if(rate < 1) + rate = 1; + rateMsec = messageSize * 1000 / rate; + rate = Sys_Milliseconds() - client->netchan.lastSentTime; + + if(rate > rateMsec) + return 0; + else + return rateMsec - rate; +} + +/* +==================== +SV_SendQueuedPackets + +Send download messages and queued packets in the time that we're idle, i.e. +not computing a server frame or sending client snapshots. +Return the time in msec until we expect to be called next +==================== +*/ + +int SV_SendQueuedPackets() +{ + int numBlocks; + int dlStart, deltaT, delayT; + static int dlNextRound = 0; + int timeVal = INT_MAX; + + // Send out fragmented packets now that we're idle + delayT = SV_SendQueuedMessages(); + if(delayT >= 0) + timeVal = delayT; + + if(sv_dlRate->integer) + { + // Rate limiting. This is very imprecise for high + // download rates due to millisecond timedelta resolution + dlStart = Sys_Milliseconds(); + deltaT = dlNextRound - dlStart; + + if(deltaT > 0) + { + if(deltaT < timeVal) + timeVal = deltaT + 1; + } + else + { + numBlocks = SV_SendDownloadMessages(); + + if(numBlocks) + { + // There are active downloads + deltaT = Sys_Milliseconds() - dlStart; + + delayT = 1000 * numBlocks * MAX_DOWNLOAD_BLKSIZE; + delayT /= sv_dlRate->integer * 1024; + + if(delayT <= deltaT + 1) + { + // Sending the last round of download messages + // took too long for given rate, don't wait for + // next round, but always enforce a 1ms delay + // between DL message rounds so we don't hog + // all of the bandwidth. This will result in an + // effective maximum rate of 1MB/s per user, but the + // low download window size limits this anyways. + if(timeVal > 2) + timeVal = 2; + + dlNextRound = dlStart + deltaT + 1; + } + else + { + dlNextRound = dlStart + delayT; + delayT -= deltaT; + + if(delayT < timeVal) + timeVal = delayT; + } + } + } + } + else + { + if(SV_SendDownloadMessages()) + timeVal = 0; + } + + return timeVal; +} -- cgit