diff options
author | Thiago Macieira <[email protected]> | 2024-05-06 14:17:26 -0700 |
---|---|---|
committer | Mårten Nordheim <[email protected]> | 2024-05-09 04:13:44 +0000 |
commit | f2f00b2a4632aaeb58e6cdae7faef9e0bafaff49 (patch) | |
tree | 1072ed17a0b43eb0b4dab8b6a31673459f839a4e | |
parent | 9724b039cac2cc309575ce5a030841939eeb1acd (diff) |
QDnsLookup: implement DNS-over-TLS
For the libresolv (Unix) implementation, we already had the packet
prepared by res_nmkquery(). This commit moves the res_nsend() to a
separate function so QDnsLookupRunnable::query() can be more concise.
On the Windows side, this commit creates a separate function for the DoT
case, because we now need to use two other functions from WinDNS so we
can create a query and parse the reply.
The rest is just QSslSocket.
Change-Id: I455fe22ef4ad4b2f9b01fffd17c805a3cb0466eb
Reviewed-by: Mårten Nordheim <[email protected]>
-rw-r--r-- | src/network/kernel/qdnslookup.cpp | 94 | ||||
-rw-r--r-- | src/network/kernel/qdnslookup_p.h | 4 | ||||
-rw-r--r-- | src/network/kernel/qdnslookup_unix.cpp | 96 | ||||
-rw-r--r-- | src/network/kernel/qdnslookup_win.cpp | 62 | ||||
-rw-r--r-- | tests/auto/network/kernel/qdnslookup/tst_qdnslookup.cpp | 87 |
5 files changed, 283 insertions, 60 deletions
diff --git a/src/network/kernel/qdnslookup.cpp b/src/network/kernel/qdnslookup.cpp index fedefe1e3b6..9a35bd3365a 100644 --- a/src/network/kernel/qdnslookup.cpp +++ b/src/network/kernel/qdnslookup.cpp @@ -8,14 +8,22 @@ #include <qapplicationstatic.h> #include <qcoreapplication.h> #include <qdatetime.h> +#include <qendian.h> #include <qloggingcategory.h> #include <qrandom.h> +#include <qspan.h> #include <qurl.h> +#if QT_CONFIG(ssl) +# include <qsslsocket.h> +#endif + #include <algorithm> QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + static Q_LOGGING_CATEGORY(lcDnsLookup, "qt.network.dnslookup", QtCriticalMsg) namespace { @@ -261,6 +269,10 @@ bool QDnsLookup::isProtocolSupported(Protocol protocol) case QDnsLookup::Standard: return true; case QDnsLookup::DnsOverTls: +# if QT_CONFIG(ssl) + if (QSslSocket::supportsSsl()) + return true; +# endif return false; } #else @@ -658,7 +670,8 @@ QList<QDnsTextRecord> QDnsLookup::textRecords() const */ void QDnsLookup::setSslConfiguration(const QSslConfiguration &sslConfiguration) { - Q_UNUSED(sslConfiguration) + Q_D(QDnsLookup); + d->sslConfiguration.emplace(sslConfiguration); } /*! @@ -668,7 +681,8 @@ void QDnsLookup::setSslConfiguration(const QSslConfiguration &sslConfiguration) */ QSslConfiguration QDnsLookup::sslConfiguration() const { - return {}; + const Q_D(QDnsLookup); + return d->sslConfiguration.value_or(QSslConfiguration::defaultConfiguration()); } #endif @@ -1291,6 +1305,82 @@ inline QDebug operator<<(QDebug &d, QDnsLookupRunnable *r) return d; } +#if QT_CONFIG(ssl) +static constexpr std::chrono::milliseconds DnsOverTlsConnectTimeout(15'000); +static constexpr std::chrono::milliseconds DnsOverTlsTimeout(120'000); + +static int makeReplyErrorFromSocket(QDnsLookupReply *reply, const QAbstractSocket *socket) +{ + QDnsLookup::Error error = [&] { + switch (socket->error()) { + case QAbstractSocket::SocketTimeoutError: + case QAbstractSocket::ProxyConnectionTimeoutError: + return QDnsLookup::TimeoutError; + default: + return QDnsLookup::ResolverError; + } + }(); + reply->setError(error, socket->errorString()); + return false; +} + +bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, + ReplyBuffer &response) +{ + QSslSocket socket; + socket.setSslConfiguration(sslConfiguration.value_or(QSslConfiguration::defaultConfiguration())); + +# if QT_CONFIG(networkproxy) + socket.setProtocolTag("domain-s"_L1); +# endif + + do { + quint16 size = qToBigEndian<quint16>(query.size()); + QDeadlineTimer timeout(DnsOverTlsTimeout); + + socket.connectToHostEncrypted(nameserver.toString(), port); + socket.write(reinterpret_cast<const char *>(&size), sizeof(size)); + socket.write(reinterpret_cast<const char *>(query.data()), query.size()); + if (!socket.waitForEncrypted(DnsOverTlsConnectTimeout.count())) + break; + + reply->sslConfiguration = socket.sslConfiguration(); + + // accumulate reply + auto waitForBytes = [&](void *buffer, int count) { + int remaining = timeout.remainingTime(); + while (remaining >= 0 && socket.bytesAvailable() < count) { + if (!socket.waitForReadyRead(remaining)) + return false; + } + return socket.read(static_cast<char *>(buffer), count) == count; + }; + if (!waitForBytes(&size, sizeof(size))) + break; + + // note: strictly speaking, we're allocating memory based on untrusted data + // but in practice, due to limited range of the data type (16 bits), + // the maximum allocation is small. + size = qFromBigEndian(size); + response.resize(size); + if (waitForBytes(response.data(), size)) + return true; + } while (false); + + // handle errors + return makeReplyErrorFromSocket(reply, &socket); +} +#else +bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, + ReplyBuffer &response) +{ + Q_UNUSED(query) + Q_UNUSED(response) + reply->setError(QDnsLookup::ResolverError, QDnsLookup::tr("SSL/TLS support not present")); + return false; +} +#endif + QT_END_NAMESPACE #include "moc_qdnslookup.cpp" diff --git a/src/network/kernel/qdnslookup_p.h b/src/network/kernel/qdnslookup_p.h index dc181d649d1..574b279fe6b 100644 --- a/src/network/kernel/qdnslookup_p.h +++ b/src/network/kernel/qdnslookup_p.h @@ -201,9 +201,13 @@ public: #else using EncodedLabel = QByteArray; #endif + // minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8) + static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8; + using ReplyBuffer = QVarLengthArray<unsigned char, ReplyBufferSize>; QDnsLookupRunnable(const QDnsLookupPrivate *d); void run() override; + bool sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, ReplyBuffer &response); signals: void finished(const QDnsLookupReply &reply); diff --git a/src/network/kernel/qdnslookup_unix.cpp b/src/network/kernel/qdnslookup_unix.cpp index 12482ae3092..7a283d4f7fd 100644 --- a/src/network/kernel/qdnslookup_unix.cpp +++ b/src/network/kernel/qdnslookup_unix.cpp @@ -6,6 +6,7 @@ #include <qendian.h> #include <qscopedpointer.h> +#include <qspan.h> #include <qurl.h> #include <qvarlengtharray.h> #include <private/qnativesocketengine_p.h> // for setSockAddr @@ -32,15 +33,13 @@ QT_REQUIRE_CONFIG(libresolv); QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; - -// minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8) -static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8; +using ReplyBuffer = QDnsLookupRunnable::ReplyBuffer; // https://www.rfc-editor.org/rfc/rfc6891 static constexpr unsigned char Edns0Record[] = { 0x00, // root label T_OPT >> 8, T_OPT & 0xff, // type OPT - ReplyBufferSize >> 8, ReplyBufferSize & 0xff, // payload size + ReplyBuffer::PreallocatedSize >> 8, ReplyBuffer::PreallocatedSize & 0xff, // payload size NOERROR, // extended rcode 0, // version 0x00, 0x00, // flags @@ -153,40 +152,19 @@ prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_r return queryLength + sizeof(Edns0Record); } -void QDnsLookupRunnable::query(QDnsLookupReply *reply) +static int sendStandardDns(QDnsLookupReply *reply, res_state state, QSpan<unsigned char> qbuffer, + ReplyBuffer &buffer, const QHostAddress &nameserver, quint16 port) { - if (protocol != QDnsLookup::Standard) - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("DNS over TLS not implemented")); - - // Initialize state. - std::remove_pointer_t<res_state> state = {}; - if (res_ninit(&state) < 0) { - int error = errno; - qErrnoWarning(error, "QDnsLookup: Resolver initialization failed"); - return reply->makeResolverSystemError(error); - } - auto guard = qScopeGuard([&] { res_nclose(&state); }); - //Check if a nameserver was set. If so, use it - if (!applyNameServer(&state, nameserver, port)) - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS")); -#ifdef QDNSLOOKUP_DEBUG - state.options |= RES_DEBUG; -#endif - - // Prepare the DNS query. - QueryBuffer qbuffer; - int queryLength = prepareQueryBuffer(&state, qbuffer, requestName.constData(), ns_rcode(requestType)); - if (Q_UNLIKELY(queryLength < 0)) - return reply->makeResolverSystemError(); + if (!applyNameServer(state, nameserver, port)) { + reply->setError(QDnsLookup::ResolverError, + QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS")); + return -1; + } - // Perform DNS query. - QVarLengthArray<unsigned char, ReplyBufferSize> buffer(ReplyBufferSize); auto attemptToSend = [&]() { std::memset(buffer.data(), 0, HFIXEDSZ); // the header is enough - int responseLength = res_nsend(&state, qbuffer.data(), queryLength, buffer.data(), buffer.size()); + int responseLength = res_nsend(state, qbuffer.data(), qbuffer.size(), buffer.data(), buffer.size()); if (responseLength >= 0) return responseLength; // success @@ -206,10 +184,10 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) }; // strictly use UDP, we'll deal with truncated replies ourselves - state.options |= RES_IGNTC; + state->options |= RES_IGNTC; int responseLength = attemptToSend(); if (responseLength < 0) - return; + return responseLength; // check if we need to use the virtual circuit (TCP) auto header = reinterpret_cast<HEADER *>(buffer.data()); @@ -220,17 +198,56 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) // remove the EDNS record in the query reinterpret_cast<HEADER *>(qbuffer.data())->arcount = 0; - queryLength -= sizeof(Edns0Record); + qbuffer = qbuffer.first(qbuffer.size() - sizeof(Edns0Record)); // send using the virtual circuit - state.options |= RES_USEVC; + state->options |= RES_USEVC; responseLength = attemptToSend(); if (Q_UNLIKELY(responseLength > buffer.size())) { // Ok, we give up. - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("Reply was too large")); + reply->setError(QDnsLookup::ResolverError, QDnsLookup::tr("Reply was too large")); + return -1; } } + + return responseLength; +} + +void QDnsLookupRunnable::query(QDnsLookupReply *reply) +{ + // Initialize state. + std::remove_pointer_t<res_state> state = {}; + if (res_ninit(&state) < 0) { + int error = errno; + qErrnoWarning(error, "QDnsLookup: Resolver initialization failed"); + return reply->makeResolverSystemError(error); + } + auto guard = qScopeGuard([&] { res_nclose(&state); }); + +#ifdef QDNSLOOKUP_DEBUG + state.options |= RES_DEBUG; +#endif + + // Prepare the DNS query. + QueryBuffer qbuffer; + int queryLength = prepareQueryBuffer(&state, qbuffer, requestName.constData(), ns_rcode(requestType)); + if (Q_UNLIKELY(queryLength < 0)) + return reply->makeResolverSystemError(); + + // Perform DNS query. + ReplyBuffer buffer(ReplyBufferSize); + int responseLength = -1; + switch (protocol) { + case QDnsLookup::Standard: + responseLength = sendStandardDns(reply, &state, qbuffer, buffer, nameserver, port); + break; + case QDnsLookup::DnsOverTls: + if (!sendDnsOverTls(reply, qbuffer, buffer)) + return; + responseLength = buffer.size(); + break; + } + if (responseLength < 0) return; @@ -239,6 +256,7 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) return reply->makeInvalidReplyError(); // Parse the reply. + auto header = reinterpret_cast<HEADER *>(buffer.data()); if (header->rcode) return reply->makeDnsRcodeError(header->rcode); diff --git a/src/network/kernel/qdnslookup_win.cpp b/src/network/kernel/qdnslookup_win.cpp index a99655f403f..3e1b76a0b01 100644 --- a/src/network/kernel/qdnslookup_win.cpp +++ b/src/network/kernel/qdnslookup_win.cpp @@ -5,9 +5,11 @@ #include <winsock2.h> #include "qdnslookup_p.h" -#include <qurl.h> +#include <qendian.h> #include <private/qnativesocketengine_p.h> #include <private/qsystemerror_p.h> +#include <qurl.h> +#include <qspan.h> #include <qt_windows.h> #include <windns.h> @@ -63,6 +65,58 @@ DNS_STATUS WINAPI DnsQueryEx(PDNS_QUERY_REQUEST pQueryRequest, QT_BEGIN_NAMESPACE +static DNS_STATUS sendAlternate(QDnsLookupRunnable *self, QDnsLookupReply *reply, + PDNS_QUERY_REQUEST request, PDNS_QUERY_RESULT results) +{ + // WinDNS wants MTU - IP Header - UDP header for some reason, in spite + // of never needing that much + QVarLengthArray<unsigned char, 1472> query(1472); + + auto dnsBuffer = new (query.data()) DNS_MESSAGE_BUFFER; + DWORD dnsBufferSize = query.size(); + WORD xid = 0; + bool recursionDesired = true; + + SetLastError(ERROR_SUCCESS); + + // MinGW winheaders incorrectly declare the third parameter as LPWSTR + if (!DnsWriteQuestionToBuffer_W(dnsBuffer, &dnsBufferSize, + const_cast<LPWSTR>(request->QueryName), request->QueryType, + xid, recursionDesired)) { + // let's try reallocating + query.resize(dnsBufferSize); + if (!DnsWriteQuestionToBuffer_W(dnsBuffer, &dnsBufferSize, + const_cast<LPWSTR>(request->QueryName), request->QueryType, + xid, recursionDesired)) { + return GetLastError(); + } + } + + // set AD bit: we want to trust this server + dnsBuffer->MessageHead.AuthenticatedData = true; + + QDnsLookupRunnable::ReplyBuffer replyBuffer; + if (!self->sendDnsOverTls(reply, { query.data(), dnsBufferSize }, replyBuffer)) + return DNS_STATUS(-1); // error set in reply + + // interpret the RCODE in the reply + auto response = reinterpret_cast<PDNS_MESSAGE_BUFFER>(replyBuffer.data()); + DNS_HEADER *header = &response->MessageHead; + if (!header->IsResponse) + return DNS_ERROR_BAD_PACKET; // not a reply + + // Convert the byte order for the 16-bit quantities in the header, so + // DnsExtractRecordsFromMessage can parse the contents. + //header->Xid = qFromBigEndian(header->Xid); + header->QuestionCount = qFromBigEndian(header->QuestionCount); + header->AnswerCount = qFromBigEndian(header->AnswerCount); + header->NameServerCount = qFromBigEndian(header->NameServerCount); + header->AdditionalCount = qFromBigEndian(header->AdditionalCount); + + results->QueryOptions = request->QueryOptions; + return DnsExtractRecordsFromMessage_W(response, replyBuffer.size(), &results->pQueryRecords); +} + void QDnsLookupRunnable::query(QDnsLookupReply *reply) { // Perform DNS query. @@ -93,10 +147,12 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) status = DnsQueryEx(&request, &results, nullptr); break; case QDnsLookup::DnsOverTls: - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("DNS over TLS not implemented")); + status = sendAlternate(this, reply, &request, &results); + break; } + if (status == DNS_STATUS(-1)) + return; // error already set in reply if (status >= DNS_ERROR_RCODE_FORMAT_ERROR && status <= DNS_ERROR_RCODE_LAST) return reply->makeDnsRcodeError(status - DNS_ERROR_RCODE_FORMAT_ERROR + 1); else if (status == ERROR_TIMEOUT) diff --git a/tests/auto/network/kernel/qdnslookup/tst_qdnslookup.cpp b/tests/auto/network/kernel/qdnslookup/tst_qdnslookup.cpp index 2723caa3b8a..058bfb341e5 100644 --- a/tests/auto/network/kernel/qdnslookup/tst_qdnslookup.cpp +++ b/tests/auto/network/kernel/qdnslookup/tst_qdnslookup.cpp @@ -13,6 +13,13 @@ #include <QtNetwork/QNetworkDatagram> #include <QtNetwork/QUdpSocket> +#if QT_CONFIG(networkproxy) +# include <QtNetwork/QNetworkProxyFactory> +#endif +#if QT_CONFIG(ssl) +# include <QtNetwork/QSslSocket> +#endif + #ifdef Q_OS_UNIX # include <QtCore/QFile> #else @@ -135,9 +142,57 @@ static QList<QHostAddress> globalPublicNameservers(QDnsLookup::Protocol proto) //"9.9.9.9", "2620:fe::9", }; + auto udpSendAndReceive = [](const QHostAddress &addr, QByteArray &data) { + QUdpSocket socket; + socket.connectToHost(addr, 53); + if (socket.waitForConnected(1)) + socket.write(data); + + if (!socket.waitForReadyRead(1000)) + return socket.errorString(); + + QNetworkDatagram dgram = socket.receiveDatagram(); + if (!dgram.isValid()) + return socket.errorString(); + + data = dgram.data(); + return QString(); + }; + + auto tlsSendAndReceive = [](const QHostAddress &addr, QByteArray &data) { +#if QT_CONFIG(ssl) + QSslSocket socket; + QDeadlineTimer timeout(2000); + socket.connectToHostEncrypted(addr.toString(), 853); + if (!socket.waitForEncrypted(2000)) + return socket.errorString(); + + quint16 size = qToBigEndian<quint16>(data.size()); + socket.write(reinterpret_cast<char *>(&size), sizeof(size)); + socket.write(data); + + if (!socket.waitForReadyRead(timeout.remainingTime())) + return socket.errorString(); + if (socket.bytesAvailable() < 2) + return u"protocol error"_s; + + socket.read(reinterpret_cast<char *>(&size), sizeof(size)); + size = qFromBigEndian(size); + + while (socket.bytesAvailable() < size) { + int remaining = timeout.remainingTime(); + if (remaining < 0 || !socket.waitForReadyRead(remaining)) + return socket.errorString(); + } + + data = socket.readAll(); + return QString(); +#else + return u"SSL/TLS support not compiled in"_s; +#endif + }; + QList<QHostAddress> result; - if (proto != QDnsLookup::Standard) - return result; QRandomGenerator &rng = *QRandomGenerator::system(); for (auto name : candidates) { // check the candidates for reachability @@ -147,23 +202,18 @@ static QList<QHostAddress> globalPublicNameservers(QDnsLookup::Protocol proto) char *ptr = data.data(); qToBigEndian(id, ptr); - QUdpSocket socket; - socket.connectToHost(addr, 53); - if (socket.waitForConnected(1)) - socket.write(data); - - if (!socket.waitForReadyRead(1000)) { - qDebug() << addr << "discarded:" << socket.errorString(); - continue; - } - - QNetworkDatagram dgram = socket.receiveDatagram(); - if (!dgram.isValid()) { - qDebug() << addr << "discarded:" << socket.errorString(); + QString errorString = [&] { + switch (proto) { + case QDnsLookup::Standard: return udpSendAndReceive(addr, data); + case QDnsLookup::DnsOverTls: return tlsSendAndReceive(addr, data); + } + Q_UNREACHABLE(); + }(); + if (!errorString.isEmpty()) { + qDebug() << addr << "discarded:" << errorString; continue; } - data = dgram.data(); ptr = data.data(); if (data.size() < HeaderSize) { qDebug() << addr << "discarded: reply too small"; @@ -190,6 +240,11 @@ void tst_QDnsLookup::initTestCase() { if (qgetenv("QTEST_ENVIRONMENT") == "ci") dnsServersMustWork = true; + +#if QT_CONFIG(networkproxy) + // for DNS-over-TLS + QNetworkProxyFactory::setUseSystemConfiguration(true); +#endif } QString tst_QDnsLookup::domainName(const QString &input) |