using Supercell.Laser.Server.Networking.Security; using Supercell.Laser.Server.Networking; using Supercell.Laser.Titan.Library.Blake; using Supercell.Laser.Server; namespace Supercell.Laser.Server.Networking { using Supercell.Laser.Logic.Message; using Supercell.Laser.Server.Message; using Supercell.Laser.Server.Settings; using Supercell.Laser.Titan.Cryptography; using Supercell.Laser.Titan.Library; using Supercell.Laser.Titan.Math; using System.Linq; public class Messaging { public byte[] SessionToken { get; } private Connection Connection; public int PepperState; // Fix 3: ограничиваем число одновременных тяжёлых crypto-операций хендшейка. // HandlePepperLogin вызывает TweetNaCl.CryptoBoxOpen — асимметричная крипта, // дорогая по CPU. Без лимита флуд-логинов держит все IO-потоки IOCP в crypto, // блокируя accept/receive/send для легитимных клиентов. // Wait(0) = неблокирующий: если лимит исчерпан — соединение обрывается мгновенно, // без выполнения крипты. 4×ProcessorCount достаточно для реальных логинов. private static readonly SemaphoreSlim HandshakeSemaphore = new SemaphoreSlim( Environment.ProcessorCount * 4, Environment.ProcessorCount * 4); private StreamEncrypter Encrypter; private StreamEncrypter Decrypter; private MessageFactory MessageFactory; private byte[] RNonce; private byte[] SNonce; private byte[] SecretKey; public int Seed { get; set; } public Messaging(Connection connection) { Connection = connection; MessageFactory = MessageFactory.Instance; PepperState = 2; SessionToken = new byte[24]; SNonce = new byte[24]; SecretKey = new byte[32]; TweetNaCl.RandomBytes(SessionToken); TweetNaCl.RandomBytes(SNonce); TweetNaCl.RandomBytes(SecretKey); } public void Send(GameMessage message) { if (message.GetMessageType() == 20100 || message.GetMessageType() == 20104 || PepperState == 5) { EncryptAndWrite(message); } else { Processor.Send(Connection, message); } } private readonly object EncryptLock = new object(); public void EncryptAndWrite(GameMessage message) { lock (EncryptLock) { if (message.GetEncodingLength() == 0) message.Encode(); byte[] payload = new byte[message.GetEncodingLength()]; Buffer.BlockCopy(message.GetMessageBytes(), 0, payload, 0, payload.Length); int messageType = message.GetMessageType(); int version = message.GetVersion(); long accountId = Connection.Avatar?.AccountId ?? 0; // Fix 6: Logger.Print вызывает Console.WriteLine, который в .NET синхронизирован // глобальным локом процесса. На каждый исходящий пакет это лок-контеншн в hot path. // WriteServerLog пишет только в асинхронную очередь файла — без Console, без лока. if (messageType != 23457 && messageType != 20405 && messageType != 20108) Logger.WriteServerLog($"[{DateTime.Now}][SEND] Type={messageType} Size={payload.Length} AccountId={accountId}"); switch (PepperState) { case 4: payload = SendPepperLoginResponse(payload); break; case 5: byte[] encrypted = new byte[payload.Length + Encrypter.GetEncryptionOverhead()]; Encrypter.Encrypt(payload, encrypted, payload.Length); payload = encrypted; break; } byte[] stream = new byte[payload.Length + 7]; int length = payload.Length; stream[0] = (byte)(messageType >> 8); stream[1] = (byte)(messageType); stream[2] = (byte)(length >> 16); stream[3] = (byte)(length >> 8); stream[4] = (byte)(length); stream[5] = (byte)(version >> 8); stream[6] = (byte)(version); Buffer.BlockCopy(payload, 0, stream, 7, payload.Length); Connection.Write(stream); } } // Верхний предел размера одного игрового сообщения. Реальные пакеты на порядки // меньше; поле длины в заголовке 3-байтовое (до ~16 МБ), поэтому без капа один // коннект мог форсировать повторные ~16 МБ аллокации ещё до получения данных. private const int MaxMessageLength = 512 * 1024; // 512 КБ public int OnReceive() { // Работаем напрямую с буфером MemoryStream (GetBuffer — без копии), // разбираем сколько влезет целых сообщений за один проход, потом один раз // компактим остаток. Раньше на каждое сообщение делался ToArray()+Skip()+ // новый MemoryStream — O(n) копирование на итерацию плюс рекурсия. byte[] data = Connection.Memory.GetBuffer(); int available = (int)Connection.Memory.Length; int consumed = 0; while (available - consumed >= 7) { int offset = consumed; int type = data[offset] << 8 | data[offset + 1]; int length = data[offset + 2] << 16 | data[offset + 3] << 8 | data[offset + 4]; int version = data[offset + 5] << 8 | data[offset + 6]; // Санитизация длины ДО любой аллокации. if (length < 0 || length > MaxMessageLength) { Logger.Print($"[Disconnect] Некорректная длина пакета type={type} length={length} (max={MaxMessageLength})."); return -1; } // Всё сообщение целиком ещё не пришло — ждём следующий чанк. if (available - consumed - 7 < length) break; WireDump.Write($"TCP-RECV type={type}", data, offset, 7 + length); byte[] payload = new byte[length]; System.Buffer.BlockCopy(data, offset + 7, payload, 0, length); consumed += 7 + length; if (type == 11000 || type == 19995 || type == 19998 || type == 19999) { if (this.ReadNewMessageAPI(type, length, version, payload) != 0) return -1; } else { if (this.ReadNewMessage(type, length, version, payload) != 0) return -1; } } if (consumed > 0) { int remaining = available - consumed; var next = new MemoryStream(); if (remaining > 0) next.Write(data, consumed, remaining); Connection.Memory = next; } return 0; } private int ReadNewMessageAPI(int type, int length, int version, byte[] payload) { GameMessage message = MessageFactory.CreateMessageByType(type); if (message != null) { message.GetByteStream().SetByteArray(payload, payload.Length); message.Decode(); if (message.GetMessageType() == 10100) { Connection.MessageManager.ReceiveMessage(message); } else { Processor.Receive(Connection, message); } } else { Logger.Print("Ignoring message of unknown type " + type); } return 0; } private int ReadNewMessage(int type, int length, int version, byte[] payload) { if (type == 10099) { int errorCode = payload.Length >= 4 ? (payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3]) : 0; Logger.Print($"[Disconnect] ClientCryptoError получен (code={errorCode}). PepperState={PepperState}"); return -1; } if (type != 11000) { switch (PepperState) { case 2: if (type == 10100) PepperState = 3; else if (Configuration.Instance.TestMode) { Logger.Print($"[TestMode] Пропущен ClientHello — принят пакет type={type} без шифрования (ТЕСТОВЫЙ РЕЖИМ, не для прода!)."); PepperState = 6; // тестовый режим: дальше все пакеты идут как есть, без крипты } else { Logger.Print($"[Disconnect] Ожидался ClientHello (10100), но получен тип {type}. PepperState=2"); return -1; } break; case 3: if (type != 10101) { Logger.Print($"[Disconnect] Ожидался Login (10101), но получен тип {type}. PepperState=3"); return -1; } // Fix 3: Wait(0) — неблокирующий захват семафора. // Если все слоты заняты (атака), обрываем без CryptoBoxOpen. if (!HandshakeSemaphore.Wait(0)) { Logger.WriteServerLog($"[Disconnect] Лимит одновременных хендшейков ({Environment.ProcessorCount * 4}) исчерпан — отклоняем без крипты."); return -1; } try { payload = HandlePepperLogin(payload); if (payload == null) return -1; } finally { HandshakeSemaphore.Release(); } break; case 5: if (type != 11000) { byte[] decrypted = new byte[length - Decrypter.GetEncryptionOverhead()]; int result = Decrypter.Decrypt(payload, decrypted, length); payload = decrypted; if (result != 0) { Logger.Print($"[Disconnect] Не удалось расшифровать пакет типа {type} (result={result}). PepperState=5"); return -1; } } break; } } GameMessage message = MessageFactory.CreateMessageByType(type); if (message != null) { message.GetByteStream().SetByteArray(payload, payload.Length); message.Decode(); if (message.GetMessageType() == 10100) { Connection.MessageManager.ReceiveMessage(message); } else { Processor.Receive(Connection, message); } } else { Logger.Print("Ignoring message of unknown type " + type); } return 0; } private byte[] client_pk; private byte[] client_sk; private byte[] HandlePepperLogin(byte[] payload) { try { client_pk = payload.Take(32).ToArray(); client_sk = PepperKey.CLIENT_SK; Blake2BHasher hasher = new Blake2BHasher(); hasher.Update(client_pk); hasher.Update(PepperKey.SERVER_PK); byte[] nonce = hasher.Finish(); byte[] decrypted = TweetNaCl.CryptoBoxOpen(payload.Skip(32).ToArray(), nonce, PepperKey.SERVER_PK, client_sk); byte[] session_token = decrypted.Skip(0).Take(24).ToArray(); if (!session_token.SequenceEqual(SessionToken)) { Logger.Print("[Disconnect] Session token mismatch при логине (10101)."); return null; } PepperState = 4; RNonce = decrypted.Skip(24).Take(24).ToArray(); return decrypted.Skip(48).ToArray(); } catch (TweetNaCl.InvalidCipherTextException) { Logger.Print("[Disconnect] Не удалось расшифровать Login (10101) — InvalidCipherText (не те ключи/nonce)."); return null; } } private byte[] SendPepperLoginResponse(byte[] payload) { byte[] packet = new byte[payload.Length + 32 + 24]; Buffer.BlockCopy(SNonce, 0, packet, 0, 24); Buffer.BlockCopy(SecretKey, 0, packet, 24, 32); Buffer.BlockCopy(payload, 0, packet, 24 + 32, payload.Length); Blake2BHasher hasher = new Blake2BHasher(); hasher.Update(RNonce); hasher.Update(client_pk); hasher.Update(PepperKey.SERVER_PK); byte[] nonce = hasher.Finish(); byte[] encrypted = TweetNaCl.CryptoBox(packet, nonce, PepperKey.SERVER_PK, client_sk); PepperState = 5; Decrypter = new PepperEncrypter(SecretKey, RNonce); Encrypter = new PepperEncrypter(SecretKey, SNonce); return encrypted; } } }