From 6d88c555019f32509f303e23dcfbba824fecd2ee Mon Sep 17 00:00:00 2001 From: Chris Hiszpanski Date: Thu, 18 Apr 2019 00:55:30 -0700 Subject: Initial public commit. mDNS and SDP are functional. Otherwise, library is still very much a work in progress. All tests pass. --- src/mdns.c | 607 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 src/mdns.c (limited to 'src/mdns.c') diff --git a/src/mdns.c b/src/mdns.c new file mode 100644 index 0000000..fa782e4 --- /dev/null +++ b/src/mdns.c @@ -0,0 +1,607 @@ +/** + * liburtc + * Copyright 2020 Chris Hiszpanski + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include // IFF_LOOPBACK (include pre ifaddrs.h) +#include // getifaddrs +#include // HOST_NAME_MAX +#include +#include // memcpy +#include // sleep + +#include +#include +#ifdef WITH_IPV6 + #include +#endif +#include // sendto, setsocketopt, socket + +#include "err.h" +#include "log.h" +#include "mdns.h" +#include "uuid.h" + +#define MDNS_ADDR "224.0.0.251" +#define MDNS_PORT 5353 + +#define DEFAULT_TTL 120 // time-to-live recommended by RFC 6762 + +#define CLASS_INTERNET 1 +#define CACHE_FLUSH (1 << 15) + +#define FLAG_RESPONSE (1 << 15) +#define FLAG_OPCODE (0xF << 11) + +#ifndef HOST_NAME_MAX + #define HOST_NAME_MAX 255 +#endif + +struct __attribute__((__packed__)) header { + uint16_t id; + uint16_t flags; + uint16_t n_question; // number of question records + uint16_t n_answer; // number of answer records + uint16_t n_authority; // number of authority records + uint16_t n_additional; // number of additional records +}; + +struct __attribute__((__packed__)) qname { + uint8_t size; + uint8_t name[]; +}; + +struct __attribute__((__packed__)) answer { + // variable length name precedes answer + uint16_t type; + uint16_t class; + uint32_t ttl; + uint16_t size; + uint8_t data[]; +}; + +/** + * Subscribe to mDNS queries + * + * Opens new multicast socket on the mDNS port. This socket receives and can + * be used to send mDNS datagrams. Close with mdns_unsubscribe(). + * + * \return Socket file descriptor on success, negative on error. + */ +int mdns_subscribe() { + // create socket + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (-1 == sockfd) { + log(ERROR, "%s", strerror(errno)); + goto _fail_create_socket; + } + + // enable address/port reuse + { + const unsigned int opt = 1; + if (-1 == setsockopt( + sockfd, + SOL_SOCKET, +#if defined(__APPLE__) || defined(__FreeBSD__) + SO_REUSEPORT, +#else + SO_REUSEADDR, +#endif + &opt, + sizeof(opt) + )) { + log(ERROR, "%s", strerror(errno)); + goto _fail_enable_addr_reuse; + } + } + + // bind socket to mDNS query port + { + const struct sockaddr_in mgroup = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_port = htons(MDNS_PORT) + }; + if (-1 == bind(sockfd, (struct sockaddr *)(&mgroup), sizeof(mgroup))) { + log(ERROR, "%s", strerror(errno)); + goto _fail_bind; + } + } + + // disable loopback + { + const int opt = 0; + if (-1 == setsockopt( + sockfd, + IPPROTO_IP, + IP_MULTICAST_LOOP, + &opt, + sizeof(opt) + )) { + log(ERROR, "setsockopt: %s", strerror(errno)); + goto _fail_disable_loopback; + } + } + + // join multicast group + { + const struct ip_mreqn imr = { + .imr_multiaddr.s_addr = inet_addr(MDNS_ADDR), + .imr_address.s_addr = htonl(INADDR_ANY) + }; + if (-1 == setsockopt( + sockfd, + IPPROTO_IP, + IP_ADD_MEMBERSHIP, + &imr, + sizeof(imr) + )) { + log(ERROR, "setsockopt: %s", strerror(errno)); + goto _fail_join_multicast_group; + } + } + + return sockfd; + +_fail_join_multicast_group: +_fail_disable_loopback: +_fail_bind: +_fail_enable_addr_reuse: + close(sockfd); +_fail_create_socket: + + return -URTC_ERR; +} + +/** + * Unsubscribe from mDNS queries + * + * Close multicast socket previously opened with mdns_subscribe(). + * + * \param sockfd Socket file descriptor returned by mdns_subscribe(). + * + * \return 0 on success, negative on error. + */ +int mdns_unsubscribe(int sockfd) { + if (-1 == close(sockfd)) { + log(ERROR, "close: %s", strerror(errno)); + return -URTC_ERR; + } + + return 0; +} + +/** + * Send a multicast DNS query + * + * \param name Hostname to query. Must end with ".local". + * + * \return 0 on success, negative on error. + */ +int mdns_query(const char *name) { + // create socket + int msockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (-1 == msockfd) { + log(FATAL, "socket: %s", strerror(errno)); + } + + uint8_t q[512]; + size_t qlen = 0; + + // header + struct header hdr = { + .n_question = htons(1) + }; + memcpy(&q[qlen], &hdr, sizeof(hdr)); + qlen += sizeof(hdr); + + // qname + q[qlen] = strlen(name); + memcpy(&q[1 + qlen], name, strlen(name)); + qlen += 1 + q[qlen]; + + q[qlen] = strlen("local"); + memcpy(&q[1 + qlen], "local", strlen("local")); + qlen += 1 + q[qlen]; + + q[qlen++] = 0; // root record + + // qtype: 255 = any + q[qlen++] = 0; + q[qlen++] = 255; + + // qclass: 1 = ipv4 + q[qlen++] = 0x80; // unicast response please + q[qlen++] = 1; + + // send query + struct sockaddr_in mgroup = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr(MDNS_ADDR), + .sin_port = htons(MDNS_PORT) + }; + if (-1 == sendto(msockfd, q, qlen, 0, (struct sockaddr *)(&mgroup), sizeof(mgroup))) { + log(FATAL, "sendto: %s", strerror(errno)); + } + + return 0; +} + +/** + * Simple mDNS response parser (single record only) + * + * \param response mDNS response packet + * \param len Length of response, in bytes + * + * \return 0 on success, negative on error. + */ +int mdns_parse_response(const void *response, size_t len) +{ + // sanity checks + if (!response) return -URTC_ERR_BAD_ARGUMENT; + if (len < sizeof(struct header)) return -URTC_ERR_MALFORMED; + + struct header *hdr = (struct header *)(response); + response += sizeof(struct header); + len -= sizeof(struct header); + + // transaction id must be zero + if (0 != hdr->id) return -URTC_ERR_MALFORMED; + + // flags must indicate a response + if (!(ntohs(hdr->flags) & FLAG_RESPONSE)) return -URTC_ERR_MALFORMED; + + // sent single question, expecting reponse for single question + if (1 != ntohs(hdr->n_question)) return -URTC_ERR_MALFORMED; + + // parse query names + struct qname *qname; + do { + if (len < 1) return -URTC_ERR_MALFORMED; + qname = (struct qname *)(response); + if (len < 1 + qname->size) return -URTC_ERR_MALFORMED; + response += 1 + qname->size; + len -= 1 + qname->size; + } while (qname->size); + + // parse query type (ignore) + if (len < sizeof(uint16_t)) return -URTC_ERR_MALFORMED; + response += sizeof(uint16_t); + len -= sizeof(uint16_t); + + // parse query class (ignore) + if (len < sizeof(uint16_t)) return -URTC_ERR_MALFORMED; + response += sizeof(uint16_t); + len -= sizeof(uint16_t); + + // answers + for (int i = 0; i < ntohs(hdr->n_answer); i++) { + uint16_t size; + uint8_t namelen; + + // parse name (assume matches) + if (len < 2) return -URTC_ERR_MALFORMED; + namelen = *(uint8_t *)response; + switch (namelen) { + // name compression + case 0xc0: + response += 2; + len -= 2; + break; + // no name compression + default: + response += 1 + namelen; + len -= 1 + namelen; + break; + } + + if (len < sizeof(struct answer)) return -URTC_ERR_MALFORMED; + struct answer *answer = (struct answer *)response; + response += sizeof(struct answer); + len -= sizeof(struct answer); + + size = ntohs(answer->size); + if (len < size) return -URTC_ERR_MALFORMED; + response += size; + len -= size; + } + + return 0; +} + +/** + * Check if run-length encoded query name resource records match hostname + * + * \param expected Expected hostname record. + * \param q Length-encoded query names. + * \param len Pointer to minimum length of q. Overwritten with actual length. + * + * \return Positive on match, 0 on no match, negative on malformed query. + */ +static int match(const char *expected, const uint8_t *q, int *len) { + struct qname *qname; + bool matches; + int n_records; + int n; + + if (!q) return -URTC_ERR_BAD_ARGUMENT; + if (!expected) return -URTC_ERR_BAD_ARGUMENT; + if (!len) return -URTC_ERR_BAD_ARGUMENT; + + n = *len; + matches = true; + + n_records = 0; + do { + // minimum size check: a single root record (1 byte) + if (n < 1) return -URTC_ERR_MALFORMED; + qname = (struct qname *)(q); + + // minimum size check: length byte plus stated length + if (n < 1 + qname->size) return -URTC_ERR_MALFORMED; + + // match records + switch (n_records) { + // match hostname record + case 0: + if (qname->size == strlen(expected)) { + if (0 == memcmp(qname->name, expected, qname->size)) { + break; // matched + } + } + matches = false; + break; + // match "local" record + case 1: + if (qname->size == sizeof("local")-1) { + if (0 == memcmp(qname->name, "local", sizeof("local")-1)) { + break; //matched + } + } + matches = false; + break; + // match root record + case 2: + if (qname->size == 0) { + break; + } + matches = false; + break; + // match ancillary records + default: + matches = false; + break; + } + + q += 1 + qname->size; + n -= 1 + qname->size; + n_records++; + } while(qname->size); + + // update with actual length of run-length encoding + *len -= n; + + return matches; +} + +/** + * Validate whether mDNS packet is a query for the specified hostname + * + * Only standard, single question queries are supported. All others + * return an error. + * + * \param q Query buffer (from network) + * \param n Size of query (in bytes) + * \param hostname Queried hostname + * + * \return 0 on success, negative on error + */ +int mdns_validate_query(const uint8_t *q, size_t n, const char *hostname) +{ + const struct header *hdr = (struct header *)q; + const uint8_t *qi = q; + const size_t ni = n; + int found; + + found = 0; + + // sanity checks + if (!q) return -URTC_ERR_BAD_ARGUMENT; + if (n < sizeof(struct header)) return -URTC_ERR_MALFORMED; + + q += sizeof(struct header); + n -= sizeof(struct header); + + // transaction id must be zero + // note: hdr in host byte order (0 is byte order agnostic) + if (0 != hdr->id) return -URTC_ERR_MALFORMED; + + // only support standard, non-truncated, non-recursive queries + // note: hdr in host byte order (non-0 is byte order agnostic) + if (hdr->flags) return -URTC_ERR_NOT_IMPLEMENTED; + + // parse questions + for (int j = 0; j < ntohs(hdr->n_question); j++) { + bool matched; + int ret; + int len; + + // upper two bits set? "compressed" records + matched = false; + if (n > 2 && q[0] >> 6 == 3) { + // compressed (i.e. offset to previous record) + const uint16_t offset = ((q[0] & 0x3F) << 8) | q[1]; + if (offset > ni) return -URTC_ERR_MALFORMED; + len = ni - offset; + ret = match(hostname, qi + offset, &len); + q += 2; + n -= 2; + } else { + // uncompressed + len = n; + ret = match(hostname, q, &len); + q += len; + n -= len; + } + if (ret < 0) return ret; + if (ret > 0) matched = true; + + // parse query type for A and AAAA records + if (n < sizeof(uint16_t)) return -URTC_ERR_MALFORMED; + if (matched) { + uint16_t type = ntohs(*(uint16_t *)(q)); + if (TYPE_A == type || TYPE_AAAA == type) { + found |= type; + } + } + q += sizeof(uint16_t); + n -= sizeof(uint16_t); + + // parse query class (ignore) + if (n < sizeof(uint16_t)) return -URTC_ERR_MALFORMED; + q += sizeof(uint16_t); + n -= sizeof(uint16_t); + } + + return found; +} + +/** + * Send mDNS response + * + * \param hostname NULL-terminated hostname (sans ".local" suffix) + * + * \return 0 on success, negative on error + */ +int mdns_send_response(int sockfd, const char *hostname) { + struct ifaddrs *ifaddrs; + struct header *hdr; + uint8_t *wr; // writer (convenience pointer) + uint8_t response[ + sizeof(struct header) + + 1 + HOST_NAME_MAX + 1 + // max size of length and host record + 1 + sizeof(".local") + // size of length and local record + 1 + // size of root record + sizeof(struct answer) + + 16 // size of AAAA record (largest record) + ]; + hdr = (struct header *)(response); + wr = response; + + // write header + *hdr = (struct header){ + .id = 0, + .flags = htons(FLAG_RESPONSE), + .n_question = 0, + .n_answer = 0, + .n_authority = 0, + .n_additional = 0 + }; + wr += sizeof(struct header); + + // get linked list of network interfaces + if (-1 == getifaddrs(&ifaddrs)) { + log(ERROR, "%s", strerror(errno)); + goto _fail_getifaddrs; + } + + // for each network interface... + for (struct ifaddrs *ifaddr = ifaddrs; ifaddr; ifaddr = ifaddr->ifa_next) { + // skip loopback interfaces + if (ifaddr->ifa_flags & IFF_LOOPBACK) continue; + + // for an AF_INET* interface address, display the address + if (ifaddr->ifa_addr && ifaddr->ifa_addr->sa_family == AF_INET) { + + // write host record + { + strncpy((char *)(wr + 1), hostname, HOST_NAME_MAX); + wr[1 + HOST_NAME_MAX - 1] = '\0'; + *wr = strlen((char *)(wr + 1)); + wr += 1 + strlen((char *)(wr + 1)); + } + + // write local record + { + const char local[] = "local"; + *wr++ = sizeof(local) - 1; + memcpy(wr, local, sizeof(local) - 1); + wr += sizeof(local) - 1; + } + + // write root record + *wr++ = 0; + + // write answer + { + struct answer ans = { + .type = htons(TYPE_A), + .class = htons(CACHE_FLUSH | CLASS_INTERNET), + .ttl = htonl(DEFAULT_TTL), + .size = htons(sizeof(struct in_addr)), + }; + memcpy(wr, &ans, sizeof(ans)); + wr += sizeof(ans); + + memcpy(wr, + &(((struct sockaddr_in *)(ifaddr->ifa_addr))->sin_addr), + sizeof(struct in_addr)); + wr += sizeof(struct in_addr); + } + + // increment number of answers + hdr->n_answer++; + + if (hdr->n_answer == 1) { + break; + } + } + } + + // free linked list of network interfaces + freeifaddrs(ifaddrs); + + // convert to network byte order + hdr->n_answer = htons(hdr->n_answer); + + // send response + struct sockaddr_in mgroup = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr(MDNS_ADDR), + .sin_port = htons(MDNS_PORT) + }; + if (-1 == sendto( + sockfd, + response, + wr - response, + 0, + (struct sockaddr *)(&mgroup), + sizeof(mgroup) + )) { + log(FATAL, "sendto: %s", strerror(errno)); + } + + return 0; + +_fail_getifaddrs: + return -URTC_ERR; +} -- cgit v1.2.3