diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 964eee375..216c8ab6c 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -515,6 +515,7 @@ public static class Core AssemblyHandler.Invoke("Initialize"); TcpServer.Start(); + PingServer.Start(); EventSink.InvokeServerStarted(); _firstTick = TickCount; RunEventLoop(); @@ -548,6 +549,7 @@ public static class Core // Handle networking TcpServer.Slice(); NetState.Slice(); + PingServer.Slice(); // Execute captured post-await methods (like Timer.Pause) LoopContext.ExecuteTasks(); diff --git a/Projects/Server/Network/PingServer.cs b/Projects/Server/Network/PingServer.cs new file mode 100644 index 000000000..ba28c1dc7 --- /dev/null +++ b/Projects/Server/Network/PingServer.cs @@ -0,0 +1,178 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using Server.Logging; + +namespace Server.Network; + +public static class PingServer +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(PingServer)); + + private const int MaxConnectionsPerLoop = 250; + + private const long _listenerErrorMessageDelay = 10000; // 10 seconds + private static long _nextMaximumSocketsReachedMessage; + + public static int MaxConnections { get; set; } + + private static ConcurrentQueue<(UdpClient, UdpReceiveResult)> _udpResponseQueue = new(); + + public static UdpClient[] Listeners { get; private set; } + + public static bool Enabled { get; private set; } + + public static int Port { get; private set; } + + public static void Configure() + { + Enabled = ServerConfiguration.GetOrUpdateSetting("pingServer.enabled", true); + Port = ServerConfiguration.GetSetting("pingServer.port", 12000); + MaxConnections = ServerConfiguration.GetSetting("pingServer.maxConnections", 2048); + } + + public static void Start() + { + if (!Enabled) + { + return; + } + + HashSet listeningAddresses = new HashSet(); + List listeners = new List(); + + foreach (var serverIpep in ServerConfiguration.Listeners) + { + var ipep = new IPEndPoint(serverIpep.Address, Port); + + var listener = CreateListener(ipep); + if (listener == null) + { + continue; + } + + if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) + { + listeningAddresses.UnionWith(TcpServer.GetListeningAddresses(ipep)); + } + else + { + listeningAddresses.Add(ipep); + } + + listeners.Add(listener); + BeginAcceptingSockets(listener); + } + + foreach (var ipep in listeningAddresses) + { + logger.Information("Listening: {Address}:{Port} (Pings)", ipep.Address, ipep.Port); + } + + Listeners = listeners.ToArray(); + } + + public static void Slice() + { + if (!Enabled || Core.Closing) + { + return; + } + + int count = 0; + + while (++count <= MaxConnectionsPerLoop && _udpResponseQueue.TryDequeue(out var udpTuple)) + { + var (listener, result) = udpTuple; + SendResponse(listener, result.Buffer, result.RemoteEndPoint); + } + } + + public static UdpClient CreateListener(IPEndPoint ipep) + { + var listener = new Socket(ipep.AddressFamily, SocketType.Dgram, ProtocolType.Udp) + { + ExclusiveAddressUse = false + }; + + try + { + listener.Bind(ipep); + + return new UdpClient + { + Client = listener + }; + } + catch (SocketException se) + { + // WSAEADDRINUSE + if (se.ErrorCode == 10048) + { + logger.Warning("Ping Listener: {Address}:{Port}: Failed (In Use)", ipep.Address, ipep.Port); + } + // WSAEADDRNOTAVAIL + else if (se.ErrorCode == 10049) + { + logger.Warning("Ping Listener {Address}:{Port}: Failed (Unavailable)", ipep.Address, ipep.Port); + } + else + { + logger.Warning(se, "Ping Listener Exception:"); + } + } + + return null; + } + + private static async void BeginAcceptingSockets(UdpClient listener) + { + while (true) + { + if (!Enabled || Core.Closing) + { + return; + } + + try + { + var result = await listener.ReceiveAsync(Core.ClosingTokenSource.Token); + + if (_udpResponseQueue.Count >= MaxConnections) + { + var ticks = Core.TickCount; + + if (ticks - _nextMaximumSocketsReachedMessage > 0) + { + if (listener.Client.RemoteEndPoint is IPEndPoint ipep) + { + var ip = ipep.Address.ToString(); + logger.Warning("Ping Listener {Address}: Failed (Maximum connections reached)", ip); + } + + _nextMaximumSocketsReachedMessage = ticks + _listenerErrorMessageDelay; + } + } + + _udpResponseQueue.Enqueue((listener, result)); + } + catch + { + // ignored + } + } + } + + private static async void SendResponse(UdpClient listener, byte[] data, IPEndPoint ipep) + { + try + { + await listener.SendAsync(data, ipep, Core.ClosingTokenSource.Token); + } + catch + { + // ignored + } + } +} diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index f5364d708..40a4eb5a9 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -30,7 +30,7 @@ public static class TcpServer private const int MaxConnectionsPerLoop = 250; // Sanity. 256 * 1024 * 4096 = ~1.3GB of ram - public static int MaxConnections { get; set; } = 4096; + public static int MaxConnections { get; set; } private const long _listenerErrorMessageDelay = 10000; // 10 seconds private static long _nextMaximumSocketsReachedMessage; @@ -46,7 +46,7 @@ public static class TcpServer public static void Configure() { - MaxConnections = ServerConfiguration.GetOrUpdateSetting("tcpServer.maxConnections", MaxConnections); + MaxConnections = ServerConfiguration.GetOrUpdateSetting("tcpServer.maxConnections", 4096); } public static void Start() @@ -72,7 +72,7 @@ public static class TcpServer } listeners.Add(listener); - listener.BeginAcceptingSockets(); + BeginAcceptingSockets(listener); } foreach (var ipep in listeningAddresses) @@ -149,7 +149,7 @@ public static class TcpServer } } - private static async void BeginAcceptingSockets(this Socket listener) + private static async void BeginAcceptingSockets(Socket listener) { while (true) { diff --git a/Projects/UOContent/Misc/ServerList.cs b/Projects/UOContent/Misc/ServerList.cs index ab02ba343..1b60f14d9 100644 --- a/Projects/UOContent/Misc/ServerList.cs +++ b/Projects/UOContent/Misc/ServerList.cs @@ -181,9 +181,9 @@ namespace Server.Misc var ipAddress = hc.GetStringAsync(_ipifyUrl).Result; return IPAddress.Parse(ipAddress); } - catch (Exception e) + catch { - logger.Error(e, "Failed to get public IP address."); + // ignored } }