Add anti-flood protection
This commit is contained in:
86
OpenRA.Game/Server/PlayerMessageTracker.cs
Normal file
86
OpenRA.Game/Server/PlayerMessageTracker.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you 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. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA.Server
|
||||
{
|
||||
class PlayerMessageTracker
|
||||
{
|
||||
[TranslationReference("remaining")]
|
||||
static readonly string ChatDisabled = "chat-disabled";
|
||||
|
||||
readonly Dictionary<int, List<long>> messageTracker = new Dictionary<int, List<long>>();
|
||||
readonly Server server;
|
||||
readonly Action<Connection, int, int, byte[]> dispatchOrdersToClient;
|
||||
readonly Action<Connection, string, Dictionary<string, object>> sendLocalizedMessageTo;
|
||||
|
||||
public PlayerMessageTracker(Server server, Action<Connection, int, int, byte[]> dispatchOrdersToClient, Action<Connection, string, Dictionary<string, object>> sendLocalizedMessageTo)
|
||||
{
|
||||
this.server = server;
|
||||
this.dispatchOrdersToClient = dispatchOrdersToClient;
|
||||
this.sendLocalizedMessageTo = sendLocalizedMessageTo;
|
||||
}
|
||||
|
||||
public void DisableChatUI(Connection conn, int time)
|
||||
{
|
||||
dispatchOrdersToClient(conn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)time }.Serialize());
|
||||
}
|
||||
|
||||
public bool IsPlayerAtFloodLimit(Connection conn)
|
||||
{
|
||||
if (!messageTracker.ContainsKey(conn.PlayerIndex))
|
||||
messageTracker.Add(conn.PlayerIndex, new List<long>());
|
||||
|
||||
var isAdmin = server.GetClient(conn)?.IsAdmin ?? false;
|
||||
var settings = server.Settings;
|
||||
var time = conn.ConnectionTimer.ElapsedMilliseconds;
|
||||
var tracker = messageTracker[conn.PlayerIndex];
|
||||
tracker.RemoveAll(t => t + settings.FloodLimitInterval < time);
|
||||
|
||||
long CalculateRemaining(long cooldown)
|
||||
{
|
||||
return (cooldown - time + 999) / 1000;
|
||||
}
|
||||
|
||||
// Block messages until join cooldown times out
|
||||
if (!isAdmin && time < settings.FloodLimitJoinCooldown)
|
||||
{
|
||||
var remaining = CalculateRemaining(settings.FloodLimitJoinCooldown);
|
||||
sendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Block messages if above flood limit
|
||||
if (tracker.Count >= settings.FloodLimitMessageCount)
|
||||
{
|
||||
var remaining = CalculateRemaining(tracker[0] + settings.FloodLimitInterval);
|
||||
sendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining));
|
||||
return true;
|
||||
}
|
||||
|
||||
tracker.Add(time);
|
||||
|
||||
// Disable chat when player has reached the flood limit
|
||||
if (tracker.Count >= settings.FloodLimitMessageCount)
|
||||
{
|
||||
var cooldownDelta = Math.Max(0, settings.FloodLimitCooldown - settings.FloodLimitInterval);
|
||||
for (var i = 0; i < tracker.Count; i++)
|
||||
tracker[i] = time + cooldownDelta;
|
||||
|
||||
DisableChatUI(conn, settings.FloodLimitCooldown);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ namespace OpenRA.Server
|
||||
GameInformation gameInfo;
|
||||
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
|
||||
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
||||
readonly PlayerMessageTracker playerMessageTracker;
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string CustomRules = "custom-rules";
|
||||
@@ -128,9 +129,6 @@ namespace OpenRA.Server
|
||||
[TranslationReference("command")]
|
||||
static readonly string UnknownServerCommand = "unknown-server-command";
|
||||
|
||||
[TranslationReference("remaining")]
|
||||
static readonly string ChatDisabled = "chat-disabled";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string LobbyDisconnected = "lobby-disconnected";
|
||||
|
||||
@@ -316,6 +314,8 @@ namespace OpenRA.Server
|
||||
Map = ModData.MapCache[settings.Map];
|
||||
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
||||
|
||||
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
|
||||
|
||||
LobbyInfo = new Session
|
||||
{
|
||||
GlobalSettings =
|
||||
@@ -558,8 +558,8 @@ namespace OpenRA.Server
|
||||
newConn.Validated = true;
|
||||
|
||||
// Disable chat UI to stop the client sending messages that we know we will reject
|
||||
if (!client.IsAdmin && Settings.JoinChatDelay > 0)
|
||||
DispatchOrdersToClient(newConn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)Settings.JoinChatDelay }.Serialize());
|
||||
if (!client.IsAdmin && Settings.FloodLimitJoinCooldown > 0)
|
||||
playerMessageTracker.DisableChatUI(newConn, Settings.FloodLimitJoinCooldown);
|
||||
|
||||
Log.Write("server", $"Client {newConn.PlayerIndex}: Accepted connection from {newConn.EndPoint}.");
|
||||
|
||||
@@ -1006,14 +1006,7 @@ namespace OpenRA.Server
|
||||
|
||||
case "Chat":
|
||||
{
|
||||
var isAdmin = GetClient(conn)?.IsAdmin ?? false;
|
||||
var connected = conn.ConnectionTimer.ElapsedMilliseconds;
|
||||
if (!isAdmin && connected < Settings.JoinChatDelay)
|
||||
{
|
||||
var remaining = (Settings.JoinChatDelay - connected + 999) / 1000;
|
||||
SendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining));
|
||||
}
|
||||
else
|
||||
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
|
||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||
|
||||
break;
|
||||
|
||||
@@ -103,7 +103,16 @@ namespace OpenRA
|
||||
public bool EnableLintChecks = true;
|
||||
|
||||
[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
|
||||
public int JoinChatDelay = 5000;
|
||||
public int FloodLimitJoinCooldown = 5000;
|
||||
|
||||
[Desc("Amount of miliseconds player chat messages are tracked for.")]
|
||||
public int FloodLimitInterval = 5000;
|
||||
|
||||
[Desc("Amount of chat messages per FloodLimitInterval a players can send before flood is detected.")]
|
||||
public int FloodLimitMessageCount = 5;
|
||||
|
||||
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
|
||||
public int FloodLimitCooldown = 15000;
|
||||
|
||||
public ServerSettings Clone()
|
||||
{
|
||||
|
||||
@@ -8,11 +8,6 @@ no-start-until-required-slots-full = Unable to start the game until required slo
|
||||
no-start-without-players = Unable to start the game with no players.
|
||||
insufficient-enabled-spawnPoints = Unable to start the game until more spawn points are enabled.
|
||||
malformed-command = Malformed { $command } command
|
||||
chat-disabled =
|
||||
{ $remaining ->
|
||||
[one] Chat is disabled. Please try again in { $remaining } second.
|
||||
*[other] Chat is disabled. Please try again in { $remaining } seconds.
|
||||
}
|
||||
state-unchanged-ready = Cannot change state when marked as ready.
|
||||
invalid-faction-selected = Invalid faction selected: { $faction }
|
||||
supported-factions = Supported values: { $factions }
|
||||
@@ -91,6 +86,13 @@ game-started = Game started
|
||||
## Server also LobbyUtils
|
||||
bots-disabled = Bots Disabled
|
||||
|
||||
## PlayerMessageTracker
|
||||
chat-disabled =
|
||||
{ $remaining ->
|
||||
[one] Chat is disabled. Please try again in { $remaining } second.
|
||||
*[other] Chat is disabled. Please try again in { $remaining } seconds.
|
||||
}
|
||||
|
||||
## ActorEditLogic
|
||||
duplicate-actor-id = Duplicate Actor ID
|
||||
enter-actor-id = Enter an Actor ID
|
||||
|
||||
Reference in New Issue
Block a user