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:
itsRevela
2026-03-28 19:18:06 -05:00
parent ed3fffcc6a
commit ba3ebe666c
42 changed files with 3293 additions and 34 deletions
+139 -2
View File
@@ -27,6 +27,7 @@ namespace ServerRuntime
std::mutex writeLock;
std::shared_ptr<BanManager> banManager;
std::shared_ptr<WhitelistManager> whitelistManager;
std::shared_ptr<OpManager> opManager;
bool whitelistEnabled = false;
};
@@ -63,6 +64,18 @@ namespace ServerRuntime
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistManager = whitelistManager;
}
static std::shared_ptr<OpManager> GetOpManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.opManager;
}
static void PublishOpManagerSnapshot(const std::shared_ptr<OpManager> &opManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.opManager = opManager;
}
}
std::string FormatXuid(PlayerUID xuid)
@@ -101,6 +114,7 @@ namespace ServerRuntime
// Build the replacement manager privately so readers keep using the last published snapshot during disk I/O.
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(baseDirectory);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(baseDirectory);
std::shared_ptr<OpManager> opManager = std::make_shared<OpManager>(baseDirectory);
if (!banManager->EnsureBanFilesExist())
{
LogError("access", "failed to ensure dedicated server ban files exist");
@@ -111,6 +125,11 @@ namespace ServerRuntime
LogError("access", "failed to ensure dedicated server whitelist file exists");
return false;
}
if (!opManager->EnsureOpFileExists())
{
LogError("access", "failed to ensure dedicated server ops file exists");
return false;
}
if (!banManager->Reload())
{
@@ -122,15 +141,23 @@ namespace ServerRuntime
LogError("access", "failed to load dedicated server whitelist file");
return false;
}
if (!opManager->Reload())
{
LogError("access", "failed to load dedicated server ops file");
return false;
}
std::vector<BannedPlayerEntry> playerEntries;
std::vector<BannedIpEntry> ipEntries;
std::vector<WhitelistedPlayerEntry> whitelistEntries;
std::vector<OpPlayerEntry> opEntries;
banManager->SnapshotBannedPlayers(&playerEntries);
banManager->SnapshotBannedIps(&ipEntries);
whitelistManager->SnapshotWhitelistedPlayers(&whitelistEntries);
opManager->SnapshotOps(&opEntries);
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
PublishOpManagerSnapshot(opManager);
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = whitelistEnabled;
@@ -138,10 +165,11 @@ namespace ServerRuntime
LogInfof(
"access",
"loaded %u player bans, %u ip bans, and %u whitelist entries (whitelist=%s)",
"loaded %u player bans, %u ip bans, %u whitelist entries, and %u ops (whitelist=%s)",
(unsigned)playerEntries.size(),
(unsigned)ipEntries.size(),
(unsigned)whitelistEntries.size(),
(unsigned)opEntries.size(),
whitelistEnabled ? "enabled" : "disabled");
return true;
}
@@ -151,6 +179,7 @@ namespace ServerRuntime
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
PublishBanManagerSnapshot(std::shared_ptr<BanManager>{});
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>{});
PublishOpManagerSnapshot(std::shared_ptr<OpManager>{});
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = false;
}
@@ -214,7 +243,9 @@ namespace ServerRuntime
bool IsInitialized()
{
return GetBanManagerSnapshot() != nullptr && GetWhitelistManagerSnapshot() != nullptr;
return GetBanManagerSnapshot() != nullptr
&& GetWhitelistManagerSnapshot() != nullptr
&& GetOpManagerSnapshot() != nullptr;
}
bool IsWhitelistEnabled()
@@ -456,5 +487,111 @@ namespace ServerRuntime
return whitelistManager->SnapshotWhitelistedPlayers(outEntries);
}
bool IsPlayerOp(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<OpManager> opManager = GetOpManagerSnapshot();
return (opManager != nullptr) ? opManager->IsPlayerOp(formatted) : false;
}
bool AddOp(PlayerUID xuid, const std::string &name, const OpMetadata &metadata)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<OpManager> current = GetOpManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto opManager = std::make_shared<OpManager>(*current);
OpPlayerEntry entry;
entry.xuid = formatted;
entry.name = name;
entry.metadata = metadata;
if (!opManager->AddOp(entry))
{
return false;
}
PublishOpManagerSnapshot(opManager);
return true;
}
bool RemoveOp(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<OpManager> current = GetOpManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto opManager = std::make_shared<OpManager>(*current);
if (!opManager->RemoveOpByXuid(formatted))
{
return false;
}
PublishOpManagerSnapshot(opManager);
return true;
}
bool ReloadOps()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetOpManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto opManager = std::make_shared<OpManager>(*current);
if (!opManager->EnsureOpFileExists())
{
return false;
}
if (!opManager->Reload())
{
return false;
}
PublishOpManagerSnapshot(opManager);
return true;
}
bool SnapshotOps(std::vector<OpPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
const auto opManager = GetOpManagerSnapshot();
if (opManager == nullptr)
{
outEntries->clear();
return false;
}
return opManager->SnapshotOps(outEntries);
}
}
}
+6
View File
@@ -2,6 +2,7 @@
#include "BanManager.h"
#include "WhitelistManager.h"
#include "OpManager.h"
namespace ServerRuntime
{
@@ -14,6 +15,7 @@ namespace ServerRuntime
void Shutdown();
bool Reload();
bool ReloadWhitelist();
bool ReloadOps();
bool IsInitialized();
bool IsWhitelistEnabled();
void SetWhitelistEnabled(bool enabled);
@@ -21,6 +23,7 @@ namespace ServerRuntime
bool IsPlayerBanned(PlayerUID xuid);
bool IsIpBanned(const std::string &ip);
bool IsPlayerWhitelisted(PlayerUID xuid);
bool IsPlayerOp(PlayerUID xuid);
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata);
bool AddIpBan(const std::string &ip, const BanMetadata &metadata);
@@ -28,6 +31,8 @@ namespace ServerRuntime
bool RemoveIpBan(const std::string &ip);
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata);
bool RemoveWhitelistedPlayer(PlayerUID xuid);
bool AddOp(PlayerUID xuid, const std::string &name, const OpMetadata &metadata);
bool RemoveOp(PlayerUID xuid);
/**
* Copies the current cached player bans for inspection or command output
@@ -40,6 +45,7 @@ namespace ServerRuntime
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries);
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries);
bool SnapshotOps(std::vector<OpPlayerEntry> *outEntries);
std::string FormatXuid(PlayerUID xuid);
bool TryParseXuid(const std::string &text, PlayerUID *outXuid);
+284
View File
@@ -0,0 +1,284 @@
#include "stdafx.h"
#include "OpManager.h"
#include "..\Common\AccessStorageUtils.h"
#include "..\Common\FileUtils.h"
#include "..\Common\StringUtils.h"
#include "..\ServerLogger.h"
#include "..\vendor\nlohmann\json.hpp"
#include <algorithm>
namespace ServerRuntime
{
namespace Access
{
using OrderedJson = nlohmann::ordered_json;
namespace
{
static const char *kOpFileName = "ops.json";
}
OpManager::OpManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool OpManager::EnsureOpFileExists() const
{
const std::string path = GetOpFilePath();
if (!AccessStorageUtils::EnsureJsonListFileExists(path))
{
LogErrorf("access", "failed to create %s", path.c_str());
return false;
}
return true;
}
bool OpManager::Reload()
{
std::vector<OpPlayerEntry> ops;
if (!LoadOps(&ops))
{
return false;
}
m_ops.swap(ops);
return true;
}
bool OpManager::Save() const
{
std::vector<OpPlayerEntry> ops;
return SnapshotOps(&ops) && SaveOps(ops);
}
bool OpManager::LoadOps(std::vector<OpPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetOpFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
OrderedJson root;
try
{
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
}
catch (const nlohmann::json::exception &e)
{
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
return false;
}
if (!root.is_array())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
for (const auto &object : root)
{
if (!object.is_object())
{
LogWarnf("access", "skipping op entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping op entry without xuid in %s", path.c_str());
continue;
}
OpPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping op entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
outEntries->push_back(entry);
}
return true;
}
bool OpManager::SaveOps(const std::vector<OpPlayerEntry> &entries) const
{
OrderedJson root = OrderedJson::array();
for (const auto &entry : entries)
{
OrderedJson object = OrderedJson::object();
object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid);
object["name"] = entry.name;
object["created"] = entry.metadata.created;
object["source"] = entry.metadata.source;
root.push_back(object);
}
const std::string path = GetOpFilePath();
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<OpPlayerEntry> &OpManager::GetOps() const
{
return m_ops;
}
bool OpManager::SnapshotOps(std::vector<OpPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
*outEntries = m_ops;
return true;
}
bool OpManager::IsPlayerOp(const std::string &xuid) const
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
return std::any_of(
m_ops.begin(),
m_ops.end(),
[&normalized](const OpPlayerEntry &entry)
{
return entry.xuid == normalized;
});
}
bool OpManager::AddOp(const OpPlayerEntry &entry)
{
std::vector<OpPlayerEntry> updatedEntries;
if (!SnapshotOps(&updatedEntries))
{
return false;
}
auto normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const OpPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
*existing = normalized;
if (!SaveOps(updatedEntries))
{
return false;
}
m_ops.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SaveOps(updatedEntries))
{
return false;
}
m_ops.swap(updatedEntries);
return true;
}
bool OpManager::RemoveOpByXuid(const std::string &xuid)
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<OpPlayerEntry> updatedEntries;
if (!SnapshotOps(&updatedEntries))
{
return false;
}
const auto oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const OpPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SaveOps(updatedEntries))
{
return false;
}
m_ops.swap(updatedEntries);
return true;
}
std::string OpManager::GetOpFilePath() const
{
return BuildPath(kOpFileName);
}
OpMetadata OpManager::BuildDefaultMetadata(const char *source)
{
OpMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
return metadata;
}
std::string OpManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
struct OpMetadata
{
std::string created;
std::string source;
};
struct OpPlayerEntry
{
std::string xuid;
std::string name;
OpMetadata metadata;
};
/**
* Persistent OP (operator) list manager.
*
* Stores XUID-based operator entries in `ops.json`.
* Used as the authoritative source of truth for who has OP privileges,
* preventing in-memory-only OP escalation via crafted packets.
*/
class OpManager
{
public:
explicit OpManager(const std::string &baseDirectory = ".");
bool EnsureOpFileExists() const;
bool Reload();
bool Save() const;
bool LoadOps(std::vector<OpPlayerEntry> *outEntries) const;
bool SaveOps(const std::vector<OpPlayerEntry> &entries) const;
const std::vector<OpPlayerEntry> &GetOps() const;
bool SnapshotOps(std::vector<OpPlayerEntry> *outEntries) const;
bool IsPlayerOp(const std::string &xuid) const;
bool AddOp(const OpPlayerEntry &entry);
bool RemoveOpByXuid(const std::string &xuid);
std::string GetOpFilePath() const;
static OpMetadata BuildDefaultMetadata(const char *source = "Server");
private:
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<OpPlayerEntry> m_ops;
};
}
}
@@ -23,6 +23,7 @@
#include "commands\tp\CliCommandTp.h"
#include "commands\weather\CliCommandWeather.h"
#include "commands\whitelist\CliCommandWhitelist.h"
#include "commands\revoketoken\CliCommandRevokeToken.h"
#include "..\Common\StringUtils.h"
#include "..\ServerShutdown.h"
#include "..\ServerLogger.h"
@@ -100,6 +101,7 @@ namespace ServerRuntime
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardonIp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanList()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWhitelist()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandRevokeToken()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTime()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWeather()));
@@ -0,0 +1,85 @@
#include "stdafx.h"
#include "CliCommandRevokeToken.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Security\IdentityTokenManager.h"
#include "..\..\..\ServerLogManager.h"
namespace ServerRuntime
{
const char *CliCommandRevokeToken::Name() const
{
return "revoketoken";
}
const char *CliCommandRevokeToken::Usage() const
{
return "revoketoken <name|xuid>";
}
const char *CliCommandRevokeToken::Description() const
{
return "Revoke a player's identity token. They will be issued a new one on next login.";
}
bool CliCommandRevokeToken::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2)
{
engine->LogWarn(std::string("Usage: ") + Usage());
return false;
}
PlayerUID xuid = INVALID_XUID;
// Try parsing as XUID first
if (ServerRuntime::Access::TryParseXuid(line.tokens[1], &xuid))
{
// Direct XUID
}
else
{
// Try name lookup from cache
std::vector<PlayerUID> cachedXuids;
int count = ServerRuntime::ServerLogManager::GetCachedXuids(line.tokens[1], &cachedXuids);
if (count == 0)
{
engine->LogWarn("Unknown player: " + line.tokens[1]);
engine->LogWarn("The player must have attempted to connect, or use: revoketoken <xuid>");
return false;
}
if (count > 1)
{
engine->LogWarn("Ambiguous: " + std::to_string(count) + " XUIDs seen for '" + line.tokens[1] + "':");
for (size_t i = 0; i < cachedXuids.size(); ++i)
{
std::string label = (i == cachedXuids.size() - 1) ? " (most recent)" : "";
engine->LogWarn(" " + ServerRuntime::Access::FormatXuid(cachedXuids[i]) + label);
}
engine->LogWarn("Re-run with the explicit XUID: revoketoken <xuid>");
return false;
}
xuid = cachedXuids.back();
engine->LogInfo("Resolved '" + line.tokens[1] + "' to XUID " + ServerRuntime::Access::FormatXuid(xuid));
}
if (!ServerRuntime::Security::GetIdentityTokenManager().HasToken(xuid))
{
engine->LogWarn("No identity token found for XUID " + ServerRuntime::Access::FormatXuid(xuid));
return false;
}
if (!ServerRuntime::Security::GetIdentityTokenManager().RevokeToken(xuid))
{
engine->LogError("Failed to revoke token.");
return false;
}
engine->LogInfo("Revoked identity token for XUID " + ServerRuntime::Access::FormatXuid(xuid) +
". Player will receive a new token on next login.");
return true;
}
}
@@ -0,0 +1,15 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandRevokeToken : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
};
}
@@ -7,6 +7,7 @@
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\ServerProperties.h"
#include "..\..\..\ServerLogManager.h"
#include <algorithm>
#include <array>
@@ -181,14 +182,44 @@ namespace ServerRuntime
{
if (line.tokens.size() < 3)
{
engine->LogWarn("Usage: whitelist add <xuid> [name ...]");
engine->LogWarn("Usage: whitelist add <xuid|name> [display name ...]");
return false;
}
PlayerUID xuid = INVALID_XUID;
if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid))
std::string name;
if (ServerRuntime::Access::TryParseXuid(line.tokens[2], &xuid))
{
return false;
// Argument is a XUID
name = StringUtils::JoinTokens(line.tokens, 3);
}
else
{
// Argument is a player name -- look up XUID from recent login cache
std::vector<PlayerUID> cachedXuids;
int count = ServerRuntime::ServerLogManager::GetCachedXuids(line.tokens[2], &cachedXuids);
if (count == 0)
{
engine->LogWarn("Unknown player: " + line.tokens[2]);
engine->LogWarn("The player must attempt to connect first so the server can learn their XUID.");
engine->LogWarn("Alternatively, use: whitelist add <xuid>");
return false;
}
if (count > 1)
{
engine->LogWarn("Ambiguous: " + std::to_string(count) + " different XUIDs have been seen for '" + line.tokens[2] + "':");
for (size_t i = 0; i < cachedXuids.size(); ++i)
{
std::string label = (i == cachedXuids.size() - 1) ? " (most recent)" : "";
engine->LogWarn(" " + ServerRuntime::Access::FormatXuid(cachedXuids[i]) + label);
}
engine->LogWarn("Re-run with the explicit XUID: whitelist add <xuid> [name]");
return false;
}
xuid = cachedXuids.back();
name = line.tokens[2];
engine->LogInfo("Resolved '" + name + "' to XUID " + ServerRuntime::Access::FormatXuid(xuid));
}
if (ServerRuntime::Access::IsPlayerWhitelisted(xuid))
@@ -198,7 +229,6 @@ namespace ServerRuntime
}
const auto metadata = ServerRuntime::Access::WhitelistManager::BuildDefaultMetadata("Console");
const auto name = StringUtils::JoinTokens(line.tokens, 3);
if (!ServerRuntime::Access::AddWhitelistedPlayer(xuid, name, metadata))
{
engine->LogError("Failed to write whitelist entry.");
@@ -0,0 +1,60 @@
#include "stdafx.h"
#include "CipherHandshakeEnforcer.h"
#include "ConnectionCipher.h"
namespace ServerRuntime
{
namespace Security
{
CipherHandshakeEnforcer::CipherHandshakeEnforcer()
{
memset(m_sentTick, 0, sizeof(m_sentTick));
memset(m_tracked, 0, sizeof(m_tracked));
}
CipherHandshakeEnforcer::~CipherHandshakeEnforcer()
{
}
void CipherHandshakeEnforcer::OnCipherKeySent(unsigned char smallId, unsigned int currentTick)
{
m_sentTick[smallId] = currentTick;
m_tracked[smallId] = true;
}
void CipherHandshakeEnforcer::CheckTimeouts(unsigned int currentTick,
std::vector<unsigned char> &outExpired,
std::vector<unsigned char> &outCompleted)
{
auto &registry = GetCipherRegistry();
for (int i = 0; i < MAX_CONNECTIONS; ++i)
{
if (!m_tracked[i])
continue;
if (registry.IsCipherActive(static_cast<unsigned char>(i)))
{
outCompleted.push_back(static_cast<unsigned char>(i));
m_tracked[i] = false;
}
else if ((currentTick - m_sentTick[i]) > static_cast<unsigned int>(kGraceTicks))
{
outExpired.push_back(static_cast<unsigned char>(i));
m_tracked[i] = false;
}
}
}
void CipherHandshakeEnforcer::OnDisconnected(unsigned char smallId)
{
m_tracked[smallId] = false;
}
CipherHandshakeEnforcer &GetHandshakeEnforcer()
{
static CipherHandshakeEnforcer s_instance;
return s_instance;
}
}
}
@@ -0,0 +1,64 @@
#pragma once
#ifdef _WINDOWS64
#include <Windows.h>
#endif
#include <vector>
namespace ServerRuntime
{
namespace Security
{
/**
* Tracks pending cipher handshakes and kicks clients that don't complete
* within the grace period.
*
* When require-secure-client is enabled, old/unpatched clients that ignore
* MC|CKey are disconnected before they receive any PlayerInfoPacket data
* containing other players' XUIDs.
*
* Called from the main tick thread only (PlayerList::tick).
*/
class CipherHandshakeEnforcer
{
public:
// 5 seconds at 20 TPS. The security gate buffers all game data until
// cipher completes, so no data leaks regardless of grace period length.
// 5 seconds accommodates high-latency connections.
static const int kGraceTicks = 100;
CipherHandshakeEnforcer();
~CipherHandshakeEnforcer();
CipherHandshakeEnforcer(const CipherHandshakeEnforcer &) = delete;
CipherHandshakeEnforcer &operator=(const CipherHandshakeEnforcer &) = delete;
/**
* Register that MC|CKey was sent to this smallId at the given tick.
*/
void OnCipherKeySent(unsigned char smallId, unsigned int currentTick);
/**
* Check for timed-out handshakes. Returns smallIds that exceeded the
* grace period without the cipher becoming active. Also returns
* smallIds that just completed (cipher became active) in outCompleted.
*/
void CheckTimeouts(unsigned int currentTick,
std::vector<unsigned char> &outExpired,
std::vector<unsigned char> &outCompleted);
/**
* Clean up tracking for a disconnected connection.
*/
void OnDisconnected(unsigned char smallId);
private:
static const int MAX_CONNECTIONS = 256;
unsigned int m_sentTick[MAX_CONNECTIONS]; // 0 = not tracked
bool m_tracked[MAX_CONNECTIONS];
};
CipherHandshakeEnforcer &GetHandshakeEnforcer();
}
}
@@ -0,0 +1,115 @@
#include "stdafx.h"
#include "ConnectionCipher.h"
#include <cstring>
namespace ServerRuntime
{
namespace Security
{
ConnectionCipherRegistry::ConnectionCipherRegistry()
{
InitializeCriticalSection(&m_lock);
memset(m_pending, 0, sizeof(m_pending));
memset(m_pendingKeys, 0, sizeof(m_pendingKeys));
}
ConnectionCipherRegistry::~ConnectionCipherRegistry()
{
SecureZeroMemory(m_pendingKeys, sizeof(m_pendingKeys));
DeleteCriticalSection(&m_lock);
}
bool ConnectionCipherRegistry::PrepareKey(unsigned char smallId, uint8_t outKey[StreamCipher::KEY_SIZE])
{
uint8_t key[StreamCipher::KEY_SIZE];
if (!StreamCipher::GenerateKey(key))
{
return false;
}
EnterCriticalSection(&m_lock);
memcpy(m_pendingKeys[smallId], key, StreamCipher::KEY_SIZE);
m_pending[smallId] = true;
LeaveCriticalSection(&m_lock);
memcpy(outKey, key, StreamCipher::KEY_SIZE);
SecureZeroMemory(key, sizeof(key));
return true;
}
bool ConnectionCipherRegistry::CommitCipher(unsigned char smallId)
{
EnterCriticalSection(&m_lock);
if (!m_pending[smallId])
{
LeaveCriticalSection(&m_lock);
return false;
}
m_ciphers[smallId].Initialize(m_pendingKeys[smallId]);
SecureZeroMemory(m_pendingKeys[smallId], StreamCipher::KEY_SIZE);
m_pending[smallId] = false;
LeaveCriticalSection(&m_lock);
return true;
}
void ConnectionCipherRegistry::CancelPending(unsigned char smallId)
{
EnterCriticalSection(&m_lock);
SecureZeroMemory(m_pendingKeys[smallId], StreamCipher::KEY_SIZE);
m_pending[smallId] = false;
LeaveCriticalSection(&m_lock);
}
bool ConnectionCipherRegistry::HasPendingKey(unsigned char smallId) const
{
EnterCriticalSection(&m_lock);
bool pending = m_pending[smallId];
LeaveCriticalSection(&m_lock);
return pending;
}
void ConnectionCipherRegistry::DeactivateCipher(unsigned char smallId)
{
EnterCriticalSection(&m_lock);
m_ciphers[smallId].Reset();
SecureZeroMemory(m_pendingKeys[smallId], StreamCipher::KEY_SIZE);
m_pending[smallId] = false;
LeaveCriticalSection(&m_lock);
}
bool ConnectionCipherRegistry::TryEncryptOutgoing(unsigned char smallId, uint8_t *data, int length)
{
EnterCriticalSection(&m_lock);
bool active = m_ciphers[smallId].IsActive();
if (active)
{
m_ciphers[smallId].Encrypt(data, length);
}
LeaveCriticalSection(&m_lock);
return active;
}
bool ConnectionCipherRegistry::IsCipherActive(unsigned char smallId) const
{
EnterCriticalSection(&m_lock);
bool active = m_ciphers[smallId].IsActive();
LeaveCriticalSection(&m_lock);
return active;
}
void ConnectionCipherRegistry::DecryptIncoming(unsigned char smallId, uint8_t *data, int length)
{
EnterCriticalSection(&m_lock);
m_ciphers[smallId].Decrypt(data, length);
LeaveCriticalSection(&m_lock);
}
ConnectionCipherRegistry &GetCipherRegistry()
{
static ConnectionCipherRegistry s_instance;
return s_instance;
}
}
}
@@ -0,0 +1,97 @@
#pragma once
#include "StreamCipher.h"
#ifdef _WINDOWS64
#include <Windows.h>
#endif
namespace ServerRuntime
{
namespace Security
{
/**
* Per-connection cipher registry for the dedicated server.
*
* Handshake protocol (4-message, via CustomPayloadPacket):
* 1. Server calls PrepareKey(smallId) -> sends MC|CKey with key to client
* 2. Client stores key, sends MC|CAck, activates send cipher
* 3. Server recv thread detects MC|CAck -> calls SendCOnAndCommit which
* atomically sends MC|COn plaintext then calls CommitCipher(smallId)
* 4. Client recv thread detects MC|COn -> activates recv cipher
*
* Backwards compatible: old clients ignore MC|CKey, server never gets ack,
* cipher stays inactive. Old servers never send MC|CKey, client stays plaintext.
*/
class ConnectionCipherRegistry
{
public:
ConnectionCipherRegistry();
~ConnectionCipherRegistry();
ConnectionCipherRegistry(const ConnectionCipherRegistry &) = delete;
ConnectionCipherRegistry &operator=(const ConnectionCipherRegistry &) = delete;
ConnectionCipherRegistry(ConnectionCipherRegistry &&) = delete;
ConnectionCipherRegistry &operator=(ConnectionCipherRegistry &&) = delete;
/**
* Generate a random key and store it in pending state for the given smallId.
* Does NOT activate the cipher. Call CommitCipher() after the client acks.
* Returns the generated key in outKey.
*/
bool PrepareKey(unsigned char smallId, uint8_t outKey[StreamCipher::KEY_SIZE]);
/**
* Activate a previously prepared cipher. Called from the recv thread
* when the client's MC|CAck is detected by raw byte matching.
* Returns false if no key was pending for this smallId.
*/
bool CommitCipher(unsigned char smallId);
/**
* Cancel a pending key (e.g., client disconnected before ack).
*/
void CancelPending(unsigned char smallId);
/**
* Check if a key is pending for the given smallId (no side effects).
*/
bool HasPendingKey(unsigned char smallId) const;
/**
* Deactivate the cipher and cancel any pending key for a disconnected connection.
*/
void DeactivateCipher(unsigned char smallId);
/**
* Atomically check if cipher is active and encrypt outgoing data.
* Returns true if data was encrypted, false if cipher is inactive (data untouched).
*/
bool TryEncryptOutgoing(unsigned char smallId, uint8_t *data, int length);
/**
* Check if the cipher is active (handshake completed) for a given smallId.
* Thread-safe, read-only query.
*/
bool IsCipherActive(unsigned char smallId) const;
/**
* Decrypt incoming data from a specific connection.
* No-op if the cipher is not active for this connection.
*/
void DecryptIncoming(unsigned char smallId, uint8_t *data, int length);
private:
static const int MAX_CONNECTIONS = 256;
StreamCipher m_ciphers[MAX_CONNECTIONS];
bool m_pending[MAX_CONNECTIONS];
uint8_t m_pendingKeys[MAX_CONNECTIONS][StreamCipher::KEY_SIZE];
mutable CRITICAL_SECTION m_lock;
};
/**
* Global cipher registry singleton.
*/
ConnectionCipherRegistry &GetCipherRegistry();
}
}
@@ -0,0 +1,280 @@
#include "stdafx.h"
#include "IdentityTokenManager.h"
#include "StreamCipher.h"
#include "..\Common\FileUtils.h"
#include "..\Common\StringUtils.h"
#include "..\ServerLogger.h"
#include "..\vendor\nlohmann\json.hpp"
#include <algorithm>
namespace ServerRuntime
{
namespace Security
{
using OrderedJson = nlohmann::ordered_json;
IdentityTokenManager::IdentityTokenManager()
: m_initialized(false)
{
InitializeCriticalSection(&m_lock);
}
IdentityTokenManager::~IdentityTokenManager()
{
DeleteCriticalSection(&m_lock);
}
static std::string BytesToBase64(const uint8_t *data, int length)
{
static const char kTable[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve(((length + 2) / 3) * 4);
for (int i = 0; i < length; i += 3)
{
uint32_t n = static_cast<uint32_t>(data[i]) << 16;
if (i + 1 < length) n |= static_cast<uint32_t>(data[i + 1]) << 8;
if (i + 2 < length) n |= static_cast<uint32_t>(data[i + 2]);
out.push_back(kTable[(n >> 18) & 0x3F]);
out.push_back(kTable[(n >> 12) & 0x3F]);
out.push_back((i + 1 < length) ? kTable[(n >> 6) & 0x3F] : '=');
out.push_back((i + 2 < length) ? kTable[n & 0x3F] : '=');
}
return out;
}
static bool Base64ToBytes(const std::string &encoded, std::vector<uint8_t> &out)
{
static const int kDecodeTable[128] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1
};
out.clear();
out.reserve(encoded.size() * 3 / 4);
uint32_t buf = 0;
int bits = 0;
for (char c : encoded)
{
if (c == '=') break;
if (c < 0 || c >= 128 || kDecodeTable[(int)c] < 0) return false;
buf = (buf << 6) | kDecodeTable[(int)c];
bits += 6;
if (bits >= 8)
{
bits -= 8;
out.push_back(static_cast<uint8_t>((buf >> bits) & 0xFF));
}
}
return true;
}
static std::string FormatXuid(PlayerUID xuid)
{
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", (unsigned long long)xuid);
return buffer;
}
bool IdentityTokenManager::Initialize(const std::string &filePath)
{
EnterCriticalSection(&m_lock);
m_filePath = filePath;
m_tokens.clear();
bool ok = Load();
m_initialized = true;
LeaveCriticalSection(&m_lock);
if (ok)
{
LogInfof("security", "loaded %u identity tokens from %s",
(unsigned)m_tokens.size(), filePath.c_str());
}
else
{
LogInfof("security", "no existing identity tokens found, starting fresh");
}
return true;
}
void IdentityTokenManager::Shutdown()
{
EnterCriticalSection(&m_lock);
m_tokens.clear();
m_initialized = false;
LeaveCriticalSection(&m_lock);
}
bool IdentityTokenManager::HasToken(PlayerUID xuid) const
{
EnterCriticalSection(&m_lock);
bool found = m_tokens.find(xuid) != m_tokens.end();
LeaveCriticalSection(&m_lock);
return found;
}
bool IdentityTokenManager::GetToken(PlayerUID xuid, uint8_t outToken[TOKEN_SIZE]) const
{
EnterCriticalSection(&m_lock);
auto it = m_tokens.find(xuid);
if (it == m_tokens.end() || it->second.size() != TOKEN_SIZE)
{
LeaveCriticalSection(&m_lock);
return false;
}
memcpy(outToken, it->second.data(), TOKEN_SIZE);
LeaveCriticalSection(&m_lock);
return true;
}
bool IdentityTokenManager::IssueToken(PlayerUID xuid, uint8_t outToken[TOKEN_SIZE])
{
// Generate a random 32-byte token using two 16-byte CryptGenRandom calls
uint8_t token[TOKEN_SIZE];
bool ok1 = StreamCipher::GenerateKey(token);
bool ok2 = StreamCipher::GenerateKey(token + StreamCipher::KEY_SIZE);
if (!ok1 || !ok2)
{
SecureZeroMemory(token, sizeof(token));
return false;
}
EnterCriticalSection(&m_lock);
m_tokens[xuid] = std::vector<uint8_t>(token, token + TOKEN_SIZE);
bool saved = Save();
LeaveCriticalSection(&m_lock);
if (saved)
{
memcpy(outToken, token, TOKEN_SIZE);
SecureZeroMemory(token, sizeof(token));
return true;
}
SecureZeroMemory(token, sizeof(token));
return false;
}
bool IdentityTokenManager::VerifyToken(PlayerUID xuid, const uint8_t token[TOKEN_SIZE]) const
{
EnterCriticalSection(&m_lock);
auto it = m_tokens.find(xuid);
if (it == m_tokens.end() || it->second.size() != TOKEN_SIZE)
{
LeaveCriticalSection(&m_lock);
return false;
}
// Constant-time comparison to prevent timing attacks
uint8_t diff = 0;
for (int i = 0; i < TOKEN_SIZE; ++i)
{
diff |= it->second[i] ^ token[i];
}
LeaveCriticalSection(&m_lock);
return diff == 0;
}
bool IdentityTokenManager::RevokeToken(PlayerUID xuid)
{
EnterCriticalSection(&m_lock);
auto it = m_tokens.find(xuid);
if (it == m_tokens.end())
{
LeaveCriticalSection(&m_lock);
return false;
}
SecureZeroMemory(it->second.data(), it->second.size());
m_tokens.erase(it);
bool saved = Save();
LeaveCriticalSection(&m_lock);
return saved;
}
bool IdentityTokenManager::Load()
{
std::string text;
if (!FileUtils::ReadTextFile(m_filePath, &text))
{
return false;
}
if (text.empty())
{
return true;
}
OrderedJson root;
try
{
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
}
catch (const nlohmann::json::exception &)
{
LogErrorf("security", "failed to parse %s", m_filePath.c_str());
return false;
}
if (!root.is_object() || !root.contains("tokens") || !root["tokens"].is_object())
{
return true;
}
for (auto it = root["tokens"].begin(); it != root["tokens"].end(); ++it)
{
const std::string &xuidStr = it.key();
if (!it.value().is_string()) continue;
unsigned long long parsed = 0;
if (!StringUtils::TryParseUnsignedLongLong(xuidStr, &parsed) || parsed == 0ULL)
continue;
std::vector<uint8_t> tokenBytes;
if (!Base64ToBytes(it.value().get<std::string>(), tokenBytes))
continue;
if (tokenBytes.size() != TOKEN_SIZE)
continue;
m_tokens[static_cast<PlayerUID>(parsed)] = tokenBytes;
}
return true;
}
bool IdentityTokenManager::Save() const
{
OrderedJson root = OrderedJson::object();
OrderedJson tokens = OrderedJson::object();
for (const auto &pair : m_tokens)
{
std::string xuidStr = FormatXuid(pair.first);
std::string tokenB64 = BytesToBase64(pair.second.data(), TOKEN_SIZE);
tokens[xuidStr] = tokenB64;
}
root["tokens"] = tokens;
std::string json = root.dump(2) + "\n";
if (!FileUtils::WriteTextFileAtomic(m_filePath, json))
{
LogErrorf("security", "failed to write %s", m_filePath.c_str());
return false;
}
return true;
}
IdentityTokenManager &GetIdentityTokenManager()
{
static IdentityTokenManager s_instance;
return s_instance;
}
}
}
@@ -0,0 +1,63 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <cstdint>
#ifdef _WINDOWS64
#include <Windows.h>
#endif
typedef unsigned __int64 PlayerUID;
namespace ServerRuntime
{
namespace Security
{
/**
* Persistent XUID-to-token binding for identity verification.
*
* On first login, the server issues a random 32-byte token to the client
* over the encrypted cipher channel. The client stores it locally.
* On subsequent logins, the server challenges the client to present
* its stored token. Mismatch = kicked.
*
* This prevents XUID replay attacks: an attacker who steals a XUID
* still needs the token, which was only sent over the encrypted channel.
*
* Tokens are stored in `identity-tokens.json` and persist across restarts.
*/
class IdentityTokenManager
{
public:
static const int TOKEN_SIZE = 32;
IdentityTokenManager();
~IdentityTokenManager();
IdentityTokenManager(const IdentityTokenManager &) = delete;
IdentityTokenManager &operator=(const IdentityTokenManager &) = delete;
bool Initialize(const std::string &filePath);
void Shutdown();
bool HasToken(PlayerUID xuid) const;
bool GetToken(PlayerUID xuid, uint8_t outToken[TOKEN_SIZE]) const;
bool IssueToken(PlayerUID xuid, uint8_t outToken[TOKEN_SIZE]);
bool VerifyToken(PlayerUID xuid, const uint8_t token[TOKEN_SIZE]) const;
bool RevokeToken(PlayerUID xuid);
private:
bool Load();
bool Save() const;
std::string m_filePath;
std::unordered_map<PlayerUID, std::vector<uint8_t>> m_tokens;
mutable CRITICAL_SECTION m_lock;
bool m_initialized;
};
IdentityTokenManager &GetIdentityTokenManager();
}
}
+78
View File
@@ -0,0 +1,78 @@
#include "stdafx.h"
#include "RateLimiter.h"
namespace ServerRuntime
{
namespace Security
{
RateLimiter::RateLimiter()
{
InitializeCriticalSection(&m_lock);
}
RateLimiter::~RateLimiter()
{
DeleteCriticalSection(&m_lock);
}
bool RateLimiter::AllowConnection(const std::string &ip, int maxPerWindow, int windowMs)
{
if (maxPerWindow <= 0 || windowMs <= 0)
{
return true;
}
ULONGLONG now = GetTickCount64();
ULONGLONG windowDuration = static_cast<ULONGLONG>(windowMs);
EnterCriticalSection(&m_lock);
auto &timestamps = m_connectionTimes[ip];
// Remove timestamps outside the sliding window
while (!timestamps.empty() && (now - timestamps.front()) > windowDuration)
{
timestamps.pop_front();
}
bool allowed = timestamps.size() < static_cast<size_t>(maxPerWindow);
if (allowed)
{
timestamps.push_back(now);
}
LeaveCriticalSection(&m_lock);
return allowed;
}
void RateLimiter::EvictStale(int evictionAgeMs)
{
ULONGLONG now = GetTickCount64();
ULONGLONG evictionAge = static_cast<ULONGLONG>(evictionAgeMs);
EnterCriticalSection(&m_lock);
auto it = m_connectionTimes.begin();
while (it != m_connectionTimes.end())
{
if (it->second.empty() ||
(now - it->second.back()) > evictionAge)
{
it = m_connectionTimes.erase(it);
}
else
{
++it;
}
}
LeaveCriticalSection(&m_lock);
}
RateLimiter &GetGlobalRateLimiter()
{
static RateLimiter s_instance;
return s_instance;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <deque>
#include <unordered_map>
#ifdef _WINDOWS64
#include <Windows.h>
#endif
namespace ServerRuntime
{
namespace Security
{
class RateLimiter
{
public:
RateLimiter();
~RateLimiter();
RateLimiter(const RateLimiter &) = delete;
RateLimiter &operator=(const RateLimiter &) = delete;
RateLimiter(RateLimiter &&) = delete;
RateLimiter &operator=(RateLimiter &&) = delete;
/**
* Returns true if the connection from this IP should be allowed.
* Returns false if the IP has exceeded maxPerWindow connections within windowMs milliseconds.
*/
bool AllowConnection(const std::string &ip, int maxPerWindow, int windowMs);
/**
* Removes stale entries older than evictionAgeMs from the tracking map.
*/
void EvictStale(int evictionAgeMs = 300000);
private:
CRITICAL_SECTION m_lock;
std::unordered_map<std::string, std::deque<ULONGLONG>> m_connectionTimes;
};
/**
* Global rate limiter instance for the dedicated server accept loop.
*/
RateLimiter &GetGlobalRateLimiter();
}
}
@@ -0,0 +1,27 @@
#include "stdafx.h"
#include "SecurityConfig.h"
namespace ServerRuntime
{
namespace Security
{
namespace
{
// Initialized once from main() before any worker threads start.
// Default member initializers in SecuritySettings provide safe hardened
// defaults if GetSettings() is called before InitializeSettings().
// This global must NOT be written after threads are running.
SecuritySettings g_settings;
}
void InitializeSettings(const SecuritySettings &settings)
{
g_settings = settings;
}
const SecuritySettings &GetSettings()
{
return g_settings;
}
}
}
@@ -0,0 +1,22 @@
#pragma once
namespace ServerRuntime
{
namespace Security
{
struct SecuritySettings
{
bool hidePlayerListPreLogin = true;
int rateLimitConnectionsPerWindow = 5;
int rateLimitWindowSeconds = 30;
int maxPendingConnections = 10;
bool requireChallengeToken = false;
bool enableStreamCipher = true;
bool requireSecureClient = true;
bool proxyProtocol = false;
};
void InitializeSettings(const SecuritySettings &settings);
const SecuritySettings &GetSettings();
}
}
@@ -0,0 +1,90 @@
#include "stdafx.h"
#include "StreamCipher.h"
#ifdef _WINDOWS64
#include <Windows.h>
#include <wincrypt.h>
#pragma comment(lib, "Advapi32.lib")
#endif
#include <cstring>
namespace ServerRuntime
{
namespace Security
{
StreamCipher::StreamCipher()
: m_sendPos(0)
, m_recvPos(0)
, m_active(false)
{
memset(m_key, 0, sizeof(m_key));
}
void StreamCipher::Initialize(const uint8_t key[KEY_SIZE])
{
memcpy(m_key, key, KEY_SIZE);
m_sendPos = 0;
m_recvPos = 0;
m_active = true;
}
void StreamCipher::Reset()
{
SecureZeroMemory(m_key, sizeof(m_key));
m_sendPos = 0;
m_recvPos = 0;
m_active = false;
}
void StreamCipher::Encrypt(uint8_t *data, int length)
{
if (!m_active || data == nullptr || length <= 0)
{
return;
}
for (int i = 0; i < length; ++i)
{
data[i] ^= m_key[m_sendPos];
m_sendPos = (m_sendPos + 1) % KEY_SIZE;
}
}
void StreamCipher::Decrypt(uint8_t *data, int length)
{
if (!m_active || data == nullptr || length <= 0)
{
return;
}
for (int i = 0; i < length; ++i)
{
data[i] ^= m_key[m_recvPos];
m_recvPos = (m_recvPos + 1) % KEY_SIZE;
}
}
bool StreamCipher::GenerateKey(uint8_t outKey[KEY_SIZE])
{
#ifdef _WINDOWS64
HCRYPTPROV hProv = 0;
if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT))
{
return false;
}
BOOL result = CryptGenRandom(hProv, KEY_SIZE, outKey);
CryptReleaseContext(hProv, 0);
return result != FALSE;
#else
// Fallback: not cryptographically random, but better than nothing
for (int i = 0; i < KEY_SIZE; ++i)
{
outKey[i] = static_cast<uint8_t>(rand() & 0xFF);
}
return true;
#endif
}
}
}
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <cstdint>
namespace ServerRuntime
{
namespace Security
{
/**
* Lightweight XOR stream cipher for traffic obfuscation.
*
* This is NOT cryptographically secure. It prevents passive packet sniffing
* (e.g., Wireshark-based XUID harvesting) but does not protect against
* active man-in-the-middle attacks. For real encryption, use TLS via a
* reverse proxy (stunnel, nginx stream).
*
* Usage:
* 1. Server generates a random 16-byte key during PreLogin handshake
* 2. Key is sent to the client (in a SecurityHandshakePacket)
* 3. Both sides create a StreamCipher with the same key
* 4. All subsequent TCP traffic is XOR'd through the cipher
* 5. The cipher maintains separate send/recv rolling key positions
*/
class StreamCipher
{
public:
static const int KEY_SIZE = 16;
StreamCipher();
/**
* Initialize with a key. Call before any encrypt/decrypt.
*/
void Initialize(const uint8_t key[KEY_SIZE]);
/**
* XOR-encrypt data in place for sending.
* Advances the send key position.
*/
void Encrypt(uint8_t *data, int length);
/**
* XOR-decrypt data in place after receiving.
* Advances the recv key position.
*/
void Decrypt(uint8_t *data, int length);
/**
* Returns true if the cipher has been initialized with a key.
*/
bool IsActive() const { return m_active; }
/**
* Reset to inactive state and securely wipe key material.
*/
void Reset();
/**
* Generates a cryptographically random key using CryptGenRandom (Windows).
*/
static bool GenerateKey(uint8_t outKey[KEY_SIZE]);
private:
uint8_t m_key[KEY_SIZE];
int m_sendPos;
int m_recvPos;
bool m_active;
};
}
}
+277 -3
View File
@@ -7,6 +7,7 @@
#include <array>
#include <mutex>
#include <unordered_map>
extern bool g_Win64DedicatedServer;
@@ -26,6 +27,12 @@ namespace ServerRuntime
{
std::string remoteIp;
std::string playerName;
PlayerUID offlineXuid = INVALID_XUID;
PlayerUID onlineXuid = INVALID_XUID;
bool isGuest = false;
bool cipherActive = false;
bool tokenIssued = false;
bool tokenVerified = false;
};
/**
@@ -36,6 +43,10 @@ namespace ServerRuntime
{
std::mutex stateLock;
std::array<ConnectionLogEntry, 256> entries;
// Name->XUIDs cache from recent login attempts (case-insensitive name key)
// Multiple XUIDs per name for ambiguity detection
std::unordered_map<std::string, std::vector<PlayerUID>> nameToXuidCache;
};
ServerLogState g_serverLogState;
@@ -54,6 +65,12 @@ namespace ServerRuntime
entry->remoteIp.clear();
entry->playerName.clear();
entry->offlineXuid = INVALID_XUID;
entry->onlineXuid = INVALID_XUID;
entry->isGuest = false;
entry->cipherActive = false;
entry->tokenIssued = false;
entry->tokenVerified = false;
}
static std::string NormalizeRemoteIp(const char *ip)
@@ -148,6 +165,9 @@ namespace ServerRuntime
case eTcpRejectReason_BannedIp: return "banned-ip";
case eTcpRejectReason_GameNotReady: return "game-not-ready";
case eTcpRejectReason_ServerFull: return "server-full";
case eTcpRejectReason_RateLimited: return "rate-limited";
case eTcpRejectReason_TooManyPending: return "too-many-pending";
case eTcpRejectReason_InvalidProxyHeader: return "invalid-proxy-header";
default: return "unknown";
}
}
@@ -283,8 +303,17 @@ namespace ServerRuntime
LogInfof("network", "accepted tcp connection from %s as smallId=%u", remoteIp.c_str(), (unsigned)smallId);
}
// Once login succeeds, bind the resolved player name onto the cached transport entry.
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName)
static std::string FormatXuidForLog(PlayerUID xuid)
{
if (xuid == INVALID_XUID) return "none";
char buf[32] = {};
sprintf_s(buf, sizeof(buf), "0x%016llx", (unsigned long long)xuid);
return buf;
}
// Once login succeeds, bind the resolved player name and identity onto the cached transport entry.
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName,
PlayerUID offlineXuid, PlayerUID onlineXuid, bool isGuest)
{
if (!IsDedicatedServerLoggingEnabled())
{
@@ -297,13 +326,29 @@ namespace ServerRuntime
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.playerName = playerNameUtf8;
entry.offlineXuid = offlineXuid;
entry.onlineXuid = onlineXuid;
entry.isGuest = isGuest;
if (!entry.remoteIp.empty())
{
remoteIp = entry.remoteIp;
}
}
LogInfof("network", "accepted player login: name=\"%s\" ip=%s smallId=%u", playerNameUtf8.c_str(), remoteIp.c_str(), (unsigned)smallId);
std::string xuidStr = FormatXuidForLog(offlineXuid);
std::string logMsg = "accepted player login: name=\"" + playerNameUtf8 +
"\" ip=" + remoteIp +
" xuid=" + xuidStr;
if (onlineXuid != INVALID_XUID && onlineXuid != offlineXuid)
{
logMsg += " online-xuid=" + FormatXuidForLog(onlineXuid);
}
if (isGuest)
{
logMsg += " guest=yes";
}
logMsg += " smallId=" + std::to_string((unsigned)smallId);
LogInfof("network", "%s", logMsg.c_str());
}
// Read the cached IP for the rejection log, then clear the slot because the player never fully joined.
@@ -398,5 +443,234 @@ namespace ServerRuntime
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ResetConnectionLogEntry(&g_serverLogState.entries[smallId]);
}
// ---- Security milestone tracking ----
static void TryEmitPlayerSecuredSummary(unsigned char smallId, const ConnectionLogEntry &entry)
{
// Only emit when cipher is confirmed active (the primary security gate)
if (!entry.cipherActive) return;
// If tokens are required, wait until token status is resolved
if (!entry.tokenIssued && !entry.tokenVerified) return;
const char *tokenStatus = entry.tokenVerified ? "verified" : (entry.tokenIssued ? "issued" : "n/a");
std::string xuidStr = FormatXuidForLog(entry.offlineXuid);
std::string logMsg = "player secured: name=\"" + entry.playerName +
"\" xuid=" + xuidStr +
" ip=" + (entry.remoteIp.empty() ? "unknown" : entry.remoteIp) +
" cipher=active token=" + tokenStatus;
if (entry.isGuest)
{
logMsg += " guest=yes";
}
LogInfof("security", "%s", logMsg.c_str());
}
void OnCipherHandshakeCompleted(unsigned char smallId)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.cipherActive = true;
// If tokens are not required, emit summary now
// (check if player name is cached -- it should be by this point)
if (!entry.playerName.empty())
{
// Defer: token status may still arrive. Summary emits from token methods
// or if tokens are disabled, we need to check config.
// For simplicity: always defer to token methods. If tokens are disabled,
// the caller in PlayerList.cpp will call a direct emit.
}
}
void OnCipherCompletedNoTokenRequired(unsigned char smallId)
{
// Called when cipher completes and require-challenge-token is false
if (!IsDedicatedServerLoggingEnabled()) return;
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.cipherActive = true;
if (!entry.playerName.empty())
{
std::string xuidStr = FormatXuidForLog(entry.offlineXuid);
LogInfof("security", "player secured: name=\"%s\" xuid=%s ip=%s cipher=active token=n/a%s",
entry.playerName.c_str(), xuidStr.c_str(),
entry.remoteIp.empty() ? "unknown" : entry.remoteIp.c_str(),
entry.isGuest ? " guest=yes" : "");
}
}
void OnIdentityTokenIssued(unsigned char smallId)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.tokenIssued = true;
TryEmitPlayerSecuredSummary(smallId, entry);
}
void OnIdentityTokenVerified(unsigned char smallId)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.tokenVerified = true;
TryEmitPlayerSecuredSummary(smallId, entry);
}
void OnIdentityTokenMismatch(unsigned char smallId, const std::wstring &playerName)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::string name = NormalizePlayerName(playerName);
std::string ip("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
const auto &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty()) ip = entry.remoteIp;
}
LogWarnf("security", "identity token mismatch for player \"%s\" (ip=%s) - use: revoketoken %s",
name.c_str(), ip.c_str(), name.c_str());
}
void OnIdentityTokenTimeout(unsigned char smallId, const std::wstring &playerName)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::string name = NormalizePlayerName(playerName);
std::string ip("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
const auto &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty()) ip = entry.remoteIp;
}
LogWarnf("security", "kicked player \"%s\" (ip=%s) - identity token response timed out",
name.c_str(), ip.c_str());
}
void OnUnsecuredClientKicked(unsigned char smallId)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::string ip("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
const auto &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty()) ip = entry.remoteIp;
}
LogWarnf("security", "kicked unsecured client (smallId=%u, ip=%s) - cipher handshake timed out",
(unsigned)smallId, ip.c_str());
}
void OnXuidSpoofDetected(unsigned char smallId, const std::wstring &claimedName,
const char *newIp, const char *existingIp)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::string name = NormalizePlayerName(claimedName);
LogWarnf("security", "XUID spoof suspected for \"%s\" - new IP %s conflicts with existing IP %s",
name.c_str(),
(newIp != nullptr) ? newIp : "unknown",
(existingIp != nullptr) ? existingIp : "unknown");
}
void OnUnauthorizedCommand(unsigned char smallId, const std::wstring &playerName, const char *action)
{
if (!IsDedicatedServerLoggingEnabled()) return;
std::string name = NormalizePlayerName(playerName);
std::string ip("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
const auto &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty()) ip = entry.remoteIp;
}
LogWarnf("security", "non-OP player \"%s\" attempted %s (ip=%s)",
name.c_str(), (action != nullptr) ? action : "unknown-action", ip.c_str());
}
// ---- Name-to-XUID cache ----
// Normalize a player name for cache key consistency (lowercase + trim)
static std::string NormalizeNameKey(const std::string &name)
{
return StringUtils::ToLowerAscii(StringUtils::TrimAscii(name));
}
// Maximum entries in the name->XUID cache to prevent unbounded growth
static const size_t kMaxCacheEntries = 256;
// Maximum XUIDs tracked per name
static const size_t kMaxXuidsPerName = 8;
void CachePlayerXuid(const std::wstring &playerName, PlayerUID xuid)
{
if (playerName.empty() || xuid == INVALID_XUID)
{
return;
}
// Note: playerName is from the LoginPacket and is attacker-controlled.
// This cache is an operator convenience tool for `whitelist add <name>`,
// not a security mechanism. The operator sees the resolved XUID and can
// verify it before whitelisting. Ambiguous names are blocked.
std::string key = NormalizeNameKey(StringUtils::WideToUtf8(playerName));
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
// Evict oldest cache entry if at capacity
if (g_serverLogState.nameToXuidCache.size() >= kMaxCacheEntries &&
g_serverLogState.nameToXuidCache.find(key) == g_serverLogState.nameToXuidCache.end())
{
g_serverLogState.nameToXuidCache.erase(g_serverLogState.nameToXuidCache.begin());
}
auto &entries = g_serverLogState.nameToXuidCache[key];
// Move matching XUID to the back (most recent) or append if new
for (auto it = entries.begin(); it != entries.end(); ++it)
{
if (*it == xuid)
{
entries.erase(it);
break;
}
}
entries.push_back(xuid);
// Cap per-name vector
while (entries.size() > kMaxXuidsPerName)
{
entries.erase(entries.begin());
}
}
int GetCachedXuids(const std::string &playerName, std::vector<PlayerUID> *outXuids)
{
if (playerName.empty())
{
if (outXuids != nullptr) outXuids->clear();
return 0;
}
std::string key = NormalizeNameKey(playerName);
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
auto it = g_serverLogState.nameToXuidCache.find(key);
if (it == g_serverLogState.nameToXuidCache.end() || it->second.empty())
{
if (outXuids != nullptr) outXuids->clear();
return 0;
}
if (outXuids != nullptr)
{
*outXuids = it->second;
}
return static_cast<int>(it->second.size());
}
}
}
+40 -4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <vector>
#include <stdarg.h>
#include "..\Minecraft.World\DisconnectPacket.h"
@@ -17,7 +18,10 @@ namespace ServerRuntime
{
eTcpRejectReason_BannedIp = 0,
eTcpRejectReason_GameNotReady,
eTcpRejectReason_ServerFull
eTcpRejectReason_ServerFull,
eTcpRejectReason_RateLimited,
eTcpRejectReason_TooManyPending,
eTcpRejectReason_InvalidProxyHeader
};
/**
@@ -89,10 +93,26 @@ namespace ServerRuntime
void OnAcceptedTcpConnection(unsigned char smallId, const char *ip);
/**
* Associates a player name with the connection and emits the accepted login log
* 接続にプレイヤー名を関連付けてログイン成功を記録
* Associates a player name and identity with the connection and emits the accepted login log
*/
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName);
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName,
PlayerUID offlineXuid = INVALID_XUID, PlayerUID onlineXuid = INVALID_XUID, bool isGuest = false);
// Security milestone recording -- accumulates per-connection state for the
// consolidated "player secured" summary line
void OnCipherHandshakeCompleted(unsigned char smallId);
void OnCipherCompletedNoTokenRequired(unsigned char smallId);
void OnIdentityTokenIssued(unsigned char smallId);
void OnIdentityTokenVerified(unsigned char smallId);
void OnIdentityTokenTimeout(unsigned char smallId, const std::wstring &playerName);
// Security warnings -- emit immediately to CLI
void OnIdentityTokenMismatch(unsigned char smallId, const std::wstring &playerName);
void OnIdentityTokenTimeout(unsigned char smallId, const std::wstring &playerName);
void OnUnsecuredClientKicked(unsigned char smallId);
void OnXuidSpoofDetected(unsigned char smallId, const std::wstring &claimedName,
const char *newIp, const char *existingIp);
void OnUnauthorizedCommand(unsigned char smallId, const std::wstring &playerName, const char *action);
/**
* Emits a named login rejection log and clears cached metadata for that smallId
@@ -123,5 +143,21 @@ namespace ServerRuntime
* 指定smallIdに紐づく接続キャッシュを消去
*/
void ClearConnection(unsigned char smallId);
/**
* Cache a player name -> XUID mapping from a login attempt (accepted or rejected).
* Used by `whitelist add <name>` to resolve names to XUIDs.
*/
void CachePlayerXuid(const std::wstring &playerName, PlayerUID xuid);
/**
* Get all cached XUIDs for a player name (case-insensitive).
* Returns the number of distinct XUIDs seen. If > 1, the name is ambiguous
* and the operator should use an explicit XUID.
*
* Note: cached names are attacker-controlled (from LoginPacket). This cache
* is an operator convenience tool, not a security mechanism.
*/
int GetCachedXuids(const std::string &playerName, std::vector<PlayerUID> *outXuids);
}
}
+46
View File
@@ -7,10 +7,48 @@
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <mutex>
namespace ServerRuntime
{
static volatile LONG g_minLogLevel = (LONG)eServerLogLevel_Info;
static FILE *g_logFile = NULL;
static std::once_flag g_logFileOnce;
static void OpenLogFile()
{
if (g_logFile != NULL)
return;
errno_t err = fopen_s(&g_logFile, "server.log", "a");
if (err != 0 || g_logFile == NULL)
{
g_logFile = NULL;
printf("[ServerLogger] Warning: Could not open server.log for writing (errno=%d)\n", (int)err);
fflush(stdout);
}
}
static void CloseLogFile()
{
if (g_logFile != NULL)
{
fflush(g_logFile);
fclose(g_logFile);
g_logFile = NULL;
}
}
static void EnsureLogFileInitialized()
{
std::call_once(g_logFileOnce, []() {
OpenLogFile();
if (g_logFile != NULL)
{
atexit(CloseLogFile);
}
});
}
static const char *NormalizeCategory(const char *category)
{
@@ -121,6 +159,14 @@ static void WriteLogLine(EServerLogLevel level, const char *category, const char
SetConsoleTextAttribute(stdoutHandle, originalInfo.wAttributes);
}
EnsureLogFileInitialized();
if (g_logFile != NULL)
{
fprintf(g_logFile, "[%s][%s][%s] %s\n",
timestamp, LogLevelToString(level), safeCategory, safeMessage);
fflush(g_logFile);
}
linenoiseExternalWriteEnd();
}
+18 -1
View File
@@ -82,7 +82,15 @@ static const ServerPropertyDefault kServerPropertyDefaults[] =
{ "spawn-monsters", "true" },
{ "spawn-npcs", "true" },
{ "tnt", "true" },
{ "trust-players", "true" }
{ "trust-players", "true" },
{ "hide-player-list-prelogin", "true" },
{ "rate-limit-connections-per-window", "5" },
{ "rate-limit-window-seconds", "30" },
{ "max-pending-connections", "10" },
{ "require-challenge-token", "false" },
{ "enable-stream-cipher", "true" },
{ "require-secure-client", "true" },
{ "proxy-protocol", "false" }
};
static std::string BoolToString(bool value)
@@ -883,6 +891,15 @@ ServerPropertiesConfig LoadServerPropertiesConfig()
config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite);
config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite);
config.hidePlayerListPreLogin = ReadNormalizedBoolProperty(&merged, "hide-player-list-prelogin", true, &shouldWrite);
config.rateLimitConnectionsPerWindow = ReadNormalizedIntProperty(&merged, "rate-limit-connections-per-window", 5, 1, 100, &shouldWrite);
config.rateLimitWindowSeconds = ReadNormalizedIntProperty(&merged, "rate-limit-window-seconds", 30, 5, 300, &shouldWrite);
config.maxPendingConnections = ReadNormalizedIntProperty(&merged, "max-pending-connections", 10, 1, 50, &shouldWrite);
config.requireChallengeToken = ReadNormalizedBoolProperty(&merged, "require-challenge-token", false, &shouldWrite);
config.enableStreamCipher = ReadNormalizedBoolProperty(&merged, "enable-stream-cipher", true, &shouldWrite);
config.requireSecureClient = ReadNormalizedBoolProperty(&merged, "require-secure-client", true, &shouldWrite);
config.proxyProtocol = ReadNormalizedBoolProperty(&merged, "proxy-protocol", false, &shouldWrite);
if (shouldWrite)
{
if (WriteServerPropertiesFile(kServerPropertiesPath, merged))
+18
View File
@@ -80,6 +80,24 @@ namespace ServerRuntime
/** `hardcore-ban-ip` — whether hardcore death bans include IP bans */
bool hardcoreBanIp;
/** security settings */
/** `hide-player-list-prelogin` — strip XUIDs from PreLoginPacket response */
bool hidePlayerListPreLogin;
/** `rate-limit-connections-per-window` — max TCP connections per IP within the rate limit window */
int rateLimitConnectionsPerWindow;
/** `rate-limit-window-seconds` — sliding window duration for connection rate limiting */
int rateLimitWindowSeconds;
/** `max-pending-connections` — max simultaneous pending (pre-login) connections */
int maxPendingConnections;
/** `require-challenge-token` — reserved for future protocol extension (not yet enforced) */
bool requireChallengeToken;
/** `enable-stream-cipher` — enable XOR stream cipher for traffic obfuscation */
bool enableStreamCipher;
/** `require-secure-client` — kick clients that do not complete the cipher handshake */
bool requireSecureClient;
/** `proxy-protocol` — parse PROXY protocol v1 headers from TCP tunnel (e.g. playit.gg) */
bool proxyProtocol;
/** other MinecraftServer runtime settings */
int maxBuildHeight;
std::string levelType;
+38
View File
@@ -11,6 +11,9 @@
#include "..\ServerLogManager.h"
#include "..\ServerProperties.h"
#include "..\ServerShutdown.h"
#include "..\Security\SecurityConfig.h"
#include "..\Security\RateLimiter.h"
#include "..\Security\IdentityTokenManager.h"
#include "..\WorldManager.h"
#include "..\Console\ServerCli.h"
#include "Tesselator.h"
@@ -416,6 +419,41 @@ int main(int argc, char **argv)
return 2;
}
accessShutdownGuard.Activate();
{
ServerRuntime::Security::SecuritySettings secSettings;
secSettings.hidePlayerListPreLogin = serverProperties.hidePlayerListPreLogin;
secSettings.rateLimitConnectionsPerWindow = serverProperties.rateLimitConnectionsPerWindow;
secSettings.rateLimitWindowSeconds = serverProperties.rateLimitWindowSeconds;
secSettings.maxPendingConnections = serverProperties.maxPendingConnections;
secSettings.requireChallengeToken = serverProperties.requireChallengeToken;
secSettings.enableStreamCipher = serverProperties.enableStreamCipher;
secSettings.requireSecureClient = serverProperties.requireSecureClient;
secSettings.proxyProtocol = serverProperties.proxyProtocol;
ServerRuntime::Security::InitializeSettings(secSettings);
LogInfof("startup", "Security: hide-player-list=%s, rate-limit=%d/%ds, max-pending=%d, challenge-token=%s, stream-cipher=%s, require-secure-client=%s",
secSettings.hidePlayerListPreLogin ? "true" : "false",
secSettings.rateLimitConnectionsPerWindow,
secSettings.rateLimitWindowSeconds,
secSettings.maxPendingConnections,
secSettings.requireChallengeToken ? "required" : "optional",
secSettings.enableStreamCipher ? "enabled" : "disabled",
secSettings.requireSecureClient ? "true" : "false");
if (secSettings.proxyProtocol)
{
LogInfof("startup", "PROXY protocol: enabled (all connections must send PROXY v1 header)");
}
if (secSettings.requireSecureClient && !secSettings.enableStreamCipher)
{
LogInfof("startup", "WARNING: require-secure-client is enabled but enable-stream-cipher is disabled -- secure client enforcement will have no effect");
}
if (secSettings.requireChallengeToken)
{
ServerRuntime::Security::GetIdentityTokenManager().Initialize("identity-tokens.json");
}
}
LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled");
LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled");
LogInfof("startup", "Spawn protection radius: %d", serverProperties.spawnProtectionRadius);
@@ -521,9 +521,27 @@ set(_MINECRAFT_SERVER_COMMON_SERVER_ACCESS
"${CMAKE_CURRENT_SOURCE_DIR}/Access/BanManager.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Access/WhitelistManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Access/WhitelistManager.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Access/OpManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Access/OpManager.h"
)
source_group("Server/Access" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_ACCESS})
set(_MINECRAFT_SERVER_COMMON_SERVER_SECURITY
"${CMAKE_CURRENT_SOURCE_DIR}/Security/SecurityConfig.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/SecurityConfig.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/RateLimiter.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/RateLimiter.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/StreamCipher.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/StreamCipher.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/ConnectionCipher.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/ConnectionCipher.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/CipherHandshakeEnforcer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/CipherHandshakeEnforcer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/IdentityTokenManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Security/IdentityTokenManager.h"
)
source_group("Server/Security" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_SECURITY})
set(_MINECRAFT_SERVER_COMMON_SERVER_COMMON
"${CMAKE_CURRENT_SOURCE_DIR}/Common/AccessStorageUtils.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/FileUtils.cpp"
@@ -585,6 +603,8 @@ set(_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE_COMMANDS
"${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/weather/CliCommandWeather.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/whitelist/CliCommandWhitelist.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/whitelist/CliCommandWhitelist.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/revoketoken/CliCommandRevokeToken.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/revoketoken/CliCommandRevokeToken.h"
)
source_group("Server/Console/Commands" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE_COMMANDS})
@@ -598,6 +618,7 @@ set(MINECRAFT_SERVER_COMMON
${_MINECRAFT_SERVER_COMMON_ROOT}
${_MINECRAFT_SERVER_COMMON_SERVER}
${_MINECRAFT_SERVER_COMMON_SERVER_ACCESS}
${_MINECRAFT_SERVER_COMMON_SERVER_SECURITY}
${_MINECRAFT_SERVER_COMMON_SERVER_COMMON}
${_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE}
${_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE_COMMANDS}