/*
* discoverylistener.cpp
*
* Created on: 25.01.2013
* Author: sr
*/
#include "discoverylistener.h"
#include "certmanager.h"
#include "../serverapp/serverapp.h"
#include "../../shared/settings.h"
#include "../../shared/network.h"
#include "../../shared/util.h"
#include <QCryptographicHash>
#include <QSslCertificate>
#include <QSslKey>
#define UDPBUFSIZ 9000
#define SPAM_CUTOFF 50
#define SPAM_MODERATE_INTERVAL 6787
#define SPAM_MODERATE_AT_ONCE 100
// static objects
// +++ static ++++ hash ip address +++
/**
* @brief DiscoveryListener::DiscoveryListener
*/
DiscoveryListener::DiscoveryListener() :
_socket(this), _counterResetPos(0)
{
if (!_socket.bind(QHostAddress::AnyIPv4, SERVICE_DISCOVERY_PORT))
qFatal("Could not bind to service discovery port %d", int(SERVICE_DISCOVERY_PORT));
connect(&_socket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
for (int i = 0; i < SD_PACKET_TABLE_SIZE; ++i)
_packetCounter[i] = 0;
startTimer((SPAM_MODERATE_AT_ONCE * SPAM_MODERATE_INTERVAL) / SD_PACKET_TABLE_SIZE + 1);
}
/**
* @brief DiscoveryListener::~DiscoveryListener
*/
DiscoveryListener::~DiscoveryListener()
{
}
/**
* @brief hash
* @param host
* @return
*/
static quint16 hash(const QHostAddress& host)
{
static quint16 seed1 = 0, seed2 = 0;
while (seed1 == 0) { // Make sure the algorithm uses different seeds each time the program is
// run to prevent hash collision attacks
seed1 = quint16(qrand() & 0xffff);
seed2 = quint16(qrand() & 0xffff);
}
quint8 data[16], len;
if (host.protocol() == QAbstractSocket::IPv4Protocol) {
// IPv4
quint32 addr = host.toIPv4Address();
len = 4;
memcpy(data, &addr, len);
} else if (host.protocol() == QAbstractSocket::IPv6Protocol) {
// IPv6
len = 16;
// Fast version (might break with future qt versions)
memcpy(data, host.toIPv6Address().c, len);
/* // --- Safe version (but better try to figure out a new fast way if above stops working ;))
Q_IPV6ADDR addr = host.toIPv6Address();
for (int i = 0; i < len; ++i)
data[i] = addr[i];
*/
} else {
// Durr?
len = 2;
data[0] = quint8(qrand());
data[1] = quint8(qrand());
}
quint16 result = 0;
quint16 mod = seed1;
for (quint8 i = 0; i < len; ++i) {
result = quint16(((result << 1) + data[i]) ^ mod); // because of the shift this algo is not suitable for len(input) > 8
mod = quint16(mod + seed2 + data[i]);
}
return result;
}
/*
* Overrides
*/
/**
* @brief Decrease packet counters per source IP in our "spam protection" table.
* @param event
*/
void DiscoveryListener::timerEvent(QTimerEvent* /* event */ )
{
for (int i = 0; i < SPAM_MODERATE_AT_ONCE; ++i) {
if (++_counterResetPos >= SD_PACKET_TABLE_SIZE)
_counterResetPos = 0;
if (_packetCounter[_counterResetPos] > 10) {
_packetCounter[_counterResetPos] = quint8(_packetCounter[_counterResetPos] - 10);
} else if (_packetCounter[_counterResetPos] != 0) {
_packetCounter[_counterResetPos] = 0;
}
}
}
/*
* Slots
*/
/**
* @brief Incoming UDP packet on service discovery port - handle.
*/
void DiscoveryListener::onReadyRead()
{
static int certFails = 0;
char data[UDPBUFSIZ];
QHostAddress addr;
quint16 port;
while (_socket.hasPendingDatagrams()) {
const qint64 size = _socket.readDatagram(data, UDPBUFSIZ, &addr, &port);
if (size <= 0)
continue;
const quint16 bucket = hash(addr) % SD_PACKET_TABLE_SIZE;
if (_packetCounter[bucket] > SPAM_CUTOFF) {
qDebug() << "SD: Potential (D)DoS from " << _socket.peerAddress().toString();
// emit some signal and pop up a big warning that someone is flooding/ddosing the PVS SD
// ... on the other hand, will the user understand? ;)
continue;
}
++_packetCounter[bucket];
_packet.reset();
if (_packet.readMessage(data, quint32(size)) != NM_READ_OK)
continue;
// Valid packet, process it:
const QByteArray iplist(_packet.getFieldBytes(_IPLIST));
const QByteArray hash(_packet.getFieldBytes(_HASH));
const QByteArray salt1(_packet.getFieldBytes(_SALT1));
const QByteArray salt2(_packet.getFieldBytes(_SALT2));
// For security, the salt has to be at least 16 bytes long
if (salt1.size() < 16 || salt2.size() < 16)
continue; // To make this more secure, you could remember the last X salts used, and ignore new packets using the same
// Check if the source IP of the packet matches any of the addresses given in the IP list
if (!Network::isAddressInList(QString::fromUtf8(iplist), addr.toString()))
continue;
// If so, check if the submitted hash seems valid
if (genSha1(&serverApp->sessionNameArray(), &salt1, &iplist) != hash &&
!(serverApp->getCurrentRoom()->clientPositions.contains(addr.toString()))) {
// did not match local session name and client is not in same room.
continue;
}
qDebug("Got matching discovery request...");
QByteArray myiplist(Network::interfaceAddressesToString().toUtf8());
QSslKey key;
QSslCertificate cert;
if (!CertManager::getPrivateKeyAndCert("manager", key, cert)) {
if (++certFails > 5) {
CertManager::fatal();
}
continue;
}
QByteArray certhash(cert.digest(QCryptographicHash::Sha1));
// Reply to client
_packet.reset();
_packet.setField(_HASH, genSha1(&serverApp->sessionNameArray(), &salt2, &myiplist, &CLIENT_PORT_ARRAY, &certhash));
_packet.setField(_IPLIST, myiplist);
_packet.setField(_PORT, CLIENT_PORT_ARRAY);
_packet.setField(_CERT, certhash);
_packet.writeMessage(&_socket, addr, port);
}
}