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

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