|
23 | 23 | #include "has_multicast.hpp" |
24 | 24 |
|
25 | 25 | #include <algorithm> |
| 26 | +#include <array> |
26 | 27 | #include <cstdint> |
27 | | -#include <string> |
28 | | -#include <system_error> |
| 28 | +#include <cstring> |
29 | 29 |
|
30 | | -#include "util/FileDescriptor.hpp" |
31 | 30 | #include "util/network/get_interfaces.hpp" |
32 | | -#include "util/network/resolve.hpp" |
33 | 31 | #include "util/platform.hpp" |
34 | 32 |
|
| 33 | +#ifndef _WIN32 |
| 34 | + #include <sys/select.h> |
| 35 | +#endif |
| 36 | + |
35 | 37 | namespace test_util { |
| 38 | + |
36 | 39 | namespace { |
37 | 40 |
|
38 | | -/// Multicast addresses used by tests/tests/dsl/UDP.cpp. |
39 | | -constexpr uint16_t IPV4_MULTICAST_PROBE_PORT = 40003; |
40 | | -constexpr uint16_t IPV6_MULTICAST_PROBE_PORT = 40004; |
41 | | -const std::string IPV4_MULTICAST_ADDRESS = "230.12.3.22"; |
42 | | -const std::string IPV6_MULTICAST_ADDRESS = "ff02::230:12:3:22"; |
43 | | - |
44 | | -bool can_send_udp_datagram(const std::string& to_addr, |
45 | | - const uint16_t to_port, |
46 | | - const std::string& bind_addr = "") { |
47 | | - try { |
48 | | - const NUClear::util::network::sock_t remote = NUClear::util::network::resolve(to_addr, to_port); |
49 | | - NUClear::util::FileDescriptor fd = ::socket(remote.sock.sa_family, SOCK_DGRAM, IPPROTO_UDP); |
50 | | - if (!fd.valid()) { |
| 41 | +constexpr std::array<char, 11> k_test_msg = {'M', 'C', 'A', 'S', 'T', '_', 'T', 'E', 'S', 'T', '\0'}; |
| 42 | + |
| 43 | +/** |
| 44 | + * Attempt an actual multicast send/receive round-trip. |
| 45 | + * Returns true only if the packet is successfully delivered. |
| 46 | + * This detects environments (e.g., macOS CI VMs) where interfaces report IFF_MULTICAST |
| 47 | + * but the hypervisor doesn't actually deliver multicast packets. |
| 48 | + */ |
| 49 | +bool test_multicast_roundtrip(int af, const char* group_addr) { |
| 50 | + // Create a UDP socket for receiving |
| 51 | + const NUClear::fd_t recv_fd = ::socket(af, SOCK_DGRAM, 0); |
| 52 | + if (recv_fd < 0) { |
| 53 | + return false; |
| 54 | + } |
| 55 | + |
| 56 | + // Allow address reuse |
| 57 | + int one = 1; |
| 58 | + ::setsockopt(recv_fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&one), sizeof(one)); |
| 59 | +#ifdef SO_REUSEPORT |
| 60 | + ::setsockopt(recv_fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char*>(&one), sizeof(one)); |
| 61 | +#endif |
| 62 | + |
| 63 | + // Bind to any address on an ephemeral port |
| 64 | + uint16_t port = 0; |
| 65 | + if (af == AF_INET) { |
| 66 | + sockaddr_in bind_addr{}; |
| 67 | + bind_addr.sin_family = AF_INET; |
| 68 | + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); |
| 69 | + bind_addr.sin_port = 0; |
| 70 | + |
| 71 | + if (::bind(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) { |
| 72 | + ::close(recv_fd); |
51 | 73 | return false; |
52 | 74 | } |
53 | 75 |
|
54 | | - const int yes = 1; |
55 | | - if (::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes)) < 0) { |
| 76 | + // Get the assigned port |
| 77 | + socklen_t len = sizeof(bind_addr); |
| 78 | + ::getsockname(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), &len); |
| 79 | + port = ntohs(bind_addr.sin_port); |
| 80 | + |
| 81 | + // Join the multicast group |
| 82 | + struct ip_mreq mreq {}; |
| 83 | + ::inet_pton(AF_INET, group_addr, &mreq.imr_multiaddr); |
| 84 | + mreq.imr_interface.s_addr = htonl(INADDR_ANY); |
| 85 | + if (::setsockopt(recv_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast<const char*>(&mreq), sizeof(mreq)) |
| 86 | + < 0) { |
| 87 | + ::close(recv_fd); |
56 | 88 | return false; |
57 | 89 | } |
| 90 | + } |
| 91 | + else { |
| 92 | + sockaddr_in6 bind_addr{}; |
| 93 | + bind_addr.sin6_family = AF_INET6; |
| 94 | + bind_addr.sin6_addr = in6addr_any; |
| 95 | + bind_addr.sin6_port = 0; |
58 | 96 |
|
59 | | - if (!bind_addr.empty()) { |
60 | | - const NUClear::util::network::sock_t local = NUClear::util::network::resolve(bind_addr, 0); |
61 | | - if (local.sock.sa_family != remote.sock.sa_family) { |
62 | | - return false; |
63 | | - } |
64 | | - if (::bind(fd, &local.sock, local.size()) != 0) { |
65 | | - return false; |
66 | | - } |
| 97 | + if (::bind(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) { |
| 98 | + ::close(recv_fd); |
| 99 | + return false; |
67 | 100 | } |
68 | 101 |
|
69 | | - const char payload = 0; |
70 | | - if (::sendto(fd, &payload, 1, 0, &remote.sock, remote.size()) < 0) { |
| 102 | + socklen_t len = sizeof(bind_addr); |
| 103 | + ::getsockname(recv_fd, reinterpret_cast<sockaddr*>(&bind_addr), &len); |
| 104 | + port = ntohs(bind_addr.sin6_port); |
| 105 | + |
| 106 | + // Join the multicast group |
| 107 | + struct ipv6_mreq mreq {}; |
| 108 | + ::inet_pton(AF_INET6, group_addr, &mreq.ipv6mr_multiaddr); |
| 109 | + mreq.ipv6mr_interface = 0; |
| 110 | + if (::setsockopt(recv_fd, |
| 111 | + IPPROTO_IPV6, |
| 112 | + IPV6_JOIN_GROUP, |
| 113 | + reinterpret_cast<const char*>(&mreq), |
| 114 | + sizeof(mreq)) |
| 115 | + < 0) { |
| 116 | + ::close(recv_fd); |
71 | 117 | return false; |
72 | 118 | } |
73 | | - return true; |
74 | 119 | } |
75 | | - catch (const std::exception&) { |
| 120 | + |
| 121 | + // Create a send socket |
| 122 | + const NUClear::fd_t send_fd = ::socket(af, SOCK_DGRAM, 0); |
| 123 | + if (send_fd < 0) { |
| 124 | + ::close(recv_fd); |
76 | 125 | return false; |
77 | 126 | } |
| 127 | + |
| 128 | + // Set multicast loopback so we receive our own packet |
| 129 | + if (af == AF_INET) { |
| 130 | + uint8_t loop = 1; |
| 131 | + ::setsockopt(send_fd, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast<const char*>(&loop), sizeof(loop)); |
| 132 | + } |
| 133 | + else { |
| 134 | + int loop = 1; |
| 135 | + ::setsockopt(send_fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, reinterpret_cast<const char*>(&loop), sizeof(loop)); |
| 136 | + } |
| 137 | + |
| 138 | + // Send a test packet to the multicast group |
| 139 | + if (af == AF_INET) { |
| 140 | + sockaddr_in dest{}; |
| 141 | + dest.sin_family = AF_INET; |
| 142 | + dest.sin_port = htons(port); |
| 143 | + ::inet_pton(AF_INET, group_addr, &dest.sin_addr); |
| 144 | + ::sendto(send_fd, |
| 145 | + k_test_msg.data(), |
| 146 | + static_cast<int>(k_test_msg.size()), |
| 147 | + 0, |
| 148 | + reinterpret_cast<sockaddr*>(&dest), |
| 149 | + sizeof(dest)); |
| 150 | + } |
| 151 | + else { |
| 152 | + sockaddr_in6 dest{}; |
| 153 | + dest.sin6_family = AF_INET6; |
| 154 | + dest.sin6_port = htons(port); |
| 155 | + ::inet_pton(AF_INET6, group_addr, &dest.sin6_addr); |
| 156 | + ::sendto(send_fd, |
| 157 | + k_test_msg.data(), |
| 158 | + static_cast<int>(k_test_msg.size()), |
| 159 | + 0, |
| 160 | + reinterpret_cast<sockaddr*>(&dest), |
| 161 | + sizeof(dest)); |
| 162 | + } |
| 163 | + |
| 164 | + // Wait for the packet with a 200ms timeout using select (portable across all platforms) |
| 165 | + fd_set read_fds; |
| 166 | + FD_ZERO(&read_fds); // NOLINT(hicpp-signed-bitwise,readability-isolate-declaration) |
| 167 | + FD_SET(recv_fd, &read_fds); // NOLINT(hicpp-signed-bitwise) |
| 168 | + timeval tv{}; |
| 169 | + tv.tv_sec = 0; |
| 170 | + tv.tv_usec = 200000; // 200ms |
| 171 | + |
| 172 | + const int ready = ::select(static_cast<int>(recv_fd) + 1, &read_fds, nullptr, nullptr, &tv); |
| 173 | + |
| 174 | + bool success = false; |
| 175 | + if (ready > 0) { |
| 176 | + // Verify the received data matches what we sent to avoid false positives |
| 177 | + std::array<char, 64> buf{}; |
| 178 | + const ssize_t n = ::recvfrom(recv_fd, buf.data(), buf.size(), 0, nullptr, nullptr); |
| 179 | + success = (n == static_cast<ssize_t>(k_test_msg.size()) |
| 180 | + && std::equal(k_test_msg.begin(), k_test_msg.end(), buf.begin())); |
| 181 | + } |
| 182 | + |
| 183 | + ::close(send_fd); |
| 184 | + ::close(recv_fd); |
| 185 | + |
| 186 | + return success; |
78 | 187 | } |
79 | 188 |
|
80 | 189 | } // namespace |
81 | 190 |
|
82 | 191 | bool has_ipv4_multicast() { |
| 192 | + // First check if any interface reports multicast support |
83 | 193 | const auto ifaces = NUClear::util::network::get_interfaces(); |
84 | | - const bool iface_multicast = |
85 | | - std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) { |
86 | | - return iface.ip.sock.sa_family == AF_INET && iface.flags.multicast; |
87 | | - }); |
88 | | - if (!iface_multicast) { |
| 194 | + const bool has_flag = std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) { |
| 195 | + return iface.ip.sock.sa_family == AF_INET && iface.flags.multicast; |
| 196 | + }); |
| 197 | + if (!has_flag) { |
89 | 198 | return false; |
90 | 199 | } |
91 | | - return can_send_udp_datagram(IPV4_MULTICAST_ADDRESS, IPV4_MULTICAST_PROBE_PORT); |
| 200 | + |
| 201 | + // Then verify multicast actually works with a real round-trip |
| 202 | + return test_multicast_roundtrip(AF_INET, "239.255.255.250"); |
92 | 203 | } |
93 | 204 |
|
94 | 205 | bool has_ipv6_multicast() { |
| 206 | + // First check if any interface reports multicast support |
95 | 207 | const auto ifaces = NUClear::util::network::get_interfaces(); |
96 | | - const bool iface_multicast = |
97 | | - std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) { |
98 | | - return iface.ip.sock.sa_family == AF_INET6 && iface.flags.multicast; |
99 | | - }); |
100 | | - if (!iface_multicast) { |
| 208 | + const bool has_flag = std::any_of(ifaces.begin(), ifaces.end(), [](const auto& iface) { |
| 209 | + return iface.ip.sock.sa_family == AF_INET6 && iface.flags.multicast; |
| 210 | + }); |
| 211 | + if (!has_flag) { |
101 | 212 | return false; |
102 | 213 | } |
103 | | -#ifdef __APPLE__ |
104 | | - // Match UDP.cpp: bind to ::1 so sends succeed when there is no default IPv6 multicast route. |
105 | | - return can_send_udp_datagram(IPV6_MULTICAST_ADDRESS, IPV6_MULTICAST_PROBE_PORT, "::1"); |
106 | | -#else |
107 | | - return can_send_udp_datagram(IPV6_MULTICAST_ADDRESS, IPV6_MULTICAST_PROBE_PORT); |
108 | | -#endif |
| 214 | + |
| 215 | + // Then verify multicast actually works with a real round-trip |
| 216 | + return test_multicast_roundtrip(AF_INET6, "ff02::1"); |
109 | 217 | } |
110 | 218 |
|
111 | 219 | } // namespace test_util |
0 commit comments