feature: fail2ban native integration

This commit is contained in:
Diavolo 2024-01-12 09:53:58 +01:00
parent 984c63532e
commit 4059bf4f82
No known key found for this signature in database
GPG Key ID: FA77F074E98D98A5
8 changed files with 108 additions and 11 deletions

View File

@ -144,6 +144,7 @@ namespace Components
Proto::Auth::Connect connectData; Proto::Auth::Connect connectData;
if (msg->cursize <= 12 || !connectData.ParseFromString(std::string(reinterpret_cast<char*>(&msg->data[12]), msg->cursize - 12))) if (msg->cursize <= 12 || !connectData.ParseFromString(std::string(reinterpret_cast<char*>(&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!"); Network::Send(address, "error\nInvalid connect packet!");
return; return;
} }
@ -161,6 +162,7 @@ namespace Components
} }
else else
{ {
Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address));
Network::Send(address, "error\nInvalid infostring data!"); Network::Send(address, "error\nInvalid infostring data!");
} }
} }
@ -170,6 +172,7 @@ namespace Components
// Validate proto data // Validate proto data
if (connectData.signature().empty() || connectData.publickey().empty() || connectData.token().empty() || connectData.infostring().empty()) 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!"); Network::Send(address, "error\nInvalid connect data!");
return; return;
} }
@ -184,6 +187,7 @@ namespace Components
// Ensure there are enough params // Ensure there are enough params
if (params.size() < 3) if (params.size() < 3)
{ {
Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address));
Network::Send(address, "error\nInvalid connect string!"); Network::Send(address, "error\nInvalid connect string!");
return; return;
} }
@ -197,6 +201,7 @@ namespace Components
if (steamId.empty() || challenge.empty()) if (steamId.empty() || challenge.empty())
{ {
Logger::PrintFail2Ban("Failed connect attempt from IP address: {}\n", Network::AdrToString(address));
Network::Send(address, "error\nInvalid connect data!"); Network::Send(address, "error\nInvalid connect data!");
return; return;
} }

View File

@ -13,7 +13,8 @@ namespace Components
std::recursive_mutex Logger::LoggingMutex; std::recursive_mutex Logger::LoggingMutex;
std::vector<Network::Address> Logger::LoggingAddresses[2]; std::vector<Network::Address> 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;; void(*Logger::PipeCallback)(const std::string&) = nullptr;;
@ -43,8 +44,8 @@ namespace Components
if (shouldPrint) if (shouldPrint)
{ {
std::printf("%s", msg.data()); (void)std::fputs(msg.data(), stdout);
std::fflush(stdout); (void)std::fflush(stdout);
return; return;
} }
@ -116,6 +117,38 @@ namespace Components
MessagePrint(channel, msg); 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<std::string>(), msg, true);
}
void Logger::Frame() void Logger::Frame()
{ {
std::unique_lock _(MessageMutex); std::unique_lock _(MessageMutex);
@ -233,7 +266,7 @@ namespace Components
{ {
if (std::strcmp(folder, "userraw") != 0) if (std::strcmp(folder, "userraw") != 0)
{ {
if (IW4x_oneLog.get<bool>()) if (IW4x_one_log.get<bool>())
{ {
strncpy_s(folder, 256, "userraw", _TRUNCATE); strncpy_s(folder, 256, "userraw", _TRUNCATE);
} }
@ -388,7 +421,6 @@ namespace Components
Logger::Logger() Logger::Logger()
{ {
IW4x_oneLog = Dvar::Register<bool>("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(); Utils::Hook(0x642139, BuildOSPath_Stub, HOOK_JUMP).install()->quick();
Scheduler::Loop(Frame, Scheduler::Pipeline::SERVER); Scheduler::Loop(Frame, Scheduler::Pipeline::SERVER);
@ -405,6 +437,11 @@ namespace Components
} }
Events::OnSVInit(AddServerCommands); Events::OnSVInit(AddServerCommands);
Events::OnDvarInit([]
{
IW4x_one_log = Dvar::Register<bool>("iw4x_onelog", false, Game::DVAR_LATCH, "Only write the game log to the 'userraw' OS folder");
IW4x_fail2ban_location = Dvar::Register<const char*>("iw4x_fail2ban_location", "/var/log/iw4x.log", Game::DVAR_NONE, "Fail2Ban logfile location");
});
} }
Logger::~Logger() Logger::~Logger()

View File

@ -18,6 +18,7 @@ namespace Components
static void ErrorInternal(Game::errorParm_t error, const std::string_view& fmt, std::format_args&& args); 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 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 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 DebugInternal(const std::string_view& fmt, std::format_args&& args, const std::source_location& loc);
static void Print(const std::string_view& fmt) static void Print(const std::string_view& fmt)
@ -80,6 +81,18 @@ namespace Components
PrintErrorInternal(channel, fmt, std::make_format_args(args...)); PrintErrorInternal(channel, fmt, std::make_format_args(args...));
} }
static void PrintFail2Ban(const std::string_view& fmt)
{
PrintFail2BanInternal(fmt, std::make_format_args(0));
}
template <typename... Args>
static void PrintFail2Ban(const std::string_view& fmt, Args&&... args)
{
(Utils::String::SanitizeFormatArgs(args), ...);
PrintFail2BanInternal(fmt, std::make_format_args(args...));
}
struct FormatWithLocation struct FormatWithLocation
{ {
std::string_view format; std::string_view format;
@ -114,7 +127,8 @@ namespace Components
static std::recursive_mutex LoggingMutex; static std::recursive_mutex LoggingMutex;
static std::vector<Network::Address> LoggingAddresses[2]; static std::vector<Network::Address> 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&); static void(*PipeCallback)(const std::string&);

View File

@ -39,6 +39,11 @@ namespace Components
return ntohs(this->address.port); return ntohs(this->address.port);
} }
unsigned short Network::Address::getPortRaw() const
{
return this->address.port;
}
void Network::Address::setIP(DWORD ip) void Network::Address::setIP(DWORD ip)
{ {
this->address.ip.full = 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); 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) 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! // Do not use NET_OutOfBandPrint. It only supports non-binary data!

View File

@ -22,6 +22,7 @@ namespace Components
void setPort(unsigned short port); void setPort(unsigned short port);
[[nodiscard]] unsigned short getPort() const; [[nodiscard]] unsigned short getPort() const;
[[nodiscard]] unsigned short getPortRaw() const;
void setIP(DWORD ip); void setIP(DWORD ip);
void setIP(Game::netIP_t ip); void setIP(Game::netIP_t ip);
@ -51,6 +52,8 @@ namespace Components
Network(); Network();
static const char* AdrToString(const Address& a, bool port = false);
static std::uint16_t GetPort(); static std::uint16_t GetPort();
// Send quake-styled binary data // Send quake-styled binary data

View File

@ -180,7 +180,8 @@ namespace Components
const auto pos = data.find_first_of(' '); const auto pos = data.find_first_of(' ');
if (pos == std::string::npos) 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; return;
} }
@ -203,7 +204,8 @@ namespace Components
if (svPassword != password) 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; return;
} }
@ -213,7 +215,7 @@ namespace Components
if (RConLogRequests.get<bool>()) if (RConLogRequests.get<bool>())
#endif #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) Logger::PipeOutput([](const std::string& output)
@ -318,6 +320,7 @@ namespace Components
const auto time = Game::Sys_Milliseconds(); const auto time = Game::Sys_Milliseconds();
if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time))
{ {
Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address));
return; return;
} }
@ -341,6 +344,7 @@ namespace Components
const auto time = Game::Sys_Milliseconds(); const auto time = Game::Sys_Milliseconds();
if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time)) if (!IsRateLimitCheckDisabled() && !RateLimitCheck(address, time))
{ {
Logger::PrintFail2Ban("Invalid packet from IP address: {}\n", Network::AdrToString(address));
return; return;
} }
@ -360,13 +364,15 @@ namespace Components
Proto::RCon::Command directive; Proto::RCon::Command directive;
if (!directive.ParseFromString(data)) 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; return;
} }
if (!Utils::Cryptography::RSA::VerifyMessage(key, directive.command(), directive.signature())) 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; return;
} }

View File

@ -119,6 +119,7 @@ namespace Components
{ {
if ((client->reliableSequence - client->reliableAcknowledge) < 0) 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", Logger::Print(Game::CON_CHANNEL_NETWORK, "Negative reliableAcknowledge from {} - cl->reliableSequence is {}, reliableAcknowledge is {}\n",
client->name, client->reliableSequence, client->reliableAcknowledge); client->name, client->reliableSequence, client->reliableAcknowledge);
client->reliableAcknowledge = client->reliableSequence; client->reliableAcknowledge = client->reliableSequence;

View File

@ -168,6 +168,7 @@ namespace Components
voicePacket.dataSize = Game::MSG_ReadByte(msg); voicePacket.dataSize = Game::MSG_ReadByte(msg);
if (voicePacket.dataSize <= 0 || voicePacket.dataSize > MAX_VOICE_PACKET_DATA) 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); Logger::Print(Game::CON_CHANNEL_SERVER, "Received invalid voice packet of size {} from {}\n", voicePacket.dataSize, cl->name);
return; return;
} }