Skip to content
Open
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
12 changes: 12 additions & 0 deletions src/api_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ void api_config::load(INI &pt) {
multicast_addresses_.push_back(addr);
}

// Parse and store the machine-local (loopback/unicast) addresses separately. They are also
// part of multicast_addresses_, but because a unicast datagram to the shared multicast_port is
// delivered to only one of the sockets bound there, the resolver must additionally probe these
// across the per-stream service-port range to reach every local stream (see resolver_impl).
for (auto &it : machine_group) {
try {
ip::address addr = ip::make_address(it);
if ((addr.is_v4() && allow_ipv4_) || (addr.is_v6() && allow_ipv6_))
machine_addresses_.push_back(addr);
} catch (std::exception &) {}
}

// The network stack requires the source interfaces for multicast packets to be
// specified as IPv4 address or an IPv6 interface index
// Try getting the interfaces from the configuration files
Expand Down
13 changes: 13 additions & 0 deletions src/api_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ class api_config {
*/
const std::vector<ip::address> &multicast_addresses() const { return multicast_addresses_; }

/**
* @brief The machine-local (loopback/unicast) addresses from multicast.MachineAddresses.
*
* These are a subset of multicast_addresses() but, being unicast, a datagram sent to them
* at the shared multicast_port is delivered to only one of the responder sockets bound there
* (the "unicast lottery" — only one local stream answers, depending on bind order). The
* resolver therefore additionally probes these addresses across the per-stream service-port
* range [base_port, base_port+port_range), where each stream owns a unique socket, so every
* local stream is discoverable regardless of bind order.
*/
const std::vector<ip::address> &machine_addresses() const { return machine_addresses_; }

/**
* @brief The address of the local interface on which to listen to multicast traffic.
*
Expand Down Expand Up @@ -277,6 +289,7 @@ class api_config {
uint16_t multicast_port_;
std::string resolve_scope_;
std::vector<ip::address> multicast_addresses_;
std::vector<ip::address> machine_addresses_;
int multicast_ttl_;
std::string listen_address_;
std::vector<std::string> known_peers_;
Expand Down
22 changes: 22 additions & 0 deletions src/resolver_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <asio/io_context.hpp>
#include <asio/ip/basic_resolver.hpp>
#include <asio/ip/udp.hpp>
#include <algorithm>
#include <exception>
#include <loguru.hpp>
#include <memory>
Expand Down Expand Up @@ -46,6 +47,27 @@ resolver_impl::resolver_impl()
} catch (std::exception &) {}
}

// Machine-local addresses (e.g. 127.0.0.1) are multicast targets too, but a unicast datagram
// to the shared multicast_port is delivered to only one of the responder sockets bound there
// (the "unicast lottery"): only one local stream answers, and which one depends on bind order.
// Probe them across the per-stream service-port range as well, where every stream owns a unique
// socket, so all local streams are discoverable regardless of bind order.
for (const auto &addr : cfg_->machine_addresses()) {
for (int p = cfg_->base_port(); p < cfg_->base_port() + cfg_->port_range(); p++)
ucast_endpoints_.emplace_back(addr, p);
}

// The same endpoint can be enqueued more than once (e.g. a machine address that also appears in
// KnownPeers, or a repeated config entry). Drop duplicates so we don't send identical queries to
// the same socket. This is distinct from the UID-based dedup of *results*: two different
// endpoints can still return the same stream (e.g. its multicast_port and its service port).
auto dedupe = [](std::vector<udp::endpoint> &eps) {
std::sort(eps.begin(), eps.end());
eps.erase(std::unique(eps.begin(), eps.end()), eps.end());
};
dedupe(mcast_endpoints_);
dedupe(ucast_endpoints_);

// generate the list of protocols to use
if (cfg_->allow_ipv6()) {
udp_protocols_.push_back(udp::v6());
Expand Down
13 changes: 12 additions & 1 deletion testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ add_executable(lsl_test_runtime_config int/runtime_config.cpp)
target_link_libraries(lsl_test_runtime_config PRIVATE lslobj lslboost common catch_main)
target_compile_definitions(lsl_test_runtime_config PRIVATE LIBLSL_EXPORTS)

# resolve_machine test sets the api_config singleton, so it lives in its own executable.
add_executable(lsl_test_resolve_machine int/resolve_machine.cpp)
target_link_libraries(lsl_test_resolve_machine PRIVATE lslobj lslboost common catch_main)
target_compile_definitions(lsl_test_resolve_machine PRIVATE LIBLSL_EXPORTS)
# Needs pugixml headers (resolver_impl.h -> stream_info_impl.h includes pugixml)
if(LSL_PUGIXML_IS_FETCHED)
target_include_directories(lsl_test_resolve_machine PRIVATE ${pugixml_SOURCE_DIR}/src)
else()
target_link_libraries(lsl_test_resolve_machine PRIVATE pugixml::pugixml)
endif()

if(LSL_BENCHMARKS)
# to get somewhat reproducible performance numbers:
# /usr/bin/time -v testing/lsl_test_exported --benchmark-samples 100 bounce
Expand All @@ -122,7 +133,7 @@ if(LSL_BENCHMARKS)
)
endif()

set(LSL_TESTS lsl_test_exported lsl_test_internal lsl_test_runtime_config)
set(LSL_TESTS lsl_test_exported lsl_test_internal lsl_test_runtime_config lsl_test_resolve_machine)
foreach(lsltest ${LSL_TESTS})
add_test(NAME ${lsltest} COMMAND ${lsltest} --wait-for-keypress never)
if(LSL_INSTALL)
Expand Down
45 changes: 45 additions & 0 deletions testing/int/resolve_machine.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "api_config.h"
#include <catch2/catch_test_macros.hpp>
#include <lsl_cpp.h>
#include <string>
#include <vector>

// Regression test for the same-machine "unicast lottery" (only one local stream resolves).
//
// With ResolveScope=machine the only discovery target is the machine address (127.0.0.1) at the
// shared multicast_port. That port is unicast, not multicast, so a query datagram is delivered to
// exactly one of the responder sockets bound there — only one outlet answers, and which one
// depends on bind order. Several outlets are therefore created, but pre-fix only one is found.
//
// The fix additionally probes the machine addresses across the per-stream service-port range,
// where every outlet owns a unique socket, so all of them are discoverable.
//
// Sets the api_config singleton, so (like runtime_config) it lives in its own executable.

TEST_CASE("all same-machine streams resolve under ResolveScope=machine", "[resolver][machine]") {
lsl::api_config::set_api_config_content(
"[ports]\n"
"IPv6 = disable\n"
"[multicast]\n"
"ResolveScope = machine\n"
"MachineAddresses = {127.0.0.1}\n"
"[lab]\n"
"SessionID = resolve_machine_test\n"
"KnownPeers = {}\n"); // no KnownPeers: discovery relies solely on the machine address

// Stand up several outlets on this machine. Each binds its own per-stream service port.
constexpr int n = 3;
std::vector<lsl::stream_outlet> outlets;
outlets.reserve(n);
for (int i = 0; i < n; i++) {
lsl::stream_info info("machtest" + std::to_string(i), "machtest", 1, lsl::IRREGULAR_RATE,
lsl::cf_float32, "machsrc" + std::to_string(i));
outlets.emplace_back(info);
}

// Resolve by the shared type. Pre-fix the unicast lottery yields fewer than n; the fix probes
// each outlet's unique service port, so all n are found.
std::vector<lsl::stream_info> found = lsl::resolve_stream("type", "machtest", n, 5.0);

CHECK(found.size() == n);
}
Loading