From 638a38435c87151f948a1a2e144b1ef6583c8379 Mon Sep 17 00:00:00 2001 From: Gavin John Date: Sat, 27 Jun 2026 15:47:51 -0400 Subject: [PATCH] feat: allow setting listen address explicitly --- include/lsl/outlet.h | 6 ++++++ src/lsl_outlet_c.cpp | 7 +++++++ src/socket_utils.cpp | 26 ++++++++++++++++++++++++++ src/socket_utils.h | 5 +++++ src/stream_outlet_impl.cpp | 19 +++++++++++-------- src/stream_outlet_impl.h | 8 ++++++-- src/tcp_server.cpp | 22 ++++++++++++++++++---- src/tcp_server.h | 6 +++++- 8 files changed, 84 insertions(+), 15 deletions(-) diff --git a/include/lsl/outlet.h b/include/lsl/outlet.h index 937fde47b..08b6d017c 100644 --- a/include/lsl/outlet.h +++ b/include/lsl/outlet.h @@ -40,6 +40,12 @@ extern LIBLSL_C_API lsl_outlet lsl_create_outlet(lsl_streaminfo info, int32_t ch */ extern LIBLSL_C_API lsl_outlet lsl_create_outlet_ex( lsl_streaminfo info, int32_t chunk_size, int32_t max_buffered, lsl_transport_options_t flags); +/** @copydoc lsl_create_outlet() + * @param listen_address The local IP address to bind to (e.g. "192.168.1.5"). + * Pass NULL or "" to use the default from the API config (usually all interfaces). + */ +extern LIBLSL_C_API lsl_outlet lsl_create_outlet_bound( + lsl_streaminfo info, int32_t chunk_size, int32_t max_buffered, const char *listen_address); /** * Destroy an outlet. diff --git a/src/lsl_outlet_c.cpp b/src/lsl_outlet_c.cpp index b79ba3153..8f2f8c2ce 100644 --- a/src/lsl_outlet_c.cpp +++ b/src/lsl_outlet_c.cpp @@ -25,6 +25,13 @@ LIBLSL_C_API lsl_outlet lsl_create_outlet( return lsl_create_outlet_ex(info, chunk_size, max_buffered, transp_default); } +LIBLSL_C_API lsl_outlet lsl_create_outlet_bound( + lsl_streaminfo info, int32_t chunk_size, int32_t max_buffered, const char *listen_address) { + return create_object_noexcept( + *info, chunk_size, max_buffered, transp_default, + listen_address ? std::string(listen_address) : std::string{}); +} + LIBLSL_C_API void lsl_destroy_outlet(lsl_outlet out) { try { delete out; diff --git a/src/socket_utils.cpp b/src/socket_utils.cpp index 80d9a8a56..65c63a185 100644 --- a/src/socket_utils.cpp +++ b/src/socket_utils.cpp @@ -34,3 +34,29 @@ uint16_t lsl::bind_and_listen_to_port_in_range( acc.listen(backlog); return port; } + +uint16_t lsl::bind_and_listen_to_port_in_range( + tcp_acceptor &acc, asio::ip::address addr, int backlog) { + const auto *cfg = lsl::api_config::get_instance(); + asio::error_code ec; + for (uint16_t port = cfg->base_port(), e = port + cfg->port_range(); port < e; port++) { + acc.bind(asio::ip::tcp::endpoint(addr, port), ec); + if (ec == asio::error::address_in_use) continue; + if (!ec) { + acc.listen(backlog); + return port; + } + } + if (cfg->allow_random_ports()) { + acc.bind(asio::ip::tcp::endpoint(addr, 0), ec); + if (!ec) { + acc.listen(backlog); + return acc.local_endpoint().port(); + } + } + throw std::runtime_error( + "All local ports were found occupied. You may have more open outlets on this machine " + "than your PortRange setting allows (see " + "https://labstreaminglayer.readthedocs.io/info/network-connectivity.html" + ") or you have a problem with your network configuration."); +} diff --git a/src/socket_utils.h b/src/socket_utils.h index bc7946514..71a40ac6c 100644 --- a/src/socket_utils.h +++ b/src/socket_utils.h @@ -1,6 +1,7 @@ #ifndef SOCKET_UTILS_H #define SOCKET_UTILS_H +#include #include #include @@ -19,6 +20,10 @@ uint16_t bind_port_in_range(udp_socket &sock, asio::ip::udp protocol); /// Bind and listen to an acceptor on a free port in the configured port range or throw an error. uint16_t bind_and_listen_to_port_in_range( tcp_acceptor &acc, asio::ip::tcp protocol, int backlog); + +/// Bind and listen to an acceptor on a specific local address, free port in the configured range. +uint16_t bind_and_listen_to_port_in_range( + tcp_acceptor &acc, asio::ip::address addr, int backlog); } // namespace lsl #endif diff --git a/src/stream_outlet_impl.cpp b/src/stream_outlet_impl.cpp index 62f9a38fc..24f980b2c 100644 --- a/src/stream_outlet_impl.cpp +++ b/src/stream_outlet_impl.cpp @@ -14,8 +14,9 @@ namespace lsl { stream_outlet_impl::stream_outlet_impl(const stream_info_impl &info, int32_t chunk_size, - int32_t requested_bufsize, lsl_transport_options_t flags) - : sample_factory_(std::make_shared(info.channel_format(), info.channel_count(), + int32_t requested_bufsize, lsl_transport_options_t flags, std::string listen_address) + : listen_address_(std::move(listen_address)), + sample_factory_(std::make_shared(info.channel_format(), info.channel_count(), static_cast( info.nominal_srate() ? info.nominal_srate() * api_config::get_instance()->outlet_buffer_reserve_ms() / @@ -41,21 +42,25 @@ stream_outlet_impl::stream_outlet_impl(const stream_info_impl &info, int32_t chu const api_config *cfg = api_config::get_instance(); + // resolve effective listen address: explicit override takes priority over api_config + const std::string &effective_listen = + listen_address_.empty() ? cfg->listen_address() : listen_address_; + // instantiate IPv4 and/or IPv6 stacks (depending on settings) if (cfg->allow_ipv4()) try { - instantiate_stack(udp::v4()); + instantiate_stack(udp::v4(), effective_listen); } catch (std::exception &e) { LOG_F(WARNING, "Could not instantiate IPv4 stack: %s", e.what()); } if (cfg->allow_ipv6()) try { - instantiate_stack(udp::v6()); + instantiate_stack(udp::v6(), effective_listen); } catch (std::exception &e) { LOG_F(WARNING, "Could not instantiate IPv6 stack: %s", e.what()); } // create TCP data server tcp_server_ = std::make_shared(info_, io_ctx_data_, send_buffer_, sample_factory_, - chunk_size_, cfg->allow_ipv4(), cfg->allow_ipv6(), sync_mode_); + chunk_size_, cfg->allow_ipv4(), cfg->allow_ipv6(), sync_mode_, effective_listen); // fail if both stacks failed to instantiate if (udp_servers_.empty()) @@ -82,10 +87,8 @@ stream_outlet_impl::stream_outlet_impl(const stream_info_impl &info, int32_t chu })); } -void stream_outlet_impl::instantiate_stack(udp udp_protocol) { - // get api_config +void stream_outlet_impl::instantiate_stack(udp udp_protocol, const std::string &listen_address) { const api_config *cfg = api_config::get_instance(); - std::string listen_address = cfg->listen_address(); int multicast_ttl = cfg->multicast_ttl(); uint16_t multicast_port = cfg->multicast_port(); LOG_F(2, "%s: Trying to listen at address '%s'", info().name().c_str(), listen_address.c_str()); diff --git a/src/stream_outlet_impl.h b/src/stream_outlet_impl.h index e8fb3116a..f9b948393 100644 --- a/src/stream_outlet_impl.h +++ b/src/stream_outlet_impl.h @@ -41,9 +41,11 @@ class stream_outlet_impl { * to hold a bit more than 15 minutes of data at 512Hz, while consuming not more than ca. 512MB * of RAM. Depends on `flags` as calculated in `stream_info_impl::calc_transport_buf_samples()` * @param flags Bitwise-OR'd flags from lsl_transport_options_t + * @param listen_address Local IP address to bind to; empty string means all interfaces. */ stream_outlet_impl(const stream_info_impl &info, int32_t chunk_size = 0, - int32_t requested_bufsize = 900, lsl_transport_options_t flags = transp_default); + int32_t requested_bufsize = 900, lsl_transport_options_t flags = transp_default, + std::string listen_address = {}); /** * Destructor. @@ -313,7 +315,7 @@ class stream_outlet_impl { private: /// Instantiate a new server stack. - void instantiate_stack(udp udp_protocol); + void instantiate_stack(udp udp_protocol, const std::string &listen_address); /// Allocate and enqueue a new sample into the send buffer. template void enqueue(const T *data, double timestamp, bool pushthrough); @@ -346,6 +348,8 @@ class stream_outlet_impl { "stream's number of channels."); } + /// local IP address to bind to; empty means all interfaces + std::string listen_address_; /// a factory for samples of appropriate type factory_p sample_factory_; /// the preferred chunk size diff --git a/src/tcp_server.cpp b/src/tcp_server.cpp index c4173b55b..4411c88d1 100644 --- a/src/tcp_server.cpp +++ b/src/tcp_server.cpp @@ -310,8 +310,10 @@ class client_session : public std::enable_shared_from_this { }; tcp_server::tcp_server(stream_info_impl_p info, io_context_p io, send_buffer_p sendbuf, - factory_p factory, int chunk_size, bool allow_v4, bool allow_v6, bool do_sync) - : chunk_size_(chunk_size), info_(std::move(info)), io_(std::move(io)), + factory_p factory, int chunk_size, bool allow_v4, bool allow_v6, bool do_sync, + std::string listen_address) + : listen_address_(std::move(listen_address)), chunk_size_(chunk_size), + info_(std::move(info)), io_(std::move(io)), factory_(std::move(factory)), send_buffer_(std::move(sendbuf)) { // Create sync handler if sync mode is requested if (do_sync) { @@ -333,7 +335,13 @@ tcp_server::tcp_server(stream_info_impl_p info, io_context_p io, send_buffer_p s if (allow_v4) { try { acceptor_v4_ = std::make_unique(*io_, asio::ip::tcp::v4()); - auto port = bind_and_listen_to_port_in_range(*acceptor_v4_, asio::ip::tcp::v4(), 10); + uint16_t port; + if (!listen_address_.empty()) { + auto addr = asio::ip::make_address(listen_address_); + port = bind_and_listen_to_port_in_range(*acceptor_v4_, addr, 10); + } else { + port = bind_and_listen_to_port_in_range(*acceptor_v4_, asio::ip::tcp::v4(), 10); + } info_->v4data_port(port); LOG_F(1, "Created IPv%d TCP acceptor for %s @ port %d", 4, info_->name().c_str(), port); } catch (std::exception &e) { @@ -344,7 +352,13 @@ tcp_server::tcp_server(stream_info_impl_p info, io_context_p io, send_buffer_p s if (allow_v6) { try { acceptor_v6_ = std::make_unique(*io_, asio::ip::tcp::v6()); - auto port = bind_and_listen_to_port_in_range(*acceptor_v6_, asio::ip::tcp::v6(), 10); + uint16_t port; + if (!listen_address_.empty()) { + auto addr = asio::ip::make_address(listen_address_); + port = bind_and_listen_to_port_in_range(*acceptor_v6_, addr, 10); + } else { + port = bind_and_listen_to_port_in_range(*acceptor_v6_, asio::ip::tcp::v6(), 10); + } info_->v6data_port(port); LOG_F(1, "Created IPv%d TCP acceptor for %s @ port %d", 6, info_->name().c_str(), port); } catch (std::exception &e) { diff --git a/src/tcp_server.h b/src/tcp_server.h index efe13762e..2b8a19808 100644 --- a/src/tcp_server.h +++ b/src/tcp_server.h @@ -55,7 +55,8 @@ class tcp_server : public std::enable_shared_from_this { * @param do_sync If true, use synchronous (blocking) socket writes for zero-copy transfer. */ tcp_server(stream_info_impl_p info, io_context_p io, send_buffer_p sendbuf, factory_p factory, - int chunk_size, bool allow_v4, bool allow_v6, bool do_sync = false); + int chunk_size, bool allow_v4, bool allow_v6, bool do_sync = false, + std::string listen_address = {}); /// Destructor (must be defined in .cpp due to unique_ptr to incomplete type) ~tcp_server(); @@ -106,6 +107,9 @@ class tcp_server : public std::enable_shared_from_this { /// Post a close of all in-flight sockets. void close_inflight_sessions(); + // local IP address to bind to; empty means all interfaces + std::string listen_address_; + // data used by the transfer threads int chunk_size_; // the chunk size to use (or 0)