Skip to content

Commit

Permalink
Refactored Bad Actor logic
Browse files Browse the repository at this point in the history
  • Loading branch information
FallenDev committed Jan 2, 2025
1 parent cd68a26 commit 7d066a1
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 440 deletions.
2 changes: 1 addition & 1 deletion Zolian.GameServer/Zolian.GameServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Chaos-Networking" Version="2.4.0" />
<PackageReference Include="Chaos-Networking" Version="2.4.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-preview3.24332.3" />
<PackageReference Include="Microsoft.DependencyValidation.Analyzers" Version="0.11.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0">
Expand Down
160 changes: 160 additions & 0 deletions Zolian.Server.Base/Network/Server/BadActor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Net;

using Darkages.Models;

using Microsoft.Extensions.Logging;

using Newtonsoft.Json;

using RestSharp;

using ServiceStack;

namespace Darkages.Network.Server;

public static class BadActor
{
private const string InternalIP = "192.168.50.1"; // Cannot use ServerConfig due to value needing to be constant

public static bool ClientOnBlackList(string remoteIp)
{
if (remoteIp.IsNullOrEmpty() || !IPAddress.TryParse(remoteIp, out _)) return true;
if (remoteIp is "127.0.0.1" or InternalIP) return false;

try
{
var keyCode = ServerSetup.Instance.KeyCode;
if (keyCode is null || keyCode.Length == 0)
{
ServerSetup.ConnectionLogger("Keycode not valid or not set within ServerConfig.json");
return false;
}

// BLACKLIST check
var request = new RestRequest("", Method.Get);
request.AddHeader("Key", keyCode);
request.AddHeader("Accept", "application/json");
request.AddParameter("ipAddress", remoteIp);
request.AddParameter("maxAgeInDays", "90");
request.AddParameter("verbose", "");
var response = ExecuteWithRetry(() => ServerSetup.Instance.RestClient.Execute<Ipdb>(request));

if (response?.IsSuccessful == true)
{
var ipdb = JsonConvert.DeserializeObject<Ipdb>(response.Content!);
var abuseConfidenceScore = ipdb?.Data?.AbuseConfidenceScore;
var tor = ipdb?.Data?.IsTor;
var usageType = ipdb?.Data?.UsageType;

if (tor == true)
{
LogBadActor(remoteIp, "using tor");
return true;
}

// Block if known malicious usage type
if (IsMaliciousUsageType(usageType))
{
LogBadActor(remoteIp, $"using {usageType}");
return true;
}

// Block based on abuse confidence score
if (abuseConfidenceScore >= 5)
{
LogBadActor(remoteIp, $"high risk score of {abuseConfidenceScore}");
return true;
}
}
else
{
ServerSetup.ConnectionLogger($"{remoteIp} - API Issue or Failed response");
}
}
catch (Exception ex)
{
ServerSetup.ConnectionLogger($"Error checking blacklist for {remoteIp}: {ex.Message}", LogLevel.Warning);
SentrySdk.CaptureException(ex);
}

return true;
}

private static T ExecuteWithRetry<T>(Func<T> operation, int maxRetries = 3)
{
var attempt = 0;

while (attempt < maxRetries)
{
try
{
return operation();
}
catch (Exception ex)
{
attempt++;
if (attempt >= maxRetries)
{
ServerSetup.ConnectionLogger($"Max retries reached. Operation failed: {ex.Message}", LogLevel.Warning);
SentrySdk.CaptureException(ex);
return default(T);
}

// Wait before retrying
Thread.Sleep(1000); // Retry delay
}
}

return default(T);
}

private static void LogBadActor(string remoteIp, string reason)
{
ServerSetup.ConnectionLogger($"Blocking {remoteIp} - Reason: {reason}", LogLevel.Warning);
SentrySdk.CaptureMessage($"{remoteIp} blocked due to {reason}");
ReportEndpoint(remoteIp, $"Blocked due to {reason}");
}

private static bool IsMaliciousUsageType(string usageType)
{
return usageType switch
{
"Commercial" or "Organization" or "Government" or "Military" or "Content Delivery Network" or "Data Center/Web Hosting/Transit" => true,
_ => false
};
}

public static void ReportEndpoint(string remoteIp, string comment)
{
var keyCode = ServerSetup.Instance.KeyCode;
if (keyCode is null || keyCode.Length == 0)
{
ServerSetup.ConnectionLogger("Keycode not valid or not set within ServerConfig.json");
return;
}

try
{
var request = new RestRequest("", Method.Post);
request.AddHeader("Key", keyCode);
request.AddHeader("Accept", "application/json");
request.AddParameter("ip", remoteIp);
request.AddParameter("categories", "14, 15, 16, 21");
request.AddParameter("comment", comment);
var response = ExecuteWithRetry(() => ServerSetup.Instance.RestReport.Execute(request));

if (response?.IsSuccessful == true)
{
return;
}

ServerSetup.ConnectionLogger($"Error reporting {remoteIp} : {comment}");
SentrySdk.CaptureMessage($"Error reporting {remoteIp} : {comment}");
}
catch (Exception ex)
{
ServerSetup.ConnectionLogger($"Exception while reporting {remoteIp}: {ex.Message}", LogLevel.Warning);
SentrySdk.CaptureException(ex);
}
}
}
165 changes: 12 additions & 153 deletions Zolian.Server.Base/Network/Server/LobbyServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@
using Chaos.Packets;
using Chaos.Packets.Abstractions;
using Darkages.Meta;
using Darkages.Models;
using Darkages.Network.Client;
using Microsoft.Extensions.Logging;
using RestSharp;

using System.Net;
using System.Net.Sockets;
using System.Text;
using Chaos.Networking.Abstractions.Definitions;
using JetBrains.Annotations;
using Newtonsoft.Json;
using ServiceStack;
using ConnectionInfo = Chaos.Networking.Options.ConnectionInfo;
using ServerOptions = Chaos.Networking.Options.ServerOptions;
Expand All @@ -34,7 +30,6 @@ public sealed class LobbyServer : ServerBase<ILobbyClient>, ILobbyServer<ILobbyC
{
private readonly IClientFactory<LobbyClient> _clientProvider;
private readonly MServerTable _serverTable;
private const string InternalIP = "192.168.50.1"; // Cannot use ServerConfig due to value needing to be constant

public LobbyServer(
IClientFactory<LobbyClient> clientProvider,
Expand Down Expand Up @@ -154,12 +149,21 @@ protected override void OnConnected(Socket clientSocket)
client.OnDisconnected += OnDisconnect;
var safe = false;

foreach (var _ in ServerSetup.Instance.GlobalKnownGoodActorsCache.Values.Where(savedIp => savedIp == ipAddress.ToString()))
var banned = BannedIpCheck(ipAddress.ToString());
if (banned)
{
client.Disconnect();
ServerSetup.ConnectionLogger($"Banned connection attempt from {ip}");
return;
}

var foundIp = ServerSetup.Instance.GlobalKnownGoodActorsCache.Values.First(savedIp => savedIp == ipAddress.ToString());
if (!foundIp.IsEmpty())
safe = true;

if (!safe)
{
var badActor = ClientOnBlackList(ipAddress.ToString());
var badActor = BadActor.ClientOnBlackList(ipAddress.ToString());
if (badActor)
{
try
Expand Down Expand Up @@ -202,152 +206,7 @@ private void OnDisconnect(object sender, EventArgs e)
var client = (ILobbyClient)sender!;
ClientRegistry.TryRemove(client.Id, out _);
}

/// <summary>
/// Client IP Check - Blacklist and BOGON list checks
/// </summary>
/// <returns>Boolean, whether or not the IP has been listed as valid</returns>
private bool ClientOnBlackList(string remoteIp)
{
if (remoteIp.IsNullOrEmpty()) return true;

switch (remoteIp)
{
case "127.0.0.1":
case InternalIP:
return false;
}

var bogonCheck = BannedIpCheck(remoteIp);
if (bogonCheck)
{
ServerSetup.ConnectionLogger("-----------------------------------");
ServerSetup.ConnectionLogger($"{remoteIp} is banned and unable to connect");
SentrySdk.CaptureMessage($"{remoteIp} is banned and unable to connect");
return true;
}

try
{
var keyCode = ServerSetup.Instance.KeyCode;
if (keyCode is null || keyCode.Length == 0)
{
ServerSetup.ConnectionLogger("Keycode not valid or not set within ServerConfig.json");
return false;
}

// BLACKLIST check
var request = new RestRequest("", Method.Get);
request.AddHeader("Key", keyCode);
request.AddHeader("Accept", "application/json");
request.AddParameter("ipAddress", remoteIp);
request.AddParameter("maxAgeInDays", "90");
request.AddParameter("verbose", "");
var response = ServerSetup.Instance.RestClient.Execute<Ipdb>(request);

if (response.IsSuccessful)
{
var json = response.Content;

if (json is null || json.Length == 0)
{
ServerSetup.ConnectionLogger($"{remoteIp} - API Issue, response is null or length is 0");
return false;
}

var ipdb = JsonConvert.DeserializeObject<Ipdb>(json!);
var abuseConfidenceScore = ipdb?.Data?.AbuseConfidenceScore;
var tor = ipdb?.Data?.IsTor;
var usageType = ipdb?.Data?.UsageType;

if (tor == true)
{
ServerSetup.ConnectionLogger("---------Lobby-Server---------");
ServerSetup.ConnectionLogger($"{remoteIp} is using tor and automatically blocked", LogLevel.Warning);
SentrySdk.CaptureMessage($"{remoteIp} has a confidence score of {abuseConfidenceScore}, and was using tor, and IP type: {usageType}");
return true;
}

if (usageType == "Reserved")
{
ServerSetup.ConnectionLogger("---------Lobby-Server---------");
ServerSetup.ConnectionLogger($"{remoteIp} was blocked due to being a reserved address (bogon)", LogLevel.Warning);
SentrySdk.CaptureMessage($"{remoteIp} has a confidence score of {abuseConfidenceScore}, and was using a Reserved Address");
return true;
}

if (usageType == "Data Center/Web Hosting/Transit")
{
ServerSetup.ConnectionLogger("---------Lobby-Server---------");
ServerSetup.ConnectionLogger($"{remoteIp} was blocked due to being a data center, web hosting, or transit address", LogLevel.Warning);
SentrySdk.CaptureMessage($"{remoteIp} has a confidence score of {abuseConfidenceScore}, and is a data center, web host, or transit service.");
return true;
}

switch (abuseConfidenceScore)
{
case >= 5:
ServerSetup.ConnectionLogger("---------Lobby-Server---------");
var comment = $"{remoteIp} has been blocked due to a high risk assessment score of {abuseConfidenceScore}, indicating a recognized malicious entity.";
ServerSetup.ConnectionLogger(comment, LogLevel.Warning);
SentrySdk.CaptureMessage($"{remoteIp} has a confidence score of {abuseConfidenceScore}, is using tor: {tor}, and IP type: {usageType}");
ReportEndpoint(remoteIp, comment);
return true;
case >= 0:
return false;
case null:
// Can be null if there is an error in the API, don't want to punish players if its the APIs fault
ServerSetup.ConnectionLogger($"{remoteIp} - API Issue, confidence score was null");
return false;
}
}
else
{
// Can be null if there is an error in the API, don't want to punish players if its the APIs fault
ServerSetup.ConnectionLogger($"{remoteIp} - API Issue, response was not successful");
return false;
}
}
catch (Exception ex)
{
ServerSetup.ConnectionLogger("Unknown issue with IPDB, connections refused", LogLevel.Warning);
ServerSetup.ConnectionLogger($"{ex}");
SentrySdk.CaptureException(ex);
return false;
}

return true;
}

private static void ReportEndpoint(string remoteIp, string comment)
{
var keyCode = ServerSetup.Instance.KeyCode;
if (keyCode is null || keyCode.Length == 0)
{
ServerSetup.ConnectionLogger("Keycode not valid or not set within ServerConfig.json");
return;
}

try
{
var request = new RestRequest("", Method.Post);
request.AddHeader("Key", keyCode);
request.AddHeader("Accept", "application/json");
request.AddParameter("ip", remoteIp);
request.AddParameter("categories", "14, 15, 16, 21");
request.AddParameter("comment", comment);
var response = ServerSetup.Instance.RestReport.Execute(request);

if (response.IsSuccessful) return;
ServerSetup.ConnectionLogger($"Error reporting {remoteIp} : {comment}");
SentrySdk.CaptureMessage($"Error reporting {remoteIp} : {comment}");
}
catch
{
// ignore
}
}


private readonly HashSet<string> _bannedIPs = [];

private bool BannedIpCheck(string ip)
Expand Down
Loading

0 comments on commit 7d066a1

Please sign in to comment.