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 ea436ef..8e7ba2d 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; @@ -41,8 +42,9 @@ public Client(Client other, Socket socket) { } public void Dispose() { - if (Socket?.Connected is true) + if (Socket?.Connected is true) { Socket.Disconnect(false); + } } @@ -51,9 +53,14 @@ 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, + Id = sender?.Id ?? Id, + Type = packetAttribute.Type, PacketSize = packet.Size }, packet, memory.Memory); } @@ -69,11 +76,17 @@ 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) { + + 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); } @@ -88,8 +101,8 @@ public void CleanMetadataOnNewConnection() { } public TagPacket? GetTagPacket() { - var time = (Time?) this.Metadata?["time"]; - var seek = (bool?) this.Metadata?["seeking"]; + var time = (Time?) (this.Metadata.ContainsKey("time") ? this.Metadata["time"] : null); + var seek = (bool?) (this.Metadata.ContainsKey("seeking") ? this.Metadata["seeking"] : null); if (time == null && seek == null) { return null; } return new TagPacket { UpdateType = (seek != null ? TagPacket.TagUpdate.State : 0) | (time != null ? TagPacket.TagUpdate.Time: 0), diff --git a/Server/Program.cs b/Server/Program.cs index d27582d..5d11b73 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -120,11 +120,20 @@ 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; } + c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); // reset lastPlayerPacket on stage changes @@ -177,6 +186,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; @@ -191,7 +205,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; @@ -200,6 +214,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 da40fd6..3a74431 100644 --- a/Server/Server.cs +++ b/Server/Server.cs @@ -29,15 +29,11 @@ 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); - if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) { - Logger.Warn($"Ignoring banned IPv4 address {socket.RemoteEndPoint}"); - continue; - } - if (! Settings.Instance.JsonApi.Enabled) { Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}"); } + // start sub thread to handle client try { #pragma warning disable CS4014 Task.Run(() => HandleSocket(socket)) @@ -80,27 +76,29 @@ 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(c => c.Connected && !c.Ignored && sender.Id != c.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); } 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, - 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); @@ -114,7 +112,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(); } @@ -124,7 +122,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) { @@ -136,9 +134,7 @@ 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 { while (true) { @@ -161,8 +157,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]); if (first && await JsonApi.JsonApi.HandleAPIRequest(this, socket, header, memory)) { goto close; } @@ -172,71 +169,100 @@ 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; + } } // 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 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? + 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.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; - client.Logger.Warn($"Ignoring banned profile ID {header.Id}"); + } + + // send server init (required to crash ignored players later) + await client.Send(new InitPacket { + 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 lock (Clients) { - 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; } - 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); @@ -244,18 +270,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, @@ -270,7 +297,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; @@ -281,7 +308,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); } @@ -291,14 +318,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; } @@ -306,7 +338,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 @@ -315,7 +349,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 @@ -328,7 +363,7 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { memory?.Dispose(); } - disconnect: + // client disconnected 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"); } @@ -338,7 +373,6 @@ await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { close: bool wasConnected = client.Connected; - // Clients.Remove(client) client.Connected = false; try { client.Dispose(); @@ -364,7 +398,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, _) => {