Server LuaCsForBarotrauma
DoSProtection.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Diagnostics;
7 
8 namespace Barotrauma
9 {
10  internal sealed class DoSProtection
11  {
15  public readonly ref struct DoSAction
16  {
17  private readonly Client sender;
18  private readonly Action<Client> end;
19 
20  public DoSAction(Client sender, Action<Client> start, Action<Client> end)
21  {
22  this.sender = sender;
23  this.end = end;
24  start(sender);
25  }
26 
27  public void Dispose()
28  {
29  end(sender);
30  }
31  }
32 
33  private sealed class OffenseData
34  {
38  public readonly Stopwatch Stopwatch = new();
39 
43  public int Strikes;
44 
48  public int PacketCount;
49 
53  public void ResetStrikes()
54  {
55  Strikes = 0;
56  PacketCount = 0;
57  }
58 
62  public void ResetTimer() => Stopwatch.Reset();
63  }
64 
65  private readonly Dictionary<Client, OffenseData> clients = new();
66 
67  private float stopwatchResetTimer,
68  strikesResetTimer;
69 
70  private const int StopwatchResetInterval = 1,
71  StrikesResetInterval = 60,
72  StrikeThreshold = 6;
73 
74  private const int MinPacketLimitMultipler = 1;
75 
76  private static int GetMaxPacketLimit(ServerSettings settings)
77  => (int)MathF.Ceiling(
78  settings.MaxPacketAmount *
79  MathF.Max(
80  settings.TickRate / (float)ServerSettings.DefaultTickRate,
81  MinPacketLimitMultipler)); // Prevent the rate limit multiplier from being less than 1.
82 
109  public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor);
110 
130  public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor);
131 
132  private void StartFor(Client client)
133  {
134  clients.TryAdd(client, new OffenseData());
135  clients[client].Stopwatch.Start();
136  }
137 
138  private void EndFor(Client client)
139  {
140  if (GetData(client) is not { } data) { return; }
141 
142  data.PacketCount++;
143  data.Stopwatch.Stop();
144  UpdateOffense(client, data);
145  }
146 
147  // stops the clock but doesn't update offenses
148  private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop();
149 
150  private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start();
151 
152  private void UpdateOffense(Client client, OffenseData data)
153  {
154  if (GameMain.Server?.ServerSettings is not { } settings) { return; }
155 
156  // client is sending too many packets, kick them
157  if (data.PacketCount > GetMaxPacketLimit(settings) && settings.MaxPacketAmount > ServerSettings.PacketLimitMin)
158  {
159  AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
160  clients.Remove(client);
161  return;
162  }
163 
164  // 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
165  if (data.Stopwatch.ElapsedMilliseconds < 100) { return; }
166 
167  data.Strikes++;
168  data.ResetTimer();
169 
170  GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection);
171 
172  // too many strikes, get them out of here
173  if (data.Strikes < StrikeThreshold) { return; }
174 
175  if (settings.EnableDoSProtection)
176  {
177  AttemptKickClient(client, TextManager.Get("DoSProtectionKicked"));
178  }
179 
180  clients.Remove(client);
181 
182  static void AttemptKickClient(Client client, LocalizedString reason)
183  {
184  // ReSharper disable once ConvertToConstant.Local
185  bool doesRateLimitAffectClient =
186 #if DEBUG
187  true; // for testing
188 #else
189  !RateLimiter.IsExempt(client);
190 #endif
191 
192  if (!doesRateLimitAffectClient)
193  {
194  return;
195  }
196 
197  GameMain.Server?.KickClient(client, reason.Value);
198  }
199  }
200 
201  public void Update(float deltaTime)
202  {
203  stopwatchResetTimer += deltaTime;
204  strikesResetTimer += deltaTime;
205 
206  // reset the stopwatch every second
207  if (stopwatchResetTimer > StopwatchResetInterval)
208  {
209  stopwatchResetTimer = 0;
210  foreach (OffenseData data in clients.Values)
211  {
212  data.ResetTimer();
213  }
214  }
215 
216  // reset the strikes every minute
217  if (strikesResetTimer > StrikesResetInterval)
218  {
219  strikesResetTimer = 0;
220  foreach (var (client, data) in clients)
221  {
222  if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings)
223  {
224  if (data.PacketCount > GetMaxPacketLimit(settings) * 0.9f)
225  {
226  GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection);
227  }
228  }
229 
230  data.ResetStrikes();
231  }
232  }
233  }
234 
235  private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null;
236  }
237 }
static void Log(string line, ServerLog.MessageType messageType)
Definition: GameServer.cs:4609
A struct that executes an action when it's created and another one when it's disposed.
DoSAction(Client sender, Action< Client > start, Action< Client > end)