QDnsLookup: add the ability to tell if the reply was authenticated

This is implemented for DNS-over-TLS and for the native Unix resolver,
because I can find no way to get the state of the reply on Windows with
the WinDNS.h API.

Change-Id: I455fe22ef4ad4b2f9b01fffd17c7bc022ded2363
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Thiago Macieira 2024-04-19 09:41:40 -07:00
parent f2f00b2a46
commit 503fd60988
5 changed files with 89 additions and 17 deletions

View File

@ -168,7 +168,7 @@ static void qt_qdnsservicerecord_sort(QList<QDnsServiceRecord> &records)
name, or the host name associated with an IP address you should use
QHostInfo instead.
\section1 DNS-over-TLS
\section1 DNS-over-TLS and Authentic Data
QDnsLookup supports DNS-over-TLS (DoT, as specified by \l{RFC 7858}) on
some platforms. That currently includes all Unix platforms where regular
@ -182,6 +182,27 @@ static void qt_qdnsservicerecord_sort(QList<QDnsServiceRecord> &records)
server being connected to. Clients may use setSslConfiguration() to impose
additional restrictions and sslConfiguration() to obtain information after
the query is complete.
QDnsLookup will request DNS servers queried over TLS to perform
authentication on the data they return. If they confirm the data is valid,
the \l authenticData property will be set to true. QDnsLookup does not
verify the integrity of the data by itself, so applications should only
trust this property on servers they have confirmed through other means to
be trustworthy.
\section2 Authentic Data without TLS
QDnsLookup request Authentic Data for any server set with setNameserver(),
even if TLS encryption is not required. This is useful when querying a
caching nameserver on the same host as the application or on a trusted
network. Though similar to the TLS case, the application is responsible for
determining if the server it chose to use is trustworthy, and if the
unencrypted connection cannot be tampered with.
QDnsLookup obeys the system configuration to request Authentic Data on the
default nameserver (that is, if setNameserver() is not called). This is
currently only supported on Linux systems using glibc 2.31 or later. On any
other systems, QDnsLookup will ignore the AD bit in the query header.
*/
/*!
@ -418,6 +439,28 @@ QDnsLookup::~QDnsLookup()
{
}
/*!
\since 6.8
\property QDnsLookup::authenticData
\brief whether the reply was authenticated by the resolver.
QDnsLookup does not perform the authentication itself. Instead, it trusts
the name server that was queried to perform the authentication and report
it. The application is responsible for determining if any servers it
configured with setNameserver() are trustworthy; if no server was set,
QDnsLookup obeys system configuration on whether responses should be
trusted.
This property may be set even if error() indicates a resolver error
occurred.
\sa setNameserver(), nameserverProtocol()
*/
bool QDnsLookup::isAuthenticData() const
{
return d_func()->reply.authenticData;
}
/*!
\property QDnsLookup::error
\brief the type of error that occurred if the DNS lookup failed, or NoError.
@ -1308,6 +1351,7 @@ inline QDebug operator<<(QDebug &d, QDnsLookupRunnable *r)
#if QT_CONFIG(ssl)
static constexpr std::chrono::milliseconds DnsOverTlsConnectTimeout(15'000);
static constexpr std::chrono::milliseconds DnsOverTlsTimeout(120'000);
static constexpr quint8 DnsAuthenticDataBit = 0x20;
static int makeReplyErrorFromSocket(QDnsLookupReply *reply, const QAbstractSocket *socket)
{
@ -1334,6 +1378,9 @@ bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned c
socket.setProtocolTag("domain-s"_L1);
# endif
// Request the name server attempt to authenticate the reply.
query[3] |= DnsAuthenticDataBit;
do {
quint16 size = qToBigEndian<quint16>(query.size());
QDeadlineTimer timeout(DnsOverTlsTimeout);
@ -1363,8 +1410,12 @@ bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned c
// the maximum allocation is small.
size = qFromBigEndian(size);
response.resize(size);
if (waitForBytes(response.data(), size))
if (waitForBytes(response.data(), size)) {
// check if the AD bit is set; we'll trust it over TLS requests
if (size >= 4)
reply->authenticData = response[3] & DnsAuthenticDataBit;
return true;
}
} while (false);
// handle errors

View File

@ -142,6 +142,7 @@ class Q_NETWORK_EXPORT QDnsLookup : public QObject
{
Q_OBJECT
Q_PROPERTY(Error error READ error NOTIFY finished)
Q_PROPERTY(bool authenticData READ isAuthenticData NOTIFY finished)
Q_PROPERTY(QString errorString READ errorString NOTIFY finished)
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged BINDABLE bindableName)
Q_PROPERTY(Type type READ type WRITE setType NOTIFY typeChanged BINDABLE bindableType)
@ -196,6 +197,7 @@ public:
quint16 port = 0, QObject *parent = nullptr);
~QDnsLookup();
bool isAuthenticData() const;
Error error() const;
QString errorString() const;
bool isFinished() const;

View File

@ -48,6 +48,7 @@ class QDnsLookupReply
{
public:
QDnsLookup::Error error = QDnsLookup::NoError;
bool authenticData = false;
QString errorString;
QList<QDnsDomainNameRecord> canonicalNameRecords;

View File

@ -67,11 +67,9 @@ using Cache = QList<QDnsCachedName>; // QHash or QMap are overkill
// 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
@ -122,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)
@ -155,11 +150,22 @@ prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_r
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 (!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 (!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 = [&]() {
@ -210,6 +216,15 @@ static int sendStandardDns(QDnsLookupReply *reply, res_state state, QSpan<unsign
}
}
// 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;
}

View File

@ -118,10 +118,13 @@ static void printAnswers(const QDnsLookup &lookup)
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),
printf(";; status: %s (%s)", QMetaEnum::fromType<QDnsLookup::Error>().valueToKey(error),
qPrintable(lookup.errorString()));
else
printf(";; status: NoError\n");
printf(";; status: NoError");
if (lookup.isAuthenticData())
printf("; AuthenticData");
puts("");
QMetaEnum me = QMetaEnum::fromType<QDnsLookup::Type>();
printf(";; QUESTION:\n");