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