Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 40 additions & 37 deletions src/reflector/interface_address.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,27 +259,10 @@ void ResolveViaNetlink(unsigned index, InterfaceAddresses& result) noexcept {

#else

// The interface's link-layer MAC from a sockaddr_dl, or a default (all-zero) MacAddress if it
// has none. Reads straight out of the kernel-provided sockaddr rather than copying the whole
// (variable-length) sockaddr into a fixed local: the MAC sits at a name-dependent offset in
// sdl_data that a long interface name could push past any local sockaddr_dl. Byte access
// through std::byte* is alignment- and aliasing-safe.
MacAddress ReadMac(const ifaddrs& ifa) noexcept {
const auto* bytes = reinterpret_cast<const std::byte*>(ifa.ifa_addr);
const auto sa_len = static_cast<size_t>(ifa.ifa_addr->sa_len);
const auto name_len = std::to_integer<size_t>(bytes[offsetof(sockaddr_dl, sdl_nlen)]);
const auto addr_len = std::to_integer<size_t>(bytes[offsetof(sockaddr_dl, sdl_alen)]);
const auto mac_offset = offsetof(sockaddr_dl, sdl_data) + name_len;
if (addr_len != MAC_SIZE || mac_offset + MAC_SIZE > sa_len) {
return {};
}
return MacAddress::FromBytes(std::span<const std::byte, MAC_SIZE>{bytes + mac_offset, MAC_SIZE});
}

// True only if the kernel confirms the IPv6 address is a valid source — not tentative,
// deprecated, duplicated, etc. Queried via SIOCGIFAFLAG_IN6 since getifaddrs doesn't surface
// per-address flags. If the ioctl fails — most likely because the address was removed between
// enumeration and now — we can't confirm it's usable, so we skip it rather than risk a
// True only if the kernel confirms the IPv6 address is a valid source. Queried via SIOCGIFAFLAG_IN6
// since getifaddrs doesn't surface per-address flags; the usable/unusable decision itself is
// detail::Ipv6SourceFlagsUsable. If the ioctl fails — most likely because the address was removed
// between enumeration and now — we can't confirm it's usable, so we skip it rather than risk a
// tentative or stale source.
bool IsUsableIpv6(int inet6_fd, const char* interface, const sockaddr_in6& sin6) noexcept {
in6_ifreq request{};
Expand All @@ -289,20 +272,7 @@ bool IsUsableIpv6(int inet6_fd, const char* interface, const sockaddr_in6& sin6)
GetLogger().Warning("Cannot query IPv6 address flags on interface \"{}\": {}", interface, Error::FromErrno());
return false;
}
constexpr int unusable = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED | IN6_IFF_DETACHED
| IN6_IFF_DEPRECATED | IN6_IFF_ANYCAST;
return (request.ifr_ifru.ifru_flags6 & unusable) == 0;
}

// BSD/KAME embeds the scope id (interface index) in bytes 2-3 of a link-local sin6_addr;
// clear it so the on-wire source is the canonical fe80::<interface-id>. macOS-only: Linux's
// netlink returns canonical link-local addresses, so the same clearing there would be a no-op
// at best and corrupt a non-conforming address at worst.
IpAddress CanonicalizeLinkLocal(const IpAddress& address) noexcept {
auto bytes = address.Bytes();
bytes[2] = std::byte{0};
bytes[3] = std::byte{0};
return IpAddress::FromV6Bytes(bytes);
return detail::Ipv6SourceFlagsUsable(request.ifr_ifru.ifru_flags6);
}

void ResolveViaGetifaddrs(std::string_view interface, InterfaceAddresses& result) noexcept {
Expand Down Expand Up @@ -344,14 +314,14 @@ void ResolveViaGetifaddrs(std::string_view interface, InterfaceAddresses& result
}
if (auto address = IpAddress::FromSockaddr(ifa->ifa_addr); address) {
if (address->IsLinkLocal()) {
address = CanonicalizeLinkLocal(*address);
address = detail::CanonicalizeLinkLocalV6(*address);
}
candidates.push_back(*address);
}
break;
}
case AF_LINK:
result.mac = ReadMac(*ifa);
result.mac = detail::MacFromLinkSockaddr(*ifa->ifa_addr);
break;
default:
break;
Expand All @@ -378,6 +348,39 @@ void SelectSourceAddresses(std::span<const IpAddress> candidates, InterfaceAddre
}
}

#if !defined(__linux__)

// Reads straight out of the kernel-provided sockaddr rather than copying the whole (variable-length)
// sockaddr into a fixed local: the MAC sits at a name-length-dependent offset in sdl_data that a long
// interface name could push past any local sockaddr_dl. Byte access through std::byte* is alignment-
// and aliasing-safe.
MacAddress MacFromLinkSockaddr(const sockaddr& link_addr) noexcept {
const auto* bytes = reinterpret_cast<const std::byte*>(&link_addr);
const auto sa_len = static_cast<size_t>(link_addr.sa_len);
const auto name_len = std::to_integer<size_t>(bytes[offsetof(sockaddr_dl, sdl_nlen)]);
const auto addr_len = std::to_integer<size_t>(bytes[offsetof(sockaddr_dl, sdl_alen)]);
const auto mac_offset = offsetof(sockaddr_dl, sdl_data) + name_len;
if (addr_len != MAC_SIZE || mac_offset + MAC_SIZE > sa_len) {
return {};
}
return MacAddress::FromBytes(std::span<const std::byte, MAC_SIZE>{bytes + mac_offset, MAC_SIZE});
}

bool Ipv6SourceFlagsUsable(int flags6) noexcept {
constexpr int unusable = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED | IN6_IFF_DETACHED
| IN6_IFF_DEPRECATED | IN6_IFF_ANYCAST;
return (flags6 & unusable) == 0;
}

IpAddress CanonicalizeLinkLocalV6(const IpAddress& address) noexcept {
auto bytes = address.Bytes();
bytes[2] = std::byte{0};
bytes[3] = std::byte{0};
return IpAddress::FromV6Bytes(bytes);
}

#endif

} // namespace detail

#if defined(__linux__)
Expand Down
20 changes: 20 additions & 0 deletions src/reflector/interface_address.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <optional>
#include <span>
#include <string_view>
#include <sys/socket.h>

namespace reflector {

Expand All @@ -30,6 +31,25 @@ namespace detail {
// without a real interface that happens to carry the right address mix.
void SelectSourceAddresses(std::span<const IpAddress> candidates, InterfaceAddresses& result) noexcept;

#if !defined(__linux__)
// The BSD getifaddrs resolver's pure helpers, split out for unit testing (the resolver itself needs
// a real interface). `link_addr` is an AF_LINK sockaddr (a sockaddr_dl).

// The interface's link-layer MAC read from an AF_LINK sockaddr_dl, or all-zero if it carries none
// (addr_len != 6) or the address would run past the sockaddr's sa_len. Reads by byte offset — the
// MAC sits at a name-length-dependent offset a long interface name could push past a fixed local.
[[nodiscard]] MacAddress MacFromLinkSockaddr(const sockaddr& link_addr) noexcept;

// Whether the IN6_IFF_* flags (from SIOCGIFAFLAG_IN6) mark the address a usable source — not
// tentative, duplicated, detached, deprecated, or anycast.
[[nodiscard]] bool Ipv6SourceFlagsUsable(int flags6) noexcept;

// A KAME link-local sin6_addr with the embedded scope id (interface index in bytes 2-3) cleared to
// the canonical fe80::<id> on-wire form. macOS/FreeBSD only — Linux netlink already yields canonical
// addresses, so the same clearing there could corrupt a non-conforming one.
[[nodiscard]] IpAddress CanonicalizeLinkLocalV6(const IpAddress& address) noexcept;
#endif

} // namespace detail

// Resolves an interface's MAC and per-family source addresses; fields are left empty for
Expand Down
178 changes: 78 additions & 100 deletions tests/application_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ApplicationTest : public ::testing::Test {
// The signal-wakeup self-pipe read fd (friendship isn't inherited, so the TEST_F body reaches the
// private member through this fixture accessor, like the counts above).
static int WakeupReadFd(const Application& app) { return app.wakeup_read_.Get(); }
static int WakeupWriteFd(const Application& app) { return app.wakeup_write_.Get(); }

// Per-interface settings the factories stamp onto the Interface and socket when first
// created. Interfaces with no entry get the defaults (valid, can send both families,
Expand Down Expand Up @@ -218,36 +219,76 @@ TEST_F(ApplicationTest, CreatesDistinctSocketsForDistinctInterfaces) {
EXPECT_EQ(dispatcher_->RegistrationCount(), 2); // two distinct source fds watched
}

TEST_F(ApplicationTest, FailsWhenSourceSocketInvalid) {
ConfigureSocket("bad-src", {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeWolConfig("tv", "bad-src", "dst", {9}))
.Build();
// Which side of the reflector (source or target) gets the invalid socket.
enum class SocketRole { Source, Target };

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});
using ReflectorConfigFactory = Config (*)(std::string_view source_if, std::string_view target_if);

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("bad-src"), std::string::npos) << output; // the log names the source interface
}
struct InvalidSocketParam {
std::string_view protocol;
ReflectorConfigFactory make_config;
SocketRole role;

// gtest prints each param to annotate --gtest_list_tests output. Without a printer it falls back
// to RawBytesPrinter, which formats the struct's trailing padding -- uninitialised, so memcheck
// (the Valgrind gate) flags it. A hidden friend is found by gtest via ADL and reads only the
// initialised fields.
friend void PrintTo(const InvalidSocketParam& param, std::ostream* os) {
*os << param.protocol << (param.role == SocketRole::Source ? "/source" : "/target");
}
};

TEST_F(ApplicationTest, FailsWhenTargetSocketInvalid) {
ConfigureSocket("bad-dst", {.valid = false});
// Value-parameterized over {protocol, source-or-target}: ConfigureReflectors<T>'s invalid-socket
// branch (Application::ConfigureReflectors in application.cpp) is one code path shared by every
// protocol via the ReflectorType/ConfigType template parameters, so this replaces six near-
// identical TEST_Fs (one pair per protocol) with a single test body instantiated six times — every
// protocol stays covered even if the template is later split apart.
class ApplicationInvalidSocketTest : public ApplicationTest,
public ::testing::WithParamInterface<InvalidSocketParam> {
public:
static Config WolReflectorConfig(std::string_view source_if, std::string_view target_if) {
return TestConfigBuilder{}.Add(MakeWolConfig("tv", source_if, target_if, {9})).Build();
}

static Config MdnsReflectorConfig(std::string_view source_if, std::string_view target_if) {
return TestConfigBuilder{}.Add(MakeMdnsConfig("cast", source_if, target_if)).Build();
}

static Config SsdpReflectorConfig(std::string_view source_if, std::string_view target_if) {
return TestConfigBuilder{}.Add(MakeSsdpConfig("cast", source_if, target_if)).Build();
}
};

TEST_P(ApplicationInvalidSocketTest, FailsAndLogsTheInterface) {
const auto& param = GetParam();
const bool bad_is_source = param.role == SocketRole::Source;
const std::string_view bad_if = bad_is_source ? "bad-src" : "bad-dst";
ConfigureSocket(bad_if, {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeWolConfig("tv", "src", "bad-dst", {9}))
.Build();
const Config config = bad_is_source ? param.make_config(bad_if, "dst") : param.make_config("src", bad_if);

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("bad-dst"), std::string::npos) << output; // the log names the target interface
EXPECT_NE(output.find(param.protocol), std::string::npos) << output; // the log names the protocol
EXPECT_NE(output.find(bad_if), std::string::npos) << output; // and the invalid interface
}

INSTANTIATE_TEST_SUITE_P(Protocols, ApplicationInvalidSocketTest,
::testing::Values(
InvalidSocketParam{"wol", &ApplicationInvalidSocketTest::WolReflectorConfig, SocketRole::Source},
InvalidSocketParam{"wol", &ApplicationInvalidSocketTest::WolReflectorConfig, SocketRole::Target},
InvalidSocketParam{"mdns", &ApplicationInvalidSocketTest::MdnsReflectorConfig, SocketRole::Source},
InvalidSocketParam{"mdns", &ApplicationInvalidSocketTest::MdnsReflectorConfig, SocketRole::Target},
InvalidSocketParam{"ssdp", &ApplicationInvalidSocketTest::SsdpReflectorConfig, SocketRole::Source},
InvalidSocketParam{"ssdp", &ApplicationInvalidSocketTest::SsdpReflectorConfig, SocketRole::Target}),
[](const ::testing::TestParamInfo<InvalidSocketParam>& param_info) {
return std::string{param_info.param.protocol}
+ (param_info.param.role == SocketRole::Source ? "Source" : "Target");
});

TEST_F(ApplicationTest, FailsWhenReflectorSetupFails) {
// The target socket is valid but can't originate IPv4, so the (IPv4) reflector fails to
// initialize even though both socket checks pass.
Expand Down Expand Up @@ -315,37 +356,6 @@ TEST_F(ApplicationTest, SharesSocketsBetweenWolAndMdns) {
EXPECT_EQ(dispatcher_->RegistrationCount(), 2);
}

TEST_F(ApplicationTest, FailsWhenMdnsSourceSocketInvalid) {
ConfigureSocket("bad-src", {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeMdnsConfig("cast", "bad-src", "dst"))
.Build();

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("mdns"), std::string::npos) << output; // the log names the protocol
EXPECT_NE(output.find("bad-src"), std::string::npos) << output; // and the source interface
}

TEST_F(ApplicationTest, FailsWhenMdnsTargetSocketInvalid) {
ConfigureSocket("bad-dst", {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeMdnsConfig("cast", "src", "bad-dst"))
.Build();

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("bad-dst"), std::string::npos) << output; // the log names the target interface
}

TEST_F(ApplicationTest, FailsWhenMdnsReflectorSetupFails) {
// mDNS needs the family sendable on both interfaces; the IPv4 config can't be reflected when
// the target can't originate IPv4, so the reflector fails to initialize.
Expand Down Expand Up @@ -417,37 +427,6 @@ TEST_F(ApplicationTest, SharesSocketsAcrossAllProtocols) {
EXPECT_EQ(dispatcher_->RegistrationCount(), 2);
}

TEST_F(ApplicationTest, FailsWhenSsdpSourceSocketInvalid) {
ConfigureSocket("bad-src", {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeSsdpConfig("cast", "bad-src", "dst"))
.Build();

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("ssdp"), std::string::npos) << output; // the log names the protocol
EXPECT_NE(output.find("bad-src"), std::string::npos) << output; // and the source interface
}

TEST_F(ApplicationTest, FailsWhenSsdpTargetSocketInvalid) {
ConfigureSocket("bad-dst", {.valid = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeSsdpConfig("cast", "src", "bad-dst"))
.Build();

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0);
EXPECT_NE(output.find("bad-dst"), std::string::npos) << output; // the log names the target interface
}

TEST_F(ApplicationTest, FailsWhenSsdpReflectorSetupFails) {
// SSDP needs the family sendable on both interfaces; the IPv4 config can't be reflected when the
// target can't originate IPv4, so the reflector fails to initialize.
Expand All @@ -464,24 +443,6 @@ TEST_F(ApplicationTest, FailsWhenSsdpReflectorSetupFails) {
EXPECT_EQ(ReflectorCount(app), 0) << output;
}

TEST_F(ApplicationTest, ClearsEarlierReflectorsWhenSsdpFails) {
// SSDP is configured last; a source that can't originate IPv4 breaks SSDP (which reflects in
// both directions) but not the WoL reflector wired before it. Configure is transactional, so the
// earlier success is rolled back, leaving nothing wired.
ConfigureSocket("src", {.can_send_v4 = false});
auto app = MakeApp();
const Config config = TestConfigBuilder{}
.Add(MakeWolConfig("tv", "src", "dst", {9}))
.Add(MakeSsdpConfig("cast", "src", "dst"))
.Build();

const std::string output = CaptureStdout([&] {
EXPECT_FALSE(app.Configure(config));
});

EXPECT_EQ(ReflectorCount(app), 0) << output; // the WoL reflector wired earlier is rolled back
}

TEST_F(ApplicationTest, SubscribesToTheAddressMonitor) {
auto app = MakeApp();

Expand Down Expand Up @@ -529,8 +490,11 @@ TEST_F(ApplicationTest, NotifiesReflectorsWhenAnInterfaceChanges) {
Iface("dst")->SetHasSource(IpAddress::Family::V6, true); // a v6 address appears on the target
const std::string output = CaptureStdout([&] { monitor_->FireChange(9); });

EXPECT_NE(output.find(std::format("Starting {} reflection", IpAddress::Family::V6)),
std::string::npos) << output;
// Pin only the stable tail of family_capability.cpp's wording, not the full
// "Starting {} reflection: ..." sentence: e2e/run.py's ADDR_CHANGE_EXPECTED_ERROR pins the
// complementary "a source address is no longer available" tail for the loss case, so keeping
// to just the tail here means a prefix wording change doesn't need fixing in three places.
EXPECT_NE(output.find("a source address is available"), std::string::npos) << output;
}

TEST_F(ApplicationTest, RefreshesAllInterfacesOnOverflowSignal) {
Expand Down Expand Up @@ -607,4 +571,18 @@ TEST_F(ApplicationTest, SignalWakeupRegistersAndDrains) {
EXPECT_TRUE(IsWouldBlockErrno(errno));
}

// If the dispatcher can't watch the read end, PrepareSignalWakeup rolls back: both pipe fds are
// closed (they would otherwise leak -- LeakSanitizer doesn't track fds) and it returns -1 so main()
// falls back to poll-interval-bounded shutdown rather than a live but unwatched pipe.
TEST_F(ApplicationTest, SignalWakeupRollsBackWhenRegistrationFails) {
auto app = MakeApp();
const size_t before = dispatcher_->RegistrationCount();
dispatcher_->fail_registers_remaining = 1; // the wakeup pipe is the next (and only) Register here

EXPECT_EQ(app.PrepareSignalWakeup(), -1);
EXPECT_EQ(dispatcher_->RegistrationCount(), before); // nothing left watched
EXPECT_EQ(WakeupReadFd(app), -1); // read end closed on rollback
EXPECT_EQ(WakeupWriteFd(app), -1); // write end closed on rollback
}

} // namespace reflector
Loading