fix: Fixes stalled connections and infinite throttle (#1796)

> [!Warning]
> **Developer Warning**
> The `PacketThrottle` callback return value is now reversed. `true` indicates the connection is _throttled_.

### Summary
- Fixes an issue where connections get stalled forever
- Fixes an issue where the throttler is not working properly
- Removes account attack limiter
- Rewrites IP limiter
- Removes IP restrictions (they weren't used, and not practical)
- Fixes issue where IP limiter was counting before firewall was blocking.

View without whitespace:
https://github.com/modernuo/ModernUO/pull/1796/files?diff=split&w=1
This commit is contained in:
Kamron Batman 2024-05-27 21:32:55 -07:00 committed by GitHub
parent ccad915464
commit a4522b9d43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1538 additions and 1783 deletions

View file

@ -16,100 +16,103 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices;
namespace Server.Misc;
public static class IPLimiter
{
private static readonly Dictionary<IPAddress, int> _connectionAttempts = new(128);
private static readonly HashSet<IPAddress> _throttledAddresses = new();
private static readonly SortedSet<IPAccessLog> _connectionAttempts = [];
private static readonly SortedSet<IPAccessLog> _throttledAddresses = [];
private static long _lastClearedThrottles;
private static long _lastClearedAttempts;
private static readonly IPAddress _localHost = IPAddress.Parse("127.0.0.1");
public static readonly IPAddress[] Exemptions =
{
IPAddress.Parse( "127.0.0.1" )
};
public static TimeSpan ClearConnectionAttemptsDuration { get; private set; }
public static TimeSpan ClearThrottledDuration { get; private set; }
public static TimeSpan ConnectionAttemptsDuration { get; private set; }
public static TimeSpan ConnectionThrottleDuration { get; private set; }
public static bool Enabled { get; private set; }
public static int MaxAddresses { get; private set; }
public static int MaxConnections { get; private set; }
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10);
ClearConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
ClearThrottledDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearThrottledDuration", TimeSpan.FromMinutes(2));
MaxConnections = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 5);
ConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
ConnectionThrottleDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.connectionThrottleDuration", TimeSpan.FromMinutes(5));
}
public static bool IsExempt(IPAddress ip)
{
for (int i = 0; i < Exemptions.Length; i++)
{
if (ip.Equals(Exemptions[i]))
{
return true;
}
}
return false;
}
private static readonly IPAccessLog _accessCheck = new(IPAddress.None, DateTime.MinValue);
public static bool Verify(IPAddress ourAddress)
{
if (!Enabled || IsExempt(ourAddress))
if (!Enabled || ourAddress.Equals(_localHost))
{
return true;
}
var now = Core.TickCount;
var now = Core.Now;
if (_throttledAddresses.Count > 0)
CheckThrottledAddresses(now);
_accessCheck.IPAddress = ourAddress;
if (_connectionAttempts.TryGetValue(_accessCheck, out var accessLog))
{
if (now - _lastClearedThrottles > ClearThrottledDuration.TotalMilliseconds)
{
_lastClearedThrottles = now;
ClearThrottledAddresses();
}
else if (_throttledAddresses.Contains(ourAddress))
_connectionAttempts.Remove(accessLog);
accessLog.Count++;
if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections)
{
BlockConnection(now, accessLog);
return false;
}
}
if (_connectionAttempts.Count > 0 && now - _lastClearedAttempts > ClearConnectionAttemptsDuration.TotalMilliseconds)
accessLog.Expiration = now + ConnectionAttemptsDuration;
}
else
{
_lastClearedAttempts = now;
ClearConnectionAttempts();
accessLog = new IPAccessLog(ourAddress, now + ConnectionAttemptsDuration);
}
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_connectionAttempts, ourAddress, out _);
count++;
if (count > MaxAddresses)
{
_connectionAttempts.Remove(ourAddress);
_throttledAddresses.Add(ourAddress);
return false;
}
// Add it back so it is sorted properly
_connectionAttempts.Add(accessLog);
return true;
}
private static void ClearThrottledAddresses()
private static void BlockConnection(DateTime now, IPAccessLog accessLog)
{
_throttledAddresses.Clear();
accessLog.Expiration = now + ConnectionAttemptsDuration;
_throttledAddresses.Add(accessLog);
}
private static void ClearConnectionAttempts()
private static void CheckThrottledAddresses(DateTime now)
{
_connectionAttempts.Clear();
_connectionAttempts.TrimExcess(128);
while (_throttledAddresses.Count > 0)
{
var accessLog = _throttledAddresses.Min;
if (now <= accessLog.Expiration)
{
break;
}
_throttledAddresses.Remove(accessLog);
}
}
private class IPAccessLog : IComparable<IPAccessLog>
{
public IPAddress IPAddress;
public DateTime Expiration;
public int Count;
public IPAccessLog(IPAddress ipAddress, DateTime expiration)
{
IPAddress = ipAddress;
Expiration = expiration;
Count = 1;
}
public int CompareTo(IPAccessLog other) =>
IPAddress.Equals(other.IPAddress) ? 0 : Expiration.CompareTo(other.Expiration);
}
}

View file

@ -33,15 +33,13 @@ public static class MovementThrottle
IncomingPackets.RegisterThrottler(0x02, &Throttle);
}
public static bool Throttle(int packetId, NetState ns, out bool drop)
public static bool Throttle(int packetId, NetState ns)
{
drop = false;
var from = ns.Mobile;
if (from?.Deleted != false || from.AccessLevel > AccessLevel.Player)
{
return true;
return false;
}
long now = Core.TickCount;
@ -53,7 +51,7 @@ public static class MovementThrottle
{
ns._movementCredit = 0;
ns._nextMovementTime = now;
return true;
return false;
}
long cost = nextMove - now;
@ -61,11 +59,11 @@ public static class MovementThrottle
if (credit < cost)
{
// Not enough credit, therefore throttled
return false;
return true;
}
// On the next event loop, the player receives up to 400ms in grace latency
ns._movementCredit = Math.Min(_throttleThreshold, credit - cost);
return true;
return false;
}
}

View file

@ -823,9 +823,9 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
var throttler = handler.ThrottleCallback;
if (throttler != null)
{
if (!throttler(packetId, this, out bool drop))
if (throttler(packetId, this))
{
return drop ? ParserState.Throttled : ParserState.AwaitingNextPacket;
return ParserState.Throttled;
}
SetPacketTime(packetId);

View file

@ -35,7 +35,7 @@ public unsafe class PacketHandler
public delegate*<NetState, SpanReader, void> OnReceive { get; }
public delegate*<int, NetState, out bool, bool> ThrottleCallback { get; set; }
public delegate*<int, NetState, bool> ThrottleCallback { get; set; }
public bool Ingame { get; }
}

View file

@ -57,7 +57,7 @@ public static class IncomingPackets
}
}
public static unsafe void RegisterThrottler(int packetID, delegate*<int, NetState, out bool, bool> t)
public static unsafe void RegisterThrottler(int packetID, delegate*<int, NetState, bool> t)
{
var ph = GetHandler(packetID);

View file

@ -223,7 +223,6 @@ public static class TcpServer
private static void ProcessConnection(Socket socket)
{
var ipLimiter = IPLimiter.Enabled;
try
{
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
@ -247,15 +246,6 @@ public static class TcpServer
return;
}
if (ipLimiter && !IPLimiter.Verify(remoteIP))
{
TraceDisconnect("Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
CloseSocket(socket);
return;
}
var firewalled = Firewall.IsBlocked(remoteIP);
if (!firewalled)
{
@ -273,6 +263,15 @@ public static class TcpServer
return;
}
if (!IPLimiter.Verify(remoteIP))
{
TraceDisconnect("Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
CloseSocket(socket);
return;
}
var ns = new NetState(socket);
ConnectedQueue.Enqueue(ns);
}

View file

@ -3,121 +3,136 @@ using System.IO;
using System.Net;
using Server.Accounting.Security;
namespace Server.Accounting
namespace Server.Accounting;
public partial class Account
{
public partial class Account
// Deleted IP Restrictions
private void MigrateFrom(V5Content content)
{
// Username was not interned
private void MigrateFrom(V4Content content)
_username = content.Username;
_passwordAlgorithm = content.PasswordAlgorithm;
_password = content.Password;
_accessLevel = content.AccessLevel;
_flags = content.Flags;
_lastLogin = content.LastLogin;
_totalGold = content.TotalGold;
_totalPlat = content.TotalPlat;
_mobiles = content.Mobiles;
_comments = content.Comments;
_tags = content.Tags;
_loginIPs = content.LoginIPs;
_totalGameTime = content.TotalGameTime;
_email = content.Email;
}
// Username was not interned
private void MigrateFrom(V4Content content)
{
_username = content.Username;
_passwordAlgorithm = content.PasswordAlgorithm;
_password = content.Password;
_accessLevel = content.AccessLevel;
_flags = content.Flags;
_lastLogin = content.LastLogin;
_totalGold = content.TotalGold;
_totalPlat = content.TotalPlat;
_mobiles = content.Mobiles;
_comments = content.Comments;
_tags = content.Tags;
_loginIPs = content.LoginIPs;
_totalGameTime = content.TotalGameTime;
_email = content.Email;
}
private void MigrateFrom(V3Content content)
{
_username = content.Username;
_username.Intern();
_passwordAlgorithm = content.PasswordAlgorithm;
_password = content.Password;
_accessLevel = content.AccessLevel;
_flags = content.Flags;
Created = content.Created;
_lastLogin = content.LastLogin;
_totalGold = content.TotalGold;
_totalPlat = content.TotalPlat;
_mobiles = content.Mobiles;
_comments = content.Comments;
_tags = content.Tags;
_loginIPs = content.LoginIPs;
_totalGameTime = content.TotalGameTime;
_email = content.Email;
}
private void Deserialize(IGenericReader reader, int version)
{
if (version != 2)
{
_username = content.Username;
_passwordAlgorithm = content.PasswordAlgorithm;
_password = content.Password;
_accessLevel = content.AccessLevel;
_flags = content.Flags;
_lastLogin = content.LastLogin;
_totalGold = content.TotalGold;
_totalPlat = content.TotalPlat;
_mobiles = content.Mobiles;
_comments = content.Comments;
_tags = content.Tags;
_loginIPs = content.LoginIPs;
_ipRestrictions = content.IpRestrictions;
_totalGameTime = content.TotalGameTime;
_email = content.Email;
// Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing
reader.Seek(0, SeekOrigin.Begin);
}
private void MigrateFrom(V3Content content)
_username = reader.ReadString(true);
_passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum<PasswordProtectionAlgorithm>();
_password = reader.ReadString();
_accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum<AccessLevel>();
_flags = reader.ReadInt();
Created = reader.ReadDateTime();
_lastLogin = reader.ReadDateTime();
_totalGold = reader.ReadInt();
_totalPlat = reader.ReadInt();
var length = reader.ReadInt();
_mobiles = new Mobile[length];
for (int i = 0; i < length; i++)
{
_username = content.Username;
_username.Intern();
_passwordAlgorithm = content.PasswordAlgorithm;
_password = content.Password;
_accessLevel = content.AccessLevel;
_flags = content.Flags;
Created = content.Created;
_lastLogin = content.LastLogin;
_totalGold = content.TotalGold;
_totalPlat = content.TotalPlat;
_mobiles = content.Mobiles;
_comments = content.Comments;
_tags = content.Tags;
_loginIPs = content.LoginIPs;
_ipRestrictions = content.IpRestrictions;
_totalGameTime = content.TotalGameTime;
_email = content.Email;
_mobiles[i] = reader.ReadEntity<Mobile>();
}
private void Deserialize(IGenericReader reader, int version)
length = reader.ReadInt();
_comments = length > 0 ? new List<AccountComment>(length) : null;
for (int i = 0; i < length; i++)
{
if (version != 2)
_comments!.Add(new AccountComment(reader));
}
length = reader.ReadInt();
_tags = length > 0 ? new List<AccountTag>(length) : null;
for (int i = 0; i < length; i++)
{
_tags!.Add(new AccountTag(reader));
}
length = reader.ReadInt();
_loginIPs = new IPAddress[length];
for (int i = 0; i < length; i++)
{
if (version < 2)
{
// Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing
reader.Seek(0, SeekOrigin.Begin);
}
_username = reader.ReadString(true);
_passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum<PasswordProtectionAlgorithm>();
_password = reader.ReadString();
_accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum<AccessLevel>();
_flags = reader.ReadInt();
Created = reader.ReadDateTime();
_lastLogin = reader.ReadDateTime();
_totalGold = reader.ReadInt();
_totalPlat = reader.ReadInt();
var length = reader.ReadInt();
_mobiles = new Mobile[length];
for (int i = 0; i < length; i++)
{
_mobiles[i] = reader.ReadEntity<Mobile>();
}
length = reader.ReadInt();
_comments = length > 0 ? new List<AccountComment>(length) : null;
for (int i = 0; i < length; i++)
{
_comments!.Add(new AccountComment(reader));
}
length = reader.ReadInt();
_tags = length > 0 ? new List<AccountTag>(length) : null;
for (int i = 0; i < length; i++)
{
_tags!.Add(new AccountTag(reader));
}
length = reader.ReadInt();
_loginIPs = new IPAddress[length];
for (int i = 0; i < length; i++)
{
if (version < 2)
if (IPAddress.TryParse(reader.ReadString(), out var address))
{
if (IPAddress.TryParse(reader.ReadString(), out var address))
{
_loginIPs[i] = Utility.Intern(address);
}
}
else
{
_loginIPs[i] = reader.ReadIPAddress();
_loginIPs[i] = Utility.Intern(address);
}
}
length = reader.ReadInt();
_ipRestrictions = new string[length];
for (int i = 0; i < length; i++)
else
{
_ipRestrictions[i] = reader.ReadString();
_loginIPs[i] = reader.ReadIPAddress();
}
}
_totalGameTime = reader.ReadTimeSpan();
length = reader.ReadInt();
for (int i = 0; i < length; i++)
{
reader.ReadString(); // IP Restrictions
}
if (version > 1)
{
_email = reader.ReadString();
}
_totalGameTime = reader.ReadTimeSpan();
if (version > 1)
{
_email = reader.ReadString();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,163 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Server.Network;
namespace Server.Accounting
{
public static class AccountAttackLimiter
{
public static bool Enabled;
private static readonly List<InvalidAccountAccessLog> m_List = new();
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("accountAttackLimiter.enable", true);
}
public static unsafe void Initialize()
{
if (!Enabled)
{
return;
}
IncomingPackets.RegisterThrottler(0x80, &Throttle);
IncomingPackets.RegisterThrottler(0x91, &Throttle);
IncomingPackets.RegisterThrottler(0xCF, &Throttle);
}
public static bool Throttle(int packetId, NetState ns, out bool drop)
{
var accessLog = FindAccessLog(ns);
if (accessLog == null)
{
drop = false;
return true;
}
var date = Core.Now;
var access = accessLog.LastAccessTime + ComputeThrottle(accessLog.Counts);
var allow = date >= access;
drop = !allow;
return allow;
}
public static InvalidAccountAccessLog FindAccessLog(NetState ns)
{
if (ns == null)
{
return null;
}
var ipAddress = ns.Address;
for (var i = 0; i < m_List.Count; ++i)
{
var accessLog = m_List[i];
if (accessLog.HasExpired)
{
m_List.RemoveAt(i--);
}
else if (accessLog.Address.Equals(ipAddress))
{
return accessLog;
}
}
return null;
}
public static void RegisterInvalidAccess(NetState ns)
{
if (ns == null || !Enabled)
{
return;
}
var accessLog = FindAccessLog(ns);
if (accessLog == null)
{
m_List.Add(accessLog = new InvalidAccountAccessLog(ns.Address));
}
accessLog.Counts += 1;
accessLog.RefreshAccessTime();
if (accessLog.Counts >= 3)
{
try
{
using var op = new StreamWriter("throttle.log", true);
op.WriteLine(
"{0}\t{1}\t{2}",
Core.Now,
ns,
accessLog.Counts
);
}
catch
{
// ignored
}
}
}
public static TimeSpan ComputeThrottle(int counts)
{
if (counts >= 15)
{
return TimeSpan.FromMinutes(5.0);
}
if (counts >= 10)
{
return TimeSpan.FromMinutes(1.0);
}
if (counts >= 5)
{
return TimeSpan.FromSeconds(20.0);
}
if (counts >= 3)
{
return TimeSpan.FromSeconds(10.0);
}
if (counts >= 1)
{
return TimeSpan.FromSeconds(2.0);
}
return TimeSpan.Zero;
}
}
public class InvalidAccountAccessLog
{
public InvalidAccountAccessLog(IPAddress address)
{
Address = address;
RefreshAccessTime();
}
public IPAddress Address { get; set; }
public DateTime LastAccessTime { get; set; }
public bool HasExpired => Core.Now >= LastAccessTime + TimeSpan.FromHours(1.0);
public int Counts { get; set; }
public void RefreshAccessTime()
{
LastAccessTime = Core.Now;
}
}
}

View file

@ -369,11 +369,6 @@ public static class AccountHandler
acct.LogAccess(e.State);
}
if (!e.Accepted)
{
AccountAttackLimiter.RegisterInvalidAccess(e.State);
}
}
public static void EventSink_GameLogin(GameLoginEventArgs e)
@ -409,11 +404,6 @@ public static class AccountHandler
e.Accepted = true;
e.CityInfo = CharacterCreation.GetStartingCities(acct.Young);
}
if (!e.Accepted)
{
AccountAttackLimiter.RegisterInvalidAccess(e.State);
}
}
public static bool CheckAccount(Mobile mobCheck, Mobile accCheck)

View file

@ -36,7 +36,6 @@ namespace Server.Gumps
AccountDetails_Characters,
AccountDetails_Access,
AccountDetails_Access_ClientIPs,
AccountDetails_Access_Restrictions,
AccountDetails_Comments,
AccountDetails_Tags,
AccountDetails_ChangePassword,
@ -132,7 +131,6 @@ namespace Server.Gumps
AdminGumpPage.AccountDetails_Characters,
AdminGumpPage.AccountDetails_Access,
AdminGumpPage.AccountDetails_Access_ClientIPs,
AdminGumpPage.AccountDetails_Access_Restrictions,
AdminGumpPage.AccountDetails_Comments,
AdminGumpPage.AccountDetails_Tags,
AdminGumpPage.AccountDetails_ChangeAccess,
@ -801,8 +799,7 @@ namespace Server.Gumps
GetButtonID(5, 13),
"Access",
AdminGumpPage.AccountDetails_Access,
AdminGumpPage.AccountDetails_Access_ClientIPs,
AdminGumpPage.AccountDetails_Access_Restrictions
AdminGumpPage.AccountDetails_Access_ClientIPs
);
AddPageButton(190, 70, GetButtonID(5, 2), "Comments", AdminGumpPage.AccountDetails_Comments);
AddPageButton(190, 90, GetButtonID(5, 3), "Tags", AdminGumpPage.AccountDetails_Tags);
@ -968,13 +965,6 @@ namespace Server.Gumps
"View client addresses",
AdminGumpPage.AccountDetails_Access_ClientIPs
);
AddPageButton(
20,
170,
GetButtonID(5, 15),
"Manage restrictions",
AdminGumpPage.AccountDetails_Access_Restrictions
);
goto case AdminGumpPage.AccountDetails;
}
@ -1038,67 +1028,6 @@ namespace Server.Gumps
AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(10, index));
}
goto case AdminGumpPage.AccountDetails_Access;
}
case AdminGumpPage.AccountDetails_Access_Restrictions:
{
if (state is not Account a)
{
break;
}
m_List ??= a.IpRestrictions.ToList<object>();
AddHtml(10, 195, 400, 20, "Address Restrictions".Center(LabelColor32));
AddTextField(227, 225, 120, 20, 0);
AddButtonLabeled(352, 225, GetButtonID(5, 19), "Add");
AddHtml(
225,
255,
180,
120,
"Any clients connecting from an address not in this list will be rejected. Or, if the list is empty, any client may connect.".Color(
LabelColor32
)
);
AddImageTiled(15, 219, 206, 156, 0xBBC);
AddBlackAlpha(16, 220, 204, 154);
AddHtml(18, 221, 114, 20, "IP Address".Color(LabelColor32));
if (listPage > 0)
{
AddButton(184, 223, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(184, 223, 0x25EA);
}
if ((listPage + 1) * 6 < m_List.Count)
{
AddButton(201, 223, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(201, 223, 0x25E6);
}
if (m_List.Count == 0)
{
AddHtml(18, 243, 200, 60, "There are no addresses in this list.".Color(LabelColor32));
}
for (int i = 0, index = listPage * 6; i < 6 && index >= 0 && index < m_List.Count; ++i, ++index)
{
AddHtml(18, 243 + i * 22, 114, 20, ((string)m_List[index]).Color(LabelColor32));
AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(8, index));
}
goto case AdminGumpPage.AccountDetails_Access;
}
case AdminGumpPage.AccountDetails_Characters:
@ -2733,20 +2662,6 @@ namespace Server.Gumps
);
break;
}
case 15:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_Restrictions,
0,
null,
null,
m_State
)
);
break;
}
case 4:
{
from.Prompt = new AddCommentPrompt(m_State as Account);
@ -3101,65 +3016,6 @@ namespace Server.Gumps
);
}
break;
}
case 19: // add
{
if (m_State is not Account a)
{
break;
}
var ip = info.GetTextEntry(0)?.Trim();
string notice;
if (string.IsNullOrEmpty(ip))
{
notice = "You must enter an address to add.";
}
else
{
var list = a.IpRestrictions;
var contains = false;
for (var i = 0; !contains && i < list.Length; ++i)
{
contains = list[i] == ip;
}
if (contains)
{
notice = "That address is already contained in the list.";
}
else
{
var newList = new string[list.Length + 1];
for (var i = 0; i < list.Length; ++i)
{
newList[i] = list[i];
}
newList[list.Length] = ip;
a.IpRestrictions = newList;
notice = $"{ip} : Added to restriction list.";
}
}
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_Restrictions,
0,
null,
notice,
m_State
)
);
break;
}
case 20: // Change access level
@ -3957,23 +3813,6 @@ namespace Server.Gumps
)
);
}
else if (m_PageType == AdminGumpPage.AccountDetails_Access_Restrictions)
{
var list = a.IpRestrictions.ToList();
list.Remove(m_List[index] as string);
a.IpRestrictions = list.ToArray();
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_Restrictions,
0,
null,
$"{m_List[index]} : Removed from list.",
a
)
);
}
}
break;

View file

@ -0,0 +1,115 @@
{
"version": 6,
"type": "Server.Accounting.Account",
"properties": [
{
"name": "Username",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"InternString"
]
},
{
"name": "PasswordAlgorithm",
"type": "Server.Accounting.Security.PasswordProtectionAlgorithm",
"rule": "EnumMigrationRule"
},
{
"name": "Password",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "AccessLevel",
"type": "Server.AccessLevel",
"rule": "EnumMigrationRule"
},
{
"name": "Flags",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "LastLogin",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "TotalGold",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "TotalPlat",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Mobiles",
"type": "Server.Mobile[]",
"rule": "ArrayMigrationRule",
"ruleArguments": [
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Comments",
"type": "System.Collections.Generic.List\u003CServer.Accounting.AccountComment\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Accounting.AccountComment",
"SerializationMethodSignatureMigrationRule",
""
]
},
{
"name": "Tags",
"type": "System.Collections.Generic.List\u003CServer.Accounting.AccountTag\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Accounting.AccountTag",
"SerializationMethodSignatureMigrationRule",
""
]
},
{
"name": "LoginIPs",
"type": "System.Net.IPAddress[]",
"rule": "ArrayMigrationRule",
"ruleArguments": [
"System.Net.IPAddress",
"PrimitiveTypeMigrationRule"
]
},
{
"name": "TotalGameTime",
"type": "System.TimeSpan",
"rule": "PrimitiveTypeMigrationRule"
},
{
"name": "Email",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -2,147 +2,140 @@ using System;
using System.Collections.Generic;
using System.IO;
using Server.Json;
using Server.Mobiles;
namespace Server.Network
namespace Server.Network;
public static class PacketThrottles
{
public static class PacketThrottles
// Delay in milliseconds
private static readonly int[] Delays = new int[0x100];
private const string ThrottlesConfiguration = "Configuration/throttles.json";
public static void Configure()
{
// Delay in milliseconds
private static readonly int[] Delays = new int[0x100];
private const string ThrottlesConfiguration = "Configuration/throttles.json";
public static void Configure()
{
CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle);
CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle);
}
public static unsafe void Initialize()
{
var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration);
if (File.Exists(path))
{
var throttles = JsonConfig.Deserialize<SortedDictionary<string, int>>(path);
foreach (var (k, v) in throttles)
{
if (!Utility.ToInt32(k, out var packetId))
{
Utility.PushColor(ConsoleColor.DarkYellow);
Console.WriteLine("Packet Throttles: Error deserializing {0} from {1}", k, ThrottlesConfiguration);
Utility.PopColor();
continue;
}
Delays[packetId] = v;
}
}
else
{
Delays[0x03] = 25; // Speech
Delays[0xAD] = 25; // Speech
Delays[0x12] = 25; // Text Commands
Delays[0x75] = 500; // Rename request
}
for (int i = 0; i < 0x100; i++)
{
if (Delays[i] > 0)
{
IncomingPackets.RegisterThrottler(i, &Throttle);
}
}
SaveDelays();
}
[Usage("GetThrottle <packetID>")]
[Description("Gets throttle for the given packet.")]
public static void GetThrottle(CommandEventArgs e)
{
if (e.Length != 1)
{
e.Mobile.SendMessage("Invalid Command Format. Should be [GetThrottle <packetID>");
return;
}
int packetID = e.GetInt32(0);
if (packetID is < 0 or > 0x100)
{
e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100.");
return;
}
e.Mobile.SendMessage($"Packet 0x{packetID:X} throttle is currently {Delays[packetID]}ms.");
}
[Usage("SetThrottle <packetID> <timeInMilliseconds>")]
[Description("Sets a throttle for the given packet.")]
public static unsafe void SetThrottle(CommandEventArgs e)
{
if (e.Length != 2)
{
e.Mobile.SendMessage("Invalid Command Format. Should be [SetThrottle <packetID> <timeInMilliseconds>");
return;
}
int packetID = e.GetInt32(0);
int delay = e.GetInt32(1);
if (packetID is < 0 or > 0x100)
{
e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100.");
return;
}
if (delay > 5000)
{
e.Mobile.SendMessage("Invalid Command Format. Delay cannot exceed 5000 milliseconds.");
return;
}
long oldDelay = Delays[packetID];
if (oldDelay == 0 && delay > 0)
{
IncomingPackets.RegisterThrottler(packetID, &Throttle);
e.Mobile.SendMessage($"Set throttle for packet 0x{packetID:X2} to {delay}ms.");
}
else if (oldDelay > 0 && delay == 0)
{
IncomingPackets.RegisterThrottler(packetID, null);
e.Mobile.SendMessage($"Removed throttle for packet 0x{packetID:X2}");
}
Delays[packetID] = delay;
SaveDelays();
}
private static void SaveDelays()
{
SortedDictionary<string, int> table = new();
for (var i = 0; i < Delays.Length; i++)
{
var delay = Delays[i];
if (delay != 0)
{
table[$"0x{i:X2}"] = delay;
}
}
var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration);
JsonConfig.Serialize(path, table);
}
public static bool Throttle(int packetID, NetState ns, out bool drop)
{
drop = ns.Mobile is PlayerMobile { AccessLevel: < AccessLevel.Counselor }
&& Core.TickCount - ns.GetPacketTime(packetID) < Delays[packetID];
return !drop;
}
CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle);
CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle);
}
public static unsafe void Initialize()
{
var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration);
if (File.Exists(path))
{
var throttles = JsonConfig.Deserialize<SortedDictionary<string, int>>(path);
foreach (var (k, v) in throttles)
{
if (!Utility.ToInt32(k, out var packetId))
{
Utility.PushColor(ConsoleColor.DarkYellow);
Console.WriteLine("Packet Throttles: Error deserializing {0} from {1}", k, ThrottlesConfiguration);
Utility.PopColor();
continue;
}
Delays[packetId] = v;
}
}
else
{
Delays[0x03] = 25; // Speech
Delays[0xAD] = 25; // Speech
Delays[0x12] = 25; // Text Commands
Delays[0x75] = 500; // Rename request
}
for (int i = 0; i < 0x100; i++)
{
if (Delays[i] > 0)
{
IncomingPackets.RegisterThrottler(i, &Throttle);
}
}
SaveDelays();
}
[Usage("GetThrottle <packetID>")]
[Description("Gets throttle for the given packet.")]
public static void GetThrottle(CommandEventArgs e)
{
if (e.Length != 1)
{
e.Mobile.SendMessage("Invalid Command Format. Should be [GetThrottle <packetID>");
return;
}
int packetID = e.GetInt32(0);
if (packetID is < 0 or > 0x100)
{
e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100.");
return;
}
e.Mobile.SendMessage($"Packet 0x{packetID:X} throttle is currently {Delays[packetID]}ms.");
}
[Usage("SetThrottle <packetID> <timeInMilliseconds>")]
[Description("Sets a throttle for the given packet.")]
public static unsafe void SetThrottle(CommandEventArgs e)
{
if (e.Length != 2)
{
e.Mobile.SendMessage("Invalid Command Format. Should be [SetThrottle <packetID> <timeInMilliseconds>");
return;
}
int packetID = e.GetInt32(0);
int delay = e.GetInt32(1);
if (packetID is < 0 or > 0x100)
{
e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100.");
return;
}
if (delay > 5000)
{
e.Mobile.SendMessage("Invalid Command Format. Delay cannot exceed 5000 milliseconds.");
return;
}
long oldDelay = Delays[packetID];
if (oldDelay == 0 && delay > 0)
{
IncomingPackets.RegisterThrottler(packetID, &Throttle);
e.Mobile.SendMessage($"Set throttle for packet 0x{packetID:X2} to {delay}ms.");
}
else if (oldDelay > 0 && delay == 0)
{
IncomingPackets.RegisterThrottler(packetID, null);
e.Mobile.SendMessage($"Removed throttle for packet 0x{packetID:X2}");
}
Delays[packetID] = delay;
SaveDelays();
}
private static void SaveDelays()
{
SortedDictionary<string, int> table = new();
for (var i = 0; i < Delays.Length; i++)
{
var delay = Delays[i];
if (delay != 0)
{
table[$"0x{i:X2}"] = delay;
}
}
var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration);
JsonConfig.Serialize(path, table);
}
public static bool Throttle(int packetId, NetState ns) =>
ns.Mobile is { AccessLevel: AccessLevel.Player } && Core.TickCount - ns.GetPacketTime(packetId) < Delays[packetId];
}