feat: dedicated server security hardening
Comprehensive security system to protect against packet-sniffing attacks, XUID harvesting, privilege escalation, bot flooding, and XUID impersonation. - Stream cipher: per-session XOR cipher with 4-message handshake via CustomPayloadPacket (MC|CKey, MC|CAck, MC|COn). Negotiated per-connection, backwards compatible (old clients/servers fall back to plaintext). - Security gate: buffers all game data until cipher handshake completes, preventing unsecured clients from receiving any XUIDs or game state. - Cipher handshake enforcer: kicks clients that don't complete the handshake within 5 seconds (configurable via require-secure-client). - Identity tokens: persistent per-XUID tokens in identity-tokens.json, issued over the encrypted channel, verified on reconnect. Prevents XUID replay attacks. Client stores server-specific tokens. - PROXY protocol v1: parses real client IPs from playit.gg tunnel headers so rate limiting, IP bans, and XUID spoof detection work per-player. - Rate limiting: per-IP sliding window (default 5 connections/30s) with pending connection cap (default 10). - Privilege hardening: OP requires ops.json, live checks on every command and privilege packet. Host-only server settings changes. - XUID stripping: PreLoginPacket response sends INVALID_XUID placeholders. - Packet validation: readUtf global string cap, reduced max packet size, stream desync protection on oversized strings. - OpManager: persistent ops.json with XUID-based OP list. - Whitelist improvements: whitelist add accepts player names with ambiguity detection, XUID cache from login attempts. - revoketoken command: revoke identity tokens for players who lost theirs. - server.log: persistent log file written alongside console output with flush-per-write to survive crashes. - CLI security logging: consolidated per-join security summary with cipher status, token status, XUID, and real IP. Security warnings for kicks, spoofing, and unauthorized commands.
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
#ifdef _WINDOWS64
|
||||
#include "Xbox\Network\NetworkPlayerXbox.h"
|
||||
#include "Common\Network\PlatformNetworkManagerStub.h"
|
||||
#include "Windows64\Network\WinsockNetLayer.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -3787,6 +3788,120 @@ void ClientConnection::handleSoundEvent(shared_ptr<LevelSoundPacket> packet)
|
||||
|
||||
void ClientConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> customPayloadPacket)
|
||||
{
|
||||
#ifdef _WINDOWS64
|
||||
// Build a server-specific identity token file path next to the executable.
|
||||
// Each server gets its own token file based on a hash of the server address,
|
||||
// so connecting to multiple secured servers doesn't overwrite tokens.
|
||||
auto buildIdentityTokenPath = []() -> std::string {
|
||||
char exePath[MAX_PATH] = {};
|
||||
DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
|
||||
if (len == 0 || len >= MAX_PATH) return std::string();
|
||||
char *lastSlash = strrchr(exePath, '\\');
|
||||
if (lastSlash != NULL) *(lastSlash + 1) = 0;
|
||||
|
||||
// Hash the server IP:port to create a unique filename per server
|
||||
char serverAddr[300] = {};
|
||||
sprintf_s(serverAddr, sizeof(serverAddr), "%s:%d", g_Win64MultiplayerIP, g_Win64MultiplayerPort);
|
||||
unsigned int hash = 5381;
|
||||
for (const char *p = serverAddr; *p; ++p)
|
||||
hash = ((hash << 5) + hash) + static_cast<unsigned char>(*p);
|
||||
|
||||
char filename[64] = {};
|
||||
sprintf_s(filename, sizeof(filename), "identity-token-%08x.dat", hash);
|
||||
return std::string(exePath) + filename;
|
||||
};
|
||||
|
||||
// Identity token: server issued us a new token - store it locally
|
||||
if (CustomPayloadPacket::IDENTITY_TOKEN_ISSUE.compare(customPayloadPacket->identifier) == 0)
|
||||
{
|
||||
if (customPayloadPacket->data.data != nullptr && customPayloadPacket->length == 32)
|
||||
{
|
||||
std::string tokenPath = buildIdentityTokenPath();
|
||||
if (!tokenPath.empty())
|
||||
{
|
||||
FILE *f = nullptr;
|
||||
fopen_s(&f, tokenPath.c_str(), "wb");
|
||||
if (f != nullptr)
|
||||
{
|
||||
size_t written = fwrite(customPayloadPacket->data.data, 1, 32, f);
|
||||
fclose(f);
|
||||
if (written == 32)
|
||||
{
|
||||
app.DebugPrintf("Client: Stored identity token to %s\n", tokenPath.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("Client: Failed to write full identity token (wrote %zu/32)\n", written);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("Client: Failed to open %s for writing\n", tokenPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Identity token: server is challenging us to present our stored token
|
||||
if (CustomPayloadPacket::IDENTITY_TOKEN_CHALLENGE.compare(customPayloadPacket->identifier) == 0)
|
||||
{
|
||||
std::string tokenPath = buildIdentityTokenPath();
|
||||
FILE *f = nullptr;
|
||||
if (!tokenPath.empty())
|
||||
fopen_s(&f, tokenPath.c_str(), "rb");
|
||||
if (f != nullptr)
|
||||
{
|
||||
uint8_t token[32] = {};
|
||||
size_t bytesRead = fread(token, 1, 32, f);
|
||||
fclose(f);
|
||||
if (bytesRead == 32)
|
||||
{
|
||||
byteArray tokenData(32);
|
||||
memcpy(tokenData.data, token, 32);
|
||||
connection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, tokenData));
|
||||
app.DebugPrintf("Client: Sent identity token response\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("Client: identity-token.dat is invalid (%zu bytes)\n", bytesRead);
|
||||
connection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, byteArray()));
|
||||
}
|
||||
SecureZeroMemory(token, sizeof(token));
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("Client: No identity-token.dat found, sending empty response\n");
|
||||
connection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, byteArray()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream cipher handshake: server sent us a key
|
||||
if (CustomPayloadPacket::CIPHER_KEY_CHANNEL.compare(customPayloadPacket->identifier) == 0)
|
||||
{
|
||||
if (customPayloadPacket->length == ServerRuntime::Security::StreamCipher::KEY_SIZE &&
|
||||
customPayloadPacket->data.data != nullptr)
|
||||
{
|
||||
app.DebugPrintf("Client: Received MC|CKey from server (%d bytes)\n", customPayloadPacket->length);
|
||||
// Store key and send ack+activate atomically to prevent ResetClientCipher race
|
||||
WinsockNetLayer::StoreClientCipherKey(customPayloadPacket->data.data);
|
||||
if (!WinsockNetLayer::SendAckAndActivateClientSendCipher())
|
||||
{
|
||||
app.DebugPrintf("Client: Failed to send cipher ack, connection will be closed\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("Client: Received malformed MC|CKey (length=%d)\n", customPayloadPacket->length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (CustomPayloadPacket::TRADER_LIST_PACKET.compare(customPayloadPacket->identifier) == 0)
|
||||
{
|
||||
ByteArrayInputStream bais(customPayloadPacket->data);
|
||||
|
||||
@@ -196,9 +196,29 @@ void IQNetPlayer::SendData(IQNetPlayer * player, const void* pvData, DWORD dwDat
|
||||
{
|
||||
if (!WinsockNetLayer::IsHosting() && !m_isRemote)
|
||||
{
|
||||
// Client sending to server via local socket (bypasses SendToSmallId)
|
||||
SOCKET sock = WinsockNetLayer::GetLocalSocket(m_smallId);
|
||||
if (sock != INVALID_SOCKET)
|
||||
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
|
||||
{
|
||||
// Encrypt if client send cipher is active
|
||||
if (dwDataSize > 0)
|
||||
{
|
||||
std::vector<BYTE> buf(static_cast<const BYTE*>(pvData),
|
||||
static_cast<const BYTE*>(pvData) + dwDataSize);
|
||||
if (WinsockNetLayer::TryEncryptClientOutgoing(buf.data(), static_cast<int>(dwDataSize)))
|
||||
{
|
||||
WinsockNetLayer::SendOnSocket(sock, buf.data(), static_cast<int>(dwDataSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\Minecraft.Server\ServerLogManager.h"
|
||||
#include "..\Minecraft.Server\Access\Access.h"
|
||||
#include "..\Minecraft.Server\Security\SecurityConfig.h"
|
||||
#include "..\Minecraft.World\Socket.h"
|
||||
#endif
|
||||
// #ifdef __PS3__
|
||||
@@ -150,6 +151,20 @@ void PendingConnection::sendPreLoginResponse()
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Security: strip real XUIDs from pre-login response to prevent unauthenticated enumeration.
|
||||
// The client receives the correct player count but cannot identify who is connected.
|
||||
// Real XUID data is sent post-login via PlayerInfoPacket broadcasts.
|
||||
if (ServerRuntime::Security::GetSettings().hidePlayerListPreLogin)
|
||||
{
|
||||
for (DWORD i = 0; i < ugcXuidCount; ++i)
|
||||
{
|
||||
ugcXuids[i] = INVALID_XUID;
|
||||
}
|
||||
ugcFriendsOnlyBits = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
if (false)// server->onlineMode) // 4J - removed
|
||||
{
|
||||
@@ -203,6 +218,56 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
|
||||
duplicateXuid = true;
|
||||
}
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Cross-reference: if someone claims the same XUID as an existing player from a different IP,
|
||||
// log and reject as a potential spoofing attempt.
|
||||
// Note: this runs on the main tick thread (via PendingConnection::tick -> Connection::tick ->
|
||||
// handleLogin), same thread that mutates the player list, so no lock is needed.
|
||||
if (!duplicateXuid && loginXuid != INVALID_XUID)
|
||||
{
|
||||
std::string newIp;
|
||||
unsigned char newSmallId = GetPendingConnectionSmallId(connection);
|
||||
bool hasNewIp = ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(newSmallId, &newIp);
|
||||
|
||||
for (auto &existingPlayer : server->getPlayers()->players)
|
||||
{
|
||||
if (existingPlayer == nullptr) continue;
|
||||
PlayerUID existingXuid = existingPlayer->connection->m_offlineXUID;
|
||||
if (existingXuid == INVALID_XUID) existingXuid = existingPlayer->connection->m_onlineXUID;
|
||||
if (existingXuid == loginXuid)
|
||||
{
|
||||
if (hasNewIp)
|
||||
{
|
||||
std::string existingIp;
|
||||
INetworkPlayer *np = existingPlayer->connection->getNetworkPlayer();
|
||||
if (np != nullptr)
|
||||
{
|
||||
unsigned char existingSmallId = np->GetSmallId();
|
||||
if (ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(existingSmallId, &existingIp))
|
||||
{
|
||||
if (existingIp != newIp)
|
||||
{
|
||||
app.DebugPrintf("SECURITY: XUID spoofing suspected - XUID 0x%016llx claimed from IP %s while already connected from IP %s\n",
|
||||
(unsigned long long)loginXuid, newIp.c_str(), existingIp.c_str());
|
||||
ServerRuntime::ServerLogManager::OnXuidSpoofDetected(newSmallId, name, newIp.c_str(), existingIp.c_str());
|
||||
duplicateXuid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot verify IP -- treat same-XUID connection as suspicious
|
||||
app.DebugPrintf("SECURITY: XUID 0x%016llx claimed but could not verify source IP\n",
|
||||
(unsigned long long)loginXuid);
|
||||
duplicateXuid = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool bannedXuid = false;
|
||||
if (loginXuid != INVALID_XUID)
|
||||
{
|
||||
@@ -243,7 +308,11 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
|
||||
else if (!whitelistSatisfied)
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Cache name->XUID so `whitelist add <name>` can resolve the XUID
|
||||
ServerRuntime::ServerLogManager::CachePlayerXuid(name, loginXuid);
|
||||
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted);
|
||||
app.DebugPrintf("WHITELIST: Rejected %ls (XUID: 0x%016llx) - use 'whitelist add %ls' to allow\n",
|
||||
name.c_str(), (unsigned long long)loginXuid, name.c_str());
|
||||
#endif
|
||||
disconnect(DisconnectPacket::eDisconnect_Banned);
|
||||
}
|
||||
@@ -330,11 +399,17 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
|
||||
PlayerUID playerXuid = packet->m_offlineXuid;
|
||||
if(playerXuid == INVALID_XUID) playerXuid = packet->m_onlineXuid;
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Cache name->XUID for console commands (whitelist add, revoketoken, etc.)
|
||||
ServerRuntime::ServerLogManager::CachePlayerXuid(name, playerXuid);
|
||||
#endif
|
||||
|
||||
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid);
|
||||
if (playerEntity != nullptr)
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
|
||||
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name,
|
||||
packet->m_offlineXuid, packet->m_onlineXuid, packet->m_isGuest);
|
||||
#endif
|
||||
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
|
||||
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
#include "Options.h"
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\Minecraft.Server\ServerLogManager.h"
|
||||
#include "..\Minecraft.Server\Access\Access.h"
|
||||
#include "..\Minecraft.Server\Security\IdentityTokenManager.h"
|
||||
#include "..\Minecraft.Server\Security\SecurityConfig.h"
|
||||
#include "..\Minecraft.Server\Security\ConnectionCipher.h"
|
||||
extern bool g_Win64DedicatedServer;
|
||||
#endif
|
||||
|
||||
namespace
|
||||
@@ -85,6 +90,9 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
|
||||
m_onlineXUID = INVALID_XUID;
|
||||
m_bHasClientTickedOnce = false;
|
||||
m_logSmallId = 0;
|
||||
m_identityVerified = false;
|
||||
m_identityChallengeTick = -1;
|
||||
m_securityGateOpen = true; // default open; closed when cipher is required
|
||||
|
||||
// Cache the first valid transport smallId because disconnect teardown can clear it before the server logger runs.
|
||||
if (this->connection != NULL && this->connection->getSocket() != NULL)
|
||||
@@ -620,6 +628,22 @@ void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason,
|
||||
LeaveCriticalSection(&done_cs);
|
||||
}
|
||||
|
||||
void PlayerConnection::openSecurityGate()
|
||||
{
|
||||
if (m_securityGateOpen)
|
||||
return;
|
||||
|
||||
m_securityGateOpen = true;
|
||||
|
||||
// Flush all buffered packets now that the cipher is active
|
||||
for (auto &buffered : m_securityBuffer)
|
||||
{
|
||||
send(buffered);
|
||||
}
|
||||
m_securityBuffer.clear();
|
||||
m_securityBuffer.shrink_to_fit();
|
||||
}
|
||||
|
||||
void PlayerConnection::onUnhandledPacket(shared_ptr<Packet> packet)
|
||||
{
|
||||
// logger.warning(getClass() + " wasn't prepared to deal with a " + packet.getClass());
|
||||
@@ -630,6 +654,39 @@ void PlayerConnection::send(shared_ptr<Packet> packet)
|
||||
{
|
||||
if( connection->getSocket() != nullptr )
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Security gate: when require-secure-client is enabled, buffer ALL outgoing
|
||||
// packets until the cipher handshake completes. Only the cipher handshake
|
||||
// CustomPayloadPacket (MC|CKey) is sent immediately. Once the cipher activates,
|
||||
// openSecurityGate() flushes the buffer. This prevents unsecured/old clients
|
||||
// from receiving any game data (PlayerInfoPackets, XUIDs, etc.) before being kicked.
|
||||
if (!m_securityGateOpen)
|
||||
{
|
||||
// Allow cipher handshake packets through immediately
|
||||
if (packet->getId() == 250)
|
||||
{
|
||||
auto cpp = dynamic_pointer_cast<CustomPayloadPacket>(packet);
|
||||
if (cpp != nullptr &&
|
||||
(cpp->identifier == CustomPayloadPacket::CIPHER_KEY_CHANNEL ||
|
||||
cpp->identifier == CustomPayloadPacket::CIPHER_ACK_CHANNEL ||
|
||||
cpp->identifier == CustomPayloadPacket::CIPHER_ON_CHANNEL))
|
||||
{
|
||||
// Fall through to send
|
||||
}
|
||||
else
|
||||
{
|
||||
m_securityBuffer.push_back(packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_securityBuffer.push_back(packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if( !server->getPlayers()->canReceiveAllPackets( player ) )
|
||||
{
|
||||
// Check if we are allowed to send this packet type
|
||||
@@ -1070,10 +1127,19 @@ void PlayerConnection::handleServerSettingsChanged(shared_ptr<ServerSettingsChan
|
||||
{
|
||||
if(packet->action==ServerSettingsChangedPacket::HOST_IN_GAME_SETTINGS)
|
||||
{
|
||||
// Need to check that this player has permission to change each individual setting?
|
||||
|
||||
INetworkPlayer *networkPlayer = getNetworkPlayer();
|
||||
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator())
|
||||
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// On dedicated servers, only the host can change server settings.
|
||||
// Moderators (OPs) should not be able to alter game rules.
|
||||
if (!isHost)
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Non-host player %ls attempted to change server settings\n",
|
||||
player->getName().c_str());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if( isHost || player->isModerator())
|
||||
{
|
||||
app.SetGameHostOption(eGameHostOption_FireSpreads, app.GetGameHostOption(packet->data,eGameHostOption_FireSpreads));
|
||||
app.SetGameHostOption(eGameHostOption_TNT, app.GetGameHostOption(packet->data,eGameHostOption_TNT));
|
||||
@@ -1096,14 +1162,81 @@ void PlayerConnection::handleServerSettingsChanged(shared_ptr<ServerSettingsChan
|
||||
void PlayerConnection::handleKickPlayer(shared_ptr<KickPlayerPacket> packet)
|
||||
{
|
||||
INetworkPlayer *networkPlayer = getNetworkPlayer();
|
||||
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator())
|
||||
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Live ops.json check for non-host players
|
||||
if (!isHost)
|
||||
{
|
||||
PlayerUID kickerXuid = m_offlineXUID;
|
||||
if (kickerXuid == INVALID_XUID) kickerXuid = m_onlineXUID;
|
||||
if (!ServerRuntime::Access::IsPlayerOp(kickerXuid))
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Non-OP player %ls attempted to kick\n", player->getName().c_str());
|
||||
{
|
||||
INetworkPlayer *npLog = getNetworkPlayer();
|
||||
if (npLog != nullptr)
|
||||
ServerRuntime::ServerLogManager::OnUnauthorizedCommand(npLog->GetSmallId(), player->getName(), "kick");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if( isHost || player->isModerator())
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// On dedicated servers, non-host moderators cannot kick other moderators or the host.
|
||||
if (!isHost)
|
||||
{
|
||||
for (auto &checkingPlayer : server->getPlayers()->players)
|
||||
{
|
||||
if (checkingPlayer != nullptr &&
|
||||
checkingPlayer->connection->getNetworkPlayer() != nullptr &&
|
||||
checkingPlayer->connection->getNetworkPlayer()->GetSmallId() == packet->m_networkSmallId)
|
||||
{
|
||||
if (checkingPlayer->isModerator() ||
|
||||
checkingPlayer->connection->getNetworkPlayer()->IsHost())
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Moderator %ls tried to kick host/moderator %ls\n",
|
||||
player->getName().c_str(), checkingPlayer->getName().c_str());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
app.DebugPrintf("CMD: Player %ls kicked player with smallId=%d\n",
|
||||
player->getName().c_str(), packet->m_networkSmallId);
|
||||
#endif
|
||||
server->getPlayers()->kickPlayerByShortId(packet->m_networkSmallId);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerConnection::handleGameCommand(shared_ptr<GameCommandPacket> packet)
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
INetworkPlayer *networkPlayer = getNetworkPlayer();
|
||||
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
|
||||
if (!isHost)
|
||||
{
|
||||
// Live ops.json check - in-memory isModerator() can be stale if ops.json was edited mid-session
|
||||
PlayerUID cmdXuid = m_offlineXUID;
|
||||
if (cmdXuid == INVALID_XUID) cmdXuid = m_onlineXUID;
|
||||
if (!ServerRuntime::Access::IsPlayerOp(cmdXuid))
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Non-OP player %ls attempted server command id=%d\n",
|
||||
player->getName().c_str(), static_cast<int>(packet->command));
|
||||
{
|
||||
INetworkPlayer *npLog = getNetworkPlayer();
|
||||
if (npLog != nullptr)
|
||||
ServerRuntime::ServerLogManager::OnUnauthorizedCommand(npLog->GetSmallId(), player->getName(), "game-command");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
app.DebugPrintf("CMD: Player %ls (OP=%d, Host=%d) executed command id=%d\n",
|
||||
player->getName().c_str(), player->isModerator() ? 1 : 0, isHost ? 1 : 0,
|
||||
static_cast<int>(packet->command));
|
||||
#endif
|
||||
MinecraftServer::getInstance()->getCommandDispatcher()->performCommand(player, packet->command, packet->data);
|
||||
}
|
||||
|
||||
@@ -1373,10 +1506,21 @@ void PlayerConnection::handleKeepAlive(shared_ptr<KeepAlivePacket> packet)
|
||||
|
||||
void PlayerConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
|
||||
{
|
||||
// Need to check that this player has permission to change each individual setting?
|
||||
|
||||
INetworkPlayer *networkPlayer = getNetworkPlayer();
|
||||
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator() )
|
||||
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Live ops.json check for non-host players
|
||||
if (!isHost)
|
||||
{
|
||||
PlayerUID infoXuid = m_offlineXUID;
|
||||
if (infoXuid == INVALID_XUID) infoXuid = m_onlineXUID;
|
||||
if (!ServerRuntime::Access::IsPlayerOp(infoXuid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if( isHost || player->isModerator() )
|
||||
{
|
||||
shared_ptr<ServerPlayer> serverPlayer;
|
||||
// Find the player being edited
|
||||
@@ -1454,7 +1598,24 @@ void PlayerConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
|
||||
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_CanToggleClassicHunger,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_CanToggleClassicHunger) );
|
||||
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_CanTeleport,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_CanTeleport) );
|
||||
}
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// On dedicated servers, OP can only be granted/revoked if the target is in ops.json.
|
||||
// This prevents runtime OP escalation via crafted PlayerInfoPackets.
|
||||
bool wantsOp = Player::getPlayerGamePrivilege(packet->m_playerPrivileges, Player::ePlayerGamePrivilege_Op) != 0;
|
||||
PlayerUID targetXuid = serverPlayer->connection->m_offlineXUID;
|
||||
if (targetXuid == INVALID_XUID) targetXuid = serverPlayer->connection->m_onlineXUID;
|
||||
if (wantsOp && !ServerRuntime::Access::IsPlayerOp(targetXuid))
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Host tried to OP player %ls who is not in ops.json\n",
|
||||
serverPlayer->getName().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op, wantsOp ? 1u : 0u);
|
||||
}
|
||||
#else
|
||||
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_Op) );
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1492,6 +1653,44 @@ void PlayerConnection::handlePlayerAbilities(shared_ptr<PlayerAbilitiesPacket> p
|
||||
|
||||
void PlayerConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> customPayloadPacket)
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Identity token response from client
|
||||
if (CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE.compare(customPayloadPacket->identifier) == 0)
|
||||
{
|
||||
PlayerUID xuid = m_offlineXUID;
|
||||
if (xuid == INVALID_XUID) xuid = m_onlineXUID;
|
||||
|
||||
bool tokenValid = false;
|
||||
if (customPayloadPacket->length == ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE &&
|
||||
customPayloadPacket->data.length == ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE &&
|
||||
customPayloadPacket->data.data != nullptr)
|
||||
{
|
||||
tokenValid = ServerRuntime::Security::GetIdentityTokenManager().VerifyToken(xuid, customPayloadPacket->data.data);
|
||||
}
|
||||
|
||||
if (tokenValid)
|
||||
{
|
||||
m_identityVerified = true;
|
||||
app.DebugPrintf("SECURITY: Identity token verified for player %ls\n", player->getName().c_str());
|
||||
INetworkPlayer *npLog = getNetworkPlayer();
|
||||
if (npLog != nullptr)
|
||||
ServerRuntime::ServerLogManager::OnIdentityTokenVerified(npLog->GetSmallId());
|
||||
}
|
||||
else
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Identity token MISMATCH for player %ls - will disconnect\n", player->getName().c_str());
|
||||
app.DebugPrintf("SECURITY: If this player lost their token, use: revoketoken %ls\n", player->getName().c_str());
|
||||
INetworkPlayer *npLog = getNetworkPlayer();
|
||||
if (npLog != nullptr)
|
||||
ServerRuntime::ServerLogManager::OnIdentityTokenMismatch(npLog->GetSmallId(), player->getName());
|
||||
// Defer disconnect to avoid re-entrancy issues during packet dispatch
|
||||
setWasKicked();
|
||||
closeOnTick();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
if (CustomPayloadPacket.CUSTOM_BOOK_PACKET.equals(customPayloadPacket.identifier))
|
||||
{
|
||||
|
||||
@@ -137,6 +137,21 @@ public:
|
||||
// 4J Added
|
||||
bool hasClientTickedOnce() { return m_bHasClientTickedOnce; }
|
||||
|
||||
// Identity token verification state (accessed from both recv and main threads)
|
||||
std::atomic<bool> m_identityVerified;
|
||||
std::atomic<int> m_identityChallengeTick;
|
||||
|
||||
// Security gate: buffer packets until cipher handshake completes
|
||||
bool m_securityGateOpen;
|
||||
vector<shared_ptr<Packet>> m_securityBuffer;
|
||||
|
||||
bool isIdentityVerified() const { return m_identityVerified; }
|
||||
int getIdentityChallengeTick() const { return m_identityChallengeTick; }
|
||||
void setIdentityChallengeTick(int tick) { m_identityChallengeTick = tick; }
|
||||
void setIdentityVerified(bool v) { m_identityVerified = v; }
|
||||
bool isSecurityGateOpen() const { return m_securityGateOpen; }
|
||||
void openSecurityGate();
|
||||
|
||||
private:
|
||||
bool m_bCloseOnTick;
|
||||
vector<wstring> m_texturesRequested;
|
||||
|
||||
@@ -43,7 +43,13 @@
|
||||
#include "..\Minecraft.Server\ServerLogger.h"
|
||||
#include "..\Minecraft.Server\ServerLogManager.h"
|
||||
#include "..\Minecraft.Server\ServerProperties.h"
|
||||
#include "..\Minecraft.Server\Security\SecurityConfig.h"
|
||||
#include "..\Minecraft.Server\Security\ConnectionCipher.h"
|
||||
#include "..\Minecraft.Server\Security\CipherHandshakeEnforcer.h"
|
||||
#include "..\Minecraft.Server\Security\IdentityTokenManager.h"
|
||||
extern bool g_Win64DedicatedServer;
|
||||
static unsigned int s_playerListTickCount = 0;
|
||||
static const int kIdentityResponseGraceTicks = 200; // 10 seconds at 20 TPS
|
||||
#endif
|
||||
|
||||
// 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc.
|
||||
@@ -267,6 +273,22 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
|
||||
app.DebugPrintf("RECONNECT: placeNewPlayer smallId=%d entityId=%d dim=%d\n",
|
||||
newSmallId, player->entityId, level->dimension->id);
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Close the security gate before sending any game data. All packets will be
|
||||
// buffered until the cipher handshake completes, preventing unsecured clients
|
||||
// from receiving XUIDs or game state during the grace period.
|
||||
if (g_Win64DedicatedServer &&
|
||||
ServerRuntime::Security::GetSettings().enableStreamCipher &&
|
||||
ServerRuntime::Security::GetSettings().requireSecureClient)
|
||||
{
|
||||
INetworkPlayer *gateNp = connection->getSocket() ? connection->getSocket()->getPlayer() : nullptr;
|
||||
if (gateNp != nullptr && !gateNp->IsLocal())
|
||||
{
|
||||
playerConnection->m_securityGateOpen = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
playerConnection->send(std::make_shared<LoginPacket>(L"", player->entityId, level->getLevelData()->getGenerator(),
|
||||
level->getSeed(),
|
||||
player->gameMode->getGameModeForPlayer()->getId(),
|
||||
@@ -338,6 +360,39 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
// Initiate stream cipher handshake if enabled.
|
||||
// Send MC|CKey with the generated key. Old clients will ignore the unknown channel.
|
||||
if (g_Win64DedicatedServer && ServerRuntime::Security::GetSettings().enableStreamCipher)
|
||||
{
|
||||
BYTE smallId = 0;
|
||||
Socket *cipherSock = connection->getSocket();
|
||||
INetworkPlayer *cipherNp = cipherSock ? cipherSock->getPlayer() : nullptr;
|
||||
if (cipherNp != nullptr && !cipherNp->IsLocal())
|
||||
{
|
||||
smallId = cipherNp->GetSmallId();
|
||||
uint8_t key[ServerRuntime::Security::StreamCipher::KEY_SIZE];
|
||||
if (ServerRuntime::Security::GetCipherRegistry().PrepareKey(smallId, key))
|
||||
{
|
||||
byteArray keyData(ServerRuntime::Security::StreamCipher::KEY_SIZE);
|
||||
memcpy(keyData.data, key, ServerRuntime::Security::StreamCipher::KEY_SIZE);
|
||||
playerConnection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::CIPHER_KEY_CHANNEL, keyData));
|
||||
SecureZeroMemory(key, sizeof(key));
|
||||
app.DebugPrintf("Server: Sent MC|CKey to player %ls (smallId=%d)\n",
|
||||
player->getName().c_str(), smallId);
|
||||
|
||||
// Register with enforcer for timeout tracking
|
||||
if (ServerRuntime::Security::GetSettings().requireSecureClient)
|
||||
{
|
||||
ServerRuntime::Security::GetHandshakeEnforcer().OnCipherKeySent(smallId, s_playerListTickCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -570,6 +625,16 @@ void PlayerList::move(shared_ptr<ServerPlayer> player)
|
||||
|
||||
void PlayerList::remove(shared_ptr<ServerPlayer> player)
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
if (g_Win64DedicatedServer && player->connection != nullptr)
|
||||
{
|
||||
INetworkPlayer *np = player->connection->getNetworkPlayer();
|
||||
if (np != nullptr)
|
||||
{
|
||||
ServerRuntime::Security::GetHandshakeEnforcer().OnDisconnected(np->GetSmallId());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
save(player);
|
||||
//4J Stu - We don't want to save the map data for guests, so when we are sure that the player is gone delete the map
|
||||
if(player->isGuest()) playerIo->deleteMapFilesForPlayer(player);
|
||||
@@ -1038,6 +1103,131 @@ void PlayerList::repositionAcrossDimension(shared_ptr<Entity> entity, int lastDi
|
||||
|
||||
void PlayerList::tick()
|
||||
{
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
++s_playerListTickCount;
|
||||
|
||||
// Cipher handshake enforcement: kick clients that haven't completed the handshake
|
||||
if (g_Win64DedicatedServer &&
|
||||
ServerRuntime::Security::GetSettings().enableStreamCipher &&
|
||||
ServerRuntime::Security::GetSettings().requireSecureClient)
|
||||
{
|
||||
std::vector<unsigned char> expired;
|
||||
std::vector<unsigned char> completed;
|
||||
ServerRuntime::Security::GetHandshakeEnforcer().CheckTimeouts(s_playerListTickCount, expired, completed);
|
||||
|
||||
for (unsigned char smallId : expired)
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Kicking unsecured client (smallId=%d) - cipher handshake timed out\n", smallId);
|
||||
ServerRuntime::ServerLogManager::OnUnsecuredClientKicked(smallId);
|
||||
EnterCriticalSection(&m_closePlayersCS);
|
||||
m_smallIdsToClose.push_back(smallId);
|
||||
LeaveCriticalSection(&m_closePlayersCS);
|
||||
}
|
||||
|
||||
// Report cipher completion and open security gate for all completed handshakes
|
||||
for (unsigned char smallId : completed)
|
||||
{
|
||||
// Open the security gate -- flush buffered game packets now that cipher is active
|
||||
for (auto &p : players)
|
||||
{
|
||||
if (p == nullptr || p->connection == nullptr) continue;
|
||||
INetworkPlayer *np = p->connection->getNetworkPlayer();
|
||||
if (np != nullptr && np->GetSmallId() == smallId)
|
||||
{
|
||||
if (!p->connection->isSecurityGateOpen())
|
||||
{
|
||||
p->connection->openSecurityGate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ServerRuntime::Security::GetSettings().requireChallengeToken)
|
||||
{
|
||||
ServerRuntime::ServerLogManager::OnCipherHandshakeCompleted(smallId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerRuntime::ServerLogManager::OnCipherCompletedNoTokenRequired(smallId);
|
||||
}
|
||||
}
|
||||
|
||||
// For newly-completed cipher handshakes, initiate identity token exchange
|
||||
if (ServerRuntime::Security::GetSettings().requireChallengeToken)
|
||||
{
|
||||
for (unsigned char smallId : completed)
|
||||
{
|
||||
// Find the player by smallId
|
||||
for (auto &p : players)
|
||||
{
|
||||
if (p == nullptr || p->connection == nullptr) continue;
|
||||
INetworkPlayer *np = p->connection->getNetworkPlayer();
|
||||
if (np == nullptr || np->GetSmallId() != smallId) continue;
|
||||
|
||||
PlayerUID xuid = p->connection->m_offlineXUID;
|
||||
if (xuid == INVALID_XUID) xuid = p->connection->m_onlineXUID;
|
||||
|
||||
if (p->connection->getIdentityChallengeTick() >= 0)
|
||||
{
|
||||
// Already challenged, skip
|
||||
}
|
||||
else if (ServerRuntime::Security::GetIdentityTokenManager().HasToken(xuid))
|
||||
{
|
||||
// Returning player - challenge them
|
||||
p->connection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::IDENTITY_TOKEN_CHALLENGE, byteArray()));
|
||||
p->connection->setIdentityChallengeTick(s_playerListTickCount);
|
||||
app.DebugPrintf("Server: Sent identity challenge to %ls (smallId=%d)\n",
|
||||
p->getName().c_str(), smallId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// New player - issue a token over the encrypted channel
|
||||
uint8_t token[ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE];
|
||||
if (ServerRuntime::Security::GetIdentityTokenManager().IssueToken(xuid, token))
|
||||
{
|
||||
byteArray tokenData(ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE);
|
||||
memcpy(tokenData.data, token, ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE);
|
||||
p->connection->send(std::make_shared<CustomPayloadPacket>(
|
||||
CustomPayloadPacket::IDENTITY_TOKEN_ISSUE, tokenData));
|
||||
SecureZeroMemory(token, sizeof(token));
|
||||
p->connection->setIdentityVerified(true);
|
||||
app.DebugPrintf("Server: Issued identity token to %ls (smallId=%d)\n",
|
||||
p->getName().c_str(), smallId);
|
||||
ServerRuntime::ServerLogManager::OnIdentityTokenIssued(smallId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce identity token response timeout
|
||||
for (auto &p : players)
|
||||
{
|
||||
if (p == nullptr || p->connection == nullptr) continue;
|
||||
int challengeTick = p->connection->getIdentityChallengeTick();
|
||||
if (challengeTick >= 0 && !p->connection->isIdentityVerified() &&
|
||||
(s_playerListTickCount - challengeTick) > kIdentityResponseGraceTicks)
|
||||
{
|
||||
app.DebugPrintf("SECURITY: Kicking %ls - identity token response timed out\n",
|
||||
p->getName().c_str());
|
||||
INetworkPlayer *npLog = p->connection->getNetworkPlayer();
|
||||
if (npLog != nullptr)
|
||||
ServerRuntime::ServerLogManager::OnIdentityTokenTimeout(npLog->GetSmallId(), p->getName());
|
||||
p->connection->setIdentityChallengeTick(-1); // prevent re-queuing
|
||||
INetworkPlayer *np = p->connection->getNetworkPlayer();
|
||||
if (np != nullptr)
|
||||
{
|
||||
EnterCriticalSection(&m_closePlayersCS);
|
||||
m_smallIdsToClose.push_back(np->GetSmallId());
|
||||
LeaveCriticalSection(&m_closePlayersCS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// 4J - brought changes to how often this is sent forward from 1.2.3
|
||||
if (++sendAllPlayerInfoIn > SEND_PLAYER_INFO_INTERVAL)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#include "..\Minecraft.World\Socket.h"
|
||||
#include "..\Minecraft.World\net.minecraft.world.level.h"
|
||||
#include "MultiPlayerLevel.h"
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\Minecraft.Server\Security\SecurityConfig.h"
|
||||
#include "..\Minecraft.Server\ServerLogManager.h"
|
||||
#endif
|
||||
|
||||
ServerConnection::ServerConnection(MinecraftServer *server)
|
||||
{
|
||||
@@ -40,6 +44,17 @@ void ServerConnection::addPlayerConnection(shared_ptr<PlayerConnection> uc)
|
||||
void ServerConnection::handleConnection(shared_ptr<PendingConnection> uc)
|
||||
{
|
||||
EnterCriticalSection(&pending_cs);
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
int maxPending = ServerRuntime::Security::GetSettings().maxPendingConnections;
|
||||
if (maxPending > 0 && static_cast<int>(pending.size()) >= maxPending)
|
||||
{
|
||||
LeaveCriticalSection(&pending_cs);
|
||||
app.DebugPrintf("SECURITY: Rejecting connection, too many pending (%d/%d)\n",
|
||||
static_cast<int>(pending.size()), maxPending);
|
||||
uc->disconnect(DisconnectPacket::eDisconnect_ServerFull);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
pending.push_back(uc);
|
||||
LeaveCriticalSection(&pending_cs);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\..\..\Minecraft.Server\Access\Access.h"
|
||||
#include "..\..\..\Minecraft.Server\ServerLogManager.h"
|
||||
#include "..\..\..\Minecraft.Server\ServerLogger.h"
|
||||
#include "..\..\..\Minecraft.Server\Security\SecurityConfig.h"
|
||||
#include "..\..\..\Minecraft.Server\Security\RateLimiter.h"
|
||||
#include "..\..\..\Minecraft.Server\Security\ConnectionCipher.h"
|
||||
#endif
|
||||
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
|
||||
#include "..\..\Minecraft.h"
|
||||
@@ -25,6 +29,28 @@ static bool RecvExact(SOCKET sock, BYTE* buf, int len);
|
||||
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp);
|
||||
#endif
|
||||
|
||||
// Raw serialized byte patterns for cipher handshake packets (CustomPayloadPacket ID 250).
|
||||
// Used by recv threads to detect handshake messages at the byte level before packet parsing,
|
||||
// enabling atomic cipher activation at the exact byte boundary.
|
||||
|
||||
// MC|CAck: 7-char channel, empty payload. Client sends this; server recv thread matches it.
|
||||
static const BYTE kCipherAckPattern[] = {
|
||||
0xFA, // packet ID 250
|
||||
0x00, 0x07, // channel length = 7
|
||||
0x00, 0x4D, 0x00, 0x43, 0x00, 0x7C, 0x00, 0x43, 0x00, 0x41, 0x00, 0x63, 0x00, 0x6B, // "MC|CAck" UTF-16BE
|
||||
0x00, 0x00 // data length = 0
|
||||
};
|
||||
static const int kCipherAckPatternSize = sizeof(kCipherAckPattern); // 19
|
||||
|
||||
// MC|COn: 6-char channel, empty payload. Client recv thread matches this from server.
|
||||
static const BYTE kCipherOnPattern[] = {
|
||||
0xFA, // packet ID 250
|
||||
0x00, 0x06, // channel length = 6
|
||||
0x00, 0x4D, 0x00, 0x43, 0x00, 0x7C, 0x00, 0x43, 0x00, 0x4F, 0x00, 0x6E, // "MC|COn" UTF-16BE
|
||||
0x00, 0x00 // data length = 0
|
||||
};
|
||||
static const int kCipherOnPatternSize = sizeof(kCipherOnPattern); // 17
|
||||
|
||||
SOCKET WinsockNetLayer::s_listenSocket = INVALID_SOCKET;
|
||||
SOCKET WinsockNetLayer::s_hostConnectionSocket = INVALID_SOCKET;
|
||||
HANDLE WinsockNetLayer::s_acceptThread = nullptr;
|
||||
@@ -78,6 +104,12 @@ int WinsockNetLayer::s_joinPort = 0;
|
||||
BYTE WinsockNetLayer::s_joinAssignedSmallId = 0;
|
||||
DisconnectPacket::eDisconnectReason WinsockNetLayer::s_joinRejectReason = DisconnectPacket::eDisconnect_Quitting;
|
||||
|
||||
ServerRuntime::Security::StreamCipher WinsockNetLayer::s_clientSendCipher;
|
||||
ServerRuntime::Security::StreamCipher WinsockNetLayer::s_clientRecvCipher;
|
||||
CRITICAL_SECTION WinsockNetLayer::s_clientCipherLock;
|
||||
uint8_t WinsockNetLayer::s_clientPendingKey[ServerRuntime::Security::StreamCipher::KEY_SIZE] = {};
|
||||
bool WinsockNetLayer::s_clientKeyStored = false;
|
||||
|
||||
bool g_Win64MultiplayerHost = false;
|
||||
bool g_Win64MultiplayerJoin = false;
|
||||
int g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT;
|
||||
@@ -106,6 +138,7 @@ bool WinsockNetLayer::Initialize()
|
||||
InitializeCriticalSection(&s_disconnectLock);
|
||||
InitializeCriticalSection(&s_freeSmallIdLock);
|
||||
InitializeCriticalSection(&s_smallIdToSocketLock);
|
||||
InitializeCriticalSection(&s_clientCipherLock);
|
||||
for (int i = 0; i < 256; i++)
|
||||
s_smallIdToSocket[i] = INVALID_SOCKET;
|
||||
|
||||
@@ -219,6 +252,8 @@ void WinsockNetLayer::Shutdown()
|
||||
s_freeSmallIds.clear();
|
||||
LeaveCriticalSection(&s_freeSmallIdLock);
|
||||
|
||||
ResetClientCipher();
|
||||
DeleteCriticalSection(&s_clientCipherLock);
|
||||
DeleteCriticalSection(&s_sendLock);
|
||||
DeleteCriticalSection(&s_connectionsLock);
|
||||
DeleteCriticalSection(&s_advertiseLock);
|
||||
@@ -231,6 +266,163 @@ void WinsockNetLayer::Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
void WinsockNetLayer::StoreClientCipherKey(const uint8_t key[ServerRuntime::Security::StreamCipher::KEY_SIZE])
|
||||
{
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
memcpy(s_clientPendingKey, key, ServerRuntime::Security::StreamCipher::KEY_SIZE);
|
||||
s_clientKeyStored = true;
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
}
|
||||
|
||||
bool WinsockNetLayer::SendAckAndActivateClientSendCipher()
|
||||
{
|
||||
if (s_hostConnectionSocket == INVALID_SOCKET)
|
||||
return false;
|
||||
|
||||
// Atomic: send the MC|CAck plaintext then activate the send cipher, all under s_sendLock.
|
||||
// No other send can interleave between the ack and cipher activation.
|
||||
EnterCriticalSection(&s_sendLock);
|
||||
|
||||
// Write framed packet: 4-byte length header + ack pattern
|
||||
BYTE header[4];
|
||||
header[0] = static_cast<BYTE>((kCipherAckPatternSize >> 24) & 0xFF);
|
||||
header[1] = static_cast<BYTE>((kCipherAckPatternSize >> 16) & 0xFF);
|
||||
header[2] = static_cast<BYTE>((kCipherAckPatternSize >> 8) & 0xFF);
|
||||
header[3] = static_cast<BYTE>(kCipherAckPatternSize & 0xFF);
|
||||
|
||||
bool ok = true;
|
||||
int totalSent = 0;
|
||||
while (ok && totalSent < 4)
|
||||
{
|
||||
int sent = send(s_hostConnectionSocket, (const char *)header + totalSent, 4 - totalSent, 0);
|
||||
if (sent == SOCKET_ERROR || sent == 0) { ok = false; break; }
|
||||
totalSent += sent;
|
||||
}
|
||||
totalSent = 0;
|
||||
while (ok && totalSent < kCipherAckPatternSize)
|
||||
{
|
||||
int sent = send(s_hostConnectionSocket, (const char *)kCipherAckPattern + totalSent, kCipherAckPatternSize - totalSent, 0);
|
||||
if (sent == SOCKET_ERROR || sent == 0) { ok = false; break; }
|
||||
totalSent += sent;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
// Activate send cipher immediately after the ack is on the wire
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
s_clientSendCipher.Initialize(s_clientPendingKey);
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
app.DebugPrintf("Client: Send cipher activated (MC|CAck sent)\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial send corrupts the stream - force disconnect to prevent desync
|
||||
app.DebugPrintf("Client: MC|CAck send failed, closing connection\n");
|
||||
closesocket(s_hostConnectionSocket);
|
||||
s_hostConnectionSocket = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
LeaveCriticalSection(&s_sendLock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void WinsockNetLayer::ActivateClientRecvCipher()
|
||||
{
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
s_clientRecvCipher.Initialize(s_clientPendingKey);
|
||||
SecureZeroMemory(s_clientPendingKey, sizeof(s_clientPendingKey));
|
||||
s_clientKeyStored = false;
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
}
|
||||
|
||||
void WinsockNetLayer::ResetClientCipher()
|
||||
{
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
s_clientSendCipher.Reset();
|
||||
s_clientRecvCipher.Reset();
|
||||
SecureZeroMemory(s_clientPendingKey, sizeof(s_clientPendingKey));
|
||||
s_clientKeyStored = false;
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
}
|
||||
|
||||
bool WinsockNetLayer::TryEncryptClientOutgoing(uint8_t *data, int length)
|
||||
{
|
||||
if (data == nullptr || length <= 0)
|
||||
return false;
|
||||
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
bool active = s_clientSendCipher.IsActive();
|
||||
if (active)
|
||||
{
|
||||
s_clientSendCipher.Encrypt(data, length);
|
||||
}
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
return active;
|
||||
}
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
bool WinsockNetLayer::SendCOnAndCommitServerCipher(BYTE smallId)
|
||||
{
|
||||
// Verify a pending key exists before sending MC|COn (prevents rogue ack from triggering spurious activation)
|
||||
auto ®istry = ServerRuntime::Security::GetCipherRegistry();
|
||||
|
||||
SOCKET sock = GetSocketForSmallId(smallId);
|
||||
if (sock == INVALID_SOCKET)
|
||||
return false;
|
||||
|
||||
// Verify a pending key exists before sending (rejects rogue acks)
|
||||
if (!registry.HasPendingKey(smallId))
|
||||
{
|
||||
app.DebugPrintf("Server: Ignoring MC|CAck for smallId=%d (no pending key)\n", smallId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Atomic: send MC|COn plaintext then commit the cipher, all under s_sendLock.
|
||||
// No other send to this smallId can happen between MC|COn and CommitCipher.
|
||||
EnterCriticalSection(&s_sendLock);
|
||||
|
||||
BYTE header[4];
|
||||
header[0] = static_cast<BYTE>((kCipherOnPatternSize >> 24) & 0xFF);
|
||||
header[1] = static_cast<BYTE>((kCipherOnPatternSize >> 16) & 0xFF);
|
||||
header[2] = static_cast<BYTE>((kCipherOnPatternSize >> 8) & 0xFF);
|
||||
header[3] = static_cast<BYTE>(kCipherOnPatternSize & 0xFF);
|
||||
|
||||
bool ok = true;
|
||||
int totalSent = 0;
|
||||
while (ok && totalSent < 4)
|
||||
{
|
||||
int sent = send(sock, (const char *)header + totalSent, 4 - totalSent, 0);
|
||||
if (sent == SOCKET_ERROR || sent == 0) { ok = false; break; }
|
||||
totalSent += sent;
|
||||
}
|
||||
totalSent = 0;
|
||||
while (ok && totalSent < kCipherOnPatternSize)
|
||||
{
|
||||
int sent = send(sock, (const char *)kCipherOnPattern + totalSent, kCipherOnPatternSize - totalSent, 0);
|
||||
if (sent == SOCKET_ERROR || sent == 0) { ok = false; break; }
|
||||
totalSent += sent;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
// Commit AFTER the send - MC|COn is the last plaintext packet
|
||||
registry.CommitCipher(smallId);
|
||||
app.DebugPrintf("Server: Cipher committed for smallId=%d (MC|COn sent)\n", smallId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial send corrupts the stream - force close
|
||||
app.DebugPrintf("Server: MC|COn send failed for smallId=%d, closing socket\n", smallId);
|
||||
registry.CancelPending(smallId);
|
||||
closesocket(sock);
|
||||
ClearSocketForSmallId(smallId);
|
||||
}
|
||||
|
||||
LeaveCriticalSection(&s_sendLock);
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool WinsockNetLayer::HostGame(int port, const char* bindIp)
|
||||
{
|
||||
if (!s_initialized && !Initialize()) return false;
|
||||
@@ -828,10 +1020,37 @@ bool WinsockNetLayer::SendToSmallId(BYTE targetSmallId, const void* data, int da
|
||||
{
|
||||
SOCKET sock = GetSocketForSmallId(targetSmallId);
|
||||
if (sock == INVALID_SOCKET) return false;
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
// Encrypt outgoing data if a cipher is active for this connection.
|
||||
// TryEncryptOutgoing atomically checks and encrypts under a single lock
|
||||
// to avoid TOCTOU races with DeactivateCipher on disconnect.
|
||||
if (g_Win64DedicatedServer && dataSize > 0)
|
||||
{
|
||||
std::vector<BYTE> buf(static_cast<const BYTE*>(data),
|
||||
static_cast<const BYTE*>(data) + dataSize);
|
||||
if (ServerRuntime::Security::GetCipherRegistry().TryEncryptOutgoing(
|
||||
targetSmallId, buf.data(), dataSize))
|
||||
{
|
||||
return SendOnSocket(sock, buf.data(), dataSize);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return SendOnSocket(sock, data, dataSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Client sending to server - encrypt if send cipher is active
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
if (s_clientSendCipher.IsActive() && dataSize > 0)
|
||||
{
|
||||
std::vector<BYTE> buf(static_cast<const BYTE*>(data),
|
||||
static_cast<const BYTE*>(data) + dataSize);
|
||||
s_clientSendCipher.Encrypt(buf.data(), dataSize);
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
return SendOnSocket(s_hostConnectionSocket, buf.data(), dataSize);
|
||||
}
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
return SendOnSocket(s_hostConnectionSocket, data, dataSize);
|
||||
}
|
||||
}
|
||||
@@ -896,6 +1115,128 @@ static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string
|
||||
*outIp = ip;
|
||||
return true;
|
||||
}
|
||||
|
||||
enum EProxyParseResult
|
||||
{
|
||||
eProxyParse_Success, // Valid PROXY TCP4 header, IP extracted
|
||||
eProxyParse_Unknown, // Valid PROXY UNKNOWN header, no IP available
|
||||
eProxyParse_Malformed, // Invalid header format
|
||||
eProxyParse_Timeout, // Recv timed out
|
||||
eProxyParse_SocketError // Socket error during read
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a PROXY protocol v1 header from the socket.
|
||||
* Must be called immediately after accept(), before any game data is read.
|
||||
* Sets a 5-second recv timeout, reads the header, restores timeout on all paths.
|
||||
*/
|
||||
static EProxyParseResult TryReadProxyProtocolHeader(SOCKET sock, std::string *outSrcIp)
|
||||
{
|
||||
if (outSrcIp != nullptr)
|
||||
outSrcIp->clear();
|
||||
|
||||
// Set 5-second recv timeout for the header read
|
||||
DWORD timeout = 5000;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
|
||||
|
||||
auto restoreTimeout = [sock]() {
|
||||
DWORD noTimeout = 0;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&noTimeout, sizeof(noTimeout));
|
||||
};
|
||||
|
||||
// Peek at first 6 bytes to check for "PROXY " prefix
|
||||
char peekBuf[6];
|
||||
int peekResult = recv(sock, peekBuf, 6, MSG_PEEK);
|
||||
if (peekResult == 0)
|
||||
{
|
||||
restoreTimeout();
|
||||
return eProxyParse_SocketError;
|
||||
}
|
||||
if (peekResult == SOCKET_ERROR)
|
||||
{
|
||||
restoreTimeout();
|
||||
int err = WSAGetLastError();
|
||||
return (err == WSAETIMEDOUT) ? eProxyParse_Timeout : eProxyParse_SocketError;
|
||||
}
|
||||
if (peekResult < 6 || memcmp(peekBuf, "PROXY ", 6) != 0)
|
||||
{
|
||||
restoreTimeout();
|
||||
return eProxyParse_Malformed;
|
||||
}
|
||||
|
||||
// Consume header byte-by-byte until \r\n (max 107 bytes per PROXY v1 spec)
|
||||
char lineBuf[108] = {};
|
||||
int lineLen = 0;
|
||||
bool foundEnd = false;
|
||||
|
||||
while (lineLen < 107)
|
||||
{
|
||||
char ch;
|
||||
int r = recv(sock, &ch, 1, 0);
|
||||
if (r != 1)
|
||||
{
|
||||
restoreTimeout();
|
||||
int err = WSAGetLastError();
|
||||
return (r == SOCKET_ERROR && err == WSAETIMEDOUT) ? eProxyParse_Timeout : eProxyParse_SocketError;
|
||||
}
|
||||
lineBuf[lineLen++] = ch;
|
||||
|
||||
if (lineLen >= 2 && lineBuf[lineLen - 2] == '\r' && lineBuf[lineLen - 1] == '\n')
|
||||
{
|
||||
foundEnd = true;
|
||||
lineBuf[lineLen - 2] = '\0'; // null-terminate, strip \r\n
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
restoreTimeout();
|
||||
|
||||
if (!foundEnd)
|
||||
{
|
||||
return eProxyParse_Malformed;
|
||||
}
|
||||
|
||||
// Parse: "PROXY TCP4 <src_ip> <dst_ip> <src_port> <dst_port>"
|
||||
// or: "PROXY UNKNOWN"
|
||||
char *tokens[6] = {};
|
||||
int tokenCount = 0;
|
||||
char *ctx = nullptr;
|
||||
char *tok = strtok_s(lineBuf, " ", &ctx);
|
||||
while (tok != nullptr && tokenCount < 6)
|
||||
{
|
||||
tokens[tokenCount++] = tok;
|
||||
tok = strtok_s(nullptr, " ", &ctx);
|
||||
}
|
||||
|
||||
if (tokenCount < 2 || strcmp(tokens[0], "PROXY") != 0)
|
||||
{
|
||||
return eProxyParse_Malformed;
|
||||
}
|
||||
|
||||
if (strcmp(tokens[1], "UNKNOWN") == 0)
|
||||
{
|
||||
return eProxyParse_Unknown;
|
||||
}
|
||||
|
||||
if (strcmp(tokens[1], "TCP4") != 0 || tokenCount < 6)
|
||||
{
|
||||
return eProxyParse_Malformed;
|
||||
}
|
||||
|
||||
// Validate src_ip with inet_pton
|
||||
struct in_addr addr;
|
||||
if (inet_pton(AF_INET, tokens[2], &addr) != 1)
|
||||
{
|
||||
return eProxyParse_Malformed;
|
||||
}
|
||||
|
||||
if (outSrcIp != nullptr)
|
||||
{
|
||||
*outSrcIp = tokens[2];
|
||||
}
|
||||
|
||||
return eProxyParse_Success;
|
||||
}
|
||||
#endif
|
||||
|
||||
void WinsockNetLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsigned char* data, unsigned int dataSize)
|
||||
@@ -948,7 +1289,36 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
std::string remoteIp;
|
||||
const bool hasRemoteIp = TryGetNumericRemoteIp(remoteAddress, &remoteIp);
|
||||
bool hasRemoteIp = TryGetNumericRemoteIp(remoteAddress, &remoteIp);
|
||||
|
||||
// PROXY protocol v1: parse real client IP from tunnel header
|
||||
if (g_Win64DedicatedServer && ServerRuntime::Security::GetSettings().proxyProtocol)
|
||||
{
|
||||
std::string proxiedIp;
|
||||
EProxyParseResult proxyResult = TryReadProxyProtocolHeader(clientSocket, &proxiedIp);
|
||||
if (proxyResult == eProxyParse_Success)
|
||||
{
|
||||
ServerRuntime::LogInfof("network", "PROXY: real client IP %s (tunnel: %s)",
|
||||
proxiedIp.c_str(), hasRemoteIp ? remoteIp.c_str() : "unknown");
|
||||
remoteIp = proxiedIp;
|
||||
hasRemoteIp = true;
|
||||
}
|
||||
else if (proxyResult == eProxyParse_Unknown)
|
||||
{
|
||||
ServerRuntime::LogInfof("network", "PROXY: UNKNOWN header, keeping tunnel IP");
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerRuntime::LogWarnf("network", "PROXY: header parse failed (result=%d) from %s",
|
||||
(int)proxyResult, hasRemoteIp ? remoteIp.c_str() : "unknown");
|
||||
const char *rejectIp = hasRemoteIp ? remoteIp.c_str() : "unknown";
|
||||
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(rejectIp,
|
||||
ServerRuntime::ServerLogManager::eTcpRejectReason_InvalidProxyHeader);
|
||||
closesocket(clientSocket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const char *remoteIpForLog = hasRemoteIp ? remoteIp.c_str() : "unknown";
|
||||
if (g_Win64DedicatedServer)
|
||||
{
|
||||
@@ -960,6 +1330,22 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
|
||||
closesocket(clientSocket);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rate limiting: reject connections that exceed the per-IP sliding window
|
||||
if (hasRemoteIp)
|
||||
{
|
||||
const auto &secSettings = ServerRuntime::Security::GetSettings();
|
||||
bool allowed = ServerRuntime::Security::GetGlobalRateLimiter().AllowConnection(
|
||||
remoteIp,
|
||||
secSettings.rateLimitConnectionsPerWindow,
|
||||
secSettings.rateLimitWindowSeconds * 1000);
|
||||
if (!allowed)
|
||||
{
|
||||
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_RateLimited);
|
||||
closesocket(clientSocket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1138,6 +1524,25 @@ DWORD WINAPI WinsockNetLayer::RecvThreadProc(LPVOID param)
|
||||
break;
|
||||
}
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
// Check for MC|CAck cipher handshake (raw byte match, before decryption).
|
||||
// The ack is always plaintext - it's the last plaintext packet from the client.
|
||||
if (g_Win64DedicatedServer &&
|
||||
packetSize == kCipherAckPatternSize &&
|
||||
memcmp(&recvBuf[0], kCipherAckPattern, kCipherAckPatternSize) == 0)
|
||||
{
|
||||
// Atomically send MC|COn plaintext then commit the cipher
|
||||
SendCOnAndCommitServerCipher(clientSmallId);
|
||||
continue; // consumed - do not pass to game packet handler
|
||||
}
|
||||
|
||||
// Decrypt incoming data if a cipher is active for this connection
|
||||
if (g_Win64DedicatedServer)
|
||||
{
|
||||
ServerRuntime::Security::GetCipherRegistry().DecryptIncoming(clientSmallId, &recvBuf[0], packetSize);
|
||||
}
|
||||
#endif
|
||||
|
||||
HandleDataReceived(clientSmallId, s_hostSmallId, &recvBuf[0], packetSize);
|
||||
}
|
||||
|
||||
@@ -1180,6 +1585,14 @@ bool WinsockNetLayer::PopDisconnectedSmallId(BYTE* outSmallId)
|
||||
|
||||
void WinsockNetLayer::PushFreeSmallId(BYTE smallId)
|
||||
{
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
// Clean up any active cipher for this connection
|
||||
if (g_Win64DedicatedServer)
|
||||
{
|
||||
ServerRuntime::Security::GetCipherRegistry().DeactivateCipher(smallId);
|
||||
}
|
||||
#endif
|
||||
|
||||
// SmallIds 0..(XUSER_MAX_COUNT-1) are permanently reserved for the host's
|
||||
// local pads and must never be recycled to remote clients.
|
||||
if (smallId < (BYTE)XUSER_MAX_COUNT)
|
||||
@@ -1416,10 +1829,29 @@ DWORD WINAPI WinsockNetLayer::ClientRecvThreadProc(LPVOID param)
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for MC|COn cipher activation signal (raw byte match, before decryption).
|
||||
// This is always sent in plaintext as the last plaintext packet from the server.
|
||||
if (packetSize == kCipherOnPatternSize &&
|
||||
memcmp(&recvBuf[0], kCipherOnPattern, kCipherOnPatternSize) == 0)
|
||||
{
|
||||
ActivateClientRecvCipher();
|
||||
app.DebugPrintf("Client: Recv cipher activated (MC|COn received)\n");
|
||||
continue; // consumed - do not pass to game packet handler
|
||||
}
|
||||
|
||||
// Decrypt incoming data if recv cipher is active
|
||||
EnterCriticalSection(&s_clientCipherLock);
|
||||
if (s_clientRecvCipher.IsActive())
|
||||
{
|
||||
s_clientRecvCipher.Decrypt(&recvBuf[0], packetSize);
|
||||
}
|
||||
LeaveCriticalSection(&s_clientCipherLock);
|
||||
|
||||
HandleDataReceived(s_hostSmallId, s_localSmallId, &recvBuf[0], packetSize);
|
||||
}
|
||||
|
||||
s_connected = false;
|
||||
ResetClientCipher();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
#include "..\..\Common\Network\NetworkPlayerInterface.h"
|
||||
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
|
||||
#include "..\..\..\Minecraft.Server\Security\StreamCipher.h"
|
||||
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
|
||||
@@ -16,7 +17,7 @@
|
||||
#define WIN64_NET_MAX_CLIENTS 255
|
||||
#define WIN64_SMALLID_REJECT 0xFF
|
||||
#define WIN64_NET_RECV_BUFFER_SIZE 65536
|
||||
#define WIN64_NET_MAX_PACKET_SIZE (4 * 1024 * 1024)
|
||||
#define WIN64_NET_MAX_PACKET_SIZE (512 * 1024)
|
||||
#define WIN64_LAN_DISCOVERY_PORT 25566
|
||||
#define WIN64_LAN_BROADCAST_MAGIC 0x4D434C4E
|
||||
|
||||
@@ -190,8 +191,38 @@ private:
|
||||
static BYTE s_splitScreenSmallId[XUSER_MAX_COUNT];
|
||||
static HANDLE s_splitScreenRecvThread[XUSER_MAX_COUNT];
|
||||
|
||||
// Client-side stream cipher (non-host only, one connection to server)
|
||||
static ServerRuntime::Security::StreamCipher s_clientSendCipher;
|
||||
static ServerRuntime::Security::StreamCipher s_clientRecvCipher;
|
||||
static CRITICAL_SECTION s_clientCipherLock;
|
||||
static uint8_t s_clientPendingKey[ServerRuntime::Security::StreamCipher::KEY_SIZE];
|
||||
static bool s_clientKeyStored; // protected by s_clientCipherLock
|
||||
|
||||
public:
|
||||
static void ClearSocketForSmallId(BYTE smallId);
|
||||
|
||||
/** Store the cipher key received from the server. Does not activate yet. */
|
||||
static void StoreClientCipherKey(const uint8_t key[ServerRuntime::Security::StreamCipher::KEY_SIZE]);
|
||||
|
||||
/** Send MC|CAck directly to socket then activate client send cipher. Atomic under s_sendLock. */
|
||||
static bool SendAckAndActivateClientSendCipher();
|
||||
|
||||
/** Activate client recv cipher. Called from ClientRecvThreadProc on MC|COn detection. */
|
||||
static void ActivateClientRecvCipher();
|
||||
|
||||
/** Reset client ciphers on disconnect. */
|
||||
static void ResetClientCipher();
|
||||
|
||||
/**
|
||||
* Encrypt data in-place for client->server send if the client send cipher is active.
|
||||
* Returns true if data was encrypted. Thread-safe.
|
||||
*/
|
||||
static bool TryEncryptClientOutgoing(uint8_t *data, int length);
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
/** Atomically send MC|COn plaintext then commit server cipher. Called from RecvThreadProc. */
|
||||
static bool SendCOnAndCommitServerCipher(BYTE smallId);
|
||||
#endif
|
||||
};
|
||||
|
||||
extern bool g_Win64MultiplayerHost;
|
||||
|
||||
@@ -410,6 +410,8 @@ source_group("Windows64/Iggy/gdraw" FILES ${_MINECRAFT_CLIENT_COMMON_WINDOWS64_I
|
||||
set(_MINECRAFT_CLIENT_COMMON_WINDOWS64_NETWORK
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Windows64/Network/WinsockNetLayer.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Windows64/Network/WinsockNetLayer.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Server/Security/StreamCipher.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Server/Security/StreamCipher.h"
|
||||
)
|
||||
source_group("Windows64/Network" FILES ${_MINECRAFT_CLIENT_COMMON_WINDOWS64_NETWORK})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user