Skip to content

Commit c5f1f92

Browse files
Make multicast detection test actual packet delivery
The has_ipv4_multicast() and has_ipv6_multicast() functions previously only checked if network interfaces reported the IFF_MULTICAST flag. On macOS GitHub Actions runners (virtualized ARM64 VMs), interfaces report multicast capability but the hypervisor doesn't actually deliver multicast packets. Now performs an actual multicast send/receive round-trip test with a 200ms timeout. This correctly detects broken multicast environments and causes those tests to be skipped rather than hanging.
1 parent 618b472 commit c5f1f92

1 file changed

Lines changed: 158 additions & 4 deletions

File tree

tests/test_util/has_multicast.cpp

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,174 @@
2929

3030
namespace test_util {
3131

32+
namespace {
33+
34+
/**
35+
* Attempt an actual multicast send/receive round-trip.
36+
* Returns true only if the packet is successfully delivered.
37+
* This detects environments (e.g., macOS CI VMs) where interfaces report IFF_MULTICAST
38+
* but the hypervisor doesn't actually deliver multicast packets.
39+
*/
40+
bool test_multicast_roundtrip(int af, const char* group_addr) {
41+
// Create a UDP socket for receiving
42+
NUClear::fd_t recv_fd = ::socket(af, SOCK_DGRAM, 0);
43+
if (recv_fd < 0) {
44+
return false;
45+
}
46+
47+
// Allow address reuse
48+
int one = 1;
49+
::setsockopt(recv_fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&one), sizeof(one));
50+
#ifdef SO_REUSEPORT
51+
::setsockopt(recv_fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char*>(&one), sizeof(one));
52+
#endif
53+
54+
// Bind to any address on an ephemeral port
55+
uint16_t port = 0;
56+
if (af == AF_INET) {
57+
sockaddr_in bind_addr{};
58+
bind_addr.sin_family = AF_INET;
59+
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
60+
bind_addr.sin_port = 0;
61+
62+
if (::bind(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
63+
::close(recv_fd);
64+
return false;
65+
}
66+
67+
// Get the assigned port
68+
socklen_t len = sizeof(bind_addr);
69+
::getsockname(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), &len);
70+
port = ntohs(bind_addr.sin_port);
71+
72+
// Join the multicast group
73+
struct ip_mreq mreq {};
74+
::inet_pton(AF_INET, group_addr, &mreq.imr_multiaddr);
75+
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
76+
if (::setsockopt(recv_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast<const char*>(&mreq), sizeof(mreq))
77+
< 0) {
78+
::close(recv_fd);
79+
return false;
80+
}
81+
}
82+
else {
83+
sockaddr_in6 bind_addr{};
84+
bind_addr.sin6_family = AF_INET6;
85+
bind_addr.sin6_addr = in6addr_any;
86+
bind_addr.sin6_port = 0;
87+
88+
if (::bind(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
89+
::close(recv_fd);
90+
return false;
91+
}
92+
93+
socklen_t len = sizeof(bind_addr);
94+
::getsockname(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), &len);
95+
port = ntohs(bind_addr.sin6_port);
96+
97+
// Join the multicast group
98+
struct ipv6_mreq mreq {};
99+
::inet_pton(AF_INET6, group_addr, &mreq.ipv6mr_multiaddr);
100+
mreq.ipv6mr_interface = 0;
101+
if (::setsockopt(recv_fd,
102+
IPPROTO_IPV6,
103+
IPV6_JOIN_GROUP,
104+
reinterpret_cast<const char*>(&mreq),
105+
sizeof(mreq))
106+
< 0) {
107+
::close(recv_fd);
108+
return false;
109+
}
110+
}
111+
112+
// Create a send socket
113+
NUClear::fd_t send_fd = ::socket(af, SOCK_DGRAM, 0);
114+
if (send_fd < 0) {
115+
::close(recv_fd);
116+
return false;
117+
}
118+
119+
// Set multicast loopback so we receive our own packet
120+
if (af == AF_INET) {
121+
uint8_t loop = 1;
122+
::setsockopt(send_fd, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast<const char*>(&loop), sizeof(loop));
123+
}
124+
else {
125+
int loop = 1;
126+
::setsockopt(send_fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, reinterpret_cast<const char*>(&loop), sizeof(loop));
127+
}
128+
129+
// Send a test packet to the multicast group
130+
const char test_msg[] = "MCAST_TEST";
131+
if (af == AF_INET) {
132+
sockaddr_in dest{};
133+
dest.sin_family = AF_INET;
134+
dest.sin_port = htons(port);
135+
::inet_pton(AF_INET, group_addr, &dest.sin_addr);
136+
::sendto(send_fd,
137+
test_msg,
138+
sizeof(test_msg),
139+
0,
140+
reinterpret_cast<sockaddr*>(&dest),
141+
sizeof(dest));
142+
}
143+
else {
144+
sockaddr_in6 dest{};
145+
dest.sin6_family = AF_INET6;
146+
dest.sin6_port = htons(port);
147+
::inet_pton(AF_INET6, group_addr, &dest.sin6_addr);
148+
::sendto(send_fd,
149+
test_msg,
150+
sizeof(test_msg),
151+
0,
152+
reinterpret_cast<sockaddr*>(&dest),
153+
sizeof(dest));
154+
}
155+
156+
// Wait for the packet with a 200ms timeout using select (portable across all platforms)
157+
fd_set read_fds;
158+
FD_ZERO(&read_fds); // NOLINT(readability-isolate-declaration)
159+
FD_SET(recv_fd, &read_fds); // NOLINT(hicpp-signed-bitwise)
160+
struct timeval tv {};
161+
tv.tv_sec = 0;
162+
tv.tv_usec = 200000; // 200ms
163+
164+
int ready = ::select(static_cast<int>(recv_fd) + 1, &read_fds, nullptr, nullptr, &tv);
165+
166+
::close(send_fd);
167+
::close(recv_fd);
168+
169+
return ready > 0;
170+
}
171+
172+
} // namespace
173+
32174
bool has_ipv4_multicast() {
33-
// See if any interface has multicast ipv4
175+
// First check if any interface reports multicast support
34176
auto ifaces = NUClear::util::network::get_interfaces();
35-
return std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) {
177+
bool has_flag = std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) {
36178
return iface.ip.sock.sa_family == AF_INET && iface.flags.multicast;
37179
});
180+
if (!has_flag) {
181+
return false;
182+
}
183+
184+
// Then verify multicast actually works with a real round-trip
185+
return test_multicast_roundtrip(AF_INET, "239.255.255.250");
38186
}
39187

40188
bool has_ipv6_multicast() {
41-
// See if any interface has multicast ipv6
189+
// First check if any interface reports multicast support
42190
auto ifaces = NUClear::util::network::get_interfaces();
43-
return std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) {
191+
bool has_flag = std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) {
44192
return iface.ip.sock.sa_family == AF_INET6 && iface.flags.multicast;
45193
});
194+
if (!has_flag) {
195+
return false;
196+
}
197+
198+
// Then verify multicast actually works with a real round-trip
199+
return test_multicast_roundtrip(AF_INET6, "ff02::1");
46200
}
47201

48202
} // namespace test_util

0 commit comments

Comments
 (0)