Сжатие и шифрование
Соединение открывается сырым: без сжатия и без шифра. Оба слоя включаются посреди сессии, каждый своим действием кода приложения, и оба относятся к транспорту в целом - к обоим направлениям сразу.
Сжатие
Порог сжатия сообщает сервер пакетом
LoginCompressPacket.
Код приложения переносит число в CompressionThreshold у
MinecraftClient (то же
свойство есть у
MinecraftConnection,
MinecraftClient его только пробрасывает). Значение отрицательное - сжатие
выключено, это же и начальное состояние соединения. Новое значение действует с
ближайшего кадра, а не с текущего.
Порог решает судьбу каждого исходящего пакета отдельно. Пакет короче порога уходит как есть, но в кадре появляется дополнительный VarInt перед телом - 0, означающий «не сжат». Пакет не короче порога сжимается, и в этом же месте кадра стоит VarInt с исходным (несжатым) размером. На входящей стороне ноль в этом поле - сигнал развернуть пакет как есть, отличное от нуля значение - сколько байт распаковать. В протоколе этот формат описан на странице With compression.
Движок сжатия - libdeflate, из пакета McProtoNet.Native. Управляющий код
вызывает его через DllImport: libdeflate_zlib_compress,
libdeflate_zlib_decompress и парные функции alloc/free для хэндла
компрессора и декомпрессора. Нативный вызов вместо встроенного ZLibStream взят
ради скорости, формат от этого не меняется: на проводе всё тот же поток zlib.
Хэндлы компрессора и декомпрессора не создаются на каждый пакет:
LibDeflateCache держит по одному на поток ([ThreadStatic]) и переиспользует
их на все кадры этого потока. Уровень сжатия компрессора зашит в коде - 4 из
диапазона libdeflate 0-12.
Шифрование
Шифр - AES/CFB8: тот же режим, что описан в протоколе на странице
Encryption. Общий
секрет, которым стороны обменялись через
EncryptionRequestPacket
и
EncryptionResponsePacket,
служит сразу и ключом, и вектором инициализации -
PacketCipher.SharedSecretLength требует ровно 16 байт, это ключ AES-128.
PacketCipher.CreateEncryptor и CreateDecryptor собирают по шифру на каждое
направление - MinecraftConnection.EnableEncryption создаёт оба сразу и отдаёт
их читателю и писателю кадров. Шифр включается с ближайшего следующего кадра в
обе стороны и назад не выключается: EnableEncryption второй раз на том же
соединении бросает исключение.
Аппаратные пути
Конкретную реализацию
PacketCipher
выбирает статическая фабрика по поддержке процессора, в порядке проверки:
if (AesCfb8HardwareCipher.IsSupported)
return new AesCfb8HardwareCipher(sharedSecret, sharedSecret, encrypting);
if (AesCfb8ArmCipher.IsSupported)
return new AesCfb8ArmCipher(sharedSecret, sharedSecret, encrypting);
return new AesCfb8Cipher(sharedSecret, sharedSecret, encrypting);
AesCfb8HardwareCipher - интринсики x86 AES-NI поверх SSE2/SSSE3/SSE4.1.
AesCfb8ArmCipher - AES и AdvSimd (NEON) на ARM64, с собственным расширением
ключа AES-128 (AesKeySchedule), потому что платформенного Aes.Create() для
этого пути не хватает. Когда не поддержано ни то ни другое, в дело идёт
AesCfb8Cipher - обёртка над платформенным Aes с EncryptCfb/DecryptCfb и
feedbackSizeInBits: 8. Обе интринсик-реализации при расшифровке дополнительно
распараллеливают блоки по 16 байт за проход, а не идут байт за байтом, как того
требует CFB8 сам по себе.
Порядок на потоке
Снаружи, ближе к сокету, - шифр. Внутри - кадр целиком, включая varint длины и,
если он есть, конверт сжатия. При записи BufferedPacketWriter сперва
достраивает кадр (длина, при необходимости - сжатие), и только затем шифрует
получившиеся байты на месте. При чтении наоборот: сырые байты из сокета сначала
проходят через шифр, и лишь потом код разбирает в них длину кадра и, если нужно,
распаковывает тело.
Что делает приложение
Сжатие и шифрование не переключаются сами - оба включает код приложения, по одной строке на каждый:
client.CompressionThreshold = packet.Threshold;
client.EnableEncryption(secret);
Всё остальное - выбор движка, аппаратного пути, формат кадра и порядок применения на потоке - остаётся внутри транспорта.