DTLS 서버
이 예제에서는 간단한 DTLS 서버를 구현하는 방법을 설명합니다.
참고: DTLS 서버 예제는 DTLS 클라이언트 예제와 함께 실행하기 위한 것입니다.
서버는 DtlsServer 클래스에 의해 구현됩니다. 이 서버는 QUdpSocket, QDtlsClientVerifier, QDtls 를 사용하여 각 클라이언트의 도달 가능성을 테스트하고 핸드셰이크를 완료하며 암호화된 메시지를 읽고 씁니다.
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; } ...
또는 이 피어와 핸드셰이크를 계속합니다:
... 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(); } }
핸드셰이크 단계에서 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)); }
© 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.