Implement DNS-SD publishing of KISS over TCP service on Linux and Mac

This allows client applications to locate the IP addresses and the port of
the KISS TCP service, allowing the end user to just select a Dire Wolf from
a list instead of trying to guess its dynamic IP address and typing it in
manually.  This is especially convenient on mobile devices.

On Linux, the standard Avahi daemon is used via dbus and the avahi-client
library.  Building with it requires installing the development header
package; README.md is updated accordingly.

On Mac, the MacOS dnssd API is used:
https://developer.apple.com/documentation/dnssd?language=objc

I don't have Windows, but more recent Windows 10 builds apparently have
a working DNS-SD mDNS implementation that can be used on 64-bit builds.
This commit is contained in:
Heikki Hannikainen 2020-12-27 13:48:46 +00:00
parent 8ac14f86f5
commit feb1034cca
12 changed files with 524 additions and 4 deletions

View File

@ -156,6 +156,10 @@ elseif(APPLE)
set(CMAKE_MACOSX_RPATH ON) set(CMAKE_MACOSX_RPATH ON)
message(STATUS "RPATH support: ${CMAKE_MACOSX_RPATH}") message(STATUS "RPATH support: ${CMAKE_MACOSX_RPATH}")
# just blindly enable dns-sd
set(USE_MACOS_DNSSD ON)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_MACOS_DNSSD")
elseif (WIN32) elseif (WIN32)
if(NOT VS2015 AND NOT VS2017) if(NOT VS2015 AND NOT VS2017)
message(FATAL_ERROR "You must use Microsoft Visual Studio 2015 or 2017 as compiler") message(FATAL_ERROR "You must use Microsoft Visual Studio 2015 or 2017 as compiler")
@ -276,6 +280,11 @@ if(LINUX)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_CM108") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_CM108")
endif() endif()
find_package(Avahi)
if(AVAHI_CLIENT_FOUND)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_AVAHI_CLIENT")
endif()
elseif (NOT WIN32 AND NOT CYGWIN) elseif (NOT WIN32 AND NOT CYGWIN)
find_package(Portaudio REQUIRED) find_package(Portaudio REQUIRED)
if(PORTAUDIO_FOUND) if(PORTAUDIO_FOUND)

View File

@ -131,6 +131,7 @@ On Debian / Ubuntu / Raspbian / Raspberry Pi OS:
sudo apt-get install cmake sudo apt-get install cmake
sudo apt-get install libasound2-dev sudo apt-get install libasound2-dev
sudo apt-get install libudev-dev sudo apt-get install libudev-dev
sudo apt-get install libavahi-client-dev
Or on Red Hat / Fedora / CentOS: Or on Red Hat / Fedora / CentOS:
@ -140,6 +141,7 @@ Or on Red Hat / Fedora / CentOS:
sudo yum install make sudo yum install make
sudo yum install alsa-lib-devel sudo yum install alsa-lib-devel
sudo yum install libudev-devel sudo yum install libudev-devel
sudo yum install avahi-devel
CentOS 6 & 7 currently have cmake 2.8 but we need 3.1 or later. CentOS 6 & 7 currently have cmake 2.8 but we need 3.1 or later.
First you need to enable the EPEL repository. Add a symlink if you don't already have the older version and want to type cmake rather than cmake3. First you need to enable the EPEL repository. Add a symlink if you don't already have the older version and want to type cmake rather than cmake3.

View File

@ -0,0 +1,19 @@
find_library(AVAHI_COMMON_LIBRARY NAMES avahi-common PATHS ${PC_AVAHI_CLIENT_LIBRARY_DIRS})
if(AVAHI_COMMON_LIBRARY)
set(AVAHI_COMMON_FOUND TRUE)
endif()
find_library(AVAHI_CLIENT_LIBRARY NAMES avahi-client PATHS ${PC_AVAHI_CLIENT_LIBRARY_DIRS})
if(AVAHI_CLIENT_LIBRARY)
set(AVAHI_CLIENT_FOUND TRUE)
endif()
FIND_PACKAGE_HANDLE_STANDARD_ARGS(AVAHI DEFAULT_MSG AVAHI_COMMON_FOUND AVAHI_CLIENT_FOUND)
if (AVAHI_FOUND)
set(AVAHI_INCLUDE_DIRS ${AVAHI_UI_INCLUDE_DIR})
set(AVAHI_LIBRARIES ${AVAHI_COMMON_LIBRARY} ${AVAHI_CLIENT_LIBRARY})
endif()
mark_as_advanced(AVAHI_INCLUDE_DIRS AVAHI_LIBRARIES)

View File

@ -96,6 +96,12 @@ if(LINUX)
cm108.c cm108.c
) )
endif() endif()
if(AVAHI_CLIENT_FOUND)
list(APPEND direwolf_SOURCES
dns_sd_common.c
dns_sd_avahi.c
)
endif()
elseif(WIN32 OR CYGWIN) # windows elseif(WIN32 OR CYGWIN) # windows
list(APPEND direwolf_SOURCES list(APPEND direwolf_SOURCES
audio_win.c audio_win.c
@ -111,6 +117,12 @@ if(LINUX)
list(APPEND direwolf_SOURCES list(APPEND direwolf_SOURCES
audio_portaudio.c audio_portaudio.c
) )
if(USE_MACOS_DNSSD)
list(APPEND direwolf_SOURCES
dns_sd_common.c
dns_sd_macos.c
)
endif()
endif() endif()
add_executable(direwolf add_executable(direwolf
@ -127,6 +139,7 @@ target_link_libraries(direwolf
${ALSA_LIBRARIES} ${ALSA_LIBRARIES}
${UDEV_LIBRARIES} ${UDEV_LIBRARIES}
${PORTAUDIO_LIBRARIES} ${PORTAUDIO_LIBRARIES}
${AVAHI_LIBRARIES}
) )
if(WIN32 OR CYGWIN) if(WIN32 OR CYGWIN)

View File

@ -856,6 +856,8 @@ void config_init (char *fname, struct audio_s *p_audio_config,
p_misc_config->enable_kiss_pt = 0; /* -p option */ p_misc_config->enable_kiss_pt = 0; /* -p option */
p_misc_config->kiss_copy = 0; p_misc_config->kiss_copy = 0;
p_misc_config->dns_sd_enabled = 1;
/* Defaults from http://info.aprs.net/index.php?title=SmartBeaconing */ /* Defaults from http://info.aprs.net/index.php?title=SmartBeaconing */
p_misc_config->sb_configured = 0; /* TRUE if SmartBeaconing is configured. */ p_misc_config->sb_configured = 0; /* TRUE if SmartBeaconing is configured. */
@ -4564,6 +4566,43 @@ void config_init (char *fname, struct audio_s *p_audio_config,
} }
/*
* DNSSD - Enable or disable (1/0) dns-sd, DNS Service Discovery announcements
* DNSSDNAME - Set DNS-SD service name, defaults to "Dire Wolf on <hostname>"
*/
else if (strcasecmp(t, "DNSSD") == 0) {
int n;
t = split(NULL,0);
if (t == NULL) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Line %d: Missing integer value for DNSSD command.\n", line);
continue;
}
n = atoi(t);
if (n == 0 || n == 1) {
p_misc_config->dns_sd_enabled = n;
} else {
p_misc_config->dns_sd_enabled = 0;
text_color_set(DW_COLOR_ERROR);
dw_printf ("Line %d: Invalid integer value for DNSSD. Disabling dns-sd.\n", line);
}
}
else if (strcasecmp(t, "DNSSDNAME") == 0) {
t = split(NULL, 1);
if (t == NULL) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Line %d: Missing service name for DNSSDNAME.\n", line);
continue;
}
else {
strlcpy(p_misc_config->dns_sd_name, t, sizeof(p_misc_config->dns_sd_name));
}
}
/* /*
* GPSNMEA - Device name for reading from GPS receiver. * GPSNMEA - Device name for reading from GPS receiver.
*/ */

View File

@ -88,6 +88,9 @@ struct misc_config_s {
char log_path[80]; /* Either directory or full file name depending on above. */ char log_path[80]; /* Either directory or full file name depending on above. */
int dns_sd_enabled; /* DNS Service Discovery announcement enabled. */
char dns_sd_name[64]; /* Name announced on dns-sd; defaults to "Dire Wolf on <hostname>" */
int sb_configured; /* TRUE if SmartBeaconing is configured. */ int sb_configured; /* TRUE if SmartBeaconing is configured. */
int sb_fast_speed; /* MPH */ int sb_fast_speed; /* MPH */
int sb_fast_rate; /* seconds */ int sb_fast_rate; /* seconds */

View File

@ -125,6 +125,7 @@
#include "dtime_now.h" #include "dtime_now.h"
#include "fx25.h" #include "fx25.h"
#include "dwsock.h" #include "dwsock.h"
#include "dns_sd_dw.h"
//static int idx_decoded = 0; //static int idx_decoded = 0;
@ -944,6 +945,11 @@ int main (int argc, char *argv[])
server_init (&audio_config, &misc_config); server_init (&audio_config, &misc_config);
kissnet_init (&misc_config); kissnet_init (&misc_config);
#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD)
if (misc_config.kiss_port > 0 && misc_config.dns_sd_enabled)
dns_sd_announce(&misc_config);
#endif
/* /*
* Create a pseudo terminal and KISS TNC emulator. * Create a pseudo terminal and KISS TNC emulator.
*/ */

259
src/dns_sd_avahi.c Normal file
View File

@ -0,0 +1,259 @@
//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2020 Heikki Hannikainen, OH7LZB
//
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/*------------------------------------------------------------------
*
* Module: dns_sd_avahi.c
*
* Purpose: Announce the KISS over TCP service using DNS-SD via Avahi
*
* Description:
*
* Most people have typed in enough IP addresses and ports by now, and
* would rather just select an available TNC that is automatically
* discovered on the local network. Even more so on a mobile device
* such an Android or iOS phone or tablet.
*
* On Linux, the announcement can be made through Avahi, the mDNS
* framework commonly deployed on Linux systems.
*
* This is largely based on the publishing example of the Avahi library.
*/
#ifdef USE_AVAHI_CLIENT
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <avahi-client/client.h>
#include <avahi-client/publish.h>
#include <avahi-common/simple-watch.h>
#include <avahi-common/alternative.h>
#include <avahi-common/malloc.h>
#include <avahi-common/error.h>
#include "dns_sd_dw.h"
#include "dns_sd_common.h"
#include "textcolor.h"
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);
#define PRINT_PREFIX "DNS-SD: Avahi: "
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata)
{
assert(g == group || group == NULL);
group = g;
/* Called whenever the entry group state changes */
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);
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;
text_color_set(DW_COLOR_INFO);
dw_printf(PRINT_PREFIX "Service name collision, renaming service to '%s'\n", name);
/* And recreate the services */
create_services(avahi_entry_group_get_client(g));
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))));
/* Some kind of failure happened while we were registering our services */
avahi_simple_poll_quit(simple_poll);
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
;
}
}
static void create_services(AvahiClient *c)
{
char *n;
int ret;
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))) {
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;
}
} else {
avahi_entry_group_reset(group);
}
/* 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.
*/
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;
}
/* Tell the server to register the service */
if ((ret = avahi_entry_group_commit(group)) < 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf(PRINT_PREFIX "Failed to commit entry group: %s\n", avahi_strerror(ret));
goto fail;
}
}
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)
{
assert(c);
/* Called whenever the client or server state changes */
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);
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);
break;
case AVAHI_CLIENT_S_COLLISION:
/* Let's drop our registered services. When the server is back
* in AVAHI_SERVER_RUNNING state we will register them
* again with the new host name. */
case AVAHI_CLIENT_S_REGISTERING:
/* The server records are now being established. This
* might be caused by a host name change. We need to wait
* for our own records to register until the host name is
* properly esatblished. */
if (group)
avahi_entry_group_reset(group);
break;
case AVAHI_CLIENT_CONNECTING:
;
}
}
static void cleanup(void)
{
/* Cleanup things */
if (client)
avahi_client_free(client);
if (simple_poll)
avahi_simple_poll_free(simple_poll);
avahi_free(name);
}
static void *avahi_mainloop(void *arg)
{
/* Run the main loop */
avahi_simple_poll_loop(simple_poll);
cleanup();
return NULL;
}
void dns_sd_announce (struct misc_config_s *mc)
{
text_color_set(DW_COLOR_DEBUG);
kiss_port = mc->kiss_port;
int 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");
goto fail;
}
if (mc->dns_sd_name[0]) {
name = avahi_strdup(mc->dns_sd_name);
} else {
name = dns_sd_default_service_name();
}
/* Allocate a new client */
client = avahi_client_new(avahi_simple_poll_get(simple_poll), 0, client_callback, NULL, &error);
/* Check wether creating the client object succeeded */
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);
return;
fail:
cleanup();
}
#endif // USE_AVAHI_CLIENT

65
src/dns_sd_common.c Normal file
View File

@ -0,0 +1,65 @@
//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2020 Heikki Hannikainen, OH7LZB
//
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/*------------------------------------------------------------------
*
* Module: dns_sd_common.c
*
* Purpose: Announce the KISS over TCP service using DNS-SD, common functions
*
* Description:
*
* Most people have typed in enough IP addresses and ports by now, and
* would rather just select an available TNC that is automatically
* discovered on the local network. Even more so on a mobile device
* such an Android or iOS phone or tablet.
*
* This module contains common functions needed on Linux and MacOS.
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
/* Get a default service name to publish. By default,
* "Dire Wolf on <hostname>", or just "Dire Wolf" if hostname cannot
* be obtained.
*/
char *dns_sd_default_service_name(void)
{
char hostname[51];
char sname[64];
int i = gethostname(hostname, sizeof(hostname));
if (i == 0) {
hostname[sizeof(hostname)-1] = 0;
// on some systems, an FQDN is returned; remove domain part
char *dot = strchr(hostname, '.');
if (dot)
*dot = 0;
snprintf(sname, sizeof(sname), "Dire Wolf on %s", hostname);
return strdup(sname);
}
return strdup("Dire Wolf");
}

7
src/dns_sd_common.h Normal file
View File

@ -0,0 +1,7 @@
#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD)
char *dns_sd_default_service_name(void);
#endif

10
src/dns_sd_dw.h Normal file
View File

@ -0,0 +1,10 @@
#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD)
#include "config.h"
#define DNS_SD_SERVICE "_kiss-tnc._tcp"
void dns_sd_announce (struct misc_config_s *mc);
#endif // USE_AVAHI_CLIENT

88
src/dns_sd_macos.c Normal file
View File

@ -0,0 +1,88 @@
//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2020 Heikki Hannikainen, OH7LZB
//
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/*------------------------------------------------------------------
*
* Module: dns_sd_macos.c
*
* Purpose: Announce the KISS over TCP service using MacOS dns-sd
*
* Description:
*
* Most people have typed in enough IP addresses and ports by now, and
* would rather just select an available TNC that is automatically
* discovered on the local network. Even more so on a mobile device
* such an Android or iOS phone or tablet.
*
* On MacOs, the announcement can be made through dns-sd.
*/
#ifdef USE_MACOS_DNSSD
#include <string.h>
#include <dns_sd.h>
#include <arpa/inet.h>
#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)
{
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);
}
}
void dns_sd_announce (struct misc_config_s *mc)
{
int kiss_port = mc->kiss_port;
if (mc->dns_sd_name[0]) {
name = strdup(mc->dns_sd_name);
} else {
name = dns_sd_default_service_name();
}
uint16_t port_nw = htons(kiss_port);
DNSServiceRef registerRef;
DNSServiceErrorType err = DNSServiceRegister(
&registerRef, 0, 0, name, DNS_SD_SERVICE, NULL, NULL,
port_nw, 0, NULL, registerServiceCallBack, NULL);
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);
} else {
text_color_set(DW_COLOR_ERROR);
dw_printf("DNS-SD: Failed to announce '%s': %d\n", name, err);
}
}
#endif // USE_MACOS_DNSSD