From e96da6d01fdef45bb80ed0b0c3292c4603520645 Mon Sep 17 00:00:00 2001 From: "Cathy J. Fitzpatrick" Date: Mon, 1 Jul 2024 04:20:31 -0700 Subject: [PATCH] macOS: New implementation of service publication The current implementation of service publication on macOS uses `avahi-client`, but the majority of macOS machines do not have Avahi installed because macOS provides a native alternative (`mDNSresponder`), meaning that there is no reason to install Avahi. The current implementation also attempts to load the Avahi client libraries using `dlopen(3)`, which has a variety of restrictions on macOS, such as only being willing to load from certain directories. Depending on where the Avahi binaries are installed, they might not be loadable through the current invocation of `dlopen(3)`. Instead of using an Avahi client on macOS, it makes more sense to use the native macOS API for publishing services via `mDNSresponder`. This commit supplies such an implementation that uses the macOS native API. It also has the advantage of being much simpler than the previous implementation. Furthermore, this new implementation works on all macOS machines, because it relies only on native APIs, rather than on third-party software that is not commonly installed on macOS. --- docs/source/building/macos.rst | 2 +- packaging/macos/Portfile | 3 +- src/platform/linux/publish.cpp | 1 - src/platform/macos/publish.cpp | 475 +++++---------------------------- 4 files changed, 63 insertions(+), 418 deletions(-) diff --git a/docs/source/building/macos.rst b/docs/source/building/macos.rst index c2a0adbbd2e..6d90a651b25 100644 --- a/docs/source/building/macos.rst +++ b/docs/source/building/macos.rst @@ -12,7 +12,7 @@ MacPorts Install Requirements .. code-block:: bash - sudo port install avahi cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip + sudo port install cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip Homebrew """""""" diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index 4a89b1d8848..3fdb7b93718 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -38,8 +38,7 @@ depends_build-append port:doxygen \ port:python311 \ port:py311-pip -depends_lib port:avahi \ - port:curl \ +depends_lib port:curl \ port:libopus \ port:miniupnpc diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 2115ec8a798..29641411e50 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -2,7 +2,6 @@ * @file src/platform/linux/publish.cpp * @brief Definitions for publishing services on Linux. * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html - * @todo Use a common file for this and src/platform/macos/publish.cpp */ #include diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index 3eda292bf73..8f24f7f9859 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -1,442 +1,89 @@ /** * @file src/platform/macos/publish.cpp * @brief Definitions for publishing services on macOS. - * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html - * @todo Use a common file for this and src/platform/linux/publish.cpp */ +#include #include -#include "misc.h" #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" -#include "src/utility.h" using namespace std::literals; -namespace avahi { - - /** - * @brief Error codes used by avahi. - */ - enum err_e { - OK = 0, ///< OK - ERR_FAILURE = -1, ///< Generic error code - ERR_BAD_STATE = -2, ///< Object was in a bad state - ERR_INVALID_HOST_NAME = -3, ///< Invalid host name - ERR_INVALID_DOMAIN_NAME = -4, ///< Invalid domain name - ERR_NO_NETWORK = -5, ///< No suitable network protocol available - ERR_INVALID_TTL = -6, ///< Invalid DNS TTL - ERR_IS_PATTERN = -7, ///< RR key is pattern - ERR_COLLISION = -8, ///< Name collision - ERR_INVALID_RECORD = -9, ///< Invalid RR - - ERR_INVALID_SERVICE_NAME = -10, ///< Invalid service name - ERR_INVALID_SERVICE_TYPE = -11, ///< Invalid service type - ERR_INVALID_PORT = -12, ///< Invalid port number - ERR_INVALID_KEY = -13, ///< Invalid key - ERR_INVALID_ADDRESS = -14, ///< Invalid address - ERR_TIMEOUT = -15, ///< Timeout reached - ERR_TOO_MANY_CLIENTS = -16, ///< Too many clients - ERR_TOO_MANY_OBJECTS = -17, ///< Too many objects - ERR_TOO_MANY_ENTRIES = -18, ///< Too many entries - ERR_OS = -19, ///< OS error - - ERR_ACCESS_DENIED = -20, ///< Access denied - ERR_INVALID_OPERATION = -21, ///< Invalid operation - ERR_DBUS_ERROR = -22, ///< An unexpected D-Bus error occurred - ERR_DISCONNECTED = -23, ///< Daemon connection failed - ERR_NO_MEMORY = -24, ///< Memory exhausted - ERR_INVALID_OBJECT = -25, ///< The object passed to this function was invalid - ERR_NO_DAEMON = -26, ///< Daemon not running - ERR_INVALID_INTERFACE = -27, ///< Invalid interface - ERR_INVALID_PROTOCOL = -28, ///< Invalid protocol - ERR_INVALID_FLAGS = -29, ///< Invalid flags - - ERR_NOT_FOUND = -30, ///< Not found - ERR_INVALID_CONFIG = -31, ///< Configuration error - ERR_VERSION_MISMATCH = -32, ///< Version mismatch - ERR_INVALID_SERVICE_SUBTYPE = -33, ///< Invalid service subtype - ERR_INVALID_PACKET = -34, ///< Invalid packet - ERR_INVALID_DNS_ERROR = -35, ///< Invalid DNS return code - ERR_DNS_FORMERR = -36, ///< DNS Error: Form error - ERR_DNS_SERVFAIL = -37, ///< DNS Error: Server Failure - ERR_DNS_NXDOMAIN = -38, ///< DNS Error: No such domain - ERR_DNS_NOTIMP = -39, ///< DNS Error: Not implemented - - ERR_DNS_REFUSED = -40, ///< DNS Error: Operation refused - ERR_DNS_YXDOMAIN = -41, ///< TODO - ERR_DNS_YXRRSET = -42, ///< TODO - ERR_DNS_NXRRSET = -43, ///< TODO - ERR_DNS_NOTAUTH = -44, ///< DNS Error: Not authorized - ERR_DNS_NOTZONE = -45, ///< TODO - ERR_INVALID_RDATA = -46, ///< Invalid RDATA - ERR_INVALID_DNS_CLASS = -47, ///< Invalid DNS class - ERR_INVALID_DNS_TYPE = -48, ///< Invalid DNS type - ERR_NOT_SUPPORTED = -49, ///< Not supported - - ERR_NOT_PERMITTED = -50, ///< Operation not permitted - ERR_INVALID_ARGUMENT = -51, ///< Invalid argument - ERR_IS_EMPTY = -52, ///< Is empty - ERR_NO_CHANGE = -53, ///< The requested operation is invalid because it is redundant - - ERR_MAX = -54 ///< TODO - }; - - constexpr auto IF_UNSPEC = -1; - enum proto { - PROTO_INET = 0, ///< IPv4 - PROTO_INET6 = 1, ///< IPv6 - PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s) - }; - - enum ServerState { - SERVER_INVALID, ///< Invalid state (initial) - SERVER_REGISTERING, ///< Host RRs are being registered - SERVER_RUNNING, ///< All host RRs have been established - SERVER_COLLISION, ///< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() - SERVER_FAILURE ///< Some fatal failure happened, the server is unable to proceed - }; - - enum ClientState { - CLIENT_S_REGISTERING = SERVER_REGISTERING, ///< Server state: REGISTERING - CLIENT_S_RUNNING = SERVER_RUNNING, ///< Server state: RUNNING - CLIENT_S_COLLISION = SERVER_COLLISION, ///< Server state: COLLISION - CLIENT_FAILURE = 100, ///< Some kind of error happened on the client side - CLIENT_CONNECTING = 101 ///< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. - }; - - enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, ///< The group has not yet been committed, the user must still call avahi_entry_group_commit() - ENTRY_GROUP_REGISTERING, ///< The entries of the group are currently being registered - ENTRY_GROUP_ESTABLISHED, ///< The entries have successfully been established - ENTRY_GROUP_COLLISION, ///< A name collision for one of the entries in the group has been detected, the entries have been withdrawn - ENTRY_GROUP_FAILURE ///< Some kind of failure happened, the entries have been withdrawn - }; - - enum ClientFlags { - CLIENT_IGNORE_USER_CONFIG = 1, ///< Don't read user configuration - CLIENT_NO_FAIL = 2 ///< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear - }; - - /** - * @brief Flags for publishing functions. - */ - enum PublishFlags { - PUBLISH_UNIQUE = 1, ///< For raw records: The RRset is intended to be unique - PUBLISH_NO_PROBE = 2, ///< For raw records: Though the RRset is intended to be unique no probes shall be sent - PUBLISH_NO_ANNOUNCE = 4, ///< For raw records: Do not announce this RR to other hosts - PUBLISH_ALLOW_MULTIPLE = 8, ///< For raw records: Allow multiple local records of this type, even if they are intended to be unique - PUBLISH_NO_REVERSE = 16, ///< For address records: don't create a reverse (PTR) entry - PUBLISH_NO_COOKIE = 32, ///< For service records: do not implicitly add the local service cookie to TXT data - PUBLISH_UPDATE = 64, ///< Update existing records instead of adding new ones - PUBLISH_USE_WIDE_AREA = 128, ///< Register the record using wide area DNS (i.e. unicast DNS update) - PUBLISH_USE_MULTICAST = 256 ///< Register the record using multicast DNS - }; - - using IfIndex = int; - using Protocol = int; - - struct EntryGroup; - struct Poll; - struct SimplePoll; - struct Client; - - typedef void (*ClientCallback)(Client *, ClientState, void *userdata); - typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); - - typedef void (*free_fn)(void *); - - typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); - typedef void (*client_free_fn)(Client *); - typedef char *(*alternative_service_name_fn)(char *); - - typedef Client *(*entry_group_get_client_fn)(EntryGroup *); - - typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); - typedef int (*entry_group_add_service_fn)( - EntryGroup *group, - IfIndex interface, - Protocol protocol, - PublishFlags flags, - const char *name, - const char *type, - const char *domain, - const char *host, - uint16_t port, - ...); - - typedef int (*entry_group_is_empty_fn)(EntryGroup *); - typedef int (*entry_group_reset_fn)(EntryGroup *); - typedef int (*entry_group_commit_fn)(EntryGroup *); - - typedef char *(*strdup_fn)(const char *); - typedef char *(*strerror_fn)(int); - typedef int (*client_errno_fn)(Client *); - - typedef Poll *(*simple_poll_get_fn)(SimplePoll *); - typedef int (*simple_poll_loop_fn)(SimplePoll *); - typedef void (*simple_poll_quit_fn)(SimplePoll *); - typedef SimplePoll *(*simple_poll_new_fn)(); - typedef void (*simple_poll_free_fn)(SimplePoll *); - - free_fn free; - client_new_fn client_new; - client_free_fn client_free; - alternative_service_name_fn alternative_service_name; - entry_group_get_client_fn entry_group_get_client; - entry_group_new_fn entry_group_new; - entry_group_add_service_fn entry_group_add_service; - entry_group_is_empty_fn entry_group_is_empty; - entry_group_reset_fn entry_group_reset; - entry_group_commit_fn entry_group_commit; - strdup_fn strdup; - strerror_fn strerror; - client_errno_fn client_errno; - simple_poll_get_fn simple_poll_get; - simple_poll_loop_fn simple_poll_loop; - simple_poll_quit_fn simple_poll_quit; - simple_poll_new_fn simple_poll_new; - simple_poll_free_fn simple_poll_free; - - int - init_common() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" }, - { (dyn::apiproc *) &free, "avahi_free" }, - { (dyn::apiproc *) &strdup, "avahi_strdup" }, - { (dyn::apiproc *) &strerror, "avahi_strerror" }, - { (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" }, - { (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" }, - { (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" }, - { (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" }, - { (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - - int - init_client() { - if (init_common()) { - return -1; - } - - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" }); - if (!handle) { - return -1; +namespace platf::publish { + namespace { + struct ServiceRefDeleter { + typedef DNSServiceRef pointer; + void operator()(pointer serviceRef) { + DNSServiceRefDeallocate(serviceRef); + BOOST_LOG(info) << "Deregistered DNS service."sv; } - } - - std::vector> funcs { - { (dyn::apiproc *) &client_new, "avahi_client_new" }, - { (dyn::apiproc *) &client_free, "avahi_client_free" }, - { (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" }, - { (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" }, - { (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" }, - { (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" }, - { (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" }, - { (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" }, - { (dyn::apiproc *) &client_errno, "avahi_client_errno" }, }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } -} // namespace avahi - -namespace platf::publish { - - template - void - free(T *p) { - avahi::free(p); - } - - template - using ptr_t = util::safe_ptr>; - using client_t = util::dyn_safe_ptr; - using poll_t = util::dyn_safe_ptr; - - avahi::EntryGroup *group = nullptr; - - poll_t poll; - client_t client; - - ptr_t name; - - void - create_services(avahi::Client *c); - - void - entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { - group = g; - - switch (state) { - case avahi::ENTRY_GROUP_ESTABLISHED: - BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; - break; - case avahi::ENTRY_GROUP_COLLISION: - name.reset(avahi::alternative_service_name(name.get())); - - BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); - - create_services(avahi::entry_group_get_client(g)); - break; - case avahi::ENTRY_GROUP_FAILURE: - BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::ENTRY_GROUP_UNCOMMITED: - case avahi::ENTRY_GROUP_REGISTERING:; - } - } - - void - create_services(avahi::Client *c) { - int ret; - - auto fg = util::fail_guard([]() { - avahi::simple_poll_quit(poll.get()); - }); - - if (!group) { - if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { - BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); - return; + class deinit_t : public ::platf::deinit_t, std::unique_ptr { + public: + deinit_t(DNSServiceRef serviceRef): unique_ptr(serviceRef) { + // Create a thread that will use select(2) to wait for a response from the DNS service. + // The thread will give up if an error is received or if _stopRequested becomes true. + _thread = std::thread{ [serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { + const auto socket = DNSServiceRefSockFD(serviceRef); + while (!_stopRequested) { + fd_set fdset; FD_ZERO(&fdset); FD_SET(socket, &fdset); + auto timeout = timeval{ .tv_sec = 3, .tv_usec = 0 }; // 3 second timeout + const auto ready = select(socket + 1, &fdset, nullptr, nullptr, &timeout); + if (ready == -1) { + BOOST_LOG(error) << "Failed to obtain response from DNS service."sv; + break; + } else if (ready != 0) { + DNSServiceProcessResult(serviceRef); + break; + } + } + } }; } - } - - if (avahi::entry_group_is_empty(group)) { - BOOST_LOG(info) << "Adding avahi service "sv << name.get(); - - ret = avahi::entry_group_add_service( - group, - avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, - avahi::PublishFlags(0), - name.get(), - SERVICE_TYPE, - nullptr, nullptr, - net::map_port(nvhttp::PORT_HTTP), - nullptr); - - if (ret < 0) { - if (ret == avahi::ERR_COLLISION) { - // A service name collision with a local service happened. Let's pick a new name - name.reset(avahi::alternative_service_name(name.get())); - BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); - - avahi::entry_group_reset(group); - - create_services(c); - - fg.disable(); - return; - } - - BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); - return; + ~deinit_t() override { + _stopRequested = true; + _thread.join(); } + deinit_t(const deinit_t &) = delete; + deinit_t &operator=(const deinit_t &) = delete; + private: + std::thread _thread; + std::atomic _stopRequested = false; + }; - ret = avahi::entry_group_commit(group); - if (ret < 0) { - BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); + void registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, + DNSServiceErrorType errorCode, const char */*name*/, + const char */*regtype*/, const char */*domain*/, void */*context*/) { + if (errorCode != kDNSServiceErr_NoError) { + BOOST_LOG(error) << "Failed to register DNS service: Error "sv << errorCode; return; } + BOOST_LOG(info) << "Successfully registered DNS service."sv; } - - fg.disable(); - } - - void - client_callback(avahi::Client *c, avahi::ClientState state, void *) { - switch (state) { - case avahi::CLIENT_S_RUNNING: - create_services(c); - break; - case avahi::CLIENT_FAILURE: - BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::CLIENT_S_COLLISION: - case avahi::CLIENT_S_REGISTERING: - if (group) - avahi::entry_group_reset(group); - break; - case avahi::CLIENT_CONNECTING:; - } - } - - class deinit_t: public ::platf::deinit_t { - public: - std::thread poll_thread; - - explicit deinit_t(std::thread poll_thread): - poll_thread { std::move(poll_thread) } {} - - ~deinit_t() override { - if (avahi::simple_poll_quit && poll) { - avahi::simple_poll_quit(poll.get()); - } - - if (poll_thread.joinable()) { - poll_thread.join(); - } - } - }; + } // anonymous namespace [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { - if (avahi::init_client()) { - return nullptr; - } - - int avhi_error; - - poll.reset(avahi::simple_poll_new()); - if (!poll) { - BOOST_LOG(error) << "Failed to create simple poll object."sv; + auto serviceRef = DNSServiceRef{}; + const auto status = DNSServiceRegister( + &serviceRef, + 0, // flags + 0, // interfaceIndex + SERVICE_NAME, SERVICE_TYPE, + nullptr, // domain + nullptr, // host + htons(net::map_port(nvhttp::PORT_HTTP)), + 0, // txtLen + nullptr, // txtRecord + registrationCallback, + nullptr // context + ); + if (status != kDNSServiceErr_NoError) { + BOOST_LOG(error) << "Failed immediately to register DNS service: Error "sv << status; return nullptr; } - - name.reset(avahi::strdup(SERVICE_NAME)); - - client.reset( - avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); - - if (!client) { - BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); - return nullptr; - } - - return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); + return std::make_unique(serviceRef); } } // namespace platf::publish