mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-20 01:46:07 +00:00
Remove some acs
This commit is contained in:
1 parent
40388f319a
commit
4e4f87a0a7
23 files changed
+127
-722
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -35,18 +35,6 @@ public enum CheatCategory
|
||||
/// <summary>A packet was sent with more items than possible in the game.</summary>
|
||||
ItemLimits,
|
||||
|
||||
/// <summary>A kill that the killer's own cooldown cannot explain, like a second victim inside the window or another hit on a dead one.</summary>
|
||||
Murder,
|
||||
|
||||
/// <summary>A sabotage the current game state cannot produce, like switching systems too fast or sabotaging during a meeting.</summary>
|
||||
Sabotage,
|
||||
|
||||
/// <summary>A meeting or a report that this round cannot produce yet.</summary>
|
||||
Meeting,
|
||||
|
||||
/// <summary>A vote cast outside a meeting, or by a player who is already dead.</summary>
|
||||
Voting,
|
||||
|
||||
/// <summary>A client sent more messages in a window than the room can legitimately produce.</summary>
|
||||
RateLimit,
|
||||
|
||||
|
||||
@@ -28,16 +28,14 @@ namespace Empostor.Api.Config
|
||||
|
||||
public bool EnableTargetChecks { get; set; } = true;
|
||||
|
||||
public bool EnableMurderChecks { get; set; } = true;
|
||||
|
||||
public bool EnableSabotageChecks { get; set; } = true;
|
||||
|
||||
public bool EnableMeetingChecks { get; set; } = true;
|
||||
|
||||
public bool EnableVotingChecks { get; set; } = true;
|
||||
|
||||
public bool EnableRateLimits { get; set; } = true;
|
||||
|
||||
public int RpcRateLimitPerSecond { get; set; } = 20;
|
||||
|
||||
public int TaskRateLimitCount { get; set; } = 3;
|
||||
|
||||
public double TaskRateLimitWindowSeconds { get; set; } = 2;
|
||||
|
||||
public bool ForbidProtocolExtensions { get; set; } = true;
|
||||
|
||||
public bool EnablePacketSizeChecks { get; set; } = true;
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Empostor.Api.Config;
|
||||
using Empostor.Api.Innersloth.GameOptions;
|
||||
|
||||
namespace Empostor.Server.Net.Anticheat
|
||||
{
|
||||
internal sealed class AntiCheatState
|
||||
{
|
||||
private const int TimelineCount = 256;
|
||||
private const int UnknownTimelineIndex = TimelineCount - 1;
|
||||
private readonly PlayerTimeline[] _timelines = new PlayerTimeline[TimelineCount];
|
||||
private readonly IGameOptions _options;
|
||||
private double _roundStartedAt = -1;
|
||||
private double _meetingOpenedAt = -1;
|
||||
private bool _meetingActive;
|
||||
|
||||
public AntiCheatState(IGameOptions options, AntiCheatConfig config)
|
||||
{
|
||||
_options = options;
|
||||
Config = config;
|
||||
|
||||
for (var i = 0; i < _timelines.Length; i++)
|
||||
{
|
||||
_timelines[i] = new PlayerTimeline();
|
||||
}
|
||||
}
|
||||
|
||||
public AntiCheatConfig Config { get; }
|
||||
|
||||
public static double Now => (double)Stopwatch.GetTimestamp() / Stopwatch.Frequency;
|
||||
|
||||
public bool ShipLoaded => _roundStartedAt >= 0;
|
||||
|
||||
public bool ShipGraceOver
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_roundStartedAt < 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var cooldown = _options is NormalGameOptions normal ? normal.EmergencyCooldown : 0;
|
||||
return cooldown <= 0 || Now - _roundStartedAt >= cooldown / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public bool InMeeting => _meetingActive;
|
||||
|
||||
public double SecondsSinceRoundStarted => _roundStartedAt < 0 ? -1 : Now - _roundStartedAt;
|
||||
|
||||
public bool MeetingGuardArmed =>
|
||||
_meetingActive && _meetingOpenedAt >= 0 && Now - _meetingOpenedAt >= 1;
|
||||
|
||||
public PlayerTimeline For(byte playerId)
|
||||
{
|
||||
return playerId < TimelineCount ? _timelines[playerId] : _timelines[UnknownTimelineIndex];
|
||||
}
|
||||
|
||||
public void NoteRoundStarted()
|
||||
{
|
||||
_roundStartedAt = Now;
|
||||
_meetingActive = false;
|
||||
_meetingOpenedAt = -1;
|
||||
|
||||
foreach (var timeline in _timelines)
|
||||
{
|
||||
timeline.ResetRound();
|
||||
}
|
||||
}
|
||||
|
||||
public void NoteRoundEnded()
|
||||
{
|
||||
_roundStartedAt = -1;
|
||||
_meetingActive = false;
|
||||
_meetingOpenedAt = -1;
|
||||
}
|
||||
|
||||
public void NoteMeetingOpened()
|
||||
{
|
||||
_meetingActive = true;
|
||||
_meetingOpenedAt = Now;
|
||||
}
|
||||
|
||||
public void NoteMeetingClosed()
|
||||
{
|
||||
_meetingActive = false;
|
||||
_meetingOpenedAt = -1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
namespace Empostor.Server.Net.Anticheat
|
||||
{
|
||||
internal sealed class PlayerTimeline
|
||||
{
|
||||
/// <summary>Gets or sets the moment the player's control was spawned.</summary>
|
||||
public double SpawnedAt { get; set; } = -1;
|
||||
|
||||
/// <summary>Gets or sets the moment of the last completed kill.</summary>
|
||||
public double LastKillAt { get; set; } = -1;
|
||||
|
||||
/// <summary>Gets or sets the victim of the last completed kill.</summary>
|
||||
public byte LastKillTarget { get; set; } = byte.MaxValue;
|
||||
|
||||
/// <summary>Gets or sets how many times in a row this player killed an already dead victim.</summary>
|
||||
public int DeadTargetKills { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the moment of the last sabotage this player triggered.</summary>
|
||||
public double LastSabotageAt { get; set; } = -1;
|
||||
|
||||
/// <summary>Gets or sets the system of the last sabotage this player triggered.</summary>
|
||||
public int LastSabotageSystem { get; set; } = -1;
|
||||
|
||||
private RateLimitWindow _rpcWindow = new();
|
||||
|
||||
private RateLimitWindow _taskWindow = new();
|
||||
|
||||
public bool CountRpc(double now, int maxPerSecond)
|
||||
{
|
||||
return _rpcWindow.Hit(now, maxPerSecond, 1);
|
||||
}
|
||||
|
||||
public bool CountTask(double now, int maxCount, double windowSeconds)
|
||||
{
|
||||
return _taskWindow.Hit(now, maxCount, windowSeconds);
|
||||
}
|
||||
|
||||
public void NoteKill(byte victimId, double now)
|
||||
{
|
||||
LastKillAt = now;
|
||||
LastKillTarget = victimId;
|
||||
}
|
||||
|
||||
public void ResetRound()
|
||||
{
|
||||
SpawnedAt = -1;
|
||||
LastKillAt = -1;
|
||||
LastKillTarget = byte.MaxValue;
|
||||
DeadTargetKills = 0;
|
||||
LastSabotageAt = -1;
|
||||
LastSabotageSystem = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace Empostor.Server.Net.Anticheat
|
||||
{
|
||||
internal struct RateLimitWindow
|
||||
{
|
||||
private const int WindowSize = 64;
|
||||
|
||||
private readonly double[] _stamps;
|
||||
|
||||
private int _next;
|
||||
|
||||
public RateLimitWindow()
|
||||
{
|
||||
_stamps = new double[WindowSize];
|
||||
}
|
||||
|
||||
public bool Hit(double now, int maxCount, double windowSeconds)
|
||||
{
|
||||
if (maxCount <= 0 || windowSeconds <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var used = 0;
|
||||
for (var i = 0; i < WindowSize; i++)
|
||||
{
|
||||
if (_stamps[i] > 0 && now - _stamps[i] < windowSeconds && ++used >= maxCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_stamps[_next] = now;
|
||||
_next = (_next + 1) % WindowSize;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,6 @@ namespace Empostor.Server.Net
|
||||
CheatCategory.Target => _antiCheatConfig.EnableTargetChecks,
|
||||
CheatCategory.PacketSize => _antiCheatConfig.EnablePacketSizeChecks,
|
||||
CheatCategory.ItemLimits => _antiCheatConfig.EnableItemLimitChecks,
|
||||
CheatCategory.Murder => _antiCheatConfig.EnableMurderChecks,
|
||||
CheatCategory.Sabotage => _antiCheatConfig.EnableSabotageChecks,
|
||||
CheatCategory.Meeting => _antiCheatConfig.EnableMeetingChecks,
|
||||
CheatCategory.Voting => _antiCheatConfig.EnableVotingChecks,
|
||||
CheatCategory.RateLimit => _antiCheatConfig.EnableRateLimits,
|
||||
CheatCategory.Other => true,
|
||||
_ => LogUnknownCategory(category),
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Empostor.Api;
|
||||
using Empostor.Api.Innersloth.GameOptions;
|
||||
using Empostor.Api.Net;
|
||||
using Empostor.Api.Net.Inner;
|
||||
using Empostor.Api.Net.Inner.Objects;
|
||||
using Empostor.Server.Net.Anticheat;
|
||||
using Empostor.Server.Net.Inner.Objects;
|
||||
using Empostor.Server.Net.State;
|
||||
|
||||
namespace Empostor.Server.Net.Inner
|
||||
{
|
||||
internal abstract partial class InnerNetObject
|
||||
{
|
||||
private const int RpcRateLimitPerSecond = 20;
|
||||
|
||||
private const int TaskRateLimitCount = 3;
|
||||
|
||||
private const double TaskRateLimitWindowSeconds = 2;
|
||||
|
||||
protected async ValueTask<bool> ValidateOwnership(CheatContext context, IClientPlayer sender)
|
||||
{
|
||||
if (!sender.IsOwner(this))
|
||||
@@ -144,452 +132,6 @@ namespace Empostor.Server.Net.Inner
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateMurder(CheatContext context, IClientPlayer sender, byte killerPlayerId, IInnerPlayerControl? target)
|
||||
{
|
||||
if (!Game.AntiCheat.Config.EnableMurderChecks || target == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var timeline = Game.AntiCheat.For(killerPlayerId);
|
||||
|
||||
if (target.PlayerInfo is { IsDead: true })
|
||||
{
|
||||
timeline.DeadTargetKills++;
|
||||
|
||||
if (timeline.DeadTargetKills >= 6 &&
|
||||
await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Murder,
|
||||
$"Client murdered the already dead player {target.PlayerId} {timeline.DeadTargetKills} times"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
timeline.DeadTargetKills = 0;
|
||||
}
|
||||
|
||||
if (Game.Options is not NormalGameOptions options ||
|
||||
Game.Options.GameMode is not (GameModes.Normal or GameModes.NormalFools))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var now = AntiCheatState.Now;
|
||||
var window = options.KillCooldown / 2;
|
||||
|
||||
if (window > 0 &&
|
||||
timeline.LastKillAt >= 0 &&
|
||||
timeline.LastKillTarget != target.PlayerId &&
|
||||
now - timeline.LastKillAt < window)
|
||||
{
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Murder,
|
||||
$"Killed {target.PlayerId} only {now - timeline.LastKillAt:0.###}s after killing {timeline.LastKillTarget}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateMeetingTiming(CheatContext context, IClientPlayer sender)
|
||||
{
|
||||
if (Game.AntiCheat.InMeeting &&
|
||||
await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Meeting,
|
||||
"Client sent a meeting while one is already in progress"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Game.Options.GameMode is GameModes.HideNSeek &&
|
||||
await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Meeting,
|
||||
"Client sent a meeting during Hide and Seek"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Game.AntiCheat.ShipLoaded)
|
||||
{
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Meeting,
|
||||
"Client sent a meeting before the map of this round was spawned");
|
||||
}
|
||||
|
||||
if (Game.AntiCheat.SecondsSinceRoundStarted >= 0 &&
|
||||
Game.AntiCheat.SecondsSinceRoundStarted < 10)
|
||||
{
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Meeting,
|
||||
$"Client sent a meeting {Game.AntiCheat.SecondsSinceRoundStarted:0.###}s into the round (grace 10s)");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateUpdateSystem(CheatContext context, IClientPlayer sender, IMessageReader reader)
|
||||
{
|
||||
if (!Game.AntiCheat.Config.EnableSabotageChecks)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var position = reader.Position;
|
||||
|
||||
try
|
||||
{
|
||||
if (!TryReadUpdateSystem(reader, out var systemType, out var playerControl, out var isSabotageRoute, out var state))
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.ProtocolExtension, "Client sent a malformed UpdateSystem RPC"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sender.IsHost && !playerControl.IsOwnedBy(sender))
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Ownership, $"Client sent {nameof(RpcCalls.UpdateSystem)} for a {nameof(InnerPlayerControl)} it does not own"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(playerControl, sender);
|
||||
|
||||
if (!Game.AntiCheat.ShipGraceOver)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!await ValidateSabotageInHideNSeek(context, sender, systemType, state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateSystemAmount(context, sender, systemType, state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateSabotageInMeeting(context, sender, systemType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateRapidSabotage(context, sender, actorId, systemType, state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var actorIsImpostor = ResolveActorPlayerInfo(playerControl, sender)?.IsImpostor ?? false;
|
||||
return await ValidateSabotageRole(context, sender, actorIsImpostor, systemType, state);
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.Seek(position);
|
||||
}
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateSabotageInHideNSeek(CheatContext context, IClientPlayer sender, SystemTypes system, byte state)
|
||||
{
|
||||
var startingSabotage = (state & 0x80) != 0 || system == SystemTypes.MushroomMixupSabotage;
|
||||
if (Game.Options.GameMode is not GameModes.HideNSeek || !startingSabotage)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Sabotage,
|
||||
$"Client started a sabotage of {system} during Hide and Seek"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateSystemAmount(CheatContext context, IClientPlayer sender, SystemTypes system, byte amount)
|
||||
{
|
||||
if (system != SystemTypes.Electrical || (amount & 0x80) != 0 || amount <= 4)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.ProtocolExtension,
|
||||
$"Client sent an out of range state {amount} for {system}"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateSabotageInMeeting(CheatContext context, IClientPlayer sender, SystemTypes system)
|
||||
{
|
||||
if (!Game.AntiCheat.MeetingGuardArmed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.Sabotage,
|
||||
$"Client sabotaged {system} while a meeting is in progress"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateRapidSabotage(CheatContext context, IClientPlayer sender, byte playerId, SystemTypes system, byte state)
|
||||
{
|
||||
if ((int)system == 16 || (state & 0x80) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var timeline = Game.AntiCheat.For(playerId);
|
||||
var now = AntiCheatState.Now;
|
||||
|
||||
if (timeline.LastSabotageAt >= 0 &&
|
||||
now - timeline.LastSabotageAt < 3 &&
|
||||
timeline.LastSabotageSystem != (int)system)
|
||||
{
|
||||
var reason = $"Sabotaged {system} {now - timeline.LastSabotageAt:0.###}s after {timeline.LastSabotageSystem}";
|
||||
timeline.LastSabotageAt = now;
|
||||
timeline.LastSabotageSystem = (int)system;
|
||||
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Sabotage, reason))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
timeline.LastSabotageAt = now;
|
||||
timeline.LastSabotageSystem = (int)system;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateSabotageRole(CheatContext context, IClientPlayer sender, bool actorIsImpostor, SystemTypes system, byte state)
|
||||
{
|
||||
if (actorIsImpostor)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var startingSabotage = (state & 0x80) != 0;
|
||||
if (!startingSabotage && system != SystemTypes.MushroomMixupSabotage)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var reason = startingSabotage
|
||||
? $"Non-impostor started a sabotage of {system} (state 0x{state:X2})"
|
||||
: $"Non-impostor triggered {system}";
|
||||
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Sabotage, reason))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
protected async ValueTask<bool> ValidateVoteCast(CheatContext context, IClientPlayer sender)
|
||||
{
|
||||
if (!Game.AntiCheat.Config.EnableVotingChecks)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Game.AntiCheat.InMeeting &&
|
||||
await sender.Client.ReportCheatAsync(context, CheatCategory.Voting, "Client cast a vote while no meeting is in progress"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sender.Character?.PlayerInfo is { IsDead: true } &&
|
||||
await sender.Client.ReportCheatAsync(context, CheatCategory.Voting, "Client cast a vote while being dead"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateVoterCount(CheatContext context, IClientPlayer sender, IMessageReader reader)
|
||||
{
|
||||
var position = reader.Position;
|
||||
int voters;
|
||||
|
||||
try
|
||||
{
|
||||
voters = reader.ReadPackedInt32();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.Seek(position);
|
||||
}
|
||||
|
||||
var limit = Math.Max(Game.Options.MaxPlayers, Game.PlayerCount);
|
||||
if (voters >= 0 && voters <= limit && voters <= reader.Length - position)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.ProtocolExtension,
|
||||
$"Client sent a VotingComplete with {voters} voters (max {limit})");
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateTaskCompletion(CheatContext context, IClientPlayer sender, byte playerId)
|
||||
{
|
||||
if (!Game.AntiCheat.Config.EnableRateLimits)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var config = Game.AntiCheat.Config;
|
||||
if (!Game.AntiCheat.For(playerId).CountTask(AntiCheatState.Now, TaskRateLimitCount, TaskRateLimitWindowSeconds))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.RateLimit,
|
||||
$"Client completed more than {TaskRateLimitCount} tasks in {TaskRateLimitWindowSeconds}s");
|
||||
}
|
||||
|
||||
internal static async ValueTask<bool> ValidateRpcRate(Game game, CheatContext context, IClientPlayer sender, InnerNetObject obj, bool toPlayer)
|
||||
{
|
||||
if (!game.AntiCheat.Config.EnableRateLimits)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var ownerId = obj.OwnerId >= 0 ? obj.OwnerId : sender.Client.Id;
|
||||
|
||||
if (!toPlayer && ownerId != sender.Client.Id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var playerId = game.GetClientPlayer(ownerId)?.Character?.PlayerId;
|
||||
if (playerId == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!game.AntiCheat.For(playerId.Value).CountRpc(AntiCheatState.Now, RpcRateLimitPerSecond))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await sender.Client.ReportCheatAsync(
|
||||
context,
|
||||
CheatCategory.RateLimit,
|
||||
$"Client sent more than {RpcRateLimitPerSecond} RPCs in one second");
|
||||
}
|
||||
|
||||
internal static async ValueTask<bool> ValidateGameDataTag(CheatContext context, IClientPlayer sender, byte tag)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.ProtocolExtension, $"Client sent an unknown game data tag {tag}"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReadUpdateSystem(
|
||||
IMessageReader reader,
|
||||
out SystemTypes systemType,
|
||||
out InnerPlayerControl playerControl,
|
||||
out bool isSabotageRoute,
|
||||
out byte state)
|
||||
{
|
||||
systemType = default;
|
||||
playerControl = null!;
|
||||
isSabotageRoute = false;
|
||||
state = 0;
|
||||
|
||||
if (reader.Position >= reader.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
systemType = (SystemTypes)reader.ReadByte();
|
||||
|
||||
var control = reader.ReadNetObject<InnerPlayerControl>(Game);
|
||||
if (control == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
playerControl = control;
|
||||
|
||||
if (reader.Position >= reader.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
state = reader.ReadByte();
|
||||
|
||||
if (systemType == SystemTypes.Sabotage)
|
||||
{
|
||||
isSabotageRoute = true;
|
||||
systemType = (SystemTypes)state;
|
||||
state = 0x80;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private byte ResolveActorId(InnerPlayerControl control, IClientPlayer sender)
|
||||
{
|
||||
if (control.OwnerId >= 0)
|
||||
{
|
||||
var owner = Game.GetClientPlayer(control.OwnerId);
|
||||
if (owner?.Character != null)
|
||||
{
|
||||
return owner.Character.PlayerId;
|
||||
}
|
||||
}
|
||||
|
||||
return sender.Character?.PlayerId ?? byte.MaxValue;
|
||||
}
|
||||
|
||||
private InnerPlayerInfo? ResolveActorPlayerInfo(InnerPlayerControl control, IClientPlayer sender)
|
||||
{
|
||||
if (control.PlayerInfo != null)
|
||||
{
|
||||
return control.PlayerInfo;
|
||||
}
|
||||
|
||||
var owner = control.OwnerId >= 0 ? Game.GetClientPlayer(control.OwnerId) : null;
|
||||
return (InnerPlayerInfo?)(owner?.Character?.PlayerInfo ?? sender.Character?.PlayerInfo);
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> UnregisteredCall(CheatContext context, IClientPlayer sender)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.ProtocolExtension, "Client sent unregistered call"))
|
||||
|
||||
@@ -140,7 +140,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
|
||||
Rpc22Close.Deserialize(reader);
|
||||
_judgeOverrules.Clear();
|
||||
Game.AntiCheat.NoteMeetingClosed();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -151,11 +150,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateVoterCount(call, sender, reader))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sender.Client.GameVersion >= JudgeMinVersion)
|
||||
{
|
||||
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie, out var wasOverruled, out var overrideId);
|
||||
@@ -253,11 +247,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
}
|
||||
}
|
||||
|
||||
if (!await ValidateVoteCast(RpcCalls.CastVote, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sender.IsHost)
|
||||
{
|
||||
var playerVoteArea = _playerStates.Single(x => x.TargetPlayer.PlayerId == playerId);
|
||||
|
||||
@@ -13,7 +13,6 @@ using Empostor.Api.Net.Inner.Objects;
|
||||
using Empostor.Api.Net.Messages.Rpcs;
|
||||
using Empostor.Api.Utils;
|
||||
using Empostor.Server.Events.Player;
|
||||
using Empostor.Server.Net.Anticheat;
|
||||
using Empostor.Server.Net.Inner.Objects.Components;
|
||||
using Empostor.Server.Net.State;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -256,14 +255,10 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateMeetingTiming(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Rpc11ReportDeadBody.Deserialize(reader, out var targetId);
|
||||
break;
|
||||
}
|
||||
|
||||
case RpcCalls.MurderPlayer:
|
||||
{
|
||||
if (!await ValidateHost(call, sender) ||
|
||||
@@ -296,11 +291,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await ValidateMeetingTiming(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Rpc14StartMeeting.Deserialize(reader, out var targetId);
|
||||
await HandleStartMeeting(targetId);
|
||||
break;
|
||||
@@ -616,7 +606,14 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
|
||||
private async ValueTask HandleCompleteTask(ClientPlayer sender, uint taskId)
|
||||
{
|
||||
if (!await ValidateTaskCompletion(RpcCalls.CompleteTask, sender, PlayerId))
|
||||
// Rate limit: counted per player id, because the host also completes
|
||||
// tasks on behalf of other players.
|
||||
if (Game.AntiCheatConfig.EnableRateLimits
|
||||
&& Game.CountTask(PlayerId)
|
||||
&& await sender.Client.ReportCheatAsync(
|
||||
RpcCalls.CompleteTask,
|
||||
CheatCategory.RateLimit,
|
||||
$"Client completed more than {Game.AntiCheatConfig.TaskRateLimitCount} tasks in {Game.AntiCheatConfig.TaskRateLimitWindowSeconds}s"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1060,11 +1057,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
IsMurdering = target;
|
||||
}
|
||||
|
||||
if (!await ValidateMurder(RpcCalls.CheckMurder, sender, PlayerId, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_game.IsHostAuthoritive)
|
||||
{
|
||||
return true;
|
||||
@@ -1094,11 +1086,6 @@ namespace Empostor.Server.Net.Inner.Objects
|
||||
{
|
||||
target.ProtectedOn = null;
|
||||
await ForceMurderPlayerAsync(target, evt.Result);
|
||||
|
||||
if (!evt.Result.IsFailed())
|
||||
{
|
||||
Game.AntiCheat.For(PlayerId).NoteKill(target.PlayerId, AntiCheatState.Now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,12 +89,13 @@ namespace Empostor.Server.Net.Inner.Objects.ShipStatus
|
||||
|
||||
case RpcCalls.UpdateSystem:
|
||||
{
|
||||
if (!await ValidateCmd(call, sender, target) ||
|
||||
!await ValidateUpdateSystem(call, sender, reader))
|
||||
if (!await ValidateCmd(call, sender, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: properly deserialize this RPC
|
||||
// Rpc35UpdateSystem.Deserialize(reader, Game, out var systemType, out var playerControl, out var sequenceId, out var state, out var ventId);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -9,7 +9,6 @@ using Empostor.Api.Unity;
|
||||
using Empostor.Server.Events.Game.Player;
|
||||
using Empostor.Server.Events.Meeting;
|
||||
using Empostor.Server.Events.Player;
|
||||
using Empostor.Server.Net.Anticheat;
|
||||
using Empostor.Server.Net.Inner;
|
||||
using Empostor.Server.Net.Inner.Objects;
|
||||
using Empostor.Server.Net.Inner.Objects.Components;
|
||||
@@ -174,7 +173,19 @@ namespace Empostor.Server.Net.State
|
||||
var netId = reader.ReadPackedUInt32();
|
||||
if (_allObjects.TryGetValue(netId, out var obj))
|
||||
{
|
||||
if (!await InnerNetObject.ValidateRpcRate(this, new CheatContext(nameof(GameDataTag.RpcFlag)), sender, obj, toPlayer))
|
||||
|
||||
var ownerId = obj.OwnerId >= 0 ? obj.OwnerId : sender.Client.Id;
|
||||
var limitedPlayerId = toPlayer || ownerId == sender.Client.Id
|
||||
? GetClientPlayer(ownerId)?.Character?.PlayerId
|
||||
: null;
|
||||
|
||||
if (AntiCheatConfig.EnableRateLimits
|
||||
&& limitedPlayerId != null
|
||||
&& CountRpc(limitedPlayerId.Value)
|
||||
&& await sender.Client.ReportCheatAsync(
|
||||
new CheatContext(nameof(GameDataTag.RpcFlag)),
|
||||
CheatCategory.RateLimit,
|
||||
$"Client sent more than {AntiCheatConfig.RpcRateLimitPerSecond} RPCs in one second"))
|
||||
{
|
||||
return GameDataResult.Abort;
|
||||
}
|
||||
@@ -374,7 +385,10 @@ namespace Empostor.Server.Net.State
|
||||
{
|
||||
_logger.LogWarning("{Code} - Bad GameData tag {Tag}", Code, reader.Tag);
|
||||
|
||||
if (!await InnerNetObject.ValidateGameDataTag(new CheatContext(nameof(GameDataTag)), sender, reader.Tag))
|
||||
if (await sender.Client.ReportCheatAsync(
|
||||
new CheatContext(nameof(GameDataTag)),
|
||||
CheatCategory.ProtocolExtension,
|
||||
$"Client sent an unknown game data tag {reader.Tag}"))
|
||||
{
|
||||
return GameDataResult.Abort;
|
||||
}
|
||||
@@ -423,7 +437,6 @@ namespace Empostor.Server.Net.State
|
||||
case InnerShipStatus shipStatus:
|
||||
{
|
||||
GameNet.ShipStatus = shipStatus;
|
||||
AntiCheat.NoteRoundStarted();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -440,8 +453,6 @@ namespace Empostor.Server.Net.State
|
||||
await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), CheatCategory.GameFlow, "Failed to find player that spawned the InnerPlayerControl");
|
||||
}
|
||||
|
||||
AntiCheat.For(control.PlayerId).SpawnedAt = AntiCheatState.Now;
|
||||
|
||||
// Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo.
|
||||
var playerInfo = GameNet.GameData.GetPlayerById(control.PlayerId);
|
||||
|
||||
@@ -481,8 +492,6 @@ namespace Empostor.Server.Net.State
|
||||
|
||||
case InnerMeetingHud meetingHud:
|
||||
{
|
||||
AntiCheat.NoteMeetingOpened();
|
||||
|
||||
foreach (var player in _players.Values)
|
||||
{
|
||||
if (GameNet.ShipStatus != null)
|
||||
@@ -518,13 +527,6 @@ namespace Empostor.Server.Net.State
|
||||
case InnerShipStatus:
|
||||
{
|
||||
GameNet.ShipStatus = null;
|
||||
AntiCheat.NoteRoundEnded();
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerMeetingHud:
|
||||
{
|
||||
AntiCheat.NoteMeetingClosed();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Empostor.Server.Net.State
|
||||
{
|
||||
internal partial class Game
|
||||
{
|
||||
private const int RateLimitSlots = 256;
|
||||
private const int UnknownPlayerSlot = RateLimitSlots - 1;
|
||||
|
||||
private readonly RateLimitWindow[] _rpcWindows = CreateWindows();
|
||||
private readonly RateLimitWindow[] _taskWindows = CreateWindows();
|
||||
|
||||
/// <summary>Monotonic clock in seconds, shared by every rate-limit window.</summary>
|
||||
internal static double Now => (double)Stopwatch.GetTimestamp() / Stopwatch.Frequency;
|
||||
|
||||
/// <summary>
|
||||
/// Counts one RPC against the per-second allowance of the player.
|
||||
/// Returns true when this RPC is the one that goes over the limit.
|
||||
/// </summary>
|
||||
internal bool CountRpc(byte playerId)
|
||||
=> _rpcWindows[Slot(playerId)].Hit(Now, AntiCheatConfig.RpcRateLimitPerSecond, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Counts one completed task against the allowance of the player.
|
||||
/// Returns true when this task is the one that goes over the limit.
|
||||
/// </summary>
|
||||
internal bool CountTask(byte playerId)
|
||||
=> _taskWindows[Slot(playerId)].Hit(
|
||||
Now,
|
||||
AntiCheatConfig.TaskRateLimitCount,
|
||||
AntiCheatConfig.TaskRateLimitWindowSeconds);
|
||||
|
||||
private static int Slot(byte playerId)
|
||||
=> playerId < RateLimitSlots ? playerId : UnknownPlayerSlot;
|
||||
|
||||
private static RateLimitWindow[] CreateWindows()
|
||||
{
|
||||
var windows = new RateLimitWindow[RateLimitSlots];
|
||||
for (var i = 0; i < windows.Length; i++)
|
||||
{
|
||||
windows[i] = new RateLimitWindow();
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ring buffer of the most recent hit timestamps, used to spot a burst of
|
||||
/// messages inside a window without ever growing.
|
||||
/// </summary>
|
||||
private struct RateLimitWindow
|
||||
{
|
||||
private const int WindowSize = 64;
|
||||
|
||||
private readonly double[] _stamps;
|
||||
|
||||
private int _next;
|
||||
|
||||
public RateLimitWindow()
|
||||
{
|
||||
_stamps = new double[WindowSize];
|
||||
}
|
||||
|
||||
public bool Hit(double now, int maxCount, double windowSeconds)
|
||||
{
|
||||
if (maxCount <= 0 || windowSeconds <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var used = 0;
|
||||
for (var i = 0; i < WindowSize; i++)
|
||||
{
|
||||
if (_stamps[i] > 0 && now - _stamps[i] < windowSeconds && ++used >= maxCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_stamps[_next] = now;
|
||||
_next = (_next + 1) % WindowSize;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ using Empostor.Api.Net.Inner.Objects;
|
||||
using Empostor.Api.Net.Manager;
|
||||
using Empostor.Api.Net.Messages.S2C;
|
||||
using Empostor.Server.Events;
|
||||
using Empostor.Server.Net.Anticheat;
|
||||
using Empostor.Server.Net.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -70,10 +69,10 @@ namespace Empostor.Server.Net.State
|
||||
_compatibilityConfig = compatibilityConfig.Value;
|
||||
_timeoutConfig = timeoutConfig.Value;
|
||||
Items = new ConcurrentDictionary<object, object>();
|
||||
AntiCheat = new AntiCheatState(options, serviceProvider.GetRequiredService<IOptions<AntiCheatConfig>>().Value);
|
||||
AntiCheatConfig = serviceProvider.GetRequiredService<IOptions<AntiCheatConfig>>().Value;
|
||||
}
|
||||
|
||||
internal AntiCheatState AntiCheat { get; }
|
||||
internal AntiCheatConfig AntiCheatConfig { get; }
|
||||
|
||||
public IPEndPoint PublicIp { get; }
|
||||
|
||||
|
||||
@@ -27,11 +27,10 @@
|
||||
"EnableOwnershipChecks": true,
|
||||
"EnableRoleChecks": true,
|
||||
"EnableTargetChecks": true,
|
||||
"EnableMurderChecks": true,
|
||||
"EnableSabotageChecks": true,
|
||||
"EnableMeetingChecks": true,
|
||||
"EnableVotingChecks": true,
|
||||
"EnableRateLimits": true,
|
||||
"RpcRateLimitPerSecond": 20,
|
||||
"TaskRateLimitCount": 3,
|
||||
"TaskRateLimitWindowSeconds": 2,
|
||||
"ForbidProtocolExtensions": true,
|
||||
"EnablePacketSizeChecks": true,
|
||||
"PacketSizeLimit": 1203
|
||||
|
||||
Reference in new issue
Block a user