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;
|
GameInformation gameInfo;
|
||||||
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
|
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
|
||||||
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
||||||
|
readonly PlayerMessageTracker playerMessageTracker;
|
||||||
|
|
||||||
[TranslationReference]
|
[TranslationReference]
|
||||||
static readonly string CustomRules = "custom-rules";
|
static readonly string CustomRules = "custom-rules";
|
||||||
@@ -128,9 +129,6 @@ namespace OpenRA.Server
|
|||||||
[TranslationReference("command")]
|
[TranslationReference("command")]
|
||||||
static readonly string UnknownServerCommand = "unknown-server-command";
|
static readonly string UnknownServerCommand = "unknown-server-command";
|
||||||
|
|
||||||
[TranslationReference("remaining")]
|
|
||||||
static readonly string ChatDisabled = "chat-disabled";
|
|
||||||
|
|
||||||
[TranslationReference("player")]
|
[TranslationReference("player")]
|
||||||
static readonly string LobbyDisconnected = "lobby-disconnected";
|
static readonly string LobbyDisconnected = "lobby-disconnected";
|
||||||
|
|
||||||
@@ -316,6 +314,8 @@ namespace OpenRA.Server
|
|||||||
Map = ModData.MapCache[settings.Map];
|
Map = ModData.MapCache[settings.Map];
|
||||||
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
||||||
|
|
||||||
|
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
|
||||||
|
|
||||||
LobbyInfo = new Session
|
LobbyInfo = new Session
|
||||||
{
|
{
|
||||||
GlobalSettings =
|
GlobalSettings =
|
||||||
@@ -558,8 +558,8 @@ namespace OpenRA.Server
|
|||||||
newConn.Validated = true;
|
newConn.Validated = true;
|
||||||
|
|
||||||
// Disable chat UI to stop the client sending messages that we know we will reject
|
// Disable chat UI to stop the client sending messages that we know we will reject
|
||||||
if (!client.IsAdmin && Settings.JoinChatDelay > 0)
|
if (!client.IsAdmin && Settings.FloodLimitJoinCooldown > 0)
|
||||||
DispatchOrdersToClient(newConn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)Settings.JoinChatDelay }.Serialize());
|
playerMessageTracker.DisableChatUI(newConn, Settings.FloodLimitJoinCooldown);
|
||||||
|
|
||||||
Log.Write("server", $"Client {newConn.PlayerIndex}: Accepted connection from {newConn.EndPoint}.");
|
Log.Write("server", $"Client {newConn.PlayerIndex}: Accepted connection from {newConn.EndPoint}.");
|
||||||
|
|
||||||
@@ -1006,14 +1006,7 @@ namespace OpenRA.Server
|
|||||||
|
|
||||||
case "Chat":
|
case "Chat":
|
||||||
{
|
{
|
||||||
var isAdmin = GetClient(conn)?.IsAdmin ?? false;
|
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
|
||||||
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
|
|
||||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -103,7 +103,16 @@ namespace OpenRA
|
|||||||
public bool EnableLintChecks = true;
|
public bool EnableLintChecks = true;
|
||||||
|
|
||||||
[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
|
[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()
|
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.
|
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.
|
insufficient-enabled-spawnPoints = Unable to start the game until more spawn points are enabled.
|
||||||
malformed-command = Malformed { $command } command
|
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.
|
state-unchanged-ready = Cannot change state when marked as ready.
|
||||||
invalid-faction-selected = Invalid faction selected: { $faction }
|
invalid-faction-selected = Invalid faction selected: { $faction }
|
||||||
supported-factions = Supported values: { $factions }
|
supported-factions = Supported values: { $factions }
|
||||||
@@ -91,6 +86,13 @@ game-started = Game started
|
|||||||
## Server also LobbyUtils
|
## Server also LobbyUtils
|
||||||
bots-disabled = Bots Disabled
|
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
|
## ActorEditLogic
|
||||||
duplicate-actor-id = Duplicate Actor ID
|
duplicate-actor-id = Duplicate Actor ID
|
||||||
enter-actor-id = Enter an Actor ID
|
enter-actor-id = Enter an Actor ID
|
||||||
|
|||||||
Reference in New Issue
Block a user