fix: Adds better client verification (#853)

* Adds MinRequired and MaxRequired settings
* Removes god client detection
* Streamlines the kick messaging
* Fixes detecting client version on mac/linux
This commit is contained in:
Kamron Batman 2021-11-27 10:06:39 -08:00 committed by GitHub
parent fcc91cbd99
commit 1ac803e778
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 113 deletions

View file

@ -38,11 +38,11 @@ namespace Server
public static List<IPEndPoint> Listeners => m_Settings.Listeners;
public static string GetSetting(string key, string defaultValue)
{
m_Settings.Settings.TryGetValue(key, out var value);
return value ?? defaultValue;
}
public static ClientVersion GetSetting(string key, ClientVersion defaultValue) =>
m_Settings.Settings.TryGetValue(key, out var value) ? new ClientVersion(value) : defaultValue;
public static string GetSetting(string key, string defaultValue) =>
m_Settings.Settings.TryGetValue(key, out var value) ? value : defaultValue;
public static int GetSetting(string key, int defaultValue)
{

View file

@ -197,7 +197,9 @@ namespace Server.Network
public Pipe<byte> SendPipe { get; }
public Socket Connection { get; }
public bool Running => _running;
public Socket Connection { get; private set; }
public bool CompressionEnabled { get; set; }

View file

@ -39,7 +39,7 @@ namespace Server.Network
private static readonly byte[] _socketRejected = { 0x82, 0xFF };
public static IPEndPoint[] ListeningAddresses { get; private set; }
public static TcpListener[] Listeners { get; private set; }
public static Socket[] Listeners { get; private set; }
public static HashSet<NetState> Instances { get; } = new(2048);
private static readonly ConcurrentQueue<NetState> _connectedQueue = new();
@ -52,7 +52,7 @@ namespace Server.Network
public static void Start()
{
HashSet<IPEndPoint> listeningAddresses = new HashSet<IPEndPoint>();
List<TcpListener> listeners = new List<TcpListener>();
List<Socket> listeners = new List<Socket>();
foreach (var ipep in ServerConfiguration.Listeners)
{
@ -88,7 +88,7 @@ namespace Server.Network
{
foreach (var listener in Listeners)
{
listener.Server.Close();
listener.Close();
}
}
@ -99,21 +99,19 @@ namespace Server.Network
.Select(uip => new IPEndPoint(uip.Address, ipep.Port))
);
public static TcpListener CreateListener(IPEndPoint ipep)
public static Socket CreateListener(IPEndPoint ipep)
{
var listener = new TcpListener(ipep)
var listener = new Socket(ipep.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
Server =
{
LingerState = new LingerOption(false, 0),
ExclusiveAddressUse = true,
NoDelay = true
}
LingerState = new LingerOption(false, 0),
ExclusiveAddressUse = true,
NoDelay = true
};
try
{
listener.Start(32);
listener.Bind(ipep);
listener.Listen(32);
return listener;
}
catch (SocketException se)
@ -148,13 +146,14 @@ namespace Server.Network
}
}
private static async void BeginAcceptingSockets(this TcpListener listener)
private static async void BeginAcceptingSockets(this Socket listener)
{
while (true)
{
try
{
var socket = await listener.AcceptSocketAsync();
var socket = await listener.AcceptAsync();
var rejected = false;
if (Instances.Count >= MaxConnections)
{

View file

@ -1,10 +1,12 @@
using System;
using System.Diagnostics;
using System.Buffers.Binary;
using System.IO;
using Server.Buffers;
using Server.Gumps;
using Server.Logging;
using Server.Mobiles;
using Server.Network;
using Server.Text;
namespace Server.Misc
{
@ -12,29 +14,36 @@ namespace Server.Misc
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ClientVerification));
private static bool m_DetectClientRequirement;
private static OldClientResponse m_OldClientResponse;
private static bool _enable;
private static bool _detectClientRequirement;
private static InvalidClientResponse _invalidClientResponse;
private static string _versionExpression;
private static TimeSpan m_AgeLeniency;
private static TimeSpan m_GameTimeLeniency;
private static TimeSpan _ageLeniency;
private static TimeSpan _gameTimeLeniency;
public static ClientVersion Required { get; set; }
public static ClientVersion MinRequired { get; private set; }
public static ClientVersion MaxRequired { get; private set; }
public static bool AllowRegular { get; set; } = true;
public static bool AllowUOTD { get; set; } = true;
public static bool AllowGod { get; set; } = true;
public static TimeSpan KickDelay { get; set; }
public static bool AllowRegular => true;
public static bool AllowUOTD => false;
public static TimeSpan KickDelay { get; private set; }
public static void Configure()
{
m_DetectClientRequirement = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true);
m_OldClientResponse =
ServerConfiguration.GetOrUpdateSetting("clientVerification.oldClientResponse", OldClientResponse.Kick);
m_AgeLeniency = ServerConfiguration.GetOrUpdateSetting("clientVerification.ageLeniency", TimeSpan.FromDays(10));
m_GameTimeLeniency = ServerConfiguration.GetOrUpdateSetting(
MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null);
MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null);
if (MinRequired == null && MaxRequired == null)
{
_detectClientRequirement = ServerConfiguration.GetOrUpdateSetting("clientVerification.detectFromClientExe", true);
}
_enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true);
_invalidClientResponse =
ServerConfiguration.GetOrUpdateSetting("clientVerification.invalidClientResponse", InvalidClientResponse.Kick);
_ageLeniency = ServerConfiguration.GetOrUpdateSetting("clientVerification.ageLeniency", TimeSpan.FromDays(10));
_gameTimeLeniency = ServerConfiguration.GetOrUpdateSetting(
"clientVerification.gameTimeLeniency",
TimeSpan.FromHours(25)
);
@ -45,121 +54,154 @@ namespace Server.Misc
{
EventSink.ClientVersionReceived += EventSink_ClientVersionReceived;
if (m_DetectClientRequirement)
if (_detectClientRequirement)
{
var path = Core.FindDataFile("client.exe", false);
if (File.Exists(path))
{
var info = FileVersionInfo.GetVersionInfo(path);
if (info.FileMajorPart != 0 || info.FileMinorPart != 0 || info.FileBuildPart != 0 ||
info.FilePrivatePart != 0)
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var buffer = GC.AllocateUninitializedArray<byte>((int)fs.Length, true);
fs.Read(buffer);
// VS_VERSION_INFO (unicode)
Span<byte> vsVersionInfo = stackalloc byte[]
{
Required = new ClientVersion(
info.FileMajorPart,
info.FileMinorPart,
info.FileBuildPart,
info.FilePrivatePart
);
0x56, 0x00, 0x53, 0x00, 0x5F, 0x00, 0x56, 0x00,
0x45, 0x00, 0x52, 0x00, 0x53, 0x00, 0x49, 0x00,
0x4F, 0x00, 0x4E, 0x00, 0x5F, 0x00, 0x49, 0x00,
0x4E, 0x00, 0x46, 0x00, 0x4F, 0x00
};
for (var i = 0; i < buffer.Length; i++)
{
if (vsVersionInfo.SequenceEqual(buffer.AsSpan(i, 30)))
{
var offset = i + 42; // 30 + 12
var minorPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset));
var majorPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 2));
var privatePart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 4));
var buildPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 6));
MinRequired = new ClientVersion(majorPart, minorPart, buildPart, privatePart);
break;
}
}
}
}
if (Required != null)
if (MinRequired != null || MaxRequired != null)
{
logger.Information(
"Restricting client version to {0}. Action to be taken: {1}",
Required,
m_OldClientResponse
$"Restricting client version to {GetVersionExpression()}. Action to be taken: {_invalidClientResponse}"
);
}
}
private static string GetVersionExpression()
{
if (_versionExpression == null)
{
if (MinRequired != null && MaxRequired != null)
{
_versionExpression = $"{MinRequired}-{MaxRequired}";
}
else if (MinRequired != null)
{
_versionExpression = $"{MinRequired} or newer";
}
else
{
_versionExpression = $"{MaxRequired} or older";
}
}
return _versionExpression;
}
private static void EventSink_ClientVersionReceived(NetState state, ClientVersion version)
{
string kickMessage = null;
using var message = new ValueStringBuilder();
if (state.Mobile?.AccessLevel != AccessLevel.Player)
if (!_enable || state.Mobile?.AccessLevel != AccessLevel.Player)
{
return;
}
if (Required != null && version < Required && (m_OldClientResponse == OldClientResponse.Kick ||
m_OldClientResponse == OldClientResponse.LenientKick &&
Core.Now - state.Mobile.Created > m_AgeLeniency &&
state.Mobile is PlayerMobile mobile &&
mobile.GameTime > m_GameTimeLeniency))
var strictRequirement = _invalidClientResponse == InvalidClientResponse.Kick ||
_invalidClientResponse == InvalidClientResponse.LenientKick &&
Core.Now - state.Mobile.Created > _ageLeniency &&
state.Mobile is PlayerMobile mobile &&
mobile.GameTime > _gameTimeLeniency;
bool shouldKick = false;
if (MinRequired != null && version < MinRequired)
{
kickMessage = $"This server requires your client version be at least {Required}.";
message.Append($"This server doesn't support clients older than {MinRequired}.");
shouldKick = strictRequirement;
}
else if (!AllowGod || !AllowRegular || !AllowUOTD)
else if (MaxRequired != null && version > MaxRequired)
{
if (!AllowGod && version.Type == ClientType.God)
message.Append($"This server doesn't support clients newer than {MaxRequired}.");
shouldKick = strictRequirement;
}
else if (!AllowRegular || !AllowUOTD)
{
if (!AllowRegular && version.Type == ClientType.Regular)
{
kickMessage = "This server does not allow god clients to connect.";
}
else if (!AllowRegular && version.Type == ClientType.Regular)
{
kickMessage = "This server does not allow regular clients to connect.";
message.Append("This server does not allow regular clients to connect.");
shouldKick = true;
}
else if (!AllowUOTD && state.IsUOTDClient)
{
kickMessage = "This server does not allow UO:TD clients to connect.";
message.Append("This server does not allow UO:TD clients to connect.");
shouldKick = true;
}
if (!AllowGod && !AllowRegular && !AllowUOTD)
{
kickMessage = "This server does not allow any clients to connect.";
}
else if (AllowGod && !AllowRegular && !AllowUOTD && version.Type != ClientType.God)
{
kickMessage = "This server requires you to use the god client.";
}
else if (kickMessage != null)
if (message.Length > 0)
{
if (AllowRegular && AllowUOTD)
{
kickMessage += " You can use regular or UO:TD clients.";
message.Append(" You can use regular or UO:TD clients.");
}
else if (AllowRegular)
{
kickMessage += " You can use regular clients.";
message.Append(" You can use regular clients.");
}
else if (AllowUOTD)
{
kickMessage += " You can use UO:TD clients.";
message.Append(" You can use UO:TD clients.");
}
}
}
if (kickMessage != null)
if (message.Length > 0)
{
state.Mobile.SendMessage(0x22, kickMessage);
state.Mobile.SendMessage(0x22, "You will be disconnected in {0} seconds.", KickDelay.TotalSeconds);
Timer.StartTimer(KickDelay, () => OnKick(state));
state.Mobile.SendMessage(0x22, message.ToString());
}
else if (Required != null && version < Required)
if (shouldKick)
{
switch (m_OldClientResponse)
state.Mobile.SendMessage(0x22, "You will be disconnected in {0} seconds.", KickDelay.TotalSeconds);
Timer.StartTimer(KickDelay, () => OnKick(state));
return;
}
if (message.Length > 0)
{
switch (_invalidClientResponse)
{
case OldClientResponse.Warn:
case InvalidClientResponse.Warn:
{
state.Mobile.SendMessage(
0x22,
"Your client is out of date. Please update your client.",
Required
);
state.Mobile.SendMessage(
0x22,
"This server recommends that your client version be at least {0}.",
Required
$"This server recommends that your client version is {GetVersionExpression()}."
);
break;
}
case OldClientResponse.LenientKick:
case OldClientResponse.Annoy:
case InvalidClientResponse.LenientKick:
case InvalidClientResponse.Annoy:
{
SendAnnoyGump(state.Mobile);
break;
@ -170,10 +212,11 @@ namespace Server.Misc
private static void OnKick(NetState ns)
{
if (ns.Connection != null)
if (ns.Running)
{
ns.LogInfo("Disconnecting, bad version");
ns.Disconnect($"Invalid client version {ns.Version}.");
var version = ns.Version;
ns.LogInfo($"Disconnecting, bad version ({version})");
ns.Disconnect($"Invalid client version {version}.");
}
}
@ -181,12 +224,12 @@ namespace Server.Misc
{
from.SendMessage("You will be reminded of this again.");
if (m_OldClientResponse == OldClientResponse.LenientKick)
if (_invalidClientResponse == InvalidClientResponse.LenientKick)
{
from.SendMessage(
"Old clients will be kicked after {0} days of character age and {1} hours of play time",
m_AgeLeniency,
m_GameTimeLeniency
"Invalid clients will be kicked after {0} days of character age and {1} hours of play time",
_ageLeniency,
_gameTimeLeniency
);
}
@ -195,28 +238,29 @@ namespace Server.Misc
private static void SendAnnoyGump(Mobile m)
{
if (m.NetState != null && m.NetState.Version < Required)
if (m.NetState != null)
{
Gump g = new WarningGump(
1060637,
30720,
$"Your client is out of date. Please update your client.<br>This server recommends that your client version be at least {Required}.<br> <br>You are currently using version {m.NetState.Version}.<br> <br>To patch, run UOPatch.exe inside your Ultima Online folder.",
$"Your client is invalid.<br>This server recommends that your client version is {GetVersionExpression()}.<br> <br>You are currently using version {m.NetState.Version}.",
0xFFC000,
480,
360,
okay => KickMessage(m, okay),
false
);
g.Draggable = false;
g.Closable = false;
g.Resizable = false;
)
{
Draggable = false,
Closable = false,
Resizable = false,
};
m.SendGump(g);
}
}
private enum OldClientResponse
private enum InvalidClientResponse
{
Ignore,
Warn,