Revision control
Copy as Markdown
// Copyright (C) 2012 Jeremy Lainé <jeremy.laine@m4x.org>
// Copyright (C) 2023 Intel Corporation.
// Copyright (C) 2026 Linus Jahn <lnj@kaidan.im>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qdnslookup_p.h"
#include <qendian.h>
#include <qrandom.h>
#include <qspan.h>
#include <qurl.h>
#include <qvarlengtharray.h>
#include <private/qnativesocketengine_p.h> // for setSockAddr
#include <private/qtnetwork-config_p.h>
// Three backends: libresolv on Unix, Android's DnsResolver (bionic has no
// libresolv) and HarmonyOS' stateless resolver (musl has no res_n* functions).
// They share everything but the transport.
#if !QT_CONFIG(libresolv) && !QT_CONFIG(android_dnsresolver) \
&& !QT_CONFIG(harmony_dnsresolver)
# error "This file requires libresolv, Android's DnsResolver or the HarmonyOS stateless resolver"
#endif
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#if __has_include(<arpa/nameser_compat.h>)
# include <arpa/nameser_compat.h>
#endif
#include <errno.h>
#include <resolv.h>
#if QT_CONFIG(android_dnsresolver)
# include <android/multinetwork.h>
#endif
#include <array>
#ifndef T_OPT
// the older arpa/nameser_compat.h wasn't updated between 1999 and 2016 in glibc
# define T_OPT ns_t_opt
#endif
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using ReplyBuffer = QDnsLookupRunnable::ReplyBuffer;
static constexpr unsigned char Edns0Record[] = {
0x00, // root label
T_OPT >> 8, T_OPT & 0xff, // type OPT
ReplyBuffer::PreallocatedSize >> 8, ReplyBuffer::PreallocatedSize & 0xff, // payload size
NOERROR, // extended rcode
0, // version
0x00, 0x00, // flags
0x00, 0x00, // option length
};
// maximum length of a EDNS0 query with a 255-character domain (rounded up to 16)
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(libresolv)
#if QT_CONFIG(res_setservers)
static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
{
union res_sockaddr_union u;
setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port);
res_setservers(state, &u, 1);
return true;
}
#else
template <typename T> void setNsMap(T &ext, std::enable_if_t<sizeof(T::nsmap) != 0, uint16_t> v)
{
// Set nsmap[] to indicate that nsaddrs[0] is an IPv6 address
// Unneeded since glibc 2.22 (2015), but doesn't hurt to set it
ext.nsmap[0] = v;
}
template <typename T> void setNsMap(T &, ...)
{
// fallback
}
template <bool Condition>
using EnableIfIPv6 = std::enable_if_t<Condition, const QHostAddress *>;
template <typename State>
bool setIpv6NameServer(State *state,
EnableIfIPv6<sizeof(std::declval<State>()._u._ext.nsaddrs) != 0> addr,
quint16 port)
{
// glibc-like API to set IPv6 name servers
struct sockaddr_in6 *ns = state->_u._ext.nsaddrs[0];
// nsaddrs will be NULL if no nameserver is set in /etc/resolv.conf
if (!ns) {
// Memory allocated here will be free()'d in res_close() as we
// have done res_init() above.
ns = static_cast<struct sockaddr_in6*>(calloc(1, sizeof(struct sockaddr_in6)));
Q_CHECK_PTR(ns);
state->_u._ext.nsaddrs[0] = ns;
}
setNsMap(state->_u._ext, MAXNS + 1);
state->_u._ext.nscount6 = 1;
setSockaddr(ns, *addr, port);
return true;
}
template <typename State> bool setIpv6NameServer(State *, const void *, quint16)
{
// fallback
return false;
}
static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
{
state->nscount = 1;
state->nsaddr_list[0].sin_family = AF_UNSPEC;
if (nameserver.protocol() == QAbstractSocket::IPv6Protocol)
return setIpv6NameServer(state, &nameserver, port);
setSockaddr(&state->nsaddr_list[0], nameserver, port);
return true;
}
#endif // !QT_CONFIG(res_setservers)
static int
prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_rcode type)
{
// Create header and our query
int queryLength = res_nmkquery(state, QUERY, label, C_IN, type, nullptr, 0, nullptr,
buffer.data(), buffer.size());
Q_ASSERT(queryLength < int(buffer.size()));
if (Q_UNLIKELY(queryLength < 0))
return queryLength;
// Append EDNS0 record and set the number of additional RRs to 1
Q_ASSERT(queryLength + sizeof(Edns0Record) < buffer.size());
std::copy_n(std::begin(Edns0Record), sizeof(Edns0Record), buffer.begin() + queryLength);
reinterpret_cast<HEADER *>(buffer.data())->arcount = qToBigEndian<quint16>(1);
return queryLength + sizeof(Edns0Record);
}
static int sendStandardDns(QDnsLookupReply *reply, res_state state, QSpan<unsigned char> qbuffer,
ReplyBuffer &buffer, const QHostAddress &nameserver, quint16 port)
{
// 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;
}
// Request the name server attempt to authenticate the reply.
reinterpret_cast<HEADER *>(buffer.data())->ad = true;
#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
}
auto attemptToSend = [&]() {
std::memset(buffer.data(), 0, HFIXEDSZ); // the header is enough
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;
int responseLength = attemptToSend();
if (responseLength < 0)
return responseLength;
// check if we need to use the virtual circuit (TCP)
auto header = reinterpret_cast<HEADER *>(buffer.data());
if (header->rcode == NOERROR && header->tc) {
// yes, increase our buffer size
buffer.resize(std::numeric_limits<quint16>::max());
header = reinterpret_cast<HEADER *>(buffer.data());
// remove the EDNS record in the query
reinterpret_cast<HEADER *>(qbuffer.data())->arcount = 0;
qbuffer = qbuffer.first(qbuffer.size() - sizeof(Edns0Record));
// send using the virtual circuit
state->options |= RES_USEVC;
responseLength = attemptToSend();
if (Q_UNLIKELY(responseLength > buffer.size())) {
// Ok, we give up.
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.
QSpan query(qbuffer.data(), queryLength);
ReplyBuffer buffer(ReplyBufferSize);
int responseLength = -1;
switch (protocol) {
case QDnsLookup::Standard:
responseLength = sendStandardDns(reply, &state, query, buffer, nameserver, port);
break;
case QDnsLookup::DnsOverTls:
if (!sendDnsOverTls(reply, query, buffer))
return;
responseLength = buffer.size();
break;
}
if (responseLength < 0)
return;
parseResponse(reply, buffer, responseLength);
}
#elif QT_CONFIG(android_dnsresolver) || QT_CONFIG(harmony_dnsresolver)
// Neither bionic nor musl exports res_nmkquery(), so we assemble the query
// ourselves.
static int prepareQueryBuffer(QueryBuffer &buffer, const char *label, ns_type type)
{
std::memset(buffer.data(), 0, HFIXEDSZ);
auto header = new (buffer.data()) HEADER;
header->id = QRandomGenerator::system()->generate();
header->opcode = QUERY;
header->rd = true; // recursion desired
header->qdcount = qToBigEndian<quint16>(1);
header->arcount = qToBigEndian<quint16>(1); // EDNS0 record
// the question: QNAME, QTYPE, QCLASS; a single-question query has nothing
// to compress against, so dn_comp() gets no compression pointers
unsigned char *ptr = buffer.data() + HFIXEDSZ;
int labelLength = dn_comp(label, ptr,
int(buffer.size() - HFIXEDSZ - QFIXEDSZ - sizeof(Edns0Record)),
nullptr, nullptr);
if (Q_UNLIKELY(labelLength < 0))
return labelLength;
ptr += labelLength;
qToBigEndian<quint16>(type, ptr);
qToBigEndian<quint16>(C_IN, ptr + sizeof(quint16));
ptr += QFIXEDSZ;
Q_ASSERT(ptr + sizeof(Edns0Record) <= buffer.data() + buffer.size());
ptr = std::copy_n(std::begin(Edns0Record), sizeof(Edns0Record), ptr);
return ptr - buffer.data();
}
#if QT_CONFIG(android_dnsresolver)
// These are API level 29, while Qt still supports 28, where the NDK headers
// mark them unavailable; weakrefs are resolved to null if they are missing.
static int local_res_nsend(net_handle_t network, const uint8_t *msg, size_t msglen, uint32_t flags)
__attribute__((weakref("android_res_nsend")));
static int local_res_nresult(int fd, int *rcode, uint8_t *answer, size_t anslen)
__attribute__((weakref("android_res_nresult")));
bool qt_androidDnsResolverAvailable()
{
return local_res_nsend && local_res_nresult;
}
static int sendStandardDns(QDnsLookupReply *reply, QSpan<unsigned char> qbuffer,
ReplyBuffer &buffer)
{
if (!qt_androidDnsResolverAvailable()) {
reply->setError(QDnsLookup::ResolverError,
QDnsLookup::tr("DNS lookups require Android 10 or later"));
return -1;
}
// unused: the rcode is also in the reply header, where parseResponse() finds it
int rcode = 0;
auto attemptToSend = [&]() {
int fd = local_res_nsend(NETWORK_UNSPECIFIED, qbuffer.data(), qbuffer.size(), 0);
if (fd < 0)
return fd;
// android_res_nresult() closes the file descriptor in all cases
return local_res_nresult(fd, &rcode, buffer.data(), buffer.size());
};
int responseLength = attemptToSend();
if (responseLength == -EMSGSIZE) {
// The reply did not fit (was truncated) and the resolver closed the
// socket, so ask again with a buffer that any DNS message fits into.
// The repeated query will normally be answered from the resolver's cache.
buffer.resize(std::numeric_limits<quint16>::max());
responseLength = attemptToSend();
}
if (responseLength < 0) {
if (responseLength == -ETIMEDOUT)
reply->makeTimeoutError();
else
reply->makeResolverSystemError(-responseLength);
return -1;
}
return responseLength;
}
#endif // QT_CONFIG(android_dnsresolver)
#if QT_CONFIG(harmony_dnsresolver)
static int sendStandardDns(QDnsLookupReply *reply, QSpan<unsigned char> qbuffer,
ReplyBuffer &buffer)
{
auto attemptToSend = [&]() {
std::memset(buffer.data(), 0, HFIXEDSZ);
int responseLength = res_send(qbuffer.data(), qbuffer.size(), buffer.data(), buffer.size());
if (responseLength >= 0)
return responseLength;
if (errno == ECONNREFUSED)
reply->setError(QDnsLookup::ServerRefusedError, qt_error_string());
else if (errno != EAGAIN && errno != ETIMEDOUT)
reply->makeResolverSystemError();
auto query = reinterpret_cast<const HEADER *>(qbuffer.data());
auto header = reinterpret_cast<const HEADER *>(buffer.data());
if (query->id == header->id && header->qr)
reply->makeDnsRcodeError(header->rcode);
else
reply->makeTimeoutError();
return -1;
};
int responseLength = attemptToSend();
if (responseLength > buffer.size()) {
buffer.resize(std::numeric_limits<quint16>::max());
responseLength = attemptToSend();
if (Q_UNLIKELY(responseLength > buffer.size())) {
reply->setError(QDnsLookup::ResolverError, QDnsLookup::tr("Reply was too large"));
return -1;
}
}
return responseLength;
}
#endif // QT_CONFIG(harmony_dnsresolver)
void QDnsLookupRunnable::query(QDnsLookupReply *reply)
{
// Prepare the DNS query.
QueryBuffer qbuffer;
int queryLength = prepareQueryBuffer(qbuffer, requestName.constData(), ns_type(requestType));
if (Q_UNLIKELY(queryLength < 0))
return reply->setError(QDnsLookup::InvalidRequestError,
QDnsLookup::tr("Invalid domain name"));
// Perform DNS query.
QSpan query(qbuffer.data(), queryLength);
ReplyBuffer buffer(ReplyBufferSize);
int responseLength = -1;
switch (protocol) {
case QDnsLookup::Standard:
if (!nameserver.isNull()) {
reply->setError(QDnsLookup::ResolverError,
QDnsLookup::tr("Setting a nameserver is currently not supported on this OS"));
return;
}
responseLength = sendStandardDns(reply, query, buffer);
// We can't tell whether the system resolver validated the reply, so don't
// pass on the AD bit (see the RES_TRUSTAD handling above).
if (responseLength >= int(sizeof(HEADER)))
reinterpret_cast<HEADER *>(buffer.data())->ad = false;
break;
case QDnsLookup::DnsOverTls:
if (!sendDnsOverTls(reply, query, buffer))
return;
responseLength = buffer.size();
break;
}
if (responseLength < 0)
return;
parseResponse(reply, buffer, responseLength);
}
#endif // resolver backend selection
void QDnsLookupRunnable::parseResponse(QDnsLookupReply *reply, ReplyBuffer &buffer,
int responseLength)
{
// Check the reply is valid.
if (responseLength < int(sizeof(HEADER)))
return reply->makeInvalidReplyError();
// Parse the reply.
auto header = reinterpret_cast<HEADER *>(buffer.data());
if (header->rcode)
return reply->makeDnsRcodeError(header->rcode);
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).
expandHost(offset);
if (status < 0)
return;
if (offset + status + 4 > responseLength)
header->qdcount = 0xffff; // invalid reply below
else
offset += status + 4;
}
if (ntohs(header->qdcount) > 1)
return reply->makeInvalidReplyError();
// Extract results.
const int answerCount = ntohs(header->ancount);
int answerIndex = 0;
while ((offset < responseLength) && (answerIndex < answerCount)) {
const QString name = expandHost(offset);
if (status < 0)
return;
offset += status;
if (offset + RRFIXEDSZ > responseLength) {
// probably just a truncated reply, return what we have
return;
}
const quint16 type = qFromBigEndian<quint16>(response + offset);
const qint16 rrclass = qFromBigEndian<quint16>(response + offset + 2);
const quint32 ttl = qFromBigEndian<quint32>(response + offset + 4);
const quint16 size = qFromBigEndian<quint16>(response + offset + 8);
offset += RRFIXEDSZ;
if (offset + size > responseLength)
return; // truncated
if (rrclass != C_IN)
continue;
if (type == QDnsLookup::A) {
if (size != 4)
return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid IPv4 address record"));
const quint32 addr = qFromBigEndian<quint32>(response + offset);
QDnsHostAddressRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
record.d->value = QHostAddress(addr);
reply->hostAddressRecords.append(record);
} else if (type == QDnsLookup::AAAA) {
if (size != 16)
return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid IPv6 address record"));
QDnsHostAddressRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
record.d->value = QHostAddress(response + offset);
reply->hostAddressRecords.append(record);
} else if (type == QDnsLookup::CNAME) {
QDnsDomainNameRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
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) {
QDnsDomainNameRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
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) {
QDnsDomainNameRecord record;
record.d->name = name;
record.d->timeToLive = ttl;
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);
QDnsMailExchangeRecord record;
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);
QDnsServiceRecord record;
record.d->name = name;
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) {
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;
record.d->timeToLive = ttl;
qptrdiff txt = offset;
while (txt < offset + size) {
const unsigned char length = response[txt];
txt++;
if (txt + length > offset + size)
return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid text record"));
record.d->values << QByteArrayView(response + txt, length).toByteArray();
txt += length;
}
reply->textRecords.append(record);
}
offset += size;
answerIndex++;
}
}
QT_END_NAMESPACE