ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
network/mdns/mdns.c
Go to the documentation of this file.
1
8
9/* Storage capacities in elements; text buffers include the terminating NUL. */
10#define NETWORK_MDNS_SERVICE_TYPE_BUFFER_SIZE 256
11#define NETWORK_MDNS_INSTANCE_BUFFER_SIZE 256
12#define NETWORK_MDNS_HOST_BUFFER_SIZE 256
13#define NETWORK_MDNS_QUERY_SERVICE_BUFFER_SIZE 256
14#define MDNS_MAX_TXT_RECORDS 16
15#define NETWORK_MDNS_REGTYPE_BUFFER_SIZE 256
16#define NETWORK_MDNS_KEY_BUFFER_SIZE 256
17#define NETWORK_MDNS_INSTANCE_NAME_BUFFER_SIZE 64
18
19/* Existing buffer budgets in bytes, including NUL for text storage. */
20#define MDNS_DISCOVERY_CAPACITY 64
21
23#include <ascii-chat/common.h>
24#include <libplatform/socket.h>
26#include <libplatform/compiler.h>
27#include <libplatform/system.h>
28#include <libplatform/terminal.h>
29#include <libplatform/atomic.h>
31#include <string.h>
32#include <stdio.h>
33#include <ascii-chat-deps/mdns/mdns.h>
34#ifdef _WIN32
35#include <winsock2.h>
36#include <ws2tcpip.h>
37#include <iphlpapi.h>
38#else
39#include <ifaddrs.h>
40#include <net/if.h>
41#include <netinet/in.h> /* For struct sockaddr_in, sockaddr_in6 */
42#endif
43#ifdef __APPLE__
44#include <dns_sd.h>
45typedef struct {
46 asciichat_mdns_t *owner;
47 DNSServiceRef ref, address_ref;
50} bonjour_resolver_t;
51#endif
52
81
82#ifdef __APPLE__
83static void bonjour_query_close(asciichat_mdns_t *mdns) {
84 for (size_t i = 0; i < mdns->resolver_count; ++i)
85 if (mdns->resolvers[i].ref) {
86 if (mdns->resolvers[i].address_ref)
87 DNSServiceRefDeallocate(mdns->resolvers[i].address_ref);
88 DNSServiceRefDeallocate(mdns->resolvers[i].ref);
89 }
90 mdns->resolver_count = 0;
91 if (mdns->browser)
92 DNSServiceRefDeallocate(mdns->browser);
93 mdns->browser = NULL;
94 mdns->query_error = kDNSServiceErr_NoError;
95}
96
97static void bonjour_record_error(asciichat_mdns_t *mdns, DNSServiceErrorType error) {
98 // An individual record may disappear while the remaining services are resolved.
99 if (error != kDNSServiceErr_NoSuchName && error != kDNSServiceErr_NoSuchRecord)
100 mdns->query_error = error;
101}
102
103static void bonjour_address(DNSServiceRef ref, DNSServiceFlags flags, uint32_t interface_index,
104 DNSServiceErrorType error, const char *hostname, const struct sockaddr *address,
105 uint32_t ttl, void *context) {
106 bonjour_resolver_t *resolver = context;
107 if (error != kDNSServiceErr_NoError) {
108 bonjour_record_error(resolver->owner, error);
109 return;
110 }
111 if (!(flags & kDNSServiceFlagsAdd) || !address || address->sa_family != AF_INET)
112 return;
113 const uint8_t *ip = (const uint8_t *)&((const struct sockaddr_in *)address)->sin_addr;
114 safe_snprintf(resolver->found.ipv4, sizeof(resolver->found.ipv4), "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
115 resolver->found.ttl = ttl;
116 resolver->owner->callback(&resolver->found, resolver->owner->callback_data);
117 (void)ref;
118 (void)interface_index;
119 (void)hostname;
120}
121
122static void bonjour_resolved(DNSServiceRef ref, DNSServiceFlags flags, uint32_t interface_index,
123 DNSServiceErrorType error, const char *fullname, const char *host, uint16_t port,
124 uint16_t txt_length, const unsigned char *txt, void *context) {
125 bonjour_resolver_t *resolver = context;
126 asciichat_mdns_t *mdns = resolver->owner;
127 if (error != kDNSServiceErr_NoError) {
128 bonjour_record_error(mdns, error);
129 return;
130 }
131 asciichat_mdns_discovery_t *found = &resolver->found;
132 SAFE_STRNCPY(found->name, resolver->name, sizeof(found->name));
133 SAFE_STRNCPY(found->type, fullname, sizeof(found->type));
134 SAFE_STRNCPY(found->host, host, sizeof(found->host));
135 found->port = ntohs(port);
136 uint8_t length = 0;
137 const void *session = TXTRecordGetValuePtr(txt_length, txt, "session_string", &length);
138 if (session)
139 safe_snprintf(found->txt, sizeof(found->txt), "session_string=%.*s", (int)length, (const char *)session);
140 if (resolver->address_ref)
141 return;
142 error = DNSServiceGetAddrInfo(&resolver->address_ref, 0, interface_index, kDNSServiceProtocol_IPv4, host,
143 bonjour_address, resolver);
144 if (error != kDNSServiceErr_NoError)
145 bonjour_record_error(mdns, error);
146 (void)ref;
147 (void)flags;
148 (void)interface_index;
149}
150
151static void bonjour_browsed(DNSServiceRef ref, DNSServiceFlags flags, uint32_t interface_index,
152 DNSServiceErrorType error, const char *name, const char *type, const char *domain,
153 void *context) {
154 asciichat_mdns_t *mdns = context;
155 if (error != kDNSServiceErr_NoError) {
156 mdns->query_error = error;
157 return;
158 }
159 if (!(flags & kDNSServiceFlagsAdd) || mdns->resolver_count == 64)
160 return;
161 bonjour_resolver_t *resolver = &mdns->resolvers[mdns->resolver_count];
162 memset(resolver, 0, sizeof(*resolver));
163 resolver->owner = mdns;
164 SAFE_STRNCPY(resolver->name, name, sizeof(resolver->name));
165 error = DNSServiceResolve(&resolver->ref, 0, interface_index, name, type, domain, bonjour_resolved, resolver);
166 if (error == kDNSServiceErr_NoError)
167 ++mdns->resolver_count;
168 else
169 bonjour_record_error(mdns, error);
170 (void)ref;
171}
172
173static asciichat_error_t bonjour_update(asciichat_mdns_t *mdns, int timeout_ms) {
174 DNSServiceRef refs[129];
175 struct pollfd fds[129];
176 size_t count = 0;
177 if (mdns->browser)
178 refs[count++] = mdns->browser;
179 for (size_t i = 0; i < mdns->resolver_count; ++i) {
180 refs[count++] = mdns->resolvers[i].ref;
181 if (mdns->resolvers[i].address_ref)
182 refs[count++] = mdns->resolvers[i].address_ref;
183 }
184 for (size_t i = 0; i < count; ++i)
185 fds[i] = (struct pollfd){.fd = DNSServiceRefSockFD(refs[i]), .events = POLLIN};
186 if (count && socket_poll(fds, count, (int64_t)timeout_ms * NS_PER_MS_INT) > 0) {
187 for (size_t i = 0; i < count; ++i) {
188 if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
189 mdns->query_error = fds[i].revents & POLLNVAL ? kDNSServiceErr_BadReference : kDNSServiceErr_ServiceNotRunning;
190 continue;
191 }
192 if (!(fds[i].revents & POLLIN))
193 continue;
194 DNSServiceErrorType error = DNSServiceProcessResult(refs[i]);
195 if (error != kDNSServiceErr_NoError)
196 mdns->query_error = error;
197 }
198 }
199 if (mdns->query_error != kDNSServiceErr_NoError)
200 return SET_ERRNO(ERROR_NETWORK, "Bonjour discovery failed (error %d)", mdns->query_error);
201 return ASCIICHAT_OK;
202}
203#endif
204
206#define MDNS_BUFFER_SIZE (4 * 1024)
207
210 if (!mdns) {
211 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mDNS context");
212 return NULL;
213 }
214
215 memset(mdns, 0, sizeof(asciichat_mdns_t));
216
217 /* Allocate I/O buffer for mDNS packets */
219 if (!mdns->buffer) {
220 SAFE_FREE(mdns);
221 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mDNS buffer");
222 return NULL;
223 }
225
226 /* Open IPv4 mDNS socket */
227 mdns->socket_fd = mdns_socket_open_ipv4(NULL);
228 if (mdns->socket_fd < 0) {
229 SAFE_FREE(mdns->buffer);
230 SAFE_FREE(mdns);
231 SET_ERRNO(ERROR_NETWORK_BIND, "Failed to open mDNS socket");
232 return NULL;
233 }
234
235 log_dev("mDNS context initialized (socket: %d, buffer: %zu bytes)", mdns->socket_fd, mdns->buffer_capacity);
236
237 // Register mDNS context for debugging
238 NAMED_REGISTER_CONTEXT(mdns, "asciichat_mdns", "mdns_context", NULL);
239
240 return mdns;
241}
242
243static void mdns_stop_advertising(asciichat_mdns_t *mdns) {
244 if (!mdns->advertising)
245 return;
246#ifdef __APPLE__
247 if (mdns->dns_service_ref) {
248 DNSServiceRefDeallocate(mdns->dns_service_ref);
249 mdns->dns_service_ref = NULL;
250 }
251#else
252 atomic_store(&mdns->stop_advertising, true);
253 thread_join(&mdns->advertiser, NULL);
254 mdns_goodbye_multicast(mdns->socket_fd, mdns->buffer, mdns->buffer_capacity, mdns->ptr, NULL, 0, mdns->additional,
255 mdns->additional_count);
256#endif
257 mdns->advertising = false;
258}
259
260static int mdns_service_callback(int sock, const struct sockaddr *from, size_t addrlen, mdns_entry_type_t entry,
261 uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void *data,
262 size_t size, size_t name_offset, size_t name_length, size_t record_offset,
263 size_t record_length, void *user_data) {
264 (void)ttl;
265 (void)name_length;
266 (void)record_offset;
267 (void)record_length;
268 asciichat_mdns_t *mdns = user_data;
269 if (entry != MDNS_ENTRYTYPE_QUESTION || (rtype != MDNS_RECORDTYPE_PTR && rtype != MDNS_RECORDTYPE_ANY))
270 return 0;
271 char name[NETWORK_MDNS_NAME_BUFFER_SIZE] = {0};
272 mdns_string_extract(data, size, &name_offset, name, sizeof(name) - 1);
273 if (strcmp(name, mdns->service_type) != 0)
274 return 0;
275 uint8_t response[MDNS_BUFFER_SIZE];
276 if (rclass & MDNS_UNICAST_RESPONSE)
277 mdns_query_answer_unicast(sock, from, addrlen, response, sizeof(response), query_id, (mdns_record_type_t)rtype,
278 name, strlen(name), mdns->ptr, NULL, 0, mdns->additional, mdns->additional_count);
279 else
280 mdns_query_answer_multicast(sock, response, sizeof(response), mdns->ptr, NULL, 0, mdns->additional,
281 mdns->additional_count);
282 return 0;
283}
284
285static void *mdns_advertiser(void *data) {
286 asciichat_mdns_t *mdns = data;
287 while (!atomic_load(&mdns->stop_advertising)) {
288 struct pollfd fd = {.fd = mdns->socket_fd, .events = POLLIN};
289 if (socket_poll(&fd, 1, 100 * NS_PER_MS_INT) > 0)
290 mdns_socket_listen(mdns->socket_fd, mdns->buffer, mdns->buffer_capacity, mdns_service_callback, mdns);
291 }
292 return NULL;
293}
294
296 if (!mdns) {
297 return;
298 }
299
300 mdns_stop_advertising(mdns);
301#ifdef __APPLE__
302 bonjour_query_close(mdns);
303#endif
304 if (mdns->socket_fd >= 0) {
305 mdns_socket_close(mdns->socket_fd);
306 }
307
308 SAFE_FREE(mdns->buffer);
309 SAFE_FREE(mdns);
310 log_debug("mDNS context shutdown");
311}
312
314 if (!mdns || !service) {
315 return SET_ERRNO(ERROR_INVALID_PARAM, "mDNS context or service is NULL");
316 }
317
318 if (!service->name || !service->type || !service->host) {
319 return SET_ERRNO(ERROR_INVALID_PARAM, "Service name, type, or host is NULL");
320 }
321
322 log_debug("Advertising mDNS service: %s (%s:%d)", service->name, service->host, service->port);
323
324 if (!service->port || !service->name[0] || !service->type[0] || !service->host[0] || service->txt_count > 16 ||
325 (service->txt_count && !service->txt_records))
326 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid mDNS service records");
327 if (strlen(service->name) > 55 || strlen(service->type) > 180 || strlen(service->host) > 240)
328 return SET_ERRNO(ERROR_INVALID_PARAM, "mDNS service name is too long");
329 for (size_t i = 0; i < service->txt_count; ++i)
330 if (!service->txt_records[i] || strlen(service->txt_records[i]) >= sizeof(mdns->txt[i]))
331 return SET_ERRNO(ERROR_INVALID_PARAM, "mDNS TXT record is missing or too long");
332 mdns_stop_advertising(mdns);
333#ifdef __APPLE__
334 TXTRecordRef txt_record;
335 TXTRecordCreate(&txt_record, 0, NULL);
337 SAFE_STRNCPY(regtype, service->type, sizeof(regtype));
338 char *local_suffix = strstr(regtype, ".local");
339 if (local_suffix)
340 *local_suffix = '\0';
341 size_t regtype_length = strlen(regtype);
342 if (regtype_length && regtype[regtype_length - 1] == '.')
343 regtype[regtype_length - 1] = '\0';
344 for (size_t i = 0; i < service->txt_count; ++i) {
345 const char *separator = strchr(service->txt_records[i], '=');
346 size_t key_length = separator ? (size_t)(separator - service->txt_records[i]) : strlen(service->txt_records[i]);
347 if (!key_length || key_length >= 256) {
348 TXTRecordDeallocate(&txt_record);
349 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid mDNS TXT record key");
350 }
352 safe_snprintf(key, sizeof(key), "%.*s", (int)key_length, service->txt_records[i]);
353 const void *value = separator ? separator + 1 : NULL;
354 size_t value_length = separator ? strlen(separator + 1) : 0;
355 if (value_length > UINT8_MAX ||
356 TXTRecordSetValue(&txt_record, key, (uint8_t)value_length, value) != kDNSServiceErr_NoError) {
357 TXTRecordDeallocate(&txt_record);
358 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid mDNS TXT record value");
359 }
360 }
361 char instance_name[NETWORK_MDNS_INSTANCE_NAME_BUFFER_SIZE];
362 safe_snprintf(instance_name, sizeof(instance_name), "%s-%u", service->name, service->port);
363 DNSServiceErrorType error = DNSServiceRegister(
364 &mdns->dns_service_ref, 0, kDNSServiceInterfaceIndexAny, instance_name, regtype, NULL, NULL, htons(service->port),
365 TXTRecordGetLength(&txt_record), TXTRecordGetBytesPtr(&txt_record), NULL, NULL);
366 TXTRecordDeallocate(&txt_record);
367 if (error != kDNSServiceErr_NoError)
368 return SET_ERRNO(ERROR_NETWORK, "Cannot register mDNS service with Bonjour (error %d)", error);
369 mdns->advertising = true;
370 return ASCIICHAT_OK;
371#else
372 mdns_socket_close(mdns->socket_fd);
373 struct sockaddr_in listen_address = {.sin_family = AF_INET, .sin_port = htons(MDNS_PORT)};
374 mdns->socket_fd = mdns_socket_open_ipv4(&listen_address);
375 if (mdns->socket_fd < 0)
376 return SET_ERRNO(ERROR_NETWORK_BIND, "Cannot listen for mDNS service queries");
377 safe_snprintf(mdns->service_type, sizeof(mdns->service_type), "%s%s", service->type,
378 strstr(service->type, ".local") ? (service->type[strlen(service->type) - 1] == '.' ? "" : ".")
379 : ".local.");
380 safe_snprintf(mdns->instance, sizeof(mdns->instance), "%s-%u.%s", service->name, service->port, mdns->service_type);
381 safe_snprintf(mdns->host, sizeof(mdns->host), "%s%s", service->host,
382 strstr(service->host, ".local") ? (service->host[strlen(service->host) - 1] == '.' ? "" : ".")
383 : ".local.");
384 mdns_string_t type = {mdns->service_type, strlen(mdns->service_type)};
385 mdns_string_t instance = {mdns->instance, strlen(mdns->instance)};
386 mdns->ptr = (mdns_record_t){.name = type, .type = MDNS_RECORDTYPE_PTR, .data.ptr.name = instance};
387 mdns->additional[0] = (mdns_record_t){.name = instance,
388 .type = MDNS_RECORDTYPE_SRV,
389 .data.srv = {.name = {mdns->host, strlen(mdns->host)}, .port = service->port}};
390 mdns->additional_count = 1;
392 socket_t route = socket_create("mdns_route", AF_INET, SOCK_DGRAM, IPPROTO_UDP);
393 if (route != INVALID_SOCKET_VALUE) {
394 struct sockaddr_in multicast = {
395 .sin_family = AF_INET, .sin_port = htons(MDNS_PORT), .sin_addr.s_addr = htonl(0xe00000fb)};
396 struct sockaddr_in local = {0};
397 socklen_t length = sizeof(local);
398 if (socket_connect(route, (struct sockaddr *)&multicast, sizeof(multicast)) == 0 &&
399 socket_getsockname(route, (struct sockaddr *)&local, &length) == 0)
400 mdns->additional[mdns->additional_count++] =
401 (mdns_record_t){.name = {mdns->host, strlen(mdns->host)}, .type = MDNS_RECORDTYPE_A, .data.a.addr = local};
402 socket_close(route);
403 }
405 for (size_t i = 0; i < service->txt_count; ++i) {
406 SAFE_STRNCPY(mdns->txt[i], service->txt_records[i], sizeof(mdns->txt[i]));
407 char *value = strchr(mdns->txt[i], '=');
408 if (value)
409 *value++ = '\0';
410 mdns->additional[mdns->additional_count++] = (mdns_record_t){
411 .name = instance,
412 .type = MDNS_RECORDTYPE_TXT,
413 .data.txt = {.key = {mdns->txt[i], strlen(mdns->txt[i])}, .value = {value, value ? strlen(value) : 0}}};
414 }
415 if (mdns_announce_multicast(mdns->socket_fd, mdns->buffer, mdns->buffer_capacity, mdns->ptr, NULL, 0,
416 mdns->additional, mdns->additional_count) < 0)
417 return SET_ERRNO(ERROR_NETWORK, "Cannot announce mDNS service");
418 atomic_store(&mdns->stop_advertising, false);
419 if (platform_thread_create(&mdns->advertiser, "mdns_advertiser", mdns_advertiser, mdns) != 0)
420 return SET_ERRNO(ERROR_THREAD, "Cannot start mDNS advertiser");
421 mdns->advertising = true;
422 return ASCIICHAT_OK;
423#endif
424}
425
427 if (!mdns || !service_name) {
428 return SET_ERRNO(ERROR_INVALID_PARAM, "mDNS context or service name is NULL");
429 }
430
431 log_info("Stopped advertising service: %s", service_name);
432
433 mdns_stop_advertising(mdns);
434
435 return ASCIICHAT_OK;
436}
437
444static int mdns_record_callback(int sock, const struct sockaddr *from, size_t addrlen, mdns_entry_type_t entry,
445 uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void *data,
446 size_t size, size_t name_offset, size_t name_length, size_t record_offset,
447 size_t record_length, void *user_data) {
448 asciichat_mdns_t *mdns = (asciichat_mdns_t *)user_data;
449
450 if (!mdns || !mdns->callback) {
451 return 0;
452 }
453
454 // A complete SRV answer binds one instance to a host and port. PTR/A/TXT
455 // records alone are never exposed as selectable, port-zero servers.
456 if ((entry != MDNS_ENTRYTYPE_ANSWER && entry != MDNS_ENTRYTYPE_ADDITIONAL) ||
457 (rtype != MDNS_RECORDTYPE_SRV && rtype != MDNS_RECORDTYPE_TXT) || !ttl)
458 return 0;
459 asciichat_mdns_discovery_t discovery = {0};
460 discovery.ttl = ttl;
461 size_t offset = name_offset;
462 mdns_string_extract(data, size, &offset, discovery.type, sizeof(discovery.type) - 1);
463 const char *suffix = strstr(discovery.type, mdns->query_service);
464 if (!suffix || suffix == discovery.type || suffix[-1] != '.' || strcmp(suffix, mdns->query_service) != 0)
465 return 0;
466 safe_snprintf(discovery.name, sizeof(discovery.name), "%.*s", (int)(suffix - discovery.type - 1), discovery.type);
467 size_t index = 0;
468 while (index < mdns->discovered_count && strcmp(mdns->discovered[index].type, discovery.type) != 0)
469 ++index;
470 if (index == mdns->discovered_count) {
471 if (index == sizeof(mdns->discovered) / sizeof(mdns->discovered[0]))
472 return 0;
473 mdns->discovered[index] = discovery;
474 ++mdns->discovered_count;
475 }
477 if (rtype == MDNS_RECORDTYPE_TXT) {
478 mdns_record_txt_t records[MDNS_MAX_TXT_RECORDS];
479 size_t count = mdns_record_parse_txt(data, size, record_offset, record_length, records, 16);
480 for (size_t i = 0; i < count; ++i) {
481 if (records[i].key.length == strlen("session_string") &&
482 memcmp(records[i].key.str, "session_string", records[i].key.length) == 0 && records[i].value.length < 256)
483 safe_snprintf(cached->txt, sizeof(cached->txt), "session_string=%.*s", (int)records[i].value.length,
484 records[i].value.str);
485 }
486 if (cached->port)
487 mdns->callback(cached, mdns->callback_data);
488 return 0;
489 }
490 if (record_length < 7)
491 return 0;
492 mdns_record_srv_t srv =
493 mdns_record_parse_srv(data, size, record_offset, record_length, discovery.host, sizeof(discovery.host) - 1);
494 if (!srv.port || !srv.name.length)
495 return 0;
496 discovery.port = srv.port;
497 // Direct LAN responders send from the interface on which they are reachable.
498 // Retain the advertised hostname for peers without a usable source address.
499 if (from && from->sa_family == AF_INET && addrlen >= sizeof(struct sockaddr_in)) {
500 const uint8_t *ip = (const uint8_t *)&((const struct sockaddr_in *)from)->sin_addr;
501 safe_snprintf(discovery.ipv4, sizeof(discovery.ipv4), "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
502 }
503 SAFE_STRNCPY(discovery.txt, cached->txt, sizeof(discovery.txt));
504 *cached = discovery;
505 mdns->callback(cached, mdns->callback_data);
506
507 (void)sock;
508 (void)query_id;
509 (void)rclass;
510 (void)addrlen;
511 (void)from;
512 (void)name_length;
513 return 0;
514}
515
516static int mdns_send_query_on_interface(asciichat_mdns_t *mdns, const char *service_type, size_t service_type_len,
517 struct in_addr address) {
518 if (setsockopt(mdns->socket_fd, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&address, sizeof(address)) != 0)
519 return -1;
520 return mdns_query_send(mdns->socket_fd, MDNS_RECORDTYPE_PTR, service_type, service_type_len, mdns->buffer,
521 mdns->buffer_capacity, 0);
522}
523
524static int mdns_send_query_all_interfaces(asciichat_mdns_t *mdns, const char *service_type, size_t service_type_len) {
525 int sent = 0;
526#ifdef _WIN32
527 ULONG size = 0;
528 if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_MULTICAST, NULL,
529 NULL, &size) == ERROR_BUFFER_OVERFLOW) {
530 IP_ADAPTER_ADDRESSES *adapters = SAFE_MALLOC(size, IP_ADAPTER_ADDRESSES *);
531 if (adapters) {
532 if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_MULTICAST,
533 NULL, adapters, &size) == NO_ERROR) {
534 for (IP_ADAPTER_ADDRESSES *adapter = adapters; adapter; adapter = adapter->Next) {
535 if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK ||
536 adapter->NoMulticast)
537 continue;
538 for (IP_ADAPTER_UNICAST_ADDRESS *unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) {
539 if (!unicast->Address.lpSockaddr || unicast->Address.lpSockaddr->sa_family != AF_INET)
540 continue;
541 struct in_addr address = ((struct sockaddr_in *)unicast->Address.lpSockaddr)->sin_addr;
542 if (mdns_send_query_on_interface(mdns, service_type, service_type_len, address) >= 0)
543 ++sent;
544 }
545 }
546 }
547 SAFE_FREE(adapters);
548 }
549 }
550#else
551 struct ifaddrs *interfaces = NULL;
552 if (getifaddrs(&interfaces) == 0) {
553 for (struct ifaddrs *interface = interfaces; interface; interface = interface->ifa_next) {
554 if (!interface->ifa_addr || interface->ifa_addr->sa_family != AF_INET || !(interface->ifa_flags & IFF_UP) ||
555 !(interface->ifa_flags & IFF_MULTICAST) || (interface->ifa_flags & IFF_LOOPBACK))
556 continue;
557 struct in_addr address = ((struct sockaddr_in *)interface->ifa_addr)->sin_addr;
558 if (mdns_send_query_on_interface(mdns, service_type, service_type_len, address) >= 0)
559 ++sent;
560 }
561 freeifaddrs(interfaces);
562 }
563#endif
564 if (!sent)
565 return mdns_query_send(mdns->socket_fd, MDNS_RECORDTYPE_PTR, service_type, service_type_len, mdns->buffer,
566 mdns->buffer_capacity, 0);
567 return 0;
568}
569
571 asciichat_mdns_discovery_callback_fn callback, void *user_data) {
572 if (!mdns || !service_type || !callback) {
573 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid mDNS query parameters");
574 }
575 if (mdns->advertising)
576 return SET_ERRNO(ERROR_INVALID_STATE, "Use a separate mDNS context for discovery while advertising");
577
578 /* Validate service type is non-empty and has minimum length
579 * Prevents underflow bug in mdns library when processing empty strings
580 */
581 size_t service_type_len = strlen(service_type);
582 if (service_type_len == 0 || service_type_len > 253) {
583 return SET_ERRNO(ERROR_INVALID_PARAM, "Service type cannot be empty");
584 }
585
586 safe_snprintf(mdns->query_service, sizeof(mdns->query_service), "%s%s", service_type,
587 service_type[service_type_len - 1] == '.' ? "" : ".");
588 mdns->callback = callback;
589 mdns->callback_data = user_data;
590 mdns->discovered_count = 0;
591
592#ifdef __APPLE__
593 bonjour_query_close(mdns);
595 SAFE_STRNCPY(regtype, service_type, sizeof(regtype));
596 char *domain = strstr(regtype, ".local");
597 if (domain)
598 *domain = '\0';
599 size_t length = strlen(regtype);
600 if (length && regtype[length - 1] == '.')
601 regtype[length - 1] = '\0';
602 DNSServiceErrorType error =
603 DNSServiceBrowse(&mdns->browser, 0, kDNSServiceInterfaceIndexAny, regtype, "local.", bonjour_browsed, mdns);
604 if (error != kDNSServiceErr_NoError)
605 return SET_ERRNO(ERROR_NETWORK, "Cannot browse mDNS services with Bonjour (error %d)", error);
606 return ASCIICHAT_OK;
607#else
608 // Use the standard mDNS port so responders multicast their answers. Some
609 // system mDNS responders do not return QU answers to an ephemeral source
610 // port, and multicast answers are also visible across all active adapters.
611 mdns_socket_close(mdns->socket_fd);
612 struct sockaddr_in listen_address = {.sin_family = AF_INET, .sin_port = htons(MDNS_PORT)};
613 mdns->socket_fd = mdns_socket_open_ipv4(&listen_address);
614 if (mdns->socket_fd < 0)
615 return SET_ERRNO(ERROR_NETWORK_BIND, "Cannot listen for mDNS query responses");
616
617 log_info("Starting mDNS query for: %s", service_type);
618
619 /* Send PTR query for service type (one-shot query)
620 * PTR query discovers all instances of a service type
621 * mdns_query_send returns the query ID for response filtering
622 */
623 int query_id = mdns_send_query_all_interfaces(mdns, service_type, service_type_len);
624
625 if (query_id < 0) {
626 return SET_ERRNO(ERROR_NETWORK, "mDNS query send failed for %s (query_id=%d)", service_type, query_id);
627 }
628
629 mdns->query_id = (uint16_t)query_id;
630 log_debug("mDNS query sent for service type: %s (query_id: %d)", service_type, query_id);
631
632 return ASCIICHAT_OK;
633#endif
634}
635
637 if (!mdns) {
638 return SET_ERRNO(ERROR_INVALID_PARAM, "mDNS context is NULL");
639 }
640 if (mdns->advertising)
641 return ASCIICHAT_OK;
642
643#ifdef __APPLE__
644 return bonjour_update(mdns, timeout_ms);
645#else
646 /* Process incoming mDNS packets (responses from previous queries)
647 * mdns_query_recv processes all records received since last call
648 * The callback function (mdns_record_callback) is invoked for each record
649 */
650 struct pollfd fd = {.fd = mdns->socket_fd, .events = POLLIN};
651 if (socket_poll(&fd, 1, (int64_t)timeout_ms * NS_PER_MS_INT) <= 0)
652 return ASCIICHAT_OK;
653 int num_records =
654 mdns_query_recv(mdns->socket_fd, mdns->buffer, mdns->buffer_capacity, mdns_record_callback, mdns, mdns->query_id);
655
656 if (num_records < 0) {
657 return SET_ERRNO(ERROR_NETWORK, "Failed to receive mDNS query responses");
658 }
659
660 if (num_records > 0) {
661 log_debug("Processed %d mDNS records", num_records);
662 }
663
664 (void)timeout_ms;
665 return ASCIICHAT_OK;
666#endif
667}
668
670 if (!mdns) {
671 return -1;
672 }
673#ifdef __APPLE__
674 if (mdns->browser)
675 return DNSServiceRefSockFD(mdns->browser);
676#endif
677 return mdns->socket_fd;
678}
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_checkpoint(void)
#define SET_ERRNO(code,...)
@ ASCIICHAT_ERRNO_DISMISSED
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_CONTEXT(context, context_type, name, parent_ptr)
Register a generic context with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_NETWORK_BIND
Definition error_codes.h:73
@ ERROR_NETWORK
Definition error_codes.h:72
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
@ ERROR_THREAD
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_MS_INT
Definition time.h:168
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
int socket_t
#define NETWORK_MDNS_QUERY_SERVICE_BUFFER_SIZE
#define NETWORK_MDNS_REGTYPE_BUFFER_SIZE
#define NETWORK_MDNS_SERVICE_TYPE_BUFFER_SIZE
asciichat_error_t asciichat_mdns_update(asciichat_mdns_t *mdns, int timeout_ms)
Process pending mDNS events (must be called regularly).
asciichat_error_t asciichat_mdns_unadvertise(asciichat_mdns_t *mdns, const char *service_name)
Stop advertising a service.
#define MDNS_BUFFER_SIZE
mDNS packet buffer size (4KB should handle most service records)
#define MDNS_MAX_TXT_RECORDS
void asciichat_mdns_destroy(asciichat_mdns_t *mdns)
Shutdown mDNS context and cleanup.
#define NETWORK_MDNS_KEY_BUFFER_SIZE
#define NETWORK_MDNS_INSTANCE_NAME_BUFFER_SIZE
#define NETWORK_MDNS_HOST_BUFFER_SIZE
#define NETWORK_MDNS_INSTANCE_BUFFER_SIZE
asciichat_mdns_t * asciichat_mdns_init(void)
Initialize mDNS context.
#define MDNS_DISCOVERY_CAPACITY
int asciichat_mdns_get_socket(asciichat_mdns_t *mdns)
Get the socket file descriptor for integration with select/poll.
asciichat_error_t asciichat_mdns_query(asciichat_mdns_t *mdns, const char *service_type, asciichat_mdns_discovery_callback_fn callback, void *user_data)
Query for services on the local network.
asciichat_error_t asciichat_mdns_advertise(asciichat_mdns_t *mdns, const asciichat_mdns_service_t *service)
Advertise a service on the local network.
#define NETWORK_MDNS_TXT_BUFFER_SIZE
#define NETWORK_MDNS_NAME_BUFFER_SIZE
void(* asciichat_mdns_discovery_callback_fn)(const asciichat_mdns_discovery_t *service, void *user_data)
This is the handoff point for asciichat mdns discovery. Callers fill port, ttl before passing it on; ...
static bool size_t index
Definition queries.h:8
if(!queries||!out||index >=DATABASE_QUERY_COUNT) return false
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
Service information for advertisement.
Service information for advertisement.
Owns one mDNS browse or advertisement session, including its socket, event-loop registration,...
_Atomic bool stop_advertising
asciichat_mdns_discovery_callback_fn callback
char txt[16][NETWORK_MDNS_TXT_BUFFER_SIZE]
mdns_record_t additional[18]
platform_thread_t advertiser
asciichat_mdns_discovery_t discovered[64]
⏱️ High-precision timing utilities using sokol_time.h and uthash