Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions src/game/CServerConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -968,6 +970,7 @@ const CAssocReg CServerConfig::sm_szLoadKeys[RC_QTY + 1]
{ "PACKETDEATHANIMATION", { ELEM_BOOL, static_cast<uint>OFFSETOF(CServerConfig,m_iPacketDeathAnimation) }},
{ "PAYFROMPACKONLY", { ELEM_BOOL, static_cast<uint>OFFSETOF(CServerConfig,m_fPayFromPackOnly) }},
{ "PETSINHERITNOTORIETY", { ELEM_INT, static_cast<uint>OFFSETOF(CServerConfig,m_iPetsInheritNotoriety) }},
{ "PINGFLOODMAX", { ELEM_INT, static_cast<uint>OFFSETOF(CServerConfig,m_iPingFloodMax) }},
{ "PLAYEREVIL", { ELEM_INT, static_cast<uint>OFFSETOF(CServerConfig,m_iPlayerKarmaEvil) }},
{ "PLAYERNEUTRAL", { ELEM_INT, static_cast<uint>OFFSETOF(CServerConfig,m_iPlayerKarmaNeutral) }},
{ "PROFILE", { ELEM_VOID, 0 }},
Expand Down Expand Up @@ -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 )
Expand Down
1 change: 1 addition & 0 deletions src/game/CServerConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/network/CNetState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/network/CNetState.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/network/receive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/sphere.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading