|
29 | 29 |
|
30 | 30 | namespace test_util { |
31 | 31 |
|
| 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 | + |
32 | 174 | bool has_ipv4_multicast() { |
33 | | - // See if any interface has multicast ipv4 |
| 175 | + // First check if any interface reports multicast support |
34 | 176 | 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) { |
36 | 178 | return iface.ip.sock.sa_family == AF_INET && iface.flags.multicast; |
37 | 179 | }); |
| 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"); |
38 | 186 | } |
39 | 187 |
|
40 | 188 | bool has_ipv6_multicast() { |
41 | | - // See if any interface has multicast ipv6 |
| 189 | + // First check if any interface reports multicast support |
42 | 190 | 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) { |
44 | 192 | return iface.ip.sock.sa_family == AF_INET6 && iface.flags.multicast; |
45 | 193 | }); |
| 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"); |
46 | 200 | } |
47 | 201 |
|
48 | 202 | } // namespace test_util |
0 commit comments