/** * @brief Responds with all the info that the server browser can see * @sa SV_StatusString */ static void SVC_Status (struct net_stream* s) { if (SVC_RateLimitAddress(*s)) { Com_DPrintf(DEBUG_SERVER, "SVC_Status: rate limit from %s exceeded, dropping request\n", NET_StreamToString(s)); return; } /* Allow getstatus to be DoSed relatively easily, but prevent excess outbound bandwidth usage when being flooded inbound */ if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) { Com_DPrintf(DEBUG_SERVER, "SVC_Status: rate limit exceeded, dropping request\n"); return; } dbuffer msg; NET_WriteByte(&msg, svc_oob); NET_WriteRawString(&msg, SV_CMD_PRINT "\n"); char info[MAX_INFO_STRING]; NET_WriteRawString(&msg, Cvar_Serverinfo(info, sizeof(info))); NET_WriteRawString(&msg, "\n"); client_t* cl = nullptr; while ((cl = SV_GetNextClient(cl)) != nullptr) { if (cl->state <= cs_free) continue; char player[1024]; Com_sprintf(player, sizeof(player), "%i \"%s\"\n", svs.ge->ClientGetTeamNum(*cl->player), cl->name); NET_WriteRawString(&msg, player); } NET_WriteMsg(s, msg); }
/* =============== SV_ClientCommand =============== */ static qboolean SV_ClientCommand( client_t *cl, msg_t *msg ) { int seq; const char *s; qboolean clientOk = qtrue; seq = MSG_ReadLong( msg ); s = MSG_ReadString( msg ); // see if we have already executed it if ( cl->lastClientCommand >= seq ) { return qtrue; } Com_DPrintf( "clientCommand: %s : %i : %s\n", cl->name, seq, s ); // drop the connection if we have somehow lost commands if ( seq > cl->lastClientCommand + 1 ) { Com_Printf( "Client %s lost %i clientCommands\n", cl->name, seq - cl->lastClientCommand + 1 ); SV_DropClient( cl, "Lost reliable commands" ); return qfalse; } // malicious users may try using too many string commands // to lag other players. If we decide that we want to stall // the command, we will stop processing the rest of the packet, // including the usercmd. This causes flooders to lag themselves // but not other people // We don't do this when the client hasn't been active yet since its // normal to spam a lot of commands when downloading // Applying floodprotect only to "CS_ACTIVE" clients leaves too much room for abuse. Extending floodprotect to clients pre CS_ACTIVE shouldn't cause any issues, as the download-commands are handled within the engine and floodprotect only filters calls to the VM. if ( !com_cl_running->integer && /* cl->state >= CS_ACTIVE && */ sv_floodProtect->integer ) { if ( sv_floodProtect->integer == 1 && svs.time < cl->nextReliableTime ) { clientOk = qfalse; } else if ( SVC_RateLimit(&cl->cmdBucket, sv_floodProtect->integer, 1000, svs.time) ) { clientOk = qfalse; } } // don't allow another command for one second cl->nextReliableTime = svs.time + 1000; SV_ExecuteClientCommand( cl, s, clientOk ); cl->lastClientCommand = seq; Com_sprintf(cl->lastClientCommandString, sizeof(cl->lastClientCommandString), "%s", s); return qtrue; // continue procesing }
/** * @brief A client issued an rcon command. Shift down the remaining args. Redirect all printfs */ static void SVC_RemoteCommand (struct net_stream* stream) { char buf[64]; const char* peername = NET_StreamPeerToName(stream, buf, sizeof(buf), false); /* Prevent using rcon as an amplifier and make dictionary attacks impractical */ if (SVC_RateLimitAddress(*stream)) { Com_DPrintf(DEBUG_SERVER, "SVC_RemoteCommand: rate limit from %s exceeded, dropping request\n", peername); return; } const bool valid = Rcon_Validate(Cmd_Argv(1)); if (!valid) { static leakyBucket_t bucket; /* Make DoS via rcon impractical */ if (SVC_RateLimit(&bucket, 10, 1000)) { Com_DPrintf(DEBUG_SERVER, "SVC_RemoteCommand: rate limit exceeded, dropping request\n"); return; } Com_Printf("Bad rcon from %s with password: '******'\n", peername, Cmd_Argv(1)); } else { Com_Printf("Rcon from %s\n", peername); } static char sv_outputbuf[1024]; Com_BeginRedirect(stream, sv_outputbuf, sizeof(sv_outputbuf)); if (!valid) { /* inform the client */ Com_Printf(BAD_RCON_PASSWORD); } else { char remaining[1024] = ""; int i; /* execute the rcon commands */ for (i = 2; i < Cmd_Argc(); i++) { Q_strcat(remaining, sizeof(remaining), "%s ", Cmd_Argv(i)); } /* execute the string */ Cmd_ExecuteString("%s", remaining); } Com_EndRedirect(); }
/** * @brief Responds with short info for broadcast scans * @note The second parameter should be the current protocol version number. * @note Only a short server description - the user can determine whether he is * interested in a full status * @sa CL_ParseStatusMessage * @sa CL_ProcessPingReply */ static void SVC_Info (struct net_stream* s) { if (SVC_RateLimitAddress(*s)) { Com_DPrintf(DEBUG_SERVER, "SVC_Info: rate limit from %s exceeded, dropping request\n", NET_StreamToString(s)); return; } /* Allow getinfo to be DoSed relatively easily, but prevent excess outbound bandwidth usage when being flooded inbound */ if (SVC_RateLimit(&outboundLeakyBucket)) { Com_DPrintf(DEBUG_SERVER, "SVC_Info: rate limit exceeded, dropping request\n"); return; } if (sv_maxclients->integer == 1) { Com_DPrintf(DEBUG_SERVER, "Ignore info string in singleplayer mode\n"); return; /* ignore in single player */ } const int version = atoi(Cmd_Argv(1)); if (version != PROTOCOL_VERSION) { char string[MAX_VAR]; Com_sprintf(string, sizeof(string), "%s: wrong version (client: %i, host: %i)\n", sv_hostname->string, version, PROTOCOL_VERSION); NET_OOB_Printf(s, SV_CMD_PRINT "\n%s", string); return; } int count = 0; client_t* cl = nullptr; while ((cl = SV_GetNextClient(cl)) != nullptr) if (cl->state >= cs_spawning) count++; char infostring[MAX_INFO_STRING]; infostring[0] = '\0'; Info_SetValueForKey(infostring, sizeof(infostring), "sv_protocol", DOUBLEQUOTE(PROTOCOL_VERSION)); Info_SetValueForKey(infostring, sizeof(infostring), "sv_hostname", sv_hostname->string); Info_SetValueForKey(infostring, sizeof(infostring), "sv_dedicated", sv_dedicated->string); Info_SetValueForKey(infostring, sizeof(infostring), "sv_gametype", sv_gametype->string); Info_SetValueForKey(infostring, sizeof(infostring), "sv_mapname", sv->name); Info_SetValueForKeyAsInteger(infostring, sizeof(infostring), "clients", count); Info_SetValueForKey(infostring, sizeof(infostring), "sv_maxclients", sv_maxclients->string); Info_SetValueForKey(infostring, sizeof(infostring), "sv_version", UFO_VERSION); NET_OOB_Printf(s, SV_CMD_INFO "\n%s", infostring); }
/** * @brief Responds with teaminfo such as free team num * @sa CL_ParseTeamInfoMessage */ static void SVC_TeamInfo (struct net_stream* s) { if (SVC_RateLimitAddress(*s)) { Com_DPrintf(DEBUG_SERVER, "SVC_TeamInfo: rate limit from %s exceeded, dropping request\n", NET_StreamToString(s)); return; } /* Allow getinfo to be DoSed relatively easily, but prevent excess outbound bandwidth usage when being flooded inbound */ if (SVC_RateLimit(&outboundLeakyBucket)) { Com_DPrintf(DEBUG_SERVER, "SVC_TeamInfo: rate limit exceeded, dropping request\n"); return; } char infoGlobal[MAX_INFO_STRING] = ""; Info_SetValueForKey(infoGlobal, sizeof(infoGlobal), "sv_teamplay", Cvar_GetString("sv_teamplay")); Info_SetValueForKey(infoGlobal, sizeof(infoGlobal), "sv_maxteams", Cvar_GetString("sv_maxteams")); Info_SetValueForKey(infoGlobal, sizeof(infoGlobal), "sv_maxplayersperteam", Cvar_GetString("sv_maxplayersperteam")); dbuffer msg; NET_WriteByte(&msg, svc_oob); NET_WriteRawString(&msg, "teaminfo\n"); NET_WriteString(&msg, infoGlobal); client_t* cl = nullptr; while ((cl = SV_GetNextClient(cl)) != nullptr) { if (cl->state < cs_connected) continue; char infoPlayer[MAX_INFO_STRING] = ""; /* show players that already have a team with their teamnum */ int teamId = svs.ge->ClientGetTeamNum(*cl->player); if (!teamId) teamId = TEAM_NO_ACTIVE; Info_SetValueForKeyAsInteger(infoPlayer, sizeof(infoPlayer), "cl_team", teamId); Info_SetValueForKeyAsInteger(infoPlayer, sizeof(infoPlayer), "cl_ready", svs.ge->ClientIsReady(cl->player)); Info_SetValueForKey(infoPlayer, sizeof(infoPlayer), "cl_name", cl->name); NET_WriteString(&msg, infoPlayer); } NET_WriteByte(&msg, 0); NET_WriteMsg(s, msg); }
/* ================= SV_GetChallenge A "getchallenge" OOB command has been received Returns a challenge number that can be used in a subsequent connectResponse command. We do this to prevent denial of service attacks that flood the server with invalid connection IPs. With a challenge, they must give a valid IP address. If we are authorizing, a challenge request will cause a packet to be sent to the authorize server. When an authorizeip is returned, a challenge response will be sent to that ip. ioquake3/openjk: we added a possibility for clients to add a challenge to their packets, to make it more difficult for malicious servers to hi-jack client connections. ================= */ void SV_GetChallenge( netadr_t from ) { int i; int oldest; int oldestTime; int clientChallenge; challenge_t *challenge; // ignore if we are in single player /* if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) { return; } */ if (Cvar_VariableValue("ui_singlePlayerActive")) { return; } // Prevent using getchallenge as an amplifier if ( SVC_RateLimitAddress( from, 10, 1000 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit from %s exceeded, dropping request\n", NET_AdrToString( from ) ); return; } // Allow getchallenge to be DoSed relatively easily, but prevent // excess outbound bandwidth usage when being flooded inbound if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit exceeded, dropping request\n" ); return; } oldest = 0; oldestTime = 0x7fffffff; // see if we already have a challenge for this ip challenge = &svs.challenges[0]; clientChallenge = atoi(Cmd_Argv(1)); for (i = 0 ; i < MAX_CHALLENGES ; i++, challenge++) { if(!challenge->connected && NET_CompareAdr(from, challenge->adr)) { break; } if ( challenge->time < oldestTime ) { oldestTime = challenge->time; oldest = i; } } if (i == MAX_CHALLENGES) { // this is the first time this client has asked for a challenge challenge = &svs.challenges[oldest]; challenge->adr = from; challenge->firstTime = svs.time; challenge->time = svs.time; challenge->connected = qfalse; } // always generate a new challenge number, so the client cannot circumvent sv_maxping challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time; challenge->wasrefused = qfalse; challenge->pingTime = svs.time; NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %i %i", challenge->challenge, clientChallenge ); }
/** * @brief Rate limit for a particular address */ static bool SVC_RateLimitAddress (struct net_stream& from, int burst = 10, int period = 1000) { leakyBucket_t* bucket = SVC_BucketForAddress(from, burst, period); return SVC_RateLimit(bucket, burst, period); }
/* ================= SV_GetChallenge A "getchallenge" OOB command has been received Returns a challenge number that can be used in a subsequent connectResponse command. We do this to prevent denial of service attacks that flood the server with invalid connection IPs. With a challenge, they must give a valid IP address. If we are authorizing, a challenge request will cause a packet to be sent to the authorize server. When an authorizeip is returned, a challenge response will be sent to that ip. ioquake3: we added a possibility for clients to add a challenge to their packets, to make it more difficult for malicious servers to hi-jack client connections. Also, the auth stuff is completely disabled for com_standalone games as well as IPv6 connections, since there is no way to use the v4-only auth server for these new types of connections. ================= */ void SV_GetChallenge(netadr_t from) { int i; int oldest; int oldestTime; int oldestClientTime; int clientChallenge; challenge_t *challenge; qboolean wasfound = qfalse; char *gameName; qboolean gameMismatch; // Prevent using getchallenge as an amplifier if ( SVC_RateLimitAddress( from, 10, 1000 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit from %s exceeded, dropping request\n", NET_AdrToString( from ) ); return; } // Allow getchallenge to be DoSed relatively easily, but prevent // excess outbound bandwidth usage when being flooded inbound if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit exceeded, dropping request\n" ); return; } gameName = Cmd_Argv(2); gameMismatch = !*gameName || strcmp(gameName, com_gamename->string) != 0; // reject client if the gamename string sent by the client doesn't match ours if (gameMismatch) { NET_OutOfBandPrint(NS_SERVER, from, "print\nGame mismatch: This is a %s server\n", com_gamename->string); return; } oldest = 0; oldestClientTime = oldestTime = 0x7fffffff; // see if we already have a challenge for this ip challenge = &svs.challenges[0]; clientChallenge = atoi(Cmd_Argv(1)); for(i = 0 ; i < MAX_CHALLENGES ; i++, challenge++) { if(!challenge->connected && NET_CompareAdr(from, challenge->adr)) { wasfound = qtrue; if(challenge->time < oldestClientTime) oldestClientTime = challenge->time; } if(wasfound && i >= MAX_CHALLENGES_MULTI) { i = MAX_CHALLENGES; break; } if(challenge->time < oldestTime) { oldestTime = challenge->time; oldest = i; } } if (i == MAX_CHALLENGES) { // this is the first time this client has asked for a challenge challenge = &svs.challenges[oldest]; challenge->clientChallenge = clientChallenge; challenge->adr = from; challenge->firstTime = svs.time; challenge->connected = qfalse; } // always generate a new challenge number, so the client cannot circumvent sv_maxping challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time; challenge->wasrefused = qfalse; challenge->time = svs.time; challenge->pingTime = svs.time; NET_OutOfBandPrint(NS_SERVER, challenge->adr, "challengeResponse %d %d %d", challenge->challenge, clientChallenge, PROTOCOL_VERSION); }
/* ================= SV_GetChallenge A "getchallenge" OOB command has been received Returns a challenge number that can be used in a subsequent connectResponse command. We do this to prevent denial of service attacks that flood the server with invalid connection IPs. With a challenge, they must give a valid IP address. If we are authorizing, a challenge request will cause a packet to be sent to the authorize server. When an authorizeip is returned, a challenge response will be sent to that ip. ioquake3: we added a possibility for clients to add a challenge to their packets, to make it more difficult for malicious servers to hi-jack client connections. Also, the auth stuff is completely disabled for com_standalone games as well as IPv6 connections, since there is no way to use the v4-only auth server for these new types of connections. ================= */ void SV_GetChallenge( netadr_t from ) { static leakyBucket_t outboundLeakyBucket; int i; int oldest; int oldestTime; int oldestClientTime; int clientChallenge; challenge_t *challenge; qboolean wasfound = qfalse; // ignore if we are in single player if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) { return; } // Prevent using getchallenge as an amplifier if ( SVC_RateLimitAddress( from, 10, 1000 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit from %s exceeded, dropping request\n", NET_AdrToString( from ) ); return; } // Allow getchallenge to be DoSed relatively easily, but prevent // excess outbound bandwidth usage when being flooded inbound if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) { Com_DPrintf( "SV_GetChallenge: rate limit exceeded, dropping request\n" ); return; } oldest = 0; oldestClientTime = oldestTime = 0x7fffffff; // see if we already have a challenge for this ip challenge = &svs.challenges[0]; clientChallenge = atoi( Cmd_Argv( 1 ) ); for (i = 0 ; i < MAX_CHALLENGES ; i++, challenge++ ) { if ( !challenge->connected && NET_CompareAdr( from, challenge->adr ) ) { wasfound = qtrue; if ( challenge->time < oldestClientTime ) oldestClientTime = challenge->time; } if ( wasfound && i >= MAX_CHALLENGES_MULTI ) { i = MAX_CHALLENGES; break; } if ( challenge->time < oldestTime ) { oldestTime = challenge->time; oldest = i; } } if (i == MAX_CHALLENGES) { // this is the first time this client has asked for a challenge challenge = &svs.challenges[oldest]; challenge->clientChallenge = clientChallenge; challenge->adr = from; challenge->firstTime = svs.time; challenge->connected = qfalse; } // always generate a new challenge number, so the client cannot circumvent sv_maxping challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time; challenge->wasrefused = qfalse; challenge->time = svs.time; #if !defined(STANDALONE) && defined (USE_AUTHORIZE) // Drop the authorize stuff if this client is coming in via v6 as the auth server does not support ipv6. // Drop also for addresses coming in on local LAN and for stand-alone games independent from id's assets. if(challenge->adr.type == NA_IP && !Cvar_VariableIntegerValue("com_standalone") && !Sys_IsLANAddress(from)) { // look up the authorize server's IP if (svs.authorizeAddress.type == NA_BAD) { Com_Printf( "Resolving %s\n", AUTHORIZE_SERVER_NAME ); if (NET_StringToAdr(AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP)) { svs.authorizeAddress.port = BigShort( PORT_AUTHORIZE ); Com_Printf( "%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME, svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1], svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3], BigShort( svs.authorizeAddress.port ) ); } } // we couldn't contact the auth server, let them in. if(svs.authorizeAddress.type == NA_BAD) Com_Printf("Couldn't resolve auth server address\n"); // if they have been challenging for a long time and we // haven't heard anything from the authorize server, go ahead and // let them in, assuming the id server is down else if(svs.time - oldestClientTime > AUTHORIZE_TIMEOUT) Com_DPrintf( "authorize server timed out\n" ); else { // otherwise send their ip to the authorize server cvar_t *fs; char game[1024]; Com_DPrintf( "sending getIpAuthorize for %s\n", NET_AdrToString( from )); strcpy(game, IDBASEGAME); fs = Cvar_Get ("fs_game", "", CVAR_INIT|CVAR_SYSTEMINFO ); if (fs && fs->string[0] != 0) { strcpy(game, fs->string); } // the 0 is for backwards compatibility with obsolete sv_allowanonymous flags // getIpAuthorize <challenge> <IP> <game> 0 <auth-flag> NET_OutOfBandPrint( NS_SERVER, svs.authorizeAddress, "getIpAuthorize %i %i.%i.%i.%i %s 0 %s", challenge->challenge, from.ip[0], from.ip[1], from.ip[2], from.ip[3], game, sv_strictAuth->string ); return; } } #endif challenge->pingTime = svs.time; NET_OutOfBandPrint(NS_SERVER, challenge->adr, "challengeResponse %d %d", challenge->challenge, clientChallenge); }