From 2eb5a9952569e6e9b39f3c3cca819614eafc49c4 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Sun, 11 Oct 2026 21:09:09 +0200 Subject: [PATCH] aggregation: browse mDNS from port 5353 so peers answer by multicast From an ephemeral port the PTR query sets the QU bit and peers answer by unicast to that port, which a host firewall allowing only the mdns service drops. Bind the browse socket to 5353 (shared with announce and avahi), fall back to an ephemeral port when 5353 is unavailable. --- .../src/core/aggregation/mdns_discovery.cpp | 22 ++++- .../test/test_mdns_discovery.cpp | 84 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/ros2_medkit_gateway/src/core/aggregation/mdns_discovery.cpp b/src/ros2_medkit_gateway/src/core/aggregation/mdns_discovery.cpp index 5c815f35f..aa076e9a5 100644 --- a/src/ros2_medkit_gateway/src/core/aggregation/mdns_discovery.cpp +++ b/src/ros2_medkit_gateway/src/core/aggregation/mdns_discovery.cpp @@ -513,8 +513,21 @@ void MdnsDiscovery::announce_loop() { } void MdnsDiscovery::browse_loop() { - // Open a socket on an ephemeral port for sending queries - int sock = mdns_socket_open_ipv4(nullptr); + // Query from port 5353 so peers answer by multicast to 5353. From an ephemeral + // port the query asks for unicast replies to that port, which a host firewall + // allowing only the mdns service (udp/5353) drops. The socket shares 5353 with + // our announce socket and with avahi (SO_REUSEADDR/SO_REUSEPORT). + struct sockaddr_in saddr {}; + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = INADDR_ANY; + saddr.sin_port = htons(MDNS_PORT); + + bool on_mdns_port = true; + int sock = mdns_socket_open_ipv4(&saddr); + if (sock < 0) { + on_mdns_port = false; + sock = mdns_socket_open_ipv4(nullptr); + } if (sock < 0) { if (config_.on_error) { config_.on_error( @@ -527,7 +540,10 @@ void MdnsDiscovery::browse_loop() { discovering_.store(true); if (config_.on_log) { - config_.on_log("browse_loop: socket opened on ephemeral port (fd=" + std::to_string(sock) + ")"); + config_.on_log(std::string("browse_loop: socket opened on ") + + (on_mdns_port ? "port 5353 (multicast replies)" + : "an ephemeral port (unicast replies, port 5353 unavailable)") + + " (fd=" + std::to_string(sock) + ")"); } BrowseContext ctx; diff --git a/src/ros2_medkit_gateway/test/test_mdns_discovery.cpp b/src/ros2_medkit_gateway/test/test_mdns_discovery.cpp index 13c07f15e..30dbebc77 100644 --- a/src/ros2_medkit_gateway/test/test_mdns_discovery.cpp +++ b/src/ros2_medkit_gateway/test/test_mdns_discovery.cpp @@ -14,10 +14,21 @@ #include +#include + +#include +#include #include #include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wold-style-cast" +#include "mdns.h" +#pragma GCC diagnostic pop + #include "ros2_medkit_gateway/core/aggregation/mdns_discovery.hpp" using namespace ros2_medkit_gateway; @@ -391,3 +402,76 @@ TEST(MdnsDiscovery, start_announce_on_ephemeral_port_does_not_crash) { SUCCEED(); } } + +namespace { + +struct QueryCapture { + std::string service; + std::atomic seen{false}; + std::atomic unicast{false}; +}; + +int capture_query(int /*sock*/, const struct sockaddr * /*from*/, size_t /*addrlen*/, mdns_entry_type_t entry, + uint16_t /*query_id*/, uint16_t rtype, uint16_t rclass, uint32_t /*ttl*/, const void * data, + size_t size, size_t name_offset, size_t /*name_length*/, size_t /*record_offset*/, + size_t /*record_length*/, void * user_data) { + auto * capture = static_cast(user_data); + if (entry != MDNS_ENTRYTYPE_QUESTION || rtype != MDNS_RECORDTYPE_PTR) { + return 0; + } + std::array name_buf{}; + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, name_buf.data(), name_buf.size()); + if (std::string(name.str, name.length) != capture->service) { + return 0; + } + capture->unicast.store((rclass & MDNS_UNICAST_RESPONSE) != 0); + capture->seen.store(true); + return 0; +} + +} // namespace + +// @verifies REQ_INTEROP_003 +TEST(MdnsDiscovery, browse_query_asks_for_multicast_replies) { + // A unicast-response query makes peers answer to the browser's source port, + // which a host firewall that only allows the mdns service drops. The browse + // query must leave the QU bit clear so peers answer on 5353. + struct sockaddr_in saddr {}; + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = INADDR_ANY; + saddr.sin_port = htons(MDNS_PORT); + int listener = mdns_socket_open_ipv4(&saddr); + if (listener < 0) { + GTEST_SKIP() << "cannot bind udp/5353 here"; + } + + MdnsDiscovery::Config config; + config.announce = false; + config.discover = true; + config.service = "_medkit-qu-test._tcp.local"; + MdnsDiscovery discovery(config); + discovery.start([](const std::string &, const std::string &) {}, [](const std::string &) {}); + + QueryCapture capture; + capture.service = config.service + "."; + std::array buffer{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!capture.seen.load() && std::chrono::steady_clock::now() < deadline) { + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(listener, &readfds); + struct timeval tv {}; + tv.tv_usec = 100 * 1000; + if (select(listener + 1, &readfds, nullptr, nullptr, &tv) > 0) { + mdns_socket_listen(listener, buffer.data(), buffer.size(), capture_query, &capture); + } + } + discovery.stop(); + mdns_socket_close(listener); + + if (!capture.seen.load()) { + GTEST_SKIP() << "no multicast loopback here, the browse query never arrived"; + } + EXPECT_FALSE(capture.unicast.load()); +}