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