From fe380ca33117d094b32742a1f12cfac149bb7fcb Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 6 Feb 2020 18:35:30 -0600 Subject: [PATCH] Use game time from log to ignore potential false disconnect lines - Fix for latent linking issues with multiple ips - Anticheat fix for T6 - retry kick on update if they're not allowed to connect --- .gitignore | 1 + Application/EventParsers/BaseEventParser.cs | 51 ++++++++++++++----- Application/IW4MServer.cs | 45 ++++++++++++---- Plugins/Stats/Cheat/Detection.cs | 15 ++---- Plugins/Stats/Helpers/StatManager.cs | 23 +++++---- SharedLibraryCore/Events/GameEvent.cs | 4 ++ SharedLibraryCore/PartialEntities/EFClient.cs | 3 +- SharedLibraryCore/Services/ClientService.cs | 31 +++++++---- 8 files changed, 120 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 8baefa9b1..edbc2eec0 100644 --- a/.gitignore +++ b/.gitignore @@ -240,3 +240,4 @@ launchSettings.json /Master/master/persistence /WebfrontCore/wwwroot/fonts /WebfrontCore/wwwroot/font +/Plugins/Tests/TestSourceFiles diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index 3d0bc746b..cb73d54ab 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -2,6 +2,8 @@ using SharedLibraryCore.Database.Models; using SharedLibraryCore.Interfaces; using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using static SharedLibraryCore.Server; @@ -78,7 +80,18 @@ namespace IW4MAdmin.Application.EventParsers public virtual GameEvent GenerateGameEvent(string logLine) { - logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim(); + var timeMatch = Regex.Match(logLine, @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )"); + int gameTime = 0; + + if (timeMatch.Success) + { + gameTime = (timeMatch.Groups.Values as IEnumerable) + .Skip(2) + .Select(_value => int.Parse(_value.ToString())) + .Sum(); + logLine = logLine.Substring(timeMatch.Value.Length); + } + string[] lineSplit = logLine.Split(';'); string eventType = lineSplit[0]; @@ -107,7 +120,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = new EFClient() { NetworkId = originId }, Message = message, Extra = logLine, - RequiredEntity = GameEvent.EventRequiredEntity.Origin + RequiredEntity = GameEvent.EventRequiredEntity.Origin, + GameTime = gameTime }; } @@ -118,7 +132,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = new EFClient() { NetworkId = originId }, Message = message, Extra = logLine, - RequiredEntity = GameEvent.EventRequiredEntity.Origin + RequiredEntity = GameEvent.EventRequiredEntity.Origin, + GameTime = gameTime }; } } @@ -139,7 +154,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = new EFClient() { NetworkId = originId }, Target = new EFClient() { NetworkId = targetId }, - RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target + RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, + GameTime = gameTime }; } } @@ -159,7 +175,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = new EFClient() { NetworkId = originId }, Target = new EFClient() { NetworkId = targetId }, - RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target + RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, + GameTime = gameTime }; } } @@ -185,7 +202,8 @@ namespace IW4MAdmin.Application.EventParsers State = EFClient.ClientState.Connecting, }, RequiredEntity = GameEvent.EventRequiredEntity.None, - IsBlocking = true + IsBlocking = true, + GameTime = gameTime }; } } @@ -210,7 +228,8 @@ namespace IW4MAdmin.Application.EventParsers State = EFClient.ClientState.Disconnecting }, RequiredEntity = GameEvent.EventRequiredEntity.None, - IsBlocking = true + IsBlocking = true, + GameTime = gameTime }; } } @@ -223,7 +242,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), - RequiredEntity = GameEvent.EventRequiredEntity.None + RequiredEntity = GameEvent.EventRequiredEntity.None, + GameTime = gameTime }; } @@ -238,7 +258,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), Extra = dump.DictionaryFromKeyValue(), - RequiredEntity = GameEvent.EventRequiredEntity.None + RequiredEntity = GameEvent.EventRequiredEntity.None, + GameTime = gameTime }; } @@ -250,7 +271,8 @@ namespace IW4MAdmin.Application.EventParsers Type = GameEvent.EventType.JoinTeam, Data = logLine, Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle) }, - RequiredEntity = GameEvent.EventRequiredEntity.Target + RequiredEntity = GameEvent.EventRequiredEntity.Target, + GameTime = gameTime }; } @@ -267,7 +289,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = new EFClient() { NetworkId = originId }, Target = new EFClient() { NetworkId = targetId }, - RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target + RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, + GameTime = gameTime }; } @@ -283,7 +306,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = new EFClient() { NetworkId = originId }, Target = new EFClient() { NetworkId = targetId }, - RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target + RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, + GameTime = gameTime }; } @@ -293,7 +317,8 @@ namespace IW4MAdmin.Application.EventParsers Data = logLine, Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), - RequiredEntity = GameEvent.EventRequiredEntity.None + RequiredEntity = GameEvent.EventRequiredEntity.None, + GameTime = gameTime }; } } diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 6afdb1104..877fcc2f9 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -28,6 +28,7 @@ namespace IW4MAdmin private readonly ITranslationLookup _translationLookup; private const int REPORT_FLAG_COUNT = 4; private readonly IPluginImporter _pluginImporter; + private int lastGameTime = 0; public int Id { get; private set; } @@ -93,18 +94,18 @@ namespace IW4MAdmin if (client.ClientNumber >= 0) { #endif - Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting..."); - Clients[client.ClientNumber] = null; - await client.OnDisconnect(); + Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting..."); + Clients[client.ClientNumber] = null; + await client.OnDisconnect(); - var e = new GameEvent() - { - Origin = client, - Owner = this, - Type = GameEvent.EventType.Disconnect - }; + var e = new GameEvent() + { + Origin = client, + Owner = this, + Type = GameEvent.EventType.Disconnect + }; - Manager.GetEventHandler().AddEvent(e); + Manager.GetEventHandler().AddEvent(e); #if DEBUG == true } #endif @@ -434,6 +435,14 @@ namespace IW4MAdmin else if (E.Type == GameEvent.EventType.PreDisconnect) { + bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime; + + if (isPotentialFalseQuit) + { + Logger.WriteInfo($"Receive predisconnect event for {E}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring"); + return false; + } + // predisconnect comes from minimal rcon polled players and minimal log players // so we need to disconnect the "full" version of the client var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); @@ -531,11 +540,21 @@ namespace IW4MAdmin string mapname = dict["mapname"]; UpdateMap(mapname); } + + if (E.GameTime.HasValue) + { + lastGameTime = E.GameTime.Value; + } } if (E.Type == GameEvent.EventType.MapEnd) { Logger.WriteInfo("Game ending..."); + + if (E.GameTime.HasValue) + { + lastGameTime = E.GameTime.Value; + } } if (E.Type == GameEvent.EventType.Tell) @@ -601,6 +620,12 @@ namespace IW4MAdmin Logger.WriteDebug(e.GetExceptionInfo()); } } + + else if (client.IPAddress != null && client.State == ClientState.Disconnecting) + { + Logger.WriteWarning($"{client} state is Disconnecting (probably kicked), but they are still connected. trying to kick again..."); + await client.CanConnect(client.IPAddress); + } } /// diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index a66ca7806..55f70b02f 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -70,7 +70,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat /// /// kill performed by the player /// true if detection reached thresholds, false otherwise - public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage) + public IEnumerable ProcessHit(EFClientKill hit) { var results = new List(); @@ -81,10 +81,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat // hack: prevents false positives (LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50)) { - return new DetectionPenaltyResult() + return new[] {new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any, - }; + }}; } LastWeapon = hit.Weapon; @@ -92,7 +92,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat HitLocationCount[hit.HitLoc].Count++; HitCount++; - if (!isDamage) + if (hit.IsKill) { Kills++; } @@ -464,12 +464,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Tracker.OnChange(snapshot); - return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? - results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? - new DetectionPenaltyResult() - { - ClientPenalty = EFPenalty.PenaltyType.Any, - }; + return results; } } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 79f9e93ab..0155a046d 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -510,7 +510,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId) { - DetectionPenaltyResult result = new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any }; clientDetection.TrackedHits.Add(hit); if (clientDetection.TrackedHits.Count >= MIN_HITS_TO_RUN_DETECTION) @@ -525,9 +524,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (oldestHit.IsAlive) { - result = clientDetection.ProcessHit(oldestHit, isDamage); + var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint); #if !DEBUG - await ApplyPenalty(result, attacker); + await ApplyPenalty(result, attacker); #endif if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) @@ -564,6 +563,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } + private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable results, long serverId) + { + // allow disabling of certain detection types + results = results.Where(_result => ShouldUseDetection(serverId, _result.Type)); + return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? + results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? + new DetectionPenaltyResult() + { + ClientPenalty = EFPenalty.PenaltyType.Any, + }; + } + public async Task SaveHitCache(long serverId) { using (var ctx = new DatabaseContext(true)) @@ -594,12 +605,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker) { - // allow disabling of certain detection types - if (!ShouldUseDetection(attacker.CurrentServer.EndPoint, penalty.Type)) - { - return; - } - var penaltyClient = Utilities.IW4MAdminClient(attacker.CurrentServer); switch (penalty.ClientPenalty) { diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs index bf60abf70..24f53c070 100644 --- a/SharedLibraryCore/Events/GameEvent.cs +++ b/SharedLibraryCore/Events/GameEvent.cs @@ -217,6 +217,10 @@ namespace SharedLibraryCore public EventRequiredEntity RequiredEntity { get; set; } public string Data; // Data is usually the message sent by player public string Message; + /// + /// Specifies the game time offset as printed in the log + /// + public int? GameTime { get; set; } public EFClient Origin; public EFClient Target; public Server Owner; diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index 545951ff8..1ce137eca 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -328,6 +328,7 @@ namespace SharedLibraryCore.Database.Models e.FailReason = GameEvent.EventFailReason.Permission; } + State = ClientState.Disconnecting; sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } @@ -563,7 +564,7 @@ namespace SharedLibraryCore.Database.Models CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}"); } - private async Task CanConnect(int? ipAddress) + public async Task CanConnect(int? ipAddress) { var loc = Utilities.CurrentLocalization.LocalizationIndex; var autoKickClient = Utilities.IW4MAdminClient(CurrentServer); diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index bd3efa8a2..5cc6e9b88 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -110,11 +110,14 @@ namespace SharedLibraryCore.Services .Include(a => a.Link) // we only want alias that have the same IP address or share a link .Where(_alias => _alias.IPAddress == ip || (_alias.LinkId == entity.AliasLinkId)); - + var aliases = await iqAliases.ToListAsync(); + var currentIPs = aliases.Where(_a2 => _a2.IPAddress != null).Select(_a2 => _a2.IPAddress).Distinct(); + var floatingIPAliases = await context.Aliases.Where(_alias => currentIPs.Contains(_alias.IPAddress)).ToListAsync(); + aliases.AddRange(floatingIPAliases); // see if they have a matching IP + Name but new NetworkId - var existingExactAlias = aliases.FirstOrDefault(a => a.Name == name && a.IPAddress == ip); + var existingExactAlias = aliases.OrderBy(_alias => _alias.LinkId).FirstOrDefault(a => a.Name == name && a.IPAddress == ip); bool hasExactAliasMatch = existingExactAlias != null; // if existing alias matches link them @@ -128,17 +131,22 @@ namespace SharedLibraryCore.Services bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId; await context.SaveChangesAsync(); - + int distinctLinkCount = aliases.Select(_alias => _alias.LinkId).Distinct().Count(); // this happens when the link we found is different than the one we create before adding an IP - if (isAliasLinkUpdated) + if (isAliasLinkUpdated || distinctLinkCount > 1) { entity.CurrentServer.Logger.WriteDebug($"[updatealias] found a link for {entity} so we are updating link from {entity.AliasLink.AliasLinkId} to {newAliasLink.AliasLinkId}"); - var oldAliasLink = entity.AliasLink; + var completeAliasLinkIds = aliases.Select(_item => _item.LinkId) + .Append(entity.AliasLinkId) + .Distinct() + .ToList(); + + entity.CurrentServer.Logger.WriteDebug($"[updatealias] updating aliasLinks {string.Join(',', completeAliasLinkIds)} for IP {ip} to {newAliasLink.AliasLinkId}"); // update all the clients that have the old alias link await context.Clients - .Where(_client => _client.AliasLinkId == oldAliasLink.AliasLinkId) + .Where(_client => completeAliasLinkIds.Contains(_client.AliasLinkId)) .ForEachAsync(_client => _client.AliasLinkId = newAliasLink.AliasLinkId); // we also need to update all the penalties or they get deleted @@ -151,7 +159,7 @@ namespace SharedLibraryCore.Services // link2 is deleted // link2 penalties are orphaned await context.Penalties - .Where(_penalty => _penalty.LinkId == oldAliasLink.AliasLinkId) + .Where(_penalty => completeAliasLinkIds.Contains(_penalty.LinkId)) .ForEachAsync(_penalty => _penalty.LinkId = newAliasLink.AliasLinkId); entity.AliasLink = newAliasLink; @@ -159,13 +167,16 @@ namespace SharedLibraryCore.Services // update all previous aliases await context.Aliases - .Where(_alias => _alias.LinkId == oldAliasLink.AliasLinkId) + .Where(_alias => completeAliasLinkIds.Contains(_alias.LinkId)) .ForEachAsync(_alias => _alias.LinkId = newAliasLink.AliasLinkId); await context.SaveChangesAsync(); // we want to delete the now inactive alias - context.AliasLinks.Remove(oldAliasLink); - await context.SaveChangesAsync(); + if (newAliasLink.AliasLinkId != entity.AliasLinkId) + { + context.AliasLinks.Remove(entity.AliasLink); + await context.SaveChangesAsync(); + } } // the existing alias matches ip and name, so we can just ignore the temporary one