OpenCPN Partial API docs
Loading...
Searching...
No Matches
mdns_service.cpp
Go to the documentation of this file.
1/**************************************************************************
2 * Copyright (C) 2022 by David Register *
3 * Copyright (C) 2022 Alec Leamas *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
17 **************************************************************************/
18
25#include <chrono>
26#include <string>
27#include <mutex>
28#include <vector>
29#include <thread>
30
31#if defined(_WIN32) && !defined(_CRT_SECURE_NO_WARNINGS)
32#define _CRT_SECURE_NO_WARNINGS 1
33#endif
34
35#include <stdio.h>
36#include <errno.h>
37#include <signal.h>
38
39#ifdef _WIN32
40#include <winsock2.h>
41#include <iphlpapi.h>
42#define sleep(x) Sleep(x * 1000)
43#else
44#include <netdb.h>
45#include <ifaddrs.h>
46#include <net/if.h>
47#endif
48
49#include "config.h"
50#include "mdns_util.h"
51
52#ifndef OCPN_ENABLE_MDNS_DEBUG
53#define printf(...)
54#endif
55
56static char addrbuffer[64];
57static char namebuffer[256];
58static char sendbuffer[1024];
59
60static struct sockaddr_in service_address_ipv4;
61static struct sockaddr_in6 service_address_ipv6;
62
63volatile sig_atomic_t running_server = 1;
64
65// Data for our service including the mDNS records
66typedef struct {
67 mdns_string_t service;
68 mdns_string_t hostname;
69 mdns_string_t service_instance;
70 mdns_string_t hostname_qualified;
71 struct sockaddr_in address_ipv4;
72 struct sockaddr_in6 address_ipv6;
73 int port;
74 mdns_record_t record_ptr;
75 mdns_record_t record_srv;
76 mdns_record_t record_a;
77 mdns_record_t record_aaaa;
78 mdns_record_t txt_record[2];
79} service_t;
80
81// Callback handling questions incoming on service sockets
82int ocpn_service_callback(int sock, const struct sockaddr* from, size_t addrlen,
83 mdns_entry_type_t entry, uint16_t query_id,
84 uint16_t rtype, uint16_t rclass, uint32_t ttl,
85 const void* data, size_t size, size_t name_offset,
86 size_t name_length, size_t record_offset,
87 size_t record_length, void* user_data) {
88 (void)sizeof(ttl);
89 if (entry != MDNS_ENTRYTYPE_QUESTION) return 0;
90
91 const char dns_sd[] = "_services._dns-sd._udp.local.";
92 const service_t* service = (const service_t*)user_data;
93
94 mdns_string_t fromaddrstr =
95 ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen);
96
97 size_t offset = name_offset;
98 mdns_string_t name =
99 mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer));
100
101 const char* record_name = 0;
102 if (rtype == MDNS_RECORDTYPE_PTR)
103 record_name = "PTR";
104 else if (rtype == MDNS_RECORDTYPE_SRV)
105 record_name = "SRV";
106 else if (rtype == MDNS_RECORDTYPE_A)
107 record_name = "A";
108 else if (rtype == MDNS_RECORDTYPE_AAAA)
109 record_name = "AAAA";
110 else if (rtype == MDNS_RECORDTYPE_TXT)
111 record_name = "TXT";
112 else if (rtype == MDNS_RECORDTYPE_ANY)
113 record_name = "ANY";
114 else
115 return 0;
116 printf("Query %s %.*s\n", record_name, MDNS_STRING_FORMAT(name));
117
118 if ((name.length == (sizeof(dns_sd) - 1)) &&
119 (strncmp(name.str, dns_sd, sizeof(dns_sd) - 1) == 0)) {
120 if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) {
121 // The PTR query was for the DNS-SD domain, send answer with a PTR record
122 // for the service name we advertise, typically on the
123 // "<_service-name>._tcp.local." format
124
125 // Answer PTR record reverse mapping "<_service-name>._tcp.local." to
126 // "<hostname>.<_service-name>._tcp.local."
127 mdns_record_t answer;
128 answer.name = name;
129 answer.type = MDNS_RECORDTYPE_PTR;
130 answer.data.ptr.name = service->service;
131
132 // Send the answer, unicast or multicast depending on flag in query
133 uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE);
134 printf(" --> answer %.*s (%s)\n",
135 MDNS_STRING_FORMAT(answer.data.ptr.name),
136 (unicast ? "unicast" : "multicast"));
137
138 if (unicast) {
139 mdns_query_answer_unicast(sock, from, addrlen, sendbuffer,
140 sizeof(sendbuffer), query_id,
141 (mdns_record_type_t)rtype, name.str,
142 name.length, answer, 0, 0, 0, 0);
143 } else {
144 mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer),
145 answer, 0, 0, 0, 0);
146 }
147 }
148 } else if ((name.length == service->service.length) &&
149 (strncmp(name.str, service->service.str, name.length) == 0)) {
150 if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) {
151 // The PTR query was for our service (usually
152 // "<_service-name._tcp.local"), answer a PTR record reverse mapping the
153 // queried service name to our service instance name (typically on the
154 // "<hostname>.<_service-name>._tcp.local." format), and add additional
155 // records containing the SRV record mapping the service instance name to
156 // our qualified hostname (typically "<hostname>.local.") and port, as
157 // well as any IPv4/IPv6 address for the hostname as A/AAAA records, and
158 // two test TXT records
159
160 // Answer PTR record reverse mapping "<_service-name>._tcp.local." to
161 // "<hostname>.<_service-name>._tcp.local."
162 mdns_record_t answer = service->record_ptr;
163
164 mdns_record_t additional[5]{{}};
165 size_t additional_count = 0;
166
167 // SRV record mapping "<hostname>.<_service-name>._tcp.local." to
168 // "<hostname>.local." with port. Set weight & priority to 0.
169 additional[additional_count++] = service->record_srv;
170
171 // A/AAAA records mapping "<hostname>.local." to IPv4/IPv6 addresses
172 if (service->address_ipv4.sin_family == AF_INET)
173 additional[additional_count++] = service->record_a;
174 if (service->address_ipv6.sin6_family == AF_INET6)
175 additional[additional_count++] = service->record_aaaa;
176
177 // Add two test TXT records for our service instance name, will be
178 // coalesced into one record with both key-value pair strings by the
179 // library
180 // additional[additional_count++] = service->txt_record[0];
181 // additional[additional_count++] = service->txt_record[1];
182
183 // Send the answer, unicast or multicast depending on flag in query
184 uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE);
185 printf(" --> answer %.*s (%s)\n",
186 MDNS_STRING_FORMAT(service->record_ptr.data.ptr.name),
187 (unicast ? "unicast" : "multicast"));
188
189 if (unicast) {
190 mdns_query_answer_unicast(
191 sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id,
192 (mdns_record_type)rtype, name.str, name.length, answer, 0, 0,
193 additional, additional_count);
194 } else {
195 mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer),
196 answer, 0, 0, additional, additional_count);
197 }
198 }
199 } else if ((name.length == service->service_instance.length) &&
200 (strncmp(name.str, service->service_instance.str, name.length) ==
201 0)) {
202 if ((rtype == MDNS_RECORDTYPE_SRV) || (rtype == MDNS_RECORDTYPE_ANY)) {
203 // The SRV query was for our service instance (usually
204 // "<hostname>.<_service-name._tcp.local"), answer a SRV record mapping
205 // the service instance name to our qualified hostname (typically
206 // "<hostname>.local.") and port, as well as any IPv4/IPv6 address for the
207 // hostname as A/AAAA records, and two test TXT records
208
209 // Answer PTR record reverse mapping "<_service-name>._tcp.local." to
210 // "<hostname>.<_service-name>._tcp.local."
211 mdns_record_t answer = service->record_srv;
212
213 mdns_record_t additional[5]{{}};
214 size_t additional_count = 0;
215
216 // A/AAAA records mapping "<hostname>.local." to IPv4/IPv6 addresses
217 if (service->address_ipv4.sin_family == AF_INET)
218 additional[additional_count++] = service->record_a;
219 if (service->address_ipv6.sin6_family == AF_INET6)
220 additional[additional_count++] = service->record_aaaa;
221
222 // Add two test TXT records for our service instance name, will be
223 // coalesced into one record with both key-value pair strings by the
224 // library
225 additional[additional_count++] = service->txt_record[0];
226 additional[additional_count++] = service->txt_record[1];
227
228 // Send the answer, unicast or multicast depending on flag in query
229 uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE);
230 printf(" --> answer %.*s port %d (%s)\n",
231 MDNS_STRING_FORMAT(service->record_srv.data.srv.name),
232 service->port, (unicast ? "unicast" : "multicast"));
233
234 if (unicast) {
235 mdns_query_answer_unicast(
236 sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id,
237 (mdns_record_type)rtype, name.str, name.length, answer, 0, 0,
238 additional, additional_count);
239 } else {
240 mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer),
241 answer, 0, 0, additional, additional_count);
242 }
243 }
244 } else if ((name.length == service->hostname_qualified.length) &&
245 (strncmp(name.str, service->hostname_qualified.str, name.length) ==
246 0)) {
247 if (((rtype == MDNS_RECORDTYPE_A) || (rtype == MDNS_RECORDTYPE_ANY)) &&
248 (service->address_ipv4.sin_family == AF_INET)) {
249 // The A query was for our qualified hostname (typically
250 // "<hostname>.local.") and we have an IPv4 address, answer with an A
251 // record mappiing the hostname to an IPv4 address, as well as any IPv6
252 // address for the hostname, and two test TXT records
253
254 // Answer A records mapping "<hostname>.local." to IPv4 address
255 mdns_record_t answer = service->record_a;
256
257 mdns_record_t additional[5]{{}};
258 size_t additional_count = 0;
259
260 // AAAA record mapping "<hostname>.local." to IPv6 addresses
261 if (service->address_ipv6.sin6_family == AF_INET6)
262 additional[additional_count++] = service->record_aaaa;
263
264 // Add two test TXT records for our service instance name, will be
265 // coalesced into one record with both key-value pair strings by the
266 // library
267 additional[additional_count++] = service->txt_record[0];
268 additional[additional_count++] = service->txt_record[1];
269
270 // Send the answer, unicast or multicast depending on flag in query
271 uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE);
272 mdns_string_t addrstr =
273 ip_address_to_string(addrbuffer, sizeof(addrbuffer),
274 (struct sockaddr*)&service->record_a.data.a.addr,
275 sizeof(service->record_a.data.a.addr));
276 printf(" --> answer %.*s IPv4 %.*s (%s)\n",
277 MDNS_STRING_FORMAT(service->record_a.name),
278 MDNS_STRING_FORMAT(addrstr), (unicast ? "unicast" : "multicast"));
279
280 if (unicast) {
281 mdns_query_answer_unicast(
282 sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id,
283 (mdns_record_type)rtype, name.str, name.length, answer, 0, 0,
284 additional, additional_count);
285 } else {
286 mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer),
287 answer, 0, 0, additional, additional_count);
288 }
289 } else if (((rtype == MDNS_RECORDTYPE_AAAA) ||
290 (rtype == MDNS_RECORDTYPE_ANY)) &&
291 (service->address_ipv6.sin6_family == AF_INET6)) {
292 // The AAAA query was for our qualified hostname (typically
293 // "<hostname>.local.") and we have an IPv6 address, answer with an AAAA
294 // record mappiing the hostname to an IPv6 address, as well as any IPv4
295 // address for the hostname, and two test TXT records
296
297 // Answer AAAA records mapping "<hostname>.local." to IPv6 address
298 mdns_record_t answer = service->record_aaaa;
299
300 mdns_record_t additional[5]{{}};
301 size_t additional_count = 0;
302
303 // A record mapping "<hostname>.local." to IPv4 addresses
304 if (service->address_ipv4.sin_family == AF_INET)
305 additional[additional_count++] = service->record_a;
306
307 // Add two test TXT records for our service instance name, will be
308 // coalesced into one record with both key-value pair strings by the
309 // library
310 additional[additional_count++] = service->txt_record[0];
311 additional[additional_count++] = service->txt_record[1];
312
313 // Send the answer, unicast or multicast depending on flag in query
314 uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE);
315 mdns_string_t addrstr = ip_address_to_string(
316 addrbuffer, sizeof(addrbuffer),
317 (struct sockaddr*)&service->record_aaaa.data.aaaa.addr,
318 sizeof(service->record_aaaa.data.aaaa.addr));
319 printf(" --> answer %.*s IPv6 %.*s (%s)\n",
320 MDNS_STRING_FORMAT(service->record_aaaa.name),
321 MDNS_STRING_FORMAT(addrstr), (unicast ? "unicast" : "multicast"));
322
323 if (unicast) {
324 mdns_query_answer_unicast(
325 sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id,
326 (mdns_record_type)rtype, name.str, name.length, answer, 0, 0,
327 additional, additional_count);
328 } else {
329 mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer),
330 answer, 0, 0, additional, additional_count);
331 }
332 }
333 }
334 return 0;
335}
336
337// Provide a mDNS service, answering incoming DNS-SD and mDNS queries
338void service_mdns(const char* hostname, const char* service_name,
339 int service_port) {
340 int sockets[32];
341 int num_sockets =
342 open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]));
343
344 // The multicast join (IP_ADD_MEMBERSHIP) in socket setup needs a usable
345 // multicast route, which may not exist yet when we start early at boot (e.g.
346 // kiosk launch racing network bring-up). Rather than giving up permanently,
347 // retry until the network is ready, so the service becomes discoverable once
348 // connectivity comes up.
349 while (num_sockets <= 0 && running_server) {
350 printf("mDNS service: no sockets yet (network not ready?), retrying\n");
351 std::this_thread::sleep_for(std::chrono::seconds(2));
352 num_sockets =
353 open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]));
354 }
355
356 if (num_sockets <= 0) return;
357
358 printf("Opened %d socket%s for mDNS service\n", num_sockets,
359 num_sockets ? "s" : "");
360
361 size_t service_name_length = strlen(service_name);
362 if (!service_name_length) {
363 printf("Invalid service name\n");
364 return;
365 }
366
367 char* service_name_buffer = (char*)malloc(service_name_length + 2);
368 memcpy(service_name_buffer, service_name, service_name_length);
369 if (service_name_buffer[service_name_length - 1] != '.')
370 service_name_buffer[service_name_length++] = '.';
371 service_name_buffer[service_name_length] = 0;
372 service_name = service_name_buffer;
373
374 printf("Service mDNS: %s:%d\n", service_name, service_port);
375 printf("Hostname: %s\n", hostname);
376
377 size_t capacity = 2048;
378 void* buffer = malloc(capacity);
379
380 mdns_string_t service_string;
381 service_string.str = service_name;
382 service_string.length = strlen(service_name);
383 mdns_string_t hostname_string;
384 hostname_string.str = hostname;
385 hostname_string.length = strlen(hostname);
386
387 // Build the service instance "<hostname>.<_service-name>._tcp.local." string
388 char service_instance_buffer[256] = {0};
389 snprintf(service_instance_buffer, sizeof(service_instance_buffer) - 1,
390 "%.*s.%.*s", MDNS_STRING_FORMAT(hostname_string),
391 MDNS_STRING_FORMAT(service_string));
392
393 mdns_string_t service_instance_string;
394 service_instance_string.str = service_instance_buffer;
395 service_instance_string.length = strlen(service_instance_buffer);
396
397 // Build the "<hostname>.local." string
398 char qualified_hostname_buffer[256] = {0};
399 snprintf(qualified_hostname_buffer, sizeof(qualified_hostname_buffer) - 1,
400 "%.*s.local.", MDNS_STRING_FORMAT(hostname_string));
401 mdns_string_t hostname_qualified_string;
402 hostname_qualified_string.str = qualified_hostname_buffer;
403 hostname_qualified_string.length = strlen(qualified_hostname_buffer);
404
405 service_t service{};
406 service.service = service_string;
407 service.hostname = hostname_string;
408 service.service_instance = service_instance_string;
409 service.hostname_qualified = hostname_qualified_string;
410 service.address_ipv4 = service_address_ipv4;
411 service.address_ipv6 = service_address_ipv6;
412 service.port = service_port;
413
414 // Setup our mDNS records
415
416 // PTR record reverse mapping "<_service-name>._tcp.local." to
417 // "<hostname>.<_service-name>._tcp.local."
418 service.record_ptr.name = service.service;
419 service.record_ptr.type = MDNS_RECORDTYPE_PTR;
420 service.record_ptr.rclass = 0;
421 service.record_ptr.ttl = 0;
422 service.record_ptr.data.ptr.name = service.service_instance;
423
424 // SRV record mapping "<hostname>.<_service-name>._tcp.local." to
425 // "<hostname>.local." with port. Set weight & priority to 0.
426 service.record_srv.name = service.service_instance;
427 service.record_srv.type = MDNS_RECORDTYPE_SRV;
428 service.record_srv.data.srv.name = service.hostname_qualified;
429 service.record_srv.data.srv.port = service.port;
430 service.record_srv.data.srv.priority = 0;
431 service.record_srv.data.srv.weight = 0;
432 service.record_srv.rclass = 0;
433 service.record_srv.ttl = 0;
434
435 // A/AAAA records mapping "<hostname>.local." to IPv4/IPv6 addresses
436
437 service.record_a.name = service.hostname_qualified;
438 service.record_a.type = MDNS_RECORDTYPE_A;
439 service.record_a.data.a.addr = service.address_ipv4;
440 service.record_a.rclass = 0;
441 service.record_a.ttl = 0;
442
443 service.record_aaaa.name = service.hostname_qualified;
444 service.record_aaaa.type = MDNS_RECORDTYPE_AAAA;
445 service.record_aaaa.data.aaaa.addr = service.address_ipv6;
446 service.record_aaaa.rclass = 0;
447 service.record_aaaa.ttl = 0;
448
449 // Send an announcement on startup of service
450 {
451 printf("Sending announce\n");
452 mdns_record_t additional[5]{{}};
453 size_t additional_count = 0;
454 additional[additional_count++] = service.record_srv;
455 if (service.address_ipv4.sin_family == AF_INET)
456 additional[additional_count++] = service.record_a;
457 if (service.address_ipv6.sin6_family == AF_INET6)
458 additional[additional_count++] = service.record_aaaa;
459 // additional[additional_count++] = service.txt_record[0];
460 // additional[additional_count++] = service.txt_record[1];
461
462 for (int isock = 0; isock < num_sockets; ++isock)
463 mdns_announce_multicast(sockets[isock], buffer, capacity,
464 service.record_ptr, 0, 0, additional,
465 additional_count);
466 }
467
468 // This is a crude implementation that checks for incoming queries
469 while (running_server) {
470 int nfds = 0;
471 fd_set readfs;
472 FD_ZERO(&readfs);
473 for (int isock = 0; isock < num_sockets; ++isock) {
474 if (sockets[isock] >= nfds) nfds = sockets[isock] + 1;
475 FD_SET(sockets[isock], &readfs);
476 }
477
478 struct timeval timeout;
479 timeout.tv_sec = 0;
480 timeout.tv_usec = 100000;
481
482 if (select(nfds, &readfs, 0, 0, &timeout) >= 0) {
483 for (int isock = 0; isock < num_sockets; ++isock) {
484 if (FD_ISSET(sockets[isock], &readfs)) {
485 mdns_socket_listen(sockets[isock], buffer, capacity,
486 ocpn_service_callback, &service);
487 }
488 FD_SET(sockets[isock], &readfs);
489 }
490 } else {
491 break;
492 }
493 }
494
495 // Send a goodbye on end of service
496 {
497 printf("Sending goodbye\n");
498 mdns_record_t additional[5]{{}};
499 size_t additional_count = 0;
500 additional[additional_count++] = service.record_srv;
501 if (service.address_ipv4.sin_family == AF_INET)
502 additional[additional_count++] = service.record_a;
503 if (service.address_ipv6.sin6_family == AF_INET6)
504 additional[additional_count++] = service.record_aaaa;
505 // additional[additional_count++] = service.txt_record[0];
506 // additional[additional_count++] = service.txt_record[1];
507
508 for (int isock = 0; isock < num_sockets; ++isock)
509 mdns_goodbye_multicast(sockets[isock], buffer, capacity,
510 service.record_ptr, 0, 0, additional,
511 additional_count);
512 }
513
514 free(buffer);
515 free(service_name_buffer);
516
517 for (int isock = 0; isock < num_sockets; ++isock)
518 mdns_socket_close(sockets[isock]);
519 printf("Closed socket%s\n", num_sockets ? "s" : "");
520
521 return;
522}
523
524std::string host;
525std::string service;
526
527int StartMDNSService(std::string hostname, std::string service_name,
528 int service_port) {
529 host = hostname;
530 service = service_name;
531
532 std::thread mdns_service_thread(service_mdns, host.c_str(), service.c_str(),
533 service_port);
534 mdns_service_thread.detach();
535
536 return 0;
537}
538
539bool StopMDNSService() { return true; }