From f51ef765e5d1f0d29d101a93f58d768b95b231da Mon Sep 17 00:00:00 2001 From: "Robin C. Ladiges" Date: Sat, 27 Apr 2024 17:23:34 +0200 Subject: [PATCH 1/4] small refactorings - use brackets when possible - set client.Id and client.Name earlier - use client.Id instead of header.Id - rename firstConn to isClientNew --- Server/Client.cs | 8 ++-- Server/Program.cs | 5 ++- Server/Server.cs | 102 +++++++++++++++++++++++++++++----------------- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/Server/Client.cs b/Server/Client.cs index ea436ef..9c21a3f 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -41,8 +41,9 @@ public Client(Client other, Socket socket) { } public void Dispose() { - if (Socket?.Connected is true) + if (Socket?.Connected is true) { Socket.Disconnect(false); + } } @@ -52,8 +53,8 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)]; try { Server.FillPacket(new PacketHeader { - Id = sender?.Id ?? Id, - Type = packetAttribute.Type, + Id = sender?.Id ?? Id, + Type = packetAttribute.Type, PacketSize = packet.Size }, packet, memory.Memory); } @@ -69,6 +70,7 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa public async Task Send(Memory data, Client? sender) { PacketHeader header = new PacketHeader(); header.Deserialize(data.Span); + if (!Connected && header.Type is not PacketType.Connect) { Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet"); return; diff --git a/Server/Program.cs b/Server/Program.cs index 7b98085..6debd13 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -120,11 +120,13 @@ void logError(Task x) { server.PacketHandler = (c, p) => { switch (p) { case GamePacket gamePacket: { + // crash player entering a banned stage if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) { c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}."); BanLists.Crash(c, false, false, 500); return false; } + c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); // reset lastPlayerPacket on stage changes @@ -184,7 +186,7 @@ void logError(Task x) { break; } - case CostumePacket costumePacket: + case CostumePacket costumePacket: { c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}"); c.Metadata["lastCostumePacket"] = costumePacket; c.CurrentCostume = costumePacket; @@ -193,6 +195,7 @@ void logError(Task x) { #pragma warning restore CS4014 c.Metadata["loadedSave"] = true; break; + } case ShinePacket shinePacket: { if (!Settings.Instance.Shines.Enabled) return false; diff --git a/Server/Server.cs b/Server/Server.cs index 483c2e7..a1206f5 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -29,6 +29,7 @@ public async Task Listen(CancellationToken? token = null) { Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync(); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + // is the IPv4 address banned? if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { Logger.Warn($"Ignoring banned IPv4 address {socket.RemoteEndPoint}"); continue; @@ -36,6 +37,7 @@ public async Task Listen(CancellationToken? token = null) { Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}"); + // start sub thread to handle client try { #pragma warning disable CS4014 Task.Run(() => HandleSocket(socket)) @@ -78,15 +80,17 @@ public static void FillPacket(PacketHeader header, T packet, Memory mem public delegate void PacketReplacer(Client from, Client to, T value); // replacer must send public void BroadcastReplace(T packet, Client sender, PacketReplacer packetReplacer) where T : struct, IPacket { - foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet); + foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) { + packetReplacer(sender, client, packet); + } } public async Task Broadcast(T packet, Client sender) where T : struct, IPacket { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = sender?.Id ?? Guid.Empty, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size + Id = sender?.Id ?? Guid.Empty, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size, }; FillPacket(header, packet, memory.Memory); await Broadcast(memory, sender); @@ -96,9 +100,9 @@ public Task Broadcast(T packet) where T : struct, IPacket { return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { - Id = client.Id, - Type = Constants.PacketMap[typeof(T)].Type, - PacketSize = packet.Size + Id = client.Id, + Type = Constants.PacketMap[typeof(T)].Type, + PacketSize = packet.Size, }; FillPacket(header, packet, memory.Memory); await client.Send(memory.Memory, client); @@ -137,6 +141,7 @@ private async void HandleSocket(Socket socket) { await client.Send(new InitPacket { MaxPlayers = Settings.Instance.Server.MaxPlayers }); + bool first = true; try { while (true) { @@ -159,8 +164,9 @@ async Task Read(Memory readMem, int readSize, int readOffset) { return true; } - if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) + if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) { break; + } PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]); Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize); if (header.PacketSize > 0) { @@ -168,10 +174,10 @@ async Task Read(Memory readMem, int readSize, int readOffset) { memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize); memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]); memTemp.Dispose(); - if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) + if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) { break; + } } - if (client.Ignored) { memory.Dispose(); continue; @@ -179,60 +185,71 @@ async Task Read(Memory readMem, int readSize, int readOffset) { // connection initialization if (first) { - first = false; - if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}"); + first = false; // only do this once + + // first client packet has to be the client init + if (header.Type != PacketType.Connect) { + throw new Exception($"First packet was not init, instead it was {header.Type} ({remote})"); + } ConnectPacket connect = new ConnectPacket(); connect.Deserialize(memory.Memory.Span[packetRange]); - bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; + client.Id = header.Id; + client.Name = connect.ClientName; - if (BanLists.Enabled && BanLists.IsProfileBanned(header.Id)) { - client.Id = header.Id; - client.Name = connect.ClientName; + // is the profile ID banned? + if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) { client.Ignored = true; - client.Logger.Warn($"Ignoring banned profile ID {header.Id}"); + client.Logger.Warn($"Ignoring banned profile ID {client.Id}"); memory.Dispose(); continue; } + bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; + + // add client to the set of connected players lock (Clients) { + // is the server full? if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { client.Logger.Error($"Turned away as server is at max clients"); memory.Dispose(); goto disconnect; } - bool firstConn = true; + // detect and handle reconnections + bool isClientNew = true; switch (connect.ConnectionType) { case ConnectPacket.ConnectionTypes.FirstConnection: case ConnectPacket.ConnectionTypes.Reconnecting: { - client.Id = header.Id; - if (FindExistingClient(header.Id) is { } oldClient) { - firstConn = false; + if (FindExistingClient(client.Id) is { } oldClient) { + isClientNew = false; client = new Client(oldClient, socket); + client.Name = connect.ClientName; Clients.Remove(oldClient); Clients.Add(client); if (oldClient.Connected) { oldClient.Logger.Info($"Disconnecting already connected client {oldClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}"); oldClient.Dispose(); } - } else { + } + else { connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection; } break; } - default: - throw new Exception($"Invalid connection type {connect.ConnectionType}"); + default: { + throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})"); + } } - client.Name = connect.ClientName; client.Connected = true; - if (firstConn) { + + if (isClientNew) { // do any cleanup required when it comes to new clients - List toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null); - Clients.RemoveAll(c => c.Id == header.Id); + List toDisconnect = Clients.FindAll(c => c.Id == client.Id && c.Connected && c.Socket != null); + Clients.RemoveAll(c => c.Id == client.Id); Clients.Add(client); @@ -240,18 +257,19 @@ async Task Read(Memory readMem, int readSize, int readOffset) { // done disconnecting and removing stale clients with the same id ClientJoined?.Invoke(client, connect); - // a new connection, not a reconnect, for an existing client - } else if (wasFirst) { + } + // a known client reconnects, but with a new first connection (e.g. after a restart) + else if (wasFirst) { client.CleanMetadataOnNewConnection(); } } // for all other clients that are already connected - List otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null); + List otherConnectedPlayers = Clients.FindAll(c => c.Id != client.Id && c.Connected && c.Socket != null); await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { IMemoryOwner tempBuffer = MemoryPool.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size)); - // make the other client known to the (new) client + // make the other client known to the new client PacketHeader connectHeader = new PacketHeader { Id = other.Id, Type = PacketType.Connect, @@ -266,7 +284,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]); await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null); - // tell the (new) client what costume the other client has + // tell the new client what costume the other client has if (other.CurrentCostume.HasValue) { connectHeader.Type = PacketType.Costume; connectHeader.PacketSize = other.CurrentCostume.Value.Size; @@ -277,7 +295,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { tempBuffer.Dispose(); - // make the other client reset their puppet cache for this client, if it is a new connection (after restart) + // make the other client reset their puppet cache for this new client, if it is a new connection (after restart) if (wasFirst) { await SendEmptyPackets(client, other); } @@ -287,14 +305,19 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { // send missing or outdated packets from others to the new client await ResendPackets(client); - } else if (header.Id != client.Id && client.Id != Guid.Empty) { + } + else if (header.Id != client.Id && client.Id != Guid.Empty) { throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}"); } try { + // parse the packet IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!; packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]); + + // process the packet if (PacketHandler?.Invoke(client, packet) is false) { + // don't broadcast the packet to everyone memory.Dispose(); continue; } @@ -302,7 +325,9 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { client.Logger.Error($"Packet handler warning: {e}"); } + #pragma warning disable CS4014 + // broadcast the packet to everyone Broadcast(memory, client) .ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } }); #pragma warning restore CS4014 @@ -311,7 +336,8 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { catch (Exception e) { if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) { client.Logger.Info($"Disconnected from the server: Connection reset"); - } else { + } + else { client.Logger.Error($"Disconnecting due to exception: {e}"); if (socket.Connected) { #pragma warning disable CS4014 @@ -324,6 +350,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { memory?.Dispose(); } + // client disconnected disconnect: if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) { Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server"); @@ -333,7 +360,6 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } bool wasConnected = client.Connected; - // Clients.Remove(client) client.Connected = false; try { client.Dispose(); @@ -359,7 +385,7 @@ async Task trySendPack(Client other, T? packet) where T : struct, IPacket { } }; async Task trySendMeta(Client other, string packetType) where T : struct, IPacket { - if (! other.Metadata.ContainsKey(packetType)) { return; } + if (!other.Metadata.ContainsKey(packetType)) { return; } await trySendPack(other, (T) other.Metadata[packetType]!); }; await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => { From b418331a0368b4352e425ad04a586e62b567d3fc Mon Sep 17 00:00:00 2001 From: "Robin C. Ladiges" Date: Sat, 27 Apr 2024 17:32:17 +0200 Subject: [PATCH 2/4] send server init after the client init To make service discovery by internet scan bots harder. --- Server/Server.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Server/Server.cs b/Server/Server.cs index a1206f5..d71bfd4 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -138,9 +138,6 @@ private async void HandleSocket(Socket socket) { Client client = new Client(socket) {Server = this}; var remote = socket.RemoteEndPoint; IMemoryOwner memory = null!; - await client.Send(new InitPacket { - MaxPlayers = Settings.Instance.Server.MaxPlayers - }); bool first = true; try { @@ -206,6 +203,11 @@ async Task Read(Memory readMem, int readSize, int readOffset) { continue; } + // send server init + await client.Send(new InitPacket { + MaxPlayers = Settings.Instance.Server.MaxPlayers, + }); + bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; // add client to the set of connected players From 07526a7c383a8af998a7968ed0c65800b74f31e0 Mon Sep 17 00:00:00 2001 From: "Robin C. Ladiges" Date: Sat, 27 Apr 2024 17:46:18 +0200 Subject: [PATCH 3/4] crash ignored players Otherwise they keep sending all their packets (including positional updates) to the server which costs bandwidth and processing power. --- Server/BanLists.cs | 14 ++++++-------- Server/Client.cs | 13 ++++++++++++- Server/Program.cs | 14 +++++++++++++- Server/Server.cs | 41 +++++++++++++++++++++-------------------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/Server/BanLists.cs b/Server/BanLists.cs index dbe0d12..3f16a61 100644 --- a/Server/BanLists.cs +++ b/Server/BanLists.cs @@ -148,30 +148,27 @@ private static void Save() { public static void Crash( Client user, - bool permanent = false, - bool dispose_user = true, - int delay_ms = 0 + int delay_ms = 0 ) { user.Ignored = true; Task.Run(async () => { if (delay_ms > 0) { await Task.Delay(delay_ms); } + bool permanent = user.Banned; await user.Send(new ChangeStagePacket { Id = (permanent ? "$agogus/ban4lyfe" : "$among$us/cr4sh%"), Stage = (permanent ? "$ejected" : "$agogusStage"), Scenario = (sbyte) (permanent ? 69 : 21), SubScenarioType = (byte) (permanent ? 21 : 69), }); - if (dispose_user) { - user.Dispose(); - } }); } private static void CrashMultiple(string[] args, MUCH much) { foreach (Client user in much(args).toActUpon) { - Crash(user, true); + user.Banned = true; + Crash(user); } } @@ -245,8 +242,9 @@ public static string HandleBanCommand(string[] args, MUCH much) { } foreach (Client user in res.toActUpon) { + user.Banned = true; BanClient(user); - Crash(user, true); + Crash(user); } Save(); diff --git a/Server/Client.cs b/Server/Client.cs index 9c21a3f..21add6f 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -13,6 +13,7 @@ public class Client : IDisposable { public readonly ConcurrentDictionary Metadata = new ConcurrentDictionary(); // can be used to store any information about a player public bool Connected = false; public bool Ignored = false; + public bool Banned = false; public CostumePacket? CurrentCostume = null; // required for proper client sync public string Name { get => Logger.Name; @@ -52,6 +53,11 @@ public async Task Send(T packet, Client? sender = null) where T : struct, IPa PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)]; try { + // don't send most packets to ignored players + if (Ignored && packetAttribute.Type != PacketType.Init && packetAttribute.Type != PacketType.ChangeStage) { + memory.Dispose(); + return; + } Server.FillPacket(new PacketHeader { Id = sender?.Id ?? Id, Type = packetAttribute.Type, @@ -71,11 +77,16 @@ public async Task Send(Memory data, Client? sender) { PacketHeader header = new PacketHeader(); header.Deserialize(data.Span); - if (!Connected && header.Type is not PacketType.Connect) { + if (!Connected && !Ignored && header.Type != PacketType.Connect) { Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet"); return; } + // don't send most packets to ignored players + if (Ignored && header.Type != PacketType.Init && header.Type != PacketType.ChangeStage) { + return; + } + await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None); } diff --git a/Server/Program.cs b/Server/Program.cs index 6debd13..a0a7dd3 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -120,10 +120,17 @@ void logError(Task x) { server.PacketHandler = (c, p) => { switch (p) { case GamePacket gamePacket: { + // crash ignored player + if (c.Ignored) { + c.Logger.Info($"Crashing ignored player after entering stage {gamePacket.Stage}."); + BanLists.Crash(c, 500); + return false; + } + // crash player entering a banned stage if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) { c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}."); - BanLists.Crash(c, false, false, 500); + BanLists.Crash(c, 500); return false; } @@ -172,6 +179,11 @@ void logError(Task x) { break; } + // ignore all other packets from ignored players + case IPacket pack when c.Ignored: { + return false; + } + case TagPacket tagPacket: { // c.Logger.Info($"Got tag packet: {tagPacket.IsIt}"); if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt; diff --git a/Server/Server.cs b/Server/Server.cs index d71bfd4..51d9ad0 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -29,12 +29,6 @@ public async Task Listen(CancellationToken? token = null) { Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync(); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - // is the IPv4 address banned? - if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { - Logger.Warn($"Ignoring banned IPv4 address {socket.RemoteEndPoint}"); - continue; - } - Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}"); // start sub thread to handle client @@ -80,7 +74,7 @@ public static void FillPacket(PacketHeader header, T packet, Memory mem public delegate void PacketReplacer(Client from, Client to, T value); // replacer must send public void BroadcastReplace(T packet, Client sender, PacketReplacer packetReplacer) where T : struct, IPacket { - foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) { + foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.Id)) { packetReplacer(sender, client, packet); } } @@ -97,7 +91,7 @@ public async Task Broadcast(T packet, Client sender) where T : struct, IPacke } public Task Broadcast(T packet) where T : struct, IPacket { - return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => { + return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).Select(async client => { IMemoryOwner memory = MemoryPool.Shared.RentZero(Constants.HeaderSize + packet.Size); PacketHeader header = new PacketHeader { Id = client.Id, @@ -116,7 +110,7 @@ public Task Broadcast(T packet) where T : struct, IPacket { /// Memory owner to dispose once done /// Optional sender to not broadcast data to public async Task Broadcast(IMemoryOwner data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender))); data.Dispose(); } @@ -126,7 +120,7 @@ public async Task Broadcast(IMemoryOwner data, Client? sender = null) { /// Memory to send to the clients /// Optional sender to not broadcast data to public async void Broadcast(Memory data, Client? sender = null) { - await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data, sender))); + await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender))); } public Client? FindExistingClient(Guid id) { @@ -175,10 +169,6 @@ async Task Read(Memory readMem, int readSize, int readOffset) { break; } } - if (client.Ignored) { - memory.Dispose(); - continue; - } // connection initialization if (first) { @@ -195,19 +185,30 @@ async Task Read(Memory readMem, int readSize, int readOffset) { client.Id = header.Id; client.Name = connect.ClientName; + // is the IPv4 address banned? + if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { + Logger.Warn($"Ignoring banned IPv4 address for {client.Name} ({client.Id}/{remote})"); + client.Ignored = true; + client.Banned = true; + } // is the profile ID banned? - if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) { + else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) { + client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})"); client.Ignored = true; - client.Logger.Warn($"Ignoring banned profile ID {client.Id}"); - memory.Dispose(); - continue; + client.Banned = true; } - // send server init + // send server init (required to crash ignored players later) await client.Send(new InitPacket { - MaxPlayers = Settings.Instance.Server.MaxPlayers, + MaxPlayers = (client.Ignored ? (ushort) 1 : Settings.Instance.Server.MaxPlayers), }); + // don't init or announce an ignored client to other players any further + if (client.Ignored) { + memory.Dispose(); + continue; + } + bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection; // add client to the set of connected players From b661cd5c93ac48d39533b66eec7529207e90551d Mon Sep 17 00:00:00 2001 From: "Robin C. Ladiges" Date: Sat, 27 Apr 2024 17:50:28 +0200 Subject: [PATCH 4/4] ignore & crash instead of disconnect clients after reaching the MaxPlayers limit Otherwise they'll enter an endless disconnect-reconnnect loop spamming the server with new TCP connections. --- Server/Server.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Server/Server.cs b/Server/Server.cs index 51d9ad0..555e204 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -197,6 +197,11 @@ async Task Read(Memory readMem, int readSize, int readOffset) { client.Ignored = true; client.Banned = true; } + // is the server full? + else if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { + client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); + client.Ignored = true; + } // send server init (required to crash ignored players later) await client.Send(new InitPacket { @@ -213,11 +218,12 @@ await client.Send(new InitPacket { // add client to the set of connected players lock (Clients) { - // is the server full? - if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { - client.Logger.Error($"Turned away as server is at max clients"); + // is the server full? (check again, to prevent race conditions) + if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) { + client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}"); + client.Ignored = true; memory.Dispose(); - goto disconnect; + continue; } // detect and handle reconnections @@ -354,7 +360,6 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { } // client disconnected - disconnect: if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) { Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server"); }