diff --git a/README.md b/README.md index 9d5f8eb..0da1d35 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,12 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
  • sv_rehlds_local_gametime <1|0> // A feature of local gametime which decrease "lags" if you run same map for a long time. Default: 0
  • sv_use_entity_file // Use custom entity file for a map. Path to an entity file will be "maps/[map name].ent". 0 - use original entities. 1 - use .ent files from maps directory. 2 - use .ent files from maps directory and create new .ent file if not exist.
  • sv_usercmd_custom_random_seed // When enabled server will populate an additional random seed independent of the client. Default: 0 -
  • sv_net_incoming_decompression <1|0> // When enabled server will decompress of incoming compressed file transfer payloads. Default: 1 -
  • sv_net_incoming_decompression_max_ratio <0|100> // Sets the max allowed ratio between compressed and uncompressed data for file transfer. (A ratio close to 90 indicates large uncompressed data with low entropy) Default: 80.0 -
  • sv_net_incoming_decompression_max_size <16|65536> // Sets the max allowed size for decompressed file transfer data. Default: 65536 bytes +
  • sv_net_incoming_decompression <1|0> // When enabled server will decompress of incoming compressed file transfer payloads. Default: 1 +
  • sv_net_incoming_decompression_max_ratio <0|100> // Sets the max allowed ratio between compressed and uncompressed data for file transfer. (A ratio close to 90 indicates large uncompressed data with low entropy) Default: 80.0 +
  • sv_net_incoming_decompression_max_size <16|65536> // Sets the max allowed size for decompressed file transfer data. Default: 65536 bytes +
  • sv_net_incoming_decompression_min_failures <0|10> // Sets the min number of decompression failures required before a player's connection is flagged for potential punishment. Default: 4 +
  • sv_net_incoming_decompression_max_failures <0|10> // Sets the max number of decompression failures allowed within a specified time window before action is taken against the player. Default: 10 +
  • sv_net_incoming_decompression_min_failuretime: <0.1|10.0> // Sets the min time in secs within which decompression failures are tracked to determine if the player exceeds the failure thresholds. Default: 0.1
  • sv_net_incoming_decompression_punish // Time in minutes for which the player will be banned for malformed/abnormal bzip2 fragments (0 - Permanent, use a negative number for a kick). Default: -1
  • sv_tags <comma-delimited string list of tags> // Sets a string defining the "gametags" for this server, this is optional, but if it is set it allows users/scripts to filter in the matchmaking/server-browser interfaces based on the value. Default: ""
  • sv_filterban <-1|0|1>// Set packet filtering by IP mode. -1 - All players will be rejected without any exceptions. 0 - No checks will happen. 1 - All incoming players will be checked if they're IP banned (if they have an IP filter entry), if they are, they will be kicked. Default: 1 diff --git a/rehlds/engine/net.h b/rehlds/engine/net.h index 072ba33..1b8cb39 100644 --- a/rehlds/engine/net.h +++ b/rehlds/engine/net.h @@ -276,6 +276,15 @@ typedef struct flow_s float avgkbytespersec; } flow_t; +const int NET_DECOMPRESS_MAX_TIMES = 10; + +typedef struct frag_decomp_failure_s +{ + float failure_times[NET_DECOMPRESS_MAX_TIMES]; + // Count of abnormal fragment decompressions in a time window + int num_failures; +} frag_decomp_failure_t; + const int FRAGMENT_C2S_MIN_SIZE = 16; const int FRAGMENT_S2C_MIN_SIZE = 256; const int FRAGMENT_S2C_MAX_SIZE = 1024; @@ -422,6 +431,10 @@ typedef struct netchan_s // Incoming and outgoing flow metrics flow_t flow[MAX_FLOWS]; + + // Stats for decompression of incoming fragments + frag_decomp_failure_t frag_decompress[MAX_STREAMS]; + } netchan_t; #ifdef REHLDS_FIXES diff --git a/rehlds/engine/net_chan.cpp b/rehlds/engine/net_chan.cpp index b4b75e9..b95b0a4 100644 --- a/rehlds/engine/net_chan.cpp +++ b/rehlds/engine/net_chan.cpp @@ -40,7 +40,10 @@ cvar_t net_chokeloopback = { "net_chokeloop", "0", 0, 0.0f, nullptr}; cvar_t sv_net_incoming_decompression = { "sv_net_incoming_decompression", "1", 0, 1.0f, nullptr }; cvar_t sv_net_incoming_decompression_max_ratio = { "sv_net_incoming_decompression_max_ratio", "80.0", 0, 80.0f, nullptr }; cvar_t sv_net_incoming_decompression_max_size = { "sv_net_incoming_decompression_max_size", "65536", 0, 65536.0f, nullptr }; -cvar_t sv_net_incoming_decompression_punish = { "sv_net_incoming_decompression_punish", "-1", 0, -1.0f, NULL }; +cvar_t sv_net_incoming_decompression_min_failures = { "sv_net_incoming_decompression_min_failures", "4", 0, 4.0f, nullptr }; +cvar_t sv_net_incoming_decompression_max_failures = { "sv_net_incoming_decompression_max_failures", "10", 0, 10.0f, nullptr }; +cvar_t sv_net_incoming_decompression_min_failuretime = { "sv_net_incoming_decompression_min_failuretime", "0.1", 0, 0.1f, nullptr }; +cvar_t sv_net_incoming_decompression_punish = { "sv_net_incoming_decompression_punish", "-1", 0, -1.0f, nullptr }; cvar_t sv_filetransfercompression = { "sv_filetransfercompression", "1", 0, 0.0f, nullptr}; cvar_t sv_filetransfermaxsize = { "sv_filetransfermaxsize", "10485760", 0, 0.0f, nullptr}; @@ -171,6 +174,10 @@ void Netchan_Clear(netchan_t *chan) chan->frag_startpos[i] = 0; chan->frag_length[i] = 0; chan->incomingready[i] = FALSE; + + for (int j = 0; j < NET_DECOMPRESS_MAX_TIMES; j++) + chan->frag_decompress[i].failure_times[j] = 0; + chan->frag_decompress[i].num_failures = 0; } if (chan->tempbuffer) @@ -1423,6 +1430,94 @@ void Netchan_FlushIncoming(netchan_t *chan, int stream) chan->incomingready[stream] = FALSE; } +void Netchan_DecompressionCvarsBounds() +{ + if (sv_net_incoming_decompression_min_failures.value < 1) + Cvar_SetValue("sv_net_incoming_decompression_min_failures", 1); + + else if (sv_net_incoming_decompression_min_failures.value > NET_DECOMPRESS_MAX_TIMES) + Cvar_SetValue("sv_net_incoming_decompression_min_failures", NET_DECOMPRESS_MAX_TIMES); + + if (sv_net_incoming_decompression_max_failures.value < 1) + Cvar_SetValue("sv_net_incoming_decompression_max_failures", 1); + + else if (sv_net_incoming_decompression_max_failures.value > NET_DECOMPRESS_MAX_TIMES) + Cvar_SetValue("sv_net_incoming_decompression_max_failures", NET_DECOMPRESS_MAX_TIMES); + + if (sv_net_incoming_decompression_max_failures.value < sv_net_incoming_decompression_min_failures.value) + { + int iTemp = sv_net_incoming_decompression_max_failures.value; + Cvar_SetValue("sv_net_incoming_decompression_max_failures", sv_net_incoming_decompression_min_failures.value); + Cvar_SetValue("sv_net_incoming_decompression_min_failures", iTemp); + } + + if (sv_net_incoming_decompression_min_failuretime.value <= 0.0f) + Cvar_SetValue("sv_net_incoming_decompression_min_failuretime", 0.1f); +} + +// Check for an abnormal size ratio between compressed and uncompressed data +qboolean Netchan_ValidateDecompress(netchan_t *chan, int stream, unsigned int compressedSize, unsigned int uncompressedSize) +{ +#ifdef REHLDS_FIXES + int i; + frag_decomp_failure_t *decomp; + + if (sv_net_incoming_decompression_max_ratio.value <= 0) + return TRUE; // validation is disabled + + if (compressedSize >= uncompressedSize) + return TRUE; + + float ratio = ((float)(uncompressedSize - compressedSize) / uncompressedSize) * 100.0f; + if (ratio < sv_net_incoming_decompression_max_ratio.value) + return TRUE; // no low entropy for uncompressed data + + if ((chan->player_slot - 1) != host_client - g_psvs.clients) + return TRUE; + + Netchan_DecompressionCvarsBounds(); + + decomp = &chan->frag_decompress[stream]; + + // check if the client should be rejected based on total failed decompress + if (decomp->num_failures >= sv_net_incoming_decompression_max_failures.value) + { + for (i = 0; i < sv_net_incoming_decompression_max_failures.value - 1; i++) + decomp->failure_times[i] = decomp->failure_times[i + 1]; + + decomp->num_failures = sv_net_incoming_decompression_max_failures.value - 1; + } + + decomp->failure_times[decomp->num_failures++] = realtime; + + // check if the client should be rejected based on recent failed decompress + int recent_failures = 0; + for (i = 0; i < decomp->num_failures; i++) + { + if ((realtime - decomp->failure_times[i]) <= sv_net_incoming_decompression_min_failuretime.value) + recent_failures++; + } + + if (recent_failures >= sv_net_incoming_decompression_min_failures.value) + { + if (chan->player_slot == 0) + Con_DPrintf("Incoming abnormal uncompressed size with ratio %.2f\n", ratio); + else + Con_DPrintf("%s:Incoming abnormal uncompressed size with ratio %.2f from %s\n", NET_AdrToString(chan->remote_address), ratio, host_client->name); + + if (sv_net_incoming_decompression_punish.value >= 0) + { + Con_DPrintf("%s:Banned for malformed/abnormal bzip2 fragments from %s\n", NET_AdrToString(chan->remote_address), host_client->name); + Cbuf_AddText(va("addip %.1f %s\n", sv_net_incoming_decompression_punish.value, NET_BaseAdrToString(chan->remote_address))); + } + + return FALSE; + } +#endif + + return TRUE; +} + qboolean Netchan_CopyNormalFragments(netchan_t *chan) { fragbuf_t *p, *n; @@ -1483,8 +1578,6 @@ qboolean Netchan_CopyNormalFragments(netchan_t *chan) } #endif // REHLDS_FIXES - qboolean success = TRUE; - if (*(uint32 *)net_message.data == MAKEID('B', 'Z', '2', '\0')) { // Determine whether decompression of compressed data is allowed @@ -1493,13 +1586,13 @@ qboolean Netchan_CopyNormalFragments(netchan_t *chan) { if (chan->player_slot == 0) { - Con_DPrintf("Incoming compressed data disallowed from\n"); + Con_DPrintf("Incoming compressed normal fragment disallowed from\n"); return FALSE; } // compressed data is expected only after requesting resource list else if (host_client->m_sendrescount == 0) { - Con_DPrintf("%s:Incoming compressed data disallowed from %s\n", NET_AdrToString(chan->remote_address), host_client->name); + Con_DPrintf("%s:Incoming compressed normal fragment disallowed from %s\n", NET_AdrToString(chan->remote_address), host_client->name); return FALSE; } } @@ -1510,56 +1603,34 @@ qboolean Netchan_CopyNormalFragments(netchan_t *chan) unsigned int compressedSize = net_message.cursize - 4; // Decompress net buffer data - if (success && (BZ2_bzBuffToBuffDecompress(uncompressed, &uncompressedSize, (char *)net_message.data + 4, compressedSize, 1, 0) == BZ_OK)) - { -#ifdef REHLDS_FIXES - // Check for an abnormal size ratio between compressed and uncompressed data - if (sv_net_incoming_decompression_max_ratio.value > 0 && compressedSize < uncompressedSize) - { - float ratio = ((float)(uncompressedSize - compressedSize) / uncompressedSize) * 100.0f; - if (ratio >= sv_net_incoming_decompression_max_ratio.value) - { - if (chan->player_slot == 0) - Con_DPrintf("Incoming abnormal uncompressed size with ratio %.2f\n", ratio); - else - Con_DPrintf("%s:Incoming abnormal uncompressed size with ratio %.2f from %s\n", NET_AdrToString(chan->remote_address), ratio, host_client->name); + qboolean success = TRUE; - success = FALSE; - } - } -#endif - - // Copy uncompressed data back to the net buffer - Q_memcpy(net_message.data, uncompressed, uncompressedSize); - net_message.cursize = uncompressedSize; - } - else + if (BZ2_bzBuffToBuffDecompress(uncompressed, &uncompressedSize, (char *)net_message.data + 4, compressedSize, 1, 0) != BZ_OK) { // malformed data or compressed data exceeding sv_net_incoming_decompression_max_size success = FALSE; } + else if (!Netchan_ValidateDecompress(chan, FRAG_NORMAL_STREAM, compressedSize, uncompressedSize)) + { + success = FALSE; + } - // Drop client if decompression was unsuccessful +#ifdef REHLDS_FIXES if (!success) { - if ((chan->player_slot - 1) == host_client - g_psvs.clients) - { -#ifdef REHLDS_FIXES - if (sv_net_incoming_decompression_punish.value >= 0) - { - Con_DPrintf("%s:Banned for malformed/abnormal bzip2 fragments from %s\n", NET_AdrToString(chan->remote_address), host_client->name); - Cbuf_AddText(va("addip %.1f %s\n", sv_net_incoming_decompression_punish.value, NET_BaseAdrToString(chan->remote_address))); - } + // Drop client if decompression was unsuccessful + SV_DropClient(host_client, FALSE, "Malformed/abnormal compressed data"); + SZ_Clear(&net_message); + return FALSE; + } #endif - SV_DropClient(host_client, FALSE, "Malformed/abnormal compressed data"); - } - - SZ_Clear(&net_message); - } + // Copy uncompressed data back to the net buffer + Q_memcpy(net_message.data, uncompressed, uncompressedSize); + net_message.cursize = uncompressedSize; } - return success; + return TRUE; } qboolean Netchan_CopyFileFragments(netchan_t *chan) @@ -1575,7 +1646,6 @@ qboolean Netchan_CopyFileFragments(netchan_t *chan) qboolean bCompressed; unsigned int uncompressedSize; - if (!chan->incomingready[FRAG_FILE_STREAM]) return FALSE; @@ -1587,6 +1657,19 @@ qboolean Netchan_CopyFileFragments(netchan_t *chan) return FALSE; } +#ifdef REHLDS_FIXES + if (chan->player_slot > 0 && (chan->player_slot - 1) == host_client - g_psvs.clients) + { + // customization already uploaded with request by operator, + // do not accept any other customization + if (host_client->uploaddoneregistering) + { + SV_DropClient(host_client, FALSE, "Too many customization have been uploaded (unrequested customization)"); + return FALSE; + } + } +#endif + bCompressed = FALSE; SZ_Clear(&net_message); MSG_BeginReading(); @@ -1701,10 +1784,53 @@ qboolean Netchan_CopyFileFragments(netchan_t *chan) if (bCompressed) { - unsigned char* uncompressedBuffer = (unsigned char*)Mem_Malloc(uncompressedSize); - Con_DPrintf("Decompressing file %s (%d -> %d)\n", filename, nsize, uncompressedSize); - BZ2_bzBuffToBuffDecompress((char*)uncompressedBuffer, &uncompressedSize, (char*)buffer, nsize, 1, 0); + // Determine whether decompression of compressed data is allowed +#ifdef REHLDS_FIXES + if (!sv_net_incoming_decompression.value) + { + if (chan->player_slot == 0) + { + Con_DPrintf("Incoming compressed file fragment disallowed from\n"); + return FALSE; + } + // compressed data is expected only after requesting resource list + else if (host_client->m_sendrescount == 0) + { + Con_DPrintf("%s:Incoming compressed file fragment disallowed from %s\n", NET_AdrToString(chan->remote_address), host_client->name); + return FALSE; + } + } +#endif + + uncompressedSize = clamp(uncompressedSize, 16u, (unsigned)sv_net_incoming_decompression_max_size.value); // valid range (16 - 65536) bytes + + qboolean success = TRUE; + unsigned char *uncompressedBuffer = (unsigned char *)Mem_Malloc(uncompressedSize); + unsigned int compressedSize = nsize; + + // Decompress net buffer data + if (BZ2_bzBuffToBuffDecompress((char *)uncompressedBuffer, &uncompressedSize, (char *)buffer, compressedSize, 1, 0) != BZ_OK) + { + // malformed data or compressed data exceeding sv_net_incoming_decompression_max_size + success = FALSE; + } + else if (!Netchan_ValidateDecompress(chan, FRAG_NORMAL_STREAM, compressedSize, uncompressedSize)) + { + success = FALSE; + } + Mem_Free(buffer); + + if (!success) + { + // Drop client if decompression was unsuccessful + SV_DropClient(host_client, FALSE, "Malformed/abnormal compressed data"); + SZ_Clear(&net_message); + Mem_Free(uncompressedBuffer); + return FALSE; + } + + Con_DPrintf("Decompressing file %s (%d -> %d)\n", filename, compressedSize, uncompressedSize); pos = uncompressedSize; buffer = uncompressedBuffer; } @@ -1899,6 +2025,9 @@ void Netchan_Init(void) Cvar_RegisterVariable(&sv_net_incoming_decompression); Cvar_RegisterVariable(&sv_net_incoming_decompression_max_ratio); Cvar_RegisterVariable(&sv_net_incoming_decompression_max_size); + Cvar_RegisterVariable(&sv_net_incoming_decompression_min_failures); + Cvar_RegisterVariable(&sv_net_incoming_decompression_max_failures); + Cvar_RegisterVariable(&sv_net_incoming_decompression_min_failuretime); Cvar_RegisterVariable(&sv_net_incoming_decompression_punish); #endif Cvar_RegisterVariable(&sv_filetransfermaxsize);