DTLS server

This examples demonstrates how to implement a simple DTLS server.

../_images/secureudpserver-example.png

Note

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(QObject):

    Q_OBJECT
# public
    DtlsServer()
    ~DtlsServer()
    listen = bool(QHostAddress address, quint16 port)
    isListening = bool()
    def close():
signals:
    def errorMessage(message):
    def warningMessage(message):
    def infoMessage(message):
    def datagramReceived(peerInfo, cipherText,):
                          plainText) = QByteArray()
slots: = private()
    def readyRead():
    def pskRequired(auth):
# private
    def handleNewConnection(peerAddress, peerPort,):
                             clientHello) = QByteArray()
    def doHandshake(newConnection, clientHello):
    def decryptDatagram(connection, clientMessage):
    def shutdown():
    listening = False()
    serverSocket = QUdpSocket()
    serverConfiguration = QSslConfiguration()
    cookieSender = QDtlsClientVerifier()
    std::vector<std::unique_ptr<QDtls>> knownClients
    Q_DISABLE_COPY(DtlsServer)

The constructor connects the readyRead() signal to its readyRead() slot and sets the minimal needed TLS configuration:

def __init__(self):

    connect(serverSocket, QAbstractSocket.readyRead, self, DtlsServer.readyRead)
    serverConfiguration = QSslConfiguration.defaultDtlsConfiguration()
    serverConfiguration.setPreSharedKeyIdentityHint("Qt DTLS example server")
    serverConfiguration.setPeerVerifyMode(QSslSocket.VerifyNone)

Note

The server is not using a certificate and is relying on Pre-Shared Key (PSK) handshake.

listen() binds QUdpSocket :

def listen(self, QHostAddress address, quint16 port):

    if (address != serverSocket.localAddress() or port != serverSocket.localPort()) {
        shutdown()
        listening = serverSocket.bind(address, port)
        if (not listening)
            errorMessage.emit(serverSocket.errorString())
    else:
        listening = True

    return listening

The readyRead() slot processes incoming datagrams:

    ...
bytesToRead = serverSocket.pendingDatagramSize()
if (bytesToRead <= 0) {
    warningMessage.emit(tr("A spurious read notification"))
    return

dgram = QByteArray(bytesToRead, Qt.Uninitialized)
peerAddress = QHostAddress()
peerPort = 0
bytesRead = serverSocket.readDatagram(dgram.data(), dgram.size(),
                                                   peerAddress, peerPort)
if (bytesRead <= 0) {
    warningMessage.emit(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() or not peerPort) {
    warningMessage.emit(tr("Failed to extract peer info (address, port)"))
    return

client = std::find_if(knownClients.begin(), knownClients.end(),
                                 [](std::unique_ptr<QDtls> connection){
    return connection.peerAddress() == peerAddress
           and connection.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())
    def handleNewConnection(peerAddress,peerPort,dgram):            ...

If it’s a known DTLS client, the server either decrypts the datagram:

    ...
if ((client).isConnectionEncrypted()) {
    decryptDatagram(client.get(), dgram)
    if ((client).dtlsError() == QDtlsError.RemoteClosedConnectionError)
        knownClients.erase(client)
    return            ...

or continues a handshake with this peer:

    ...
doHandshake(client.get(), dgram)            ...

handleNewConnection() verifies it’s a reachable DTLS client, or sends a HelloVerifyRequest:

def handleNewConnection(self, peerAddress,):
                                     peerPort, = quint16()

    if (not listening)
        return
    peerInfo = peer_info(peerAddress, peerPort)
    if (cookieSender.verifyClient(serverSocket, clientHello, peerAddress, peerPort)) {
        infoMessage.emit(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<QDtls> newConnection{QDtls{QSslSocket.SslServerMode}}
newConnection.setDtlsConfiguration(serverConfiguration)
newConnection.setPeer(peerAddress, peerPort)
newConnection.connect(newConnection.get(), QDtls.pskRequired,
                       self, DtlsServer::pskRequired)
knownClients.push_back(std::move(newConnection))
doHandshake(knownClients.back().get(), clientHello)            ...

doHandshake() progresses through the handshake phase:

def doHandshake(self, newConnection, clientHello):

    result = newConnection.doHandshake(serverSocket, clientHello)
    if (not result) {
        errorMessage.emit(newConnection.dtlsErrorString())
        return

    peerInfo = peer_info(newConnection.peerAddress(),
                                       newConnection.peerPort())
    switch (newConnection.handshakeState()) {
    QDtls.HandshakeInProgress: = case()
        infoMessage.emit(peerInfo + tr(": handshake is in progress ..."))
        break
    QDtls.HandshakeComplete: = case()
        infoMessage.emit(tr("Connection with %1 encrypted. %2")
                         .arg(peerInfo, connection_info(newConnection)))
        break
    default:
        Q_UNREACHABLE()

During the handshake phase, the pskRequired() signal is emitted and the pskRequired() slot provides the preshared key:

def pskRequired(self, auth):

    Q_ASSERT(auth)
    infoMessage.emit(tr("PSK callback, received a client's identity: '%1'")
                     .arg(QString.fromLatin1(auth.identity())))
    auth.setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"))

Note

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:

def decryptDatagram(self, connection, clientMessage):

    Q_ASSERT(connection.isConnectionEncrypted())
    peerInfo = peer_info(connection.peerAddress(), connection.peerPort())
    dgram = connection.decryptDatagram(serverSocket, clientMessage)
    if (dgram.size()) {
        datagramReceived.emit(peerInfo, clientMessage, dgram)
        connection.writeDatagramEncrypted(serverSocket, tr("to %1: ACK").arg(peerInfo).toLatin1())
    } else if (connection.dtlsError() == QDtlsError.NoError) {
        warningMessage.emit(peerInfo + ": " + tr("0 byte dgram, could be a re-connect attempt?"))
    else:
        errorMessage.emit(peerInfo + ": " + connection.dtlsErrorString())

The server closes its DTLS connections by calling shutdown() :

def shutdown(self):

    for connection in qExchange(knownClients, {}):
        connection.shutdown(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:

colorizer = QString(QStringLiteral("<font color=\"%1\">%2"))
def addErrorMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg(QStringLiteral("Crimson"), message))

def addWarningMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message))

def addInfoMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message))

def addClientMessage(self, peerInfo, datagram,):
                                  plainText) = QByteArray()

    messageColor = QStringLiteral("DarkMagenta")
    formatter = QStringLiteral("<br>---------------"()
                                                    "<br>A message from %1"
                                                    "<br>DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3")
    html = formatter.arg(peerInfo, QString.fromUtf8(datagram.toHex(' ')),
                                       QString.fromUtf8(plainText))
    ui.messages.insertHtml(colorizer.arg(messageColor, html))

Example project @ code.qt.io