Cliente DTLS

Este ejemplo muestra cómo implementar conexiones DTLS del lado del cliente.

Captura de pantalla del ejemplo de cliente DTLS.

Nota: El ejemplo de cliente DTLS está pensado para ejecutarse junto con el ejemplo de servidor DT LS.

El ejemplo de cliente DTLS puede establecer varias conexiones DTLS a uno o varios servidores DTLS. Una conexión DTLS del lado del cliente es implementada por la clase DtlsAssociation. Esta clase utiliza QUdpSocket para leer y escribir datagramas y QDtls para el cifrado:

class DtlsAssociation : public QObject
{
    Q_OBJECT

public:
    DtlsAssociation(const QHostAddress &address, quint16 port,
                    const QString &connectionName);
    ~DtlsAssociation();
    void startHandshake();

signals:
    void errorMessage(const QString &message);
    void warningMessage(const QString &message);
    void infoMessage(const QString &message);
    void serverResponse(const QString &clientInfo, const QByteArray &datagraam,
                        const QByteArray &plainText);

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

private:
    QString name;
    QUdpSocket socket;
    QDtls crypto;

    QTimer pingTimer;
    unsigned ping = 0;

    Q_DISABLE_COPY(DtlsAssociation)
};

El constructor establece la configuración TLS mínima para la nueva conexión DTLS, y establece la dirección y el puerto del servidor:

    ...
auto configuration = QSslConfiguration::defaultDtlsConfiguration();
configuration.setPeerVerifyMode(QSslSocket::VerifyNone);
crypto.setPeer(address, port);
crypto.setDtlsConfiguration(configuration);
    ...

La señal QDtls::handshakeTimeout() se conecta al slot handleTimeout() para tratar la pérdida y retransmisión de paquetes durante la fase de handshake:

    ...
connect(&crypto, &QDtls::handshakeTimeout, this, &DtlsAssociation::handshakeTimeout);
    ...

Para asegurarnos de que sólo recibimos los datagramas del servidor, conectamos nuestro socket UDP al servidor:

    ...
socket.connectToHost(address.toString(), port);
    ...

La señal QUdpSocket::readyRead() se conecta a la ranura readyRead():

    ...
connect(&socket, &QUdpSocket::readyRead, this, &DtlsAssociation::readyRead);
    ...

Cuando se establece una conexión segura con un servidor, un objeto DtlsAssociation enviará mensajes ping cortos al servidor, utilizando un temporizador:

pingTimer.setInterval(5000);
connect(&pingTimer, &QTimer::timeout, this, &DtlsAssociation::pingTimeout);

startHandshake() inicia un handshake con el servidor:

void DtlsAssociation::startHandshake()
{
    if (socket.state() != QAbstractSocket::ConnectedState) {
        emit infoMessage(tr("%1: connecting UDP socket first ...").arg(name));
        connect(&socket, &QAbstractSocket::connected, this, &DtlsAssociation::udpSocketConnected);
        return;
    }

    if (!crypto.doHandshake(&socket))
        emit errorMessage(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString()));
    else
        emit infoMessage(tr("%1: starting a handshake").arg(name));
}

La ranura readyRead() lee un datagrama enviado por el servidor:

QByteArray dgram(socket.pendingDatagramSize(), Qt::Uninitialized);
const qint64 bytesRead = socket.readDatagram(dgram.data(), dgram.size());
if (bytesRead <= 0) {
    emit warningMessage(tr("%1: spurious read notification?").arg(name));
    return;
}

dgram.resize(bytesRead);

Si el handshake ya se ha completado, este datagrama se descifra:

if (crypto.isConnectionEncrypted()) {
    const QByteArray plainText = crypto.decryptDatagram(&socket, dgram);
    if (plainText.size()) {
        emit serverResponse(name, dgram, plainText);
        return;
    }

    if (crypto.dtlsError() == QDtlsError::RemoteClosedConnectionError) {
        emit errorMessage(tr("%1: shutdown alert received").arg(name));
        socket.close();
        pingTimer.stop();
        return;
    }

    emit warningMessage(tr("%1: zero-length datagram received?").arg(name));
} else {

en caso contrario, se intenta continuar el handshake:

    if (!crypto.doHandshake(&socket, dgram)) {
        emit errorMessage(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString()));
        return;
    }

Cuando el handshake se ha completado, enviamos nuestro primer mensaje ping:

    if (crypto.isConnectionEncrypted()) {
        emit infoMessage(tr("%1: encrypted connection established!").arg(name));
        pingTimer.start();
        pingTimeout();
    } else {

La ranura pskRequired() proporciona la clave precompartida (PSK) necesaria durante la fase de handshake:

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

    emit infoMessage(tr("%1: providing pre-shared key ...").arg(name));
    auth->setIdentity(name.toLatin1());
    auth->setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"));
}

Nota: En aras de la brevedad, la definición de pskRequired() está simplificada en exceso. La documentación de la clase QSslPreSharedKeyAuthenticator explica en detalle cómo se puede implementar correctamente esta ranura.

pingTimeout() envía un mensaje encriptado al servidor:

void DtlsAssociation::pingTimeout()
{
    static const QString message = QStringLiteral("I am %1, please, accept our ping %2");
    const qint64 written = crypto.writeDatagramEncrypted(&socket, message.arg(name).arg(ping).toLatin1());
    if (written <= 0) {
        emit errorMessage(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString()));
        pingTimer.stop();
        return;
    }

    ++ping;
}

Durante la fase de handshake el cliente debe manejar posibles timeouts, que pueden ocurrir debido a la pérdida de paquetes. La ranura handshakeTimeout() retransmite los mensajes handshake:

void DtlsAssociation::handshakeTimeout()
{
    emit warningMessage(tr("%1: handshake timeout, trying to re-transmit").arg(name));
    if (!crypto.handleTimeout(&socket))
        emit errorMessage(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString()));
}

Antes de que se destruya la conexión de un cliente, debe cerrarse su conexión DTLS:

DtlsAssociation::~DtlsAssociation()
{
    if (crypto.isConnectionEncrypted())
        crypto.shutdown(&socket);
}

Los mensajes de error, los mensajes informativos y las respuestas descifradas de los servidores se muestran en la interfaz de usuario:

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

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

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

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

void MainWindow::addServerResponse(const QString &clientInfo, const QByteArray &datagram,
                                   const QByteArray &plainText)
{
    static const QString messageColor = QStringLiteral("DarkMagenta");
    static const QString formatter = QStringLiteral("<br>---------------"
                                                    "<br>%1 received a DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3");

    const QString html = formatter.arg(clientInfo, QString::fromUtf8(datagram.toHex(' ')),
                                       QString::fromUtf8(plainText));
    ui->serverMessages->insertHtml(colorizer.arg(messageColor, html));
}

Proyecto de ejemplo @ 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.