diff --git a/Jamulus.pro b/Jamulus.pro index afa1bde3f4..3fcf52020d 100644 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -493,6 +493,9 @@ HEADERS_OPUS_X86 = libs/opus/celt/x86/celt_lpc_sse.h \ libs/opus/celt/x86/x86cpu.h \ $$files(libs/opus/silk/x86/*.h) +HEADERS += src/centraldefense.h +SOURCES += src/centraldefense.cpp + SOURCES += src/plugins/audioreverb.cpp \ src/buffer.cpp \ src/channel.cpp \ diff --git a/src/centraldefense.cpp b/src/centraldefense.cpp new file mode 100644 index 0000000000..fcc1e36331 --- /dev/null +++ b/src/centraldefense.cpp @@ -0,0 +1,433 @@ +#include "centraldefense.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCentralDefense, "jamulus.centraldefense") + +CentralDefense::CentralDefense(const QUrl& blockListUrl, + const QUrl& asnLookupBase, + int refreshIntervalSeconds, + QObject* parent) + : QObject(parent), + m_blockListUrl(blockListUrl), + m_asnLookupBase(asnLookupBase), + m_refreshIntervalSeconds(refreshIntervalSeconds) +{ + m_nam = new QNetworkAccessManager(this); + m_timer = new QTimer(this); + m_timer->setInterval(qMax(1, m_refreshIntervalSeconds) * 1000); + connect(m_timer, &QTimer::timeout, this, &CentralDefense::onTimerTick); +} + +CentralDefense::~CentralDefense() +{ + stop(); + if (m_inflightBlockList) { m_inflightBlockList->abort(); m_inflightBlockList->deleteLater(); m_inflightBlockList = nullptr; } + if (m_inflightReply) { m_inflightReply->abort(); m_inflightReply->deleteLater(); m_inflightReply = nullptr; } + if (m_inflightTimeoutTimer) { m_inflightTimeoutTimer->stop(); m_inflightTimeoutTimer->deleteLater(); m_inflightTimeoutTimer = nullptr; } +} + +void CentralDefense::start() +{ + qCInfo(lcCentralDefense) << "starting: blocklist" << m_blockListUrl.toString() + << "lookup_base" << m_asnLookupBase.toString() + << "refresh_s" << m_refreshIntervalSeconds + << "queue_cap" << m_maxPending; + fetchBlockList(); + m_timer->start(); +} + +void CentralDefense::stop() +{ + qCInfo(lcCentralDefense) << "stopping"; + m_timer->stop(); +} + +void CentralDefense::onTimerTick() +{ + purgeExpired(); + fetchBlockList(); +} + +void CentralDefense::fetchBlockList() +{ + if (!m_blockListUrl.isValid()) { + qCWarning(lcCentralDefense) << "invalid block list URL"; + return; + } + if (m_inflightBlockList) return; + + // qCInfo(lcCentralDefense) << "blocklist: fetch start -" << m_blockListUrl.toString(); + QNetworkRequest req(m_blockListUrl); + req.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("Jamulus-CentralDefense/1.0")); + m_inflightBlockList = m_nam->get(req); + connect(m_inflightBlockList, &QNetworkReply::finished, this, &CentralDefense::onBlockListFetched); +} + +void CentralDefense::onBlockListFetched() +{ + QNetworkReply* reply = m_inflightBlockList; + m_inflightBlockList = nullptr; + if (!reply) return; + + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcCentralDefense) << "blocklist: fetch error -" << reply->errorString(); + reply->deleteLater(); + return; + } + + QByteArray data = reply->readAll(); + reply->deleteLater(); + + parseBlockList(data); + m_lastSuccessfulFetch = QDateTime::currentDateTimeUtc(); +} + +void CentralDefense::parseBlockList(const QByteArray& data) +{ + QVector newCidrs; + QSet newAsns; + + QString text = QString::fromUtf8(data); + const QStringList lines = text.split(QRegExp("[\r\n]"), Qt::SkipEmptyParts); + int lineNo = 0; + for (QString rawLine : lines) { + ++lineNo; + QString line = rawLine.trimmed(); + if (line.isEmpty()) continue; + if (line.startsWith('#')) continue; + + // FIX: Split by whitespace and take the first token only. + // This strips comments or names like " Datacamp Limited" from "AS212238 Datacamp Limited" + const QString token = line.split(QRegExp("\\s+"), Qt::SkipEmptyParts).first(); + + Ipv4Cidr cidr; + // Use 'token' instead of 'line' for parsing + if (tryParseIpv4CidrLine(token, cidr)) { + cidr.temporary = false; + newCidrs.append(cidr); + continue; + } + + // Use 'token' instead of 'line' for ASN normalization + QString asn = normalizeAsnString(token); + if (!asn.isEmpty()) newAsns.insert(asn); + else qCDebug(lcCentralDefense) << "blocklist: parse ignored line" << lineNo << ":" << line; + } + + { + QWriteLocker locker(&m_lock); + m_blockedCidrs = std::move(newCidrs); + m_blockedAsns = std::move(newAsns); + } + + // qCInfo(lcCentralDefense) << "blocklist: fetch ok - ASNs:" << m_blockedAsns.size() << "CIDRs:" << m_blockedCidrs.size(); + emit updated(m_blockedAsns.size(), m_blockedCidrs.size()); +} + +bool CentralDefense::tryParseIpv4CidrLine(const QString& line, Ipv4Cidr& outCidr) +{ + if (!line.contains('/')) return false; + QStringList parts = line.split('/', Qt::SkipEmptyParts); + if (parts.size() != 2) return false; + QString ipPart = parts.at(0).trimmed(); + QString prefixPart = parts.at(1).trimmed(); + + quint32 ip = ipv4FromString(ipPart); + if (ip == 0 && ipPart != "0.0.0.0") return false; + + bool ok = false; + int prefix = prefixPart.toInt(&ok); + if (!ok || prefix < 0 || prefix > 32) return false; + + quint32 mask = cidrMaskBits(prefix); + quint32 network = (ip & mask); + + outCidr.network = network; + outCidr.prefixLen = prefix; + outCidr.temporary = false; + return true; +} + +QString CentralDefense::normalizeAsnString(const QString& s) const +{ + QString t = s.trimmed().toUpper(); + if (t.isEmpty()) return QString(); + if (t.startsWith("AS")) { + bool ok = false; t.mid(2).toUInt(&ok); + if (!ok) return QString(); + return t; + } else { + bool ok = false; t.toUInt(&ok); + if (!ok) return QString(); + return QStringLiteral("AS%1").arg(t); + } +} + +quint32 CentralDefense::ipv4FromString(const QString& s) const +{ + QHostAddress addr(s); + if (addr.protocol() != QAbstractSocket::IPv4Protocol) return 0; + return addr.toIPv4Address(); +} + +quint32 CentralDefense::cidrMaskBits(int prefixLen) const +{ + if (prefixLen <= 0) return 0u; + if (prefixLen >= 32) return 0xFFFFFFFFu; + return (0xFFFFFFFFu << (32 - prefixLen)); +} + +bool CentralDefense::ipv4InCidr(quint32 ip, const Ipv4Cidr& cidr) const +{ + quint32 mask = cidrMaskBits(cidr.prefixLen); + return (ip & mask) == cidr.network; +} + +bool CentralDefense::isBlockedCached(const QHostAddress& addr) const +{ + QString ipStr = addr.toString(); + + QReadLocker locker(&m_lock); + + auto itCache = m_ipAsnCache.constFind(ipStr); + if (itCache != m_ipAsnCache.constEnd()) { + if (itCache->expiry > QDateTime::currentDateTimeUtc()) { + if (m_blockedAsns.contains(itCache->asn)) return true; + } + } + + if (addr.protocol() == QAbstractSocket::IPv4Protocol) { + quint32 ip = addr.toIPv4Address(); + for (const Ipv4Cidr& c : m_blockedCidrs) { + if (ipv4InCidr(ip, c)) return true; + } + } + + return false; +} + +void CentralDefense::checkAndLookup(const QHostAddress& addr) +{ + if (isBlockedCached(addr)) { + // FIX: Verify we shout "BLOCKED" so the server can kick the user + emit addressBlocked(addr, QStringLiteral("cached/cidr block")); + + emit addressChecked(addr, true, QStringLiteral("cached")); + return; + } + + if (addr.protocol() != QAbstractSocket::IPv4Protocol) { + emit addressChecked(addr, false, QStringLiteral("ipv6-unsupported")); + return; + } + + QString ipStr = addr.toString(); + + { + QMutexLocker l(&m_pendingMutex); + + if (m_inflightIp == ipStr) { + qCInfo(lcCentralDefense) << "lookup: coalesced (inflight)" << ipStr; + return; + } + if (m_pendingSet.contains(ipStr)) { + qCInfo(lcCentralDefense) << "lookup: coalesced (queued)" << ipStr; + return; + } + + if (m_pendingQueue.size() >= m_maxPending) { + qCWarning(lcCentralDefense) << "lookup: dropped (queue full)" << ipStr << "cap=" << m_maxPending; + emit addressChecked(addr, false, QStringLiteral("queue-full")); + return; + } + + m_pendingQueue.enqueue(ipStr); + m_pendingSet.insert(ipStr); + qCInfo(lcCentralDefense) << "lookup: enqueue" << ipStr << "queue_len=" << m_pendingQueue.size(); + } + + if (!m_inflightReply) startNextLookup(); +} + +void CentralDefense::startNextLookup() +{ + if (m_inflightReply) return; + + QString nextIp; + { + QMutexLocker l(&m_pendingMutex); + if (m_pendingQueue.isEmpty()) return; + nextIp = m_pendingQueue.dequeue(); + m_pendingSet.remove(nextIp); + } + + if (nextIp.isEmpty()) return; + + QUrl url = m_asnLookupBase; + QString path = url.path(); + if (!path.endsWith('/')) path += '/'; + path += nextIp; + url.setPath(path); + + qCInfo(lcCentralDefense) << "lookup: start" << nextIp << "url=" << url.toString() << "pending=" << m_pendingQueue.size(); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("Jamulus-CentralDefense/1.0")); + + QNetworkReply* reply = m_nam->get(req); + m_inflightReply = reply; + m_inflightIp = nextIp; + + connect(reply, &QNetworkReply::finished, this, &CentralDefense::onAsnLookupFinished); + + if (m_inflightTimeoutTimer) { m_inflightTimeoutTimer->stop(); m_inflightTimeoutTimer->deleteLater(); m_inflightTimeoutTimer = nullptr; } + m_inflightTimeoutTimer = new QTimer(this); + m_inflightTimeoutTimer->setSingleShot(true); + connect(m_inflightTimeoutTimer, &QTimer::timeout, this, [reply]() { + if (reply && reply->isRunning()) reply->abort(); + }); + m_inflightTimeoutTimer->start(m_lookupTimeoutSeconds * 1000); +} + +void CentralDefense::onAsnLookupFinished() +{ + QNetworkReply* reply = qobject_cast(sender()); + if (!reply) return; + + if (m_inflightTimeoutTimer) { + m_inflightTimeoutTimer->stop(); + m_inflightTimeoutTimer->deleteLater(); + m_inflightTimeoutTimer = nullptr; + } + + QString ip = m_inflightIp; + m_inflightReply = nullptr; + m_inflightIp.clear(); + + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcCentralDefense) << "lookup: error" << ip << reply->errorString(); + QHostAddress addr(ip); + emit addressChecked(addr, false, QStringLiteral("lookup-failed")); + reply->deleteLater(); + QTimer::singleShot(m_lookupStartSpacingMs, this, &CentralDefense::startNextLookup); + return; + } + + QByteArray body = reply->readAll(); + reply->deleteLater(); + + QHostAddress addr(ip); + QJsonParseError perr; + QJsonDocument doc = QJsonDocument::fromJson(body, &perr); + if (perr.error != QJsonParseError::NoError || !doc.isObject()) { + qCWarning(lcCentralDefense) << "lookup: invalid JSON" << ip; + emit addressChecked(addr, false, QStringLiteral("invalid-lookup-json")); + QTimer::singleShot(m_lookupStartSpacingMs, this, &CentralDefense::startNextLookup); + return; + } + + QJsonObject obj = doc.object(); + QString asField = obj.value(QStringLiteral("as")).toString(); + QString asnToken; + if (!asField.isEmpty()) { + asnToken = asField.split(' ', Qt::SkipEmptyParts).first().toUpper(); + } + + QString normalizedAsn; + if (!asnToken.isEmpty() && asnToken.startsWith("AS")) normalizedAsn = normalizeAsnString(asnToken); + + QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime expiry = now.addSecs(m_temporaryBlockSeconds); + + if (!normalizedAsn.isEmpty()) { + QWriteLocker locker(&m_lock); + AsnCacheEntry e{ normalizedAsn, expiry }; + m_ipAsnCache.insert(ip, e); + } + + bool isBlocked = false; + QString reason; + + if (!normalizedAsn.isEmpty()) { + QReadLocker locker(&m_lock); + if (m_blockedAsns.contains(normalizedAsn)) { + isBlocked = true; + reason = QStringLiteral("AS Block: %1").arg(normalizedAsn); + } + } + + if (isBlocked && addr.protocol() == QAbstractSocket::IPv4Protocol) { + quint32 ipv4 = addr.toIPv4Address(); + quint32 mask = cidrMaskBits(24); + quint32 net = ipv4 & mask; + Ipv4Cidr cidr; + cidr.network = net; + cidr.prefixLen = 24; + cidr.temporary = true; + + QString key = QString("%1/%2").arg(QHostAddress(cidr.network).toString()).arg(cidr.prefixLen); + + { + QWriteLocker locker(&m_lock); + m_blockedCidrs.append(cidr); + m_tempCidrExpiry.insert(key, expiry); + } + + qCInfo(lcCentralDefense) << "temp-block: add" << QHostAddress(cidr.network).toString() << "/" << cidr.prefixLen + << "trigger_ip=" << addr.toString() << "asn=" << normalizedAsn + << "expiry=" << expiry.toString(Qt::ISODate); + + emit addressBlocked(addr, reason); + emit addressChecked(addr, true, reason); + } else { + qCInfo(lcCentralDefense) << "lookup: finish" << ip << "asn=" << normalizedAsn << "blocked=" << isBlocked; + emit addressChecked(addr, false, QStringLiteral("asn-lookup-ok")); + } + + QTimer::singleShot(m_lookupStartSpacingMs, this, &CentralDefense::startNextLookup); +} + +void CentralDefense::purgeExpired() +{ + QDateTime now = QDateTime::currentDateTimeUtc(); + + QWriteLocker locker(&m_lock); + + QList ipKeys = m_ipAsnCache.keys(); + int purgedIpCache = 0; + for (const QString& k : ipKeys) { + const AsnCacheEntry& e = m_ipAsnCache.value(k); + if (e.expiry <= now) { m_ipAsnCache.remove(k); ++purgedIpCache; } + } + + QVector kept; + int purgedCidrs = 0; + for (const Ipv4Cidr& c : m_blockedCidrs) { + if (!c.temporary) { kept.append(c); continue; } + QString key = QString("%1/%2").arg(QHostAddress(c.network).toString()).arg(c.prefixLen); + if (m_tempCidrExpiry.contains(key) && m_tempCidrExpiry.value(key) > now) { + kept.append(c); + } else { + m_tempCidrExpiry.remove(key); + ++purgedCidrs; + } + } + m_blockedCidrs = std::move(kept); + + // qCDebug(lcCentralDefense) << "purge: expired ip_cache=" << purgedIpCache << "temp_cidrs=" << purgedCidrs + // << "remaining_asns=" << m_blockedAsns.size() << "remaining_cidrs=" << m_blockedCidrs.size(); +} + +QString Ipv4Cidr::toString() const +{ + QHostAddress a(network); + return QString("%1/%2").arg(a.toString()).arg(prefixLen); +} diff --git a/src/centraldefense.h b/src/centraldefense.h new file mode 100644 index 0000000000..f4bb0fd08a --- /dev/null +++ b/src/centraldefense.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Ipv4Cidr +{ + quint32 network; + int prefixLen; + bool temporary; + QString toString() const; +}; + +Q_DECLARE_TYPEINFO(Ipv4Cidr, Q_MOVABLE_TYPE); + +class CentralDefense : public QObject +{ + Q_OBJECT + +public: + explicit CentralDefense(const QUrl& blockListUrl, + const QUrl& asnLookupBase = QUrl("http://ip-api.com/json/"), + int refreshIntervalSeconds = 60, + QObject* parent = nullptr); + ~CentralDefense() override; + + void start(); + void stop(); + + bool isBlockedCached(const QHostAddress& addr) const; + void checkAndLookup(const QHostAddress& addr); + void setTemporaryBlockSeconds(int seconds) { m_temporaryBlockSeconds = seconds; } + +signals: + void addressChecked(const QHostAddress& addr, bool isBlocked, const QString& reason); + void addressBlocked(const QHostAddress& addr, const QString& reason); + void updated(int numAsns, int numCidrs); + +private slots: + void onBlockListFetched(); + void onAsnLookupFinished(); + void onTimerTick(); + +private: + void fetchBlockList(); + void parseBlockList(const QByteArray& data); + bool tryParseIpv4CidrLine(const QString& line, Ipv4Cidr& outCidr); + QString normalizeAsnString(const QString& s) const; + quint32 ipv4FromString(const QString& s) const; + quint32 cidrMaskBits(int prefixLen) const; + bool ipv4InCidr(quint32 ip, const Ipv4Cidr& cidr) const; + void purgeExpired(); + void startNextLookup(); + +private: + QUrl m_blockListUrl; + QUrl m_asnLookupBase; + int m_refreshIntervalSeconds; + + QNetworkAccessManager* m_nam; + QTimer* m_timer; + + mutable QReadWriteLock m_lock; + QSet m_blockedAsns; + QVector m_blockedCidrs; + + struct AsnCacheEntry { QString asn; QDateTime expiry; }; + QHash m_ipAsnCache; + QHash m_tempCidrExpiry; + + QQueue m_pendingQueue; + QSet m_pendingSet; + QMutex m_pendingMutex; + int m_maxPending = 25; + + QNetworkReply* m_inflightReply = nullptr; + QString m_inflightIp; + QTimer* m_inflightTimeoutTimer = nullptr; + int m_lookupStartSpacingMs = 1000; + int m_lookupTimeoutSeconds = 10; + int m_temporaryBlockSeconds = 24 * 3600; + + QNetworkReply* m_inflightBlockList = nullptr; + QDateTime m_lastSuccessfulFetch; +}; diff --git a/src/server.cpp b/src/server.cpp index 0ddb5644d5..10d094f931 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -23,6 +23,7 @@ \******************************************************************************/ #include "server.h" +#include "centraldefense.h" // CServer implementation ****************************************************** CServer::CServer ( const int iNewMaxNumChan, @@ -301,6 +302,34 @@ CServer::CServer ( const int iNewMaxNumChan, connectChannelSignalsToServerSlots(); + + // --- Central Defense Integration --- + m_centralDefense = new CentralDefense(QUrl("https://jamulus.live/asn-ip-client-blocks.txt"), + QUrl("http://ip-api.com/json/"), + 60, + this); + + connect(m_centralDefense, &CentralDefense::updated, this, [](int a, int b){ + // qInfo() << "Central Defense updated. ASNs:" << a << "CIDRs:" << b; + }); + + // Banhammer: Disconnect active audio clients if they are blocked + connect(m_centralDefense, &CentralDefense::addressBlocked, this, [this](const QHostAddress& addr, const QString& reason) { + qInfo() << "Central Defense: Blocking active client from" << addr.toString() << "Reason:" << reason; + + for (int i = 0; i < iMaxNumChannels; i++) { + if (vecChannels[i].IsConnected()) { + if (vecChannels[i].GetAddress().InetAddr == addr) { + qInfo() << "Disconnecting Channel" << i << "due to block."; + vecChannels[i].Disconnect(); + } + } + } + }); + + m_centralDefense->start(); + // ----------------------------------- + // start the socket (it is important to start the socket after all // initializations and connections) Socket.Start(); @@ -379,6 +408,31 @@ void CServer::OnNewConnection ( int iChID, int iTotChans, CHostAddress RecHostAd { QMutexLocker locker ( &Mutex ); + // --- Central Defense: Trap the Bouncer's Whistle (FORCE SYNC) --- + if ( m_centralDefense ) + { + bool bIsBlocked = false; + + // FIX: Added 'this' as the 3rd argument and moved Qt::DirectConnection to the 5th. + // This satisfies the specific Qt 5 overload for lambdas with connection types. + QMetaObject::Connection conn = connect( m_centralDefense, &CentralDefense::addressBlocked, this, + [&bIsBlocked]( const QHostAddress&, const QString& ) { + bIsBlocked = true; + }, Qt::DirectConnection ); + + // Run the check + m_centralDefense->checkAndLookup( RecHostAddr.InetAddr ); + + disconnect( conn ); + + if ( bIsBlocked ) + { + // Abort immediately. We haven't sent the "Welcome" message yet, + // so the client will just time out. + return; + } + } + // inform the client about its own ID at the server (note that this // must be the first message to be sent for a new connection) vecChannels[iChID].CreateClientIDMes ( iChID ); diff --git a/src/server.h b/src/server.h index 6ea36ac9cb..3498c2c088 100644 --- a/src/server.h +++ b/src/server.h @@ -51,6 +51,8 @@ #define INVALID_CHANNEL_ID ( MAX_NUM_CHANNELS + 1 ) /* Classes ********************************************************************/ +class CentralDefense; + template class CServerSlots : public CServerSlots { @@ -169,6 +171,9 @@ class CServer : public QObject, public CServerSlots void SetEnableDelayPanning ( bool bDelayPanningOn ) { bDelayPan = bDelayPanningOn; } bool IsDelayPanningEnabled() { return bDelayPan; } + CServerLogging* GetLogging() { return &Logging; } + + protected: // access functions for actual channels bool IsConnected ( const int iChanNum ) { return vecChannels[iChanNum].IsConnected(); } @@ -308,6 +313,8 @@ class CServer : public QObject, public CServerSlots std::unique_ptr pThreadPool; + CentralDefense* m_centralDefense = nullptr; + signals: void Started(); void Stopped(); diff --git a/src/serverlogging.cpp b/src/serverlogging.cpp index 9b57502103..335ab49479 100644 --- a/src/serverlogging.cpp +++ b/src/serverlogging.cpp @@ -55,6 +55,17 @@ void CServerLogging::AddNewConnection ( const QHostAddress& ClientInetAddr, cons *this << strLogStr; // in log file } + +void CServerLogging::AddEarlyConnection(const QHostAddress& ClientInetAddr, int iNumberOfConnectedClients) +{ + const QString strLogStr = + CurTimeDatetoLogString() + ", " + ClientInetAddr.toString() + ", new connection detected (" + QString::number(iNumberOfConnectedClients) + ")"; + qInfo() << qUtf8Printable(strLogStr); // Console + *this << strLogStr; // Log file (uses operator<<, which is protected but accessible here) +} + + + void CServerLogging::AddServerStopped() { const QString strLogStr = CurTimeDatetoLogString() + ",, server idling " diff --git a/src/serverlogging.h b/src/serverlogging.h index e0ea7b8768..f40393fb18 100644 --- a/src/serverlogging.h +++ b/src/serverlogging.h @@ -44,6 +44,8 @@ class CServerLogging void AddServerStopped(); void AddNewConnection ( const QHostAddress& ClientInetAddr, const int iNumberOfConnectedClients ); + void AddEarlyConnection(const QHostAddress& ClientInetAddr, int iNumberOfConnectedClients); + protected: void operator<< ( const QString& sNewStr ); diff --git a/src/socket.cpp b/src/socket.cpp index 9ebfc21cdd..ff63fd2ae9 100644 --- a/src/socket.cpp +++ b/src/socket.cpp @@ -463,8 +463,12 @@ void CSocket::OnDataReceived() if ( pServer->PutAudioData ( vecbyRecBuf, iNumBytesRead, RecHostAddr, iCurChanID ) ) { - // we have a new connection, emit a signal - emit NewConnection ( iCurChanID, pServer->GetNumberOfConnectedClients(), RecHostAddr ); + // EARLY LOG: Announce new connection as soon as detected + pServer->GetLogging()->AddEarlyConnection(RecHostAddr.InetAddr, pServer->GetNumberOfConnectedClients()); + + QThread::msleep(250); + + emit NewConnection(iCurChanID, pServer->GetNumberOfConnectedClients(), RecHostAddr); // this was an audio packet, start server if it is in sleep mode if ( !pServer->IsRunning() )