DTLS server▲
The DTLS server example is intended to be run alongside the DTLS client example.
The server is implemented by the DtlsServer class. It uses QUdpSocket, QDtlsClientVerifier, and QDtls to test each client's reachability, complete a handshake, and read and write encrypted messages.
class
DtlsServer : public
QObject
{
Q_OBJECT
public
:
DtlsServer();
~
DtlsServer();
bool
listen(const
QHostAddress &
amp;address, quint16 port);
bool
isListening() const
;
void
close();
signals
:
void
errorMessage(const
QString &
amp;message);
void
warningMessage(const
QString &
amp;message);
void
infoMessage(const
QString &
amp;message);
void
datagramReceived(const
QString &
amp;peerInfo, const
QByteArray &
amp;cipherText,
const
QByteArray &
amp;plainText);
private
slots:
void
readyRead();
void
pskRequired(QSslPreSharedKeyAuthenticator *
auth);
private
:
void
handleNewConnection(const
QHostAddress &
amp;peerAddress, quint16 peerPort,
const
QByteArray &
amp;clientHello);
void
doHandshake(QDtls *
newConnection, const
QByteArray &
amp;clientHello);
void
decryptDatagram(QDtls *
connection, const
QByteArray &
amp;clientMessage);
void
shutdown();
bool
listening =
false
;
QUdpSocket serverSocket;
QSslConfiguration serverConfiguration;
QDtlsClientVerifier cookieSender;
std::
vector&
lt;std::
unique_ptr&
lt;QDtls&
gt;&
gt; knownClients;
Q_DISABLE_COPY(DtlsServer)
}
;
The constructor connects the QUdpSocket::readyRead() signal to its readyRead() slot and sets the minimal needed TLS configuration:
DtlsServer::
DtlsServer()
{
connect(&
amp;serverSocket, &
amp;QAbstractSocket::
readyRead, this
, &
amp;DtlsServer::
readyRead);
serverConfiguration =
QSslConfiguration::
defaultDtlsConfiguration();
serverConfiguration.setPreSharedKeyIdentityHint("Qt DTLS example server"
);
serverConfiguration.setPeerVerifyMode(QSslSocket::
VerifyNone);
}
The server is not using a certificate and is relying on Pre-Shared Key (PSK) handshake.
listen() binds QUdpSocket:
bool
DtlsServer::
listen(const
QHostAddress &
amp;address, quint16 port)
{
if
(address !=
serverSocket.localAddress() ||
port !=
serverSocket.localPort()) {
shutdown();
listening =
serverSocket.bind(address, port);
if
(!
listening)
emit errorMessage(serverSocket.errorString());
}
else
{
listening =
true
;
}
return
listening;
}
The readyRead() slot processes incoming datagrams:
...
const
qint64 bytesToRead =
serverSocket.pendingDatagramSize();
if
(bytesToRead &
lt;=
0
) {
emit warningMessage(tr("A spurious read notification"
));
return
;
}
QByteArray dgram(bytesToRead, Qt::
Uninitialized);
QHostAddress peerAddress;
quint16 peerPort =
0
;
const
qint64 bytesRead =
serverSocket.readDatagram(dgram.data(), dgram.size(),
&
amp;peerAddress, &
amp;peerPort);
if
(bytesRead &
lt;=
0
) {
emit warningMessage(tr("Failed to read a datagram: "
) +
serverSocket.errorString());
return
;
}
dgram.resize(bytesRead);
...
After extracting an address and a port number, the server first tests if it's a datagram from an already known peer:
...
if
(peerAddress.isNull() ||
!
peerPort) {
emit warningMessage(tr("Failed to extract peer info (address, port)"
));
return
;
}
const
auto
client =
std::
find_if(knownClients.begin(), knownClients.end(),
[&
amp;](const
std::
unique_ptr&
lt;QDtls&
gt; &
amp;connection){
return
connection-&
gt;peerAddress() ==
peerAddress
&
amp;&
amp; connection-&
gt;peerPort() ==
peerPort;
}
);
...
If it is a new, unknown address and port, the datagram is processed as a potential ClientHello message, sent by a DTLS client:
...
if
(client ==
knownClients.end())
return
handleNewConnection(peerAddress, peerPort, dgram);
...
If it's a known DTLS client, the server either decrypts the datagram:
...
if
((*
client)-&
gt;isConnectionEncrypted()) {
decryptDatagram(client-&
gt;get(), dgram);
if
((*
client)-&
gt;dtlsError() ==
QDtlsError::
RemoteClosedConnectionError)
knownClients.erase(client);
return
;
}
...
or continues a handshake with this peer:
...
doHandshake(client-&
gt;get(), dgram);
...
handleNewConnection() verifies it's a reachable DTLS client, or sends a HelloVerifyRequest:
void
DtlsServer::
handleNewConnection(const
QHostAddress &
amp;peerAddress,
quint16 peerPort, const
QByteArray &
amp;clientHello)
{
if
(!
listening)
return
;
const
QString peerInfo =
peer_info(peerAddress, peerPort);
if
(cookieSender.verifyClient(&
amp;serverSocket, clientHello, peerAddress, peerPort)) {
emit infoMessage(peerInfo +
tr(": verified, starting a handshake"
));
...
If the new client was verified to be a reachable DTLS client, the server creates and configures a new QDtls object, and starts a server-side handshake:
...
std::
unique_ptr&
lt;QDtls&
gt; newConnection{
new
QDtls{
QSslSocket::
SslServerMode}}
;
newConnection-&
gt;setDtlsConfiguration(serverConfiguration);
newConnection-&
gt;setPeer(peerAddress, peerPort);
newConnection-&
gt;connect(newConnection.get(), &
amp;QDtls::
pskRequired,
this
, &
amp;DtlsServer::
pskRequired);
knownClients.push_back(std::
move(newConnection));
doHandshake(knownClients.back().get(), clientHello);
...
doHandshake() progresses through the handshake phase:
void
DtlsServer::
doHandshake(QDtls *
newConnection, const
QByteArray &
amp;clientHello)
{
const
bool
result =
newConnection-&
gt;doHandshake(&
amp;serverSocket, clientHello);
if
(!
result) {
emit errorMessage(newConnection-&
gt;dtlsErrorString());
return
;
}
const
QString peerInfo =
peer_info(newConnection-&
gt;peerAddress(),
newConnection-&
gt;peerPort());
switch
(newConnection-&
gt;handshakeState()) {
case
QDtls::
HandshakeInProgress:
emit infoMessage(peerInfo +
tr(": handshake is in progress ..."
));
break
;
case
QDtls::
HandshakeComplete:
emit infoMessage(tr("Connection with %1 encrypted. %2"
)
.arg(peerInfo, connection_info(newConnection)));
break
;
default
:
Q_UNREACHABLE();
}
}
During the handshake phase, the QDtls::pskRequired() signal is emitted and the pskRequired() slot provides the preshared key:
void
DtlsServer::
pskRequired(QSslPreSharedKeyAuthenticator *
auth)
{
Q_ASSERT(auth);
emit infoMessage(tr("PSK callback, received a client's identity: '%1'"
)
.arg(QString::
fromLatin1(auth-&
gt;identity())));
auth-&
gt;setPreSharedKey(QByteArrayLiteral("
\x1a\x2b\x3c\x4d\x5e\x6f
"
));
}
For the sake of brevity, the definition of pskRequired() is oversimplified. The documentation for the QSslPreSharedKeyAuthenticator class explains in detail how this slot can be properly implemented.
After the handshake is completed for the network peer, an encrypted DTLS connection is considered to be established and the server decrypts subsequent datagrams, sent by the peer, by calling decryptDatagram(). The server also sends an encrypted response to the peer:
void
DtlsServer::
decryptDatagram(QDtls *
connection, const
QByteArray &
amp;clientMessage)
{
Q_ASSERT(connection-&
gt;isConnectionEncrypted());
const
QString peerInfo =
peer_info(connection-&
gt;peerAddress(), connection-&
gt;peerPort());
const
QByteArray dgram =
connection-&
gt;decryptDatagram(&
amp;serverSocket, clientMessage);
if
(dgram.size()) {
emit datagramReceived(peerInfo, clientMessage, dgram);
connection-&
gt;writeDatagramEncrypted(&
amp;serverSocket, tr("to %1: ACK"
).arg(peerInfo).toLatin1());
}
else
if
(connection-&
gt;dtlsError() ==
QDtlsError::
NoError) {
emit warningMessage(peerInfo +
": "
+
tr("0 byte dgram, could be a re-connect attempt?"
));
}
else
{
emit errorMessage(peerInfo +
": "
+
connection-&
gt;dtlsErrorString());
}
}
The server closes its DTLS connections by calling QDtls::shutdown():
void
DtlsServer::
shutdown()
{
for
(const
auto
&
amp;connection : qExchange(knownClients, {}
))
connection-&
gt;shutdown(&
amp;serverSocket);
serverSocket.close();
}
During its operation, the server reports errors, informational messages, and decrypted datagrams, by emitting signals errorMessage(), warningMessage(), infoMessage(), and datagramReceived(). These messages are logged by the server's UI:
const
QString colorizer(QStringLiteral("<font color=
\"
%1
\"
>%2</font><br>"
));
void
MainWindow::
addErrorMessage(const
QString &
amp;message)
{
ui-&
gt;serverInfo-&
gt;insertHtml(colorizer.arg(QStringLiteral("Crimson"
), message));
}
void
MainWindow::
addWarningMessage(const
QString &
amp;message)
{
ui-&
gt;serverInfo-&
gt;insertHtml(colorizer.arg(QStringLiteral("DarkOrange"
), message));
}
void
MainWindow::
addInfoMessage(const
QString &
amp;message)
{
ui-&
gt;serverInfo-&
gt;insertHtml(colorizer.arg(QStringLiteral("DarkBlue"
), message));
}
void
MainWindow::
addClientMessage(const
QString &
amp;peerInfo, const
QByteArray &
amp;datagram,
const
QByteArray &
amp;plainText)
{
static
const
QString messageColor =
QStringLiteral("DarkMagenta"
);
static
const
QString formatter =
QStringLiteral("<br>---------------"
"<br>A message from %1"
"<br>DTLS datagram:<br> %2"
"<br>As plain text:<br> %3"
);
const
QString html =
formatter.arg(peerInfo, QString::
fromUtf8(datagram.toHex(' '
)),
QString::
fromUtf8(plainText));
ui-&
gt;messages-&
gt;insertHtml(colorizer.arg(messageColor, html));
}