From 4e4f40430aefdd49c7e23bbc20f45bd54baf8e06 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 18 Oct 2025 17:09:12 +0200 Subject: [PATCH 01/16] connectdlg: adaptive ping/stealth stats + shutdown timer (ping stealth) --- src/connectdlg.cpp | 145 ++++++++++++++++++++++++++++++++++++++++++++- src/connectdlg.h | 24 +++++++- 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index a83d499b7d..cb0dd5beac 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -137,11 +137,13 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // 4: server version // 5: minimum ping time (invisible) // 6: maximum number of clients (invisible) + // 7: last ping timestamp (invisible) // (see EConnectListViewColumns in connectdlg.h, which must match the above) lvwServers->setColumnCount ( LVC_COLUMNS ); lvwServers->hideColumn ( LVC_PING_MIN_HIDDEN ); lvwServers->hideColumn ( LVC_CLIENTS_MAX_HIDDEN ); + lvwServers->hideColumn ( LVC_LAST_PING_TIMESTAMP_HIDDEN ); // per default the root shall not be decorated (to save space) lvwServers->setRootIsDecorated ( false ); @@ -162,6 +164,8 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // setup timers TimerInitialSort.setSingleShot ( true ); // only once after list request + TimerPingShutdown.setSingleShot ( true ); // single shot shutdown timer + #if defined( ANDROID ) || defined( Q_OS_IOS ) // for the Android and iOS version maximize the window setWindowState ( Qt::WindowMaximized ); @@ -197,10 +201,15 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing ); QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList ); + + QObject::connect ( &TimerPingShutdown, &QTimer::timeout, this, &CConnectDlg::OnTimerPingShutdown ); } void CConnectDlg::showEvent ( QShowEvent* ) { + // Stop shutdown timer if dialog is shown again before it expires + TimerPingShutdown.stop(); + // load stored IP addresses in combo box cbxServerAddr->clear(); cbxServerAddr->clearEditText(); @@ -220,6 +229,9 @@ void CConnectDlg::showEvent ( QShowEvent* ) void CConnectDlg::RequestServerList() { + // Ensure shutdown timer is stopped when requesting new server list + TimerPingShutdown.stop(); + // reset flags bServerListReceived = false; bReducedServerListReceived = false; @@ -233,6 +245,10 @@ void CConnectDlg::RequestServerList() // clear server list view lvwServers->clear(); +#ifdef PING_STEALTH_MODE + mapPingHistory.clear(); +#endif + // update list combo box (disable events to avoid a signal) cbxDirectory->blockSignals ( true ); if ( pSettings->eDirectoryType == AT_CUSTOM ) @@ -271,9 +287,19 @@ void CConnectDlg::RequestServerList() void CConnectDlg::hideEvent ( QHideEvent* ) { - // if window is closed, stop timers - TimerPing.stop(); + // Stop the regular server list request timer immediately TimerReRequestServList.stop(); + +#ifdef PING_STEALTH_MODE + // Start shutdown timer with randomized duration (30-50 seconds) + // This keeps ping timer running for stealth purposes to avoid correlation + const int iShutdownTimeMs = PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ); // 15s + 0-15s = 15-30s + TimerPingShutdown.start ( iShutdownTimeMs ); +#else + TimerPing.stop(); +#endif + + } void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) @@ -295,6 +321,12 @@ void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) RequestServerList(); } +void CConnectDlg::OnTimerPingShutdown() +{ + // Shutdown timer expired - now stop all ping activities + TimerPing.stop(); +} + void CConnectDlg::OnTimerReRequestServList() { // if the server list is not yet received, retransmit the request for the @@ -452,6 +484,8 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); + pNewListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, "0" ); + // store host address pNewListViewItem->setData ( LVC_NAME, Qt::UserRole, CurHostAddress.toString() ); @@ -788,12 +822,78 @@ void CConnectDlg::OnTimerPing() // in the server list item GUI control element if ( NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, Qt::UserRole ).toString(), haServerAddress, bEnableIPv6 ) ) { + // Get minimum ping time and last ping timestamp + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); + const qint64 iLastPingTimestamp = pCurListViewItem->text ( LVC_LAST_PING_TIMESTAMP_HIDDEN ).toLongLong(); + const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; + + // Calculate adaptive ping interval based on latency using linear formula: + // - Ping < 15ms: ping every timer cycle (skip 0) + // - Ping 20ms: ping every 2nd timer cycle (skip 1) + // - Ping 25ms: ping every 3rd timer cycle (skip 2) + // - Ping 30ms: ping every 4th timer cycle (skip 3) + // - Ping 150ms+: ping every 10th timer cycle (skip 9, maximum) + // Formula: skip_count = min(9, (ping - 15) / 5) + // Interval multiplier = 1 + skip_count + int iPingInterval; + if ( iMinPingTime == 0 || iMinPingTime > 99999999 ) + { + // Never pinged or invalid - always ping to get initial measurement + iPingInterval = 0; + } + else + { + // Calculate number of timer cycles to skip based on ping time + // Linear mapping: 15ms->0 skips, 20ms->1 skip, 25ms->2 skips, etc. + // Capped at 150ms which gives 9 skips (ping every 10th cycle) + const int iSkipCount = std::min ( 9, std::max ( 0, ( iMinPingTime - 15 ) / 5 ) ); + const int iIntervalMultiplier = 1 + iSkipCount; + iPingInterval = PING_UPDATE_TIME_SERVER_LIST_MS * iIntervalMultiplier; + } + + // Add randomization as absolute time offset (500ms) to prevent synchronized pings + // This avoids regular intervals (e.g., exactly 2500ms every time) + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms + iPingInterval += iRandomOffsetMs; + +#if 1 // Set to 1 to enable detailed ping diagnostics tooltip + + const int iPingsLastMinute = GetPingCountLastMinute ( pCurListViewItem->text ( LVC_NAME ) ); + const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; + + // Update diagnostic tooltip with ping statistics + QString strTooltip = QString ( "Server: %1\n" + "Min Ping: %2 ms\n" + "Time Since Last Ping: %3 ms\n" + "Calculated Interval: %4 ms\n" + "Theoretical Rate: %5 pings/min\n" + "Actual Pings (last 60s): %6" ) + .arg ( pCurListViewItem->text ( LVC_NAME ) ) + .arg ( iMinPingTime ) + .arg ( iTimeSinceLastPing ) + .arg ( iPingInterval ) + .arg ( dTheoreticalPingRatePerMin, 0, 'f', 1 ) + .arg ( iPingsLastMinute ); + + // Set tooltip only for ping column + pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); +#endif + + // Skip this server if not enough time has passed since last ping + if ( iTimeSinceLastPing < iPingInterval ) + { + continue; + } + + pCurListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iCurrentTime ) ); + TrackPingSent ( pCurListViewItem->text ( LVC_NAME ) ); // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); Q_UNUSED ( f ); #else - QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); + QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion, lastPingTime ); #endif } } @@ -814,6 +914,9 @@ void CConnectDlg::EmitCLServerListPingMes ( const CHostAddress& haServerAddress, emit CreateCLServerListReqVerAndOSMes ( haServerAddress ); } + + + emit CreateCLServerListPingMes ( haServerAddress ); } @@ -1032,3 +1135,39 @@ void CConnectDlg::UpdateDirectoryComboBox() } } } + +#ifdef PING_STEALTH_MODE +void CConnectDlg::TrackPingSent ( const QString& strServerAddr ) +{ + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + + // Add current ping timestamp to history + mapPingHistory[strServerAddr].enqueue ( iCurrentTime ); + + // Remove pings older than 60 seconds (60000 ms) + const qint64 iOneMinuteAgo = iCurrentTime - 60000; + while ( !mapPingHistory[strServerAddr].isEmpty() && mapPingHistory[strServerAddr].head() < iOneMinuteAgo ) + { + mapPingHistory[strServerAddr].dequeue(); + } +} + +int CConnectDlg::GetPingCountLastMinute ( const QString& strServerAddr ) +{ + if ( !mapPingHistory.contains ( strServerAddr ) ) + { + return 0; + } + + // Clean up old entries first + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + const qint64 iOneMinuteAgo = iCurrentTime - 60000; + + while ( !mapPingHistory[strServerAddr].isEmpty() && mapPingHistory[strServerAddr].head() < iOneMinuteAgo ) + { + mapPingHistory[strServerAddr].dequeue(); + } + + return mapPingHistory[strServerAddr].size(); +} +#endif diff --git a/src/connectdlg.h b/src/connectdlg.h index 62cb2dcfdf..fbcb047bbb 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,6 +43,13 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms +#define PING_STEALTH_MODE +#ifdef PING_STEALTH_MODE +#define PING_SHUTDOWN_TIME_MS_MIN 15000 +#define PING_SHUTDOWN_TIME_MS_VAR 15000 +#endif + + /* Classes ********************************************************************/ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase { @@ -76,7 +83,8 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_LOCATION, // location LVC_VERSION, // server version LVC_PING_MIN_HIDDEN, // minimum ping time (invisible) - LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible) + LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible), + LVC_LAST_PING_TIMESTAMP_HIDDEN, // timestamp of last ping measurement (invisible) LVC_COLUMNS // total number of columns }; @@ -95,6 +103,7 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase CClientSettings* pSettings; QTimer TimerPing; + QTimer TimerPingShutdown; QTimer TimerReRequestServList; QTimer TimerInitialSort; CHostAddress haDirectoryAddress; @@ -107,6 +116,18 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase bool bListFilterWasActive; bool bShowAllMusicians; bool bEnableIPv6; +#ifdef PING_STEALTH_MODE +private: + // Ping statistics tracking: stores timestamps of actual pings sent + // Key: server address string, Value: queue of ping timestamps (ms since epoch) + QMap> mapPingHistory; + + // Helper function to track ping and update statistics + void TrackPingSent ( const QString& strServerAddr ); + + // Helper function to get actual ping count in last minute + int GetPingCountLastMinute ( const QString& strServerAddr ); + #endif public slots: void OnServerListItemDoubleClicked ( QTreeWidgetItem* Item, int ); @@ -118,6 +139,7 @@ public slots: void OnConnectClicked(); void OnDeleteServerAddrClicked(); void OnTimerPing(); + void OnTimerPingShutdown(); void OnTimerReRequestServList(); signals: From 75addcb692770e54c90b60b71df7939265b40e7b Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 18 Oct 2025 20:40:49 +0200 Subject: [PATCH 02/16] removed leftover --- src/connectdlg.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index cb0dd5beac..357100478b 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -852,7 +852,7 @@ void CConnectDlg::OnTimerPing() iPingInterval = PING_UPDATE_TIME_SERVER_LIST_MS * iIntervalMultiplier; } - // Add randomization as absolute time offset (500ms) to prevent synchronized pings + // Add randomization as absolute time offset ( 500ms) to prevent synchronized pings // This avoids regular intervals (e.g., exactly 2500ms every time) const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms iPingInterval += iRandomOffsetMs; @@ -893,7 +893,7 @@ void CConnectDlg::OnTimerPing() QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); Q_UNUSED ( f ); #else - QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion, lastPingTime ); + QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); #endif } } From 452330c41a374eb6fb85775a093390c61809a0eb Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 18 Oct 2025 21:24:01 +0200 Subject: [PATCH 03/16] interval needs to be larger to capture also distant pings at least once --- src/connectdlg.cpp | 17 +++++------------ src/connectdlg.h | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index cb0dd5beac..47c37b7e83 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -693,7 +693,7 @@ void CConnectDlg::UpdateListFilter() bFilterFound = true; } - // search children + // search children for ( int iCCnt = 0; iCCnt < pCurListViewItem->childCount(); iCCnt++ ) { if ( pCurListViewItem->child ( iCCnt )->text ( LVC_NAME ).indexOf ( sFilterText, 0, Qt::CaseInsensitive ) >= 0 ) @@ -829,13 +829,6 @@ void CConnectDlg::OnTimerPing() const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; // Calculate adaptive ping interval based on latency using linear formula: - // - Ping < 15ms: ping every timer cycle (skip 0) - // - Ping 20ms: ping every 2nd timer cycle (skip 1) - // - Ping 25ms: ping every 3rd timer cycle (skip 2) - // - Ping 30ms: ping every 4th timer cycle (skip 3) - // - Ping 150ms+: ping every 10th timer cycle (skip 9, maximum) - // Formula: skip_count = min(9, (ping - 15) / 5) - // Interval multiplier = 1 + skip_count int iPingInterval; if ( iMinPingTime == 0 || iMinPingTime > 99999999 ) { @@ -846,15 +839,15 @@ void CConnectDlg::OnTimerPing() { // Calculate number of timer cycles to skip based on ping time // Linear mapping: 15ms->0 skips, 20ms->1 skip, 25ms->2 skips, etc. - // Capped at 150ms which gives 9 skips (ping every 10th cycle) - const int iSkipCount = std::min ( 9, std::max ( 0, ( iMinPingTime - 15 ) / 5 ) ); + // Capped at 55ms which gives 8 skips (ping every 9th cycle) + const int iSkipCount = std::min ( 8, std::max ( 0, ( iMinPingTime - 15 ) / 5 ) ); const int iIntervalMultiplier = 1 + iSkipCount; iPingInterval = PING_UPDATE_TIME_SERVER_LIST_MS * iIntervalMultiplier; - } + } // Add randomization as absolute time offset (500ms) to prevent synchronized pings // This avoids regular intervals (e.g., exactly 2500ms every time) - const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 500 ) - 250; // -250ms to +250ms iPingInterval += iRandomOffsetMs; #if 1 // Set to 1 to enable detailed ping diagnostics tooltip diff --git a/src/connectdlg.h b/src/connectdlg.h index fbcb047bbb..946d0653f7 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -45,8 +45,8 @@ #define PING_STEALTH_MODE #ifdef PING_STEALTH_MODE -#define PING_SHUTDOWN_TIME_MS_MIN 15000 -#define PING_SHUTDOWN_TIME_MS_VAR 15000 +#define PING_SHUTDOWN_TIME_MS_MIN 40000 // needs to be reasonable higher than the 10 x ping timer to have every server at least once pinged +#define PING_SHUTDOWN_TIME_MS_VAR 20000 #endif From 731c1a5b4691ffc12c8020792f551447b9b0dbe2 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 19 Oct 2025 11:30:10 +0200 Subject: [PATCH 04/16] Rework of ping stealth mode: - make sure that after the dialog is hidden all server get a ping soon, also distant ones, but still in a way that it is not regular pattern - use randomized timer interval of 1000-1250ms since the real interval is calculated internally by skipping, also changed this to be smooth scaled - cleanup the map stuff, just store this in the Qt::UserRole data - network address is costly, cache it in Qt::UserRole - renamed things a bit and cleanup --- src/connectdlg.cpp | 245 +++++++++++++++++++++++++-------------------- src/connectdlg.h | 29 +++--- 2 files changed, 146 insertions(+), 128 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 732a738726..7058beef42 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -143,7 +143,9 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR lvwServers->setColumnCount ( LVC_COLUMNS ); lvwServers->hideColumn ( LVC_PING_MIN_HIDDEN ); lvwServers->hideColumn ( LVC_CLIENTS_MAX_HIDDEN ); +#ifdef PING_STEALTH_MODE lvwServers->hideColumn ( LVC_LAST_PING_TIMESTAMP_HIDDEN ); +#endif // per default the root shall not be decorated (to save space) lvwServers->setRootIsDecorated ( false ); @@ -163,8 +165,10 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // setup timers TimerInitialSort.setSingleShot ( true ); // only once after list request - - TimerPingShutdown.setSingleShot ( true ); // single shot shutdown timer +#ifdef PING_STEALTH_MODE + TimerKeepPingAfterHide.setSingleShot ( true ); // single shot shutdown timer + iKeepPingAfterHideStartTime = 0; +#endif #if defined( ANDROID ) || defined( Q_OS_IOS ) // for the Android and iOS version maximize the window @@ -201,14 +205,18 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing ); QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList ); +#ifdef PING_STEALTH_MODE + QObject::connect ( &TimerKeepPingAfterHide, &QTimer::timeout, this, &CConnectDlg::OnTimerKeepPingAfterHide ); +#endif - QObject::connect ( &TimerPingShutdown, &QTimer::timeout, this, &CConnectDlg::OnTimerPingShutdown ); } void CConnectDlg::showEvent ( QShowEvent* ) { +#ifdef PING_STEALTH_MODE // Stop shutdown timer if dialog is shown again before it expires - TimerPingShutdown.stop(); + TimerKeepPingAfterHide.stop(); +#endif // load stored IP addresses in combo box cbxServerAddr->clear(); @@ -229,8 +237,10 @@ void CConnectDlg::showEvent ( QShowEvent* ) void CConnectDlg::RequestServerList() { +#ifdef PING_STEALTH_MODE // Ensure shutdown timer is stopped when requesting new server list - TimerPingShutdown.stop(); + TimerKeepPingAfterHide.stop(); +#endif // reset flags bServerListReceived = false; @@ -245,10 +255,6 @@ void CConnectDlg::RequestServerList() // clear server list view lvwServers->clear(); -#ifdef PING_STEALTH_MODE - mapPingHistory.clear(); -#endif - // update list combo box (disable events to avoid a signal) cbxDirectory->blockSignals ( true ); if ( pSettings->eDirectoryType == AT_CUSTOM ) @@ -291,10 +297,10 @@ void CConnectDlg::hideEvent ( QHideEvent* ) TimerReRequestServList.stop(); #ifdef PING_STEALTH_MODE - // Start shutdown timer with randomized duration (30-50 seconds) + // Start shutdown timer with randomized duration // This keeps ping timer running for stealth purposes to avoid correlation - const int iShutdownTimeMs = PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ); // 15s + 0-15s = 15-30s - TimerPingShutdown.start ( iShutdownTimeMs ); + iKeepPingAfterHideStartTime = PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ); + TimerKeepPingAfterHide.start ( iKeepPingAfterHideStartTime ); #else TimerPing.stop(); #endif @@ -321,7 +327,7 @@ void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) RequestServerList(); } -void CConnectDlg::OnTimerPingShutdown() +void CConnectDlg::OnTimerKeepPingAfterHide() { // Shutdown timer expired - now stop all ping activities TimerPing.stop(); @@ -483,11 +489,15 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); - +#ifdef PING_STEALTH_MODE pNewListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, "0" ); + pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant() ); // QQueue for ping stats, will be initialized on first ping +#endif // store host address pNewListViewItem->setData ( LVC_NAME, Qt::UserRole, CurHostAddress.toString() ); + pNewListViewItem->setData ( LVC_NAME, Qt::UserRole + 1, QVariant() ); // cache QHostAddress, will be updated on first ping + pNewListViewItem->setData ( LVC_NAME, Qt::UserRole + 2, QVariant() ); // cache quint16 port number, will be updated on first ping // per default expand the list item (if not "show all servers") if ( bShowAllMusicians ) @@ -499,7 +509,14 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 500 ) + 1000; +#else + int iPingUpdateInterval = PING_UPDATE_TIME_SERVER_LIST_MS; +#endif + TimerPing.start ( iPingUpdateInterval ); } void CConnectDlg::SetConnClientsList ( const CHostAddress& InetAddr, const CVector& vecChanInfo ) @@ -816,79 +833,121 @@ void CConnectDlg::OnTimerPing() // we need to ask for the server version only if we have not received it const bool bNeedVersion = pCurListViewItem->text ( LVC_VERSION ).isEmpty(); + // retrieve cached QHostAddress and port from UserData + QVariant varCachedIP = pCurListViewItem->data ( LVC_NAME, Qt::UserRole + 1 ); + QVariant varCachedPort = pCurListViewItem->data ( LVC_NAME, Qt::UserRole + 2 ); + CHostAddress haServerAddress; - // try to parse host address string which is stored as user data - // in the server list item GUI control element - if ( NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, Qt::UserRole ).toString(), haServerAddress, bEnableIPv6 ) ) + if ( varCachedIP.canConvert() && varCachedPort.canConvert() && !varCachedIP.isNull() && !varCachedPort.isNull() ) { - // Get minimum ping time and last ping timestamp - const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); - const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); - const qint64 iLastPingTimestamp = pCurListViewItem->text ( LVC_LAST_PING_TIMESTAMP_HIDDEN ).toLongLong(); - const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; - - // Calculate adaptive ping interval based on latency using linear formula: - int iPingInterval; - if ( iMinPingTime == 0 || iMinPingTime > 99999999 ) + // Use cached values + haServerAddress.InetAddr = varCachedIP.value(); + haServerAddress.iPort = varCachedPort.value(); + } + else + { + // Fallback: parse and cache if not present (should not happen in normal flow) + if ( !NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, Qt::UserRole ).toString(), haServerAddress, bEnableIPv6 ) ) { - // Never pinged or invalid - always ping to get initial measurement - iPingInterval = 0; + continue; // Skip this server if parsing fails } - else + + // Cache the parsed values for future use + pCurListViewItem->setData ( LVC_NAME, Qt::UserRole + 1, QVariant::fromValue ( haServerAddress.InetAddr ) ); + pCurListViewItem->setData ( LVC_NAME, Qt::UserRole + 2, QVariant::fromValue ( haServerAddress.iPort ) ); + } + +#ifdef PING_STEALTH_MODE + // Get minimum ping time and last ping timestamp + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); + const qint64 iLastPingTimestamp = pCurListViewItem->text ( LVC_LAST_PING_TIMESTAMP_HIDDEN ).toLongLong(); + const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; + + // Calculate adaptive ping interval based on latency using linear formula: + int iPingInterval; + if ( iMinPingTime == 0 || iMinPingTime > 99999999 ) + { + // Never pinged or invalid - always ping to get initial measurement + iPingInterval = 0; + } + else + { + // Calculate adaptive ping interval using smooth floating-point multiplier + // Linear mapping: 15ms->1.0x, 20ms->1.2x, 25ms->1.4x, etc. + // Capped at ~265ms ping which gives 9.0x multiplier (max ~22.5s interval) + const float fIntervalMultiplier = std::min ( 9.0f, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ); + iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); + + // During shutdown: cap max interval to 3 seconds for first 5 seconds only + if ( TimerKeepPingAfterHide.isActive() && TimerKeepPingAfterHide.remainingTime() > ( iKeepPingAfterHideStartTime - 5000 ) ) { - // Calculate number of timer cycles to skip based on ping time - // Linear mapping: 15ms->0 skips, 20ms->1 skip, 25ms->2 skips, etc. - // Capped at 55ms which gives 8 skips (ping every 9th cycle) - const int iSkipCount = std::min ( 8, std::max ( 0, ( iMinPingTime - 15 ) / 5 ) ); - const int iIntervalMultiplier = 1 + iSkipCount; - iPingInterval = PING_UPDATE_TIME_SERVER_LIST_MS * iIntervalMultiplier; - } - - // Add randomization as absolute time offset ( 500ms) to prevent synchronized pings - // This avoids regular intervals (e.g., exactly 2500ms every time) - const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 500 ) - 250; // -250ms to +250ms - iPingInterval += iRandomOffsetMs; - -#if 1 // Set to 1 to enable detailed ping diagnostics tooltip - - const int iPingsLastMinute = GetPingCountLastMinute ( pCurListViewItem->text ( LVC_NAME ) ); - const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; - - // Update diagnostic tooltip with ping statistics - QString strTooltip = QString ( "Server: %1\n" - "Min Ping: %2 ms\n" - "Time Since Last Ping: %3 ms\n" - "Calculated Interval: %4 ms\n" - "Theoretical Rate: %5 pings/min\n" - "Actual Pings (last 60s): %6" ) - .arg ( pCurListViewItem->text ( LVC_NAME ) ) - .arg ( iMinPingTime ) - .arg ( iTimeSinceLastPing ) - .arg ( iPingInterval ) - .arg ( dTheoreticalPingRatePerMin, 0, 'f', 1 ) - .arg ( iPingsLastMinute ); - - // Set tooltip only for ping column - pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); -#endif + iPingInterval = std::min ( iPingInterval, 3000 ); + } + } + + // Add randomization as absolute time offset ( 500ms) to prevent any synchronized pings across servers + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms + iPingInterval += iRandomOffsetMs; + + QQueue pingQueue = pCurListViewItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); - // Skip this server if not enough time has passed since last ping - if ( iTimeSinceLastPing < iPingInterval ) + const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; + double dActualPingRatePerMin = 0.0; + if ( pingQueue.size() > 1 ) + { + const qint64 iTimeSpanMs = pingQueue.last() - pingQueue.first(); + if ( iTimeSpanMs > 0 ) { - continue; + dActualPingRatePerMin = ( pingQueue.size() - 1 ) * 60000.0 / static_cast ( iTimeSpanMs ); } + } + + // Update diagnostic tooltip with ping statistics + QString strTooltip = QString ( "Server: %1\n" + "Min Ping: %2 ms\n" + "Time Since Last Ping: %3 ms\n" + "Calculated Interval: %4 ms\n" + "Theoretical Rate: %5 pings/min\n" + "Actual Rate: %6 pings/min" ) + .arg ( pCurListViewItem->text ( LVC_NAME ) ) + .arg ( iMinPingTime ) + .arg ( iTimeSinceLastPing ) + .arg ( iPingInterval ) + .arg ( dTheoreticalPingRatePerMin, 0, 'f', 1 ) + .arg ( dActualPingRatePerMin, 0, 'f', 1 ); + + // Set tooltip only for ping column + pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); + + // Skip this server if not enough time has passed since last ping + if ( iTimeSinceLastPing < iPingInterval ) + { + continue; + } + + pCurListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iCurrentTime ) ); - pCurListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iCurrentTime ) ); - TrackPingSent ( pCurListViewItem->text ( LVC_NAME ) ); - // if address is valid, send ping message using a new thread + // Track ping timestamp in queue stored in Qt::UserRole of LVC_LAST_PING_TIMESTAMP_HIDDEN + pingQueue.enqueue ( iCurrentTime ); + + // Remove pings older than 60 seconds + const qint64 iOneMinuteAgo = iCurrentTime - 60000; + while ( !pingQueue.isEmpty() && pingQueue.head() < iOneMinuteAgo ) + { + pingQueue.dequeue(); + } + pCurListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant::fromValue ( pingQueue ) ); +#endif + + // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) - QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); - Q_UNUSED ( f ); + QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); + Q_UNUSED ( f ); #else - QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); + QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); #endif - } } } @@ -1128,39 +1187,3 @@ void CConnectDlg::UpdateDirectoryComboBox() } } } - -#ifdef PING_STEALTH_MODE -void CConnectDlg::TrackPingSent ( const QString& strServerAddr ) -{ - const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); - - // Add current ping timestamp to history - mapPingHistory[strServerAddr].enqueue ( iCurrentTime ); - - // Remove pings older than 60 seconds (60000 ms) - const qint64 iOneMinuteAgo = iCurrentTime - 60000; - while ( !mapPingHistory[strServerAddr].isEmpty() && mapPingHistory[strServerAddr].head() < iOneMinuteAgo ) - { - mapPingHistory[strServerAddr].dequeue(); - } -} - -int CConnectDlg::GetPingCountLastMinute ( const QString& strServerAddr ) -{ - if ( !mapPingHistory.contains ( strServerAddr ) ) - { - return 0; - } - - // Clean up old entries first - const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); - const qint64 iOneMinuteAgo = iCurrentTime - 60000; - - while ( !mapPingHistory[strServerAddr].isEmpty() && mapPingHistory[strServerAddr].head() < iOneMinuteAgo ) - { - mapPingHistory[strServerAddr].dequeue(); - } - - return mapPingHistory[strServerAddr].size(); -} -#endif diff --git a/src/connectdlg.h b/src/connectdlg.h index 946d0653f7..b176e3600a 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,10 +43,12 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms -#define PING_STEALTH_MODE +#define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #ifdef PING_STEALTH_MODE -#define PING_SHUTDOWN_TIME_MS_MIN 40000 // needs to be reasonable higher than the 10 x ping timer to have every server at least once pinged -#define PING_SHUTDOWN_TIME_MS_VAR 20000 +#define PING_SHUTDOWN_TIME_MS_MIN 30000 // recommend to keep it like this and not lower +#define PING_SHUTDOWN_TIME_MS_VAR 10000 +// Register QQueue as Qt metatype for use in QVariant to keep ping stats in UserRole +Q_DECLARE_METATYPE ( QQueue ) #endif @@ -84,7 +86,9 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_VERSION, // server version LVC_PING_MIN_HIDDEN, // minimum ping time (invisible) LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible), +#ifdef PING_STEALTH_MODE LVC_LAST_PING_TIMESTAMP_HIDDEN, // timestamp of last ping measurement (invisible) +#endif LVC_COLUMNS // total number of columns }; @@ -103,7 +107,10 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase CClientSettings* pSettings; QTimer TimerPing; - QTimer TimerPingShutdown; +#ifdef PING_STEALTH_MODE + QTimer TimerKeepPingAfterHide; + qint64 iKeepPingAfterHideStartTime; // shutdown ping timer in ms epoch +#endif QTimer TimerReRequestServList; QTimer TimerInitialSort; CHostAddress haDirectoryAddress; @@ -116,18 +123,6 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase bool bListFilterWasActive; bool bShowAllMusicians; bool bEnableIPv6; -#ifdef PING_STEALTH_MODE -private: - // Ping statistics tracking: stores timestamps of actual pings sent - // Key: server address string, Value: queue of ping timestamps (ms since epoch) - QMap> mapPingHistory; - - // Helper function to track ping and update statistics - void TrackPingSent ( const QString& strServerAddr ); - - // Helper function to get actual ping count in last minute - int GetPingCountLastMinute ( const QString& strServerAddr ); - #endif public slots: void OnServerListItemDoubleClicked ( QTreeWidgetItem* Item, int ); @@ -139,7 +134,7 @@ public slots: void OnConnectClicked(); void OnDeleteServerAddrClicked(); void OnTimerPing(); - void OnTimerPingShutdown(); + void OnTimerKeepPingAfterHide(); void OnTimerReRequestServList(); signals: From da4b53bfff8598c40d773f954635453c63747245 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 19 Oct 2025 13:45:39 +0200 Subject: [PATCH 05/16] Avoid correlation on hide-phase. Should be --- src/connectdlg.cpp | 213 ++++++++++++++++++++++++++++++++++++++++++--- src/connectdlg.h | 9 +- 2 files changed, 206 insertions(+), 16 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 7058beef42..91a7f0df48 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -1,4 +1,4 @@ -/******************************************************************************\ +/******************************************************************************\ * Copyright (c) 2004-2025 * * Author(s): @@ -168,6 +168,7 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR #ifdef PING_STEALTH_MODE TimerKeepPingAfterHide.setSingleShot ( true ); // single shot shutdown timer iKeepPingAfterHideStartTime = 0; + iKeepPingAfterHideStartTimestamp = 0; #endif #if defined( ANDROID ) || defined( Q_OS_IOS ) @@ -300,6 +301,17 @@ void CConnectDlg::hideEvent ( QHideEvent* ) // Start shutdown timer with randomized duration // This keeps ping timer running for stealth purposes to avoid correlation iKeepPingAfterHideStartTime = PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ); + iKeepPingAfterHideStartTimestamp = QDateTime::currentMSecsSinceEpoch(); + + // Set all servers' last ping timestamp to current time + // This ensures the shutdown logic in OnTimerPing() will trigger random delays (500-4000ms) + // for each server's first shutdown ping, creating a uniform distribution + const int iServerListLen = lvwServers->topLevelItemCount(); + for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) + { + QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); + pItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iKeepPingAfterHideStartTimestamp ) ); + } TimerKeepPingAfterHide.start ( iKeepPingAfterHideStartTime ); #else TimerPing.stop(); @@ -331,6 +343,9 @@ void CConnectDlg::OnTimerKeepPingAfterHide() { // Shutdown timer expired - now stop all ping activities TimerPing.stop(); +#ifdef PING_STEALTH_MODE_DETAILED_STATS + pingStealthModeDebugStats(); +#endif } void CConnectDlg::OnTimerReRequestServList() @@ -492,6 +507,7 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, "0" ); pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant() ); // QQueue for ping stats, will be initialized on first ping + pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole +1, QRandomGenerator::global()->bounded ( 800 ) ); // random ping salt per server #endif // store host address @@ -512,7 +528,7 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 500 ) + 1000; + int iPingUpdateInterval = QRandomGenerator::global()->bounded ( 250 ) + 500; #else int iPingUpdateInterval = PING_UPDATE_TIME_SERVER_LIST_MS; #endif @@ -863,6 +879,7 @@ void CConnectDlg::OnTimerPing() const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); const qint64 iLastPingTimestamp = pCurListViewItem->text ( LVC_LAST_PING_TIMESTAMP_HIDDEN ).toLongLong(); + const int iServerSalt = pCurListViewItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole + 1 ).toInt(); const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; // Calculate adaptive ping interval based on latency using linear formula: @@ -874,23 +891,29 @@ void CConnectDlg::OnTimerPing() } else { - // Calculate adaptive ping interval using smooth floating-point multiplier - // Linear mapping: 15ms->1.0x, 20ms->1.2x, 25ms->1.4x, etc. - // Capped at ~265ms ping which gives 9.0x multiplier (max ~22.5s interval) - const float fIntervalMultiplier = std::min ( 9.0f, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ); + // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at 9x), + // combined with geographic (high-latency servers get 3-50% extra variance) and random factors (±15%) + const float fGeoFactor = 1.0f + (std::min(500,iMinPingTime) / 100.0f ) * 0.2f; // high ping get more variance + const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; + const float fIntervalMultiplier = std::min ( 9.0f, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); - // During shutdown: cap max interval to 3 seconds for first 5 seconds only + iPingInterval += iServerSalt; + + // Add randomization as absolute time offset ( 500ms) to prevent any synchronized pings across servers + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms + iPingInterval += iRandomOffsetMs; + + // During shutdown: randomly sent a ping for first 7 seconds only if ( TimerKeepPingAfterHide.isActive() && TimerKeepPingAfterHide.remainingTime() > ( iKeepPingAfterHideStartTime - 5000 ) ) - { - iPingInterval = std::min ( iPingInterval, 3000 ); - } - } + { + iPingInterval = QRandomGenerator::global()->bounded ( 4000 ); + } - // Add randomization as absolute time offset ( 500ms) to prevent any synchronized pings across servers - const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms - iPingInterval += iRandomOffsetMs; + iPingInterval = std::max ( iPingInterval, 100 ); // enforce minimum interval + } + QQueue pingQueue = pCurListViewItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; @@ -1187,3 +1210,165 @@ void CConnectDlg::UpdateDirectoryComboBox() } } } + +#ifdef PING_STEALTH_MODE_DETAILED_STATS +void CConnectDlg::pingStealthModeDebugStats() +{ + // Output final ping statistics for all servers before stopping + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + const int iServerListLen = lvwServers->topLevelItemCount(); + + qDebug() << "\n========================================"; + qDebug() << "FINAL PING STATISTICS (Last 60 seconds)"; + qDebug() << "Timestamp:" << QDateTime::fromMSecsSinceEpoch ( iCurrentTime ).toString ( "yyyy-MM-dd hh:mm:ss" ); + qDebug() << "Total servers:" << iServerListLen; + qDebug() << "========================================\n"; + + // Collect aggregate data for pattern analysis + QVector allPingsPerSecond ( 60, 0 ); // 60 buckets for 60 seconds + int iTotalPings = 0; + int iServersWithPings = 0; + + for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) + { + QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); + QString serverName = pItem->text ( LVC_NAME ); + QQueue pingQueue = pItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); + + if ( pingQueue.isEmpty() ) + continue; + + iServersWithPings++; + iTotalPings += pingQueue.size(); + + // ← NEU: Finde ersten Ping in der Shutdown-Phase + qint64 iFirstShutdownPing = -1; + for ( const qint64& timestamp : pingQueue ) + { + if ( timestamp >= iKeepPingAfterHideStartTimestamp ) + { + iFirstShutdownPing = timestamp; + break; + } + } + + qint64 iMsSinceShutdownStart = -1; + if ( iFirstShutdownPing > 0 ) + { + iMsSinceShutdownStart = iFirstShutdownPing - iKeepPingAfterHideStartTimestamp; + } + + // Per-server statistics + qDebug() << "Server:" << serverName.leftJustified ( 30 ) << "Pings:" << pingQueue.size() + << "First:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.first() ).toString ( "hh:mm:ss" ) + << "Last:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.last() ).toString ( "hh:mm:ss" ) + << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); // ← NEU + + // Build histogram (1-second buckets) + QVector histogram ( 60, 0 ); + for ( const qint64& timestamp : pingQueue ) + { + qint64 age = iCurrentTime - timestamp; + if ( age >= 0 && age < 60000 ) + { + int bucket = age / 1000; // 1-second buckets + histogram[59 - bucket]++; // Reverse order (oldest -> newest) + allPingsPerSecond[59 - bucket]++; + } + } + + // ASCII chart (60 characters for 60 seconds, '*' for each ping) + QString chart; + for ( int count : histogram ) + { + chart += ( count > 0 ) ? QString ( count, '*' ) : "."; + } + qDebug() << " Timeline:" << chart; + + // Calculate rate + if ( pingQueue.size() > 1 ) + { + qint64 timeSpan = pingQueue.last() - pingQueue.first(); + double rate = ( timeSpan > 0 ) ? ( pingQueue.size() - 1 ) * 60000.0 / timeSpan : 0.0; + qDebug() << " Rate:" << QString::number ( rate, 'f', 2 ) << "pings/min"; + } + qDebug() << ""; + } + + // Aggregate pattern analysis + qDebug() << "========================================"; + qDebug() << "AGGREGATE PATTERN ANALYSIS"; + qDebug() << "Total pings across all servers:" << iTotalPings; + qDebug() << "Servers with pings:" << iServersWithPings << "/" << iServerListLen; + qDebug() << "Average pings per server:" << ( iServersWithPings > 0 ? (double) iTotalPings / iServersWithPings : 0.0 ); + qDebug() << ""; + + // Aggregate timeline (all servers combined) + qDebug() << "Combined timeline (all servers, last 60s):"; + QString aggregateChart; + int maxPingsInBucket = *std::max_element ( allPingsPerSecond.begin(), allPingsPerSecond.end() ); + + // Normalize to max 10 characters height + for ( int count : allPingsPerSecond ) + { + if ( maxPingsInBucket > 0 ) + { + int normalizedHeight = ( count * 10 ) / maxPingsInBucket; + aggregateChart += QString::number ( normalizedHeight ); + } + else + { + aggregateChart += "0"; + } + } + qDebug() << "Scale: 0-9 (9 = max" << maxPingsInBucket << "pings/sec)"; + qDebug() << aggregateChart; + qDebug() << "Time: [60s ago ←→ now]"; + + // Detect potential patterns (spikes, regularity) + int iSpikesDetected = 0; + double avgPingsPerSecond = (double) iTotalPings / 60.0; + for ( int count : allPingsPerSecond ) + { + if ( count > avgPingsPerSecond * 2.0 ) // Spike = 2x average + { + iSpikesDetected++; + } + } + + qDebug() << "\nPattern analysis:"; + qDebug() << " Average pings/sec:" << QString::number ( avgPingsPerSecond, 'f', 2 ); + qDebug() << " Spikes detected (>2x avg):" << iSpikesDetected << "seconds"; + qDebug() << " Distribution: " << ( iSpikesDetected > 10 ? "IRREGULAR (many spikes)" : "SMOOTH (good stealth)" ); + + // Gruppiere Server nach Rate (±0.5 pings/min Toleranz) + QMap rateGroups; // Key: Rate*100 (z.B. 308 = 3.08), Value: Count + for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) + { + QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); + QQueue pingQueue = pItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); + + if ( pingQueue.size() > 1 ) + { + qint64 timeSpan = pingQueue.last() - pingQueue.first(); + if ( timeSpan > 0 ) + { + double rate = ( pingQueue.size() - 1 ) * 60000.0 / timeSpan; + int rateKey = static_cast ( rate * 100 ); // z.B. 3.08 → 308 + rateGroups[rateKey]++; + } + } + } + + qDebug() << "Rate clustering (servers with similar rates):"; + for ( auto it = rateGroups.begin(); it != rateGroups.end(); ++it ) + { + if ( it.value() > 1 ) // Nur Gruppen mit >1 Server + { + qDebug() << " Rate:" << QString::number ( it.key() / 100.0, 'f', 2 ) << "pings/min → Servers:" << it.value(); + } + } + + qDebug() << "========================================\n"; +} +#endif diff --git a/src/connectdlg.h b/src/connectdlg.h index b176e3600a..4ddbfdc75b 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -45,8 +45,9 @@ #define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #ifdef PING_STEALTH_MODE -#define PING_SHUTDOWN_TIME_MS_MIN 30000 // recommend to keep it like this and not lower -#define PING_SHUTDOWN_TIME_MS_VAR 10000 +//#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging +#define PING_SHUTDOWN_TIME_MS_MIN 40000 // recommend to keep it like this and not lower +#define PING_SHUTDOWN_TIME_MS_VAR 20000 // Register QQueue as Qt metatype for use in QVariant to keep ping stats in UserRole Q_DECLARE_METATYPE ( QQueue ) #endif @@ -103,6 +104,9 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase void RequestServerList(); void EmitCLServerListPingMes ( const CHostAddress& haServerAddress, const bool bNeedVersion ); void UpdateDirectoryComboBox(); +#ifdef PING_STEALTH_MODE_DETAILED_STATS + void pingStealthModeDebugStats(); +#endif CClientSettings* pSettings; @@ -110,6 +114,7 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase #ifdef PING_STEALTH_MODE QTimer TimerKeepPingAfterHide; qint64 iKeepPingAfterHideStartTime; // shutdown ping timer in ms epoch + qint64 iKeepPingAfterHideStartTimestamp; // timestamp when keeping pings after hide started #endif QTimer TimerReRequestServList; QTimer TimerInitialSort; From f1aff9dad4214ccd442ecda6e715a7cf2878014a Mon Sep 17 00:00:00 2001 From: Stefan <2741523+stefan1000@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:52:25 +0200 Subject: [PATCH 06/16] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index aa525b0cfa..d153071080 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +# This is a fork to test out anti-correlation strategies, use at your own risk. + +For details see discussion in +https://github.com/orgs/jamulussoftware/discussions/3545 + +This is a client-side only fix: instead of pinging each server at a constant rate and stop when connecting this patch will randomize the frequency and keeps pinging even when the dialog is closed, makeing correlation events harder. It may still be possible at times when there is really no activity on jamulus servers over a long time, so with all randomization you are still the only user... + + + [![Homepage picture](https://github.com/jamulussoftware/jamuluswebsite/blob/release/assets/img/jamulusbannersmall.png)](https://jamulus.io) [![Auto-Build](https://github.com/jamulussoftware/jamulus/actions/workflows/autobuild.yml/badge.svg)](https://github.com/jamulussoftware/jamulus/actions/workflows/autobuild.yml) From 62750c101271e51610d1b2bb1cf22383254d2072 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 19 Oct 2025 17:23:45 +0200 Subject: [PATCH 07/16] Initial settings for adaptive too extreme, we need to be nearer to the normal 2500ms ping interval otherwise the longer delay will be treated as a stop of ping, fPingMaxMultiplier is now 3.0 instead of 9.0, could be increased in the future... --- src/connectdlg.cpp | 23 +++++++++++++---------- src/connectdlg.h | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 91a7f0df48..918f0afafe 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -507,7 +507,7 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, "0" ); pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant() ); // QQueue for ping stats, will be initialized on first ping - pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole +1, QRandomGenerator::global()->bounded ( 800 ) ); // random ping salt per server + pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole +1, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server #endif // store host address @@ -528,7 +528,7 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 250 ) + 500; + int iPingUpdateInterval = QRandomGenerator::global()->bounded ( 100 ) + 500; #else int iPingUpdateInterval = PING_UPDATE_TIME_SERVER_LIST_MS; #endif @@ -891,23 +891,26 @@ void CConnectDlg::OnTimerPing() } else { - // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at 9x), + // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at fPingMaxMultiplier), // combined with geographic (high-latency servers get 3-50% extra variance) and random factors (±15%) + const float fPingMaxMultiplier = 3.0f; const float fGeoFactor = 1.0f + (std::min(500,iMinPingTime) / 100.0f ) * 0.2f; // high ping get more variance const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; - const float fIntervalMultiplier = std::min ( 9.0f, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; - iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); + const float fIntervalMultiplier = std::min (fPingMaxMultiplier, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; + + iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); iPingInterval += iServerSalt; - // Add randomization as absolute time offset ( 500ms) to prevent any synchronized pings across servers - const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 1000 ) - 500; // -500ms to +500ms + // Add randomization as absolute time offset ( 300ms) to prevent any synchronized pings across servers + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 600 ) - 300; // -300ms to +300ms iPingInterval += iRandomOffsetMs; - // During shutdown: randomly sent a ping for first 7 seconds only - if ( TimerKeepPingAfterHide.isActive() && TimerKeepPingAfterHide.remainingTime() > ( iKeepPingAfterHideStartTime - 5000 ) ) + // During shutdown: randomly sent a ping for first 15 seconds only + const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; + if ( TimerKeepPingAfterHide.isActive() && iTimeSinceHide < 15000 ) { - iPingInterval = QRandomGenerator::global()->bounded ( 4000 ); + iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); } iPingInterval = std::max ( iPingInterval, 100 ); // enforce minimum interval diff --git a/src/connectdlg.h b/src/connectdlg.h index 4ddbfdc75b..ad6d6328ef 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -46,8 +46,8 @@ #define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #ifdef PING_STEALTH_MODE //#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging -#define PING_SHUTDOWN_TIME_MS_MIN 40000 // recommend to keep it like this and not lower -#define PING_SHUTDOWN_TIME_MS_VAR 20000 +#define PING_SHUTDOWN_TIME_MS_MIN 45000 // recommend to keep it like this and not lower +#define PING_SHUTDOWN_TIME_MS_VAR 15000 // Register QQueue as Qt metatype for use in QVariant to keep ping stats in UserRole Q_DECLARE_METATYPE ( QQueue ) #endif From 8a070903efa8052397684ecd1beaff05b7c258d8 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 19 Oct 2025 19:05:21 +0200 Subject: [PATCH 08/16] qhostaddress is not registered in qt5 --- src/connectdlg.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/connectdlg.h b/src/connectdlg.h index ad6d6328ef..bf9939aa4c 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -48,8 +48,10 @@ //#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging #define PING_SHUTDOWN_TIME_MS_MIN 45000 // recommend to keep it like this and not lower #define PING_SHUTDOWN_TIME_MS_VAR 15000 +// Register QHostAddress as a Qt meta type +Q_DECLARE_METATYPE ( QHostAddress ) // Register QQueue as Qt metatype for use in QVariant to keep ping stats in UserRole -Q_DECLARE_METATYPE ( QQueue ) +Q_DECLARE_METATYPE ( QQueue ) #endif From cd27e8cbfac1a70096ac9cdeadf136d980c9519b Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 20 Oct 2025 17:37:26 +0200 Subject: [PATCH 09/16] On main: Cleanup: - store all data in UserRole of first column, no need to have an invisible column, - added enum for userrole fields - detailed stats only in debug - some minor adjustments on timings --- src/connectdlg.cpp | 108 +++++++++++++++++++++++---------------------- src/connectdlg.h | 33 +++++++++----- 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 918f0afafe..961eca7f58 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -143,9 +143,6 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR lvwServers->setColumnCount ( LVC_COLUMNS ); lvwServers->hideColumn ( LVC_PING_MIN_HIDDEN ); lvwServers->hideColumn ( LVC_CLIENTS_MAX_HIDDEN ); -#ifdef PING_STEALTH_MODE - lvwServers->hideColumn ( LVC_LAST_PING_TIMESTAMP_HIDDEN ); -#endif // per default the root shall not be decorated (to save space) lvwServers->setRootIsDecorated ( false ); @@ -166,8 +163,7 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // setup timers TimerInitialSort.setSingleShot ( true ); // only once after list request #ifdef PING_STEALTH_MODE - TimerKeepPingAfterHide.setSingleShot ( true ); // single shot shutdown timer - iKeepPingAfterHideStartTime = 0; + TimerKeepPingAfterHide.setSingleShot ( true ); iKeepPingAfterHideStartTimestamp = 0; #endif @@ -300,19 +296,18 @@ void CConnectDlg::hideEvent ( QHideEvent* ) #ifdef PING_STEALTH_MODE // Start shutdown timer with randomized duration // This keeps ping timer running for stealth purposes to avoid correlation - iKeepPingAfterHideStartTime = PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ); iKeepPingAfterHideStartTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Set all servers' last ping timestamp to current time - // This ensures the shutdown logic in OnTimerPing() will trigger random delays (500-4000ms) - // for each server's first shutdown ping, creating a uniform distribution + // Set all servers' last ping timestamp somehow near to the current time const int iServerListLen = lvwServers->topLevelItemCount(); for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) { QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - pItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iKeepPingAfterHideStartTimestamp ) ); + qint64 iRandomOffset = QRandomGenerator::global()->bounded ( 1000 ); + pItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, iKeepPingAfterHideStartTimestamp - iRandomOffset ); } - TimerKeepPingAfterHide.start ( iKeepPingAfterHideStartTime ); + // this will initiate the "after hide" phase where ping will continue, at the end of which pinging will stop + TimerKeepPingAfterHide.start ( PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ) ); #else TimerPing.stop(); #endif @@ -505,15 +500,15 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); #ifdef PING_STEALTH_MODE - pNewListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, "0" ); - pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant() ); // QQueue for ping stats, will be initialized on first ping - pNewListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole +1, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE, QVariant() ); // QQueue for ping stats, will be initialized on first ping + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_SALT, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, 0 ); #endif // store host address - pNewListViewItem->setData ( LVC_NAME, Qt::UserRole, CurHostAddress.toString() ); - pNewListViewItem->setData ( LVC_NAME, Qt::UserRole + 1, QVariant() ); // cache QHostAddress, will be updated on first ping - pNewListViewItem->setData ( LVC_NAME, Qt::UserRole + 2, QVariant() ); // cache quint16 port number, will be updated on first ping + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_HOST_ADDRESS, CurHostAddress.toString() ); + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE, QVariant() ); // cache QHostAddress, will be updated on first ping + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE, QVariant() ); // cache quint16 port number, will be updated on first ping // per default expand the list item (if not "show all servers") if ( bShowAllMusicians ) @@ -525,7 +520,6 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 100 ) + 500; @@ -795,7 +789,7 @@ void CConnectDlg::OnConnectClicked() QTreeWidgetItem* pCurSelTopListItem = GetParentListViewItem ( CurSelListItemList[0] ); // get host address from selected list view item as a string - strSelectedAddress = pCurSelTopListItem->data ( LVC_NAME, Qt::UserRole ).toString(); + strSelectedAddress = pCurSelTopListItem->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString(); // store selected server name strSelectedServerName = pCurSelTopListItem->text ( LVC_NAME ); @@ -850,8 +844,8 @@ void CConnectDlg::OnTimerPing() const bool bNeedVersion = pCurListViewItem->text ( LVC_VERSION ).isEmpty(); // retrieve cached QHostAddress and port from UserData - QVariant varCachedIP = pCurListViewItem->data ( LVC_NAME, Qt::UserRole + 1 ); - QVariant varCachedPort = pCurListViewItem->data ( LVC_NAME, Qt::UserRole + 2 ); + QVariant varCachedIP = pCurListViewItem->data ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE ); + QVariant varCachedPort = pCurListViewItem->data ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE ); CHostAddress haServerAddress; @@ -864,22 +858,24 @@ void CConnectDlg::OnTimerPing() else { // Fallback: parse and cache if not present (should not happen in normal flow) - if ( !NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, Qt::UserRole ).toString(), haServerAddress, bEnableIPv6 ) ) + if ( !NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString(), + haServerAddress, + bEnableIPv6 ) ) { continue; // Skip this server if parsing fails } // Cache the parsed values for future use - pCurListViewItem->setData ( LVC_NAME, Qt::UserRole + 1, QVariant::fromValue ( haServerAddress.InetAddr ) ); - pCurListViewItem->setData ( LVC_NAME, Qt::UserRole + 2, QVariant::fromValue ( haServerAddress.iPort ) ); + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE, QVariant::fromValue ( haServerAddress.InetAddr ) ); + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE, QVariant::fromValue ( haServerAddress.iPort ) ); } #ifdef PING_STEALTH_MODE // Get minimum ping time and last ping timestamp const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); - const qint64 iLastPingTimestamp = pCurListViewItem->text ( LVC_LAST_PING_TIMESTAMP_HIDDEN ).toLongLong(); - const int iServerSalt = pCurListViewItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole + 1 ).toInt(); + const qint64 iLastPingTimestamp = pCurListViewItem->data ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP ).toLongLong(); + const int iServerSalt = pCurListViewItem->data ( LVC_NAME, USER_ROLE_PING_SALT ).toInt(); const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; // Calculate adaptive ping interval based on latency using linear formula: @@ -893,6 +889,7 @@ void CConnectDlg::OnTimerPing() { // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at fPingMaxMultiplier), // combined with geographic (high-latency servers get 3-50% extra variance) and random factors (±15%) + // fPingMaxMultiplier is currently pretty low to keep it similar to the normal constant ping, should be increased in future versions (like 8-10) const float fPingMaxMultiplier = 3.0f; const float fGeoFactor = 1.0f + (std::min(500,iMinPingTime) / 100.0f ) * 0.2f; // high ping get more variance const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; @@ -902,22 +899,28 @@ void CConnectDlg::OnTimerPing() iPingInterval += iServerSalt; - // Add randomization as absolute time offset ( 300ms) to prevent any synchronized pings across servers + // Add randomization to prevent any synchronized pings across servers with the same ping const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 600 ) - 300; // -300ms to +300ms iPingInterval += iRandomOffsetMs; - // During shutdown: randomly sent a ping for first 15 seconds only - const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; - if ( TimerKeepPingAfterHide.isActive() && iTimeSinceHide < 15000 ) - { - iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); + if ( TimerKeepPingAfterHide.isActive() ) + { + // during shutdown: randomly sent a ping for first 15 seconds only, can be removed in the future when this mode is used + // by the majority of clients + const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; + + if ( iTimeSinceHide < 15000 ) + { + iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); + } + } iPingInterval = std::max ( iPingInterval, 100 ); // enforce minimum interval } - - QQueue pingQueue = pCurListViewItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); +#ifdef PING_STEALTH_MODE_DETAILED_STATS + QQueue pingQueue = pCurListViewItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; double dActualPingRatePerMin = 0.0; @@ -930,7 +933,6 @@ void CConnectDlg::OnTimerPing() } } - // Update diagnostic tooltip with ping statistics QString strTooltip = QString ( "Server: %1\n" "Min Ping: %2 ms\n" "Time Since Last Ping: %3 ms\n" @@ -944,8 +946,8 @@ void CConnectDlg::OnTimerPing() .arg ( dTheoreticalPingRatePerMin, 0, 'f', 1 ) .arg ( dActualPingRatePerMin, 0, 'f', 1 ); - // Set tooltip only for ping column pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); +#endif // Skip this server if not enough time has passed since last ping if ( iTimeSinceLastPing < iPingInterval ) @@ -953,19 +955,19 @@ void CConnectDlg::OnTimerPing() continue; } - pCurListViewItem->setText ( LVC_LAST_PING_TIMESTAMP_HIDDEN, QString::number ( iCurrentTime ) ); + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, qint64( iCurrentTime ) ); - // Track ping timestamp in queue stored in Qt::UserRole of LVC_LAST_PING_TIMESTAMP_HIDDEN +#ifdef PING_STEALTH_MODE_DETAILED_STATS pingQueue.enqueue ( iCurrentTime ); - - // Remove pings older than 60 seconds + // remove pings older than 60 seconds const qint64 iOneMinuteAgo = iCurrentTime - 60000; while ( !pingQueue.isEmpty() && pingQueue.head() < iOneMinuteAgo ) { pingQueue.dequeue(); } - pCurListViewItem->setData ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole, QVariant::fromValue ( pingQueue ) ); -#endif + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE, QVariant::fromValue ( pingQueue ) ); +#endif // PING_STEALTH_MODE_DETAILED_STATS +#endif // PING_STEALTH_MODE // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) @@ -1148,7 +1150,7 @@ QTreeWidgetItem* CConnectDlg::FindListViewItem ( const CHostAddress& InetAddr ) { // compare the received address with the user data string of the // host address by a string compare - if ( !lvwServers->topLevelItem ( iIdx )->data ( LVC_NAME, Qt::UserRole ).toString().compare ( InetAddr.toString() ) ) + if ( !lvwServers->topLevelItem ( iIdx )->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString().compare ( InetAddr.toString() ) ) { return lvwServers->topLevelItem ( iIdx ); } @@ -1236,7 +1238,7 @@ void CConnectDlg::pingStealthModeDebugStats() { QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); QString serverName = pItem->text ( LVC_NAME ); - QQueue pingQueue = pItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); + QQueue pingQueue = pItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); if ( pingQueue.isEmpty() ) continue; @@ -1244,7 +1246,6 @@ void CConnectDlg::pingStealthModeDebugStats() iServersWithPings++; iTotalPings += pingQueue.size(); - // ← NEU: Finde ersten Ping in der Shutdown-Phase qint64 iFirstShutdownPing = -1; for ( const qint64& timestamp : pingQueue ) { @@ -1265,7 +1266,7 @@ void CConnectDlg::pingStealthModeDebugStats() qDebug() << "Server:" << serverName.leftJustified ( 30 ) << "Pings:" << pingQueue.size() << "First:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.first() ).toString ( "hh:mm:ss" ) << "Last:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.last() ).toString ( "hh:mm:ss" ) - << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); // ← NEU + << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); // Build histogram (1-second buckets) QVector histogram ( 60, 0 ); @@ -1326,7 +1327,7 @@ void CConnectDlg::pingStealthModeDebugStats() } qDebug() << "Scale: 0-9 (9 = max" << maxPingsInBucket << "pings/sec)"; qDebug() << aggregateChart; - qDebug() << "Time: [60s ago ←→ now]"; + qDebug() << "Time: [60s ago <-> now]"; // Detect potential patterns (spikes, regularity) int iSpikesDetected = 0; @@ -1344,12 +1345,13 @@ void CConnectDlg::pingStealthModeDebugStats() qDebug() << " Spikes detected (>2x avg):" << iSpikesDetected << "seconds"; qDebug() << " Distribution: " << ( iSpikesDetected > 10 ? "IRREGULAR (many spikes)" : "SMOOTH (good stealth)" ); - // Gruppiere Server nach Rate (±0.5 pings/min Toleranz) - QMap rateGroups; // Key: Rate*100 (z.B. 308 = 3.08), Value: Count + // group by rate (±0.5 pings/min tolerance) + QMap rateGroups; // Key: Rate*100, Value: Count for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) { QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - QQueue pingQueue = pItem->data ( LVC_LAST_PING_TIMESTAMP_HIDDEN, Qt::UserRole ).value>(); + QQueue pingQueue = pItem->data + ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); if ( pingQueue.size() > 1 ) { @@ -1357,7 +1359,7 @@ void CConnectDlg::pingStealthModeDebugStats() if ( timeSpan > 0 ) { double rate = ( pingQueue.size() - 1 ) * 60000.0 / timeSpan; - int rateKey = static_cast ( rate * 100 ); // z.B. 3.08 → 308 + int rateKey = static_cast ( rate * 100 ); rateGroups[rateKey]++; } } @@ -1366,9 +1368,9 @@ void CConnectDlg::pingStealthModeDebugStats() qDebug() << "Rate clustering (servers with similar rates):"; for ( auto it = rateGroups.begin(); it != rateGroups.end(); ++it ) { - if ( it.value() > 1 ) // Nur Gruppen mit >1 Server + if ( it.value() > 1 ) { - qDebug() << " Rate:" << QString::number ( it.key() / 100.0, 'f', 2 ) << "pings/min → Servers:" << it.value(); + qDebug() << " Rate:" << QString::number ( it.key() / 100.0, 'f', 2 ) << "pings/min -> Servers:" << it.value(); } } diff --git a/src/connectdlg.h b/src/connectdlg.h index bf9939aa4c..7e3b9b0bd0 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,15 +43,17 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms +#define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity + #define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #ifdef PING_STEALTH_MODE -//#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging -#define PING_SHUTDOWN_TIME_MS_MIN 45000 // recommend to keep it like this and not lower -#define PING_SHUTDOWN_TIME_MS_VAR 15000 -// Register QHostAddress as a Qt meta type -Q_DECLARE_METATYPE ( QHostAddress ) -// Register QQueue as Qt metatype for use in QVariant to keep ping stats in UserRole -Q_DECLARE_METATYPE ( QQueue ) +#ifdef _DEBUG +#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging +#endif +#define PING_SHUTDOWN_TIME_MS_MIN 45000 +#define PING_SHUTDOWN_TIME_MS_VAR 30000 +Q_DECLARE_METATYPE(QQueue) +Q_DECLARE_METATYPE(QHostAddress) #endif @@ -89,12 +91,22 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_VERSION, // server version LVC_PING_MIN_HIDDEN, // minimum ping time (invisible) LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible), -#ifdef PING_STEALTH_MODE - LVC_LAST_PING_TIMESTAMP_HIDDEN, // timestamp of last ping measurement (invisible) -#endif LVC_COLUMNS // total number of columns }; + + // those are the custom user data fields, all stored in the UserRole of the list view item (LVC_NAME) + enum EColumnPingNameUserRoles + { + USER_ROLE_HOST_ADDRESS = Qt::UserRole, // QString: CHostAddress as string + USER_ROLE_QHOST_ADDRESS_CACHE, // QHostAddress: cache QHostAddress, will be updated on first ping + USER_ROLE_QHOST_PORT_CACHE, // quint16: cache port number, will be updated on first ping + USER_ROLE_LAST_PING_TIMESTAMP, // qint64: timestamp of last ping measurement + USER_ROLE_PING_TIMES_QUEUE, // QQueue: for ping stats, will be initialized on first ping + USER_ROLE_PING_SALT // int: random ping salt per server + }; + + virtual void showEvent ( QShowEvent* ); virtual void hideEvent ( QHideEvent* ); @@ -115,7 +127,6 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase QTimer TimerPing; #ifdef PING_STEALTH_MODE QTimer TimerKeepPingAfterHide; - qint64 iKeepPingAfterHideStartTime; // shutdown ping timer in ms epoch qint64 iKeepPingAfterHideStartTimestamp; // timestamp when keeping pings after hide started #endif QTimer TimerReRequestServList; From 043dc6c7c8f1f5d4592e96a97d239ce8aa78810e Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 20 Oct 2025 17:41:24 +0200 Subject: [PATCH 10/16] Cleanup, double define --- src/connectdlg.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/connectdlg.h b/src/connectdlg.h index 7e3b9b0bd0..9b98fa9eaf 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,7 +43,6 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms -#define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity #ifdef PING_STEALTH_MODE From cc3367027e6e91ac449a152a023709971c056a7f Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 22 Oct 2025 20:27:19 +0200 Subject: [PATCH 11/16] Revert README to original --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index d153071080..aa525b0cfa 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ -# This is a fork to test out anti-correlation strategies, use at your own risk. - -For details see discussion in -https://github.com/orgs/jamulussoftware/discussions/3545 - -This is a client-side only fix: instead of pinging each server at a constant rate and stop when connecting this patch will randomize the frequency and keeps pinging even when the dialog is closed, makeing correlation events harder. It may still be possible at times when there is really no activity on jamulus servers over a long time, so with all randomization you are still the only user... - - - [![Homepage picture](https://github.com/jamulussoftware/jamuluswebsite/blob/release/assets/img/jamulusbannersmall.png)](https://jamulus.io) [![Auto-Build](https://github.com/jamulussoftware/jamulus/actions/workflows/autobuild.yml/badge.svg)](https://github.com/jamulussoftware/jamulus/actions/workflows/autobuild.yml) From cbd362b1045c6b0a2ea65b53fa989133a6116675 Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 22 Oct 2025 20:46:54 +0200 Subject: [PATCH 12/16] Cleanup, removed define, only one ping enforced, keep normal frequency. clang format. --- src/connectdlg.cpp | 101 ++++++++++++++++----------------------------- src/connectdlg.h | 27 +++++------- 2 files changed, 45 insertions(+), 83 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 961eca7f58..bb9cb95ffd 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -36,7 +36,8 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR bServerListItemWasChosen ( false ), bListFilterWasActive ( false ), bShowAllMusicians ( true ), - bEnableIPv6 ( bNEnableIPv6 ) + bEnableIPv6 ( bNEnableIPv6 ), + iKeepPingAfterHideStartTimestamp ( 0 ) { setupUi ( this ); @@ -162,10 +163,8 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // setup timers TimerInitialSort.setSingleShot ( true ); // only once after list request -#ifdef PING_STEALTH_MODE + TimerKeepPingAfterHide.setSingleShot ( true ); - iKeepPingAfterHideStartTimestamp = 0; -#endif #if defined( ANDROID ) || defined( Q_OS_IOS ) // for the Android and iOS version maximize the window @@ -202,18 +201,14 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing ); QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList ); -#ifdef PING_STEALTH_MODE - QObject::connect ( &TimerKeepPingAfterHide, &QTimer::timeout, this, &CConnectDlg::OnTimerKeepPingAfterHide ); -#endif + QObject::connect ( &TimerKeepPingAfterHide, &QTimer::timeout, this, &CConnectDlg::OnTimerKeepPingAfterHide ); } void CConnectDlg::showEvent ( QShowEvent* ) { -#ifdef PING_STEALTH_MODE // Stop shutdown timer if dialog is shown again before it expires TimerKeepPingAfterHide.stop(); -#endif // load stored IP addresses in combo box cbxServerAddr->clear(); @@ -234,10 +229,8 @@ void CConnectDlg::showEvent ( QShowEvent* ) void CConnectDlg::RequestServerList() { -#ifdef PING_STEALTH_MODE // Ensure shutdown timer is stopped when requesting new server list TimerKeepPingAfterHide.stop(); -#endif // reset flags bServerListReceived = false; @@ -293,26 +286,12 @@ void CConnectDlg::hideEvent ( QHideEvent* ) // Stop the regular server list request timer immediately TimerReRequestServList.stop(); -#ifdef PING_STEALTH_MODE - // Start shutdown timer with randomized duration - // This keeps ping timer running for stealth purposes to avoid correlation iKeepPingAfterHideStartTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Set all servers' last ping timestamp somehow near to the current time - const int iServerListLen = lvwServers->topLevelItemCount(); - for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) - { - QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - qint64 iRandomOffset = QRandomGenerator::global()->bounded ( 1000 ); - pItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, iKeepPingAfterHideStartTimestamp - iRandomOffset ); - } // this will initiate the "after hide" phase where ping will continue, at the end of which pinging will stop - TimerKeepPingAfterHide.start ( PING_SHUTDOWN_TIME_MS_MIN + QRandomGenerator::global()->bounded ( PING_SHUTDOWN_TIME_MS_VAR ) ); -#else - TimerPing.stop(); -#endif - - + const int iRandomizedShutdownMs = + static_cast ( KEEP_PING_RUNNING_AFTER_HIDE_MS * ( 0.8f + QRandomGenerator::global()->generateDouble() * 0.4f ) ); + TimerKeepPingAfterHide.start ( iRandomizedShutdownMs ); } void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) @@ -499,11 +478,11 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); -#ifdef PING_STEALTH_MODE - pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE, QVariant() ); // QQueue for ping stats, will be initialized on first ping + pNewListViewItem->setData ( LVC_NAME, + USER_ROLE_PING_TIMES_QUEUE, + QVariant() ); // QQueue for ping stats, will be initialized on first ping pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_SALT, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server - pNewListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, 0 ); -#endif + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, 0 ); // store host address pNewListViewItem->setData ( LVC_NAME, USER_ROLE_HOST_ADDRESS, CurHostAddress.toString() ); @@ -520,12 +499,9 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 100 ) + 500; -#else - int iPingUpdateInterval = PING_UPDATE_TIME_SERVER_LIST_MS; -#endif + int iPingUpdateInterval = QRandomGenerator::global()->bounded ( 50 ) + 500; TimerPing.start ( iPingUpdateInterval ); } @@ -720,7 +696,7 @@ void CConnectDlg::UpdateListFilter() bFilterFound = true; } - // search children + // search children for ( int iCCnt = 0; iCCnt < pCurListViewItem->childCount(); iCCnt++ ) { if ( pCurListViewItem->child ( iCCnt )->text ( LVC_NAME ).indexOf ( sFilterText, 0, Qt::CaseInsensitive ) >= 0 ) @@ -870,7 +846,6 @@ void CConnectDlg::OnTimerPing() pCurListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE, QVariant::fromValue ( haServerAddress.iPort ) ); } -#ifdef PING_STEALTH_MODE // Get minimum ping time and last ping timestamp const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); @@ -889,34 +864,33 @@ void CConnectDlg::OnTimerPing() { // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at fPingMaxMultiplier), // combined with geographic (high-latency servers get 3-50% extra variance) and random factors (±15%) - // fPingMaxMultiplier is currently pretty low to keep it similar to the normal constant ping, should be increased in future versions (like 8-10) - const float fPingMaxMultiplier = 3.0f; - const float fGeoFactor = 1.0f + (std::min(500,iMinPingTime) / 100.0f ) * 0.2f; // high ping get more variance - const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; - const float fIntervalMultiplier = std::min (fPingMaxMultiplier, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; + // fPingMaxMultiplier is currently pretty low to keep it similar to the normal constant ping, should be increased in future versions (like + // 8-10) + const float fPingMaxMultiplier = 3.0f; + const float fGeoFactor = 1.0f + ( std::min ( 500, iMinPingTime ) / 100.0f ) * 0.2f; // high ping get more variance + const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; + const float fIntervalMultiplier = + std::min ( fPingMaxMultiplier, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); iPingInterval += iServerSalt; - + // Add randomization to prevent any synchronized pings across servers with the same ping const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 600 ) - 300; // -300ms to +300ms iPingInterval += iRandomOffsetMs; + iPingInterval = std::max ( iPingInterval, 500 ); + // for now force one ping after the hide phase has elapsed 3500ms + // can be removed once the majority of clients is using this if ( TimerKeepPingAfterHide.isActive() ) { - // during shutdown: randomly sent a ping for first 15 seconds only, can be removed in the future when this mode is used - // by the majority of clients const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; - - if ( iTimeSinceHide < 15000 ) + if ( iTimeSinceHide >= 3500 && iLastPingTimestamp < iKeepPingAfterHideStartTimestamp ) { - iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); + iPingInterval = 0; } - - } - - iPingInterval = std::max ( iPingInterval, 100 ); // enforce minimum interval + } } #ifdef PING_STEALTH_MODE_DETAILED_STATS @@ -947,7 +921,7 @@ void CConnectDlg::OnTimerPing() .arg ( dActualPingRatePerMin, 0, 'f', 1 ); pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); -#endif +#endif // PING_STEALTH_MODE_DETAILED_STATS // Skip this server if not enough time has passed since last ping if ( iTimeSinceLastPing < iPingInterval ) @@ -955,7 +929,7 @@ void CConnectDlg::OnTimerPing() continue; } - pCurListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, qint64( iCurrentTime ) ); + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, qint64 ( iCurrentTime ) ); #ifdef PING_STEALTH_MODE_DETAILED_STATS pingQueue.enqueue ( iCurrentTime ); @@ -966,8 +940,7 @@ void CConnectDlg::OnTimerPing() pingQueue.dequeue(); } pCurListViewItem->setData ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE, QVariant::fromValue ( pingQueue ) ); -#endif // PING_STEALTH_MODE_DETAILED_STATS -#endif // PING_STEALTH_MODE +#endif // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) @@ -994,9 +967,6 @@ void CConnectDlg::EmitCLServerListPingMes ( const CHostAddress& haServerAddress, emit CreateCLServerListReqVerAndOSMes ( haServerAddress ); } - - - emit CreateCLServerListPingMes ( haServerAddress ); } @@ -1266,7 +1236,7 @@ void CConnectDlg::pingStealthModeDebugStats() qDebug() << "Server:" << serverName.leftJustified ( 30 ) << "Pings:" << pingQueue.size() << "First:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.first() ).toString ( "hh:mm:ss" ) << "Last:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.last() ).toString ( "hh:mm:ss" ) - << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); + << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); // Build histogram (1-second buckets) QVector histogram ( 60, 0 ); @@ -1350,8 +1320,7 @@ void CConnectDlg::pingStealthModeDebugStats() for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) { QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - QQueue pingQueue = pItem->data - ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); + QQueue pingQueue = pItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); if ( pingQueue.size() > 1 ) { @@ -1359,7 +1328,7 @@ void CConnectDlg::pingStealthModeDebugStats() if ( timeSpan > 0 ) { double rate = ( pingQueue.size() - 1 ) * 60000.0 / timeSpan; - int rateKey = static_cast ( rate * 100 ); + int rateKey = static_cast ( rate * 100 ); rateGroups[rateKey]++; } } @@ -1368,7 +1337,7 @@ void CConnectDlg::pingStealthModeDebugStats() qDebug() << "Rate clustering (servers with similar rates):"; for ( auto it = rateGroups.begin(); it != rateGroups.end(); ++it ) { - if ( it.value() > 1 ) + if ( it.value() > 1 ) { qDebug() << " Rate:" << QString::number ( it.key() / 100.0, 'f', 2 ) << "pings/min -> Servers:" << it.value(); } diff --git a/src/connectdlg.h b/src/connectdlg.h index 9b98fa9eaf..99ffd56a0f 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,18 +43,15 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms +// defines the time interval it will keep pinging servers after the dialog was hidden (randomized +/- 20%) +#define KEEP_PING_RUNNING_AFTER_HIDE_MS 60000 -#define PING_STEALTH_MODE // prototype to avoid correlation of pings and user activity -#ifdef PING_STEALTH_MODE #ifdef _DEBUG -#define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging -#endif -#define PING_SHUTDOWN_TIME_MS_MIN 45000 -#define PING_SHUTDOWN_TIME_MS_VAR 30000 -Q_DECLARE_METATYPE(QQueue) -Q_DECLARE_METATYPE(QHostAddress) +# define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging #endif +Q_DECLARE_METATYPE ( QQueue ) +Q_DECLARE_METATYPE ( QHostAddress ) /* Classes ********************************************************************/ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase @@ -93,19 +90,17 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_COLUMNS // total number of columns }; - // those are the custom user data fields, all stored in the UserRole of the list view item (LVC_NAME) enum EColumnPingNameUserRoles { USER_ROLE_HOST_ADDRESS = Qt::UserRole, // QString: CHostAddress as string - USER_ROLE_QHOST_ADDRESS_CACHE, // QHostAddress: cache QHostAddress, will be updated on first ping - USER_ROLE_QHOST_PORT_CACHE, // quint16: cache port number, will be updated on first ping - USER_ROLE_LAST_PING_TIMESTAMP, // qint64: timestamp of last ping measurement - USER_ROLE_PING_TIMES_QUEUE, // QQueue: for ping stats, will be initialized on first ping - USER_ROLE_PING_SALT // int: random ping salt per server + USER_ROLE_QHOST_ADDRESS_CACHE, // QHostAddress: cache QHostAddress, will be updated on first ping + USER_ROLE_QHOST_PORT_CACHE, // quint16: cache port number, will be updated on first ping + USER_ROLE_LAST_PING_TIMESTAMP, // qint64: timestamp of last ping measurement + USER_ROLE_PING_TIMES_QUEUE, // QQueue: for ping stats, will be initialized on first ping + USER_ROLE_PING_SALT // int: random ping salt per server }; - virtual void showEvent ( QShowEvent* ); virtual void hideEvent ( QHideEvent* ); @@ -124,10 +119,8 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase CClientSettings* pSettings; QTimer TimerPing; -#ifdef PING_STEALTH_MODE QTimer TimerKeepPingAfterHide; qint64 iKeepPingAfterHideStartTimestamp; // timestamp when keeping pings after hide started -#endif QTimer TimerReRequestServList; QTimer TimerInitialSort; CHostAddress haDirectoryAddress; From 776e7f33115829b0f6558706b14b2d3899625a3f Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 22 Oct 2025 20:54:08 +0200 Subject: [PATCH 13/16] Cleanup --- src/connectdlg.cpp | 3 +-- src/connectdlg.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index bb9cb95ffd..3024cd9015 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -1,4 +1,4 @@ -/******************************************************************************\ +/******************************************************************************\ * Copyright (c) 2004-2025 * * Author(s): @@ -138,7 +138,6 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // 4: server version // 5: minimum ping time (invisible) // 6: maximum number of clients (invisible) - // 7: last ping timestamp (invisible) // (see EConnectListViewColumns in connectdlg.h, which must match the above) lvwServers->setColumnCount ( LVC_COLUMNS ); diff --git a/src/connectdlg.h b/src/connectdlg.h index 99ffd56a0f..2c04828b88 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -86,7 +86,7 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_LOCATION, // location LVC_VERSION, // server version LVC_PING_MIN_HIDDEN, // minimum ping time (invisible) - LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible), + LVC_CLIENTS_MAX_HIDDEN, // maximum number of clients (invisible) LVC_COLUMNS // total number of columns }; From f84b79cc9e1affc7ab0b87a5bb21f36815009779 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 24 Oct 2025 16:50:28 +0200 Subject: [PATCH 14/16] Revert back to the original 15 second window approach, more stable. --- src/connectdlg.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 3024cd9015..c66716de71 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -880,16 +880,17 @@ void CConnectDlg::OnTimerPing() iPingInterval += iRandomOffsetMs; iPingInterval = std::max ( iPingInterval, 500 ); - // for now force one ping after the hide phase has elapsed 3500ms - // can be removed once the majority of clients is using this + // during shutdown: randomly sent a ping for first 15 seconds only, can be removed in the future when this mode is used + // by the majority of clients if ( TimerKeepPingAfterHide.isActive() ) { const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; - if ( iTimeSinceHide >= 3500 && iLastPingTimestamp < iKeepPingAfterHideStartTimestamp ) - { - iPingInterval = 0; + if ( iTimeSinceHide < 15000 ) + { + iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); } } + } #ifdef PING_STEALTH_MODE_DETAILED_STATS From 411f42b3c5fe8bc4dc1d6b6f7348b070e319fb49 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 25 Oct 2025 15:25:04 +0200 Subject: [PATCH 15/16] increase interval --- src/connectdlg.cpp | 4 ++-- src/connectdlg.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index c66716de71..3d234eac17 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -880,12 +880,12 @@ void CConnectDlg::OnTimerPing() iPingInterval += iRandomOffsetMs; iPingInterval = std::max ( iPingInterval, 500 ); - // during shutdown: randomly sent a ping for first 15 seconds only, can be removed in the future when this mode is used + // during shutdown: randomly sent a ping for first 20 to 30 seconds only, can be removed in the future when this mode is used // by the majority of clients if ( TimerKeepPingAfterHide.isActive() ) { const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; - if ( iTimeSinceHide < 15000 ) + if ( iTimeSinceHide < (20000 + QRandomGenerator::global()->bounded ( 10000 ) ) ) { iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); } diff --git a/src/connectdlg.h b/src/connectdlg.h index 2c04828b88..2bab7a472f 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -44,7 +44,7 @@ #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms // defines the time interval it will keep pinging servers after the dialog was hidden (randomized +/- 20%) -#define KEEP_PING_RUNNING_AFTER_HIDE_MS 60000 +#define KEEP_PING_RUNNING_AFTER_HIDE_MS (1000*120) #ifdef _DEBUG # define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging From bd4436ceb50c9c7825de76dc674a67d3c110b74e Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 9 Nov 2025 13:10:03 +0100 Subject: [PATCH 16/16] removed debug/stat code, increased hidden timeout, fixed clang format. --- src/connectdlg.cpp | 213 +-------------------------------------------- src/connectdlg.h | 13 +-- 2 files changed, 4 insertions(+), 222 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 3d234eac17..ff8810db80 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -316,9 +316,6 @@ void CConnectDlg::OnTimerKeepPingAfterHide() { // Shutdown timer expired - now stop all ping activities TimerPing.stop(); -#ifdef PING_STEALTH_MODE_DETAILED_STATS - pingStealthModeDebugStats(); -#endif } void CConnectDlg::OnTimerReRequestServList() @@ -477,9 +474,6 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); - pNewListViewItem->setData ( LVC_NAME, - USER_ROLE_PING_TIMES_QUEUE, - QVariant() ); // QQueue for ping stats, will be initialized on first ping pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_SALT, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server pNewListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, 0 ); @@ -885,44 +879,13 @@ void CConnectDlg::OnTimerPing() if ( TimerKeepPingAfterHide.isActive() ) { const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; - if ( iTimeSinceHide < (20000 + QRandomGenerator::global()->bounded ( 10000 ) ) ) - { + if ( iTimeSinceHide < ( 20000 + QRandomGenerator::global()->bounded ( 10000 ) ) ) + { iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); } } - - } - -#ifdef PING_STEALTH_MODE_DETAILED_STATS - QQueue pingQueue = pCurListViewItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); - - const double dTheoreticalPingRatePerMin = iPingInterval > 0 ? 60000.0 / static_cast ( iPingInterval ) : 0.0; - double dActualPingRatePerMin = 0.0; - if ( pingQueue.size() > 1 ) - { - const qint64 iTimeSpanMs = pingQueue.last() - pingQueue.first(); - if ( iTimeSpanMs > 0 ) - { - dActualPingRatePerMin = ( pingQueue.size() - 1 ) * 60000.0 / static_cast ( iTimeSpanMs ); - } } - QString strTooltip = QString ( "Server: %1\n" - "Min Ping: %2 ms\n" - "Time Since Last Ping: %3 ms\n" - "Calculated Interval: %4 ms\n" - "Theoretical Rate: %5 pings/min\n" - "Actual Rate: %6 pings/min" ) - .arg ( pCurListViewItem->text ( LVC_NAME ) ) - .arg ( iMinPingTime ) - .arg ( iTimeSinceLastPing ) - .arg ( iPingInterval ) - .arg ( dTheoreticalPingRatePerMin, 0, 'f', 1 ) - .arg ( dActualPingRatePerMin, 0, 'f', 1 ); - - pCurListViewItem->setToolTip ( LVC_PING, iMinPingTime < 99999999 ? strTooltip : "n/a" ); -#endif // PING_STEALTH_MODE_DETAILED_STATS - // Skip this server if not enough time has passed since last ping if ( iTimeSinceLastPing < iPingInterval ) { @@ -931,17 +894,6 @@ void CConnectDlg::OnTimerPing() pCurListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, qint64 ( iCurrentTime ) ); -#ifdef PING_STEALTH_MODE_DETAILED_STATS - pingQueue.enqueue ( iCurrentTime ); - // remove pings older than 60 seconds - const qint64 iOneMinuteAgo = iCurrentTime - 60000; - while ( !pingQueue.isEmpty() && pingQueue.head() < iOneMinuteAgo ) - { - pingQueue.dequeue(); - } - pCurListViewItem->setData ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE, QVariant::fromValue ( pingQueue ) ); -#endif - // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); @@ -1185,164 +1137,3 @@ void CConnectDlg::UpdateDirectoryComboBox() } } } - -#ifdef PING_STEALTH_MODE_DETAILED_STATS -void CConnectDlg::pingStealthModeDebugStats() -{ - // Output final ping statistics for all servers before stopping - const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); - const int iServerListLen = lvwServers->topLevelItemCount(); - - qDebug() << "\n========================================"; - qDebug() << "FINAL PING STATISTICS (Last 60 seconds)"; - qDebug() << "Timestamp:" << QDateTime::fromMSecsSinceEpoch ( iCurrentTime ).toString ( "yyyy-MM-dd hh:mm:ss" ); - qDebug() << "Total servers:" << iServerListLen; - qDebug() << "========================================\n"; - - // Collect aggregate data for pattern analysis - QVector allPingsPerSecond ( 60, 0 ); // 60 buckets for 60 seconds - int iTotalPings = 0; - int iServersWithPings = 0; - - for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) - { - QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - QString serverName = pItem->text ( LVC_NAME ); - QQueue pingQueue = pItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); - - if ( pingQueue.isEmpty() ) - continue; - - iServersWithPings++; - iTotalPings += pingQueue.size(); - - qint64 iFirstShutdownPing = -1; - for ( const qint64& timestamp : pingQueue ) - { - if ( timestamp >= iKeepPingAfterHideStartTimestamp ) - { - iFirstShutdownPing = timestamp; - break; - } - } - - qint64 iMsSinceShutdownStart = -1; - if ( iFirstShutdownPing > 0 ) - { - iMsSinceShutdownStart = iFirstShutdownPing - iKeepPingAfterHideStartTimestamp; - } - - // Per-server statistics - qDebug() << "Server:" << serverName.leftJustified ( 30 ) << "Pings:" << pingQueue.size() - << "First:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.first() ).toString ( "hh:mm:ss" ) - << "Last:" << QDateTime::fromMSecsSinceEpoch ( pingQueue.last() ).toString ( "hh:mm:ss" ) - << "FirstShutdown:" << ( iMsSinceShutdownStart >= 0 ? QString::number ( iMsSinceShutdownStart ) + "ms" : "n/a" ); - - // Build histogram (1-second buckets) - QVector histogram ( 60, 0 ); - for ( const qint64& timestamp : pingQueue ) - { - qint64 age = iCurrentTime - timestamp; - if ( age >= 0 && age < 60000 ) - { - int bucket = age / 1000; // 1-second buckets - histogram[59 - bucket]++; // Reverse order (oldest -> newest) - allPingsPerSecond[59 - bucket]++; - } - } - - // ASCII chart (60 characters for 60 seconds, '*' for each ping) - QString chart; - for ( int count : histogram ) - { - chart += ( count > 0 ) ? QString ( count, '*' ) : "."; - } - qDebug() << " Timeline:" << chart; - - // Calculate rate - if ( pingQueue.size() > 1 ) - { - qint64 timeSpan = pingQueue.last() - pingQueue.first(); - double rate = ( timeSpan > 0 ) ? ( pingQueue.size() - 1 ) * 60000.0 / timeSpan : 0.0; - qDebug() << " Rate:" << QString::number ( rate, 'f', 2 ) << "pings/min"; - } - qDebug() << ""; - } - - // Aggregate pattern analysis - qDebug() << "========================================"; - qDebug() << "AGGREGATE PATTERN ANALYSIS"; - qDebug() << "Total pings across all servers:" << iTotalPings; - qDebug() << "Servers with pings:" << iServersWithPings << "/" << iServerListLen; - qDebug() << "Average pings per server:" << ( iServersWithPings > 0 ? (double) iTotalPings / iServersWithPings : 0.0 ); - qDebug() << ""; - - // Aggregate timeline (all servers combined) - qDebug() << "Combined timeline (all servers, last 60s):"; - QString aggregateChart; - int maxPingsInBucket = *std::max_element ( allPingsPerSecond.begin(), allPingsPerSecond.end() ); - - // Normalize to max 10 characters height - for ( int count : allPingsPerSecond ) - { - if ( maxPingsInBucket > 0 ) - { - int normalizedHeight = ( count * 10 ) / maxPingsInBucket; - aggregateChart += QString::number ( normalizedHeight ); - } - else - { - aggregateChart += "0"; - } - } - qDebug() << "Scale: 0-9 (9 = max" << maxPingsInBucket << "pings/sec)"; - qDebug() << aggregateChart; - qDebug() << "Time: [60s ago <-> now]"; - - // Detect potential patterns (spikes, regularity) - int iSpikesDetected = 0; - double avgPingsPerSecond = (double) iTotalPings / 60.0; - for ( int count : allPingsPerSecond ) - { - if ( count > avgPingsPerSecond * 2.0 ) // Spike = 2x average - { - iSpikesDetected++; - } - } - - qDebug() << "\nPattern analysis:"; - qDebug() << " Average pings/sec:" << QString::number ( avgPingsPerSecond, 'f', 2 ); - qDebug() << " Spikes detected (>2x avg):" << iSpikesDetected << "seconds"; - qDebug() << " Distribution: " << ( iSpikesDetected > 10 ? "IRREGULAR (many spikes)" : "SMOOTH (good stealth)" ); - - // group by rate (±0.5 pings/min tolerance) - QMap rateGroups; // Key: Rate*100, Value: Count - for ( int iIdx = 0; iIdx < iServerListLen; iIdx++ ) - { - QTreeWidgetItem* pItem = lvwServers->topLevelItem ( iIdx ); - QQueue pingQueue = pItem->data ( LVC_NAME, USER_ROLE_PING_TIMES_QUEUE ).value>(); - - if ( pingQueue.size() > 1 ) - { - qint64 timeSpan = pingQueue.last() - pingQueue.first(); - if ( timeSpan > 0 ) - { - double rate = ( pingQueue.size() - 1 ) * 60000.0 / timeSpan; - int rateKey = static_cast ( rate * 100 ); - rateGroups[rateKey]++; - } - } - } - - qDebug() << "Rate clustering (servers with similar rates):"; - for ( auto it = rateGroups.begin(); it != rateGroups.end(); ++it ) - { - if ( it.value() > 1 ) - { - qDebug() << " Rate:" << QString::number ( it.key() / 100.0, 'f', 2 ) << "pings/min -> Servers:" << it.value(); - } - } - - qDebug() << "========================================\n"; -} -#endif diff --git a/src/connectdlg.h b/src/connectdlg.h index 2bab7a472f..21d2b58953 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -44,13 +44,8 @@ #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms // defines the time interval it will keep pinging servers after the dialog was hidden (randomized +/- 20%) -#define KEEP_PING_RUNNING_AFTER_HIDE_MS (1000*120) +#define KEEP_PING_RUNNING_AFTER_HIDE_MS ( 1000 * 180 ) -#ifdef _DEBUG -# define PING_STEALTH_MODE_DETAILED_STATS // enable to log detailed ping stats for debugging -#endif - -Q_DECLARE_METATYPE ( QQueue ) Q_DECLARE_METATYPE ( QHostAddress ) /* Classes ********************************************************************/ @@ -97,7 +92,6 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase USER_ROLE_QHOST_ADDRESS_CACHE, // QHostAddress: cache QHostAddress, will be updated on first ping USER_ROLE_QHOST_PORT_CACHE, // quint16: cache port number, will be updated on first ping USER_ROLE_LAST_PING_TIMESTAMP, // qint64: timestamp of last ping measurement - USER_ROLE_PING_TIMES_QUEUE, // QQueue: for ping stats, will be initialized on first ping USER_ROLE_PING_SALT // int: random ping salt per server }; @@ -112,10 +106,7 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase void RequestServerList(); void EmitCLServerListPingMes ( const CHostAddress& haServerAddress, const bool bNeedVersion ); void UpdateDirectoryComboBox(); -#ifdef PING_STEALTH_MODE_DETAILED_STATS - void pingStealthModeDebugStats(); -#endif - +# CClientSettings* pSettings; QTimer TimerPing;