1// Copyright (C) 2012 Jeremy Lainé <jeremy.laine@m4x.org>
2// Copyright (C) 2023 Intel Corporation.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qdnslookup_p.h"
6
7#include <qendian.h>
8#include <qscopedpointer.h>
9#include <qspan.h>
10#include <qurl.h>
11#include <qvarlengtharray.h>
12#include <private/qnativesocketengine_p.h> // for setSockAddr
13#include <private/qtnetwork-config_p.h>
14
15QT_REQUIRE_CONFIG(libresolv);
16
17#include <sys/types.h>
18#include <netinet/in.h>
19#include <arpa/nameser.h>
20#if __has_include(<arpa/nameser_compat.h>)
21# include <arpa/nameser_compat.h>
22#endif
23#include <errno.h>
24#include <resolv.h>
25
26#include <array>
27
28#ifndef T_OPT
29// the older arpa/nameser_compat.h wasn't updated between 1999 and 2016 in glibc
30# define T_OPT ns_t_opt
31#endif
32
33QT_BEGIN_NAMESPACE
34
35using namespace Qt::StringLiterals;
36using ReplyBuffer = QDnsLookupRunnable::ReplyBuffer;
37
38// https://www.rfc-editor.org/rfc/rfc6891
39static constexpr unsigned char Edns0Record[] = {
40 0x00, // root label
41 T_OPT >> 8, T_OPT & 0xff, // type OPT
42 ReplyBuffer::PreallocatedSize >> 8, ReplyBuffer::PreallocatedSize & 0xff, // payload size
43 NOERROR, // extended rcode
44 0, // version
45 0x00, 0x00, // flags
46 0x00, 0x00, // option length
47};
48
49// maximum length of a EDNS0 query with a 255-character domain (rounded up to 16)
50static constexpr qsizetype QueryBufferSize =
51 HFIXEDSZ + QFIXEDSZ + MAXCDNAME + 1 + sizeof(Edns0Record);
52using QueryBuffer = std::array<unsigned char, (QueryBufferSize + 15) / 16 * 16>;
53
54namespace {
55struct QDnsCachedName
56{
57 QString name;
58 int code = 0;
59 QDnsCachedName(const QString &name, int code) : name(name), code(code) {}
60};
61}
62Q_DECLARE_TYPEINFO(QDnsCachedName, Q_RELOCATABLE_TYPE);
63using Cache = QList<QDnsCachedName>; // QHash or QMap are overkill
64
65#if QT_CONFIG(res_setservers)
66// https://www.ibm.com/docs/en/i/7.3?topic=ssw_ibm_i_73/apis/ressetservers.html
67// https://docs.oracle.com/cd/E86824_01/html/E54774/res-setservers-3resolv.html
68static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
69{
70 union res_sockaddr_union u;
71 setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port);
72 res_setservers(state, &u, 1);
73 return true;
74}
75#else
76template <typename T> void setNsMap(T &ext, std::enable_if_t<sizeof(T::nsmap) != 0, uint16_t> v)
77{
78 // Set nsmap[] to indicate that nsaddrs[0] is an IPv6 address
79 // See: https://sourceware.org/ml/libc-hacker/2002-05/msg00035.html
80 // Unneeded since glibc 2.22 (2015), but doesn't hurt to set it
81 // See: https://sourceware.org/git/?p=glibc.git;a=commit;h=2212c1420c92a33b0e0bd9a34938c9814a56c0f7
82 ext.nsmap[0] = v;
83}
84template <typename T> void setNsMap(T &, ...)
85{
86 // fallback
87}
88
89template <bool Condition>
90using EnableIfIPv6 = std::enable_if_t<Condition, const QHostAddress *>;
91
92template <typename State>
93bool setIpv6NameServer(State *state,
94 EnableIfIPv6<sizeof(std::declval<State>()._u._ext.nsaddrs) != 0> addr,
95 quint16 port)
96{
97 // glibc-like API to set IPv6 name servers
98 struct sockaddr_in6 *ns = state->_u._ext.nsaddrs[0];
99
100 // nsaddrs will be NULL if no nameserver is set in /etc/resolv.conf
101 if (!ns) {
102 // Memory allocated here will be free()'d in res_close() as we
103 // have done res_init() above.
104 ns = static_cast<struct sockaddr_in6*>(calloc(nmemb: 1, size: sizeof(struct sockaddr_in6)));
105 Q_CHECK_PTR(ns);
106 state->_u._ext.nsaddrs[0] = ns;
107 }
108
109 setNsMap(state->_u._ext, MAXNS + 1);
110 state->_u._ext.nscount6 = 1;
111 setSockaddr(ns, *addr, port);
112 return true;
113}
114
115template <typename State> bool setIpv6NameServer(State *, const void *, quint16)
116{
117 // fallback
118 return false;
119}
120
121static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
122{
123 state->nscount = 1;
124 state->nsaddr_list[0].sin_family = AF_UNSPEC;
125 if (nameserver.protocol() == QAbstractSocket::IPv6Protocol)
126 return setIpv6NameServer(state, addr: &nameserver, port);
127 setSockaddr(sin: &state->nsaddr_list[0], addr: nameserver, port);
128 return true;
129}
130#endif // !QT_CONFIG(res_setservers)
131
132static int
133prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_rcode type)
134{
135 // Create header and our query
136 int queryLength = res_nmkquery(state, QUERY, label, C_IN, type, nullptr, 0, nullptr,
137 buffer.data(), buffer.size());
138 Q_ASSERT(queryLength < int(buffer.size()));
139 if (Q_UNLIKELY(queryLength < 0))
140 return queryLength;
141
142 // Append EDNS0 record and set the number of additional RRs to 1
143 Q_ASSERT(queryLength + sizeof(Edns0Record) < buffer.size());
144 std::copy_n(first: std::begin(arr: Edns0Record), n: sizeof(Edns0Record), result: buffer.begin() + queryLength);
145 reinterpret_cast<HEADER *>(buffer.data())->arcount = qToBigEndian<quint16>(source: 1);
146
147 return queryLength + sizeof(Edns0Record);
148}
149
150static int sendStandardDns(QDnsLookupReply *reply, res_state state, QSpan<unsigned char> qbuffer,
151 ReplyBuffer &buffer, const QHostAddress &nameserver, quint16 port)
152{
153 // Check if a nameserver was set. If so, use it.
154 if (!nameserver.isNull()) {
155 if (!applyNameServer(state, nameserver, port)) {
156 reply->setError(err: QDnsLookup::ResolverError,
157 msg: QDnsLookup::tr(s: "IPv6 nameservers are currently not supported on this OS"));
158 return -1;
159 }
160
161 // Request the name server attempt to authenticate the reply.
162 reinterpret_cast<HEADER *>(buffer.data())->ad = true;
163
164#ifdef RES_TRUSTAD
165 // Need to set this option even though we set the AD bit, otherwise
166 // glibc turns it off.
167 state->options |= RES_TRUSTAD;
168#endif
169 }
170
171 auto attemptToSend = [&]() {
172 std::memset(s: buffer.data(), c: 0, HFIXEDSZ); // the header is enough
173 int responseLength = res_nsend(state, qbuffer.data(), qbuffer.size(), buffer.data(), buffer.size());
174 if (responseLength >= 0)
175 return responseLength; // success
176
177 // libresolv uses ETIMEDOUT for resolver errors ("no answer")
178 if (errno == ECONNREFUSED)
179 reply->setError(err: QDnsLookup::ServerRefusedError, msg: qt_error_string());
180 else if (errno != ETIMEDOUT)
181 reply->makeResolverSystemError(); // some other error
182
183 auto query = reinterpret_cast<HEADER *>(qbuffer.data());
184 auto header = reinterpret_cast<HEADER *>(buffer.data());
185 if (query->id == header->id && header->qr)
186 reply->makeDnsRcodeError(rcode: header->rcode);
187 else
188 reply->makeTimeoutError(); // must really be a timeout
189 return -1;
190 };
191
192 // strictly use UDP, we'll deal with truncated replies ourselves
193 state->options |= RES_IGNTC;
194 int responseLength = attemptToSend();
195 if (responseLength < 0)
196 return responseLength;
197
198 // check if we need to use the virtual circuit (TCP)
199 auto header = reinterpret_cast<HEADER *>(buffer.data());
200 if (header->rcode == NOERROR && header->tc) {
201 // yes, increase our buffer size
202 buffer.resize(sz: std::numeric_limits<quint16>::max());
203 header = reinterpret_cast<HEADER *>(buffer.data());
204
205 // remove the EDNS record in the query
206 reinterpret_cast<HEADER *>(qbuffer.data())->arcount = 0;
207 qbuffer = qbuffer.first(n: qbuffer.size() - sizeof(Edns0Record));
208
209 // send using the virtual circuit
210 state->options |= RES_USEVC;
211 responseLength = attemptToSend();
212 if (Q_UNLIKELY(responseLength > buffer.size())) {
213 // Ok, we give up.
214 reply->setError(err: QDnsLookup::ResolverError, msg: QDnsLookup::tr(s: "Reply was too large"));
215 return -1;
216 }
217 }
218
219 // We only trust the AD bit in the reply if we're querying a custom name
220 // server or if we can tell the system administrator configured the resolver
221 // to trust replies.
222#ifndef RES_TRUSTAD
223 if (nameserver.isNull())
224 header->ad = false;
225#endif
226 reply->authenticData = header->ad;
227
228 return responseLength;
229}
230
231void QDnsLookupRunnable::query(QDnsLookupReply *reply)
232{
233 // Initialize state.
234 std::remove_pointer_t<res_state> state = {};
235 if (res_ninit(&state) < 0) {
236 int error = errno;
237 qErrnoWarning(code: error, msg: "QDnsLookup: Resolver initialization failed");
238 return reply->makeResolverSystemError(code: error);
239 }
240 auto guard = qScopeGuard(f: [&] { res_nclose(&state); });
241
242#ifdef QDNSLOOKUP_DEBUG
243 state.options |= RES_DEBUG;
244#endif
245
246 // Prepare the DNS query.
247 QueryBuffer qbuffer;
248 int queryLength = prepareQueryBuffer(state: &state, buffer&: qbuffer, label: requestName.constData(), type: ns_rcode(requestType));
249 if (Q_UNLIKELY(queryLength < 0))
250 return reply->makeResolverSystemError();
251
252 // Perform DNS query.
253 QSpan query(qbuffer.data(), queryLength);
254 ReplyBuffer buffer(ReplyBufferSize);
255 int responseLength = -1;
256 switch (protocol) {
257 case QDnsLookup::Standard:
258 responseLength = sendStandardDns(reply, state: &state, qbuffer: query, buffer, nameserver, port);
259 break;
260 case QDnsLookup::DnsOverTls:
261 if (!sendDnsOverTls(reply, query, response&: buffer))
262 return;
263 responseLength = buffer.size();
264 break;
265 }
266
267 if (responseLength < 0)
268 return;
269
270 // Check the reply is valid.
271 if (responseLength < int(sizeof(HEADER)))
272 return reply->makeInvalidReplyError();
273
274 // Parse the reply.
275 auto header = reinterpret_cast<HEADER *>(buffer.data());
276 if (header->rcode)
277 return reply->makeDnsRcodeError(rcode: header->rcode);
278
279 qptrdiff offset = sizeof(HEADER);
280 unsigned char *response = buffer.data();
281 int status;
282
283 auto expandHost = [&, cache = Cache{}](qptrdiff offset) mutable {
284 if (uchar n = response[offset]; n & NS_CMPRSFLGS) {
285 // compressed name, see if we already have it cached
286 if (offset + 1 < responseLength) {
287 int id = ((n & ~NS_CMPRSFLGS) << 8) | response[offset + 1];
288 auto it = std::find_if(first: cache.constBegin(), last: cache.constEnd(),
289 pred: [id](const QDnsCachedName &n) { return n.code == id; });
290 if (it != cache.constEnd()) {
291 status = 2;
292 return it->name;
293 }
294 }
295 }
296
297 // uncached, expand it
298 char host[MAXCDNAME + 1];
299 status = dn_expand(response, response + responseLength, response + offset,
300 host, sizeof(host));
301 if (status >= 0)
302 return cache.emplaceBack(args: decodeLabel(encodedLabel: QLatin1StringView(host)), args&: offset).name;
303
304 // failed
305 reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Could not expand domain name"));
306 return QString();
307 };
308
309 if (ntohs(netshort: header->qdcount) == 1) {
310 // Skip the query host, type (2 bytes) and class (2 bytes).
311 expandHost(offset);
312 if (status < 0)
313 return;
314 if (offset + status + 4 > responseLength)
315 header->qdcount = 0xffff; // invalid reply below
316 else
317 offset += status + 4;
318 }
319 if (ntohs(netshort: header->qdcount) > 1)
320 return reply->makeInvalidReplyError();
321
322 // Extract results.
323 const int answerCount = ntohs(netshort: header->ancount);
324 int answerIndex = 0;
325 while ((offset < responseLength) && (answerIndex < answerCount)) {
326 const QString name = expandHost(offset);
327 if (status < 0)
328 return;
329
330 offset += status;
331 if (offset + RRFIXEDSZ > responseLength) {
332 // probably just a truncated reply, return what we have
333 return;
334 }
335 const quint16 type = qFromBigEndian<quint16>(src: response + offset);
336 const qint16 rrclass = qFromBigEndian<quint16>(src: response + offset + 2);
337 const quint32 ttl = qFromBigEndian<quint32>(src: response + offset + 4);
338 const quint16 size = qFromBigEndian<quint16>(src: response + offset + 8);
339 offset += RRFIXEDSZ;
340 if (offset + size > responseLength)
341 return; // truncated
342 if (rrclass != C_IN)
343 continue;
344
345 if (type == QDnsLookup::A) {
346 if (size != 4)
347 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid IPv4 address record"));
348 const quint32 addr = qFromBigEndian<quint32>(src: response + offset);
349 QDnsHostAddressRecord record;
350 record.d->name = name;
351 record.d->timeToLive = ttl;
352 record.d->value = QHostAddress(addr);
353 reply->hostAddressRecords.append(t: record);
354 } else if (type == QDnsLookup::AAAA) {
355 if (size != 16)
356 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid IPv6 address record"));
357 QDnsHostAddressRecord record;
358 record.d->name = name;
359 record.d->timeToLive = ttl;
360 record.d->value = QHostAddress(response + offset);
361 reply->hostAddressRecords.append(t: record);
362 } else if (type == QDnsLookup::CNAME) {
363 QDnsDomainNameRecord record;
364 record.d->name = name;
365 record.d->timeToLive = ttl;
366 record.d->value = expandHost(offset);
367 if (status < 0)
368 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid canonical name record"));
369 reply->canonicalNameRecords.append(t: record);
370 } else if (type == QDnsLookup::NS) {
371 QDnsDomainNameRecord record;
372 record.d->name = name;
373 record.d->timeToLive = ttl;
374 record.d->value = expandHost(offset);
375 if (status < 0)
376 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid name server record"));
377 reply->nameServerRecords.append(t: record);
378 } else if (type == QDnsLookup::PTR) {
379 QDnsDomainNameRecord record;
380 record.d->name = name;
381 record.d->timeToLive = ttl;
382 record.d->value = expandHost(offset);
383 if (status < 0)
384 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid pointer record"));
385 reply->pointerRecords.append(t: record);
386 } else if (type == QDnsLookup::MX) {
387 const quint16 preference = qFromBigEndian<quint16>(src: response + offset);
388 QDnsMailExchangeRecord record;
389 record.d->exchange = expandHost(offset + 2);
390 record.d->name = name;
391 record.d->preference = preference;
392 record.d->timeToLive = ttl;
393 if (status < 0)
394 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid mail exchange record"));
395 reply->mailExchangeRecords.append(t: record);
396 } else if (type == QDnsLookup::SRV) {
397 if (size < 7)
398 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid service record"));
399 const quint16 priority = qFromBigEndian<quint16>(src: response + offset);
400 const quint16 weight = qFromBigEndian<quint16>(src: response + offset + 2);
401 const quint16 port = qFromBigEndian<quint16>(src: response + offset + 4);
402 QDnsServiceRecord record;
403 record.d->name = name;
404 record.d->target = expandHost(offset + 6);
405 record.d->port = port;
406 record.d->priority = priority;
407 record.d->timeToLive = ttl;
408 record.d->weight = weight;
409 if (status < 0)
410 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid service record"));
411 reply->serviceRecords.append(t: record);
412 } else if (type == QDnsLookup::TLSA) {
413 // https://datatracker.ietf.org/doc/html/rfc6698#section-2.1
414 if (size < 3)
415 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid TLS association record"));
416
417 const quint8 usage = response[offset];
418 const quint8 selector = response[offset + 1];
419 const quint8 matchType = response[offset + 2];
420
421 QDnsTlsAssociationRecord record;
422 record.d->name = name;
423 record.d->timeToLive = ttl;
424 record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage);
425 record.d->selector = QDnsTlsAssociationRecord::Selector(selector);
426 record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType);
427 record.d->value.assign(first: response + offset + 3, last: response + offset + size);
428 reply->tlsAssociationRecords.append(t: std::move(record));
429 } else if (type == QDnsLookup::TXT) {
430 QDnsTextRecord record;
431 record.d->name = name;
432 record.d->timeToLive = ttl;
433 qptrdiff txt = offset;
434 while (txt < offset + size) {
435 const unsigned char length = response[txt];
436 txt++;
437 if (txt + length > offset + size)
438 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid text record"));
439 record.d->values << QByteArrayView(response + txt, length).toByteArray();
440 txt += length;
441 }
442 reply->textRecords.append(t: record);
443 }
444 offset += size;
445 answerIndex++;
446 }
447}
448
449QT_END_NAMESPACE
450

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtbase/src/network/kernel/qdnslookup_unix.cpp