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:
parent
15e2170100
commit
fe380ca331
1
.gitignore
vendored
1
.gitignore
vendored
@ -240,3 +240,4 @@ launchSettings.json
|
||||
/Master/master/persistence
|
||||
/WebfrontCore/wwwroot/fonts
|
||||
/WebfrontCore/wwwroot/font
|
||||
/Plugins/Tests/TestSourceFiles
|
||||
|
@ -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<object>)
|
||||
.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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -70,7 +70,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
/// </summary>
|
||||
/// <param name="hit">kill performed by the player</param>
|
||||
/// <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>();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +524,7 @@ 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);
|
||||
#endif
|
||||
@ -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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -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;
|
||||
/// <summary>
|
||||
/// Specifies the game time offset as printed in the log
|
||||
/// </summary>
|
||||
public int? GameTime { get; set; }
|
||||
public EFClient Origin;
|
||||
public EFClient Target;
|
||||
public Server Owner;
|
||||
|
@ -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<bool> CanConnect(int? ipAddress)
|
||||
public async Task<bool> CanConnect(int? ipAddress)
|
||||
{
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);
|
||||
|
@ -112,9 +112,12 @@ namespace SharedLibraryCore.Services
|
||||
.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,14 +167,17 @@ 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);
|
||||
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
|
||||
if (hasExactAliasMatch)
|
||||
|
Loading…
Reference in New Issue
Block a user