QDnsLookup/Unix: modernize the setting of IPv6 server addresses

Instead of using #if, this now uses SFINAE to detect the presence of the
glibc extensions to set IPv6 nameserver addresses. It's also possible
that this fixes some bugs that have always been there, but never checked
because we don't have a way to unit-test explicit name servers.

To that effect, this commit adds a manual unit test that mimics the BIND
tool "dig". When running:

 ./qdnslookup qt-project.org any @dns.google

it printed for me:

; <<>> QDnsLookup 6.6.0
;; status: NoError
;; QUESTION:
;qt-project.org                 IN ANY

;; ANSWER:
qt-project.org            3600  IN MX   10 mx.qt-project.org
qt-project.org            3600  IN NS   ns14.cloudns.net
qt-project.org            3600  IN NS   ns11.cloudns.net
qt-project.org            3600  IN NS   ns12.cloudns.net
qt-project.org            3600  IN NS   ns13.cloudns.net
qt-project.org            3600  IN A    52.18.144.254
qt-project.org            3600  IN TXT  "v=spf1 mx ip4:193.209.87.4 include:spf.protection.outlook.com ~all"

;; Query time: 241 ms
;; SERVER: 2001:4860:4860::8844#53

strace confirms the DNS queries were sent to the correct address.

Change-Id: I3e3bfef633af4130a03afffd175d56a92371ed16
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Thiago Macieira 2023-05-08 18:52:00 -07:00
parent df2131426e
commit eb9f88a078
5 changed files with 245 additions and 38 deletions

View File

@ -31,6 +31,8 @@ QT_BEGIN_NAMESPACE
//#define QDNSLOOKUP_DEBUG
constexpr quint16 DnsPort = 53;
class QDnsLookupRunnable;
class QDnsLookupReply

View File

@ -1,4 +1,5 @@
// 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"
@ -6,7 +7,7 @@
#include <qscopedpointer.h>
#include <qurl.h>
#include <qvarlengtharray.h>
#include <private/qnativesocketengine_p.h> // for SetSALen
#include <private/qnativesocketengine_p.h> // for setSockAddr
#include <private/qtnetwork-config_p.h>
QT_REQUIRE_CONFIG(res_ninit);
@ -23,6 +24,67 @@ QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
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 const char *applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
{
if (nameserver.isNull())
return nullptr;
state->nscount = 1;
state->nsaddr_list[0].sin_family = AF_UNSPEC;
if (nameserver.protocol() == QAbstractSocket::IPv6Protocol) {
if (!setIpv6NameServer(state, &nameserver, port))
return QDnsLookupPrivate::msgNoIpV6NameServerAdresses;
} else {
setSockaddr(&state->nsaddr_list[0], nameserver, port);
}
return nullptr;
}
void QDnsLookupRunnable::query(const int requestType, const QByteArray &requestName, const QHostAddress &nameserver, QDnsLookupReply *reply)
{
// Initialize state.
@ -35,43 +97,11 @@ void QDnsLookupRunnable::query(const int requestType, const QByteArray &requestN
auto guard = qScopeGuard([&] { res_nclose(&state); });
//Check if a nameserver was set. If so, use it
if (!nameserver.isNull()) {
if (nameserver.protocol() == QAbstractSocket::IPv4Protocol) {
state.nsaddr_list[0].sin_addr.s_addr = htonl(nameserver.toIPv4Address());
state.nscount = 1;
} else if (nameserver.protocol() == QAbstractSocket::IPv6Protocol) {
#if defined(Q_OS_LINUX)
struct sockaddr_in6 *ns;
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 = (struct sockaddr_in6*) calloc(1, sizeof(struct sockaddr_in6));
Q_CHECK_PTR(ns);
state._u._ext.nsaddrs[0] = ns;
}
#ifndef __UCLIBC__
// Set nsmap[] to indicate that nsaddrs[0] is an IPv6 address
// See: https://sourceware.org/ml/libc-hacker/2002-05/msg00035.html
state._u._ext.nsmap[0] = MAXNS + 1;
#endif
state._u._ext.nscount6 = 1;
ns->sin6_family = AF_INET6;
ns->sin6_port = htons(53);
SetSALen::set(ns, sizeof(*ns));
Q_IPV6ADDR ipv6Address = nameserver.toIPv6Address();
for (int i=0; i<16; i++) {
ns->sin6_addr.s6_addr[i] = ipv6Address[i];
}
#else
qWarning("%s", QDnsLookupPrivate::msgNoIpV6NameServerAdresses);
reply->error = QDnsLookup::ResolverError;
reply->errorString = tr(QDnsLookupPrivate::msgNoIpV6NameServerAdresses);
return;
#endif
}
if (const char *msg = applyNameServer(&state, nameserver, DnsPort)) {
qWarning("%s", msg);
reply->error = QDnsLookup::ResolverError;
reply->errorString = tr(msg);
return;
}
#ifdef QDNSLOOKUP_DEBUG
state.options |= RES_DEBUG;

View File

@ -18,6 +18,7 @@ add_subdirectory(keypadnavigation)
#add_subdirectory(lance) # qgl.h missing
add_subdirectory(qcursor)
add_subdirectory(qdesktopservices)
add_subdirectory(qdnslookup)
add_subdirectory(qgraphicsitem)
add_subdirectory(qgraphicsitemgroup)
add_subdirectory(qgraphicslayout/flicker)

View File

@ -0,0 +1,9 @@
# Copyright (C) 2023 Intel Corporation.
# SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_manual_test(qdnslookup
SOURCES
main.cpp
LIBRARIES
Qt::Network
)

View File

@ -0,0 +1,165 @@
// Copyright (C) 2023 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QElapsedTimer>
#include <QtCore/QMetaEnum>
#include <QtCore/QTimer>
#include <QtNetwork/QHostAddress>
#include <QtNetwork/QHostInfo>
#include <QtNetwork/QDnsLookup>
#include <stdlib.h>
#include <stdio.h>
#include <chrono>
using namespace Qt::StringLiterals;
using namespace std::chrono;
using namespace std::chrono_literals;
static QDnsLookup::Type typeFromString(QString str)
{
// we can use the meta object
QMetaEnum me = QMetaEnum::fromType<QDnsLookup::Type>();
bool ok;
int value = me.keyToValue(str.toUpper().toLatin1(), &ok);
if (!ok)
return QDnsLookup::Type(0);
return QDnsLookup::Type(value);
}
static int showHelp(const char *argv0, int exitcode)
{
// like dig
printf("%s [@global-server] [domain] [query-type]\n", argv0);
return exitcode;
}
static void printAnswers(const QDnsLookup &lookup)
{
printf("\n;; ANSWER:\n");
static auto printRecordCommon = [](const auto &rr, const char *rrtype) {
printf("%-23s %6d IN %s\t", qPrintable(rr.name()), rr.timeToLive(), rrtype);
};
auto printNameRecords = [](const char *rrtype, const QList<QDnsDomainNameRecord> list) {
for (const QDnsDomainNameRecord &rr : list) {
printRecordCommon(rr, rrtype);
printf("%s\n", qPrintable(rr.value()));
}
};
for (const QDnsMailExchangeRecord &rr : lookup.mailExchangeRecords()) {
printRecordCommon(rr, "MX");
printf("%d %s\n", rr.preference(), qPrintable(rr.exchange()));
}
for (const QDnsServiceRecord &rr : lookup.serviceRecords()) {
printRecordCommon(rr, "SRV");
printf("%d %d %d %s\n", rr.priority(), rr.weight(), rr.port(),
qPrintable(rr.target()));
}
printNameRecords("NS", lookup.nameServerRecords());
printNameRecords("PTR", lookup.pointerRecords());
printNameRecords("CNAME", lookup.canonicalNameRecords());
for (const QDnsHostAddressRecord &rr : lookup.hostAddressRecords()) {
QHostAddress addr = rr.value();
printRecordCommon(rr, addr.protocol() == QHostAddress::IPv6Protocol ? "AAAA" : "A");
printf("%s\n", qPrintable(addr.toString()));
}
for (const QDnsTextRecord &rr : lookup.textRecords()) {
printRecordCommon(rr, "TXT");
for (const QByteArray &data : rr.values())
printf("%s ", qPrintable(QDebug::toString(data)));
puts("");
}
}
static void printResults(const QDnsLookup &lookup, QElapsedTimer::Duration duration)
{
if (QDnsLookup::Error error = lookup.error())
printf(";; status: %s (%s)\n", QMetaEnum::fromType<QDnsLookup::Error>().valueToKey(error),
qPrintable(lookup.errorString()));
else
printf(";; status: NoError\n");
QMetaEnum me = QMetaEnum::fromType<QDnsLookup::Type>();
printf(";; QUESTION:\n");
printf(";%-30s IN %s\n", qPrintable(lookup.name()),
me.valueToKey(lookup.type()));
if (lookup.error() == QDnsLookup::NoError)
printAnswers(lookup);
printf("\n;; Query time: %lld ms\n", qint64(duration_cast<milliseconds>(duration).count()));
if (QHostAddress server = lookup.nameserver(); !server.isNull())
printf(";; SERVER: %s#53\n", qPrintable(server.toString()));
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QDnsLookup::Type type = {};
QString domain, server;
const QStringList args = QCoreApplication::arguments().sliced(1);
for (const QString &arg : args) {
if (arg.startsWith(u'@')) {
server = arg.mid(1);
continue;
}
if (arg == u"-h")
return showHelp(argv[0], EXIT_SUCCESS);
if (domain.isNull()) {
domain = arg;
continue;
}
if (type != QDnsLookup::Type{})
return showHelp(argv[0], EXIT_FAILURE);
type = typeFromString(arg);
if (type == QDnsLookup::Type{}) {
fprintf(stderr, "%s: unknown DNS record type '%s'. Valid types are:\n",
argv[0], qPrintable(arg));
QMetaEnum me = QMetaEnum::fromType<QDnsLookup::Type>();
for (int i = 0; i < me.keyCount(); ++i)
fprintf(stderr, " %s\n", me.key(i));
return EXIT_FAILURE;
}
}
if (domain.isEmpty())
domain = u"qt-project.org"_s;
if (type == QDnsLookup::Type{})
type = QDnsLookup::A;
QDnsLookup lookup(type, domain);
if (!server.isEmpty()) {
QHostAddress addr(server);
if (addr.isNull())
addr = QHostInfo::fromName(server).addresses().value(0);
if (addr.isNull()) {
fprintf(stderr, "%s: could not parse name server address '%s'\n",
argv[0], qPrintable(server));
return EXIT_FAILURE;
}
lookup.setNameserver(addr);
}
// execute the lookup
QObject::connect(&lookup, &QDnsLookup::finished, qApp, &QCoreApplication::quit);
QTimer::singleShot(15s, []() { qApp->exit(EXIT_FAILURE); });
QElapsedTimer timer;
timer.start();
lookup.lookup();
if (a.exec() == EXIT_FAILURE)
return EXIT_FAILURE;
printf("; <<>> QDnsLookup " QT_VERSION_STR " <<>> %s %s\n",
qPrintable(QCoreApplication::applicationName()), qPrintable(args.join(u' ')));
printResults(lookup, timer.durationElapsed());
return 0;
}