QSslServer: Implement handshake timeouts

If a client doesn't send any data then we would leave the socket open
for as long as it needed, wasting resources. Add timeouts to limit the
amount of time this can happen for.

Since there is a limit on number of sockets that the server will have
queued, having idle sockets stick around forever is a vector for ddos.

Pick-to: 6.4
Change-Id: Ida6251c92c625eeadf2065861b840b14255654b8
Reviewed-by: Ievgenii Meshcheriakov <ievgenii.meshcheriakov@qt.io>
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Mårten Nordheim 2022-08-16 15:25:10 +02:00
parent 1b68e0b717
commit 29a1fe72a0
4 changed files with 95 additions and 3 deletions

View File

@ -227,6 +227,42 @@ QSslConfiguration QSslServer::sslConfiguration() const
return d->sslConfiguration;
}
/*!
Sets the \a timeout to use for all incoming handshakes, in milliseconds.
This is relevant in the scenario where a client, whether malicious or
accidental, connects to the server but makes no attempt at communicating or
initiating a handshake. QSslServer will then automatically end the
connection after \a timeout milliseconds have elapsed.
By default the timeout is 5000 milliseconds (5 seconds).
\note The underlying TLS framework may have their own timeout logic now or
in the future, this function does not affect that.
\note The \a timeout passed to this function will only apply to \e{new}
connections. If a client is already connected it will use the timeout which
was set when it connected.
\sa handshakeTimeout()
*/
void QSslServer::setHandshakeTimeout(int timeout)
{
Q_D(QSslServer);
d->handshakeTimeout = timeout;
}
/*!
Returns the currently configured handshake timeout.
\sa setHandshakeTimeout()
*/
int QSslServer::handshakeTimeout() const
{
const Q_D(QSslServer);
return d->handshakeTimeout;
}
/*!
Called when a new connection is established.
@ -296,7 +332,11 @@ void QSslServerPrivate::initializeHandshakeProcess(QSslSocket *socket)
// QObject dtor, but we only use the pointer value!
removeSocketData(quintptr(obj));
});
socketData.emplace(quintptr(socket), readyRead, destroyed);
auto it = socketData.emplace(quintptr(socket), readyRead, destroyed, std::make_shared<QTimer>());
it->timeoutTimer->setSingleShot(true);
it->timeoutTimer->callOnTimeout([this, socket]() { handleHandshakeTimedOut(socket); });
it->timeoutTimer->setInterval(handshakeTimeout);
it->timeoutTimer->start();
}
// This function may be called while in the socket's QObject dtor, __never__ use
@ -335,8 +375,21 @@ void QSslServerPrivate::checkClientHelloAndContinue()
return;
}
// Be nice and restart the timeout timer since some progress was made
if (foundData)
it->timeoutTimer->start();
socket->startServerEncryption();
Q_EMIT q->startedEncryptionHandshake(socket);
}
void QSslServerPrivate::handleHandshakeTimedOut(QSslSocket *socket)
{
Q_Q(QSslServer);
removeSocketData(quintptr(socket));
socket->disconnectFromHost();
Q_EMIT q->errorOccurred(socket, QAbstractSocket::SocketTimeoutError);
socket->deleteLater();
}
QT_END_NAMESPACE

View File

@ -33,6 +33,9 @@ public:
void setSslConfiguration(const QSslConfiguration &sslConfiguration);
QSslConfiguration sslConfiguration() const;
void setHandshakeTimeout(int timeout);
int handshakeTimeout() const;
Q_SIGNALS:
void sslErrors(QSslSocket *socket, const QList<QSslError> &errors);
void peerVerifyError(QSslSocket *socket, const QSslError &error);

View File

@ -19,6 +19,7 @@
#include <QtNetwork/private/qtnetworkglobal_p.h>
#include <QtCore/qhash.h>
#include <QtCore/qtimer.h>
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/private/qtcpserver_p.h>
@ -28,6 +29,7 @@ QT_BEGIN_NAMESPACE
class Q_NETWORK_EXPORT QSslServerPrivate : public QTcpServerPrivate
{
static constexpr int DefaultHandshakeTimeout = 5'000; // 5 seconds
public:
Q_DECLARE_PUBLIC(QSslServer)
@ -35,13 +37,18 @@ public:
void checkClientHelloAndContinue();
void initializeHandshakeProcess(QSslSocket *socket);
void removeSocketData(quintptr socket);
void handleHandshakeTimedOut(QSslSocket *socket);
struct SocketData {
QMetaObject::Connection readyReadConnection;
QMetaObject::Connection destroyedConnection;
std::shared_ptr<QTimer> timeoutTimer; // shared_ptr because QHash demands copying
SocketData(QMetaObject::Connection readyRead, QMetaObject::Connection destroyed)
: readyReadConnection(readyRead), destroyedConnection(destroyed)
SocketData(QMetaObject::Connection readyRead, QMetaObject::Connection destroyed,
std::shared_ptr<QTimer> &&timer)
: readyReadConnection(readyRead),
destroyedConnection(destroyed),
timeoutTimer(std::move(timer))
{
}
@ -54,6 +61,7 @@ public:
QHash<quintptr, SocketData> socketData;
QSslConfiguration sslConfiguration;
int handshakeTimeout = DefaultHandshakeTimeout;
};

View File

@ -24,6 +24,7 @@ private slots:
void testPreSharedKeyAuthenticationRequired();
#endif
void plaintextClient();
void quietClient();
private:
QString testDataDir;
@ -459,6 +460,33 @@ void tst_QSslServer::plaintextClient()
QCOMPARE(socket.state(), QAbstractSocket::SocketState::UnconnectedState);
}
void tst_QSslServer::quietClient()
{
QSslConfiguration serverConfiguration = selfSignedServerQSslConfiguration();
SslServerSpy server(serverConfiguration);
server.server.setHandshakeTimeout(1'000);
QVERIFY(server.server.listen());
quint16 serverPeerPort = 0;
auto grabServerPeerPort = [&serverPeerPort](QSslSocket *socket) {
serverPeerPort = socket->peerPort();
};
QObject::connect(&server.server, &QSslServer::errorOccurred, &server.server,
grabServerPeerPort);
QTcpSocket socket;
QSignalSpy socketDisconnectedSpy(&socket, &QTcpSocket::disconnected);
socket.connectToHost(QHostAddress::LocalHost, server.server.serverPort());
quint16 clientLocalPort = socket.localPort();
QVERIFY(socket.waitForConnected());
// Disconnects after overlong break:
QVERIFY(socketDisconnectedSpy.wait(5'000));
QCOMPARE(socket.state(), QAbstractSocket::SocketState::UnconnectedState);
QCOMPARE_GT(server.errorOccurredSpy.size(), 0);
QCOMPARE(serverPeerPort, clientLocalPort);
}
QTEST_MAIN(tst_QSslServer)
#include "tst_qsslserver.moc"