From 4059bf4f822fb74ea7155293088f70ddd5fcace8 Mon Sep 17 00:00:00 2001 From: Diavolo Date: Fri, 12 Jan 2024 09:53:58 +0100 Subject: [PATCH] feature: fail2ban native integration --- src/Components/Modules/Auth.cpp | 5 +++ src/Components/Modules/Logger.cpp | 47 ++++++++++++++++++++++++++--- src/Components/Modules/Logger.hpp | 16 +++++++++- src/Components/Modules/Network.cpp | 30 ++++++++++++++++++ src/Components/Modules/Network.hpp | 3 ++ src/Components/Modules/RCon.cpp | 16 +++++++--- src/Components/Modules/Security.cpp | 1 + src/Components/Modules/Voice.cpp | 1 + 8 files changed, 108 insertions(+), 11 deletions(-) diff --git a/src/Components/Modules/Auth.cpp b/src/Components/Modules/Auth.cpp index 42269ba9..d46e58fb 100644 --- a/src/Components/Modules/Auth.cpp +++ b/src/Components/Modules/Auth.cpp @@ -144,6 +144,7 @@ namespace Components Proto::Auth::Connect connectData; if (msg->cursize <= 12 || !connectData.ParseFromString(std::string(reinterpret_cast(&msg->data[12]), msg->cursize - 12))) { + Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address)); Network::Send(address, "error\nInvalid connect packet!"); return; } @@ -161,6 +162,7 @@ namespace Components } else { + Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address)); Network::Send(address, "error\nInvalid infostring data!"); } } @@ -170,6 +172,7 @@ namespace Components // Validate proto data if (connectData.signature().empty() || connectData.publickey().empty() || connectData.token().empty() || connectData.infostring().empty()) { + Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address)); Network::Send(address, "error\nInvalid connect data!"); return; } @@ -184,6 +187,7 @@ namespace Components // Ensure there are enough params if (params.size() < 3) { + Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address)); Network::Send(address, "error\nInvalid connect string!"); return; } @@ -197,6 +201,7 @@ namespace Components if (steamId.empty() || challenge.empty()) { + Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address)); Network::Send(address, "error\nInvalid connect data!"); return; } diff --git a/src/Components/Modules/Logger.cpp b/src/Components/Modules/Logger.cpp index d575efe3..1d90e816 100644 --- a/src/Components/Modules/Logger.cpp +++ b/src/Components/Modules/Logger.cpp @@ -13,7 +13,8 @@ namespace Components std::recursive_mutex Logger::LoggingMutex; std::vector Logger::LoggingAddresses[2]; - Dvar::Var Logger::IW4x_oneLog; + Dvar::Var Logger::IW4x_one_log; + Dvar::Var Logger::IW4x_fail2ban_location; void(*Logger::PipeCallback)(const std::string&) = nullptr;; @@ -43,8 +44,8 @@ namespace Components if (shouldPrint) { - std::printf("%s", msg.data()); - std::fflush(stdout); + (void)std::fputs(msg.data(), stdout); + (void)std::fflush(stdout); return; } @@ -116,6 +117,38 @@ namespace Components MessagePrint(channel, msg); } + void Logger::PrintFail2BanInternal(const std::string_view& fmt, std::format_args&& args) + { + static const auto shouldPrint = []() -> bool + { + return Flags::HasFlag("fail2ban"); + }(); + + if (!shouldPrint) + { + return; + } + + auto msg = std::vformat(fmt, args); + + static auto log_next_time_stamp = true; + if (log_next_time_stamp) + { + auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + // Convert to local time + std::tm timeInfo = *std::localtime(&now); + + std::ostringstream ss; + ss << std::put_time(&timeInfo, "%Y-%m-%d %H:%M:%S "); + + msg.insert(0, ss.str()); + } + + log_next_time_stamp = (msg.find('\n') != std::string::npos); + + Utils::IO::WriteFile(IW4x_fail2ban_location.get(), msg, true); + } + void Logger::Frame() { std::unique_lock _(MessageMutex); @@ -233,7 +266,7 @@ namespace Components { if (std::strcmp(folder, "userraw") != 0) { - if (IW4x_oneLog.get()) + if (IW4x_one_log.get()) { strncpy_s(folder, 256, "userraw", _TRUNCATE); } @@ -388,7 +421,6 @@ namespace Components Logger::Logger() { - IW4x_oneLog = Dvar::Register("iw4x_onelog", false, Game::DVAR_LATCH, "Only write the game log to the 'userraw' OS folder"); Utils::Hook(0x642139, BuildOSPath_Stub, HOOK_JUMP).install()->quick(); Scheduler::Loop(Frame, Scheduler::Pipeline::SERVER); @@ -405,6 +437,11 @@ namespace Components } Events::OnSVInit(AddServerCommands); + Events::OnDvarInit([] + { + IW4x_one_log = Dvar::Register("iw4x_onelog", false, Game::DVAR_LATCH, "Only write the game log to the 'userraw' OS folder"); + IW4x_fail2ban_location = Dvar::Register("iw4x_fail2ban_location", "/var/log/iw4x.log", Game::DVAR_NONE, "Fail2Ban logfile location"); + }); } Logger::~Logger() diff --git a/src/Components/Modules/Logger.hpp b/src/Components/Modules/Logger.hpp index 2ada2a40..8b43704d 100644 --- a/src/Components/Modules/Logger.hpp +++ b/src/Components/Modules/Logger.hpp @@ -18,6 +18,7 @@ namespace Components static void ErrorInternal(Game::errorParm_t error, const std::string_view& fmt, std::format_args&& args); static void PrintErrorInternal(Game::conChannel_t channel, const std::string_view& fmt, std::format_args&& args); static void WarningInternal(Game::conChannel_t channel, const std::string_view& fmt, std::format_args&& args); + static void PrintFail2BanInternal(const std::string_view& fmt, std::format_args&& args); static void DebugInternal(const std::string_view& fmt, std::format_args&& args, const std::source_location& loc); static void Print(const std::string_view& fmt) @@ -80,6 +81,18 @@ namespace Components PrintErrorInternal(channel, fmt, std::make_format_args(args...)); } + static void PrintFail2Ban(const std::string_view& fmt) + { + PrintFail2BanInternal(fmt, std::make_format_args(0)); + } + + template + static void PrintFail2Ban(const std::string_view& fmt, Args&&... args) + { + (Utils::String::SanitizeFormatArgs(args), ...); + PrintFail2BanInternal(fmt, std::make_format_args(args...)); + } + struct FormatWithLocation { std::string_view format; @@ -114,7 +127,8 @@ namespace Components static std::recursive_mutex LoggingMutex; static std::vector LoggingAddresses[2]; - static Dvar::Var IW4x_oneLog; + static Dvar::Var IW4x_one_log; + static Dvar::Var IW4x_fail2ban_location; static void(*PipeCallback)(const std::string&); diff --git a/src/Components/Modules/Network.cpp b/src/Components/Modules/Network.cpp index fe987d0d..7ca0d506 100644 --- a/src/Components/Modules/Network.cpp +++ b/src/Components/Modules/Network.cpp @@ -39,6 +39,11 @@ namespace Components return ntohs(this->address.port); } + unsigned short Network::Address::getPortRaw() const + { + return this->address.port; + } + void Network::Address::setIP(DWORD ip) { this->address.ip.full = ip; @@ -151,6 +156,31 @@ namespace Components return (this->getType() != Game::NA_BAD && this->getType() >= Game::NA_BOT && this->getType() <= Game::NA_IP); } + const char* Network::AdrToString(const Address& a, const bool port) + { + if (a.getType() == Game::netadrtype_t::NA_LOOPBACK) + { + return "loopback"; + } + + if (a.getType() == Game::netadrtype_t::NA_BOT) + { + return "bot"; + } + + if (a.getType() == Game::netadrtype_t::NA_IP || a.getType() == Game::netadrtype_t::NA_BROADCAST) + { + if (a.getPort() && port) + { + return Utils::String::VA("%u.%u.%u.%u:%u", a.getIP().bytes[0], a.getIP().bytes[1], a.getIP().bytes[2], a.getIP().bytes[3], htons(a.getPortRaw())); + } + + return Utils::String::VA("%u.%u.%u.%u", a.getIP().bytes[0], a.getIP().bytes[1], a.getIP().bytes[2], a.getIP().bytes[3]); + } + + return "bad"; + } + void Network::Send(Game::netsrc_t type, const Address& target, const std::string& data) { // Do not use NET_OutOfBandPrint. It only supports non-binary data! diff --git a/src/Components/Modules/Network.hpp b/src/Components/Modules/Network.hpp index d4382942..5c108c09 100644 --- a/src/Components/Modules/Network.hpp +++ b/src/Components/Modules/Network.hpp @@ -22,6 +22,7 @@ namespace Components void setPort(unsigned short port); [[nodiscard]] unsigned short getPort() const; + [[nodiscard]] unsigned short getPortRaw() const; void setIP(DWORD ip); void setIP(Game::netIP_t ip); @@ -51,6 +52,8 @@ namespace Components Network(); + static const char* AdrToString(const Address& a, bool port = false); + static std::uint16_t GetPort(); // Send quake-styled binary data diff --git a/src/Components/Modules/RCon.cpp b/src/Components/Modules/RCon.cpp index 369b903d..87a20462 100644 --- a/src/Components/Modules/RCon.cpp +++ b/src/Components/Modules/RCon.cpp @@ -180,7 +180,8 @@ namespace Components const auto pos = data.find_first_of(' '); if (pos == std::string::npos) { - Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon request from {}\n", address.getString()); + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); + Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon request from {}\n", Network::AdrToString(address)); return; } @@ -203,7 +204,8 @@ namespace Components if (svPassword != password) { - Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon password sent from {}\n", address.getString()); + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); + Logger::Print(Game::CON_CHANNEL_NETWORK, "Invalid RCon password sent from {}\n", Network::AdrToString(address)); return; } @@ -213,7 +215,7 @@ namespace Components if (RConLogRequests.get()) #endif { - Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing RCon request from {}: {}\n", address.getString(), command); + Logger::Print(Game::CON_CHANNEL_NETWORK, "Executing RCon request from {}: {}\n", Network::AdrToString(address), command); } Logger::PipeOutput([](const std::string& output) @@ -318,6 +320,7 @@ namespace Components const auto time = Game::Sys_Milliseconds(); if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) { + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); return; } @@ -341,6 +344,7 @@ namespace Components const auto time = Game::Sys_Milliseconds(); if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) { + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); return; } @@ -360,13 +364,15 @@ namespace Components Proto::RCon::Command directive; if (!directive.ParseFromString(data)) { - Logger::PrintError(Game::CON_CHANNEL_NETWORK, "Unable to parse secure command from {}\n", address.getString()); + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "Unable to parse secure command from {}\n", Network::AdrToString(address)); return; } if (!Utils::Cryptography::RSA::VerifyMessage(key, directive.command(), directive.signature())) { - Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA signature verification failed for message from {}\n", address.getString()); + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address)); + Logger::PrintError(Game::CON_CHANNEL_NETWORK, "RSA signature verification failed for message from {}\n", Network::AdrToString(address)); return; } diff --git a/src/Components/Modules/Security.cpp b/src/Components/Modules/Security.cpp index 240407db..64b01e61 100644 --- a/src/Components/Modules/Security.cpp +++ b/src/Components/Modules/Security.cpp @@ -119,6 +119,7 @@ namespace Components { if ((client->reliableSequence - client->reliableAcknowledge) < 0) { + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(client->header.netchan.remoteAddress)); Logger::Print(Game::CON_CHANNEL_NETWORK, "Negative reliableAcknowledge from {} - cl->reliableSequence is {}, reliableAcknowledge is {}\n", client->name, client->reliableSequence, client->reliableAcknowledge); client->reliableAcknowledge = client->reliableSequence; diff --git a/src/Components/Modules/Voice.cpp b/src/Components/Modules/Voice.cpp index 7d7fd1cf..c8c66e69 100644 --- a/src/Components/Modules/Voice.cpp +++ b/src/Components/Modules/Voice.cpp @@ -168,6 +168,7 @@ namespace Components voicePacket.dataSize = Game::MSG_ReadByte(msg); if (voicePacket.dataSize <= 0 || voicePacket.dataSize > MAX_VOICE_PACKET_DATA) { + Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(cl->header.netchan.remoteAddress)); Logger::Print(Game::CON_CHANNEL_SERVER, "Received invalid voice packet of size {} from {}\n", voicePacket.dataSize, cl->name); return; }