Index: tools/DCSsim/DCSSimController.cpp =================================================================== diff -u -r59b4c22f45a1d098064a886452769204e90cfb4b -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 --- tools/DCSsim/DCSSimController.cpp (.../DCSSimController.cpp) (revision 59b4c22f45a1d098064a886452769204e90cfb4b) +++ tools/DCSsim/DCSSimController.cpp (.../DCSSimController.cpp) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) @@ -14,41 +14,148 @@ #include #include #include +#include +#include +#include +#include #include #include "DCSSimController.h" #include "LeahiMsgDefs.h" #include "LeahiMsgProtoUtils.h" /*! + * \brief SslTcpServer::setTls + * \details Installs the TLS material; connections accepted from here on are encrypted. + * \param tls Certificate, key and optional client CA bundle. + */ +void SslTcpServer::setTls(const Tls &tls) +{ + _tls = tls; +} + +/*! + * \brief SslTcpServer::incomingConnection + * \details Adopts the accepted descriptor and, with TLS material set, starts the + * server-side handshake before surfacing the connection. + * \param socketDescriptor Native descriptor of the accepted connection. + */ +void SslTcpServer::incomingConnection(qintptr socketDescriptor) +{ + if (_tls.cert.isNull()) { + QTcpServer::incomingConnection(socketDescriptor); + return; + } + + auto *socket = new QSslSocket(this); + if (!socket->setSocketDescriptor(socketDescriptor)) { + qWarning().noquote() << "SslTcpServer: cannot adopt the accepted socket"; + delete socket; + return; + } + + QSslConfiguration ssl = QSslConfiguration::defaultConfiguration(); + ssl.setLocalCertificate(_tls.cert); + ssl.setPrivateKey(_tls.key); + if (!_tls.caCerts.isEmpty()) { + ssl.setCaCertificates(_tls.caCerts); + // mTLS: refuse clients that do not present a certificate the CA signs. + ssl.setPeerVerifyMode(QSslSocket::VerifyPeer); + } + socket->setSslConfiguration(ssl); + + connect(socket, QOverload &>::of(&QSslSocket::sslErrors), + this, [](const QList &errors) { + for (const QSslError &error : errors) { + qWarning().noquote() << "SslTcpServer: TLS error:" << error.errorString(); + } + }); + + socket->startServerEncryption(); + addPendingConnection(socket); +} + +/*! * \brief DCSSimController::DCSSimController * \details Constructor * \param port TCP port to listen on. * \param parent QObject parent. */ -DCSSimController::DCSSimController(quint16 port, QObject *parent) : +DCSSimController::DCSSimController(quint16 port, const TlsConfig &tls, QObject *parent) : QObject(parent), - _port(port) + _port(port), + _tlsConfig(tls) { connect(&_server, &QTcpServer::newConnection, this, &DCSSimController::onNewConnection); } /*! * \brief DCSSimController::listen * \details Starts the controller listening on all interfaces. - * \return true on success, false if the port cannot be bound. + * \return true on success, false if the port cannot be bound or TLS material + * cannot be loaded. */ bool DCSSimController::listen() { + if (!_tlsConfig.certPath.isEmpty() && !initTls()) { + return false; + } + if (!_server.listen(QHostAddress::Any, _port)) { qCritical().noquote() << "DCSSimController: cannot listen on port" << _port << "—" << _server.errorString(); return false; } - qInfo().noquote() << "DCSSimController: listening on port" << _port; + qInfo().noquote() << "DCSSimController: listening on port" << _port + << (_tlsConfig.certPath.isEmpty() ? "(plain)" + : _tlsConfig.caPath.isEmpty() ? "(TLS)" : "(mTLS)"); return true; } /*! + * \brief DCSSimController::initTls + * \details Loads the PEM material from TlsConfig into the server. + * \return true if everything referenced by TlsConfig loaded. + * \note The key is tried as RSA first, then EC. + */ +bool DCSSimController::initTls() +{ + SslTcpServer::Tls tls; + + const QList certs = QSslCertificate::fromPath(_tlsConfig.certPath); + if (certs.isEmpty()) { + qCritical().noquote() << "DCSSimController: no server certificate in" << _tlsConfig.certPath; + return false; + } + tls.cert = certs.first(); + + QFile keyFile(_tlsConfig.keyPath); + if (!keyFile.open(QIODevice::ReadOnly)) { + qCritical().noquote() << "DCSSimController: cannot read private key" << _tlsConfig.keyPath; + return false; + } + const QByteArray pem = keyFile.readAll(); + tls.key = QSslKey(pem, QSsl::Rsa, QSsl::Pem); + if (tls.key.isNull()) { + tls.key = QSslKey(pem, QSsl::Ec, QSsl::Pem); + } + if (tls.key.isNull()) { + qCritical().noquote() << "DCSSimController: private key" << _tlsConfig.keyPath << "is not a PEM RSA or EC key"; + return false; + } + + if (!_tlsConfig.caPath.isEmpty()) { + tls.caCerts = QSslCertificate::fromPath(_tlsConfig.caPath); + if (tls.caCerts.isEmpty()) { + qCritical().noquote() << "DCSSimController: no CA certificates in" << _tlsConfig.caPath; + return false; + } + } + + _server.setTls(tls); + return true; +} + +/*! * \brief DCSSimController::onNewConnection * \details Accepts the pending connection, if another client is not already connect. */