diff options
Diffstat (limited to 'src/network/kernel/qdnslookup_unix.cpp')
-rw-r--r-- | src/network/kernel/qdnslookup_unix.cpp | 252 |
1 files changed, 171 insertions, 81 deletions
diff --git a/src/network/kernel/qdnslookup_unix.cpp b/src/network/kernel/qdnslookup_unix.cpp index 296c703423..9de073b781 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 @@ -52,16 +51,25 @@ static constexpr qsizetype QueryBufferSize = HFIXEDSZ + QFIXEDSZ + MAXCDNAME + 1 + sizeof(Edns0Record); using QueryBuffer = std::array<unsigned char, (QueryBufferSize + 15) / 16 * 16>; +namespace { +struct QDnsCachedName +{ + QString name; + int code = 0; + QDnsCachedName(const QString &name, int code) : name(name), code(code) {} +}; +} +Q_DECLARE_TYPEINFO(QDnsCachedName, Q_RELOCATABLE_TYPE); +using Cache = QList<QDnsCachedName>; // QHash or QMap are overkill + #if QT_CONFIG(res_setservers) // https://www.ibm.com/docs/en/i/7.3?topic=ssw_ibm_i_73/apis/ressetservers.html // https://docs.oracle.com/cd/E86824_01/html/E54774/res-setservers-3resolv.html static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port) { - if (!nameserver.isNull()) { - union res_sockaddr_union u; - setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port); - res_setservers(state, &u, 1); - } + union res_sockaddr_union u; + setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port); + res_setservers(state, &u, 1); return true; } #else @@ -112,9 +120,6 @@ template <typename State> bool setIpv6NameServer(State *, const void *, quint16) static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port) { - if (nameserver.isNull()) - return true; - state->nscount = 1; state->nsaddr_list[0].sin_family = AF_UNSPEC; if (nameserver.protocol() == QAbstractSocket::IPv6Protocol) @@ -142,51 +147,53 @@ 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) { - // 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 (!nameserver.isNull()) { + if (!applyNameServer(state, nameserver, port)) { + reply->setError(QDnsLookup::ResolverError, + QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS")); + return -1; + } - //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 + // Request the name server attempt to authenticate the reply. + reinterpret_cast<HEADER *>(buffer.data())->ad = true; - // Prepare the DNS query. - QueryBuffer qbuffer; - int queryLength = prepareQueryBuffer(&state, qbuffer, requestName, ns_rcode(requestType)); - if (Q_UNLIKELY(queryLength < 0)) - return reply->makeResolverSystemError(); +#ifdef RES_TRUSTAD + // Need to set this option even though we set the AD bit, otherwise + // glibc turns it off. + state->options |= RES_TRUSTAD; +#endif + } - // 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()); - if (responseLength < 0) { - // network error of some sort - if (errno == ETIMEDOUT) - reply->makeTimeoutError(); - else - reply->makeResolverSystemError(); - } - return responseLength; + int responseLength = res_nsend(state, qbuffer.data(), qbuffer.size(), buffer.data(), buffer.size()); + if (responseLength >= 0) + return responseLength; // success + + // libresolv uses ETIMEDOUT for resolver errors ("no answer") + if (errno == ECONNREFUSED) + reply->setError(QDnsLookup::ServerRefusedError, qt_error_string()); + else if (errno != ETIMEDOUT) + reply->makeResolverSystemError(); // some other error + + auto query = reinterpret_cast<HEADER *>(qbuffer.data()); + auto header = reinterpret_cast<HEADER *>(buffer.data()); + if (query->id == header->id && header->qr) + reply->makeDnsRcodeError(header->rcode); + else + reply->makeTimeoutError(); // must really be a timeout + return -1; }; // 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()); @@ -197,17 +204,65 @@ 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; } } + + // We only trust the AD bit in the reply if we're querying a custom name + // server or if we can tell the system administrator configured the resolver + // to trust replies. +#ifndef RES_TRUSTAD + if (nameserver.isNull()) + header->ad = false; +#endif + reply->authenticData = header->ad; + + 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; @@ -216,22 +271,46 @@ 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); - char host[PACKETSZ], answer[PACKETSZ]; qptrdiff offset = sizeof(HEADER); unsigned char *response = buffer.data(); int status; + auto expandHost = [&, cache = Cache{}](qptrdiff offset) mutable { + if (uchar n = response[offset]; n & NS_CMPRSFLGS) { + // compressed name, see if we already have it cached + if (offset + 1 < responseLength) { + int id = ((n & ~NS_CMPRSFLGS) << 8) | response[offset + 1]; + auto it = std::find_if(cache.constBegin(), cache.constEnd(), + [id](const QDnsCachedName &n) { return n.code == id; }); + if (it != cache.constEnd()) { + status = 2; + return it->name; + } + } + } + + // uncached, expand it + char host[MAXCDNAME + 1]; + status = dn_expand(response, response + responseLength, response + offset, + host, sizeof(host)); + if (status >= 0) + return cache.emplaceBack(decodeLabel(QLatin1StringView(host)), offset).name; + + // failed + reply->makeInvalidReplyError(QDnsLookup::tr("Could not expand domain name")); + return QString(); + }; + if (ntohs(header->qdcount) == 1) { // Skip the query host, type (2 bytes) and class (2 bytes). - status = dn_expand(response, response + responseLength, response + offset, host, sizeof(host)); - if (status < 0) { - reply->makeInvalidReplyError(QDnsLookup::tr("Could not expand domain name")); + expandHost(offset); + if (status < 0) return; - } - if (offset + status + 4 >= responseLength) + if (offset + status + 4 > responseLength) header->qdcount = 0xffff; // invalid reply below else offset += status + 4; @@ -243,12 +322,9 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) const int answerCount = ntohs(header->ancount); int answerIndex = 0; while ((offset < responseLength) && (answerIndex < answerCount)) { - status = dn_expand(response, response + responseLength, response + offset, host, sizeof(host)); - if (status < 0) { - reply->makeInvalidReplyError(QDnsLookup::tr("Could not expand domain name")); + const QString name = expandHost(offset); + if (status < 0) return; - } - const QString name = QUrl::fromAce(host); offset += status; if (offset + RRFIXEDSZ > responseLength) { @@ -283,58 +359,72 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) record.d->value = QHostAddress(response + offset); reply->hostAddressRecords.append(record); } else if (type == QDnsLookup::CNAME) { - status = dn_expand(response, response + responseLength, response + offset, answer, sizeof(answer)); - if (status < 0) - return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid canonical name record")); QDnsDomainNameRecord record; record.d->name = name; record.d->timeToLive = ttl; - record.d->value = QUrl::fromAce(answer); + record.d->value = expandHost(offset); + if (status < 0) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid canonical name record")); reply->canonicalNameRecords.append(record); } else if (type == QDnsLookup::NS) { - status = dn_expand(response, response + responseLength, response + offset, answer, sizeof(answer)); - if (status < 0) - return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid name server record")); QDnsDomainNameRecord record; record.d->name = name; record.d->timeToLive = ttl; - record.d->value = QUrl::fromAce(answer); + record.d->value = expandHost(offset); + if (status < 0) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid name server record")); reply->nameServerRecords.append(record); } else if (type == QDnsLookup::PTR) { - status = dn_expand(response, response + responseLength, response + offset, answer, sizeof(answer)); - if (status < 0) - return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid pointer record")); QDnsDomainNameRecord record; record.d->name = name; record.d->timeToLive = ttl; - record.d->value = QUrl::fromAce(answer); + record.d->value = expandHost(offset); + if (status < 0) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid pointer record")); reply->pointerRecords.append(record); } else if (type == QDnsLookup::MX) { const quint16 preference = qFromBigEndian<quint16>(response + offset); - status = dn_expand(response, response + responseLength, response + offset + 2, answer, sizeof(answer)); - if (status < 0) - return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid mail exchange record")); QDnsMailExchangeRecord record; - record.d->exchange = QUrl::fromAce(answer); + record.d->exchange = expandHost(offset + 2); record.d->name = name; record.d->preference = preference; record.d->timeToLive = ttl; + if (status < 0) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid mail exchange record")); reply->mailExchangeRecords.append(record); } else if (type == QDnsLookup::SRV) { + if (size < 7) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record")); const quint16 priority = qFromBigEndian<quint16>(response + offset); const quint16 weight = qFromBigEndian<quint16>(response + offset + 2); const quint16 port = qFromBigEndian<quint16>(response + offset + 4); - status = dn_expand(response, response + responseLength, response + offset + 6, answer, sizeof(answer)); - if (status < 0) - return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record")); QDnsServiceRecord record; record.d->name = name; - record.d->target = QUrl::fromAce(answer); + record.d->target = expandHost(offset + 6); record.d->port = port; record.d->priority = priority; record.d->timeToLive = ttl; record.d->weight = weight; + if (status < 0) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record")); reply->serviceRecords.append(record); + } else if (type == QDnsLookup::TLSA) { + // https://datatracker.ietf.org/doc/html/rfc6698#section-2.1 + if (size < 3) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid TLS association record")); + + const quint8 usage = response[offset]; + const quint8 selector = response[offset + 1]; + const quint8 matchType = response[offset + 2]; + + QDnsTlsAssociationRecord record; + record.d->name = name; + record.d->timeToLive = ttl; + record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage); + record.d->selector = QDnsTlsAssociationRecord::Selector(selector); + record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType); + record.d->value.assign(response + offset + 3, response + offset + size); + reply->tlsAssociationRecords.append(std::move(record)); } else if (type == QDnsLookup::TXT) { QDnsTextRecord record; record.d->name = name; |