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:
parent
fcc91cbd99
commit
1ac803e778
4 changed files with 158 additions and 113 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue