DTLSサーバー

この例では、単純なDTLSサーバーの実装方法を示す。

Screenshot of the DTLS server example.

注: DTLSサーバーの例は、DTLSクライアントの例と一緒に実行することを想定しています。

サーバーはDtlsServerクラスで実装されている。QUdpSocketQDtlsClientVerifierQDtls を使用して、各クライアントの到達可能性をテストし、ハンドシェイクを完了し、暗号化されたメッセージを読み書きする。

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)
};

コンストラクタは、QUdpSocket::readyRead() シグナルをreadyRead()スロットに接続し、必要最小限のTLSコンフィギュレーションを設定する:

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

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

注意: サーバーは証明書を使わず、事前共有鍵(PSK)ハンドシェイクに依存している。

listen() は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;
}

readyRead()スロットは受信データグラムを処理する:

    ...
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);
    ...

アドレスとポート番号を抽出した後、サーバーはまず、それが既知のピアからのデータグラムであるかどうかをテストする:

    ...
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;
});
    ...

もしそれが新しい未知のアドレスとポートであれば、そのデータグラムはDTLSクライアントから送信されたClientHelloメッセージの可能性があるものとして処理される:

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

既知のDTLSクライアントの場合、サーバーはデータグラムを復号化する:

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

既知のDTLSクライアントであれば、サーバーはデータグラムを復号化する:

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

handleNewConnection()が到達可能なDTLSクライアントであることを確認するか、 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"));
    ...

新しいクライアントが到達可能な DTLS クライアントであることが確認された場合、サーバーは新しいQDtls オブジェクトを作成して設定し、サーバー側のハンドシェイクを開始する:

    ...
    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()はハンドシェイクフェーズを進行させる:

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();
    }
}

doHandshake()はハンドシェイクフェーズを進行させる。ハンドシェイクフェーズ中、QDtls::pskRequired() シグナルが発せられ、pskRequired()スロットは事前共有鍵を提供する:

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"));
}

注: 簡略化のため、pskRequired()の定義は単純化しすぎている。QSslPreSharedKeyAuthenticator クラスのドキュメントに、このスロットの適切な実装方法が詳しく説明されている。

ネットワーク・ピアのハンドシェイクが完了すると、暗号化されたDTLS接続が確立されたとみなされ、サーバーは、ピアから送信された後続のデータグラムを、decryptDatagram()を呼び出して復号化する。サーバーはまた、暗号化された応答をピアに送信する:

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());
    }
}

サーバーはQDtls::shutdown() を呼び出してDTLS接続を閉じる:

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

    serverSocket.close();
}

サーバーは、errorMessage()、warningMessage()、infoMessage()、datagramReceived()というシグナルを発することで、エラー、情報メッセージ、暗号化解除されたデータグラムを報告する。これらのメッセージはサーバーのUIによって記録される:

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));
}

プロジェクト例 @ code.qt.io

© 2025 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.