From 62c46c7adc65ffa784ccd4acc805054da7b07110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caner=20K=C4=B1l=C4=B1=C3=A7o=C4=9Flu?= Date: Sat, 30 May 2026 13:18:03 +0300 Subject: [PATCH] Add configurable ping-flood speedhack detection (PingFloodMax) Adds a 30-second sliding window detector on packet 0x73 (ping) to catch clients running at abnormal speeds. ClassicUO sends one ping per second (~30/window); exceeding PingFloodMax triggers a warning log and kicks the client. Fixes a unit mismatch from the original port: the old sphere used tick- based time (GetTimeRaw = ticks), while Source-X returns milliseconds, so the 30s window was actually 300 ms. Now uses MSECS_PER_SEC correctly. New sphere.ini option: PingFloodMax=50 (0 = disabled). --- Changelog.txt | 7 +++++++ src/game/CServerConfig.cpp | 7 +++++++ src/game/CServerConfig.h | 1 + src/network/CNetState.cpp | 4 ++++ src/network/CNetState.h | 3 +++ src/network/receive.cpp | 24 ++++++++++++++++++++++++ src/sphere.ini | 6 ++++++ 7 files changed, 52 insertions(+) diff --git a/Changelog.txt b/Changelog.txt index 782504276..08bde30c6 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -4141,3 +4141,10 @@ When setting a property like MORE to the a spell or skill defname, trying to rea 19-05-2026, nightNR - Fixed: pre-AOS armor rating (AR) calculation (#1550). - Fixed NPCs can't cast spells from spellbook after respawn (#1551). + +30-05-2026, canerksk +- Added: Ping-flood based speedhack detection via a 30-second sliding window on packet 0x73 (PacketPingReq). + A client exceeding PingFloodMax pings within the window is logged and disconnected. + PingFloodMax is configurable in sphere.ini (default: 50, set to 0 to disable). + Note: If MaxSizeClientIn / MaxSizeClientOut are set too high or disabled (0), those byte-quota checks + will not catch abnormal packet exchange rates and should be tuned alongside PingFloodMax for best coverage. diff --git a/src/game/CServerConfig.cpp b/src/game/CServerConfig.cpp index 170b45597..71db84ff1 100644 --- a/src/game/CServerConfig.cpp +++ b/src/game/CServerConfig.cpp @@ -136,6 +136,7 @@ CServerConfig::CServerConfig() m_iLightNight = 25; // dark before t2a. m_iLightDay = LIGHT_BRIGHT; m_iContainerMaxItems = MAX_ITEMS_CONT; + m_iPingFloodMax = 50; m_iBackpackOverload = 40 * WEIGHT_UNITS; m_iBankIMax = 1000; m_iBankWMax = 1000 * WEIGHT_UNITS; @@ -673,6 +674,7 @@ enum RC_TYPE RC_PACKETDEATHANIMATION, // m_iPacketDeathAnimation RC_PAYFROMPACKONLY, // m_fPayFromPackOnly RC_PETSINHERITNOTORIETY, // m_iPetsInheritNotoriety + RC_PINGFLOODMAX, // m_iPingFloodMax RC_PLAYEREVIL, // m_iPlayerKarmaEvil RC_PLAYERNEUTRAL, // m_iPlayerKarmaNeutral RC_PROFILE, @@ -968,6 +970,7 @@ const CAssocReg CServerConfig::sm_szLoadKeys[RC_QTY + 1] { "PACKETDEATHANIMATION", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_iPacketDeathAnimation) }}, { "PAYFROMPACKONLY", { ELEM_BOOL, static_castOFFSETOF(CServerConfig,m_fPayFromPackOnly) }}, { "PETSINHERITNOTORIETY", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iPetsInheritNotoriety) }}, + { "PINGFLOODMAX", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iPingFloodMax) }}, { "PLAYEREVIL", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iPlayerKarmaEvil) }}, { "PLAYERNEUTRAL", { ELEM_INT, static_castOFFSETOF(CServerConfig,m_iPlayerKarmaNeutral) }}, { "PROFILE", { ELEM_VOID, 0 }}, @@ -1387,6 +1390,10 @@ bool CServerConfig::r_LoadVal( CScript &s ) } break; + case RC_PINGFLOODMAX: + m_iPingFloodMax = s.GetArgVal(); + break; + case RC_PLAYEREVIL: // How much bad karma makes a player evil? m_iPlayerKarmaEvil = s.GetArgVal(); if ( m_iPlayerKarmaNeutral < m_iPlayerKarmaEvil ) diff --git a/src/game/CServerConfig.h b/src/game/CServerConfig.h index 810d3045e..3ed8a4b9a 100644 --- a/src/game/CServerConfig.h +++ b/src/game/CServerConfig.h @@ -342,6 +342,7 @@ extern class CServerConfig : public CResourceHolder bool m_fMonsterFight; // Will creatures fight amoung themselves. bool m_fMonsterFear; // will they run away if hurt ? uint m_iContainerMaxItems; // Maximum number of items allowed in a container item. + int m_iPingFloodMax; // Max pings per 30s window for speedhack detection (0 = disabled). int m_iDragWeightMax; // Capacity of maxweight in % character can move with drag and drop int m_iBackpackOverload; // Maximum weight in stones extra allowed in main backpack. int m_iBankIMax; // Maximum number of items allowed in bank. diff --git a/src/network/CNetState.cpp b/src/network/CNetState.cpp index 21b7b167f..5be6939e1 100644 --- a/src/network/CNetState.cpp +++ b/src/network/CNetState.cpp @@ -137,6 +137,10 @@ void CNetState::clear(void) m_iConnectionTimeMs = -1; m_sequence = 0; + + m_pingWindowStart = 0; + m_pingCount = 0; + m_seeded = false; m_newseed = false; m_seed = 0; diff --git a/src/network/CNetState.h b/src/network/CNetState.h index 9215f6f7c..011e596ed 100644 --- a/src/network/CNetState.h +++ b/src/network/CNetState.h @@ -91,6 +91,9 @@ class CNetState dword m_reportedVersionNumber; // client version (reported) byte m_sequence; // movement sequence + int64 m_pingWindowStart; // start of current ping rate window (raw ticks) + int m_pingCount; // pings received in current window + public: explicit CNetState(int id); ~CNetState(void); diff --git a/src/network/receive.cpp b/src/network/receive.cpp index 1106e32a0..8bd745c94 100644 --- a/src/network/receive.cpp +++ b/src/network/receive.cpp @@ -1340,6 +1340,30 @@ bool PacketPingReq::onReceive(CNetState* net) { ADDTOCALLSTACK("PacketPingReq::onReceive"); + // Speedhack detection via sliding window (PINGFLOODMAX in sphere.ini, 0 = disabled). + // ClassicUO sends 0x73 every 1000ms. Normal rate: ~30 pings per 30s window. + if (g_Cfg.m_iPingFloodMax > 0) + { + const int64 WINDOW_MS = 30 * MSECS_PER_SEC; // 30 second window (GetTimeRaw is in milliseconds) + + const int64 iNow = CWorldGameTime::GetCurrentTime().GetTimeRaw(); + if (net->m_pingWindowStart == 0 || (iNow - net->m_pingWindowStart) >= WINDOW_MS) + { + net->m_pingWindowStart = iNow; + net->m_pingCount = 1; + } + else if (++net->m_pingCount > g_Cfg.m_iPingFloodMax) + { + CClient *client = net->getClient(); + g_Log.Event(LOGM_CLIENTS_LOG | LOGL_WARN, "%x:'%s' speedhack detected (%d pings in %" PRId64 " ms)\n", net->id(), + (client != nullptr && client->GetAccount() != nullptr) ? client->GetAccount()->GetName() : "?", net->m_pingCount, iNow - net->m_pingWindowStart); + if (client != nullptr) + client->SysMessage("An abnormal connection speed has been detected. You are being kicked from the game..."); + net->markReadClosed(); + return false; + } + } + byte value = readByte(); new PacketPingAck(net->getClient(), value); return true; diff --git a/src/sphere.ini b/src/sphere.ini index 077cb5140..21c88fd09 100644 --- a/src/sphere.ini +++ b/src/sphere.ini @@ -220,6 +220,12 @@ WalkBuffer=15 //Set to 0 to disable speedhack detection // WalkRegen is the a correction factor applied on point gaining. If too high, smaller speedhack will not be detected. If too low, you'll get false positives. WalkRegen=25 // Increase in case of false positive. Value below 20 will always cause false positive. +// Maximum number of 0x73 ping packets allowed within a 30-second sliding window before a speedhack is detected. +// ClassicUO sends one ping per second, so the normal rate is ~30 pings per window. +// A threshold of 50 catches clients running at 1.67x+ real-time speed without false positives on normal connections. +// Set to 0 to disable ping-flood speedhack detection. +PingFloodMax=50 + // Only commands issued by this plevel and higher will be logged CommandLog=0