TLS for the MQTT connection (iteration 2, step 5)
ca_file or the system's CAs (MOSQ_OPT_TLS_USE_OS_CERTS), client certificate, broker certificate and host name verified, settings errors reported before connecting. The client thread blocks SIGPIPE: with TLS, a write to a connection the broker closed killed the process. The reason of a failed handshake (libmosquitto's log) is appended to the error. tst_mqtt covers TLS with test certificates in tests/data (D44). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
#include "mqtt/MosquittoClient.h"
|
||||
|
||||
#include <mosquitto.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
@@ -61,6 +63,7 @@ MosquittoClient::MosquittoClient(Settings settings, QObject* parent)
|
||||
mosquitto_disconnect_callback_set(m_mosq, &MosquittoClient::onDisconnect);
|
||||
mosquitto_publish_callback_set(m_mosq, &MosquittoClient::onPublish);
|
||||
mosquitto_message_callback_set(m_mosq, &MosquittoClient::onMessage);
|
||||
mosquitto_log_callback_set(m_mosq, &MosquittoClient::onLog);
|
||||
|
||||
const MqttConfig& broker = m_settings.broker;
|
||||
int rc = MOSQ_ERR_SUCCESS;
|
||||
@@ -76,9 +79,43 @@ MosquittoClient::MosquittoClient(Settings settings, QObject* parent)
|
||||
int(will.payload.size()), will.payload.constData(),
|
||||
1, will.retain);
|
||||
}
|
||||
if (rc == MOSQ_ERR_SUCCESS && broker.tls.enabled)
|
||||
m_initError = QStringLiteral("TLS is not supported yet");
|
||||
if (rc != MOSQ_ERR_SUCCESS) m_initError = errorText(rc, errno);
|
||||
if (rc != MOSQ_ERR_SUCCESS)
|
||||
m_initError = errorText(rc, errno);
|
||||
else if (broker.tls.enabled)
|
||||
m_initError = setUpTls();
|
||||
}
|
||||
|
||||
QString MosquittoClient::setUpTls() {
|
||||
const MqttTlsConfig& tls = m_settings.broker.tls;
|
||||
for (const QString& file : {tls.caFile, tls.certFile, tls.keyFile}) {
|
||||
if (!file.isEmpty() && !QFileInfo(file).isReadable())
|
||||
return QStringLiteral("TLS: cannot read %1").arg(file);
|
||||
}
|
||||
if (tls.certFile.isEmpty() != tls.keyFile.isEmpty())
|
||||
return QStringLiteral("TLS: cert_file and key_file go together");
|
||||
if (tls.caFile.isEmpty()) {
|
||||
// The system's CA certificates. mosquitto_tls_set() insists on a CA
|
||||
// file or directory, so this option alone enables TLS, and a client
|
||||
// certificate can't be set without ca_file.
|
||||
if (!tls.certFile.isEmpty())
|
||||
return QStringLiteral("TLS: a client certificate needs ca_file");
|
||||
const int rc = mosquitto_int_option(m_mosq, MOSQ_OPT_TLS_USE_OS_CERTS, 1);
|
||||
return rc == MOSQ_ERR_SUCCESS
|
||||
? QString()
|
||||
: QStringLiteral("TLS: %1").arg(errorText(rc, errno));
|
||||
}
|
||||
const QByteArray ca = QFile::encodeName(tls.caFile);
|
||||
const QByteArray cert = QFile::encodeName(tls.certFile);
|
||||
const QByteArray key = QFile::encodeName(tls.keyFile);
|
||||
// Verifies the broker's certificate and host name (libmosquitto's
|
||||
// default).
|
||||
const int rc =
|
||||
mosquitto_tls_set(m_mosq, ca.constData(), nullptr,
|
||||
cert.isEmpty() ? nullptr : cert.constData(),
|
||||
key.isEmpty() ? nullptr : key.constData(), nullptr);
|
||||
return rc == MOSQ_ERR_SUCCESS
|
||||
? QString()
|
||||
: QStringLiteral("TLS: %1").arg(errorText(rc, errno));
|
||||
}
|
||||
|
||||
MosquittoClient::~MosquittoClient() {
|
||||
@@ -150,10 +187,20 @@ bool MosquittoClient::waitForPublished(int timeoutMs) {
|
||||
}
|
||||
|
||||
void MosquittoClient::run() {
|
||||
// With TLS, OpenSSL writes with write(), not send(MSG_NOSIGNAL): writing
|
||||
// to a connection the broker closed raises SIGPIPE, which would end the
|
||||
// whole process. Blocked in this thread, the write fails with EPIPE
|
||||
// instead. Only this thread writes to the socket (threaded mode).
|
||||
sigset_t pipe;
|
||||
sigemptyset(&pipe);
|
||||
sigaddset(&pipe, SIGPIPE);
|
||||
pthread_sigmask(SIG_BLOCK, &pipe, nullptr);
|
||||
|
||||
const QByteArray host = m_settings.broker.host.toUtf8();
|
||||
int delayMs = m_settings.reconnectMinMs;
|
||||
while (!m_stop) {
|
||||
m_lastError.clear();
|
||||
m_lastLogError.clear();
|
||||
// Blocks for the DNS lookup; the TCP connect itself completes in the
|
||||
// loop below.
|
||||
int rc =
|
||||
@@ -174,8 +221,13 @@ void MosquittoClient::run() {
|
||||
if (m_stop) break;
|
||||
// A connection that came up starts the backoff again.
|
||||
if (m_state == State::Connected) delayMs = m_settings.reconnectMinMs;
|
||||
setState(State::Disconnected,
|
||||
m_lastError.isEmpty() ? errorText(rc, err) : m_lastError);
|
||||
QString error = m_lastError.isEmpty() ? errorText(rc, err) : m_lastError;
|
||||
// libmosquitto logs the reason of a failed TLS handshake (e.g.
|
||||
// "certificate verify failed"), but returns only "TLS error" (2.0) or
|
||||
// "Protocol error" (2.1).
|
||||
if (!m_lastLogError.isEmpty())
|
||||
error += QStringLiteral(": ") + m_lastLogError;
|
||||
setState(State::Disconnected, error);
|
||||
if (!sleepUnlessStopped(delayMs)) break;
|
||||
delayMs = std::min(delayMs * 2, m_settings.reconnectMaxMs);
|
||||
}
|
||||
@@ -253,3 +305,14 @@ void MosquittoClient::onMessage(mosquitto*, void* self,
|
||||
QByteArray(static_cast<const char*>(message->payload),
|
||||
message->payloadlen));
|
||||
}
|
||||
|
||||
void MosquittoClient::onLog(mosquitto*, void* self, int level,
|
||||
const char* text) {
|
||||
if (!(level & MOSQ_LOG_ERR)) return;
|
||||
// E.g. "OpenSSL Error[0]: error:0A000086:SSL routines::certificate verify
|
||||
// failed": keep the reason at the end.
|
||||
QString line = QString::fromUtf8(text);
|
||||
const qsizetype colon = line.lastIndexOf(QLatin1Char(':'));
|
||||
if (colon >= 0) line = line.mid(colon + 1).trimmed();
|
||||
static_cast<MosquittoClient*>(self)->m_lastLogError = line;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ class MosquittoClient : public MqttClient {
|
||||
State state() const override { return m_state; }
|
||||
|
||||
private:
|
||||
// Sets up TLS from the settings; returns an error text or nothing.
|
||||
QString setUpTls();
|
||||
// Thread body: connect, loop, back off, until m_stop.
|
||||
void run();
|
||||
// Sleeps up to `ms`; returns early (false) when stopping.
|
||||
@@ -58,6 +60,7 @@ class MosquittoClient : public MqttClient {
|
||||
static void onPublish(mosquitto* mosq, void* self, int mid);
|
||||
static void onMessage(mosquitto* mosq, void* self,
|
||||
const mosquitto_message* message);
|
||||
static void onLog(mosquitto* mosq, void* self, int level, const char* text);
|
||||
|
||||
const Settings m_settings;
|
||||
mosquitto* m_mosq = nullptr;
|
||||
@@ -67,6 +70,9 @@ class MosquittoClient : public MqttClient {
|
||||
std::atomic<bool> m_stop{false};
|
||||
// Why the last attempt failed (our thread only).
|
||||
QString m_lastError;
|
||||
// libmosquitto's last error log line of this attempt (our thread only):
|
||||
// the details of a TLS error.
|
||||
QString m_lastLogError;
|
||||
// The error last reported by setState(), to report repeats only once.
|
||||
QString m_reportedError;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user