mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-10-23 03:28:06 +09:00
fix(mdns): advertise only served addresses on Linux (#5914)
This commit is contained in:
1 parent
55ff34e21b
commit
3d890eadc4
3 files changed
+1417
-149
No files matched your search
+375
-149
@@ -4,10 +4,28 @@
|
|||||||
* @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
|
* @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
|
||||||
*/
|
*/
|
||||||
// standard includes
|
// standard includes
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ifaddrs.h>
|
||||||
|
#ifdef __linux__
|
||||||
|
// Netlink is a Linux-kernel API: these headers exist only on Linux.
|
||||||
|
#include <linux/netlink.h>
|
||||||
|
#include <linux/rtnetlink.h>
|
||||||
|
#endif
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/time.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
// local includes
|
// local includes
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
#include "publish.h"
|
||||||
|
#include "src/config.h"
|
||||||
#include "src/logging.h"
|
#include "src/logging.h"
|
||||||
#include "src/network.h"
|
#include "src/network.h"
|
||||||
#include "src/nvhttp.h"
|
#include "src/nvhttp.h"
|
||||||
@@ -87,17 +105,6 @@ namespace avahi {
|
|||||||
ERR_MAX = -54 ///< Sentinel below the valid Avahi error-code range; not an error code
|
ERR_MAX = -54 ///< Sentinel below the valid Avahi error-code range; not an error code
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto IF_UNSPEC = -1; ///< Select all network interfaces instead of a specific interface index.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Avahi address-family selectors.
|
|
||||||
*/
|
|
||||||
enum proto {
|
|
||||||
PROTO_INET = 0, ///< IPv4
|
|
||||||
PROTO_INET6 = 1, ///< IPv6
|
|
||||||
PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Avahi daemon lifecycle states.
|
* @brief Avahi daemon lifecycle states.
|
||||||
*/
|
*/
|
||||||
@@ -139,34 +146,8 @@ namespace avahi {
|
|||||||
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
|
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
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Network interface index type used by Avahi.
|
|
||||||
*/
|
|
||||||
using IfIndex = int;
|
|
||||||
/**
|
|
||||||
* @brief Avahi protocol/address-family selector type.
|
|
||||||
*/
|
|
||||||
using Protocol = int;
|
|
||||||
|
|
||||||
struct EntryGroup;
|
|
||||||
struct Poll;
|
struct Poll;
|
||||||
struct SimplePoll;
|
struct SimplePoll;
|
||||||
struct Client;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Avahi client state-change callback signature.
|
* @brief Avahi client state-change callback signature.
|
||||||
@@ -177,114 +158,93 @@ namespace avahi {
|
|||||||
*/
|
*/
|
||||||
using EntryGroupCallback = void (*)(EntryGroup *g, EntryGroupState state, void *userdata);
|
using EntryGroupCallback = void (*)(EntryGroup *g, EntryGroupState state, void *userdata);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to free Avahi-allocated memory.
|
|
||||||
*/
|
|
||||||
using free_fn = void (*)(void *);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Function pointer used to create an Avahi client.
|
* @brief Function pointer used to create an Avahi client.
|
||||||
*/
|
*/
|
||||||
using client_new_fn = Client *(*)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
|
using client_new_fn = Client *(*)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
|
||||||
/**
|
|
||||||
* @brief Function pointer used to destroy an Avahi client.
|
|
||||||
*/
|
|
||||||
using client_free_fn = void (*)(Client *);
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to request an alternative Avahi service name.
|
|
||||||
*/
|
|
||||||
using alternative_service_name_fn = char *(*)(char *);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to get an Avahi client from an entry group.
|
|
||||||
*/
|
|
||||||
using entry_group_get_client_fn = Client *(*)(EntryGroup *);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Function pointer used to create an Avahi entry group.
|
* @brief Function pointer used to create an Avahi entry group.
|
||||||
*/
|
*/
|
||||||
using entry_group_new_fn = EntryGroup *(*)(Client *, EntryGroupCallback, void *userdata);
|
using entry_group_new_fn = EntryGroup *(*)(Client *, EntryGroupCallback, void *userdata);
|
||||||
/**
|
|
||||||
* @brief Function pointer used to add a service to an Avahi entry group.
|
|
||||||
*/
|
|
||||||
using entry_group_add_service_fn = int (*)(
|
|
||||||
EntryGroup *group,
|
|
||||||
IfIndex interface,
|
|
||||||
Protocol protocol,
|
|
||||||
PublishFlags flags,
|
|
||||||
const char *name,
|
|
||||||
const char *type,
|
|
||||||
const char *domain,
|
|
||||||
const char *host,
|
|
||||||
uint16_t port,
|
|
||||||
...
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to test whether an Avahi entry group is empty.
|
|
||||||
*/
|
|
||||||
using entry_group_is_empty_fn = int (*)(EntryGroup *);
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to reset an Avahi entry group.
|
|
||||||
*/
|
|
||||||
using entry_group_reset_fn = int (*)(EntryGroup *);
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to publish an Avahi entry group.
|
|
||||||
*/
|
|
||||||
using entry_group_commit_fn = int (*)(EntryGroup *);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Function pointer used to duplicate Avahi strings.
|
* @brief Function pointer used to duplicate Avahi strings.
|
||||||
*/
|
*/
|
||||||
using strdup_fn = char *(*)(const char *);
|
using strdup_fn = char *(*)(const char *);
|
||||||
/**
|
|
||||||
* @brief Function pointer used to format Avahi error codes.
|
|
||||||
*/
|
|
||||||
using strerror_fn = char *(*)(int);
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to read the last Avahi client error.
|
|
||||||
*/
|
|
||||||
using client_errno_fn = int (*)(Client *);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Function pointer used to get the Avahi simple-poll API.
|
|
||||||
*/
|
|
||||||
using simple_poll_get_fn = Poll *(*)(SimplePoll *);
|
|
||||||
/**
|
/**
|
||||||
* @brief Function pointer used to run the Avahi simple-poll loop.
|
* @brief Function pointer used to run the Avahi simple-poll loop.
|
||||||
*/
|
*/
|
||||||
using simple_poll_loop_fn = int (*)(SimplePoll *);
|
using simple_poll_loop_fn = int (*)(SimplePoll *);
|
||||||
/**
|
|
||||||
* @brief Function pointer used to stop the Avahi simple-poll loop.
|
|
||||||
*/
|
|
||||||
using simple_poll_quit_fn = void (*)(SimplePoll *);
|
|
||||||
/**
|
/**
|
||||||
* @brief Function pointer used to allocate an Avahi simple-poll instance.
|
* @brief Function pointer used to allocate an Avahi simple-poll instance.
|
||||||
*/
|
*/
|
||||||
using simple_poll_new_fn = SimplePoll *(*)();
|
using simple_poll_new_fn = SimplePoll *(*)();
|
||||||
/**
|
|
||||||
* @brief Function pointer used to free an Avahi simple-poll instance.
|
client_new_fn client_new; ///< Client new.
|
||||||
*/
|
entry_group_new_fn entry_group_new; ///< Entry group new.
|
||||||
using simple_poll_free_fn = void (*)(SimplePoll *);
|
strdup_fn strdup; ///< Strdup.
|
||||||
|
simple_poll_loop_fn simple_poll_loop; ///< Simple poll loop.
|
||||||
|
simple_poll_new_fn simple_poll_new; ///< Simple poll new.
|
||||||
|
|
||||||
free_fn free; ///< Free.
|
free_fn free; ///< Free.
|
||||||
client_new_fn client_new; ///< Client new.
|
|
||||||
client_free_fn client_free; ///< Client free.
|
client_free_fn client_free; ///< Client free.
|
||||||
|
simple_poll_free_fn simple_poll_free; ///< Simple poll free.
|
||||||
alternative_service_name_fn alternative_service_name; ///< Alternative service name.
|
alternative_service_name_fn alternative_service_name; ///< Alternative service name.
|
||||||
entry_group_get_client_fn entry_group_get_client; ///< Entry group get client.
|
entry_group_get_client_fn entry_group_get_client; ///< Entry group get client.
|
||||||
entry_group_new_fn entry_group_new; ///< Entry group new.
|
|
||||||
entry_group_add_service_fn entry_group_add_service; ///< Entry group add service.
|
entry_group_add_service_fn entry_group_add_service; ///< Entry group add service.
|
||||||
|
entry_group_add_address_fn entry_group_add_address; ///< Entry group add address.
|
||||||
entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty.
|
entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty.
|
||||||
entry_group_reset_fn entry_group_reset; ///< Entry group reset.
|
entry_group_reset_fn entry_group_reset; ///< Entry group reset.
|
||||||
entry_group_commit_fn entry_group_commit; ///< Entry group commit.
|
entry_group_commit_fn entry_group_commit; ///< Entry group commit.
|
||||||
strdup_fn strdup; ///< Strdup.
|
|
||||||
strerror_fn strerror; ///< Strerror.
|
strerror_fn strerror; ///< Strerror.
|
||||||
client_errno_fn client_errno; ///< Client errno.
|
client_errno_fn client_errno; ///< Client errno.
|
||||||
simple_poll_get_fn simple_poll_get; ///< Simple poll get.
|
|
||||||
simple_poll_loop_fn simple_poll_loop; ///< Simple poll loop.
|
|
||||||
simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit.
|
simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit.
|
||||||
simple_poll_new_fn simple_poll_new; ///< Simple poll new.
|
simple_poll_get_fn simple_poll_get; ///< Simple poll get.
|
||||||
simple_poll_free_fn simple_poll_free; ///< Simple poll free.
|
|
||||||
|
/**
|
||||||
|
* @brief Event types for Avahi poll watches.
|
||||||
|
* @details Layout-compatible with `AvahiWatchEvent` (see
|
||||||
|
* avahi-common/watch.h): the values are the `poll(2)` event flags.
|
||||||
|
* Scoped enum with an explicit `int` underlying type, so the layout
|
||||||
|
* stays identical to the C enum.
|
||||||
|
*/
|
||||||
|
enum class WatchEvent : int {
|
||||||
|
WATCH_IN = POLLIN, ///< Input event
|
||||||
|
WATCH_OUT = POLLOUT, ///< Output event
|
||||||
|
WATCH_ERR = POLLERR, ///< Error event
|
||||||
|
WATCH_HUP = POLLHUP ///< Hangup event
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Watch;
|
||||||
|
struct Timeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Avahi poll-watch callback signature.
|
||||||
|
*/
|
||||||
|
using WatchCallback = void (*)(Watch *w, int fd, WatchEvent event, void *userdata);
|
||||||
|
/**
|
||||||
|
* @brief Avahi poll-timeout callback signature.
|
||||||
|
*/
|
||||||
|
using TimeoutCallback = void (*)(Timeout *t, void *userdata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Abstracted event polling API.
|
||||||
|
* @details Layout-compatible with `AvahiPoll` (see avahi-common/watch.h):
|
||||||
|
* a user-data pointer followed by the watch and timeout operations. Only
|
||||||
|
* the watch operations are used here.
|
||||||
|
*/
|
||||||
|
struct Poll {
|
||||||
|
void *userdata; ///< User data for the poll provider.
|
||||||
|
Watch *(*watch_new)(const Poll *api, int fd, WatchEvent event, WatchCallback callback, void *userdata); ///< Create a watch.
|
||||||
|
void (*watch_update)(Watch *w, WatchEvent event); ///< Update a watch's events.
|
||||||
|
WatchEvent (*watch_get_events)(Watch *w); ///< Events that happened on a watch.
|
||||||
|
void (*watch_free)(Watch *w); ///< Free a watch.
|
||||||
|
Timeout *(*timeout_new)(const Poll *api, const timeval *tv, TimeoutCallback callback, void *userdata); ///< Create a timeout.
|
||||||
|
void (*timeout_update)(Timeout *t, const timeval *tv); ///< Update a timeout.
|
||||||
|
void (*timeout_free)(Timeout *t); ///< Free a timeout.
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Load Avahi common-library memory, naming, error, and simple-poll functions.
|
* @brief Load Avahi common-library memory, naming, error, and simple-poll functions.
|
||||||
@@ -355,6 +315,7 @@ namespace avahi {
|
|||||||
{(dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client"},
|
{(dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client"},
|
||||||
{(dyn::apiproc *) &entry_group_new, "avahi_entry_group_new"},
|
{(dyn::apiproc *) &entry_group_new, "avahi_entry_group_new"},
|
||||||
{(dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service"},
|
{(dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service"},
|
||||||
|
{(dyn::apiproc *) &entry_group_add_address, "avahi_entry_group_add_address"},
|
||||||
{(dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty"},
|
{(dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty"},
|
||||||
{(dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset"},
|
{(dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset"},
|
||||||
{(dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit"},
|
{(dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit"},
|
||||||
@@ -371,30 +332,6 @@ namespace avahi {
|
|||||||
|
|
||||||
namespace platf::publish {
|
namespace platf::publish {
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Release memory allocated by Avahi.
|
|
||||||
*
|
|
||||||
* @param p Avahi-allocated pointer to release.
|
|
||||||
*/
|
|
||||||
template<class T>
|
|
||||||
void free(T *p) {
|
|
||||||
avahi::free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Owning pointer for Avahi allocations released with `avahi_free`.
|
|
||||||
*/
|
|
||||||
template<class T>
|
|
||||||
using ptr_t = util::safe_ptr<T, free<T>>;
|
|
||||||
/**
|
|
||||||
* @brief Owning pointer for an Avahi client.
|
|
||||||
*/
|
|
||||||
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
|
|
||||||
/**
|
|
||||||
* @brief Owning pointer for an Avahi simple poll loop.
|
|
||||||
*/
|
|
||||||
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
|
|
||||||
|
|
||||||
avahi::EntryGroup *group = nullptr; ///< Active Avahi entry group that owns the published service.
|
avahi::EntryGroup *group = nullptr; ///< Active Avahi entry group that owns the published service.
|
||||||
|
|
||||||
poll_t poll; ///< Avahi poll loop used while the service is published.
|
poll_t poll; ///< Avahi poll loop used while the service is published.
|
||||||
@@ -402,12 +339,121 @@ namespace platf::publish {
|
|||||||
|
|
||||||
ptr_t<char> name; ///< Current service name, updated when Avahi reports a collision.
|
ptr_t<char> name; ///< Current service name, updated when Avahi reports a collision.
|
||||||
|
|
||||||
/**
|
unsigned host_name_collision_count = 0; ///< Suffix counter for regenerating the service host name after collisions.
|
||||||
* @brief Create or update the Avahi service entry group.
|
|
||||||
*
|
std::optional<std::vector<ServedAddress>> published_addresses; ///< Committed snapshot; nullopt while withdrawn.
|
||||||
* @param c Avahi client used to allocate and commit the service entry group.
|
|
||||||
*/
|
#ifdef __linux__
|
||||||
void create_services(avahi::Client *c);
|
int netlink_fd = -1; ///< Netlink socket watched for interface/address changes, -1 when not installed.
|
||||||
|
avahi::Watch *netlink_watch = nullptr; ///< Avahi poll watch for the netlink socket, nullptr when not installed.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Enumerate the local interface addresses available for publication. */
|
||||||
|
std::vector<ServedAddress> enumerate_interface_addresses() {
|
||||||
|
ifaddrs *interfaces = nullptr;
|
||||||
|
if (getifaddrs(&interfaces) != 0) {
|
||||||
|
BOOST_LOG(error) << "Failed to enumerate network interfaces."sv;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto cleanup = util::fail_guard([&interfaces]() {
|
||||||
|
freeifaddrs(interfaces);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<ServedAddress> addresses;
|
||||||
|
for (const auto *ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) {
|
||||||
|
if (ifa->ifa_addr == nullptr || (ifa->ifa_flags & IFF_UP) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The owning interface index scopes the record to the link it is
|
||||||
|
// reachable on; if_nametoindex() only fails for names that vanished
|
||||||
|
// mid-enumeration, and those records fall back to IF_UNSPEC.
|
||||||
|
avahi::IfIndex interface = avahi::IF_UNSPEC;
|
||||||
|
if (const auto index = if_nametoindex(ifa->ifa_name); index != 0) {
|
||||||
|
interface = static_cast<avahi::IfIndex>(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifa->ifa_addr->sa_family == AF_INET) {
|
||||||
|
const auto *inet_address = reinterpret_cast<const sockaddr_in *>(ifa->ifa_addr);
|
||||||
|
boost::asio::ip::address_v4::bytes_type bytes {};
|
||||||
|
std::memcpy(bytes.data(), &inet_address->sin_addr, bytes.size());
|
||||||
|
addresses.emplace_back(interface, boost::asio::ip::address_v4(bytes));
|
||||||
|
} else if (ifa->ifa_addr->sa_family == AF_INET6) {
|
||||||
|
const auto *inet6_address = reinterpret_cast<const sockaddr_in6 *>(ifa->ifa_addr);
|
||||||
|
boost::asio::ip::address_v6::bytes_type bytes {};
|
||||||
|
std::memcpy(bytes.data(), &inet6_address->sin6_addr, bytes.size());
|
||||||
|
addresses.emplace_back(interface, boost::asio::ip::address_v6(bytes, inet6_address->sin6_scope_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Determine the addresses published for the service host name. */
|
||||||
|
std::vector<ServedAddress> served_addresses(net::af_e effective_family) {
|
||||||
|
// An explicit, non-wildcard bind address is, by definition, an address
|
||||||
|
// Sunshine serves: publish exactly it, with the interface index of the
|
||||||
|
// interface that owns it when one does.
|
||||||
|
if (config::sunshine.bind_address.empty()) {
|
||||||
|
// A wildcard bind serves every local interface address of the effective family.
|
||||||
|
return select_served_addresses(enumerate_interface_addresses(), effective_family);
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::system::error_code ec;
|
||||||
|
auto bind_address = boost::asio::ip::make_address(config::sunshine.bind_address, ec);
|
||||||
|
if (ec) {
|
||||||
|
BOOST_LOG(warning) << "Configured bind address is not a valid IP address, falling back to interface enumeration: "sv
|
||||||
|
<< config::sunshine.bind_address;
|
||||||
|
// An unparsable bind serves every local interface address of the effective family.
|
||||||
|
return select_served_addresses(enumerate_interface_addresses(), effective_family);
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_address = net::normalize_address(bind_address);
|
||||||
|
if (bind_address.is_unspecified()) {
|
||||||
|
// A wildcard bind serves every local interface address of the effective family.
|
||||||
|
return select_served_addresses(enumerate_interface_addresses(), effective_family);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effective_family == net::IPV4 && !bind_address.is_v4()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto enumerated = enumerate_interface_addresses();
|
||||||
|
const auto owner = std::find_if(
|
||||||
|
enumerated.begin(),
|
||||||
|
enumerated.end(),
|
||||||
|
[&bind_address](const ServedAddress &entry) {
|
||||||
|
return entry.address == bind_address;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const auto interface = owner != enumerated.end() ? owner->interface : avahi::IF_UNSPEC;
|
||||||
|
return {ServedAddress {interface, bind_address}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add address records for the service host name to the entry group. */
|
||||||
|
int add_service_addresses(
|
||||||
|
avahi::EntryGroup *entry_group,
|
||||||
|
const std::string &service_host,
|
||||||
|
const std::vector<ServedAddress> &addresses
|
||||||
|
) {
|
||||||
|
using enum avahi::proto;
|
||||||
|
|
||||||
|
for (const auto &address : addresses) {
|
||||||
|
const auto avahi_address = to_avahi_address(address.address);
|
||||||
|
const int ret = avahi::entry_group_add_address(
|
||||||
|
entry_group,
|
||||||
|
address.interface,
|
||||||
|
std::to_underlying(PROTO_UNSPEC),
|
||||||
|
avahi::PUBLISH_NO_REVERSE,
|
||||||
|
service_host.c_str(),
|
||||||
|
&avahi_address
|
||||||
|
);
|
||||||
|
if (ret < 0) {
|
||||||
|
BOOST_LOG(error) << "Failed to add avahi address for "sv << service_host << ": "sv << avahi::strerror(ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief React to Avahi entry-group state changes.
|
* @brief React to Avahi entry-group state changes.
|
||||||
@@ -427,6 +473,11 @@ namespace platf::publish {
|
|||||||
|
|
||||||
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
|
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
|
||||||
|
|
||||||
|
// The collision may have been on the service host name rather than
|
||||||
|
// the service name: regenerate the host name as well so the retry
|
||||||
|
// does not collide on the same host name again.
|
||||||
|
++host_name_collision_count;
|
||||||
|
|
||||||
create_services(avahi::entry_group_get_client(g));
|
create_services(avahi::entry_group_get_client(g));
|
||||||
break;
|
break;
|
||||||
case avahi::ENTRY_GROUP_FAILURE:
|
case avahi::ENTRY_GROUP_FAILURE:
|
||||||
@@ -438,14 +489,16 @@ namespace platf::publish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* Publish Sunshine's mDNS service through Avahi. */
|
||||||
* @brief Publish Sunshine's mDNS service through Avahi.
|
void create_services(avahi::Client *c, bool quit_on_failure) {
|
||||||
*/
|
using enum avahi::proto;
|
||||||
void create_services(avahi::Client *c) {
|
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
auto fg = util::fail_guard([]() {
|
auto fg = util::fail_guard([quit_on_failure]() {
|
||||||
avahi::simple_poll_quit(poll.get());
|
if (quit_on_failure) {
|
||||||
|
avahi::simple_poll_quit(poll.get());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
@@ -456,17 +509,58 @@ namespace platf::publish {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (avahi::entry_group_is_empty(group)) {
|
if (avahi::entry_group_is_empty(group)) {
|
||||||
|
// A fresh registration attempt starts from a withdrawn snapshot: the
|
||||||
|
// group is empty, so no previously committed snapshot is still live.
|
||||||
|
published_addresses.reset();
|
||||||
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
|
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
|
||||||
|
|
||||||
|
// Discovery transport stays on both protocols: the transport only
|
||||||
|
// decides which multicast groups carry the announcement, it does not
|
||||||
|
// filter the advertised host addresses (see #5860). The filter is the
|
||||||
|
// service-specific SRV target below: the daemon publishes address
|
||||||
|
// records for that host name only from the explicit records added
|
||||||
|
// here, so an IPv4-only configuration advertises no AAAA record even
|
||||||
|
// though the daemon's shared host name still carries both A and AAAA
|
||||||
|
// (publish-aaaa-on-ipv4). The effective family accounts for
|
||||||
|
// bind_address, matching the listener setup. On non-Linux platforms
|
||||||
|
// there is no address-change watch, so the service instead uses the
|
||||||
|
// daemon-managed host name (a null SRV target) and the daemon
|
||||||
|
// maintains the address records.
|
||||||
|
const auto effective_family = net::get_effective_address_family(
|
||||||
|
net::af_from_enum_string(config::sunshine.address_family)
|
||||||
|
);
|
||||||
|
const auto addresses = served_addresses(effective_family);
|
||||||
|
|
||||||
|
// The service-specific host name and its fixed address records are
|
||||||
|
// Linux-only: only Linux installs the address-change watch that
|
||||||
|
// refreshes them (see install_address_change_watch()).
|
||||||
|
const char *service_target = nullptr;
|
||||||
|
#ifdef __linux__
|
||||||
|
const auto service_host = service_host_name(platf::get_host_name(), host_name_collision_count);
|
||||||
|
if (addresses.empty()) {
|
||||||
|
BOOST_LOG(warning) << "No served addresses determined for "sv << service_host
|
||||||
|
<< "; falling back to the daemon host name, which may advertise unserved addresses (see #5860).";
|
||||||
|
} else {
|
||||||
|
service_target = service_host.c_str();
|
||||||
|
ret = add_service_addresses(group, service_host, addresses);
|
||||||
|
if (ret < 0) {
|
||||||
|
published_addresses.reset();
|
||||||
|
avahi::entry_group_reset(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
ret = avahi::entry_group_add_service(
|
ret = avahi::entry_group_add_service(
|
||||||
group,
|
group,
|
||||||
avahi::IF_UNSPEC,
|
avahi::IF_UNSPEC,
|
||||||
avahi::PROTO_UNSPEC,
|
std::to_underlying(PROTO_UNSPEC),
|
||||||
avahi::PublishFlags(0),
|
avahi::PublishFlags(0),
|
||||||
name.get(),
|
name.get(),
|
||||||
platf::SERVICE_TYPE,
|
platf::SERVICE_TYPE,
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
service_target,
|
||||||
net::map_port(nvhttp::PORT_HTTP),
|
net::map_port(nvhttp::PORT_HTTP),
|
||||||
nullptr
|
nullptr
|
||||||
);
|
);
|
||||||
@@ -477,15 +571,22 @@ namespace platf::publish {
|
|||||||
name.reset(avahi::alternative_service_name(name.get()));
|
name.reset(avahi::alternative_service_name(name.get()));
|
||||||
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
|
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
|
||||||
|
|
||||||
|
// The collision may have been on the service host name instead:
|
||||||
|
// regenerate it as well so the retry does not collide again.
|
||||||
|
++host_name_collision_count;
|
||||||
|
|
||||||
|
published_addresses.reset();
|
||||||
avahi::entry_group_reset(group);
|
avahi::entry_group_reset(group);
|
||||||
|
|
||||||
create_services(c);
|
create_services(c, quit_on_failure);
|
||||||
|
|
||||||
fg.disable();
|
fg.disable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_LOG(error) << "Failed to add "sv << platf::SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
|
BOOST_LOG(error) << "Failed to add "sv << platf::SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
|
||||||
|
published_addresses.reset();
|
||||||
|
avahi::entry_group_reset(group);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,11 +595,112 @@ namespace platf::publish {
|
|||||||
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
|
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
published_addresses = addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
fg.disable();
|
fg.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rebuild the service records after local interface addresses change. */
|
||||||
|
void refresh_service_addresses() {
|
||||||
|
if (!client || !group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto effective_family = net::get_effective_address_family(
|
||||||
|
net::af_from_enum_string(config::sunshine.address_family)
|
||||||
|
);
|
||||||
|
const auto current = served_addresses(effective_family);
|
||||||
|
if (published_addresses && current == *published_addresses) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_LOG(info) << "Local interface addresses changed, republishing Avahi service."sv;
|
||||||
|
published_addresses.reset();
|
||||||
|
avahi::entry_group_reset(group);
|
||||||
|
// A refresh failure must not kill discovery: the group is already reset
|
||||||
|
// (records withdrawn), and the next change notification retries, while
|
||||||
|
// quitting the poll loop here would leave the service withdrawn with no
|
||||||
|
// recovery short of restarting Sunshine.
|
||||||
|
create_services(client.get(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/**
|
||||||
|
* @brief Refresh service publication after an interface/address change.
|
||||||
|
*
|
||||||
|
* @param fd Netlink route socket to drain before refreshing the addresses.
|
||||||
|
*/
|
||||||
|
void address_change_callback(avahi::Watch * /*watch*/, int fd, avahi::WatchEvent /*event*/, void * /*userdata*/) {
|
||||||
|
// Drain the netlink socket: a burst of address/link messages must not
|
||||||
|
// leave the fd readable, or the poll loop would spin on it.
|
||||||
|
std::array<std::byte, 4096> buffer {};
|
||||||
|
while (recv(fd, buffer.data(), buffer.size(), MSG_DONTWAIT) > 0) {
|
||||||
|
// Discard: the snapshot is re-read from the interfaces, not from the message.
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_service_addresses();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Subscribe to interface/address changes on the Avahi poll thread. */
|
||||||
|
bool install_address_change_watch() {
|
||||||
|
#ifdef __linux__
|
||||||
|
if (netlink_watch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No existing network-change notification exists in Sunshine to reuse:
|
||||||
|
// the only related code is UPnP's WAN-IP renewal, which is unrelated to
|
||||||
|
// mDNS. A netlink route socket is the kernel's address/link change
|
||||||
|
// feed; registering it as an Avahi poll watch dispatches the callback
|
||||||
|
// on the existing poll thread, so no new thread and no cross-thread
|
||||||
|
// Avahi calls are introduced. Failure degrades to the old behavior
|
||||||
|
// (no refresh) instead of failing startup.
|
||||||
|
const int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE);
|
||||||
|
if (fd < 0) {
|
||||||
|
BOOST_LOG(warning) << "Failed to open netlink socket for address-change notifications, mDNS records will not refresh on network changes."sv;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sockaddr_nl address {};
|
||||||
|
address.nl_family = AF_NETLINK;
|
||||||
|
address.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;
|
||||||
|
if (bind(fd, (sockaddr *) &address, sizeof(address)) < 0) {
|
||||||
|
BOOST_LOG(warning) << "Failed to bind netlink socket for address-change notifications, mDNS records will not refresh on network changes."sv;
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto *poll_api = avahi::simple_poll_get(poll.get());
|
||||||
|
if (!poll_api) {
|
||||||
|
BOOST_LOG(warning) << "Failed to get Avahi poll API for address-change notifications, mDNS records will not refresh on network changes."sv;
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *watch = poll_api->watch_new(poll_api, fd, avahi::WatchEvent::WATCH_IN, address_change_callback, nullptr);
|
||||||
|
if (!watch) {
|
||||||
|
BOOST_LOG(warning) << "Failed to install Avahi watch for address-change notifications, mDNS records will not refresh on network changes."sv;
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
netlink_fd = fd;
|
||||||
|
netlink_watch = watch;
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
// Netlink is a Linux-kernel API, so no address-change watch exists on
|
||||||
|
// other platforms. That is safe because create_services() publishes
|
||||||
|
// the service with a null SRV target there, selecting the Avahi
|
||||||
|
// daemon-managed host name: the daemon owns the interface address
|
||||||
|
// records and updates and withdraws them when addresses change, so
|
||||||
|
// discovery stays correct without a refresh.
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief React to Avahi client state changes and register services when ready.
|
* @brief React to Avahi client state changes and register services when ready.
|
||||||
*
|
*
|
||||||
@@ -517,6 +719,9 @@ namespace platf::publish {
|
|||||||
case avahi::CLIENT_S_COLLISION:
|
case avahi::CLIENT_S_COLLISION:
|
||||||
case avahi::CLIENT_S_REGISTERING:
|
case avahi::CLIENT_S_REGISTERING:
|
||||||
if (group) {
|
if (group) {
|
||||||
|
// The daemon withdraws the entries on collision/re-registration,
|
||||||
|
// so the committed snapshot is no longer live.
|
||||||
|
published_addresses.reset();
|
||||||
avahi::entry_group_reset(group);
|
avahi::entry_group_reset(group);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -552,6 +757,21 @@ namespace platf::publish {
|
|||||||
poll_thread.join();
|
poll_thread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The poll thread has exited, so no watch callback can be in flight:
|
||||||
|
// release the address-change watch and its socket now.
|
||||||
|
#ifdef __linux__
|
||||||
|
if (netlink_watch) {
|
||||||
|
if (const auto *poll_api = avahi::simple_poll_get(poll.get()); poll_api) {
|
||||||
|
poll_api->watch_free(netlink_watch);
|
||||||
|
}
|
||||||
|
netlink_watch = nullptr;
|
||||||
|
}
|
||||||
|
if (netlink_fd >= 0) {
|
||||||
|
close(netlink_fd);
|
||||||
|
netlink_fd = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
poll.reset();
|
poll.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -583,6 +803,12 @@ namespace platf::publish {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Best-effort: without the watch, discovery still works. On Linux the
|
||||||
|
// records go stale after a network change (the pre-watch behavior);
|
||||||
|
// on other platforms the daemon-managed host name delegates address
|
||||||
|
// maintenance to the Avahi daemon, so no refresh is needed.
|
||||||
|
install_address_change_watch();
|
||||||
|
|
||||||
return std::make_unique<deinit_t>(std::jthread {[]() {
|
return std::make_unique<deinit_t>(std::jthread {[]() {
|
||||||
avahi::simple_poll_loop(poll.get());
|
avahi::simple_poll_loop(poll.get());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
/**
|
||||||
|
* @file src/platform/linux/publish.h
|
||||||
|
* @brief Declarations for publishing services on Linux (Avahi).
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// standard includes
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <format>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// local includes
|
||||||
|
#include "src/network.h"
|
||||||
|
|
||||||
|
namespace avahi {
|
||||||
|
/**
|
||||||
|
* @brief Enumerates supported proto options.
|
||||||
|
*/
|
||||||
|
enum class proto : int {
|
||||||
|
PROTO_INET = 0, ///< IPv4
|
||||||
|
PROTO_INET6 = 1, ///< IPv6
|
||||||
|
PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Avahi protocol/address-family selector type.
|
||||||
|
*/
|
||||||
|
using Protocol = int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Address record published for a host name.
|
||||||
|
* @details Layout-compatible with Avahi's `AvahiAddress` (see
|
||||||
|
* avahi-common/address.h): a protocol selector followed by 16 bytes of
|
||||||
|
* address data. An IPv4 address occupies the first 4 bytes in network byte
|
||||||
|
* order and the remaining bytes are zero; an IPv6 address occupies all 16
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
struct Address {
|
||||||
|
Protocol proto; ///< Address family of the record.
|
||||||
|
std::array<std::uint8_t, 16> data; ///< Address bytes, see details above.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compare two address records for equality.
|
||||||
|
* @param other Address record to compare against.
|
||||||
|
* @return True when the protocol selector and address bytes both match.
|
||||||
|
*/
|
||||||
|
bool operator==(const Address &other) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(Address) == sizeof(Protocol) + 16, "avahi::Address must match the AvahiAddress layout");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Network interface index type used by Avahi.
|
||||||
|
*/
|
||||||
|
using IfIndex = int;
|
||||||
|
|
||||||
|
constexpr auto IF_UNSPEC = -1; ///< Protocol or platform constant for if unspec.
|
||||||
|
|
||||||
|
struct EntryGroup;
|
||||||
|
struct Client;
|
||||||
|
struct Poll;
|
||||||
|
struct SimplePoll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to free Avahi-allocated memory.
|
||||||
|
*/
|
||||||
|
using free_fn = void (*)(void *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to destroy an Avahi client.
|
||||||
|
*/
|
||||||
|
using client_free_fn = void (*)(Client *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to free an Avahi simple-poll instance.
|
||||||
|
*/
|
||||||
|
using simple_poll_free_fn = void (*)(SimplePoll *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to request an alternative Avahi service name.
|
||||||
|
*/
|
||||||
|
using alternative_service_name_fn = char *(*)(char *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to get an Avahi client from an entry group.
|
||||||
|
*/
|
||||||
|
using entry_group_get_client_fn = Client *(*)(EntryGroup *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to add a service to an Avahi entry group.
|
||||||
|
*/
|
||||||
|
using entry_group_add_service_fn = int (*)(
|
||||||
|
EntryGroup *group,
|
||||||
|
IfIndex interface,
|
||||||
|
Protocol protocol,
|
||||||
|
PublishFlags flags,
|
||||||
|
const char *name,
|
||||||
|
const char *type,
|
||||||
|
const char *domain,
|
||||||
|
const char *host,
|
||||||
|
uint16_t port,
|
||||||
|
...
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to add an address record to an Avahi entry group.
|
||||||
|
*/
|
||||||
|
using entry_group_add_address_fn = int (*)(
|
||||||
|
EntryGroup *group,
|
||||||
|
IfIndex interface,
|
||||||
|
Protocol protocol,
|
||||||
|
PublishFlags flags,
|
||||||
|
const char *name,
|
||||||
|
const Address *address
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to test whether an Avahi entry group is empty.
|
||||||
|
*/
|
||||||
|
using entry_group_is_empty_fn = int (*)(EntryGroup *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to reset an Avahi entry group.
|
||||||
|
*/
|
||||||
|
using entry_group_reset_fn = int (*)(EntryGroup *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to publish an Avahi entry group.
|
||||||
|
*/
|
||||||
|
using entry_group_commit_fn = int (*)(EntryGroup *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to format Avahi error codes.
|
||||||
|
*/
|
||||||
|
using strerror_fn = char *(*)(int);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to read the last Avahi client error.
|
||||||
|
*/
|
||||||
|
using client_errno_fn = int (*)(Client *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to stop the Avahi simple-poll loop.
|
||||||
|
*/
|
||||||
|
using simple_poll_quit_fn = void (*)(SimplePoll *);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Function pointer used to get the Avahi simple-poll API.
|
||||||
|
*/
|
||||||
|
using simple_poll_get_fn = Poll *(*)(SimplePoll *);
|
||||||
|
|
||||||
|
extern free_fn free; ///< Free.
|
||||||
|
extern client_free_fn client_free; ///< Client free.
|
||||||
|
extern simple_poll_free_fn simple_poll_free; ///< Simple poll free.
|
||||||
|
extern alternative_service_name_fn alternative_service_name; ///< Alternative service name.
|
||||||
|
extern entry_group_get_client_fn entry_group_get_client; ///< Entry group get client.
|
||||||
|
extern entry_group_add_service_fn entry_group_add_service; ///< Entry group add service.
|
||||||
|
extern entry_group_add_address_fn entry_group_add_address; ///< Entry group add address.
|
||||||
|
extern entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty.
|
||||||
|
extern entry_group_reset_fn entry_group_reset; ///< Entry group reset.
|
||||||
|
extern entry_group_commit_fn entry_group_commit; ///< Entry group commit.
|
||||||
|
extern strerror_fn strerror; ///< Strerror.
|
||||||
|
extern client_errno_fn client_errno; ///< Client errno.
|
||||||
|
extern simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit.
|
||||||
|
extern simple_poll_get_fn simple_poll_get; ///< Simple poll get.
|
||||||
|
} // namespace avahi
|
||||||
|
|
||||||
|
namespace platf::publish {
|
||||||
|
/**
|
||||||
|
* @brief Local address and the interface on which it is reachable.
|
||||||
|
*/
|
||||||
|
struct ServedAddress {
|
||||||
|
avahi::IfIndex interface; ///< Owning interface index.
|
||||||
|
boost::asio::ip::address address; ///< Local address on that interface.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compare two served addresses for equality.
|
||||||
|
* @param other Served address to compare against.
|
||||||
|
* @return True when the interface index and address both match.
|
||||||
|
*/
|
||||||
|
bool operator==(const ServedAddress &other) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release memory allocated by Avahi.
|
||||||
|
*
|
||||||
|
* @param p Avahi-allocated pointer to release.
|
||||||
|
*/
|
||||||
|
template<class T>
|
||||||
|
void free(T *p) {
|
||||||
|
avahi::free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Owning pointer for Avahi allocations released with `avahi_free`.
|
||||||
|
*/
|
||||||
|
template<class T>
|
||||||
|
using ptr_t = util::safe_ptr<T, free<T>>;
|
||||||
|
/**
|
||||||
|
* @brief Owning pointer for an Avahi client.
|
||||||
|
*/
|
||||||
|
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
|
||||||
|
/**
|
||||||
|
* @brief Owning pointer for an Avahi simple poll loop.
|
||||||
|
*/
|
||||||
|
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Build the service-specific host name used as the mDNS SRV target.
|
||||||
|
* @details The name must differ from the daemon's shared host name: the
|
||||||
|
* daemon publishes both A and AAAA records for its own host name, so
|
||||||
|
* pointing the service at it would advertise an IPv6 endpoint an IPv4-only
|
||||||
|
* configuration does not serve (see #5860). The label is sanitized from
|
||||||
|
* the machine host name (alphanumerics and dashes; spaces become dashes;
|
||||||
|
* anything else ends the label), suffixed with `-sunshine` (plus
|
||||||
|
* `-<collision_suffix>` after a host-name collision), and qualified with
|
||||||
|
* `.local`.
|
||||||
|
* @param hostname Machine host name the label is derived from.
|
||||||
|
* @param collision_suffix Suffix appended after a host-name collision; 0 for the first attempt.
|
||||||
|
* @return Fully qualified service host name, e.g. `myhost-sunshine.local`.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline std::string service_host_name(std::string_view hostname, unsigned collision_suffix) {
|
||||||
|
std::string label;
|
||||||
|
for (const char c : hostname) {
|
||||||
|
if (c == ' ') {
|
||||||
|
label.push_back('-');
|
||||||
|
} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
|
||||||
|
label.push_back(c);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (label.empty()) {
|
||||||
|
label = "sunshine";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string suffix = "-sunshine";
|
||||||
|
if (collision_suffix > 0) {
|
||||||
|
suffix += std::format("-{}", collision_suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A DNS label holds at most 63 characters.
|
||||||
|
if (constexpr std::size_t MAX_LABEL = 63; label.size() + suffix.size() > MAX_LABEL) {
|
||||||
|
label.resize(MAX_LABEL - suffix.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return label + suffix + ".local";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select the addresses published for the service host name.
|
||||||
|
* @details Keeps only addresses Sunshine serves: the candidate is
|
||||||
|
* normalized, dropped when it is unspecified or loopback, dropped when its
|
||||||
|
* family falls outside the effective family, and deduplicated by interface
|
||||||
|
* plus address, so the same address on two interfaces is published once
|
||||||
|
* per interface it is reachable on. In particular, an IPv4-only
|
||||||
|
* configuration selects no IPv6 address, so no AAAA endpoint is published
|
||||||
|
* for the service host name (see #5860).
|
||||||
|
* @param candidates Candidate addresses, e.g. from interface enumeration.
|
||||||
|
* @param effective_family Address family Sunshine actually serves.
|
||||||
|
* @return Addresses to publish, containing no address outside the effective family.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline std::vector<ServedAddress> select_served_addresses(
|
||||||
|
const std::vector<ServedAddress> &candidates,
|
||||||
|
net::af_e effective_family
|
||||||
|
) {
|
||||||
|
std::vector<ServedAddress> selected;
|
||||||
|
for (auto candidate : candidates) {
|
||||||
|
candidate.address = net::normalize_address(candidate.address);
|
||||||
|
if (candidate.address.is_unspecified() || candidate.address.is_loopback()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (effective_family == net::IPV4 && !candidate.address.is_v4()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::find(selected.begin(), selected.end(), candidate) == selected.end()) {
|
||||||
|
selected.push_back(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert a generic address to the Avahi address-record layout.
|
||||||
|
* @details The address is normalized first, so IPv4-mapped IPv6 addresses
|
||||||
|
* become plain IPv4 addresses.
|
||||||
|
* @param address Address to convert.
|
||||||
|
* @return Avahi-compatible address record for `address`.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline avahi::Address to_avahi_address(boost::asio::ip::address address) {
|
||||||
|
address = net::normalize_address(address);
|
||||||
|
|
||||||
|
avahi::Address result {};
|
||||||
|
if (address.is_v4()) {
|
||||||
|
result.proto = std::to_underlying(avahi::proto::PROTO_INET);
|
||||||
|
const auto bytes = address.to_v4().to_bytes();
|
||||||
|
std::ranges::copy(bytes, result.data.begin());
|
||||||
|
} else {
|
||||||
|
result.proto = std::to_underlying(avahi::proto::PROTO_INET6);
|
||||||
|
result.data = address.to_v6().to_bytes();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumerate the local interface addresses available for publication.
|
||||||
|
* @return Interface addresses with their owning interface index, empty when enumeration fails.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::vector<ServedAddress> enumerate_interface_addresses();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Determine the addresses published for the service host name.
|
||||||
|
* @details An explicit, non-wildcard bind address is published exactly as
|
||||||
|
* configured, with the interface index of the interface that owns it
|
||||||
|
* (`IF_UNSPEC` when no local interface owns it). A wildcard (or
|
||||||
|
* unparsable) bind address publishes the selected local interface
|
||||||
|
* addresses.
|
||||||
|
* @param effective_family Address family Sunshine actually serves.
|
||||||
|
* @return Addresses to publish; empty when none can be determined.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::vector<ServedAddress> served_addresses(net::af_e effective_family);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add address records for the service host name to the entry group.
|
||||||
|
*
|
||||||
|
* @param entry_group Entry group the address records are added to.
|
||||||
|
* @param service_host Service host name the address records are published for.
|
||||||
|
* @param addresses Addresses to publish for the service host name.
|
||||||
|
* @return 0 when all address records were added; otherwise the Avahi error code from the failed call.
|
||||||
|
*/
|
||||||
|
int add_service_addresses(
|
||||||
|
avahi::EntryGroup *entry_group,
|
||||||
|
const std::string &service_host,
|
||||||
|
const std::vector<ServedAddress> &addresses
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create or update the Avahi service entry group.
|
||||||
|
*
|
||||||
|
* @param c Avahi client used to allocate and commit the service entry group.
|
||||||
|
* @param quit_on_failure Quit the Avahi poll loop when registration fails. Initial
|
||||||
|
* registration keeps the existing fail-fast behavior; a refresh triggered by a
|
||||||
|
* network change passes false so a transient failure does not kill discovery.
|
||||||
|
*/
|
||||||
|
void create_services(avahi::Client *c, bool quit_on_failure = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Rebuild the service records after local interface addresses change.
|
||||||
|
* @details Compares the current served addresses against the last published
|
||||||
|
* snapshot and, when they differ, resets the entry group (withdrawing the
|
||||||
|
* stale records) and republishes. A no-op when the client or group is not
|
||||||
|
* yet set up, or when the snapshot is unchanged.
|
||||||
|
*/
|
||||||
|
void refresh_service_addresses();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Subscribe to interface/address changes on the Avahi poll thread.
|
||||||
|
* @details Opens a netlink route socket and registers it as an Avahi poll
|
||||||
|
* watch, so address/link changes dispatch the refresh on the existing
|
||||||
|
* poll thread. Best-effort: returns false when the watch cannot be
|
||||||
|
* installed, and discovery continues without refresh. Linux-only:
|
||||||
|
* netlink is a Linux-kernel API, so on other platforms this always
|
||||||
|
* returns false and no watch is installed; the service there uses the
|
||||||
|
* daemon-managed host name (a null SRV target), so the Avahi daemon
|
||||||
|
* maintains the address records and no watch is needed.
|
||||||
|
* @return True when the watch is installed (or was already installed).
|
||||||
|
*/
|
||||||
|
bool install_address_change_watch();
|
||||||
|
|
||||||
|
#ifdef SUNSHINE_TESTS
|
||||||
|
// Test seams: registration tests drive the publisher with stubbed Avahi
|
||||||
|
// calls, so the publisher state they need to set is visible here. These
|
||||||
|
// declarations exist only in the test build.
|
||||||
|
extern avahi::EntryGroup *group; ///< Active Avahi entry group that owns the published service.
|
||||||
|
extern poll_t poll; ///< Avahi poll loop used while the service is published.
|
||||||
|
extern client_t client; ///< Avahi client used to register the Sunshine service.
|
||||||
|
extern ptr_t<char> name; ///< Current service name, updated when Avahi reports a collision.
|
||||||
|
extern unsigned host_name_collision_count; ///< Suffix counter for regenerating the service host name after collisions.
|
||||||
|
extern std::optional<std::vector<ServedAddress>> published_addresses; ///< Committed snapshot; nullopt while withdrawn.
|
||||||
|
#endif
|
||||||
|
} // namespace platf::publish
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
/**
|
||||||
|
* @file tests/unit/test_publish.cpp
|
||||||
|
* @brief Tests for Linux mDNS (Avahi) service publication.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The publisher (src/platform/linux/publish.cpp) is built on every
|
||||||
|
// non-Apple Unix platform (see cmake/compile_definitions/linux.cmake),
|
||||||
|
// so the stubbed-registration tests below compile wherever it is built.
|
||||||
|
// The plain unit tests stay Linux-gated; only the fallback regression
|
||||||
|
// runs on non-Linux, where the address-change watch is unsupported.
|
||||||
|
#if !defined(_WIN32) && !defined(__APPLE__)
|
||||||
|
|
||||||
|
// test includes
|
||||||
|
#include "../tests_common.h"
|
||||||
|
|
||||||
|
// standard includes
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// local includes
|
||||||
|
#include <src/platform/linux/publish.h>
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
// Linux-only includes for the interface-enumeration tests.
|
||||||
|
#include <ifaddrs.h>
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
TEST(ServiceHostNameTest, IsDistinctFromDaemonHostName) {
|
||||||
|
// The SRV target must not be the daemon's shared host name: the daemon
|
||||||
|
// publishes both A and AAAA records for its own host name, so pointing the
|
||||||
|
// service at it would advertise an IPv6 endpoint an IPv4-only
|
||||||
|
// configuration does not serve (see #5860).
|
||||||
|
ASSERT_EQ(platf::publish::service_host_name("myhost", 0), "myhost-sunshine.local");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServiceHostNameTest, SanitizesInvalidLabelCharacters) {
|
||||||
|
ASSERT_EQ(platf::publish::service_host_name("my host.example", 0), "my-host-sunshine.local");
|
||||||
|
ASSERT_EQ(platf::publish::service_host_name("", 0), "sunshine-sunshine.local");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServiceHostNameTest, CollisionSuffixChangesName) {
|
||||||
|
ASSERT_EQ(platf::publish::service_host_name("myhost", 1), "myhost-sunshine-1.local");
|
||||||
|
ASSERT_NE(platf::publish::service_host_name("myhost", 0), platf::publish::service_host_name("myhost", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServiceHostNameTest, LabelStaysWithinDnsLimit) {
|
||||||
|
const auto host_name = platf::publish::service_host_name(std::string(100, 'a'), 0);
|
||||||
|
// The label (everything before ".local") must fit in 63 characters.
|
||||||
|
ASSERT_TRUE(host_name.find('.') <= 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectServedAddressesTest, Ipv4OnlyPublishesNoIpv6) {
|
||||||
|
// Regression core for #5860: with an IPv4-only configuration the selection
|
||||||
|
// contains no IPv6 address, so no AAAA endpoint is published for the
|
||||||
|
// service host name.
|
||||||
|
const auto selected = platf::publish::select_served_addresses(
|
||||||
|
{{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{1, boost::asio::ip::make_address("fe80::1")},
|
||||||
|
{2, boost::asio::ip::make_address("2001:db8::1")}},
|
||||||
|
net::IPV4
|
||||||
|
);
|
||||||
|
ASSERT_EQ(selected.size(), 1U);
|
||||||
|
ASSERT_EQ(selected[0].interface, 1);
|
||||||
|
ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectServedAddressesTest, BothPublishesBothFamilies) {
|
||||||
|
const auto selected = platf::publish::select_served_addresses(
|
||||||
|
{{1, boost::asio::ip::make_address("192.168.1.10")}, {2, boost::asio::ip::make_address("2001:db8::1")}},
|
||||||
|
net::BOTH
|
||||||
|
);
|
||||||
|
ASSERT_EQ(selected.size(), 2U);
|
||||||
|
ASSERT_EQ(selected[0].interface, 1);
|
||||||
|
ASSERT_EQ(selected[1].interface, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectServedAddressesTest, DropsUnusableAndDuplicateAddresses) {
|
||||||
|
const auto selected = platf::publish::select_served_addresses(
|
||||||
|
{{1, boost::asio::ip::make_address("127.0.0.1")},
|
||||||
|
{1, boost::asio::ip::make_address("0.0.0.0")},
|
||||||
|
{1, boost::asio::ip::make_address("::1")},
|
||||||
|
{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{1, boost::asio::ip::make_address("::ffff:192.168.1.10")}},
|
||||||
|
net::BOTH
|
||||||
|
);
|
||||||
|
ASSERT_EQ(selected.size(), 1U);
|
||||||
|
ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectServedAddressesTest, DeduplicatesByInterfaceAndAddress) {
|
||||||
|
// The same address on two interfaces is reachable on both, so it is kept
|
||||||
|
// once per interface; the same interface-plus-address pair is kept once.
|
||||||
|
// A v4-mapped duplicate normalizes to the same IPv4 address.
|
||||||
|
const auto selected = platf::publish::select_served_addresses(
|
||||||
|
{{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{2, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{1, boost::asio::ip::make_address("::ffff:192.168.1.10")}},
|
||||||
|
net::BOTH
|
||||||
|
);
|
||||||
|
ASSERT_EQ(selected.size(), 2U);
|
||||||
|
ASSERT_EQ(selected[0], (platf::publish::ServedAddress {1, boost::asio::ip::make_address("192.168.1.10")}));
|
||||||
|
ASSERT_EQ(selected[1], (platf::publish::ServedAddress {2, boost::asio::ip::make_address("192.168.1.10")}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServedAddressesTest, ExplicitBindResolvesOwningInterface) {
|
||||||
|
// An explicit bind address is published with the interface index of the
|
||||||
|
// interface that owns it, not IF_UNSPEC.
|
||||||
|
const auto enumerated = platf::publish::enumerate_interface_addresses();
|
||||||
|
if (enumerated.empty()) {
|
||||||
|
GTEST_SKIP() << "No local interface addresses to resolve a bind address against.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &entry = enumerated[0];
|
||||||
|
const auto saved_bind = config::sunshine.bind_address;
|
||||||
|
config::sunshine.bind_address = entry.address.to_string();
|
||||||
|
|
||||||
|
const auto served = platf::publish::served_addresses(net::BOTH);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = saved_bind;
|
||||||
|
|
||||||
|
ASSERT_EQ(served.size(), 1U);
|
||||||
|
ASSERT_EQ(served[0], entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServedAddressesTest, ExplicitBindWithoutOwnerFallsBackToUnspec) {
|
||||||
|
// A bind address no local interface owns is still published exactly as
|
||||||
|
// configured, with IF_UNSPEC.
|
||||||
|
const auto saved_bind = config::sunshine.bind_address;
|
||||||
|
// TEST-NET-1 (RFC 5737) is never assigned to a real interface, so no
|
||||||
|
// local interface can own it.
|
||||||
|
config::sunshine.bind_address = "192.0.2.10";
|
||||||
|
|
||||||
|
const auto served = platf::publish::served_addresses(net::BOTH);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = saved_bind;
|
||||||
|
|
||||||
|
ASSERT_EQ(served.size(), 1U);
|
||||||
|
ASSERT_EQ(served[0].interface, avahi::IF_UNSPEC);
|
||||||
|
ASSERT_EQ(served[0].address, boost::asio::ip::make_address("192.0.2.10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServedAddressesTest, ExplicitBindScopedLinkLocalResolvesOwningInterface) {
|
||||||
|
// A scoped link-local bind must resolve to the interface that owns it:
|
||||||
|
// getifaddrs() reports the zone (sin6_scope_id), and the enumeration must
|
||||||
|
// preserve it, or the scoped bind never equals its enumerated owner and
|
||||||
|
// the address falls back to IF_UNSPEC, advertising the link-local
|
||||||
|
// endpoint on interfaces where Sunshine does not serve it.
|
||||||
|
ifaddrs *interfaces = nullptr;
|
||||||
|
if (getifaddrs(&interfaces) != 0) {
|
||||||
|
GTEST_SKIP() << "getifaddrs() failed; cannot find a scoped IPv6 address.";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string scoped_bind;
|
||||||
|
avahi::IfIndex expected_interface = avahi::IF_UNSPEC;
|
||||||
|
for (const auto *ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) {
|
||||||
|
if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto *inet6_address = reinterpret_cast<const sockaddr_in6 *>(ifa->ifa_addr);
|
||||||
|
if (inet6_address->sin6_scope_id == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boost::asio::ip::address_v6::bytes_type bytes {};
|
||||||
|
std::memcpy(bytes.data(), &inet6_address->sin6_addr, bytes.size());
|
||||||
|
const auto address = boost::asio::ip::address_v6(bytes, inet6_address->sin6_scope_id);
|
||||||
|
const auto index = if_nametoindex(ifa->ifa_name);
|
||||||
|
if (index == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scoped_bind = address.to_string();
|
||||||
|
expected_interface = static_cast<avahi::IfIndex>(index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
freeifaddrs(interfaces);
|
||||||
|
if (scoped_bind.empty()) {
|
||||||
|
GTEST_SKIP() << "No scoped IPv6 interface address to resolve a bind address against.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto saved_bind = config::sunshine.bind_address;
|
||||||
|
config::sunshine.bind_address = scoped_bind;
|
||||||
|
|
||||||
|
const auto served = platf::publish::served_addresses(net::BOTH);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = saved_bind;
|
||||||
|
|
||||||
|
ASSERT_EQ(served.size(), 1U);
|
||||||
|
ASSERT_EQ(served[0].interface, expected_interface);
|
||||||
|
ASSERT_EQ(served[0].address, boost::asio::ip::make_address(scoped_bind));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ToAvahiAddressTest, Ipv4EncodesNetworkOrderBytes) {
|
||||||
|
const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"));
|
||||||
|
ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET));
|
||||||
|
ASSERT_EQ(address.data, (std::array<std::uint8_t, 16> {192, 168, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ToAvahiAddressTest, Ipv6EncodesAllSixteenBytes) {
|
||||||
|
const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("2001:db8::1"));
|
||||||
|
ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET6));
|
||||||
|
ASSERT_EQ(
|
||||||
|
address.data,
|
||||||
|
(std::array<std::uint8_t, 16> {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // defined(__linux__)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* @brief Captured Avahi entry-group calls for registration tests.
|
||||||
|
*/
|
||||||
|
struct AddressCall {
|
||||||
|
avahi::IfIndex interface; ///< Interface index the record was published on.
|
||||||
|
avahi::PublishFlags flags; ///< Flags the record was published with.
|
||||||
|
avahi::Address address; ///< Address record published.
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<AddressCall> g_address_calls; ///< Address-publication calls captured by the stub.
|
||||||
|
std::vector<std::string> g_service_hosts; ///< SRV-target host names captured by the stub.
|
||||||
|
int g_reset_count = 0; ///< Entry-group resets captured by the stub.
|
||||||
|
int g_commit_count = 0; ///< Entry-group commits captured by the stub.
|
||||||
|
int g_quit_count = 0; ///< Poll-loop quits captured by the stub.
|
||||||
|
bool g_group_empty = true; ///< Entry-group emptiness modeled by the stub: reset empties, commit fills.
|
||||||
|
bool g_add_service_collide_once = false; ///< Next add-service call reports a collision.
|
||||||
|
bool g_add_address_fail_once = false; ///< Next add-address call reports a failure.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Avahi error code for a name collision (see `avahi::err_e`).
|
||||||
|
*/
|
||||||
|
constexpr int kErrCollision = -8;
|
||||||
|
|
||||||
|
void FakeFree(void *p) {
|
||||||
|
std::free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FakeClientFree(avahi::Client * /*client*/) {
|
||||||
|
// The test client is a fake pointer; nothing to release.
|
||||||
|
}
|
||||||
|
|
||||||
|
char *FakeAlternativeServiceName(char *name) {
|
||||||
|
return ::strdup(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
char *FakeStrerror(int /*error*/) {
|
||||||
|
return const_cast<char *>("stub Avahi error");
|
||||||
|
}
|
||||||
|
|
||||||
|
int FakeClientErrno(avahi::Client * /*client*/) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FakeSimplePollQuit(avahi::SimplePoll * /*poll*/) {
|
||||||
|
++g_quit_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FakeEntryGroupIsEmpty(avahi::EntryGroup * /*group*/) {
|
||||||
|
return g_group_empty ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FakeEntryGroupReset(avahi::EntryGroup * /*group*/) {
|
||||||
|
++g_reset_count;
|
||||||
|
g_group_empty = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FakeEntryGroupCommit(avahi::EntryGroup * /*group*/) {
|
||||||
|
++g_commit_count;
|
||||||
|
g_group_empty = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FakeEntryGroupAddService(
|
||||||
|
avahi::EntryGroup * /*group*/,
|
||||||
|
avahi::IfIndex /*interface*/,
|
||||||
|
avahi::Protocol /*protocol*/,
|
||||||
|
avahi::PublishFlags /*flags*/,
|
||||||
|
const char * /*name*/,
|
||||||
|
const char * /*type*/,
|
||||||
|
const char * /*domain*/,
|
||||||
|
const char *host,
|
||||||
|
std::uint16_t /*port*/,
|
||||||
|
...
|
||||||
|
) {
|
||||||
|
g_service_hosts.emplace_back(host ? host : "");
|
||||||
|
if (g_add_service_collide_once) {
|
||||||
|
g_add_service_collide_once = false;
|
||||||
|
return kErrCollision;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stub address publication that models Avahi's reverse-PTR conflict.
|
||||||
|
* @details With the daemon's default `publish-addresses=yes`, the daemon
|
||||||
|
* already owns the reverse PTR for each local IP. Publishing the same IP
|
||||||
|
* for the service host without `PUBLISH_NO_REVERSE` creates a competing
|
||||||
|
* unique reverse PTR and Avahi rejects it with `ERR_COLLISION`.
|
||||||
|
*/
|
||||||
|
int FakeEntryGroupAddAddress(
|
||||||
|
avahi::EntryGroup * /*group*/,
|
||||||
|
avahi::IfIndex interface,
|
||||||
|
avahi::Protocol /*protocol*/,
|
||||||
|
avahi::PublishFlags flags,
|
||||||
|
const char * /*name*/,
|
||||||
|
const avahi::Address *address
|
||||||
|
) {
|
||||||
|
if ((flags & avahi::PUBLISH_NO_REVERSE) == 0) {
|
||||||
|
return kErrCollision;
|
||||||
|
}
|
||||||
|
g_address_calls.push_back({interface, flags, *address});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stub address publication without the reverse-PTR conflict check.
|
||||||
|
* @details The F2/F3 tests install this stub so their fail-before
|
||||||
|
* evidence isolates their own finding: with the strict stub, the missing
|
||||||
|
* `PUBLISH_NO_REVERSE` flag (F1) would fail those tests first. Linux-only:
|
||||||
|
* only the Linux-gated registration tests install it.
|
||||||
|
*/
|
||||||
|
#if defined(__linux__)
|
||||||
|
int FakeEntryGroupAddAddressLenient(
|
||||||
|
avahi::EntryGroup * /*group*/,
|
||||||
|
avahi::IfIndex interface,
|
||||||
|
avahi::Protocol /*protocol*/,
|
||||||
|
avahi::PublishFlags flags,
|
||||||
|
const char * /*name*/,
|
||||||
|
const avahi::Address *address
|
||||||
|
) {
|
||||||
|
if (g_add_address_fail_once) {
|
||||||
|
g_add_address_fail_once = false;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_address_calls.push_back({interface, flags, *address});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fixture driving the publisher with stubbed Avahi calls.
|
||||||
|
*/
|
||||||
|
class PublishRegistrationTest: public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
saved_bind_address_ = config::sunshine.bind_address;
|
||||||
|
saved_address_family_ = config::sunshine.address_family;
|
||||||
|
|
||||||
|
avahi::free = FakeFree;
|
||||||
|
avahi::client_free = FakeClientFree;
|
||||||
|
avahi::alternative_service_name = FakeAlternativeServiceName;
|
||||||
|
avahi::strerror = FakeStrerror;
|
||||||
|
avahi::client_errno = FakeClientErrno;
|
||||||
|
avahi::simple_poll_quit = FakeSimplePollQuit;
|
||||||
|
avahi::entry_group_is_empty = FakeEntryGroupIsEmpty;
|
||||||
|
avahi::entry_group_reset = FakeEntryGroupReset;
|
||||||
|
avahi::entry_group_commit = FakeEntryGroupCommit;
|
||||||
|
avahi::entry_group_add_service = FakeEntryGroupAddService;
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddress;
|
||||||
|
|
||||||
|
g_address_calls.clear();
|
||||||
|
g_service_hosts.clear();
|
||||||
|
g_reset_count = 0;
|
||||||
|
g_commit_count = 0;
|
||||||
|
g_quit_count = 0;
|
||||||
|
g_group_empty = true;
|
||||||
|
g_add_service_collide_once = false;
|
||||||
|
g_add_address_fail_once = false;
|
||||||
|
|
||||||
|
platf::publish::group = reinterpret_cast<avahi::EntryGroup *>(0x1000);
|
||||||
|
platf::publish::client.reset(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
platf::publish::name.reset(::strdup("test-host"));
|
||||||
|
platf::publish::host_name_collision_count = 0;
|
||||||
|
platf::publish::published_addresses.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
// Release owned state through the stubs before uninstalling them:
|
||||||
|
// the deleters call the stubbed Avahi free functions.
|
||||||
|
platf::publish::client.reset();
|
||||||
|
platf::publish::name.reset();
|
||||||
|
platf::publish::group = nullptr;
|
||||||
|
platf::publish::host_name_collision_count = 0;
|
||||||
|
platf::publish::published_addresses.reset();
|
||||||
|
|
||||||
|
avahi::free = nullptr;
|
||||||
|
avahi::client_free = nullptr;
|
||||||
|
avahi::alternative_service_name = nullptr;
|
||||||
|
avahi::strerror = nullptr;
|
||||||
|
avahi::client_errno = nullptr;
|
||||||
|
avahi::simple_poll_quit = nullptr;
|
||||||
|
avahi::entry_group_is_empty = nullptr;
|
||||||
|
avahi::entry_group_reset = nullptr;
|
||||||
|
avahi::entry_group_commit = nullptr;
|
||||||
|
avahi::entry_group_add_service = nullptr;
|
||||||
|
avahi::entry_group_add_address = nullptr;
|
||||||
|
|
||||||
|
config::sunshine.bind_address = saved_bind_address_;
|
||||||
|
config::sunshine.address_family = saved_address_family_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string saved_bind_address_;
|
||||||
|
std::string saved_address_family_;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
TEST_F(PublishRegistrationTest, AddressPublicationSkipsReversePtr) {
|
||||||
|
// The service-host alias must publish forward records only: with flags 0
|
||||||
|
// the daemon rejects the competing reverse PTR with ERR_COLLISION, the
|
||||||
|
// group resets, and the poll loop quits before registration.
|
||||||
|
const auto ret = platf::publish::add_service_addresses(
|
||||||
|
platf::publish::group,
|
||||||
|
"myhost-sunshine.local",
|
||||||
|
{{1, boost::asio::ip::make_address("192.168.1.10")}}
|
||||||
|
);
|
||||||
|
ASSERT_EQ(ret, 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 1U);
|
||||||
|
ASSERT_NE(g_address_calls[0].flags & avahi::PUBLISH_NO_REVERSE, 0);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PublishRegistrationTest, AddressPublicationUsesOwningInterface) {
|
||||||
|
// Lenient stub: isolate the interface finding from the flag finding (F1).
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient;
|
||||||
|
|
||||||
|
// A LAN address and a Docker-bridge address must each be published on
|
||||||
|
// their owning interface, including distinct link-local IPv6 addresses:
|
||||||
|
// publishing everything with IF_UNSPEC would hand LAN clients the
|
||||||
|
// bridge address (and vice versa) under the same SRV target.
|
||||||
|
const auto ret = platf::publish::add_service_addresses(
|
||||||
|
platf::publish::group,
|
||||||
|
"myhost-sunshine.local",
|
||||||
|
{{1, boost::asio::ip::make_address("192.168.1.10")},
|
||||||
|
{2, boost::asio::ip::make_address("172.17.0.1")},
|
||||||
|
{1, boost::asio::ip::make_address("fe80::1")},
|
||||||
|
{2, boost::asio::ip::make_address("fe80::2")}}
|
||||||
|
);
|
||||||
|
ASSERT_EQ(ret, 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 4U);
|
||||||
|
ASSERT_EQ(g_address_calls[0].interface, 1);
|
||||||
|
ASSERT_EQ(g_address_calls[1].interface, 2);
|
||||||
|
ASSERT_EQ(g_address_calls[2].interface, 1);
|
||||||
|
ASSERT_EQ(g_address_calls[3].interface, 2);
|
||||||
|
ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10")));
|
||||||
|
ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("172.17.0.1")));
|
||||||
|
ASSERT_EQ(g_address_calls[2].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::1")));
|
||||||
|
ASSERT_EQ(g_address_calls[3].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::2")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PublishRegistrationTest, RefreshRepublishesOnAddressChange) {
|
||||||
|
// Lenient stub: isolate the refresh finding from the flag finding (F1).
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient;
|
||||||
|
|
||||||
|
// Register .10, change the snapshot to .20, deliver the change
|
||||||
|
// notification: the group must be reset (withdrawing .10) and the new
|
||||||
|
// snapshot committed, with no stale .10 record and the poll loop alive.
|
||||||
|
config::sunshine.bind_address = "192.168.1.10";
|
||||||
|
config::sunshine.address_family = "both";
|
||||||
|
|
||||||
|
platf::publish::create_services(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 1U);
|
||||||
|
ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10")));
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.20";
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
ASSERT_EQ(g_reset_count, 1);
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 2U);
|
||||||
|
ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.20")));
|
||||||
|
|
||||||
|
// A second refresh with an unchanged snapshot is a no-op: no churn.
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
ASSERT_EQ(g_reset_count, 1);
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PublishRegistrationTest, RefreshCollisionRetryDoesNotQuitPollLoop) {
|
||||||
|
// A service collision during a refresh retry must not quit the poll
|
||||||
|
// loop: the retry inherits the refresh's no-quit-on-failure behavior,
|
||||||
|
// and the service is re-registered under a new host name.
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient;
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.10";
|
||||||
|
config::sunshine.address_family = "both";
|
||||||
|
|
||||||
|
platf::publish::create_services(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_service_hosts.size(), 1U);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.20";
|
||||||
|
g_add_service_collide_once = true;
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
// Three service registrations: the initial one, the refresh attempt that
|
||||||
|
// collided, and the retry under the regenerated host name.
|
||||||
|
ASSERT_EQ(g_service_hosts.size(), 3U);
|
||||||
|
ASSERT_EQ(g_service_hosts[1], g_service_hosts[0]);
|
||||||
|
ASSERT_NE(g_service_hosts[2], g_service_hosts[0]);
|
||||||
|
ASSERT_EQ(platf::publish::host_name_collision_count, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreRepublishes) {
|
||||||
|
// A -> failed B -> A: the failed refresh withdraws the group and must
|
||||||
|
// invalidate the snapshot, so restoring A republishes instead of
|
||||||
|
// early-returning on a stale snapshot while the group sits empty.
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient;
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.10";
|
||||||
|
config::sunshine.address_family = "both";
|
||||||
|
|
||||||
|
platf::publish::create_services(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.20";
|
||||||
|
g_add_address_fail_once = true;
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
// The refresh reset the group and the registration failed: the group is
|
||||||
|
// empty and no snapshot is committed.
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_FALSE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.10";
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
// Restoring A must commit a second time, with a live group and the
|
||||||
|
// poll loop still running.
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 2U);
|
||||||
|
ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10")));
|
||||||
|
|
||||||
|
// A further refresh with an unchanged snapshot is a no-op: no churn.
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreEmptySnapshotRepublishes) {
|
||||||
|
// Empty-A variant: a successfully published empty snapshot (the
|
||||||
|
// daemon-hostname fallback) is a valid committed snapshot, not a
|
||||||
|
// withdrawn group, so the failed-B invalidation must still let the
|
||||||
|
// restored empty snapshot republish.
|
||||||
|
avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient;
|
||||||
|
|
||||||
|
// An IPv6 bind under an IPv4-only configuration serves no addresses:
|
||||||
|
// the service falls back to the daemon host name with no address records.
|
||||||
|
config::sunshine.bind_address = "2001:db8::1";
|
||||||
|
config::sunshine.address_family = "ipv4";
|
||||||
|
|
||||||
|
platf::publish::create_services(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses->empty());
|
||||||
|
ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 0U);
|
||||||
|
ASSERT_EQ(g_service_hosts.size(), 1U);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "192.168.1.20";
|
||||||
|
config::sunshine.address_family = "both";
|
||||||
|
g_add_address_fail_once = true;
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
// The refresh reset the group and the registration failed: the group is
|
||||||
|
// empty and no snapshot is committed, even though the last good snapshot
|
||||||
|
// was itself empty.
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_FALSE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
|
||||||
|
config::sunshine.bind_address = "2001:db8::1";
|
||||||
|
config::sunshine.address_family = "ipv4";
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
|
||||||
|
// Restoring the empty snapshot must commit a second time, with a live
|
||||||
|
// group and the poll loop still running.
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses.has_value());
|
||||||
|
ASSERT_TRUE(platf::publish::published_addresses->empty());
|
||||||
|
ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0);
|
||||||
|
ASSERT_EQ(g_address_calls.size(), 0U);
|
||||||
|
ASSERT_EQ(g_service_hosts.size(), 2U);
|
||||||
|
|
||||||
|
// A further refresh with an unchanged snapshot is a no-op: no churn.
|
||||||
|
platf::publish::refresh_service_addresses();
|
||||||
|
ASSERT_EQ(g_commit_count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
TEST_F(PublishRegistrationTest, NonLinuxFallbackUsesDaemonHostName) {
|
||||||
|
// On platforms without the netlink address-change watch (e.g. FreeBSD),
|
||||||
|
// the service must not publish fixed address records for a
|
||||||
|
// service-specific host name: with no refresh, a DHCP change would
|
||||||
|
// leave clients with the stale snapshot. Instead the service uses the
|
||||||
|
// daemon-managed host name (a null SRV target), and the Avahi daemon
|
||||||
|
// maintains the interface address records.
|
||||||
|
ASSERT_FALSE(platf::publish::install_address_change_watch());
|
||||||
|
|
||||||
|
// An explicit bind with a populated snapshot is the regression case:
|
||||||
|
// the pre-fix code published the custom host plus its fixed addresses.
|
||||||
|
config::sunshine.bind_address = "192.168.1.10";
|
||||||
|
config::sunshine.address_family = "both";
|
||||||
|
|
||||||
|
platf::publish::create_services(reinterpret_cast<avahi::Client *>(0x2000));
|
||||||
|
|
||||||
|
ASSERT_EQ(g_commit_count, 1);
|
||||||
|
ASSERT_EQ(g_quit_count, 0);
|
||||||
|
// No fixed alias address records were published ...
|
||||||
|
ASSERT_TRUE(g_address_calls.empty());
|
||||||
|
// ... and the service points at the daemon-managed host name.
|
||||||
|
ASSERT_EQ(g_service_hosts.size(), 1U);
|
||||||
|
ASSERT_TRUE(g_service_hosts[0].empty());
|
||||||
|
}
|
||||||
|
#endif // defined(__linux__)
|
||||||
|
|
||||||
|
#endif // !defined(_WIN32) && !defined(__APPLE__)
|
||||||
Reference in new issue
Block a user