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;
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!");
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;
}

View File

@ -13,7 +13,8 @@ namespace Components
std::recursive_mutex Logger::LoggingMutex;
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;;
@ -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<std::string>(), 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<bool>())
if (IW4x_one_log.get<bool>())
{
strncpy_s(folder, 256, "userraw", _TRUNCATE);
}
@ -388,7 +421,6 @@ namespace Components
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();
Scheduler::Loop(Frame, Scheduler::Pipeline::SERVER);
@ -405,6 +437,11 @@ namespace Components
}
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()

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 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 <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
{
std::string_view format;
@ -114,7 +127,8 @@ namespace Components
static std::recursive_mutex LoggingMutex;
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&);

View File

@ -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!

View File

@ -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

View File

@ -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<bool>())
#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;
}

View File

@ -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;

View File

@ -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;
}