1 /*
2 SPDX-FileCopyrightText: 2016 Alex Spataru
3 SPDX-License-Identifier: MIT
4 */
5
6 #include "qMDNS.h"
7
8 #include <QHostInfo>
9 #include <QUdpSocket>
10 #include <QHostAddress>
11 #include <QNetworkInterface>
12
13 #ifdef Q_OS_LINUX
14 #include <sys/socket.h>
15 #endif
16
17 #include "kstars_debug.h"
18 /*
19 * DNS port and mutlicast addresses
20 */
21 const quint16 MDNS_PORT = 5353;
22 const QHostAddress IPV6_ADDRESS = QHostAddress ("FF02::FB");
23 const QHostAddress IPV4_ADDRESS = QHostAddress ("224.0.0.251");
24
25 /*
26 * mDNS/DNS operation flags
27 */
28 const quint16 kQR_Query = 0x0000;
29 const quint16 kQR_Response = 0x8000;
30 const quint16 kRecordA = 0x0001;
31 const quint16 kRecordAAAA = 0x001C;
32 const quint16 kNsecType = 0x002F;
33 const quint16 kFQDN_Separator = 0x0000;
34 const quint16 kFQDN_Length = 0xC00C;
35 const quint16 kIN_BitFlush = 0x8001;
36 const quint16 kIN_Normal = 0x0001;
37
38 /*
39 * DNS query properties
40 */
41 const quint16 kQuery_QDCOUNT = 0x02;
42 const quint16 kQuery_ANCOUNT = 0x00;
43 const quint16 kQuery_NSCOUNT = 0x00;
44 const quint16 kQuery_ARCOUNT = 0x00;
45
46 /*
47 * DNS response properties
48 */
49 const quint16 kResponse_QDCOUNT = 0x00;
50 const quint16 kResponse_ANCOUNT = 0x01;
51 const quint16 kResponse_NSCOUNT = 0x00;
52 const quint16 kResponse_ARCOUNT = 0x02;
53
54 /* Packet constants */
55 const int MIN_LENGTH = 13;
56 const int IPI_LENGTH = 10;
57 const int IP4_LENGTH = IPI_LENGTH + 4;
58 const int IP6_LENGTH = IPI_LENGTH + 16;
59
60 /**
61 * Encodes the 16-bit \a number as two 8-bit numbers in a byte array
62 */
ENCODE_16_BIT(quint16 number)63 QByteArray ENCODE_16_BIT (quint16 number) {
64 QByteArray data;
65 data.append ((number & 0xff00) >> 8);
66 data.append ((number & 0xff));
67 return data;
68 }
69
70 /**
71 * Encodes the 32-bit \a number as four 8-bit numbers
72 */
ENCODE_32_BIT(quint32 number)73 QByteArray ENCODE_32_BIT (quint32 number) {
74 QByteArray data;
75 data.append ((number & 0xff000000UL) >> 24);
76 data.append ((number & 0x00ff0000UL) >> 16);
77 data.append ((number & 0x0000ff00UL) >> 8);
78 data.append ((number & 0x000000ffUL));
79 return data;
80 }
81
82 /**
83 * Obtains the 16-bit number stored in the \a upper and \a lower 8-bit numbers
84 */
DECODE_16_BIT(quint8 upper,quint8 lower)85 quint16 DECODE_16_BIT (quint8 upper, quint8 lower) {
86 return (quint16) ((upper << 8) | lower);
87 }
88
89 /**
90 * Binds the given \a socket to the given \a address and \a port.
91 * Under GNU/Linux, this function implements a workaround of QTBUG-33419.
92 */
BIND(QUdpSocket * socket,const QHostAddress & address,const int port)93 bool BIND (QUdpSocket* socket, const QHostAddress& address, const int port) {
94 if (!socket)
95 return false;
96
97 #ifdef Q_OS_LINUX
98 int reuse = 1;
99 int domain = PF_UNSPEC;
100
101 if (address.protocol() == QAbstractSocket::IPv4Protocol)
102 domain = PF_INET;
103 else if (address.protocol() == QAbstractSocket::IPv6Protocol)
104 domain = PF_INET6;
105
106 socket->setSocketDescriptor (::socket (domain, SOCK_DGRAM, 0),
107 QUdpSocket::UnconnectedState);
108
109 setsockopt (socket->socketDescriptor(), SOL_SOCKET, SO_REUSEADDR,
110 &reuse, sizeof (reuse));
111 #endif
112
113 return socket->bind (address, port,
114 QUdpSocket::ShareAddress |
115 QUdpSocket::ReuseAddressHint);
116 }
117
qMDNS()118 qMDNS::qMDNS() {
119 /* Set default TTL to 4500 seconds */
120 m_ttl = 4500;
121
122 /* Initialize sockets */
123 m_IPv4Socket = new QUdpSocket (this);
124 m_IPv6Socket = new QUdpSocket (this);
125
126 /* Read and interpret data received from mDNS group */
127 connect (m_IPv4Socket, &QUdpSocket::readyRead, this, &qMDNS::onReadyRead);
128 connect (m_IPv6Socket, &QUdpSocket::readyRead, this, &qMDNS::onReadyRead);
129
130 /* Bind the sockets to the mDNS multicast group */
131 if (BIND (m_IPv4Socket, QHostAddress::AnyIPv4, MDNS_PORT))
132 m_IPv4Socket->joinMulticastGroup (IPV4_ADDRESS);
133 if (BIND (m_IPv6Socket, QHostAddress::AnyIPv6, MDNS_PORT))
134 m_IPv6Socket->joinMulticastGroup (IPV6_ADDRESS);
135 }
136
~qMDNS()137 qMDNS::~qMDNS() {
138 delete m_IPv4Socket;
139 delete m_IPv6Socket;
140 }
141
142 /**
143 * Returns the only running instance of this class
144 */
getInstance()145 qMDNS* qMDNS::getInstance() {
146 static qMDNS instance;
147 return &instance;
148 }
149
150 /**
151 * Returns the mDNS name assigned to the client computer
152 */
hostName() const153 QString qMDNS::hostName() const {
154 return m_hostName;
155 }
156
157 /**
158 * Ensures that the given \a string is a valid mDNS/DNS address.
159 */
getAddress(const QString & string)160 QString qMDNS::getAddress (const QString& string) {
161 QString address = string;
162
163 if (!string.endsWith (".local") && !string.contains ("."))
164 address = string + ".local";
165
166 if (string.endsWith ("."))
167 return "";
168
169 return address;
170 }
171
172 /**
173 * Changes the TTL send to other computers in the mDNS network
174 */
setTTL(const quint32 ttl)175 void qMDNS::setTTL (const quint32 ttl) {
176 m_ttl = ttl;
177 }
178
179 /**
180 * Performs a mDNS lookup to find the given host \a name.
181 * If \a preferIPv6 is set to \c true, then this function will generate a
182 * packet that requests an AAAA-type Resource Record instead of an A-type
183 * Resource Record.
184 */
lookup(const QString & name)185 void qMDNS::lookup (const QString& name) {
186 /* The host name is empty, abort lookup */
187 if (name.isEmpty()) {
188 qCWarning(KSTARS) << Q_FUNC_INFO << "Empty host name specified";
189 return;
190 }
191
192 qCInfo(KSTARS) << "Starting lookup for service" << name;
193
194 m_serviceName = name;
195
196 /* Ensure that we host name is a valid DNS address */
197 QString address = getAddress (name);
198 if (address.isEmpty())
199 return;
200
201 /* Check if we are dealing with a normal DNS address */
202 if (!address.endsWith (".local", Qt::CaseInsensitive)) {
203 QHostInfo::lookupHost (address, this, SIGNAL (hostFound(QHostInfo)));
204 return;
205 }
206
207 /* Perform a mDNS lookup */
208 else {
209 QByteArray data;
210
211 /* Get the host name and domain */
212 QString host = address.split (".").first();
213 QString domain = address.split (".").last();
214
215 /* Check that domain length is valid */
216 if (host.length() > 255) {
217 qWarning() << Q_FUNC_INFO << host << "is too long!";
218 return;
219 }
220
221 /* Create header & flags */
222 data.append (ENCODE_16_BIT (0));
223 data.append (ENCODE_16_BIT (kQR_Query));
224 data.append (ENCODE_16_BIT (kQuery_QDCOUNT));
225 data.append (ENCODE_16_BIT (kQuery_ANCOUNT));
226 data.append (ENCODE_16_BIT (kQuery_NSCOUNT));
227 data.append (ENCODE_16_BIT (kQuery_ARCOUNT));
228
229 /* Add name data */
230 data.append (host.length());
231 data.append (host.toUtf8());
232
233 /* Add domain data */
234 data.append (domain.length());
235 data.append (domain.toUtf8());
236
237 /* Add FQDN/TLD separator */
238 data.append ((char) kFQDN_Separator);
239
240 /* Add IPv4 record type */
241 data.append (ENCODE_16_BIT (kRecordA));
242 data.append (ENCODE_16_BIT (kIN_Normal));
243
244 /* Add FQDN length */
245 data.append (ENCODE_16_BIT (kFQDN_Length));
246
247 /* Add IPv6 record type */
248 data.append (ENCODE_16_BIT (kRecordAAAA));
249 data.append (ENCODE_16_BIT (kIN_Normal));
250
251 /* Send the datagram */
252 sendPacket (data);
253 }
254 }
255
256 /**
257 * Changes the host name of the client computer
258 */
setHostName(const QString & name)259 void qMDNS::setHostName (const QString& name) {
260 if (name.contains (".") && !name.endsWith (".local")) {
261 qWarning() << "Invalid domain name";
262 return;
263 }
264
265 m_hostName = getAddress (name);
266 }
267
268 /**
269 * Called when we receive data from a mDNS client on the network.
270 */
onReadyRead()271 void qMDNS::onReadyRead() {
272 QByteArray data;
273 QUdpSocket* socket = qobject_cast<QUdpSocket*> (sender());
274
275 /* Read data from the socket */
276 if (socket) {
277 while (socket->hasPendingDatagrams()) {
278 data.resize (socket->pendingDatagramSize());
279 socket->readDatagram (data.data(), data.size());
280 }
281 }
282
283 /* Packet is a valid mDNS datagram */
284 if (data.length() > MIN_LENGTH) {
285 quint16 flag = DECODE_16_BIT (data.at (2), data.at (3));
286
287 if (flag == kQR_Query)
288 readQuery (data);
289
290 else if (flag >= kQR_Response)
291 readResponse (data);
292 }
293 }
294
295 /**
296 * Reads the given query \a data and instructs the class to send a response
297 * packet if the query is looking for the host name assigned to this computer.
298 */
readQuery(const QByteArray & data)299 void qMDNS::readQuery (const QByteArray& data) {
300 /* Query packet is invalid */
301 if (data.length() < MIN_LENGTH)
302 return;
303
304 /* Get the lengths of the host name and domain */
305 int n = 12;
306 int hostLength = data.at (n);
307 int domainLength = data.at (n + hostLength + 1);
308
309 /* Read the host name until we stumble with the domain length character */
310 QString name;
311 int h = n + 1;
312 while (data.at (h) != (char) domainLength) {
313 name.append (data.at (h));
314 ++h;
315 }
316
317 /* Read domain length until we stumble with the FQDN/TLD separator */
318 QString domain;
319 int d = n + hostLength + 2;
320 while (data.at (d) != kFQDN_Separator) {
321 domain.append (data.at (d));
322 ++d;
323 }
324
325 /* Construct the full host name (name + domain) */
326 QString host = getAddress (name + "." + domain);
327
328 /* The query packet wants to know more about us */
329 if (host.toLower() == hostName().toLower())
330 sendResponse (DECODE_16_BIT (data.at (0), data.at (1)));
331 }
332
333 /**
334 * Sends the given \a data to both the IPv4 and IPv6 mDNS multicast groups
335 */
sendPacket(const QByteArray & data)336 void qMDNS::sendPacket (const QByteArray& data) {
337 if (!data.isEmpty()) {
338 m_IPv4Socket->writeDatagram (data, IPV4_ADDRESS, MDNS_PORT);
339 m_IPv6Socket->writeDatagram (data, IPV6_ADDRESS, MDNS_PORT);
340 }
341 }
342
343 /**
344 * Reads the given \a data of a response packet and obtains:
345 * - The remote host name
346 * - The remote IPv4
347 * - The remote IPv6
348 */
readResponse(const QByteArray & data)349 void qMDNS::readResponse (const QByteArray& data) {
350 if (data.length() < MIN_LENGTH)
351 return;
352
353 qCDebug(KSTARS) << data;
354
355 // data must contain service name
356 if (data.contains(m_serviceName.toLatin1()) == false)
357 return;
358
359 QString host = getHostNameFromResponse (data);
360 QList<QHostAddress> addresses = getAddressesFromResponse (data, host);
361
362 if (!host.isEmpty() && !addresses.isEmpty())
363 {
364 QHostInfo info;
365 info.setHostName (host);
366 info.setAddresses (addresses);
367 info.setError (QHostInfo::NoError);
368
369 qCInfo(KSTARS) << "Found service on" << host;
370
371 emit hostFound (info);
372 }
373 }
374
375 /**
376 * Sends a response packet with:
377 * - Our mDNS host name
378 * - Our IPv4 address
379 * - Our IPv6 address
380 */
sendResponse(const quint16 query_id)381 void qMDNS::sendResponse (const quint16 query_id) {
382 if (!hostName().isEmpty() && hostName().endsWith (".local")) {
383 QByteArray data;
384
385 /* Get the host name and domain */
386 QString host = hostName().split (".").first();
387 QString domain = hostName().split (".").last();
388
389 /* Get local IPs */
390 quint32 ipv4 = 0;
391 QList<QIPv6Address> ipv6;
392 foreach (QHostAddress address, QNetworkInterface::allAddresses()) {
393 if (!address.isLoopback()) {
394 if (address.protocol() == QAbstractSocket::IPv4Protocol)
395 ipv4 = (ipv4 == 0 ? address.toIPv4Address() : ipv4);
396
397 if (address.protocol() == QAbstractSocket::IPv6Protocol)
398 ipv6.append (address.toIPv6Address());
399 }
400 }
401
402 /* Check that domain length is valid */
403 if (host.length() > 255) {
404 qCWarning(KSTARS) << Q_FUNC_INFO << host << "is too long!";
405 return;
406 }
407
408 /* Create header and flags */
409 data.append (ENCODE_16_BIT (query_id));
410 data.append (ENCODE_16_BIT (kQR_Response));
411 data.append (ENCODE_16_BIT (kResponse_QDCOUNT));
412 data.append (ENCODE_16_BIT (kResponse_ANCOUNT));
413 data.append (ENCODE_16_BIT (kResponse_NSCOUNT));
414 data.append (ENCODE_16_BIT (kResponse_ARCOUNT));
415
416 /* Add name data */
417 data.append (host.length());
418 data.append (host.toUtf8());
419
420 /* Add domain data and FQDN/TLD separator */
421 data.append (domain.length());
422 data.append (domain.toUtf8());
423 data.append ((char) kFQDN_Separator);
424
425 /* Add IPv4 address header */
426 data.append (ENCODE_16_BIT (kRecordA));
427 data.append (ENCODE_16_BIT (kIN_BitFlush));
428 data.append (ENCODE_32_BIT (m_ttl));
429 data.append (ENCODE_16_BIT (sizeof (ipv4)));
430
431 /* Add IPv4 bytes */
432 data.append (ENCODE_32_BIT (ipv4));
433
434 /* Add FQDN offset */
435 data.append (ENCODE_16_BIT (kFQDN_Length));
436
437 /* Add IPv6 addresses */
438 foreach (QIPv6Address ip, ipv6) {
439 data.append (ENCODE_16_BIT (kRecordAAAA));
440 data.append (ENCODE_16_BIT (kIN_BitFlush));
441 data.append (ENCODE_32_BIT (m_ttl));
442 data.append (ENCODE_16_BIT (sizeof (ip.c)));
443
444 /* Add IPv6 bytes */
445 for (unsigned long i = 0; i < sizeof (ip.c); ++i)
446 data.append (ip.c [i]);
447
448 /* Add FQDN offset */
449 data.append (ENCODE_16_BIT (kFQDN_Length));
450 }
451
452 /* TODO: Generate NSEC code block */
453 int nsec_length = 0;
454
455 /* Add NSEC data */
456 data.append (ENCODE_16_BIT (kNsecType));
457 data.append (ENCODE_16_BIT (kIN_BitFlush));
458 data.append (ENCODE_32_BIT (m_ttl));
459 data.append (ENCODE_16_BIT (nsec_length));
460
461 /* Send the response */
462 sendPacket (data);
463 }
464 }
465
466 /**
467 * Extracts the host name from the \a data received from the mDNS network.
468 * The host name begins at byte #12 (when the header and flags end) and ends
469 * with a mandatory NUL character after the domain.
470 *
471 * The host name is constructed in the following way (without spaces):
472 * \c NAME_LENGTH + \c NAME + \c DOMAIN_LENGTH + \c DOMAIN + \c NUL
473 *
474 * For example, appletv.local would be formatted as:
475 * \c 0x07 + \c appletv + \c 0x05 + \c local + \c 0x00
476 *
477 * Or, if you prefer hex data:
478 * \c { 07 61 70 70 6c 65 74 76 05 6c 6f 63 61 6c 00 }
479 * \c { 7 a p p l e t v 5 l o c a l 0 }
480 *
481 * In order to obtain the full host name (and its mDNS domain), we construct
482 * the string backwards. When the code notices that the current character is
483 * the same as the domain length, we know that the domain name has been
484 * extracted, and thus we can replace the domain length with a dot (.) and
485 * begin extracting the host name.
486 */
getHostNameFromResponse(const QByteArray & data)487 QString qMDNS::getHostNameFromResponse (const QByteArray& data) {
488 QList<char> list;
489 QString address = "";
490
491 /* Begin reading host name at byte 13 (byte 12 is the host name length) */
492 int n = 13;
493
494 /* Read the host name until we stumble with the FQDN/TLD separator */
495 while (data.at (n) != kFQDN_Separator) {
496 list.append (data.at (n));
497 ++n;
498 }
499
500 /* Construct the string backwards (to replace domain length with a dot) */
501 for (int i = 0; i < list.count(); ++i) {
502 char character = list.at (list.count() - i - 1);
503
504 if (character == (char) address.length())
505 address.prepend (".");
506 else
507 address.prepend (character);
508 }
509
510 return address;
511 }
512
513 /**
514 * Extracts the IPv4 from the \a data received from the mDNS network.
515 * The IPv4 data begins when the host name data ends.
516 *
517 * For the packet to contain IPv4 information, the DNS Record Type code must
518 * be "A" (IPv4) and the DNS Class code should correspond to "IN" (Internet).
519 *
520 * Here is the layout of the IPv4 section of the packet:
521 *
522 * - DNS Record Type
523 * - DNS Class Code
524 * - TTL
525 * - IP length
526 * - IP address bytes
527 *
528 * This is an example IPv4 section:
529 * \c {00 01 80 01 00 00 78 00 00 04 99 6d 07 5a}
530 *
531 * Data in example section:
532 * - \c {00 01} Type Codes
533 * - \c {80 01} Class Codes
534 * - \c {00 00 78 00} IP TTL
535 * - \c {00 04} Number of address bytes (length in layman's terms)
536 * - \c {99 6d 07 5a} IPv4 Address bytes (153, 109, 7, 90)
537 */
getIPv4FromResponse(const QByteArray & data,const QString & host)538 QString qMDNS::getIPv4FromResponse (const QByteArray& data,
539 const QString& host) {
540 QString ip = "";
541
542 /* n stands for the byte index in which the host name data ends */
543 int n = MIN_LENGTH + host.length();
544
545 /* Packet is too small */
546 if (data.length() < n + IP4_LENGTH)
547 return ip;
548
549 /* Get the IP type and class codes */
550 quint16 typeCode = DECODE_16_BIT (data.at (n + 1), data.at (n + 2));
551 quint16 classCode = DECODE_16_BIT (data.at (n + 3), data.at (n + 4));
552
553 /* Check if type and class codes are good */
554 if (typeCode != kRecordA || classCode != kIN_BitFlush)
555 return ip;
556
557 /* Skip TTL indicator and obtain the number of address bytes */
558 quint8 length = data.at (n + IPI_LENGTH);
559
560 /* Append each IPv4 address byte (and decimal dots) to the IP string */
561 for (int i = 1; i < length + 1; ++i) {
562 ip += QString::number ((quint8) data.at (n + IPI_LENGTH + i));
563 ip += (i < length) ? "." : "";
564 }
565
566 return ip;
567 }
568
569 /**
570 * Extracts the IPv6 from the \a data received from the mDNS network.
571 * The IPv6 data begins when the host name data ends.
572 *
573 * For the packet to contain IPv6 information, the DNS Record Type code must
574 * be "AAAA" (IPv6) and the DNS Class code should correspond to "IN" (Internet).
575 *
576 * Here is the layout of the IPv4 section of the packet:
577 *
578 * - DNS Record Type
579 * - DNS Class Code
580 * - TTL
581 * - IP length
582 * - IP address bytes
583 *
584 * This is an example IPv6 section:
585 * \c { 00 1c 80 01 00 00 78 00 00 10 fe 80 00 00 00 00 00 00 02 23 32 ff fe b1 21 52 }
586 *
587 * Data in example section:
588 * - \c {00 1c} Type Codes
589 * - \c {80 01} Class Codes
590 * - \c {00 00 78 00} IP TTL
591 * - \c {00 10} Number of address bytes (length in layman's terms)
592 * - \c {fe 80 00 00 ... 52} IPv6 Address bytes (there are 16 of them)
593 */
getIPv6FromResponse(const QByteArray & data,const QString & host)594 QStringList qMDNS::getIPv6FromResponse (const QByteArray& data,
595 const QString& host) {
596 QStringList list;
597
598 /* Skip the FQDN and IPv4 section */
599 int n = MIN_LENGTH + IP4_LENGTH + host.length();
600
601 /* Get the IPv6 list */
602 bool isIPv6 = true;
603 while (isIPv6) {
604 /* Skip FQDN bytes */
605 n += 2;
606
607 /* Packet is invalid */
608 if (data.length() < n + IP6_LENGTH)
609 break;
610
611 /* Get the IP type and class codes */
612 quint16 typeCode = DECODE_16_BIT (data.at (n + 1), data.at (n + 2));
613 quint16 classCode = DECODE_16_BIT (data.at (n + 3), data.at (n + 4));
614 isIPv6 = (typeCode == kRecordAAAA && classCode == kIN_BitFlush);
615
616 /* IP type and class codes are OK, extract IP */
617 if (isIPv6) {
618 /* Skip TTL indicator and obtain the number of address bytes */
619 quint8 length = data.at (n + IPI_LENGTH);
620
621 /* Append each IPv6 address byte (encoded as hex) to the IP string */
622 QString ip = "";
623 for (int i = 1; i < length + 1; ++i) {
624 /* Get the hexadecimal representation of the byte */
625 QString byte;
626 byte.setNum ((quint8) data.at (n + i + IPI_LENGTH), 16);
627
628 /* Add the obtained string */
629 ip += byte;
630
631 /* Append colons after even indexes (except in the last byte) */
632 if ((i & 1) == 0 && (i < length))
633 ip += ":";
634 }
635
636 /* Increase the counter to 'jump' to the next section */
637 n += 26;
638
639 /* Append the obtained IP to the list */
640 if (!list.contains (ip))
641 list.append (ip);
642 }
643 }
644
645 return list;
646 }
647
648 /**
649 * Obtains the IPv4 and IPv6 addresses from the received data.
650 * \note This function will only generate a list with the valid IP addresses.
651 */
getAddressesFromResponse(const QByteArray & data,const QString & host)652 QList<QHostAddress> qMDNS::getAddressesFromResponse (const QByteArray& data,
653 const QString& host) {
654 QList<QHostAddress> list;
655
656 /* Add IPv4 address */
657 QHostAddress IPv4Address = QHostAddress (getIPv4FromResponse (data, host));
658 if (!IPv4Address.isNull())
659 list.append (IPv4Address);
660
661 /* Add IPv6 addresses */
662 foreach (QString ip, getIPv6FromResponse (data, host)) {
663 QHostAddress address = QHostAddress (ip);
664 if (!address.isNull())
665 list.append (address);
666 }
667
668 return list;
669 }
670