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

This commit is contained in:
RaidMax 2020-02-06 18:35:30 -06:00
parent 15e2170100
commit fe380ca331
8 changed files with 120 additions and 53 deletions

1
.gitignore vendored
View File

@ -240,3 +240,4 @@ launchSettings.json
/Master/master/persistence /Master/master/persistence
/WebfrontCore/wwwroot/fonts /WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font /WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles

View File

@ -2,6 +2,8 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -78,7 +80,18 @@ namespace IW4MAdmin.Application.EventParsers
public virtual GameEvent GenerateGameEvent(string logLine) 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<object>)
.Skip(2)
.Select(_value => int.Parse(_value.ToString()))
.Sum();
logLine = logLine.Substring(timeMatch.Value.Length);
}
string[] lineSplit = logLine.Split(';'); string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0]; string eventType = lineSplit[0];
@ -107,7 +120,8 @@ namespace IW4MAdmin.Application.EventParsers
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId },
Message = message, Message = message,
Extra = logLine, 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 }, Origin = new EFClient() { NetworkId = originId },
Message = message, Message = message,
Extra = logLine, Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime
}; };
} }
} }
@ -139,7 +154,8 @@ namespace IW4MAdmin.Application.EventParsers
Data = logLine, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId }, 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, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId }, 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, State = EFClient.ClientState.Connecting,
}, },
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true IsBlocking = true,
GameTime = gameTime
}; };
} }
} }
@ -210,7 +228,8 @@ namespace IW4MAdmin.Application.EventParsers
State = EFClient.ClientState.Disconnecting State = EFClient.ClientState.Disconnecting
}, },
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true IsBlocking = true,
GameTime = gameTime
}; };
} }
} }
@ -223,7 +242,8 @@ namespace IW4MAdmin.Application.EventParsers
Data = logLine, Data = logLine,
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = 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(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(), 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, Type = GameEvent.EventType.JoinTeam,
Data = logLine, Data = logLine,
Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle) }, 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, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId }, 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, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId }, 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, Data = logLine,
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime
}; };
} }
} }

View File

@ -28,6 +28,7 @@ namespace IW4MAdmin
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private readonly IPluginImporter _pluginImporter; private readonly IPluginImporter _pluginImporter;
private int lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
@ -93,18 +94,18 @@ namespace IW4MAdmin
if (client.ClientNumber >= 0) if (client.ClientNumber >= 0)
{ {
#endif #endif
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting..."); Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
Clients[client.ClientNumber] = null; Clients[client.ClientNumber] = null;
await client.OnDisconnect(); await client.OnDisconnect();
var e = new GameEvent() var e = new GameEvent()
{ {
Origin = client, Origin = client,
Owner = this, Owner = this,
Type = GameEvent.EventType.Disconnect Type = GameEvent.EventType.Disconnect
}; };
Manager.GetEventHandler().AddEvent(e); Manager.GetEventHandler().AddEvent(e);
#if DEBUG == true #if DEBUG == true
} }
#endif #endif
@ -434,6 +435,14 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.PreDisconnect) 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 // predisconnect comes from minimal rcon polled players and minimal log players
// so we need to disconnect the "full" version of the client // so we need to disconnect the "full" version of the client
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
@ -531,11 +540,21 @@ namespace IW4MAdmin
string mapname = dict["mapname"]; string mapname = dict["mapname"];
UpdateMap(mapname); UpdateMap(mapname);
} }
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
} }
if (E.Type == GameEvent.EventType.MapEnd) if (E.Type == GameEvent.EventType.MapEnd)
{ {
Logger.WriteInfo("Game ending..."); Logger.WriteInfo("Game ending...");
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
} }
if (E.Type == GameEvent.EventType.Tell) if (E.Type == GameEvent.EventType.Tell)
@ -601,6 +620,12 @@ namespace IW4MAdmin
Logger.WriteDebug(e.GetExceptionInfo()); 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);
}
} }
/// <summary> /// <summary>

View File

@ -70,7 +70,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
/// </summary> /// </summary>
/// <param name="hit">kill performed by the player</param> /// <param name="hit">kill performed by the player</param>
/// <returns>true if detection reached thresholds, false otherwise</returns> /// <returns>true if detection reached thresholds, false otherwise</returns>
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage) public IEnumerable<DetectionPenaltyResult> ProcessHit(EFClientKill hit)
{ {
var results = new List<DetectionPenaltyResult>(); var results = new List<DetectionPenaltyResult>();
@ -81,10 +81,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// hack: prevents false positives // hack: prevents false positives
(LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50)) (LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
{ {
return new DetectionPenaltyResult() return new[] {new DetectionPenaltyResult()
{ {
ClientPenalty = EFPenalty.PenaltyType.Any, ClientPenalty = EFPenalty.PenaltyType.Any,
}; }};
} }
LastWeapon = hit.Weapon; LastWeapon = hit.Weapon;
@ -92,7 +92,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
HitLocationCount[hit.HitLoc].Count++; HitLocationCount[hit.HitLoc].Count++;
HitCount++; HitCount++;
if (!isDamage) if (hit.IsKill)
{ {
Kills++; Kills++;
} }
@ -464,12 +464,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Tracker.OnChange(snapshot); Tracker.OnChange(snapshot);
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? return results;
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Any,
};
} }
} }
} }

View File

@ -510,7 +510,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId) if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId)
{ {
DetectionPenaltyResult result = new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any };
clientDetection.TrackedHits.Add(hit); clientDetection.TrackedHits.Add(hit);
if (clientDetection.TrackedHits.Count >= MIN_HITS_TO_RUN_DETECTION) if (clientDetection.TrackedHits.Count >= MIN_HITS_TO_RUN_DETECTION)
@ -525,9 +524,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (oldestHit.IsAlive) if (oldestHit.IsAlive)
{ {
result = clientDetection.ProcessHit(oldestHit, isDamage); var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint);
#if !DEBUG #if !DEBUG
await ApplyPenalty(result, attacker); await ApplyPenalty(result, attacker);
#endif #endif
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
@ -564,6 +563,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> 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) public async Task SaveHitCache(long serverId)
{ {
using (var ctx = new DatabaseContext(true)) using (var ctx = new DatabaseContext(true))
@ -594,12 +605,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker) 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); var penaltyClient = Utilities.IW4MAdminClient(attacker.CurrentServer);
switch (penalty.ClientPenalty) switch (penalty.ClientPenalty)
{ {

View File

@ -217,6 +217,10 @@ namespace SharedLibraryCore
public EventRequiredEntity RequiredEntity { get; set; } public EventRequiredEntity RequiredEntity { get; set; }
public string Data; // Data is usually the message sent by player public string Data; // Data is usually the message sent by player
public string Message; public string Message;
/// <summary>
/// Specifies the game time offset as printed in the log
/// </summary>
public int? GameTime { get; set; }
public EFClient Origin; public EFClient Origin;
public EFClient Target; public EFClient Target;
public Server Owner; public Server Owner;

View File

@ -328,6 +328,7 @@ namespace SharedLibraryCore.Database.Models
e.FailReason = GameEvent.EventFailReason.Permission; e.FailReason = GameEvent.EventFailReason.Permission;
} }
State = ClientState.Disconnecting;
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e; return e;
} }
@ -563,7 +564,7 @@ namespace SharedLibraryCore.Database.Models
CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}"); CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}");
} }
private async Task<bool> CanConnect(int? ipAddress) public async Task<bool> CanConnect(int? ipAddress)
{ {
var loc = Utilities.CurrentLocalization.LocalizationIndex; var loc = Utilities.CurrentLocalization.LocalizationIndex;
var autoKickClient = Utilities.IW4MAdminClient(CurrentServer); var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);

View File

@ -110,11 +110,14 @@ namespace SharedLibraryCore.Services
.Include(a => a.Link) .Include(a => a.Link)
// we only want alias that have the same IP address or share 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)); .Where(_alias => _alias.IPAddress == ip || (_alias.LinkId == entity.AliasLinkId));
var aliases = await iqAliases.ToListAsync(); 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 // 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; bool hasExactAliasMatch = existingExactAlias != null;
// if existing alias matches link them // if existing alias matches link them
@ -128,17 +131,22 @@ namespace SharedLibraryCore.Services
bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId; bool isAliasLinkUpdated = newAliasLink.AliasLinkId != entity.AliasLink.AliasLinkId;
await context.SaveChangesAsync(); 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 // 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}"); 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 // update all the clients that have the old alias link
await context.Clients await context.Clients
.Where(_client => _client.AliasLinkId == oldAliasLink.AliasLinkId) .Where(_client => completeAliasLinkIds.Contains(_client.AliasLinkId))
.ForEachAsync(_client => _client.AliasLinkId = newAliasLink.AliasLinkId); .ForEachAsync(_client => _client.AliasLinkId = newAliasLink.AliasLinkId);
// we also need to update all the penalties or they get deleted // we also need to update all the penalties or they get deleted
@ -151,7 +159,7 @@ namespace SharedLibraryCore.Services
// link2 is deleted // link2 is deleted
// link2 penalties are orphaned // link2 penalties are orphaned
await context.Penalties await context.Penalties
.Where(_penalty => _penalty.LinkId == oldAliasLink.AliasLinkId) .Where(_penalty => completeAliasLinkIds.Contains(_penalty.LinkId))
.ForEachAsync(_penalty => _penalty.LinkId = newAliasLink.AliasLinkId); .ForEachAsync(_penalty => _penalty.LinkId = newAliasLink.AliasLinkId);
entity.AliasLink = newAliasLink; entity.AliasLink = newAliasLink;
@ -159,13 +167,16 @@ namespace SharedLibraryCore.Services
// update all previous aliases // update all previous aliases
await context.Aliases await context.Aliases
.Where(_alias => _alias.LinkId == oldAliasLink.AliasLinkId) .Where(_alias => completeAliasLinkIds.Contains(_alias.LinkId))
.ForEachAsync(_alias => _alias.LinkId = newAliasLink.AliasLinkId); .ForEachAsync(_alias => _alias.LinkId = newAliasLink.AliasLinkId);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
// we want to delete the now inactive alias // we want to delete the now inactive alias
context.AliasLinks.Remove(oldAliasLink); if (newAliasLink.AliasLinkId != entity.AliasLinkId)
await context.SaveChangesAsync(); {
context.AliasLinks.Remove(entity.AliasLink);
await context.SaveChangesAsync();
}
} }
// the existing alias matches ip and name, so we can just ignore the temporary one // the existing alias matches ip and name, so we can just ignore the temporary one