From eb9f88a07812608c77d28777ba2e911431f59990 Mon Sep 17 00:00:00 2001 From: Thiago Macieira Date: Mon, 8 May 2023 18:52:00 -0700 Subject: [PATCH] QDnsLookup/Unix: modernize the setting of IPv6 server addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/network/kernel/qdnslookup_p.h | 2 + src/network/kernel/qdnslookup_unix.cpp | 106 ++++++++++------ tests/manual/CMakeLists.txt | 1 + tests/manual/qdnslookup/CMakeLists.txt | 9 ++ tests/manual/qdnslookup/main.cpp | 165 +++++++++++++++++++++++++ 5 files changed, 245 insertions(+), 38 deletions(-) create mode 100644 tests/manual/qdnslookup/CMakeLists.txt create mode 100644 tests/manual/qdnslookup/main.cpp diff --git a/src/network/kernel/qdnslookup_p.h b/src/network/kernel/qdnslookup_p.h index c1082863cd4..fa7e6cd7172 100644 --- a/src/network/kernel/qdnslookup_p.h +++ b/src/network/kernel/qdnslookup_p.h @@ -31,6 +31,8 @@ QT_BEGIN_NAMESPACE //#define QDNSLOOKUP_DEBUG +constexpr quint16 DnsPort = 53; + class QDnsLookupRunnable; class QDnsLookupReply diff --git a/src/network/kernel/qdnslookup_unix.cpp b/src/network/kernel/qdnslookup_unix.cpp index ceab943a04f..240ace3c3c0 100644 --- a/src/network/kernel/qdnslookup_unix.cpp +++ b/src/network/kernel/qdnslookup_unix.cpp @@ -1,4 +1,5 @@ // Copyright (C) 2012 Jeremy Lainé +// 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 #include #include -#include // for SetSALen +#include // for setSockAddr #include QT_REQUIRE_CONFIG(res_ninit); @@ -23,6 +24,67 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; +template void setNsMap(T &ext, std::enable_if_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 void setNsMap(T &, ...) +{ + // fallback +} + +template +using EnableIfIPv6 = std::enable_if_t; + +template +bool setIpv6NameServer(State *state, + EnableIfIPv6()._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(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 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; diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index 6c8cce89496..035e14d4c86 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -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) diff --git a/tests/manual/qdnslookup/CMakeLists.txt b/tests/manual/qdnslookup/CMakeLists.txt new file mode 100644 index 00000000000..646e972b302 --- /dev/null +++ b/tests/manual/qdnslookup/CMakeLists.txt @@ -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 +) diff --git a/tests/manual/qdnslookup/main.cpp b/tests/manual/qdnslookup/main.cpp new file mode 100644 index 00000000000..3ddf84063ef --- /dev/null +++ b/tests/manual/qdnslookup/main.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +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(); + + 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 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().valueToKey(error), + qPrintable(lookup.errorString())); + else + printf(";; status: NoError\n"); + + QMetaEnum me = QMetaEnum::fromType(); + 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(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(); + 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; +}