It does set ECONNREFUSED explicitly. So let's reuse ServerRefusedError for it too (it was so far only used when we got a REFUSED DNS answer), which allows tst_QDnsLookup to handle this as an ignorable failure. For almost everything else, it sets ETIMEDOUT to indicate "no answer", which isn't useful. So let's inspect the reply to see if it has something. Change-Id: I455fe22ef4ad4b2f9b01fffd17c771ffa4a998be Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io> (cherry picked from commit f19e9f2521ff7784223ec34fc6794583f4faa2a5) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
393 lines
15 KiB
C++
393 lines
15 KiB
C++
// Copyright (C) 2012 Jeremy Lainé <jeremy.laine@m4x.org>
|
|
// Copyright (C) 2023 Intel Corporation.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
|
|
#include "qdnslookup_p.h"
|
|
|
|
#include <qendian.h>
|
|
#include <qscopedpointer.h>
|
|
#include <qurl.h>
|
|
#include <qvarlengtharray.h>
|
|
#include <private/qnativesocketengine_p.h> // for setSockAddr
|
|
#include <private/qtnetwork-config_p.h>
|
|
|
|
QT_REQUIRE_CONFIG(libresolv);
|
|
|
|
#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>
|
|
|
|
#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;
|
|
|
|
// minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8)
|
|
static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8;
|
|
|
|
// 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
|
|
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(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);
|
|
}
|
|
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
|
|
// See: https://sourceware.org/ml/libc-hacker/2002-05/msg00035.html
|
|
// Unneeded since glibc 2.22 (2015), but doesn't hurt to set it
|
|
// See: https://sourceware.org/git/?p=glibc.git;a=commit;h=2212c1420c92a33b0e0bd9a34938c9814a56c0f7
|
|
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)
|
|
{
|
|
if (nameserver.isNull())
|
|
return true;
|
|
|
|
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);
|
|
}
|
|
|
|
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); });
|
|
|
|
//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, 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());
|
|
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;
|
|
|
|
// 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;
|
|
queryLength -= sizeof(Edns0Record);
|
|
|
|
// send using the virtual circuit
|
|
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"));
|
|
}
|
|
}
|
|
if (responseLength < 0)
|
|
return;
|
|
|
|
// Check the reply is valid.
|
|
if (responseLength < int(sizeof(HEADER)))
|
|
return reply->makeInvalidReplyError();
|
|
|
|
// Parse the reply.
|
|
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) {
|
|
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::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
|