diff --git a/CMakeLists.txt b/CMakeLists.txt index 342a3f43e..ed3c4f13b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,7 +65,7 @@ else() find_package(Qca-qt${QT_MAJOR_VERSION} ${QCA_MIN_VERSION} REQUIRED) set(Qca_LIBRARY qca-qt${QT_MAJOR_VERSION}) - set(KF5_REQUIRED_COMPONENTS I18n ConfigWidgets DBusAddons IconThemes Notifications KIO KCMUtils Service Solid Kirigami2 People WindowSystem GuiAddons DNSSD) + set(KF5_REQUIRED_COMPONENTS I18n ConfigWidgets DBusAddons IconThemes Notifications KIO KCMUtils Service Solid Kirigami2 People WindowSystem GuiAddons) set(KF5_OPTIONAL_COMPONENTS DocTools) set_package_properties(KF5Kirigami2 PROPERTIES diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 14415aa5d..b47b99bb0 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -63,7 +63,6 @@ PRIVATE Qt${QT_MAJOR_VERSION}::DBus KF5::I18n KF5::ConfigCore - KF5::DNSSD ) if(${KF5KIO_FOUND}) diff --git a/core/backends/lan/CMakeLists.txt b/core/backends/lan/CMakeLists.txt index 042581cdd..dda9b6d41 100644 --- a/core/backends/lan/CMakeLists.txt +++ b/core/backends/lan/CMakeLists.txt @@ -8,6 +8,7 @@ set(backends_kdeconnect_SRCS backends/lan/compositeuploadjob.cpp backends/lan/uploadjob.cpp backends/lan/mdnsdiscovery.cpp + backends/lan/mdns_wrapper.cpp PARENT_SCOPE ) diff --git a/core/backends/lan/mdns.h b/core/backends/lan/mdns.h new file mode 100644 index 000000000..e864981c0 --- /dev/null +++ b/core/backends/lan/mdns.h @@ -0,0 +1,1618 @@ +/* mdns.h - mDNS/DNS-SD library - Public Domain - 2017 Mattias Jansson + * + * This library provides a cross-platform mDNS and DNS-SD library in C. + * The implementation is based on RFC 6762 and RFC 6763. + * + * The latest source code is always available at + * + * https://github.com/mjansson/mdns + * + * This library is put in the public domain; you can redistribute it and/or modify it without any + * restrictions. + * + */ + +#pragma once + +#include +#include +#include +#include + +#include +#ifdef _WIN32 +#include +#include +#define strncasecmp _strnicmp +#else +#include +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define MDNS_INVALID_POS ((size_t)-1) + +#define MDNS_STRING_CONST(s) (s), (sizeof((s)) - 1) +#define MDNS_STRING_ARGS(s) s.str, s.length +#define MDNS_STRING_FORMAT(s) (int)((s).length), s.str + +#define MDNS_POINTER_OFFSET(p, ofs) ((void*)((char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_OFFSET_CONST(p, ofs) ((const void*)((const char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_DIFF(a, b) ((size_t)((const char*)(a) - (const char*)(b))) + +#define MDNS_PORT 5353 +#define MDNS_UNICAST_RESPONSE 0x8000U +#define MDNS_CACHE_FLUSH 0x8000U +#define MDNS_MAX_SUBSTRINGS 64 + +enum mdns_record_type { + MDNS_RECORDTYPE_IGNORE = 0, + // Address + MDNS_RECORDTYPE_A = 1, + // Domain Name pointer + MDNS_RECORDTYPE_PTR = 12, + // Arbitrary text string + MDNS_RECORDTYPE_TXT = 16, + // IP6 Address [Thomson] + MDNS_RECORDTYPE_AAAA = 28, + // Server Selection [RFC2782] + MDNS_RECORDTYPE_SRV = 33, + // Any available records + MDNS_RECORDTYPE_ANY = 255 +}; + +enum mdns_entry_type { + MDNS_ENTRYTYPE_QUESTION = 0, + MDNS_ENTRYTYPE_ANSWER = 1, + MDNS_ENTRYTYPE_AUTHORITY = 2, + MDNS_ENTRYTYPE_ADDITIONAL = 3 +}; + +enum mdns_class { MDNS_CLASS_IN = 1, MDNS_CLASS_ANY = 255 }; + +typedef enum mdns_record_type mdns_record_type_t; +typedef enum mdns_entry_type mdns_entry_type_t; +typedef enum mdns_class mdns_class_t; + +typedef int (*mdns_record_callback_fn)(int sock, const struct sockaddr* from, size_t addrlen, + mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, + uint16_t rclass, uint32_t ttl, const void* data, size_t size, + size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data); + +typedef struct mdns_string_t mdns_string_t; +typedef struct mdns_string_pair_t mdns_string_pair_t; +typedef struct mdns_string_table_item_t mdns_string_table_item_t; +typedef struct mdns_string_table_t mdns_string_table_t; +typedef struct mdns_record_t mdns_record_t; +typedef struct mdns_record_srv_t mdns_record_srv_t; +typedef struct mdns_record_ptr_t mdns_record_ptr_t; +typedef struct mdns_record_a_t mdns_record_a_t; +typedef struct mdns_record_aaaa_t mdns_record_aaaa_t; +typedef struct mdns_record_txt_t mdns_record_txt_t; +typedef struct mdns_query_t mdns_query_t; + +#ifdef _WIN32 +typedef int mdns_size_t; +typedef int mdns_ssize_t; +#else +typedef size_t mdns_size_t; +typedef ssize_t mdns_ssize_t; +#endif + +struct mdns_string_t { + const char* str; + size_t length; +}; + +struct mdns_string_pair_t { + size_t offset; + size_t length; + int ref; +}; + +struct mdns_string_table_t { + size_t offset[16]; + size_t count; + size_t next; +}; + +struct mdns_record_srv_t { + uint16_t priority; + uint16_t weight; + uint16_t port; + mdns_string_t name; +}; + +struct mdns_record_ptr_t { + mdns_string_t name; +}; + +struct mdns_record_a_t { + struct sockaddr_in addr; +}; + +struct mdns_record_aaaa_t { + struct sockaddr_in6 addr; +}; + +struct mdns_record_txt_t { + mdns_string_t key; + mdns_string_t value; +}; + +struct mdns_record_t { + mdns_string_t name; + mdns_record_type_t type; + union mdns_record_data { + mdns_record_ptr_t ptr; + mdns_record_srv_t srv; + mdns_record_a_t a; + mdns_record_aaaa_t aaaa; + mdns_record_txt_t txt; + } data; + uint16_t rclass; + uint32_t ttl; +}; + +struct mdns_header_t { + uint16_t query_id; + uint16_t flags; + uint16_t questions; + uint16_t answer_rrs; + uint16_t authority_rrs; + uint16_t additional_rrs; +}; + +struct mdns_query_t { + mdns_record_type_t type; + const char* name; + size_t length; +}; + +// mDNS/DNS-SD public API + +//! Open and setup a IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr); + +//! Setup an already opened IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr); + +//! Open and setup a IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr); + +//! Setup an already opened IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr); + +//! Close a socket opened with mdns_socket_open_ipv4 and mdns_socket_open_ipv6. +static inline void +mdns_socket_close(int sock); + +//! Listen for incoming multicast DNS-SD and mDNS query requests. The socket should have been opened +//! on port MDNS_PORT using one of the mdns open or setup socket functions. Buffer must be 32 bit +//! aligned. Parsing is stopped when callback function returns non-zero. Returns the number of +//! queries parsed. +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + +//! Send a multicast DNS-SD reqeuest on the given socket to discover available services. Returns 0 +//! on success, or <0 if error. +static inline int +mdns_discovery_send(int sock); + +//! Recieve unicast responses to a DNS-SD sent with mdns_discovery_send. Any data will be piped to +//! the given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + +//! Send a multicast mDNS query on the given socket for the given service name. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. The query will request a unicast response if the socket is bound to an +//! ephemeral port, or a multicast response if the socket is bound to mDNS port 5353. Returns the +//! used query ID, or <0 if error. +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id); + +//! Send a multicast mDNS query on the given socket for the given service names. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. Each additional service name query consists of a triplet - a record type +//! (mdns_record_type_t), a name string pointer (const char*) and a name length (size_t). The list +//! of variable arguments should be terminated with a record type of 0. The query will request a +//! unicast response if the socket is bound to an ephemeral port, or a multicast response if the +//! socket is bound to mDNS port 5353. Returns the used query ID, or <0 if error. +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, + size_t capacity, uint16_t query_id); + +//! Receive unicast responses to a mDNS query sent with mdns_discovery_recv, optionally filtering +//! out any responses not matching the given query ID. Set the query ID to 0 to parse all responses, +//! even if it is not matching the query ID set in a specific query. Any data will be piped to the +//! given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int query_id); + +//! Send a variable unicast mDNS query answer to any question with variable number of records to the +//! given address. Use the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query +//! recieved to determine if the answer should be sent unicast (bit set) or multicast (bit not set). +//! Buffer must be 32 bit aligned. The record type and name should match the data from the query +//! recieved. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS query answer to any question with variable number of records. Use +//! the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query recieved to determine +//! if the answer should be sent unicast (bit set) or multicast (bit not set). Buffer must be 32 bit +//! aligned. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement (as an unsolicited answer) with variable number of +//! records.Buffer must be 32 bit aligned. Returns 0 if success, or <0 if error. Use this on service +//! startup to announce your instance to the local network. +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement. Use this on service end for removing the resource +//! from the local network. The records must be identical to the according announcement. +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +// Parse records functions + +//! Parse a PTR record, returns the name in the record +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse a SRV record, returns the priority, weight, port and name in the record +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse an A record, returns the IPv4 address in the record +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr); + +//! Parse an AAAA record, returns the IPv6 address in the record +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr); + +//! Parse a TXT record, returns the number of key=value records parsed and stores the key-value +//! pairs in the supplied buffer +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity); + +// Internal functions + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity); + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset); + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset); + +//! Compare if two strings are equal. If the strings are equal it returns >0 and the offset variables are +//! updated to the end of the corresponding strings. If the strings are not equal it returns 0 and +//! the offset variables are NOT updated. +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs); + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table); + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length); + +// Implementations + +static inline uint16_t +mdns_ntohs(const void* data) { + uint16_t aligned; + memcpy(&aligned, data, sizeof(uint16_t)); + return ntohs(aligned); +} + +static inline uint32_t +mdns_ntohl(const void* data) { + uint32_t aligned; + memcpy(&aligned, data, sizeof(uint32_t)); + return ntohl(aligned); +} + +static inline void* +mdns_htons(void* data, uint16_t val) { + val = htons(val); + memcpy(data, &val, sizeof(uint16_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint16_t)); +} + +static inline void* +mdns_htonl(void* data, uint32_t val) { + val = htonl(val); + memcpy(data, &val, sizeof(uint32_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint32_t)); +} + +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr) { + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv4(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} + +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr) { + unsigned char ttl = 1; + unsigned char loopback = 1; + unsigned int reuseaddr = 1; + struct ip_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, (const char*)&ttl, sizeof(ttl)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.imr_multiaddr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + if (saddr) + req.imr_interface = saddr->sin_addr; + if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; + sock_addr.sin_addr.s_addr = INADDR_ANY; +#ifdef __APPLE__ + sock_addr.sin_len = sizeof(struct sockaddr_in); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&sock_addr.sin_addr, + sizeof(sock_addr.sin_addr)); +#ifndef _WIN32 + sock_addr.sin_addr.s_addr = INADDR_ANY; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr) { + int sock = (int)socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv6(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} + +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr) { + int hops = 1; + unsigned int loopback = 1; + unsigned int reuseaddr = 1; + struct ipv6_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (const char*)&hops, sizeof(hops)); + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.ipv6mr_multiaddr.s6_addr[0] = 0xFF; + req.ipv6mr_multiaddr.s6_addr[1] = 0x02; + req.ipv6mr_multiaddr.s6_addr[15] = 0xFB; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in6 sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; +#ifdef __APPLE__ + sock_addr.sin6_len = sizeof(struct sockaddr_in6); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in6)); + unsigned int ifindex = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char*)&ifindex, sizeof(ifindex)); +#ifndef _WIN32 + sock_addr.sin6_addr = in6addr_any; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in6))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline void +mdns_socket_close(int sock) { +#ifdef _WIN32 + closesocket(sock); +#else + close(sock); +#endif +} + +static inline int +mdns_is_string_ref(uint8_t val) { + return (0xC0 == (val & 0xC0)); +} + +static inline mdns_string_pair_t +mdns_get_next_substring(const void* rawdata, size_t size, size_t offset) { + const uint8_t* buffer = (const uint8_t*)rawdata; + mdns_string_pair_t pair = {MDNS_INVALID_POS, 0, 0}; + if (offset >= size) + return pair; + if (!buffer[offset]) { + pair.offset = offset; + return pair; + } + int recursion = 0; + while (mdns_is_string_ref(buffer[offset])) { + if (size < offset + 2) + return pair; + + offset = mdns_ntohs(MDNS_POINTER_OFFSET(buffer, offset)) & 0x3fff; + if (offset >= size) + return pair; + + pair.ref = 1; + if (++recursion > 16) + return pair; + } + + size_t length = (size_t)buffer[offset++]; + if (size < offset + length) + return pair; + + pair.offset = offset; + pair.length = length; + + return pair; +} + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset) { + size_t cur = *offset; + mdns_string_pair_t substr; + unsigned int counter = 0; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (substr.ref) { + *offset = cur + 2; + return 1; + } + cur = substr.offset + substr.length; + } while (substr.length); + + *offset = cur + 1; + return 1; +} + +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs) { + size_t lhs_cur = *ofs_lhs; + size_t rhs_cur = *ofs_rhs; + size_t lhs_end = MDNS_INVALID_POS; + size_t rhs_end = MDNS_INVALID_POS; + mdns_string_pair_t lhs_substr; + mdns_string_pair_t rhs_substr; + unsigned int counter = 0; + do { + lhs_substr = mdns_get_next_substring(buffer_lhs, size_lhs, lhs_cur); + rhs_substr = mdns_get_next_substring(buffer_rhs, size_rhs, rhs_cur); + if ((lhs_substr.offset == MDNS_INVALID_POS) || (rhs_substr.offset == MDNS_INVALID_POS) || + (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (lhs_substr.length != rhs_substr.length) + return 0; + if (strncasecmp((const char*)MDNS_POINTER_OFFSET_CONST(buffer_rhs, rhs_substr.offset), + (const char*)MDNS_POINTER_OFFSET_CONST(buffer_lhs, lhs_substr.offset), + rhs_substr.length)) + return 0; + if (lhs_substr.ref && (lhs_end == MDNS_INVALID_POS)) + lhs_end = lhs_cur + 2; + if (rhs_substr.ref && (rhs_end == MDNS_INVALID_POS)) + rhs_end = rhs_cur + 2; + lhs_cur = lhs_substr.offset + lhs_substr.length; + rhs_cur = rhs_substr.offset + rhs_substr.length; + } while (lhs_substr.length); + + if (lhs_end == MDNS_INVALID_POS) + lhs_end = lhs_cur + 1; + *ofs_lhs = lhs_end; + + if (rhs_end == MDNS_INVALID_POS) + rhs_end = rhs_cur + 1; + *ofs_rhs = rhs_end; + + return 1; +} + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity) { + size_t cur = *offset; + size_t end = MDNS_INVALID_POS; + mdns_string_pair_t substr; + mdns_string_t result; + result.str = str; + result.length = 0; + char* dst = str; + unsigned int counter = 0; + size_t remain = capacity; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return result; + if (substr.ref && (end == MDNS_INVALID_POS)) + end = cur + 2; + if (substr.length) { + size_t to_copy = (substr.length < remain) ? substr.length : remain; + memcpy(dst, (const char*)buffer + substr.offset, to_copy); + dst += to_copy; + remain -= to_copy; + if (remain) { + *dst++ = '.'; + --remain; + } + } + cur = substr.offset + substr.length; + } while (substr.length); + + if (end == MDNS_INVALID_POS) + end = cur + 1; + *offset = end; + + result.length = capacity - remain; + return result; +} + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length) { + if (!string_table) + return MDNS_INVALID_POS; + + for (size_t istr = 0; istr < string_table->count; ++istr) { + if (string_table->offset[istr] >= capacity) + continue; + size_t offset = 0; + mdns_string_pair_t sub_string = + mdns_get_next_substring(buffer, capacity, string_table->offset[istr]); + if (!sub_string.length || (sub_string.length != first_length)) + continue; + if (memcmp(str, MDNS_POINTER_OFFSET(buffer, sub_string.offset), sub_string.length)) + continue; + + // Initial substring matches, now match all remaining substrings + offset += first_length + 1; + while (offset < total_length) { + size_t dot_pos = mdns_string_find(str, total_length, '.', offset); + if (dot_pos == MDNS_INVALID_POS) + dot_pos = total_length; + size_t current_length = dot_pos - offset; + + sub_string = + mdns_get_next_substring(buffer, capacity, sub_string.offset + sub_string.length); + if (!sub_string.length || (sub_string.length != current_length)) + break; + if (memcmp(str + offset, MDNS_POINTER_OFFSET(buffer, sub_string.offset), + sub_string.length)) + break; + + offset = dot_pos + 1; + } + + // Return reference offset if entire string matches + if (offset >= total_length) + return string_table->offset[istr]; + } + + return MDNS_INVALID_POS; +} + +static inline void +mdns_string_table_add(mdns_string_table_t* string_table, size_t offset) { + if (!string_table) + return; + + string_table->offset[string_table->next] = offset; + + size_t table_capacity = sizeof(string_table->offset) / sizeof(string_table->offset[0]); + if (++string_table->count > table_capacity) + string_table->count = table_capacity; + if (++string_table->next >= table_capacity) + string_table->next = 0; +} + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset) { + const void* found; + if (offset >= length) + return MDNS_INVALID_POS; + found = memchr(str + offset, c, length - offset); + if (found) + return (size_t)MDNS_POINTER_DIFF(found, str); + return MDNS_INVALID_POS; +} + +static inline void* +mdns_string_make_ref(void* data, size_t capacity, size_t ref_offset) { + if (capacity < 2) + return 0; + return mdns_htons(data, 0xC000 | (uint16_t)ref_offset); +} + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table) { + size_t last_pos = 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (name[length - 1] == '.') + --length; + while (last_pos < length) { + size_t pos = mdns_string_find(name, length, '.', last_pos); + size_t sub_length = ((pos != MDNS_INVALID_POS) ? pos : length) - last_pos; + size_t total_length = length - last_pos; + + size_t ref_offset = + mdns_string_table_find(string_table, buffer, capacity, + (char*)MDNS_POINTER_OFFSET(name, last_pos), sub_length, + total_length); + if (ref_offset != MDNS_INVALID_POS) + return mdns_string_make_ref(data, remain, ref_offset); + + if (remain <= (sub_length + 1)) + return 0; + + *(unsigned char*)data = (unsigned char)sub_length; + memcpy(MDNS_POINTER_OFFSET(data, 1), name + last_pos, sub_length); + mdns_string_table_add(string_table, MDNS_POINTER_DIFF(data, buffer)); + + data = MDNS_POINTER_OFFSET(data, sub_length + 1); + last_pos = ((pos != MDNS_INVALID_POS) ? pos + 1 : length); + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + } + + if (!remain) + return 0; + + *(unsigned char*)data = 0; + return MDNS_POINTER_OFFSET(data, 1); +} + +static inline size_t +mdns_records_parse(int sock, const struct sockaddr* from, size_t addrlen, const void* buffer, + size_t size, size_t* offset, mdns_entry_type_t type, uint16_t query_id, + size_t records, mdns_record_callback_fn callback, void* user_data) { + size_t parsed = 0; + for (size_t i = 0; i < records; ++i) { + size_t name_offset = *offset; + mdns_string_skip(buffer, size, offset); + if (((*offset) + 10) > size) + return parsed; + size_t name_length = (*offset) - name_offset; + const uint16_t* data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, *offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + + *offset += 10; + + if (length <= (size - (*offset))) { + ++parsed; + if (callback && + callback(sock, from, addrlen, type, query_id, rtype, rclass, ttl, buffer, size, + name_offset, name_length, *offset, length, user_data)) + break; + } + + *offset += length; + } + return parsed; +} + +static inline int +mdns_unicast_send(int sock, const void* address, size_t address_size, const void* buffer, + size_t size) { + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, (const struct sockaddr*)address, + (socklen_t)address_size) < 0) + return -1; + return 0; +} + +static inline int +mdns_multicast_send(int sock, const void* buffer, size_t size) { + struct sockaddr_storage addr_storage; + struct sockaddr_in addr; + struct sockaddr_in6 addr6; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(struct sockaddr_storage); + if (getsockname(sock, saddr, &saddrlen)) + return -1; + if (saddr->sa_family == AF_INET6) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; +#ifdef __APPLE__ + addr6.sin6_len = sizeof(addr6); +#endif + addr6.sin6_addr.s6_addr[0] = 0xFF; + addr6.sin6_addr.s6_addr[1] = 0x02; + addr6.sin6_addr.s6_addr[15] = 0xFB; + addr6.sin6_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr6; + saddrlen = sizeof(addr6); + } else { + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; +#ifdef __APPLE__ + addr.sin_len = sizeof(addr); +#endif + addr.sin_addr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + addr.sin_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr; + saddrlen = sizeof(addr); + } + + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, saddr, saddrlen) < 0) + return -1; + return 0; +} + +static const uint8_t mdns_services_query[] = { + // Query ID + 0x00, 0x00, + // Flags + 0x00, 0x00, + // 1 question + 0x00, 0x01, + // No answer, authority or additional RRs + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // _services._dns-sd._udp.local. + 0x09, '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 0x07, '_', 'd', 'n', 's', '-', 's', 'd', + 0x04, '_', 'u', 'd', 'p', 0x05, 'l', 'o', 'c', 'a', 'l', 0x00, + // PTR record + 0x00, MDNS_RECORDTYPE_PTR, + // QU (unicast response) and class IN + 0x80, MDNS_CLASS_IN}; + +static inline int +mdns_discovery_send(int sock) { + return mdns_multicast_send(sock, mdns_services_query, sizeof(mdns_services_query)); +} + +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + size_t records = 0; + const uint16_t* data = (uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + // According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case) + if (query_id || (flags != 0x8400)) + return 0; // Not a reply to our question + + // It seems some implementations do not fill the correct questions field, + // so ignore this check for now and only validate answer string + // if (questions != 1) + // return 0; + + int i; + for (i = 0; i < questions; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's our question, _services._dns-sd._udp.local. + if (!mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + + // Make sure we get a reply based on our PTR question for class IN + if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN)) + return 0; + } + + for (i = 0; i < answer_rrs; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's an answer to our question, _services._dns-sd._udp.local. + size_t name_offset = offset; + int is_answer = mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset); + if (!is_answer && !mdns_string_skip(buffer, data_size, &offset)) + break; + size_t name_length = offset - name_offset; + if ((offset + 10) > data_size) + return records; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + if (length > (data_size - offset)) + return 0; + + if (is_answer) { + ++records; + offset = MDNS_POINTER_DIFF(data, buffer); + if (callback && + callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_ANSWER, query_id, rtype, rclass, ttl, + buffer, data_size, name_offset, name_length, offset, length, user_data)) + return records; + } + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(data, length); + } + + size_t total_records = records; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + size_t records; + size_t total_records = 0; + for (int iquestion = 0; iquestion < questions; ++iquestion) { + size_t question_offset = MDNS_POINTER_DIFF(data, buffer); + size_t offset = question_offset; + size_t verify_offset = 12; + int dns_sd = 0; + if (mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) { + dns_sd = 1; + } else if (!mdns_string_skip(buffer, data_size, &offset)) { + break; + } + size_t length = offset - question_offset; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint16_t class_without_flushbit = rclass & ~MDNS_CACHE_FLUSH; + + // Make sure we get a question of class IN or ANY + if (!((class_without_flushbit == MDNS_CLASS_IN) || + (class_without_flushbit == MDNS_CLASS_ANY))) { + break; + } + + if (dns_sd && flags) + continue; + + ++total_records; + if (callback && callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_QUESTION, query_id, rtype, + rclass, 0, buffer, data_size, question_offset, length, + question_offset, length, user_data)) + return total_records; + } + + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + + return total_records; +} + +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id) { + mdns_query_t query; + query.type = type; + query.name = name; + query.length = length; + return mdns_multiquery_send(sock, &query, 1, buffer, capacity, query_id); +} + +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, size_t capacity, + uint16_t query_id) { + if (!count || (capacity < (sizeof(struct mdns_header_t) + (6 * count)))) + return -1; + + // Ask for a unicast response since it's a one-shot query + uint16_t rclass = MDNS_CLASS_IN | MDNS_UNICAST_RESPONSE; + + struct sockaddr_storage addr_storage; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(addr_storage); + if (getsockname(sock, saddr, &saddrlen) == 0) { + if ((saddr->sa_family == AF_INET) && + (ntohs(((struct sockaddr_in*)saddr)->sin_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + else if ((saddr->sa_family == AF_INET6) && + (ntohs(((struct sockaddr_in6*)saddr)->sin6_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + } + + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + // Query ID + header->query_id = htons((unsigned short)query_id); + // Flags + header->flags = 0; + // Questions + header->questions = htons((unsigned short)count); + // No answer, authority or additional RRs + header->answer_rrs = 0; + header->authority_rrs = 0; + header->additional_rrs = 0; + // Fill in questions + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + for (size_t iq = 0; iq < count; ++iq) { + // Name string + data = mdns_string_make(buffer, capacity, data, query[iq].name, query[iq].length, 0); + if (!data) + return -1; + // Record type + data = mdns_htons(data, query[iq].type); + //! Optional unicast response based on local port, class IN + data = mdns_htons(data, rclass); + } + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + if (mdns_multicast_send(sock, buffer, (size_t)tosend)) + return -1; + return query_id; +} + +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int only_query_id) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + (void)sizeof(flags); + + if ((only_query_id > 0) && (query_id != only_query_id)) + return 0; // Not a reply to the wanted one-shot query + + // Skip questions part + int i; + for (i = 0; i < questions; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + if (!mdns_string_skip(buffer, data_size, &offset)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + // Record type and class not used, skip + // uint16_t rtype = mdns_ntohs(data++); + // uint16_t rclass = mdns_ntohs(data++); + data += 2; + } + + size_t records = 0; + size_t total_records = 0; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline void* +mdns_answer_add_question_unicast(void* buffer, size_t capacity, void* data, + mdns_record_type_t record_type, const char* name, + size_t name_length, mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, name, name_length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 4) + return 0; + + data = mdns_htons(data, record_type); + data = mdns_htons(data, MDNS_UNICAST_RESPONSE | MDNS_CLASS_IN); + + return data; +} + +static inline void* +mdns_answer_add_record_header(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, record.name.str, record.name.length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 10) + return 0; + + data = mdns_htons(data, record.type); + data = mdns_htons(data, record.rclass); + data = mdns_htonl(data, record.ttl); + data = mdns_htons(data, 0); // Length, to be filled later + return data; +} + +static inline void* +mdns_answer_add_record(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + // TXT records will be coalesced into one record later + if (!data || (record.type == MDNS_RECORDTYPE_TXT)) + return data; + + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return 0; + + // Pointer to length of record to be filled at end + void* record_length = MDNS_POINTER_OFFSET(data, -2); + void* record_data = data; + + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + switch (record.type) { + case MDNS_RECORDTYPE_PTR: + data = mdns_string_make(buffer, capacity, data, record.data.ptr.name.str, + record.data.ptr.name.length, string_table); + break; + + case MDNS_RECORDTYPE_SRV: + if (remain <= 6) + return 0; + data = mdns_htons(data, record.data.srv.priority); + data = mdns_htons(data, record.data.srv.weight); + data = mdns_htons(data, record.data.srv.port); + data = mdns_string_make(buffer, capacity, data, record.data.srv.name.str, + record.data.srv.name.length, string_table); + break; + + case MDNS_RECORDTYPE_A: + if (remain < 4) + return 0; + memcpy(data, &record.data.a.addr.sin_addr.s_addr, 4); + data = MDNS_POINTER_OFFSET(data, 4); + break; + + case MDNS_RECORDTYPE_AAAA: + if (remain < 16) + return 0; + memcpy(data, &record.data.aaaa.addr.sin6_addr, 16); // ipv6 address + data = MDNS_POINTER_OFFSET(data, 16); + break; + + default: + break; + } + + if (!data) + return 0; + + // Fill record length + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + return data; +} + +static inline void +mdns_record_update_rclass_ttl(mdns_record_t* record, uint16_t rclass, uint32_t ttl) { + if (!record->rclass) + record->rclass = rclass; + if (!record->ttl || !ttl) + record->ttl = ttl; + record->rclass &= (uint16_t)(MDNS_CLASS_IN | MDNS_CACHE_FLUSH); + // Never flush PTR record + if (record->type == MDNS_RECORDTYPE_PTR) + record->rclass &= ~(uint16_t)MDNS_CACHE_FLUSH; +} + +static inline void* +mdns_answer_add_txt_record(void* buffer, size_t capacity, void* data, const mdns_record_t* records, + size_t record_count, uint16_t rclass, uint32_t ttl, + mdns_string_table_t* string_table) { + // Pointer to length of record to be filled at end + void* record_length = 0; + void* record_data = 0; + + size_t remain = 0; + for (size_t irec = 0; data && (irec < record_count); ++irec) { + if (records[irec].type != MDNS_RECORDTYPE_TXT) + continue; + + mdns_record_t record = records[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + if (!record_data) { + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return data; + record_length = MDNS_POINTER_OFFSET(data, -2); + record_data = data; + } + + // TXT strings are unlikely to be shared, just make then raw. Also need one byte for + // termination, thus the <= check + size_t string_length = record.data.txt.key.length + record.data.txt.value.length + 1; + if (!data) + return 0; + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if ((remain <= string_length) || (string_length > 0x3FFF)) + return 0; + + unsigned char* strdata = (unsigned char*)data; + *strdata++ = (unsigned char)string_length; + memcpy(strdata, record.data.txt.key.str, record.data.txt.key.length); + strdata += record.data.txt.key.length; + *strdata++ = '='; + memcpy(strdata, record.data.txt.value.str, record.data.txt.value.length); + strdata += record.data.txt.value.length; + + data = strdata; + } + + // Fill record length + if (record_data) + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + + return data; +} + +static inline uint16_t +mdns_answer_get_record_count(const mdns_record_t* records, size_t record_count) { + // TXT records will be coalesced into one record + uint16_t total_count = 0; + uint16_t txt_record = 0; + for (size_t irec = 0; irec < record_count; ++irec) { + if (records[irec].type == MDNS_RECORDTYPE_TXT) + txt_record = 1; + else + ++total_count; + } + return total_count + txt_record; +} + +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // According to RFC 6762: + // The cache-flush bit MUST NOT be set in any resource records in a response message + // sent in legacy unicast responses to UDP ports other than 5353. + uint16_t rclass = MDNS_CLASS_IN; + uint32_t ttl = 10; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = htons(query_id); + header->flags = htons(0x8400); + header->questions = htons(1); + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in question + data = mdns_answer_add_question_unicast(buffer, capacity, data, record_type, name, name_length, + &string_table); + + // Fill in answer + answer.rclass = rclass; + answer.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, answer, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + mdns_record_t record = authority[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + mdns_record_t record = additional[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_unicast_send(sock, address, address_size, buffer, tosend); +} + +static inline int +mdns_answer_multicast_rclass_ttl(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count, + uint16_t rclass, uint32_t ttl) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = 0; + header->flags = htons(0x8400); + header->questions = 0; + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in answer + mdns_record_t record = answer; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + record = authority[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + record = additional[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_multicast_send(sock, buffer, tosend); +} + +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 60); +} + +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN | MDNS_CACHE_FLUSH, 60); +} + +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + // Goodbye should have ttl of 0 + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 0); +} + +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + // PTR record is just a string + if ((size >= offset + length) && (length >= 2)) + return mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + mdns_string_t empty = {0, 0}; + return empty; +} + +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + mdns_record_srv_t srv; + memset(&srv, 0, sizeof(mdns_record_srv_t)); + // Read the service priority, weight, port number and the discovery name + // SRV record format (http://www.ietf.org/rfc/rfc2782.txt): + // 2 bytes network-order unsigned priority + // 2 bytes network-order unsigned weight + // 2 bytes network-order unsigned port + // string: discovery (domain) name, minimum 2 bytes when compressed + if ((size >= offset + length) && (length >= 8)) { + const uint16_t* recorddata = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + srv.priority = mdns_ntohs(recorddata++); + srv.weight = mdns_ntohs(recorddata++); + srv.port = mdns_ntohs(recorddata++); + offset += 6; + srv.name = mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + } + return srv; +} + +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr) { + memset(addr, 0, sizeof(struct sockaddr_in)); + addr->sin_family = AF_INET; +#ifdef __APPLE__ + addr->sin_len = sizeof(struct sockaddr_in); +#endif + if ((size >= offset + length) && (length == 4)) + memcpy(&addr->sin_addr.s_addr, MDNS_POINTER_OFFSET(buffer, offset), 4); + return addr; +} + +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr) { + memset(addr, 0, sizeof(struct sockaddr_in6)); + addr->sin6_family = AF_INET6; +#ifdef __APPLE__ + addr->sin6_len = sizeof(struct sockaddr_in6); +#endif + if ((size >= offset + length) && (length == 16)) + memcpy(&addr->sin6_addr, MDNS_POINTER_OFFSET(buffer, offset), 16); + return addr; +} + +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity) { + size_t parsed = 0; + const char* strdata; + size_t end = offset + length; + + if (size < end) + end = size; + + while ((offset < end) && (parsed < capacity)) { + strdata = (const char*)MDNS_POINTER_OFFSET(buffer, offset); + size_t sublength = *(const unsigned char*)strdata; + + if (sublength >= (end - offset)) + break; + + ++strdata; + offset += sublength + 1; + + size_t separator = sublength; + for (size_t c = 0; c < sublength; ++c) { + // DNS-SD TXT record keys MUST be printable US-ASCII, [0x20, 0x7E] + if ((strdata[c] < 0x20) || (strdata[c] > 0x7E)) { + separator = 0; + break; + } + if (strdata[c] == '=') { + separator = c; + break; + } + } + + if (!separator) + continue; + + if (separator < sublength) { + records[parsed].key.str = strdata; + records[parsed].key.length = separator; + records[parsed].value.str = strdata + separator + 1; + records[parsed].value.length = sublength - (separator + 1); + } else { + records[parsed].key.str = strdata; + records[parsed].key.length = sublength; + records[parsed].value.str = 0; + records[parsed].value.length = 0; + } + + ++parsed; + } + + return parsed; +} + +#ifdef _WIN32 +#undef strncasecmp +#endif + +#ifdef __cplusplus +} +#endif diff --git a/core/backends/lan/mdns_wrapper.cpp b/core/backends/lan/mdns_wrapper.cpp new file mode 100644 index 000000000..e92f21065 --- /dev/null +++ b/core/backends/lan/mdns_wrapper.cpp @@ -0,0 +1,796 @@ +/** + * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "mdns_wrapper.h" + +#include "core_debug.h" + +#include "mdns.h" + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#endif + +#include +#include + +const char *recordTypeToStr(int rtype) +{ + switch (rtype) { + case MDNS_RECORDTYPE_PTR: return "PTR"; + case MDNS_RECORDTYPE_SRV: return "SRV"; + case MDNS_RECORDTYPE_TXT: return "TXT"; + case MDNS_RECORDTYPE_A: return "A"; + case MDNS_RECORDTYPE_AAAA: return "AAAA"; + case MDNS_RECORDTYPE_ANY: return "ANY"; + default: return "UNKNOWN"; + } +} + +const char *entryTypeToStr(int entry) +{ + switch (entry) { + case MDNS_ENTRYTYPE_QUESTION: return "QUESTION"; + case MDNS_ENTRYTYPE_ANSWER: return "ANSWER"; + case MDNS_ENTRYTYPE_AUTHORITY: return "AUTHORITY"; + case MDNS_ENTRYTYPE_ADDITIONAL: return "ADDITIONAL"; + default: return "UNKNOWN"; + } +} + +// Callback that handles responses to a query +static int query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry_type, + uint16_t query_id, uint16_t record_type, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + Q_UNUSED(sock); + Q_UNUSED(addrlen); + Q_UNUSED(query_id); + Q_UNUSED(entry_type); + Q_UNUSED(rclass); + Q_UNUSED(ttl); + Q_UNUSED(name_offset); + Q_UNUSED(name_length); + + //qCDebug(KDECONNECT_CORE) << "Received DNS record of type" << recordTypeToStr(record_type) << "from socket" << sock << "with type" << entryTypeToStr(entry_type); + + MdnsWrapper::MdnsService *discoveredService = (MdnsWrapper::MdnsService *)user_data; + + switch (record_type) { + case MDNS_RECORDTYPE_PTR: { + mdns_string_pair_t namePos = mdns_get_next_substring(data, size, record_offset); + discoveredService->serviceName = QString::fromLatin1((char *)data + namePos.offset, namePos.length); + if (discoveredService->address == QHostAddress::Null) { + discoveredService->address = QHostAddress(from); // In case we don't receive a A record, use from as address + } + } break; + case MDNS_RECORDTYPE_SRV: { + static char auxBuffer[256]; + mdns_record_srv_t record = mdns_record_parse_srv(data, size, record_offset, record_length, auxBuffer, sizeof(auxBuffer)); + discoveredService->port = record.port; + } break; + case MDNS_RECORDTYPE_A: { + sockaddr_in addr; + mdns_record_parse_a(data, size, record_offset, record_length, &addr); + discoveredService->address = QHostAddress(ntohl(addr.sin_addr.s_addr)); + } break; + case MDNS_RECORDTYPE_AAAA: + // Ignore IPv6 for now + break; + case MDNS_RECORDTYPE_TXT: { + mdns_record_txt_t records[24]; + size_t parsed = mdns_record_parse_txt(data, size, record_offset, record_length, records, sizeof(records) / sizeof(mdns_record_txt_t)); + for (size_t itxt = 0; itxt < parsed; ++itxt) { + QString key = QString::fromLatin1(records[itxt].key.str, records[itxt].key.length); + QString value = QString::fromLatin1(records[itxt].value.str, records[itxt].value.length); + discoveredService->txtRecords[key] = value; + } + } break; + default: + // Ignore unknown record types + break; + } + + return 0; +} + +void MdnsWrapper::startDiscovering(const char *serviceName) +{ + int num_sockets = listenForQueryResponses(); + if (num_sockets <= 0) { + qWarning() << "Failed to open any client sockets"; + } else { + sendQuery(serviceName); + } +} + +void MdnsWrapper::stopDiscovering() +{ + stopListeningForQueryResponses(); +} + +void MdnsWrapper::stopListeningForQueryResponses() +{ + qCDebug(KDECONNECT_CORE) << "Closing" << responseSocketNotifiers.size() << "sockets"; + for (QSocketNotifier *socketNotifier : responseSocketNotifiers) { + mdns_socket_close(socketNotifier->socket()); + delete socketNotifier; + } + responseSocketNotifiers.clear(); +} + +int MdnsWrapper::listenForQueryResponses() +{ + // Open a socket for each interface + QVector sockets; + for (const QNetworkInterface &iface : QNetworkInterface::allInterfaces()) { + int flags = iface.flags(); + if (!(flags & QNetworkInterface::IsUp) || !(flags & QNetworkInterface::CanMulticast) || (flags & QNetworkInterface::IsLoopBack)) { + continue; + } + for (const QNetworkAddressEntry &ifaceAddress : iface.addressEntries()) { + QHostAddress sourceAddress = ifaceAddress.ip(); + if (sourceAddress.protocol() == QAbstractSocket::IPv4Protocol && sourceAddress != QHostAddress::LocalHost) { + qCDebug(KDECONNECT_CORE) << "Opening socket for address" << sourceAddress; + struct sockaddr_in saddr; + memset(&saddr, 0, sizeof(saddr)); + saddr.sin_family = AF_INET; + saddr.sin_port = 0; + saddr.sin_addr.s_addr = htonl(sourceAddress.toIPv4Address()); + int socket = mdns_socket_open_ipv4(&saddr); + sockets.append(socket); + } + // Ignore IPv6 interfaces for now + } + } + + // Start listening on all sockets + for (int socket : sockets) { + QSocketNotifier *socketNotifier = new QSocketNotifier(socket, QSocketNotifier::Read); + QObject::connect(socketNotifier, &QSocketNotifier::activated, [this](QSocketDescriptor socket) { + MdnsService discoveredService; + + size_t capacity = 2048; + void *buffer = malloc(capacity); + size_t num_records = mdns_query_recv(socket, buffer, capacity, query_callback, (void *)&discoveredService, 0); + free(buffer); + + // qCDebug(KDECONNECT_CORE) << "Discovered service" << discoveredService.serviceName << "at" << discoveredService.address << "in" << num_records << + // "records via socket" << socket; + + Q_EMIT serviceFound(discoveredService); + }); + responseSocketNotifiers.append(socketNotifier); + } + + qCDebug(KDECONNECT_CORE) << "Opened" << sockets.size() << "sockets to listen for MDNS query responses"; + + return sockets.size(); +} + +void MdnsWrapper::sendQuery(const char *serviceName) +{ + qCDebug(KDECONNECT_CORE) << "Sending MDNS query for service" << serviceName; + + mdns_query_t query; + query.name = serviceName; + query.length = strlen(serviceName); + query.type = MDNS_RECORDTYPE_PTR; + + size_t capacity = 2048; + void *buffer = malloc(capacity); + for (QSocketNotifier *socketNotifier : responseSocketNotifiers) { + int socket = socketNotifier->socket(); + qCDebug(KDECONNECT_CORE) << "Sending mDNS query via socket" << socket; + int ret = mdns_multiquery_send(socket, &query, 1, buffer, capacity, 0); + if (ret < 0) { + qWarning() << "Failed to send mDNS query:" << strerror(errno); + } + } + free(buffer); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// Those are the IPs we will announce. They are the first IP we find. Maybe we should announce all our IPs instead? +struct sockaddr_in service_address_ipv4; +struct sockaddr_in6 service_address_ipv6; + + +static mdns_string_t ipv4_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in* addr, + size_t addrlen) { + char host[NI_MAXHOST] = {0}; + char service[NI_MAXSERV] = {0}; + int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST, + service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST); + int len = 0; + if (ret == 0) { + if (addr->sin_port != 0) + len = snprintf(buffer, capacity, "%s:%s", host, service); + else + len = snprintf(buffer, capacity, "%s", host); + } + if (len >= (int)capacity) + len = (int)capacity - 1; + mdns_string_t str; + str.str = buffer; + str.length = len; + return str; +} + +static mdns_string_t ipv6_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in6* addr, + size_t addrlen) { + char host[NI_MAXHOST] = {0}; + char service[NI_MAXSERV] = {0}; + int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST, + service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST); + int len = 0; + if (ret == 0) { + if (addr->sin6_port != 0) + len = snprintf(buffer, capacity, "[%s]:%s", host, service); + else + len = snprintf(buffer, capacity, "%s", host); + } + if (len >= (int)capacity) + len = (int)capacity - 1; + mdns_string_t str; + str.str = buffer; + str.length = len; + return str; +} + +static mdns_string_t ip_address_to_string(char* buffer, size_t capacity, const struct sockaddr* addr, size_t addrlen) { + if (addr->sa_family == AF_INET6) + return ipv6_address_to_string(buffer, capacity, (const struct sockaddr_in6*)addr, addrlen); + return ipv4_address_to_string(buffer, capacity, (const struct sockaddr_in*)addr, addrlen); +} + + + +char service_instance_buffer[256] = {0}; +char qualified_hostname_buffer[256] = {0}; + +// Data for our service including the mDNS records +typedef struct { + mdns_string_t service; + mdns_string_t hostname; + mdns_string_t service_instance; + mdns_string_t hostname_qualified; + struct sockaddr_in address_ipv4; + struct sockaddr_in6 address_ipv6; + int port; + mdns_record_t record_ptr; + mdns_record_t record_srv; + mdns_record_t record_a; + mdns_record_t record_aaaa; + mdns_record_t txt_record[2]; +} service_t; +service_t service = {}; + +// Open sockets to listen to incoming mDNS queries on port 5353 +static int open_service_sockets(int* sockets) +{ + // When recieving, each socket can recieve data from all network interfaces + // Thus we only need to open one socket for each address family + int num_sockets = 0; + + { + struct sockaddr_in sock_addr; + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; + #ifdef _WIN32 + sock_addr.sin_addr = in4addr_any; + #else + sock_addr.sin_addr.s_addr = INADDR_ANY; + #endif + sock_addr.sin_port = htons(MDNS_PORT); + #ifdef __APPLE__ + sock_addr.sin_len = sizeof(struct sockaddr_in); + #endif + int sock = mdns_socket_open_ipv4(&sock_addr); + if (sock >= 0) + sockets[num_sockets++] = sock; + } + + { + struct sockaddr_in6 sock_addr; + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; + sock_addr.sin6_port = htons(MDNS_PORT); + #ifdef __APPLE__ + sock_addr.sin6_len = sizeof(struct sockaddr_in6); + #endif + int sock = mdns_socket_open_ipv6(&sock_addr); + if (sock >= 0) + sockets[num_sockets++] = sock; + } + + return num_sockets; +} + +static char sendbuffer[1024]; + +// Callback handling questions incoming on service sockets +static int service_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype_int, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + + static mdns_record_txt_t txtbuffer[128]; + static char addrbuffer[64]; + static char entrybuffer[256]; + static char namebuffer[256]; + + mdns_record_type_t rtype = (mdns_record_type_t)rtype_int; + + (void)sizeof(ttl); + if (entry != MDNS_ENTRYTYPE_QUESTION) + return 0; + + const char dns_sd[] = "_services._dns-sd._udp.local."; + + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); + + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer)); + + const char* record_name = 0; + if (rtype == MDNS_RECORDTYPE_PTR) + record_name = "PTR"; + else if (rtype == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (rtype == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (rtype == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else if (rtype == MDNS_RECORDTYPE_TXT) + record_name = "TXT"; + else if (rtype == MDNS_RECORDTYPE_ANY) + record_name = "ANY"; + else + return 0; + printf("Query %s %.*s\n", record_name, MDNS_STRING_FORMAT(name)); + + if ((name.length == (sizeof(dns_sd) - 1)) && + (strncmp(name.str, dns_sd, sizeof(dns_sd) - 1) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for the DNS-SD domain, send answer with a PTR record for the + // service name we advertise, typically on the "<_service-name>._tcp.local." format + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = { + .name = name, .type = MDNS_RECORDTYPE_PTR, .data.ptr.name = service.service}; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s (%s)\n", MDNS_STRING_FORMAT(answer.data.ptr.name), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, (void*)from, addrlen, (void*)sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, NULL, 0, NULL, 0); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, 0, + 0); + } + } + } else if ((name.length == service.service.length) && + (strncmp(name.str, service.service.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for our service (usually "<_service-name._tcp.local"), answer a PTR + // record reverse mapping the queried service name to our service instance name + // (typically on the ".<_service-name>._tcp.local." format), and add + // additional records containing the SRV record mapping the service instance name to our + // qualified hostname (typically ".local.") and port, as well as any IPv4/IPv6 + // address for the hostname as A/AAAA records, and two test TXT records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service.record_ptr; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + additional[additional_count++] = service.record_srv; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s (%s)\n", + MDNS_STRING_FORMAT(service.record_ptr.data.ptr.name), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else if ((name.length == service.service_instance.length) && + (strncmp(name.str, service.service_instance.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_SRV) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The SRV query was for our service instance (usually + // ".<_service-name._tcp.local"), answer a SRV record mapping the service + // instance name to our qualified hostname (typically ".local.") and port, as + // well as any IPv4/IPv6 address for the hostname as A/AAAA records, and two test TXT + // records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service.record_srv; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s port %d (%s)\n", + MDNS_STRING_FORMAT(service.record_srv.data.srv.name), service.port, + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else { + + qWarning() << "Is this for me?" << name.str << "VS" << service.hostname_qualified.str << "i" << service.address_ipv4.sin_family; + + if ((name.length == service.hostname_qualified.length) && + (strncmp(name.str, service.hostname_qualified.str, name.length) == 0)) { + if (((rtype == MDNS_RECORDTYPE_A) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service.address_ipv4.sin_family == AF_INET)) { + // The A query was for our qualified hostname (typically ".local.") and we + // have an IPv4 address, answer with an A record mappiing the hostname to an IPv4 + // address, as well as any IPv6 address for the hostname, and two test TXT records + + // Answer A records mapping ".local." to IPv4 address + mdns_record_t answer = service.record_a; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // AAAA record mapping ".local." to IPv6 addresses + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = ip_address_to_string( + addrbuffer, sizeof(addrbuffer), (struct sockaddr*)&service.record_a.data.a.addr, + sizeof(service.record_a.data.a.addr)); + printf(" --> answer %.*s IPv4 %.*s (%s)\n", MDNS_STRING_FORMAT(service.record_a.name), + MDNS_STRING_FORMAT(addrstr), (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } else if (((rtype == MDNS_RECORDTYPE_AAAA) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service.address_ipv6.sin6_family == AF_INET6)) { + // The AAAA query was for our qualified hostname (typically ".local.") and we + // have an IPv6 address, answer with an AAAA record mappiing the hostname to an IPv6 + // address, as well as any IPv4 address for the hostname, and two test TXT records + + // Answer AAAA records mapping ".local." to IPv6 address + mdns_record_t answer = service.record_aaaa; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A record mapping ".local." to IPv4 addresses + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = + ip_address_to_string(addrbuffer, sizeof(addrbuffer), + (struct sockaddr*)&service.record_aaaa.data.aaaa.addr, + sizeof(service.record_aaaa.data.aaaa.addr)); + printf(" --> answer %.*s IPv6 %.*s (%s)\n", + MDNS_STRING_FORMAT(service.record_aaaa.name), MDNS_STRING_FORMAT(addrstr), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } + } + return 0; +} + + +int sockets[32]; + +int MdnsWrapper::startAnnouncing(const char* hostname, const char* service_name, int service_port) +{ + int num_sockets = open_service_sockets(sockets); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for mDNS service\n", num_sockets, num_sockets ? "s" : ""); + + size_t service_name_length = strlen(service_name); + if (!service_name_length) { + printf("Invalid service name\n"); + return -1; + } + + char* service_name_buffer = (char*)malloc(service_name_length + 2); + memcpy(service_name_buffer, service_name, service_name_length); + if (service_name_buffer[service_name_length - 1] != '.') + service_name_buffer[service_name_length++] = '.'; + service_name_buffer[service_name_length] = 0; + service_name = service_name_buffer; + + printf("Service mDNS: %s:%d\n", service_name, service_port); + printf("Hostname: %s\n", hostname); + + size_t capacity = 2048; + void* buffer = malloc(capacity); + + mdns_string_t service_string = (mdns_string_t){service_name, strlen(service_name)}; + mdns_string_t hostname_string = (mdns_string_t){hostname, strlen(hostname)}; + + // Build the service instance ".<_service-name>._tcp.local." string + snprintf(service_instance_buffer, sizeof(service_instance_buffer) - 1, "%.*s.%.*s", + MDNS_STRING_FORMAT(hostname_string), MDNS_STRING_FORMAT(service_string)); + + qWarning() << service_instance_buffer; + // Build the ".local." string + + snprintf(qualified_hostname_buffer, sizeof(qualified_hostname_buffer) - 1, "%.*s.local.", + MDNS_STRING_FORMAT(hostname_string)); + + memset(&service, 0, sizeof(service)); + service.service.str = service_name; + service.service.length = strlen(service_name); + service.hostname.str = hostname; + service.hostname.length = strlen(hostname); + service.service_instance.str = service_instance_buffer; + service.service_instance.length = strlen(service_instance_buffer); + service.hostname_qualified.str = qualified_hostname_buffer; + service.hostname_qualified.length = strlen(qualified_hostname_buffer); + service.address_ipv4 = service_address_ipv4; + service.address_ipv6 = service_address_ipv6; + service.port = service_port; + + // Setup our mDNS records + + // PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + service.record_ptr = (mdns_record_t){.name = service.service, + .type = MDNS_RECORDTYPE_PTR, + .data.ptr.name = service.service_instance, + .rclass = 0, + .ttl = 0}; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + service.record_srv = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_SRV, + .data.srv.name = service.hostname_qualified, + .data.srv.port = (uint16_t)service.port, + .data.srv.priority = 0, + .data.srv.weight = 0, + .rclass = 0, + .ttl = 0}; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + service.record_a = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_A, + .data.a.addr = service.address_ipv4, + .rclass = 0, + .ttl = 0}; + + service.record_aaaa = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_AAAA, + .data.aaaa.addr = service.address_ipv6, + .rclass = 0, + .ttl = 0}; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + service.txt_record[0] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("test")}, + .data.txt.value = {MDNS_STRING_CONST("1")}, + .rclass = 0, + .ttl = 0}; + service.txt_record[1] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("other")}, + .data.txt.value = {MDNS_STRING_CONST("value")}, + .rclass = 0, + .ttl = 0}; + + // Send an announcement on startup of service + { + printf("Sending announce\n"); + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_announce_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, + additional, additional_count); + } + + // This is a crude implementation that checks for incoming queries + while (true) { + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(nfds, &readfs, 0, 0, &timeout) >= 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + mdns_socket_listen(sockets[isock], buffer, capacity, service_callback, + &service); + } + FD_SET(sockets[isock], &readfs); + } + } else { + break; + } + } + + free(buffer); + free(service_name_buffer); + + return 0; +} + + + + +void MdnsWrapper::stopAnnouncing() +{ + printf("Sending goodbye\n"); + mdns_record_t additional[5] = {}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + size_t capacity = 2048; + void *buffer = malloc(capacity); + + int num_sockets = 2; // FIXME + for (int isock = 0; isock < num_sockets; ++isock) { + mdns_goodbye_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, additional, additional_count); + } + + for (int isock = 0; isock < num_sockets; ++isock) { + mdns_socket_close(sockets[isock]); + } + + free(buffer); + printf("Closed socket%s\n", num_sockets ? "s" : ""); +} diff --git a/core/backends/lan/mdns_wrapper.h b/core/backends/lan/mdns_wrapper.h new file mode 100644 index 000000000..ac01fff69 --- /dev/null +++ b/core/backends/lan/mdns_wrapper.h @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef KDECONNECT_MDNS_WRAPPER_H +#define KDECONNECT_MDNS_WRAPPER_H + +#include +#include +#include +#include +#include + +/* + * A Qt wrapper for the mdns.h header-only library + * from https://github.com/mjansson/mdns + */ +class MdnsWrapper : public QObject +{ + Q_OBJECT + +public: + struct MdnsService { + QString serviceName; + int port; + QHostAddress address; + QMap txtRecords; + }; + + int startAnnouncing(const char *hostname, const char *service_name, int service_port); + void stopAnnouncing(); + + void startDiscovering(const char *serviceName); + void stopDiscovering(); + + void sendQuery(const char *serviceName); + +Q_SIGNALS: + void serviceFound(const MdnsService &service); + +private: + int listenForQueryResponses(); + void stopListeningForQueryResponses(); + + QVector responseSocketNotifiers; +}; + +#endif diff --git a/core/backends/lan/mdnsdiscovery.cpp b/core/backends/lan/mdnsdiscovery.cpp index 9232aa2bb..7b8c2667c 100644 --- a/core/backends/lan/mdnsdiscovery.cpp +++ b/core/backends/lan/mdnsdiscovery.cpp @@ -10,26 +10,22 @@ #include "kdeconnectconfig.h" #include "lanlinkprovider.h" -#include -#include -#include +#include "mdns_wrapper.h" -const QString kServiceName = QStringLiteral("_kdeconnect._udp"); +#include +const char *kServiceName = "_kdeconnect._udp.local"; MdnsDiscovery::MdnsDiscovery(LanLinkProvider *lanLinkProvider) : lanLinkProvider(lanLinkProvider) { - switch (KDNSSD::ServiceBrowser::isAvailable()) { - case KDNSSD::ServiceBrowser::Stopped: - qWarning() << "mDNS or Avahi daemons are not running, mDNS discovery not available"; - break; - case KDNSSD::ServiceBrowser::Working: - qCDebug(KDECONNECT_CORE) << "mDNS discovery is available"; - break; - case KDNSSD::ServiceBrowser::Unsupported: - qWarning() << "mDNS discovery not available (library built without DNS-SD support)"; - break; - } + connect(&mdnsWrapper, &MdnsWrapper::serviceFound, [lanLinkProvider](const MdnsWrapper::MdnsService &service) { + if (KdeConnectConfig::instance().deviceId() == service.serviceName) { + qCDebug(KDECONNECT_CORE) << "Discovered myself, ignoring"; + return; + } + qCDebug(KDECONNECT_CORE) << "Discovered" << service.serviceName << "at" << service.address; + lanLinkProvider->sendUdpIdentityPacket(QList{service.address}); + }); } MdnsDiscovery::~MdnsDiscovery() @@ -38,8 +34,13 @@ MdnsDiscovery::~MdnsDiscovery() stopDiscovering(); } +QString hostName = QHostInfo::localHostName(); + void MdnsDiscovery::startAnnouncing() { + // mdnsWrapper.startAnnouncing(hostName.toLatin1().data(), kServiceName, LanLinkProvider::UDP_PORT); + + /* if (m_publisher != nullptr) { qCDebug(KDECONNECT_CORE) << "MDNS already announcing"; return; @@ -68,53 +69,26 @@ void MdnsDiscovery::startAnnouncing() }); m_publisher->publishAsync(); + */ } void MdnsDiscovery::stopAnnouncing() { - if (m_publisher != nullptr) { - qCDebug(KDECONNECT_CORE) << "MDNS stop announcing"; - delete m_publisher; - m_publisher = nullptr; - } + // mdnsWrapper.stopAnnouncing(); + + // if (m_publisher != nullptr) { + // qCDebug(KDECONNECT_CORE) << "MDNS stop announcing"; + // delete m_publisher; + // m_publisher = nullptr; + // } } void MdnsDiscovery::startDiscovering() { - if (m_serviceBrowser != nullptr) { - qCDebug(KDECONNECT_CORE) << "MDNS already discovering"; - return; - } - qCDebug(KDECONNECT_CORE) << "MDNS start discovering"; - - m_serviceBrowser = new KDNSSD::ServiceBrowser(kServiceName, true); - - connect(m_serviceBrowser, &KDNSSD::ServiceBrowser::serviceAdded, [this](KDNSSD::RemoteService::Ptr service) { - if (KdeConnectConfig::instance().deviceId() == service->serviceName()) { - qCDebug(KDECONNECT_CORE) << "Discovered myself, ignoring"; - return; - } - qCDebug(KDECONNECT_CORE) << "Discovered " << service->serviceName() << " at " << service->hostName(); - QHostAddress address(service->hostName()); - lanLinkProvider->sendUdpIdentityPacket(QList{address}); - }); - - connect(m_serviceBrowser, &KDNSSD::ServiceBrowser::serviceRemoved, [](KDNSSD::RemoteService::Ptr service) { - qCDebug(KDECONNECT_CORE) << "Lost " << service->serviceName(); - }); - - connect(m_serviceBrowser, &KDNSSD::ServiceBrowser::finished, []() { - qCDebug(KDECONNECT_CORE) << "Finished discovery"; - }); - - m_serviceBrowser->startBrowse(); + mdnsWrapper.startDiscovering(kServiceName); } void MdnsDiscovery::stopDiscovering() { - if (m_serviceBrowser != nullptr) { - qCDebug(KDECONNECT_CORE) << "MDNS stop discovering"; - delete m_serviceBrowser; - m_serviceBrowser = nullptr; - } + mdnsWrapper.stopDiscovering(); } diff --git a/core/backends/lan/mdnsdiscovery.h b/core/backends/lan/mdnsdiscovery.h index 2fb31df8b..8f03f8f46 100644 --- a/core/backends/lan/mdnsdiscovery.h +++ b/core/backends/lan/mdnsdiscovery.h @@ -11,12 +11,9 @@ #include "kdeconnectcore_export.h" +#include "mdns_wrapper.h" + class LanLinkProvider; -namespace KDNSSD -{ -class PublicService; -class ServiceBrowser; -}; class KDECONNECTCORE_EXPORT MdnsDiscovery : public QObject { @@ -34,8 +31,7 @@ public: private: LanLinkProvider *lanLinkProvider = nullptr; - KDNSSD::PublicService *m_publisher = nullptr; - KDNSSD::ServiceBrowser *m_serviceBrowser = nullptr; + MdnsWrapper mdnsWrapper; }; #endif // KDECONNECT_SERVER_H