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 <qurl.h>
10#include <qvarlengtharray.h>
11#include <private/qnativesocketengine_p.h> // for setSockAddr
12#include <private/qtnetwork-config_p.h>
13
14QT_REQUIRE_CONFIG(libresolv);
15
16#include <sys/types.h>
17#include <netinet/in.h>
18#include <arpa/nameser.h>
19#if __has_include(<arpa/nameser_compat.h>)
20# include <arpa/nameser_compat.h>
21#endif
22#include <errno.h>
23#include <resolv.h>
24
25#include <array>
26
27#ifndef T_OPT
28// the older arpa/nameser_compat.h wasn't updated between 1999 and 2016 in glibc
29# define T_OPT ns_t_opt
30#endif
31
32QT_BEGIN_NAMESPACE
33
34using namespace Qt::StringLiterals;
35
36// minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8)
37static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8;
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 ReplyBufferSize >> 8, ReplyBufferSize & 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 if (!nameserver.isNull()) {
72 union res_sockaddr_union u;
73 setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port);
74 res_setservers(state, &u, 1);
75 }
76 return true;
77}
78#else
79template <typename T> void setNsMap(T &ext, std::enable_if_t<sizeof(T::nsmap) != 0, uint16_t> v)
80{
81 // Set nsmap[] to indicate that nsaddrs[0] is an IPv6 address
82 // See: https://sourceware.org/ml/libc-hacker/2002-05/msg00035.html
83 // Unneeded since glibc 2.22 (2015), but doesn't hurt to set it
84 // See: https://sourceware.org/git/?p=glibc.git;a=commit;h=2212c1420c92a33b0e0bd9a34938c9814a56c0f7
85 ext.nsmap[0] = v;
86}
87template <typename T> void setNsMap(T &, ...)
88{
89 // fallback
90}
91
92template <bool Condition>
93using EnableIfIPv6 = std::enable_if_t<Condition, const QHostAddress *>;
94
95template <typename State>
96bool setIpv6NameServer(State *state,
97 EnableIfIPv6<sizeof(std::declval<State>()._u._ext.nsaddrs) != 0> addr,
98 quint16 port)
99{
100 // glibc-like API to set IPv6 name servers
101 struct sockaddr_in6 *ns = state->_u._ext.nsaddrs[0];
102
103 // nsaddrs will be NULL if no nameserver is set in /etc/resolv.conf
104 if (!ns) {
105 // Memory allocated here will be free()'d in res_close() as we
106 // have done res_init() above.
107 ns = static_cast<struct sockaddr_in6*>(calloc(nmemb: 1, size: sizeof(struct sockaddr_in6)));
108 Q_CHECK_PTR(ns);
109 state->_u._ext.nsaddrs[0] = ns;
110 }
111
112 setNsMap(state->_u._ext, MAXNS + 1);
113 state->_u._ext.nscount6 = 1;
114 setSockaddr(ns, *addr, port);
115 return true;
116}
117
118template <typename State> bool setIpv6NameServer(State *, const void *, quint16)
119{
120 // fallback
121 return false;
122}
123
124static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port)
125{
126 if (nameserver.isNull())
127 return true;
128
129 state->nscount = 1;
130 state->nsaddr_list[0].sin_family = AF_UNSPEC;
131 if (nameserver.protocol() == QAbstractSocket::IPv6Protocol)
132 return setIpv6NameServer(state, addr: &nameserver, port);
133 setSockaddr(sin: &state->nsaddr_list[0], addr: nameserver, port);
134 return true;
135}
136#endif // !QT_CONFIG(res_setservers)
137
138static int
139prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_rcode type)
140{
141 // Create header and our query
142 int queryLength = res_nmkquery(state, QUERY, label, C_IN, type, nullptr, 0, nullptr,
143 buffer.data(), buffer.size());
144 Q_ASSERT(queryLength < int(buffer.size()));
145 if (Q_UNLIKELY(queryLength < 0))
146 return queryLength;
147
148 // Append EDNS0 record and set the number of additional RRs to 1
149 Q_ASSERT(queryLength + sizeof(Edns0Record) < buffer.size());
150 std::copy_n(first: std::begin(arr: Edns0Record), n: sizeof(Edns0Record), result: buffer.begin() + queryLength);
151 reinterpret_cast<HEADER *>(buffer.data())->arcount = qToBigEndian<quint16>(source: 1);
152
153 return queryLength + sizeof(Edns0Record);
154}
155
156void QDnsLookupRunnable::query(QDnsLookupReply *reply)
157{
158 // Initialize state.
159 std::remove_pointer_t<res_state> state = {};
160 if (res_ninit(&state) < 0) {
161 int error = errno;
162 qErrnoWarning(code: error, msg: "QDnsLookup: Resolver initialization failed");
163 return reply->makeResolverSystemError(code: error);
164 }
165 auto guard = qScopeGuard(f: [&] { res_nclose(&state); });
166
167 //Check if a nameserver was set. If so, use it
168 if (!applyNameServer(state: &state, nameserver, port))
169 return reply->setError(err: QDnsLookup::ResolverError,
170 msg: QDnsLookup::tr(s: "IPv6 nameservers are currently not supported on this OS"));
171#ifdef QDNSLOOKUP_DEBUG
172 state.options |= RES_DEBUG;
173#endif
174
175 // Prepare the DNS query.
176 QueryBuffer qbuffer;
177 int queryLength = prepareQueryBuffer(state: &state, buffer&: qbuffer, label: requestName, type: ns_rcode(requestType));
178 if (Q_UNLIKELY(queryLength < 0))
179 return reply->makeResolverSystemError();
180
181 // Perform DNS query.
182 QVarLengthArray<unsigned char, ReplyBufferSize> buffer(ReplyBufferSize);
183 auto attemptToSend = [&]() {
184 std::memset(s: buffer.data(), c: 0, HFIXEDSZ); // the header is enough
185 int responseLength = res_nsend(&state, qbuffer.data(), queryLength, buffer.data(), buffer.size());
186 if (responseLength < 0) {
187 // network error of some sort
188 if (errno == ETIMEDOUT)
189 reply->makeTimeoutError();
190 else
191 reply->makeResolverSystemError();
192 }
193 return responseLength;
194 };
195
196 // strictly use UDP, we'll deal with truncated replies ourselves
197 state.options |= RES_IGNTC;
198 int responseLength = attemptToSend();
199 if (responseLength < 0)
200 return;
201
202 // check if we need to use the virtual circuit (TCP)
203 auto header = reinterpret_cast<HEADER *>(buffer.data());
204 if (header->rcode == NOERROR && header->tc) {
205 // yes, increase our buffer size
206 buffer.resize(sz: std::numeric_limits<quint16>::max());
207 header = reinterpret_cast<HEADER *>(buffer.data());
208
209 // remove the EDNS record in the query
210 reinterpret_cast<HEADER *>(qbuffer.data())->arcount = 0;
211 queryLength -= sizeof(Edns0Record);
212
213 // send using the virtual circuit
214 state.options |= RES_USEVC;
215 responseLength = attemptToSend();
216 if (Q_UNLIKELY(responseLength > buffer.size())) {
217 // Ok, we give up.
218 return reply->setError(err: QDnsLookup::ResolverError,
219 msg: QDnsLookup::tr(s: "Reply was too large"));
220 }
221 }
222 if (responseLength < 0)
223 return;
224
225 // Check the reply is valid.
226 if (responseLength < int(sizeof(HEADER)))
227 return reply->makeInvalidReplyError();
228
229 // Parse the reply.
230 if (header->rcode)
231 return reply->makeDnsRcodeError(rcode: header->rcode);
232
233 qptrdiff offset = sizeof(HEADER);
234 unsigned char *response = buffer.data();
235 int status;
236
237 auto expandHost = [&, cache = Cache{}](qptrdiff offset) mutable {
238 if (uchar n = response[offset]; n & NS_CMPRSFLGS) {
239 // compressed name, see if we already have it cached
240 if (offset + 1 < responseLength) {
241 int id = ((n & ~NS_CMPRSFLGS) << 8) | response[offset + 1];
242 auto it = std::find_if(first: cache.constBegin(), last: cache.constEnd(),
243 pred: [id](const QDnsCachedName &n) { return n.code == id; });
244 if (it != cache.constEnd()) {
245 status = 2;
246 return it->name;
247 }
248 }
249 }
250
251 // uncached, expand it
252 char host[MAXCDNAME + 1];
253 status = dn_expand(response, response + responseLength, response + offset,
254 host, sizeof(host));
255 if (status >= 0)
256 return cache.emplaceBack(args: decodeLabel(encodedLabel: QLatin1StringView(host)), args&: offset).name;
257
258 // failed
259 reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Could not expand domain name"));
260 return QString();
261 };
262
263 if (ntohs(netshort: header->qdcount) == 1) {
264 // Skip the query host, type (2 bytes) and class (2 bytes).
265 expandHost(offset);
266 if (status < 0)
267 return;
268 if (offset + status + 4 >= responseLength)
269 header->qdcount = 0xffff; // invalid reply below
270 else
271 offset += status + 4;
272 }
273 if (ntohs(netshort: header->qdcount) > 1)
274 return reply->makeInvalidReplyError();
275
276 // Extract results.
277 const int answerCount = ntohs(netshort: header->ancount);
278 int answerIndex = 0;
279 while ((offset < responseLength) && (answerIndex < answerCount)) {
280 const QString name = expandHost(offset);
281 if (status < 0)
282 return;
283
284 offset += status;
285 if (offset + RRFIXEDSZ > responseLength) {
286 // probably just a truncated reply, return what we have
287 return;
288 }
289 const quint16 type = qFromBigEndian<quint16>(src: response + offset);
290 const qint16 rrclass = qFromBigEndian<quint16>(src: response + offset + 2);
291 const quint32 ttl = qFromBigEndian<quint32>(src: response + offset + 4);
292 const quint16 size = qFromBigEndian<quint16>(src: response + offset + 8);
293 offset += RRFIXEDSZ;
294 if (offset + size > responseLength)
295 return; // truncated
296 if (rrclass != C_IN)
297 continue;
298
299 if (type == QDnsLookup::A) {
300 if (size != 4)
301 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid IPv4 address record"));
302 const quint32 addr = qFromBigEndian<quint32>(src: response + offset);
303 QDnsHostAddressRecord record;
304 record.d->name = name;
305 record.d->timeToLive = ttl;
306 record.d->value = QHostAddress(addr);
307 reply->hostAddressRecords.append(t: record);
308 } else if (type == QDnsLookup::AAAA) {
309 if (size != 16)
310 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid IPv6 address record"));
311 QDnsHostAddressRecord record;
312 record.d->name = name;
313 record.d->timeToLive = ttl;
314 record.d->value = QHostAddress(response + offset);
315 reply->hostAddressRecords.append(t: record);
316 } else if (type == QDnsLookup::CNAME) {
317 QDnsDomainNameRecord record;
318 record.d->name = name;
319 record.d->timeToLive = ttl;
320 record.d->value = expandHost(offset);
321 if (status < 0)
322 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid canonical name record"));
323 reply->canonicalNameRecords.append(t: record);
324 } else if (type == QDnsLookup::NS) {
325 QDnsDomainNameRecord record;
326 record.d->name = name;
327 record.d->timeToLive = ttl;
328 record.d->value = expandHost(offset);
329 if (status < 0)
330 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid name server record"));
331 reply->nameServerRecords.append(t: record);
332 } else if (type == QDnsLookup::PTR) {
333 QDnsDomainNameRecord record;
334 record.d->name = name;
335 record.d->timeToLive = ttl;
336 record.d->value = expandHost(offset);
337 if (status < 0)
338 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid pointer record"));
339 reply->pointerRecords.append(t: record);
340 } else if (type == QDnsLookup::MX) {
341 const quint16 preference = qFromBigEndian<quint16>(src: response + offset);
342 QDnsMailExchangeRecord record;
343 record.d->exchange = expandHost(offset + 2);
344 record.d->name = name;
345 record.d->preference = preference;
346 record.d->timeToLive = ttl;
347 if (status < 0)
348 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid mail exchange record"));
349 reply->mailExchangeRecords.append(t: record);
350 } else if (type == QDnsLookup::SRV) {
351 const quint16 priority = qFromBigEndian<quint16>(src: response + offset);
352 const quint16 weight = qFromBigEndian<quint16>(src: response + offset + 2);
353 const quint16 port = qFromBigEndian<quint16>(src: response + offset + 4);
354 QDnsServiceRecord record;
355 record.d->name = name;
356 record.d->target = expandHost(offset + 6);
357 record.d->port = port;
358 record.d->priority = priority;
359 record.d->timeToLive = ttl;
360 record.d->weight = weight;
361 if (status < 0)
362 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid service record"));
363 reply->serviceRecords.append(t: record);
364 } else if (type == QDnsLookup::TXT) {
365 QDnsTextRecord record;
366 record.d->name = name;
367 record.d->timeToLive = ttl;
368 qptrdiff txt = offset;
369 while (txt < offset + size) {
370 const unsigned char length = response[txt];
371 txt++;
372 if (txt + length > offset + size)
373 return reply->makeInvalidReplyError(msg: QDnsLookup::tr(s: "Invalid text record"));
374 record.d->values << QByteArrayView(response + txt, length).toByteArray();
375 txt += length;
376 }
377 reply->textRecords.append(t: record);
378 }
379 offset += size;
380 answerIndex++;
381 }
382}
383
384QT_END_NAMESPACE
385

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