v1.0.13.1 (first post-1.0 patch)
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class DoSProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// A struct that executes an action when it's created and another one when it's disposed.
|
||||
/// </summary>
|
||||
public readonly ref struct DoSAction
|
||||
{
|
||||
private readonly Client sender;
|
||||
private readonly Action<Client> end;
|
||||
|
||||
public DoSAction(Client sender, Action<Client> start, Action<Client> end)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.end = end;
|
||||
start(sender);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
end(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OffenseData
|
||||
{
|
||||
/// <summary>
|
||||
/// Timer that keeps track of how long it takes to process a packet.
|
||||
/// </summary>
|
||||
public readonly Stopwatch Stopwatch = new();
|
||||
|
||||
/// <summary>
|
||||
/// Amount of strikes the client has received for causing the server to slow down.
|
||||
/// </summary>
|
||||
public int Strikes;
|
||||
|
||||
/// <summary>
|
||||
/// How many packets have been sent in the last minute.
|
||||
/// </summary>
|
||||
public int PacketCount;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the strikes and packet count.
|
||||
/// </summary>
|
||||
public void ResetStrikes()
|
||||
{
|
||||
Strikes = 0;
|
||||
PacketCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the timer.
|
||||
/// </summary>
|
||||
public void ResetTimer() => Stopwatch.Reset();
|
||||
}
|
||||
|
||||
private readonly Dictionary<Client, OffenseData> clients = new();
|
||||
|
||||
private float stopwatchResetTimer,
|
||||
strikesResetTimer;
|
||||
|
||||
private const int StopwatchResetInterval = 1,
|
||||
StrikesResetInterval = 60,
|
||||
StrikeThreshold = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server receives a packet to start logging how much time it takes to process.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to start a timer for.</param>
|
||||
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
|
||||
/// <remarks>
|
||||
/// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public void ServerRead(IReadMessage msg, Client c)
|
||||
/// {
|
||||
/// // start the timer
|
||||
/// using var _ = dosProtection.Start(connectedClient);
|
||||
///
|
||||
/// if (condition)
|
||||
/// {
|
||||
/// // the timer will be stopped here.
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// ProcessMessage(msg);
|
||||
/// // the timer will be stopped here.
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor);
|
||||
|
||||
/// <summary>
|
||||
/// Temporary pauses the timer for the client.
|
||||
/// Used when we know a packet is going to slow down the server but we don't want to count it as a strike.
|
||||
/// For example when a client is starting a round.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to pause the timer for.</param>
|
||||
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
|
||||
/// <remarks>
|
||||
/// Calling resume is not required, the timer will be resumed automatically when the using block ends.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// using (dos.Pause(client))
|
||||
/// {
|
||||
/// // do something that will slow down the server
|
||||
/// }
|
||||
/// // the timer will be resumed here
|
||||
/// </code>
|
||||
/// </example>
|
||||
public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor);
|
||||
|
||||
private void StartFor(Client client)
|
||||
{
|
||||
if (!clients.ContainsKey(client))
|
||||
{
|
||||
clients.Add(client, new OffenseData());
|
||||
}
|
||||
|
||||
clients[client].Stopwatch.Start();
|
||||
}
|
||||
|
||||
private void EndFor(Client client)
|
||||
{
|
||||
if (GetData(client) is not { } data) { return; }
|
||||
|
||||
data.PacketCount++;
|
||||
data.Stopwatch.Stop();
|
||||
UpdateOffense(client, data);
|
||||
}
|
||||
|
||||
// stops the clock but doesn't update offenses
|
||||
private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop();
|
||||
|
||||
private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start();
|
||||
|
||||
private void UpdateOffense(Client client, OffenseData data)
|
||||
{
|
||||
if (GameMain.Server?.ServerSettings is not { } settings) { return; }
|
||||
|
||||
// client is sending too many packets, kick them
|
||||
if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin)
|
||||
{
|
||||
AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
|
||||
clients.Remove(client);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong
|
||||
if (data.Stopwatch.ElapsedMilliseconds < 100) { return; }
|
||||
|
||||
data.Strikes++;
|
||||
data.ResetTimer();
|
||||
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection);
|
||||
|
||||
// too many strikes, get them out of here
|
||||
if (data.Strikes < StrikeThreshold) { return; }
|
||||
|
||||
if (settings.EnableDoSProtection)
|
||||
{
|
||||
AttemptKickClient(client, TextManager.Get("DoSProtectionKicked"));
|
||||
}
|
||||
|
||||
clients.Remove(client);
|
||||
|
||||
static void AttemptKickClient(Client client, LocalizedString reason)
|
||||
{
|
||||
// ReSharper disable once ConvertToConstant.Local
|
||||
bool doesRateLimitAffectClient =
|
||||
#if DEBUG
|
||||
true; // for testing
|
||||
#else
|
||||
!RateLimiter.IsExempt(client);
|
||||
#endif
|
||||
|
||||
if (!doesRateLimitAffectClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameMain.Server?.KickClient(client, reason.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
stopwatchResetTimer += deltaTime;
|
||||
strikesResetTimer += deltaTime;
|
||||
|
||||
// reset the stopwatch every second
|
||||
if (stopwatchResetTimer > StopwatchResetInterval)
|
||||
{
|
||||
stopwatchResetTimer = 0;
|
||||
foreach (OffenseData data in clients.Values)
|
||||
{
|
||||
data.ResetTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// reset the strikes every minute
|
||||
if (strikesResetTimer > StrikesResetInterval)
|
||||
{
|
||||
strikesResetTimer = 0;
|
||||
foreach (var (client, data) in clients)
|
||||
{
|
||||
if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings)
|
||||
{
|
||||
if (data.PacketCount > settings.MaxPacketAmount * 0.9f)
|
||||
{
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection);
|
||||
}
|
||||
}
|
||||
|
||||
data.ResetStrikes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum RateLimitAction
|
||||
{
|
||||
Invalid,
|
||||
OnLimitReached,
|
||||
OnLimitDoubled,
|
||||
}
|
||||
|
||||
public enum RateLimitPunishment
|
||||
{
|
||||
None, // just ignore
|
||||
Announce, // announce to the server
|
||||
Kick, // kick the player
|
||||
Ban // ban the player
|
||||
}
|
||||
|
||||
internal sealed class RateLimiter
|
||||
{
|
||||
private sealed record RateLimit(DateTimeOffset Expiry)
|
||||
{
|
||||
public int RequestAmount;
|
||||
}
|
||||
|
||||
private readonly Dictionary<Client, RateLimit> rateLimits = new();
|
||||
private readonly HashSet<Client> expiredRateLimits = new();
|
||||
private readonly Dictionary<Client, DateTimeOffset> recentlyAnnouncedOffenders = new();
|
||||
|
||||
private readonly int maxRequests, expiryInSeconds;
|
||||
|
||||
private readonly ImmutableDictionary<RateLimitAction, RateLimitPunishment> punishments;
|
||||
|
||||
public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules)
|
||||
{
|
||||
this.maxRequests = maxRequests;
|
||||
this.expiryInSeconds = expiryInSeconds;
|
||||
|
||||
punishments = punishmentRules.ToImmutableDictionary(
|
||||
static pair => pair.Action,
|
||||
static pair => pair.Punishment);
|
||||
}
|
||||
|
||||
public bool IsLimitReached(Client client)
|
||||
{
|
||||
#if !DEBUG
|
||||
if (IsExempt(client)) { return false; }
|
||||
#endif
|
||||
expiredRateLimits.Clear();
|
||||
|
||||
foreach (var (c, limit) in rateLimits)
|
||||
{
|
||||
if (limit.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
expiredRateLimits.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Client c in expiredRateLimits)
|
||||
{
|
||||
rateLimits.Remove(c);
|
||||
}
|
||||
|
||||
if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit))
|
||||
{
|
||||
rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds));
|
||||
rateLimits.Add(client, rateLimit);
|
||||
}
|
||||
|
||||
rateLimit.RequestAmount++;
|
||||
|
||||
if (rateLimit.RequestAmount > maxRequests)
|
||||
{
|
||||
ProcessPunishment(client, rateLimit.RequestAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ProcessPunishment(Client client, int requests)
|
||||
{
|
||||
bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true };
|
||||
|
||||
foreach (var (action, punishment) in punishments)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case RateLimitAction.Invalid:
|
||||
continue;
|
||||
case RateLimitAction.OnLimitReached when requests >= maxRequests:
|
||||
case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2:
|
||||
switch (punishment)
|
||||
{
|
||||
case RateLimitPunishment.None:
|
||||
continue;
|
||||
case RateLimitPunishment.Announce:
|
||||
AnnounceOffender(client);
|
||||
break;
|
||||
case RateLimitPunishment.Ban when isDosProtectionEnabled:
|
||||
GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value);
|
||||
break;
|
||||
case RateLimitPunishment.Kick when isDosProtectionEnabled:
|
||||
GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnnounceOffender(Client client)
|
||||
{
|
||||
if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry))
|
||||
{
|
||||
if (expiry > DateTimeOffset.Now) { return; }
|
||||
|
||||
recentlyAnnouncedOffenders.Remove(client);
|
||||
}
|
||||
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection);
|
||||
recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds));
|
||||
}
|
||||
|
||||
public static bool IsExempt(Client client) =>
|
||||
(GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection)
|
||||
|| client.HasPermission(ClientPermissions.SpamImmunity);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user