summaryrefslogtreecommitdiffstats
path: root/src/server/net/discoverylistener.cpp
blob: cfeef82d8d2aadcc70ccae9439efdb6f05f03c94 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/*
 * 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);
	}
}