2
0
mirror of https://github.com/rehlds/rehlds.git synced 2025-01-12 14:48:00 +03:00

Added new CVARs for improved handling of decompression failures:

- `sv_net_incoming_decompression_min_failures`: Min failures required to flag a player
- `sv_net_incoming_decompression_max_failures`: max failures allowed before punishment
- `sv_net_incoming_decompression_min_failuretime`: Time window for failure tracking
This commit is contained in:
s1lentq 2024-12-08 23:54:20 +07:00
parent e54adb089c
commit 64c684af4a
3 changed files with 195 additions and 50 deletions

View File

@ -63,9 +63,12 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
<li>sv_rehlds_local_gametime &lt;1|0&gt; // A feature of local gametime which decrease "lags" if you run same map for a long time. Default: 0
<li>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.
<li>sv_usercmd_custom_random_seed // When enabled server will populate an additional random seed independent of the client. Default: 0
<li>sv_net_incoming_decompression <1|0> // When enabled server will decompress of incoming compressed file transfer payloads. Default: 1
<li>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
<li>sv_net_incoming_decompression_max_size <16|65536> // Sets the max allowed size for decompressed file transfer data. Default: 65536 bytes
<li>sv_net_incoming_decompression &lt;1|0&gt; // When enabled server will decompress of incoming compressed file transfer payloads. Default: 1
<li>sv_net_incoming_decompression_max_ratio &lt;0|100&gt; // 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
<li>sv_net_incoming_decompression_max_size &lt;16|65536&gt; // Sets the max allowed size for decompressed file transfer data. Default: 65536 bytes
<li>sv_net_incoming_decompression_min_failures &lt;0|10&gt; // Sets the min number of decompression failures required before a player's connection is flagged for potential punishment. Default: 4
<li>sv_net_incoming_decompression_max_failures &lt;0|10&gt; // Sets the max number of decompression failures allowed within a specified time window before action is taken against the player. Default: 10
<li>sv_net_incoming_decompression_min_failuretime: &lt;0.1|10.0&gt; // Sets the min time in secs within which decompression failures are tracked to determine if the player exceeds the failure thresholds. Default: 0.1
<li>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
<li>sv_tags &lt;comma-delimited string list of tags&gt; // 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: ""
<li>sv_filterban &lt;-1|0|1&gt;// 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

View File

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

View File

@ -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);