From bc6b25e14f3ddd8a405a974a4a2e03b9a71d4c9d Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 2 Apr 2020 23:52:28 +0200 Subject: Implement basic DNS parsing --- backends/rtpmidi.c | 246 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 33 +++++++ backends/rtpmidi.md | 4 +- 3 files changed, 237 insertions(+), 46 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 2eec871..97364cf 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -11,9 +11,9 @@ #include "rtpmidi.h" //TODO learn peer ssrcs -//TODO default session join? //TODO default mode? //TODO internal loop mode +//FIXME what qualifies an instance to be announced via mdns? static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -41,6 +41,7 @@ MM_PLUGIN_API int init(){ .conf_instance = rtpmidi_configure_instance, .channel = rtpmidi_channel, .handle = rtpmidi_set, + .interval = rtpmidi_interval, .process = rtpmidi_handle, .start = rtpmidi_start, .shutdown = rtpmidi_shutdown @@ -59,38 +60,90 @@ MM_PLUGIN_API int init(){ return 0; } -static int rtpmidi_configure(char* option, char* value){ - char* host = NULL, *port = NULL; +static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* out){ + size_t offset = 0, output_offset = 0; + uint8_t current_label = 0; + uint16_t ptr_target = 0; - if(!strcmp(option, "mdns-name")){ - if(cfg.mdns_name){ - LOG("Duplicate mdns-name assignment"); - return 1; + //reset output data length and terminate null name + out->length = 0; + if(out->name){ + out->name[0] = 0; + } + + while(start + offset < len){ + current_label = buffer[start + offset]; + + //if we're at a pointer, move there and stop counting data length + if(DNS_POINTER(current_label)){ + if(start + offset + 1 >= len){ + LOG("mDNS internal pointer out of bounds"); + return 1; + } + + //do this before setting the target + if(!ptr_target){ + out->length += 2; + } + + //calculate pointer target + ptr_target = DNS_LABEL_LENGTH(current_label) << 8 | buffer[start + offset + 1]; + + if(ptr_target >= len){ + LOG("mDNS internal pointer target out of bounds"); + return 1; + } + start = ptr_target; + offset = 0; } + else{ + if(DNS_LABEL_LENGTH(current_label) == 0){ + if(!ptr_target){ + out->length++; + } + break; + } - cfg.mdns_name = strdup(value); - if(!cfg.mdns_name){ - LOG("Failed to allocate memory"); - return 1; + //check whether we have the bytes we need + if(start + offset + DNS_LABEL_LENGTH(current_label) > len){ + LOG("mDNS bytes missing"); + return 1; + } + + //check whether we have space in the output + if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){ + out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t)); + out->alloc = output_offset + DNS_LABEL_LENGTH(current_label); + } + + //copy data from this label to output buffer + memcpy(out->name + output_offset, buffer + start + offset + 1, DNS_LABEL_LENGTH(current_label)); + output_offset += DNS_LABEL_LENGTH(current_label) + 1; + offset += DNS_LABEL_LENGTH(current_label) + 1; + out->name[output_offset - 1] = '.'; + out->name[output_offset] = 0; + if(!ptr_target){ + out->length = offset; + } } - return 0; } - else if(!strcmp(option, "mdns-bind")){ - if(cfg.mdns_fd >= 0){ - LOG( "Only one mDNS discovery bind is supported"); - return 1; - } + return 0; +} - mmbackend_parse_hostspec(value, &host, &port, NULL); +static uint32_t rtpmidi_interval(){ + return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); +} - if(!host){ - LOGPF("Not a valid mDNS bind address: %s", value); +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + LOG("Duplicate mdns-name assignment"); return 1; } - cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - LOGPF("Failed to bind mDNS interface: %s", value); + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + LOG("Failed to allocate memory"); return 1; } return 0; @@ -925,6 +978,79 @@ static int rtpmidi_service(){ return 0; } +static int rtpmidi_handle_mdns(){ + uint8_t buffer[RTPMIDI_PACKET_BUFFER]; + dns_header* hdr = (dns_header*) buffer; + dns_rr* rr = NULL; + dns_name name = { + .alloc = 0 + }; + ssize_t bytes = 0; + size_t u = 0, offset = sizeof(dns_header); + + for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ + if(bytes < sizeof(dns_header)){ + continue; + } + LOGPF("%" PRIsize_t " bytes of mDNS data", bytes); + + hdr->id = be16toh(hdr->id); + hdr->questions = be16toh(hdr->questions); + hdr->answers = be16toh(hdr->answers); + hdr->servers = be16toh(hdr->servers); + hdr->additional = be16toh(hdr->additional); + + //TODO Check for ._apple-midi._udp.local. + LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + for(u = 0; u < hdr->questions; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + offset += sizeof(dns_question); + + LOGPF("Question: %s", name.name); + } + + for(u = 0; u < hdr->answers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Answer (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->servers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Authority (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->additional; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Additional (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + } + + free(name.name); + if(bytes <= 0){ + if(errno == EAGAIN){ + return 0; + } + + LOGPF("Error reading from mDNS descriptor: %s", strerror(errno)); + return 1; + } + + return 0; +} + static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; @@ -933,7 +1059,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //handle service tasks (mdns, clock sync, peer connections) if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ - DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); + //DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); if(rtpmidi_service()){ return 1; } @@ -942,7 +1068,8 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ for(u = 0; u < num; u++){ if(!fds[u].impl){ - //TODO handle mDNS discovery input + //handle mDNS discovery input + rtpmidi_handle_mdns(); } else{ //handle rtp/control input @@ -963,30 +1090,53 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ return rv; } -static int rtpmidi_start(size_t n, instance** inst){ - size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL; +static int rtpmidi_start_mdns(){ + struct ip_mreq mcast_req = { + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)), + .imr_interface.s_addr = INADDR_ANY + }; - //if mdns name defined and no socket, bind default values - if(cfg.mdns_name && cfg.mdns_fd < 0){ - cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - return 1; - } + struct ipv6_mreq mcast6_req = { + .ipv6mr_multiaddr.s6_addr = {0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfb}, + .ipv6mr_interface = 0 + }; + + if(!cfg.mdns_name){ + LOG("No mDNS name set, disabling AppleMIDI discovery"); + return 0; } - //register mdns fd to core - if(cfg.mdns_fd >= 0){ - if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ - LOG("Failed to register mDNS socket with core"); - return 1; - } - fds++; + //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + LOG("Failed to create requested mDNS descriptor"); + return 1; } - else{ - LOG("No mDNS discovery interface bound, AppleMIDI session discovery disabled"); + + //join ipv4 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + LOGPF("Failed to join IPv4 multicast group for mDNS: %s", strerror(errno)); + return 1; + } + + //join ipv6 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ + LOGPF("Failed to join IPv6 multicast group for mDNS: %s", strerror(errno)); + return 1; } + //register mdns fd to core + return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL); +} + +static int rtpmidi_start(size_t n, instance** inst){ + size_t u, p, fds = 0; + rtpmidi_instance_data* data = NULL; + uint8_t mdns_required = 0; + for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode @@ -1013,6 +1163,9 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } + else if(data->mode == apple){ + mdns_required = 1; + } //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ @@ -1022,6 +1175,13 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } + if(mdns_required && rtpmidi_start_mdns()){ + return 1; + } + else{ + fds++; + } + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a08cf0f..b1fe26a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -8,6 +8,7 @@ static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static int rtpmidi_instance(instance* inst); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); +static uint32_t rtpmidi_interval(); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(size_t n, instance** inst); @@ -22,6 +23,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 +#define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) +#define DNS_LABEL_LENGTH(a) ((a) & 0x3F) +#define DNS_OPCODE(a) (((a) & 0x78) >> 3) +#define DNS_RESPONSE(a) ((a) & 0x80) + enum /*_rtpmidi_channel_type*/ { none = 0, note = 0x90, @@ -90,6 +96,12 @@ enum applemidi_command { apple_feedback = 0x5253 //RS }; +typedef struct /*_dns_name*/ { + size_t alloc; + char* name; + size_t length; +} dns_name; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; @@ -128,4 +140,25 @@ typedef struct /*_rtp_midi_command*/ { uint8_t flags; uint8_t length; } rtpmidi_command_header; + +typedef struct /*_dns_header*/ { + uint16_t id; + uint8_t flags[2]; + uint16_t questions; + uint16_t answers; + uint16_t servers; + uint16_t additional; +} dns_header; + +typedef struct /*_dns_question*/ { + uint16_t qtype; + uint16_t qclass; +} dns_question; + +typedef struct /*_dns_rr*/ { + uint16_t rtype; + uint16_t rclass; + uint32_t ttl; + uint16_t data; +} dns_rr; #pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index e857a5a..c188039 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -25,7 +25,6 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | | `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | #### Instance configuration @@ -51,9 +50,8 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times. | | `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | -| `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3