v1.0.13.1 (first post-1.0 patch)

This commit is contained in:
Regalis11
2023-05-10 15:07:17 +03:00
parent 96fb49ba14
commit ee1db852b1
272 changed files with 5738 additions and 2413 deletions
@@ -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);
}
}