From cee25a8b9026252a951f413f575f269ce4526e03 Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Sun, 5 Oct 2025 15:15:40 -0700 Subject: [PATCH] DNS-SD support for multiple KISS ports and AGWPE Dire Wolf 1.7 introduced the ability to configure multiple KISS TCP ports, each for a single radio channel. However, DNS Service Discovery announced only the first configured port. This set of changes expands DNS-SD support to announce all configured KISS TCP ports, and adds support for announcing the AGWPE port if configured. The functionality is available on both Linux and Mac. (Dire Wolf does not currently support DNS Service Discovery on Windows.) Details of these changes follow. On both Linux and Mac: * All configured KISS TCP and AGWPE ports are announced via DNS-SD. * Announcement messages include radio channel where appropriate. * Cleanup code added to gracefully shut down and de-register from DNS-SD on exit or error. * DNS-SD code now checks for services to publish, so that the caller doesn't need to do this. * Standard Dire Wolf style comments added for all functions. On Linux only: * Handling of service name collisions is now iterative rather than recursive. * No more gotos. On Mac only: * Now-complete implementation includes event loop for handling notifications from the DNS-SD daemon, which were previously lost. * As a consequence of the above, successful announcement message now appears in the console (as it did before only for Linux). * Text color set properly for console messages. These changes have been tested with as many combinations of config settings as possible, on Linux Mint 22.1 and macOS Sequoia 15.6.1. --- src/direwolf.c | 6 +- src/dns_sd_avahi.c | 435 ++++++++++++++++++++++++++++++++++---------- src/dns_sd_common.c | 151 +++++++++++++-- src/dns_sd_common.h | 22 ++- src/dns_sd_dw.h | 23 ++- src/dns_sd_macos.c | 282 +++++++++++++++++++++++++--- 6 files changed, 777 insertions(+), 142 deletions(-) diff --git a/src/direwolf.c b/src/direwolf.c index f89dc6e6..c438d64c 100644 --- a/src/direwolf.c +++ b/src/direwolf.c @@ -1137,7 +1137,7 @@ int main (int argc, char *argv[]) kissnet_init (&misc_config); #if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) - if (misc_config.kiss_port[0] > 0 && misc_config.dns_sd_enabled) + if (misc_config.dns_sd_enabled) dns_sd_announce(&misc_config); #endif @@ -1692,6 +1692,10 @@ static void cleanup_linux (int x) { text_color_set(DW_COLOR_INFO); dw_printf ("\nQRT\n"); +#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) + if (misc_config.dns_sd_enabled) + dns_sd_term (); +#endif log_term (); ptt_term (); dwgps_term (); diff --git a/src/dns_sd_avahi.c b/src/dns_sd_avahi.c index 63ce0b66..713fddd5 100644 --- a/src/dns_sd_avahi.c +++ b/src/dns_sd_avahi.c @@ -2,6 +2,7 @@ // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// Copyright (C) 2025 Martin F N Cooper, KD6YAM // // // This program is free software: you can redistribute it and/or modify @@ -56,45 +57,99 @@ static AvahiEntryGroup *group = NULL; static AvahiSimplePoll *simple_poll = NULL; static AvahiClient *client = NULL; -static char *name = NULL; -static int kiss_port = 0; pthread_t avahi_thread; -static void create_services(AvahiClient *c); +static void create_services(AvahiClient *c, dns_sd_service_t *ctx); #define PRINT_PREFIX "DNS-SD: Avahi: " -static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata) + +/*------------------------------------------------------------------------------ + * + * Name: rename_all_services + * + * Purpose: Rename each service, using avahi_alternative_service_name() to + * obtain a new name. + * + * Inputs: ctx - Context info for all of our services. + * + * Description: This function is used when we know there is a name conflict for + * at least one service in the group, but not which one. Thus we + * update the names for all services to cover all possibilities. + * + *------------------------------------------------------------------------------*/ + +static void rename_all_services(dns_sd_service_t *ctx) +{ + for (int i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (ctx[i].name) { + char *prev_name = ctx[i].name; + ctx[i].name = avahi_alternative_service_name(prev_name); + avahi_free(prev_name); + } + } +} + + +/*------------------------------------------------------------------------------ + * + * Name: entry_group_callback + * + * Purpose: Called whenever the entry group changes state. + * + * Inputs: g - Group on which state changes are occurring. + * This function may be called before our global + * 'group' value has been set, so we must use the + * value passed in to reference our group. + * + * state - An enumeration value indicating the new state. + * + * userdata - Context info for our services. + * + * Description: Here we are notified when all of the services in the group have + * been published, so that we can report that to the user. We could + * report the success of each service individually, but since success + * or failure applies on a group all-or-nothing basis, we report only + * collective success. + * + * We may also be notified of a service name collision here. The + * Avahi API does not provide a way for us to know to which service + * that applies. Consequently all services must be renamed and the + * group effectively recreated. + * + *------------------------------------------------------------------------------*/ + +static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) { assert(g == group || group == NULL); group = g; - /* Called whenever the entry group state changes */ + dns_sd_service_t *ctx = (dns_sd_service_t *)userdata; + switch (state) { case AVAHI_ENTRY_GROUP_ESTABLISHED : /* The entry group has been established successfully */ text_color_set(DW_COLOR_INFO); - dw_printf(PRINT_PREFIX "Service '%s' successfully registered.\n", name); + dw_printf(PRINT_PREFIX "Successfully registered all services.\n"); break; case AVAHI_ENTRY_GROUP_COLLISION: { - char *n; - /* A service name collision with a remote service - * happened. Let's pick a new name. */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; + /* A service name collision with a remote service happened. We are + * not informed of which name has a collision, so we need to rename + * all of them to be sure we catch the offending name. */ text_color_set(DW_COLOR_INFO); - dw_printf(PRINT_PREFIX "Service name collision, renaming service to '%s'\n", name); + dw_printf(PRINT_PREFIX "Service name collision, renaming services'\n"); + rename_all_services(ctx); /* And recreate the services */ - create_services(avahi_entry_group_get_client(g)); + create_services(avahi_entry_group_get_client(g), ctx); break; } case AVAHI_ENTRY_GROUP_FAILURE: text_color_set(DW_COLOR_ERROR); - dw_printf(PRINT_PREFIX "Entry group failure: %s\n", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); + dw_printf(PRINT_PREFIX "Entry group failure: %s\n", + avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); /* Some kind of failure happened while we were registering our services */ - avahi_simple_poll_quit(simple_poll); + dns_sd_term(); break; case AVAHI_ENTRY_GROUP_UNCOMMITED: case AVAHI_ENTRY_GROUP_REGISTERING: @@ -102,18 +157,107 @@ static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, } } -static void create_services(AvahiClient *c) + +/*------------------------------------------------------------------------------ + * + * Name: create_service + * + * Purpose: Creates one service and adds it to the Avahi entry group. + * + * Inputs: group - The Avahi entry group to which the service + * should be added. + * + * ctx - Context info for the service. + * + * is_agwpe - Whether to create an AGWPE service (non-zero) + * or a KISS TCP one (zero). + * + * Description: Creates a single service as specified. Handles service name + * collisions by repeatedly retrying with alternative names provided + * by Avahi. Although there are other ways in which the Avahi API + * could notify us of name conflicts, this appears to be the one + * that is presented when conflicts arise through, for example, + * multiple instances of Dire Wolf started on the same system. + * + *------------------------------------------------------------------------------*/ + +static int create_service(AvahiEntryGroup *group, dns_sd_service_t *ctx, int is_agwpe) +{ + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Announcing %s on port %d as '%s'\n", + is_agwpe ? DNS_SD_TYPE_NAME_AGWPE : DNS_SD_TYPE_NAME_KISS, ctx->port, ctx->name); + + /* Announce with AVAHI_PROTO_INET instead of AVAHI_PROTO_UNSPEC, since Dire Wolf currently + * only listens on IPv4. + */ + + int error = AVAHI_OK; + + do { + error = avahi_entry_group_add_service( + group, // entry group + AVAHI_IF_UNSPEC, // all interfaces + AVAHI_PROTO_INET, // IPv4 only + 0, // no flags + ctx->name, // service name + is_agwpe ? DNS_SD_TYPE_AGWPE : DNS_SD_TYPE_KISS, + NULL, // default domain(s) + NULL, // default hostname(s) + ctx->port, // service port + NULL // (undocumented) + ); + + if (error == AVAHI_ERR_COLLISION) { + char *prev_name = ctx->name; + ctx->name = avahi_alternative_service_name(prev_name); + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Service name collision, renaming '%s' to '%s'\n", prev_name, ctx->name); + avahi_free(prev_name); + } + } while (error == AVAHI_ERR_COLLISION); + + if (error != AVAHI_OK) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to add %s service: %s\n", + is_agwpe ? DNS_SD_TYPE_NAME_AGWPE : DNS_SD_TYPE_NAME_KISS, avahi_strerror(error)); + } + + return error; +} + + +/*------------------------------------------------------------------------------ + * + * Name: create_services + * + * Purpose: Creates all of our services and causes them to be published. + * + * Inputs: c - Client through which to create services. + * + * ctx - Context info for our services. + * + * Description: First, we create an entry group which will contain all of our + * services. This is required by the Avahi API, and provides a means + * of managing the set of services. Then we create each service and + * group. Finally, we commit the changes, which causes all of the + * services in the group to be published. + * + *------------------------------------------------------------------------------*/ + +static void create_services(AvahiClient *c, dns_sd_service_t *ctx) { - char *n; - int ret; + int result; + assert(c); + /* If this is the first time we're called, let's create a new * entry group if necessary */ if (!group) { - if (!(group = avahi_entry_group_new(c, entry_group_callback, NULL))) { + if (!(group = avahi_entry_group_new(c, entry_group_callback, (void *)ctx))) { text_color_set(DW_COLOR_ERROR); dw_printf(PRINT_PREFIX "avahi_entry_group_new() failed: %s\n", avahi_strerror(avahi_client_errno(c))); - goto fail; + dns_sd_term(); + return; } } else { avahi_entry_group_reset(group); @@ -122,60 +266,73 @@ static void create_services(AvahiClient *c) /* If the group is empty (either because it was just created, or * because it was reset previously, add our entries. */ if (avahi_entry_group_is_empty(group)) { - text_color_set(DW_COLOR_INFO); - dw_printf(PRINT_PREFIX "Announcing KISS TCP on port %d as '%s'\n", kiss_port, name); - - /* Announce with AVAHI_PROTO_INET instead of AVAHI_PROTO_UNSPEC, since Dire Wolf currently - * only listens on IPv4. - */ + /* Add each individual service. */ + for (int i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (ctx[i].port == 0) + continue; + result = create_service(group, &ctx[i], i == 0); + /* Collisions are handled within create_service(), so an error here + * is something else, almost certainly fatal to registration as a + * whole, so bail out and give up. */ + if (result != AVAHI_OK) + break; + } - if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, 0, name, DNS_SD_SERVICE, NULL, NULL, kiss_port, NULL)) < 0) { - if (ret == AVAHI_ERR_COLLISION) - goto collision; - text_color_set(DW_COLOR_ERROR); - dw_printf(PRINT_PREFIX "Failed to add _kiss-tnc._tcp service: %s\n", avahi_strerror(ret)); - goto fail; + if (result != AVAHI_OK) { + dns_sd_term(); + return; } - /* Tell the server to register the service */ - if ((ret = avahi_entry_group_commit(group)) < 0) { + /* Publish all services in the group. */ + result = avahi_entry_group_commit(group); + if (result != AVAHI_OK) { text_color_set(DW_COLOR_ERROR); - dw_printf(PRINT_PREFIX "Failed to commit entry group: %s\n", avahi_strerror(ret)); - goto fail; + dw_printf(PRINT_PREFIX "Failed to commit entry group: %s\n", avahi_strerror(result)); + dns_sd_term(); + return; } } - return; - -collision: - /* A service name collision with a local service happened. Let's - * pick a new name */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; - text_color_set(DW_COLOR_INFO); - dw_printf(PRINT_PREFIX "Service name collision, renaming service to '%s'\n", name); - avahi_entry_group_reset(group); - create_services(c); - return; - -fail: - avahi_simple_poll_quit(simple_poll); } -static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata) + +/*------------------------------------------------------------------------------ + * + * Name: client_callback + * + * Purpose: Called whenever the client or its corresponding server changes + * state. + * + * Inputs: c - Client on which state changes are occurring. + * This function may be called before our global + * 'client' value has been set, so we must use the + * value passed in to reference our client. + * + * state - An enumeration value indicating the new state. + * + * userdata - Context info for our services. + * + * Description: Here we are notified when the server is ready, and thus we can + * register our services. We may also be notified of name collisions + * or client failure. + * + *------------------------------------------------------------------------------*/ + +static void client_callback(AvahiClient *c, AvahiClientState state, void * userdata) { assert(c); - /* Called whenever the client or server state changes */ + + dns_sd_service_t *ctx = (dns_sd_service_t *)userdata; + switch (state) { case AVAHI_CLIENT_S_RUNNING: /* The server has startup successfully and registered its host * name on the network, so it's time to create our services */ - create_services(c); + create_services(c, ctx); break; case AVAHI_CLIENT_FAILURE: text_color_set(DW_COLOR_ERROR); dw_printf(PRINT_PREFIX "Client failure: %s\n", avahi_strerror(avahi_client_errno(c))); - avahi_simple_poll_quit(simple_poll); + dns_sd_term(); break; case AVAHI_CLIENT_S_COLLISION: /* Let's drop our registered services. When the server is back @@ -194,67 +351,161 @@ static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UN } } -static void cleanup(void) + +/*------------------------------------------------------------------------------ + * + * Name: cleanup + * + * Purpose: Called on exit (successful or otherwise) to release Avahi + * resources and free our own context data. + * + * Inputs: ctx - Context info for all of our announced services. + * + * Description: Frees Avahi resources and then our own context. Note that the + * order of calls here is important. Some of the Avahi objects + * keep references to others (e.g. group holds a reference to client), + * such that freeing them in the wrong order can cause a segfault. + * + *------------------------------------------------------------------------------*/ + +static void cleanup(dns_sd_service_t *ctx) { - /* Cleanup things */ - if (client) - avahi_client_free(client); + if (group) { + avahi_entry_group_free(group); + group = NULL; + } + + if (client) { + avahi_client_free(client); + client = NULL; + } - if (simple_poll) - avahi_simple_poll_free(simple_poll); + if (simple_poll) { + avahi_simple_poll_free(simple_poll); + simple_poll = NULL; + } + + for (int i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (ctx[i].name) { + avahi_free(ctx[i].name); + } + } - avahi_free(name); + free(ctx); } +/*------------------------------------------------------------------------------ + * + * Name: avahi_mainloop + * + * Purpose: Thread function to process events from the Avahi daemon. + * + * Inputs: arg - Context with info on all of our announced services. + * Needed here only so that we can clean up properly + * when we're done. + * + * Description: Starts a standard Avahi "simple poll" loop that will cause + * our client and group callbacks to be invoked at the appropriate + * time. The loop will exit when the avahi_simple_poll_quit() + * function is called elsewhere. We then clean up our context. + * + *------------------------------------------------------------------------------*/ + static void *avahi_mainloop(void *arg) { - /* Run the main loop */ - avahi_simple_poll_loop(simple_poll); + dns_sd_service_t *ctx = (dns_sd_service_t *) arg; + + /* Run the main loop */ + avahi_simple_poll_loop(simple_poll); - cleanup(); + cleanup(ctx); - return NULL; + return NULL; } + +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_announce + * + * Purpose: Announce all configured AGWPE and KISS TCP services via DNS + * Service Discovery. + * + * Inputs: mc - Dire Wolf misc config as read from the config file. + * + * Description: Register all configured AGWPE and KISS TCP services, and start + * a polling loop to watch for events that apply to those services. + * + *------------------------------------------------------------------------------*/ + void dns_sd_announce (struct misc_config_s *mc) { - text_color_set(DW_COLOR_DEBUG); - //kiss_port = mc->kiss_port; // now an array. - kiss_port = mc->kiss_port[0]; // FIXME: Quick hack until I can handle multiple TCP ports properly. - - int error; + // If there are no services to announce, we're done + if (dns_sd_service_count(mc) == 0) + return; - /* Allocate main loop object */ - if (!(simple_poll = avahi_simple_poll_new())) { - text_color_set(DW_COLOR_ERROR); - dw_printf(PRINT_PREFIX "Failed to create Avahi simple poll object.\n"); - goto fail; + dns_sd_service_t *ctx; + int i; + + ctx = dns_sd_create_context(mc); + + /* It is possible that we may need to call avahi_alternative_service_name() + * one or more times to resolve service name conflicts. That function will + * allocate a new name that must later be freed using avahi_free(). Here we + * need to reallocate our initial names using avahi_strdup() to ensure that + * calling avahi_free() on them later won't be a problem. */ + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (ctx[i].name) { + char *prev_name = ctx[i].name; + ctx[i].name = avahi_strdup(prev_name); + free(prev_name); } + } - if (mc->dns_sd_name[0]) { - name = avahi_strdup(mc->dns_sd_name); - } else { - name = dns_sd_default_service_name(); - } + int error = 0; - /* Allocate a new client */ - client = avahi_client_new(avahi_simple_poll_get(simple_poll), 0, client_callback, NULL, &error); + /* Allocate main loop object */ + if (!(simple_poll = avahi_simple_poll_new())) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to create Avahi simple poll object.\n"); + error = 1; + } - /* Check whether creating the client object succeeded */ + /* Allocate a new client */ + if (!error) { + client = avahi_client_new(avahi_simple_poll_get(simple_poll), 0, client_callback, ctx, &error); if (!client) { text_color_set(DW_COLOR_ERROR); dw_printf(PRINT_PREFIX "Failed to create Avahi client: %s\n", avahi_strerror(error)); - goto fail; } + } - pthread_create(&avahi_thread, NULL, &avahi_mainloop, NULL); + if (!error) { + /* Start the main loop */ + pthread_create(&avahi_thread, NULL, &avahi_mainloop, (void *) ctx); + } else { + cleanup(ctx); + } +} - return; -fail: - cleanup(); +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_term + * + * Purpose: Gracefully shut down the event processing thread and remove all + * service registrations. + * + * Description: By telling the simple_poll to quit, our thread function will + * continue beyond the polling loop and invoke our cleanup code + * when it's ready. + * + *------------------------------------------------------------------------------*/ + +void dns_sd_term (void) { + if (simple_poll) + avahi_simple_poll_quit(simple_poll); } #endif // USE_AVAHI_CLIENT - diff --git a/src/dns_sd_common.c b/src/dns_sd_common.c index 65a1cbf7..96f2bc87 100644 --- a/src/dns_sd_common.c +++ b/src/dns_sd_common.c @@ -2,6 +2,7 @@ // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// Copyright (C) 2025 Martin F N Cooper, KD6YAM // // // This program is free software: you can redistribute it and/or modify @@ -33,33 +34,153 @@ * This module contains common functions needed on Linux and MacOS. */ - +#include #include #include #include -/* Get a default service name to publish. By default, - * "Dire Wolf on ", or just "Dire Wolf" if hostname cannot - * be obtained. - */ -char *dns_sd_default_service_name(void) +#include "dns_sd_dw.h" +#include "dns_sd_common.h" + +#define SERVICE_BASE_NAME "Dire Wolf" + + +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_service_count + * + * Purpose: Determine the number of services that are configured and will + * thus be announced. + * + * Inputs: mc - Dire Wolf misc config as read from the config file. + * + * Returns: Count of services to be announced. + * + * Description: Counts the number of AGWPE and KISS TCP services that have a + * non-zero port number, meaning that they should be announced via + * DNS-SD. This is useful for determining whether or not there is + * anything that we need to do. + * + *------------------------------------------------------------------------------*/ + +int dns_sd_service_count(struct misc_config_s *mc) { + int count = 0; + + if (mc->agwpe_port != 0) + count++; + + for (int i = 0; i < MAX_KISS_TCP_PORTS; i++) { + if (mc->kiss_port[i] != 0) + count++; + } + + return count; +} + + +/*------------------------------------------------------------------------------ + * + * Name: make_service_name + * + * Purpose: Create a full service name based on the provided components. + * + * Inputs: basename - Base service name. Defaults to "Dire Wolf". + * + * hostname - Host name if available, else empty string. + * + * channel - Channel number, or -1 for default. + * + * Returns: A full service name suitable for DNS-SD. + * It is the caller's responsibility to free this. + * + * Description: Constructs a full service name for an AGWPE or KISS service. + * A typical name including all components might look like + * "Dire Wolf channel 2 on myhost". Channel is only relevant for + * KISS services. + * + *------------------------------------------------------------------------------*/ + +static char *make_service_name (char *basename, char *hostname, int channel) +{ + char sname[128]; + char temp[64]; + + if (basename[0]) { + strlcpy(sname, basename, sizeof(sname)); + } else { + strcpy(sname, "Dire Wolf"); + } + + if (channel != -1) { + snprintf(temp, sizeof(temp), " channel %i", channel); + strlcat(sname, temp, sizeof(sname)); + } + + if (hostname[0]) { + snprintf(temp, sizeof(temp), " on %s", hostname); + strlcat(sname, temp, sizeof(sname)); + } + + return strdup(sname); +} + + +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_create_context + * + * Purpose: Allocate and populate an array of common attributes for each of + * the DNS-SD services to be announced. This includes constructing + * a unique name for each service. + * + * Inputs: mc - Dire Wolf misc config as read from the config file. + * + * Returns: An array of dns_sd_service_t, of length MAX_DNS_SD_SERVICES. + * It is the caller's responsibility to free this. + * + * Description: The port and channel are saved, and a name created from a base + * name provided in the config, or a constant if none is provided. + * The name includes the channel, if appropriate, and the hostname + * if available. + * + * The first entry in the array is for AGWPE. The remainder are + * for however many KISS TCP ports are configured. + * + *------------------------------------------------------------------------------*/ + +dns_sd_service_t *dns_sd_create_context (struct misc_config_s *mc) +{ + dns_sd_service_t *ctx; char hostname[51]; - char sname[64]; + int i, j; - int i = gethostname(hostname, sizeof(hostname)); - if (i == 0) { - hostname[sizeof(hostname)-1] = 0; + int err = gethostname(hostname, sizeof(hostname)); + if (err == 0) { + hostname[sizeof(hostname)-1] = '\0'; // on some systems, an FQDN is returned; remove domain part char *dot = strchr(hostname, '.'); if (dot) *dot = 0; + } else + hostname[0] = '\0'; + + ctx = (dns_sd_service_t *)calloc(sizeof(dns_sd_service_t), MAX_DNS_SD_SERVICES); - snprintf(sname, sizeof(sname), "Dire Wolf on %s", hostname); - return strdup(sname); + if (mc->agwpe_port != 0) { + ctx[0].port = mc->agwpe_port; + ctx[0].channel = -1; + ctx[0].name = make_service_name(mc->dns_sd_name, hostname, -1); + } + for (i = 0, j = 1; i < MAX_KISS_TCP_PORTS; i++) { + if (mc->kiss_port[i] != 0) { + ctx[j].port = mc->kiss_port[i]; + ctx[j].channel = mc->kiss_chan[i]; + ctx[j].name = make_service_name(mc->dns_sd_name, hostname, mc->kiss_chan[i]); + j++; + } } - - return strdup("Dire Wolf"); -} + return ctx; +} diff --git a/src/dns_sd_common.h b/src/dns_sd_common.h index f104bf83..48c1b1e3 100644 --- a/src/dns_sd_common.h +++ b/src/dns_sd_common.h @@ -1,7 +1,23 @@ +/*------------------------------------------------------------------- + * + * Name: dns_sd_common.h + * + * Purpose: Header file for common DNS-SD values, types, and functions + * + *------------------------------------------------------------------*/ -#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) +#if (USE_AVAHI_CLIENT | USE_MACOS_DNSSD) -char *dns_sd_default_service_name(void); +// One for AGWPE, remainder for KISS +#define MAX_DNS_SD_SERVICES (1 + MAX_KISS_TCP_PORTS) -#endif +typedef struct dns_sd_service_s { + int port; + int channel; + char *name; +} dns_sd_service_t; +int dns_sd_service_count(struct misc_config_s *mc); +dns_sd_service_t *dns_sd_create_context(struct misc_config_s *mc); + +#endif // USE_AVAHI_CLIENT | USE_MACOS_DNSSD diff --git a/src/dns_sd_dw.h b/src/dns_sd_dw.h index 79f4b868..81ae1626 100644 --- a/src/dns_sd_dw.h +++ b/src/dns_sd_dw.h @@ -1,10 +1,27 @@ +/*------------------------------------------------------------------- + * + * Name: dns_sd_dw.h + * + * Purpose: Header file for announcing DNS-SD services + * + *------------------------------------------------------------------*/ -#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) +#if (USE_AVAHI_CLIENT | USE_MACOS_DNSSD) #include "config.h" -#define DNS_SD_SERVICE "_kiss-tnc._tcp" +// DNS-SD service types +#define DNS_SD_TYPE_AGWPE "_agwpe._tcp" +#define DNS_SD_TYPE_KISS "_kiss-tnc._tcp" + +// DNS-SD service type names +#define DNS_SD_TYPE_NAME_AGWPE "AGWPE" +#define DNS_SD_TYPE_NAME_KISS "KISS TCP" + +// Temporary until both Linux and Mac are converted +#define DNS_SD_SERVICE DNS_SD_TYPE_KISS void dns_sd_announce (struct misc_config_s *mc); +void dns_sd_term (void); -#endif // USE_AVAHI_CLIENT +#endif // USE_AVAHI_CLIENT | USE_MACOS_DNSSD diff --git a/src/dns_sd_macos.c b/src/dns_sd_macos.c index d733a412..d02e4ee3 100644 --- a/src/dns_sd_macos.c +++ b/src/dns_sd_macos.c @@ -2,6 +2,7 @@ // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// Copyright (C) 2025 Martin F N Cooper, KD6YAM // // // This program is free software: you can redistribute it and/or modify @@ -38,52 +39,277 @@ #include #include #include +#include +#include #include "dns_sd_dw.h" #include "dns_sd_common.h" #include "textcolor.h" -static char *name = NULL; -static void registerServiceCallBack(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, - const char* name, const char* regType, const char* domain, void* context) +// We don't really want select() to timeout, hence the very large number +#define SELECT_TIMEOUT 100000000 + +// Extended context for the Mac DNS-SD API +typedef struct dns_sd_services_s { + dns_sd_service_t *ctx; + DNSServiceRef sd_ref[MAX_DNS_SD_SERVICES]; + int sd_fd[MAX_DNS_SD_SERVICES]; +} dns_sd_services_t; + +// Thread required to receive events from the DNS-SD daemon +static pthread_t event_thread; + +// Pipe fds to allow for a graceful exit +static int stop_fd[2] = {-1, -1}; + + +/*------------------------------------------------------------------------------ + * + * Name: process_events + * + * Purpose: Thread function to process events from the DNS-SD daemon. + * + * Inputs: arg - Extended context with all info required to process + * events for any announced services. + * + * Description: Obtains a set of file descriptors, one per announced service, + * and creates a pipe to allow for a graceful exit. Waits for + * notification from the DNS-SD daemon, and processes any events + * received. Removes any announced services on completion. + * + * This function exits normally when the special stop_fd is ready + * for reading, which happens when the associated pipe is written. + * It may also exit abnormally if an error is encountered. + * + *------------------------------------------------------------------------------*/ + +static void *process_events (void *arg) { - if (errorCode == kDNSServiceErr_NoError) { - text_color_set(DW_COLOR_INFO); - dw_printf("DNS-SD: Successfully registered '%s'\n", name); - } else { - text_color_set(DW_COLOR_ERROR); - dw_printf("DNS-SD: Failed to register '%s': %d\n", name, errorCode); - } + dns_sd_services_t *svcs = (dns_sd_services_t *) arg; + int i, last_fd, result; + int stop_now = 0; + + // Populate the fds + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (svcs->sd_ref[i] != NULL) { + svcs->sd_fd[i] = DNSServiceRefSockFD(svcs->sd_ref[i]); + last_fd = svcs->sd_fd[i]; + } + } + + // Create a pipe to allow for a graceful exit + result = pipe(stop_fd); + if (result == 0) { + last_fd = stop_fd[1]; + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("pipe() returned %d errno %d: %s\n", result, errno, strerror(errno)); + } + + fd_set readfds; + struct timeval timeout; + + while (!stop_now) { + // Prepare the set of file descriptors + FD_ZERO(&readfds); + FD_SET(stop_fd[0], &readfds); + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (svcs->sd_fd[i] > 0) { + FD_SET(svcs->sd_fd[i], &readfds); + } + } + + timeout.tv_sec = SELECT_TIMEOUT; + timeout.tv_usec = 0; + + // Wait for something to happen + result = select(last_fd + 1, &readfds, NULL, NULL, &timeout); + if (result > 0) { + DNSServiceErrorType err; + + // If the pipe was written to, it's time to exit + if (FD_ISSET(stop_fd[0], &readfds)) { + stop_now = 1; + break; + } + // Check for services with events + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (svcs->sd_fd[i] > 0 && FD_ISSET(svcs->sd_fd[i], &readfds)) { + err = DNSServiceProcessResult(svcs->sd_ref[i]); + if (err != kDNSServiceErr_NoError) { + text_color_set(DW_COLOR_ERROR); + dw_printf("Error from the API: %i for index %i\n", err, i); + stop_now = 1; // but continue to process remaining fds + } + } + } + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("select() returned %d errno %d: %s\n", result, errno, strerror(errno)); + if (errno != EINTR) + stop_now = 1; + } + } + + // Clean up + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (svcs->sd_ref[i] != NULL) { + DNSServiceRefDeallocate(svcs->sd_ref[i]); + } + } + for (i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (svcs->ctx[i].name) + free(svcs->ctx[i].name); + } + free(svcs->ctx); + free(svcs); + close(stop_fd[0]); + close(stop_fd[1]); + + return NULL; } + +/*------------------------------------------------------------------------------ + * + * Name: registration_callback + * + * Purpose: Called when the registration for a service completes or fails. + * + * Inputs: sdRef - Service reference initialized upon registration. + * + * flags - Unused in this implementation. + * + * errorCode - Indicates success, or type of failure if + * registration failed. + * + * name - Name of service registered. It is possible + * for this to differ from the name we created, + * since name a conflict is resolved by the system + * and a new name created on our behalf. + * + * regType - Type of service registered. This will be either + * DNS_SD_TYPE_AGWPE or DNS_SD_TYPE_KISS. + * + * domain - Domain on which the service is registered. + * Always the default domain in this implementation. + * + * context - The context for this service. An instance of + * dns_sd_service_t. + * + * Description: This callback is invoked within the event processing thread + * each time a service is registered, successfully or not. At + * this time, it is used only to indicate to the user whether or + * the service was registered successfully. + * + *------------------------------------------------------------------------------*/ + +static void registration_callback (DNSServiceRef sdRef, DNSServiceFlags flags, + DNSServiceErrorType errorCode, const char* name, const char* regType, + const char* domain, void* context) +{ + char *svc_type = (char*) regType; + + if (strncmp(regType, DNS_SD_TYPE_AGWPE, strlen(DNS_SD_TYPE_AGWPE)) == 0) + svc_type = DNS_SD_TYPE_NAME_AGWPE; + else if (strncmp(regType, DNS_SD_TYPE_KISS, strlen(DNS_SD_TYPE_KISS)) == 0) + svc_type = DNS_SD_TYPE_NAME_KISS; + + if (errorCode == kDNSServiceErr_NoError) { + text_color_set(DW_COLOR_INFO); + dw_printf("DNS-SD: Successfully registered %s service '%s'\n", svc_type, name); + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("DNS-SD: Failed to register %s service '%s': %d\n", svc_type, name, errorCode); + } +} + + +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_announce + * + * Purpose: Announce all configured AGWPE and KISS TCP services via DNS + * Service Discovery. + * + * Inputs: mc - Dire Wolf misc config as read from the config file. + * + * Description: Register all configured AGWPE and KISS TCP services, and start + * a thread to watch for events that apply to those services. + * The thread is required for our registration callback to be + * invoked. + * + *------------------------------------------------------------------------------*/ + void dns_sd_announce (struct misc_config_s *mc) { - //int kiss_port = mc->kiss_port; // now an array. - int kiss_port = mc->kiss_port[0]; // FIXME: Quick hack until I can handle multiple TCP ports properly. + // If there are no services to announce, we're done + if (dns_sd_service_count(mc) == 0) + return; - if (mc->dns_sd_name[0]) { - name = strdup(mc->dns_sd_name); - } else { - name = dns_sd_default_service_name(); - } + DNSServiceRef sdRef; + DNSServiceErrorType err; + dns_sd_service_t *ctx; + dns_sd_services_t *svcs; - uint16_t port_nw = htons(kiss_port); + ctx = dns_sd_create_context(mc); + svcs = (dns_sd_services_t *)calloc(sizeof(dns_sd_services_t), 1); + svcs->ctx = ctx; - DNSServiceRef registerRef; - DNSServiceErrorType err = DNSServiceRegister( - ®isterRef, 0, 0, name, DNS_SD_SERVICE, NULL, NULL, - port_nw, 0, NULL, registerServiceCallBack, NULL); + for (int i = 0; i < MAX_DNS_SD_SERVICES; i++) { + if (ctx[i].port == 0) + continue; + err = DNSServiceRegister( + &sdRef, + 0, // no flags + 0, // all interfaces + ctx[i].name, + i == 0 ? DNS_SD_TYPE_AGWPE : DNS_SD_TYPE_KISS, + NULL, // default domain(s) + NULL, // default hostname(s) + htons(ctx[i].port), + 0, // no txt record + NULL, // no txt record + registration_callback, + (void *)&ctx[i] + ); - if (err == kDNSServiceErr_NoError) { - text_color_set(DW_COLOR_INFO); - dw_printf("DNS-SD: Announcing KISS TCP on port %d as '%s'\n", kiss_port, name); + if (err == kDNSServiceErr_NoError) { + svcs->sd_ref[i] = sdRef; + text_color_set(DW_COLOR_INFO); + dw_printf("DNS-SD: Announcing %s on port %d as '%s'\n", + i == 0 ? DNS_SD_TYPE_NAME_AGWPE : DNS_SD_TYPE_NAME_KISS, + ctx[i].port, ctx[i].name); } else { - text_color_set(DW_COLOR_ERROR); - dw_printf("DNS-SD: Failed to announce '%s': %d\n", name, err); + text_color_set(DW_COLOR_ERROR); + dw_printf("DNS-SD: Failed to announce '%s': %d\n", ctx[i].name, err); } + } + + pthread_create(&event_thread, NULL, &process_events, (void *)svcs); } -#endif // USE_MACOS_DNSSD +/*------------------------------------------------------------------------------ + * + * Name: dns_sd_term + * + * Purpose: Gracefully shut down the event processing thread and remove all + * service registrations. + * + * Description: By writing to the stop_fd pipe, select() in the event processing + * thread will wake up, and the thread will recognize that it should + * exit after cleaning up registered services. + * + *------------------------------------------------------------------------------*/ + +void dns_sd_term (void) { + if (stop_fd[1] != -1) { + int val = 1; + write(stop_fd[1], &val, sizeof(val)); + } +} + +#endif // USE_MACOS_DNSSD