/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: TcpServer.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Server.Logging;
using Server.Misc;
namespace Server.Network;
public static class TcpServer
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(TcpServer));
private const long MaximumSocketIdleDelay = 2000; // 2 seconds
private const long ListenerErrorMessageDelay = 10000; // 10 seconds
private static long _nextMaximumSocketsReachedMessage;
private static readonly SemaphoreSlim _queueSemaphore = new(0);
private static readonly ConcurrentQueue _connectingQueue = [];
private static Thread _processConnectionsThread;
// Sanity. 256 * 1024 * 4096 = ~1.3GB of ram
public static int MaxConnections { get; set; }
public static IPEndPoint[] ListeningAddresses { get; private set; }
public static Socket[] Listeners { get; private set; }
// By default should sort T1 then T2
public static readonly SortedSet<(long ConnectedAt, NetState NetState)> _socketsConnecting = [];
public static ConcurrentQueue ConnectedQueue { get; } = [];
public static void Configure()
{
MaxConnections = ServerConfiguration.GetOrUpdateSetting("tcpServer.maxConnections", 4096);
}
public static void Start()
{
_processConnectionsThread = new Thread(ProcessConnections);
_processConnectionsThread.Start();
}
public static IEnumerable GetListeningAddresses(IPEndPoint ipep) =>
NetworkInterface.GetAllNetworkInterfaces().SelectMany(adapter =>
adapter.GetIPProperties().UnicastAddresses
.Where(uip => ipep.AddressFamily == uip.Address.AddressFamily)
.Select(uip => new IPEndPoint(uip.Address, ipep.Port))
);
public static Socket CreateListener(IPEndPoint ipep)
{
var listener = new Socket(ipep.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
LingerState = new LingerOption(false, 0),
ExclusiveAddressUse = true,
NoDelay = true,
Blocking = false,
SendBufferSize = 64 * 1024,
ReceiveBufferSize = 64 * 1024
};
try
{
listener.Bind(ipep);
listener.Listen(256);
return listener;
}
catch (SocketException se)
{
// WSAEADDRINUSE
if (se.ErrorCode == 10048)
{
logger.Warning("Listener: {Address}:{Port}: Failed (In Use)", ipep.Address, ipep.Port);
}
// WSAEADDRNOTAVAIL
else if (se.ErrorCode == 10049)
{
logger.Warning("Listener {Address}:{Port}: Failed (Unavailable)", ipep.Address, ipep.Port);
}
else
{
logger.Warning(se, "Listener Exception:");
}
}
return null;
}
private static async Task BeginAcceptingSockets(Socket listener)
{
var cancellationToken = Core.ClosingTokenSource.Token;
try
{
var socket = await listener.AcceptAsync(cancellationToken);
_connectingQueue.Enqueue(socket);
_queueSemaphore.Release();
}
catch(OperationCanceledException)
{
return;
}
if (cancellationToken.IsCancellationRequested)
{
listener.Close();
return;
}
Task.Run(() => BeginAcceptingSockets(listener), cancellationToken).ConfigureAwait(false);
}
private static void ProcessConnections()
{
var cancellationToken = Core.ClosingTokenSource.Token;
HashSet listeningAddresses = [];
List listeners = [];
foreach (var ipep in ServerConfiguration.Listeners)
{
var listener = CreateListener(ipep);
if (listener == null)
{
continue;
}
bool added;
if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any))
{
var beforeCount = listeningAddresses.Count;
listeningAddresses.UnionWith(GetListeningAddresses(ipep));
added = listeningAddresses.Count > beforeCount;
}
else
{
added = listeningAddresses.Add(ipep);
}
if (added)
{
listeners.Add(listener);
Task.Run(() => BeginAcceptingSockets(listener), cancellationToken).ConfigureAwait(false);
}
}
foreach (var ipep in listeningAddresses)
{
logger.Information("Listening: {Address}:{Port}", ipep.Address, ipep.Port);
}
ListeningAddresses = listeningAddresses.ToArray();
Listeners = listeners.ToArray();
while (true)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
_queueSemaphore.Wait(cancellationToken);
Firewall.ProcessQueue();
if (_connectingQueue.TryDequeue(out var socket))
{
ProcessConnection(socket);
}
}
return;
}
catch (OperationCanceledException)
{
return;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CloseSocket(Socket socket)
{
try
{
socket.Shutdown(SocketShutdown.Both);
}
catch
{
// ignored
}
socket.Close();
}
private static void ProcessConnection(Socket socket)
{
var ipLimiter = IPLimiter.Enabled;
try
{
// Clear out any sockets that have been connecting for too long
while (_socketsConnecting.Count > 0)
{
var socketTime = _socketsConnecting.Min; // Earliest connected socket
if (Core.TickCount - socketTime.ConnectedAt <= MaximumSocketIdleDelay)
{
break;
}
var socketToCheck = socketTime.NetState;
if (socketToCheck.Running && !socketToCheck.Seeded)
{
socketToCheck.Disconnect(null);
}
_socketsConnecting.Remove(socketTime);
}
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
if (NetState.Instances.Count >= MaxConnections)
{
var ticks = Core.TickCount;
if (ticks - _nextMaximumSocketsReachedMessage > 0)
{
if (socket.RemoteEndPoint is IPEndPoint ipep)
{
var ip = ipep.Address.ToString();
logger.Warning("{Address} Failed (Maximum connections reached)", ip);
}
_nextMaximumSocketsReachedMessage = ticks + ListenerErrorMessageDelay;
}
CloseSocket(socket);
return;
}
if (ipLimiter && !IPLimiter.Verify(remoteIP))
{
TraceDisconnect("Past IP limit threshold", remoteIP);
logger.Debug("{Address} Past IP limit threshold", remoteIP);
CloseSocket(socket);
return;
}
if (Firewall.IsBlocked(remoteIP))
{
TraceDisconnect("Firewalled", remoteIP);
logger.Debug("{Address} Firewalled", remoteIP);
CloseSocket(socket);
return;
}
var ns = new NetState(socket);
_socketsConnecting.Add((Core.TickCount, ns));
ConnectedQueue.Enqueue(ns);
}
catch
{
// ignored
}
}
private static void TraceDisconnect(string reason, IPAddress ip)
{
try
{
using StreamWriter op = new StreamWriter("network-socket-disconnects.log", true);
op.WriteLine($"# {Core.Now}");
op.WriteLine($"Address: {ip}");
op.WriteLine(reason);
op.WriteLine();
op.WriteLine();
}
catch
{
// ignored
}
}
}