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 <marten.nordheim@qt.io>
This commit is contained in:
Thiago Macieira 2024-05-06 14:17:26 -07:00 committed by Mårten Nordheim
parent 9724b039ca
commit f2f00b2a46
5 changed files with 283 additions and 60 deletions

View File

@ -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"

View File

@ -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);

View File

@ -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,
if (!applyNameServer(state, nameserver, port)) {
reply->setError(QDnsLookup::ResolverError,
QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS"));
#ifdef QDNSLOOKUP_DEBUG
state.options |= RES_DEBUG;
#endif
return -1;
}
// 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.
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);

View File

@ -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)

View File

@ -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();
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;
}
QNetworkDatagram dgram = socket.receiveDatagram();
if (!dgram.isValid()) {
qDebug() << addr << "discarded:" << socket.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)