Serveur DTLS

Cet exemple montre comment mettre en œuvre un serveur DTLS simple.

Capture d'écran de l'exemple de serveur DTLS.

Note : L'exemple du serveur DTLS est destiné à être exécuté en même temps que l'exemple du client DTLS.

Le serveur est implémenté par la classe DtlsServer. Il utilise QUdpSocket, QDtlsClientVerifier, et QDtls pour tester la joignabilité de chaque client, compléter une poignée de main, et lire et écrire des messages cryptés.

class DtlsServer : public QObject
{
    Q_OBJECT

public:
    DtlsServer();
    ~DtlsServer();

    bool listen(const QHostAddress &address, quint16 port);
    bool isListening() const;
    void close();

signals:
    void errorMessage(const QString &message);
    void warningMessage(const QString &message);
    void infoMessage(const QString &message);

    void datagramReceived(const QString &peerInfo, const QByteArray &cipherText,
                          const QByteArray &plainText);

private slots:
    void readyRead();
    void pskRequired(QSslPreSharedKeyAuthenticator *auth);

private:
    void handleNewConnection(const QHostAddress &peerAddress, quint16 peerPort,
                             const QByteArray &clientHello);

    void doHandshake(QDtls *newConnection, const QByteArray &clientHello);
    void decryptDatagram(QDtls *connection, const QByteArray &clientMessage);
    void shutdown();

    bool listening = false;
    QUdpSocket serverSocket;

    QSslConfiguration serverConfiguration;
    QDtlsClientVerifier cookieSender;
    std::vector<std::unique_ptr<QDtls>> knownClients;

    Q_DISABLE_COPY(DtlsServer)
};

Le constructeur connecte le signal QUdpSocket::readyRead() à son slot readyRead() et définit la configuration TLS minimale nécessaire :

DtlsServer::DtlsServer()
{
    connect(&serverSocket, &QAbstractSocket::readyRead, this, &DtlsServer::readyRead);

    serverConfiguration = QSslConfiguration::defaultDtlsConfiguration();
    serverConfiguration.setPreSharedKeyIdentityHint("Qt DTLS example server");
    serverConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
}

Note : Le serveur n'utilise pas de certificat et s'appuie sur une poignée de main à clé pré-partagée (PSK).

listen() lie QUdpSocket:

bool DtlsServer::listen(const QHostAddress &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;
}

Le slot readyRead() traite les datagrammes entrants :

    ...
const qint64 bytesToRead = serverSocket.pendingDatagramSize();
if (bytesToRead <= 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(),
                                                   &peerAddress, &peerPort);
if (bytesRead <= 0) {
    emit warningMessage(tr("Failed to read a datagram: ") + serverSocket.errorString());
    return;
}

dgram.resize(bytesRead);
    ...

Après avoir extrait une adresse et un numéro de port, le serveur teste d'abord s'il s'agit d'un datagramme provenant d'un homologue déjà connu :

    ...
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(),
                                 [&](const std::unique_ptr<QDtls> &connection){
    return connection->peerAddress() == peerAddress
           && connection->peerPort() == peerPort;
});
    ...

S'il s'agit d'une nouvelle adresse et d'un nouveau port inconnus, le datagramme est traité comme un message ClientHello potentiel, envoyé par un client DTLS :

    ...
if (client == knownClients.end())
    return handleNewConnection(peerAddress, peerPort, dgram);
    ...

S'il s'agit d'un client DTLS connu, le serveur décrypte le datagramme :

    ...
if ((*client)->isConnectionEncrypted()) {
    decryptDatagram(client->get(), dgram);
    if ((*client)->dtlsError() == QDtlsError::RemoteClosedConnectionError)
        knownClients.erase(client);
    return;
}
    ...

soit poursuit une poignée de main avec ce pair :

    ...
doHandshake(client->get(), dgram);
    ...

handleNewConnection() vérifie qu'il s'agit d'un client DTLS joignable, ou envoie une requête HelloVerifyRequest :

void DtlsServer::handleNewConnection(const QHostAddress &peerAddress,
                                     quint16 peerPort, const QByteArray &clientHello)
{
    if (!listening)
        return;

    const QString peerInfo = peer_info(peerAddress, peerPort);
    if (cookieSender.verifyClient(&serverSocket, clientHello, peerAddress, peerPort)) {
        emit infoMessage(peerInfo + tr(": verified, starting a handshake"));
    ...

Si le nouveau client a été vérifié comme étant un client DTLS joignable, le serveur crée et configure un nouvel objet QDtls, et démarre un handshake côté serveur :

    ...
    std::unique_ptr<QDtls> newConnection{new QDtls{QSslSocket::SslServerMode}};
    newConnection->setDtlsConfiguration(serverConfiguration);
    newConnection->setPeer(peerAddress, peerPort);
    newConnection->connect(newConnection.get(), &QDtls::pskRequired,
                           this, &DtlsServer::pskRequired);
    knownClients.push_back(std::move(newConnection));
    doHandshake(knownClients.back().get(), clientHello);
    ...

doHandshake() fait progresser la phase de poignée de main :

void DtlsServer::doHandshake(QDtls *newConnection, const QByteArray &clientHello)
{
    const bool result = newConnection->doHandshake(&serverSocket, clientHello);
    if (!result) {
        emit errorMessage(newConnection->dtlsErrorString());
        return;
    }

    const QString peerInfo = peer_info(newConnection->peerAddress(),
                                       newConnection->peerPort());
    switch (newConnection->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();
    }
}

Pendant la phase de poignée de main, le signal QDtls::pskRequired() est émis et le slot pskRequired() fournit la clé prépartagée :

void DtlsServer::pskRequired(QSslPreSharedKeyAuthenticator *auth)
{
    Q_ASSERT(auth);

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

Remarque : par souci de concision, la définition de pskRequired() a été simplifiée à l'extrême. La documentation de la classe QSslPreSharedKeyAuthenticator explique en détail comment ce slot peut être correctement mis en œuvre.

Une fois la poignée de main terminée pour l'homologue du réseau, une connexion DTLS cryptée est considérée comme établie et le serveur décrypte les datagrammes suivants, envoyés par l'homologue, en appelant decryptDatagram(). Le serveur envoie également une réponse chiffrée à l'homologue :

void DtlsServer::decryptDatagram(QDtls *connection, const QByteArray &clientMessage)
{
    Q_ASSERT(connection->isConnectionEncrypted());

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

Le serveur ferme ses connexions DTLS en appelant QDtls::shutdown() :

void DtlsServer::shutdown()
{
    for (const auto &connection : std::exchange(knownClients, {}))
        connection->shutdown(&serverSocket);

    serverSocket.close();
}

Pendant son fonctionnement, le serveur signale les erreurs, les messages d'information et les datagrammes décryptés en émettant les signaux errorMessage(), warningMessage(), infoMessage() et datagramReceived(). Ces messages sont enregistrés par l'interface utilisateur du serveur :

const QString colorizer(QStringLiteral("<font color=\"%1\">%2</font><br>"));

void MainWindow::addErrorMessage(const QString &message)
{
    ui->serverInfo->insertHtml(colorizer.arg(QStringLiteral("Crimson"), message));
}

void MainWindow::addWarningMessage(const QString &message)
{
    ui->serverInfo->insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message));
}

void MainWindow::addInfoMessage(const QString &message)
{
    ui->serverInfo->insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message));
}

void MainWindow::addClientMessage(const QString &peerInfo, const QByteArray &datagram,
                                  const QByteArray &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->messages->insertHtml(colorizer.arg(messageColor, html));
}

Exemple de projet @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.