Warning
This section contains snippets that were automatically translated from C++ to Python and may contain errors.
DTLS client#
This example demonstrates how to implement client-side DTLS connections.
Note
The DTLS client example is intended to be run alongside the DTLS server example.
The example DTLS client can establish several DTLS connections to one or many DTLS servers. A client-side DTLS connection is implemented by the DtlsAssociation class. This class uses QUdpSocket
to read and write datagrams and QDtls
for encryption:
class DtlsAssociation(QObject): Q_OBJECT # public DtlsAssociation(QHostAddress address, quint16 port, connectionName) = QString() ~DtlsAssociation() def startHandshake(): # signals def errorMessage(message): def warningMessage(message): def infoMessage(message): def serverResponse(clientInfo, datagraam,): plainText) = QByteArray() # private slots def udpSocketConnected(): def readyRead(): def handshakeTimeout(): def pskRequired(auth): def pingTimeout(): # private name = QString() socket = QUdpSocket() crypto = QDtls() pingTimer = QTimer() ping = 0 Q_DISABLE_COPY(DtlsAssociation)
The constructor sets the minimal TLS configuration for the new DTLS connection, and sets the address and the port of the server:
... configuration = QSslConfiguration.defaultDtlsConfiguration() configuration.setPeerVerifyMode(QSslSocket.VerifyNone) crypto.setPeer(address, port) crypto.setDtlsConfiguration(configuration) ...
The handshakeTimeout()
signal is connected to the handleTimeout() slot to deal with packet loss and retransmission during the handshake phase:
... crypto.handshakeTimeout.connect(self.handshakeTimeout) ...
To ensure we receive only the datagrams from the server, we connect our UDP socket to the server:
... socket.connectToHost(address.toString(), port) ...
The readyRead()
signal is connected to the readyRead() slot:
... socket.readyRead.connect(self.readyRead) ...
When a secure connection to a server is established, a DtlsAssociation object will be sending short ping messages to the server, using a timer:
pingTimer.setInterval(5000) pingTimer.timeout.connect(self.pingTimeout)
startHandshake() starts a handshake with the server:
def startHandshake(self): if socket.state() != QAbstractSocket.ConnectedState: infoMessage.emit(tr("%1: connecting UDP socket first ...").arg(name)) socket.connected.connect(self.udpSocketConnected) return if not crypto.doHandshake(socket): errorMessage.emit(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString())) else: infoMessage.emit(tr("%1: starting a handshake").arg(name))
The readyRead() slot reads a datagram sent by the server:
dgram = QByteArray(socket.pendingDatagramSize(), Qt.Uninitialized) bytesRead = socket.readDatagram(dgram.data(), dgram.size()) if bytesRead <= 0: warningMessage.emit(tr("%1: spurious read notification?").arg(name)) return dgram.resize(bytesRead)
If the handshake was already completed, this datagram is decrypted:
if crypto.isConnectionEncrypted(): plainText = crypto.decryptDatagram(socket, dgram) if plainText.size(): serverResponse.emit(name, dgram, plainText) return if crypto.dtlsError() == QDtlsError.RemoteClosedConnectionError: errorMessage.emit(tr("%1: shutdown alert received").arg(name)) socket.close() pingTimer.stop() return warningMessage.emit(tr("%1: zero-length datagram received?").arg(name)) else:
otherwise, we try to continue the handshake:
if not crypto.doHandshake(socket, dgram): errorMessage.emit(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString())) return
When the handshake has completed, we send our first ping message:
if crypto.isConnectionEncrypted(): infoMessage.emit(tr("%1: encrypted connection established!").arg(name)) pingTimer.start() pingTimeout() else:
The pskRequired() slot provides the Pre-Shared Key (PSK) needed during the handshake phase:
def pskRequired(self, auth): Q_ASSERT(auth) infoMessage.emit(tr("%1: providing pre-shared key ...").arg(name)) auth.setIdentity(name.toLatin1()) 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.
pingTimeout() sends an encrypted message to the server:
def pingTimeout(self): message = "I am %1, please, accept our ping %2" written = crypto.writeDatagramEncrypted(socket, message.arg(name).arg(ping).toLatin1()) if written <= 0: errorMessage.emit(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString())) pingTimer.stop() return ping = ping + 1
During the handshake phase the client must handle possible timeouts, which can happen due to packet loss. The handshakeTimeout() slot retransmits the handshake messages:
def handshakeTimeout(self): warningMessage.emit(tr("%1: handshake timeout, trying to re-transmit").arg(name)) if not crypto.handleTimeout(socket): errorMessage.emit(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString()))
Before a client connection is destroyed, its DTLS connection must be shut down:
DtlsAssociation.~DtlsAssociation() if crypto.isConnectionEncrypted(): crypto.shutdown(socket)
Error messages, informational messages, and decrypted responses from servers are displayed by the UI:
colorizer = QString(QStringLiteral("<font color=\"%1\">%2")) def addErrorMessage(self, message): ui.clientMessages.insertHtml(colorizer.arg("Crimson", message)) def addWarningMessage(self, message): ui.clientMessages.insertHtml(colorizer.arg("DarkOrange", message)) def addInfoMessage(self, message): ui.clientMessages.insertHtml(colorizer.arg("DarkBlue", message)) def addServerResponse(self, clientInfo, datagram,): QByteArray plainText) messageColor = "DarkMagenta" formatter = QStringLiteral("<br>---------------"() "<br>%1 received a DTLS datagram:<br> %2" "<br>As plain text:<br> %3") html = formatter.arg(clientInfo, QString.fromUtf8(datagram.toHex(' ')), QString.fromUtf8(plainText)) ui.serverMessages.insertHtml(colorizer.arg(messageColor, html))