Skip to content

Commit db23439

Browse files
simonbarenclaude
andauthored
Implement TR-10-9 Section 15 DNS-SD browse strategy (#484)
* Implement TR-10-9 Section 15 DNS-SD browse strategy TR-10-9 Section 15 requires IPMX devices to: - Support both mDNS and unicast DNS for DNS-SD browse operations - Default to using both methods - Provide user mechanisms to limit browsing to unicast or mDNS only - When using both: try unicast DNS first, fall back to mDNS only if unicast is unsuccessful (no service discovered) - When multiple results are returned: select by best priority, failing over to next-best if unresponsive - Once a service is selected and responsive: perform no further browse operations for that service type - If the selected service becomes unresponsive: perform a new DNS-SD browse and restart selection Add dns_sd_browse_mode setting ("both"/"unicast"/"mdns") to implement the required dual-discovery strategy. In "both" mode (default), unicast DNS is tried first with half the timeout budget; if no records are found, mDNS fallback gets the remaining half. "unicast" and "mdns" modes restrict to a single method. Also filters browse results by domain class to prevent mDNS results leaking into unicast DNS queries and vice versa. Signed-off-by: Semyon Barenboym <simonbaren@gmail.com> * Address review feedback on TR-10-9 DNS-SD browse strategy - Use string_enum for dns_sd_browse_mode (nmos::dns_sd_browse_modes::{both,unicast,mdns}) - Add dns_sd_browse_mode example + behaviour table to nmos-cpp-node config.json - Add expected-behaviour table to settings.h - Mark unused URI-returning resolve_service overload as deprecated (comment) - Refresh resolve_service / resolve_service_ doc comments in mdns.h/.cpp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use full discovery timeout for both primary and fallback browse Per maintainer review (jonathan-r-thorpe, lo-simon): the primary service timeout should remain unchanged when a fallback is in play, with the same timeout applied per service rather than halved across them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Switch dns_sd_browse_mode from string to integer enum Per lo-simon's review: use a typed enum (compile-checked) rather than a string to reduce typo risk in user configurations. - enum dns_sd_browse_mode { dns_sd_browse_mode_both = 0, _unicast = 1, _mdns = 2 } defined file-local in mdns.cpp under nmos::experimental, mirroring how href_mode lives in settings.cpp - field becomes field_as_integer_or with default 0 - example config.json now shows //"dns_sd_browse_mode": 0, - doc comments updated; dns_sd_browse_mode.h header removed Used dns_sd_browse_mode_* prefix on the enumerators (rather than the bare both/unicast/mdns suggested in the review) to avoid polluting nmos:: with generic identifiers, matching the href_mode_* convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Semyon Barenboym <simonbaren@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e978104 commit db23439

4 files changed

Lines changed: 117 additions & 32 deletions

File tree

Development/nmos-cpp-node/config.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@
8585
// domain [registry, node]: the domain on which to browse for services or an empty string to use the default domain (specify "local." to explictly select mDNS)
8686
//"domain": "",
8787

88+
// dns_sd_browse_mode [node]: DNS-SD browse method per TR-10-9 Section 15
89+
// both(0) (default) = unicast DNS first, mDNS fallback if unsuccessful
90+
// unicast(1) = unicast DNS only
91+
// mdns(2) = mDNS only
92+
// Expected resolve behaviour for each (mode, domain) combination:
93+
// mode | domain | behaviour
94+
// --------+-------------+--------------------
95+
// both | example.com | unicast -> mdns
96+
// both | local. | mdns
97+
// unicast | example.com | unicast
98+
// unicast | local. | mdns
99+
// mdns | example.com | mdns
100+
// mdns | local. | mdns
101+
//"dns_sd_browse_mode": 0,
102+
88103
// host_address/host_addresses [registry, node]: IP addresses used to construct response headers (e.g. 'Link' or 'Location'), and host and URL fields in the data model
89104
//"host_address": "127.0.0.1",
90105
//"host_addresses": array-of-ip-address-strings,

Development/nmos/mdns.cpp

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ namespace nmos
299299
const service_type register_{ "_nmos-register._tcp" };
300300
}
301301

302+
// DNS-SD browse method per TR-10-9 Section 15; selected via the dns_sd_browse_mode setting
303+
enum dns_sd_browse_mode
304+
{
305+
dns_sd_browse_mode_both = 0, // unicast DNS first, mDNS fallback if unsuccessful
306+
dns_sd_browse_mode_unicast = 1, // unicast DNS only
307+
dns_sd_browse_mode_mdns = 2 // mDNS only
308+
};
309+
302310
namespace experimental
303311
{
304312
namespace details
@@ -538,6 +546,16 @@ namespace nmos
538546
{
539547
return discovery.browse([=, &discovery](const mdns::browse_result& resolving)
540548
{
549+
// Skip results from a different discovery domain class
550+
// (prevents mDNS results leaking into unicast DNS queries and vice versa)
551+
if (!browse_domain.empty())
552+
{
553+
if (is_local_domain(browse_domain) != is_local_domain(resolving.domain))
554+
{
555+
return true; // skip this result, keep browsing
556+
}
557+
}
558+
541559
const bool cancel = pplx::canceled == discovery.resolve([=](const mdns::resolve_result& resolved)
542560
{
543561
// "The Node [filters] out any APIs which do not support its required API version, protocol and authorization mode (TXT api_ver, api_proto and api_auth)."
@@ -625,8 +643,9 @@ namespace nmos
625643
}
626644
}
627645

628-
// helper function for resolving instances of the specified service (API)
629-
// with the highest version, highest priority instances at the front, and optionally services with the same priority ordered randomly
646+
// Helper function for resolving instances of the specified service (API), returning ((api_version, priority), uri)
647+
// tuples so callers can inspect the matched version and priority. Highest version and highest priority first,
648+
// and optionally services with the same priority ordered randomly.
630649
pplx::task<std::list<resolved_service>> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token)
631650
{
632651
const auto absolute_timeout = std::chrono::steady_clock::now() + timeout;
@@ -731,8 +750,10 @@ namespace nmos
731750
});
732751
}
733752

734-
// helper function for resolving instances of the specified service (API)
735-
// with the highest version, highest priority instances at the front, and optionally services with the same priority ordered randomly
753+
// DEPRECATED: this overload is unused; prefer resolve_service_ which also returns api_ver/priority info.
754+
// Helper function for resolving instances of the specified service (API), returning a list of base URIs
755+
// (with the API version path appended), highest version and highest priority first, and optionally
756+
// services with the same priority ordered randomly.
736757
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token)
737758
{
738759
return resolve_service_(discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, token).then([](std::list<resolved_service> resolved_services)
@@ -745,28 +766,33 @@ namespace nmos
745766
});
746767
}
747768

748-
// helper function for resolving instances of the specified service (API) based on the specified settings
749-
// with the highest version, highest priority instances at the front, and services with the same priority ordered randomly
769+
// Helper function for resolving instances of the specified service (API) based on the specified settings,
770+
// returning a list of base URIs (with the API version path appended), highest version and highest priority
771+
// first, services with the same priority ordered randomly.
772+
// The browse method is selected by the dns_sd_browse_mode setting per TR-10-9 Section 15
773+
// (delegates to resolve_service_, which carries the dual-discovery logic).
750774
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token)
751775
{
752-
const auto browse_domain = utility::us2s(nmos::get_domain(settings));
753-
const auto versions = details::service_versions(service, settings);
754-
const auto priorities = details::service_priorities(service, settings);
755-
const auto protocols = std::set<nmos::service_protocol>{ nmos::get_service_protocol(service, settings) };
756-
const auto authorization = std::set<bool>{ nmos::get_service_authorization(service, settings) };
757-
758-
// use a short timeout that's long enough to ensure the daemon's cache is exhausted
759-
// when no cancellation token is specified
760-
const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1;
761-
762-
return resolve_service(discovery, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::seconds(timeout)), token);
776+
return resolve_service_(discovery, service, settings, token).then([](std::list<resolved_service> resolved_services)
777+
{
778+
return boost::copy_range<std::list<web::uri>>(resolved_services | boost::adaptors::transformed([](const resolved_service& s)
779+
{
780+
return web::uri_builder(s.second).append_path(U("/") + make_api_version(s.first.first)).to_uri();
781+
}));
782+
});
763783
}
764784

765-
// helper function for resolving instances of the specified service (API) based on the specified settings
766-
// with the highest version, highest priority instances at the front, and services with the same priority ordered randomly
785+
// Helper function for resolving instances of the specified service (API) based on the specified settings,
786+
// returning ((api_version, priority), uri) tuples. Highest version and highest priority first, services
787+
// with the same priority ordered randomly.
788+
// The browse method is selected by the dns_sd_browse_mode setting per TR-10-9 Section 15:
789+
// - both (default): unicast DNS first, mDNS fallback if unsuccessful
790+
// - unicast : unicast DNS only
791+
// - mdns : mDNS only
767792
pplx::task<std::list<resolved_service>> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token)
768793
{
769794
const auto browse_domain = utility::us2s(nmos::get_domain(settings));
795+
const auto browse_mode = dns_sd_browse_mode(nmos::fields::dns_sd_browse_mode(settings));
770796
const auto versions = details::service_versions(service, settings);
771797
const auto priorities = details::service_priorities(service, settings);
772798
const auto protocols = std::set<nmos::service_protocol>{ nmos::get_service_protocol(service, settings) };
@@ -775,8 +801,26 @@ namespace nmos
775801
// use a short timeout that's long enough to ensure the daemon's cache is exhausted
776802
// when no cancellation token is specified
777803
const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1;
804+
const auto timeout_dur = std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::seconds(timeout));
805+
806+
// determine primary browse domain based on mode
807+
const auto primary_domain = (browse_mode == dns_sd_browse_mode_mdns) ? std::string("local.") : browse_domain;
808+
const bool has_fallback = (browse_mode == dns_sd_browse_mode_both) && !is_local_domain(browse_domain);
809+
810+
auto primary_task = resolve_service_(discovery, service, primary_domain, versions, priorities, protocols, authorization, true, timeout_dur, token);
811+
812+
if (has_fallback)
813+
{
814+
return primary_task.then([&discovery, service, versions, priorities, protocols, authorization, timeout_dur, token](std::list<resolved_service> results)
815+
{
816+
if (!results.empty()) return pplx::task_from_result(std::move(results));
817+
818+
// TR-10-9: unicast DNS unsuccessful, fall back to mDNS (full timeout per service)
819+
return resolve_service_(discovery, service, std::string("local."), versions, priorities, protocols, authorization, true, timeout_dur, token);
820+
});
821+
}
778822

779-
return resolve_service_(discovery, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::seconds(timeout)), token);
823+
return primary_task;
780824
}
781825
}
782826
}

Development/nmos/mdns.h

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,39 +160,50 @@ namespace nmos
160160
// helper function for updating the specified service (API) TXT records
161161
void update_service(mdns::service_advertiser& advertiser, const nmos::service_type& service, const nmos::settings& settings, mdns::structured_txt_records add_records = {});
162162

163-
// helper function for resolving instances of the specified service (API)
164-
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
163+
// DEPRECATED: this overload is unused; prefer resolve_service_ which also returns api_ver/priority info.
164+
// Helper function for resolving instances of the specified service (API), returning a list of base URIs
165+
// (with the API version path appended), highest version and highest priority first, and (by default)
166+
// services with the same priority ordered randomly.
165167
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none());
166168

167-
// helper function for resolving instances of the specified service (API) based on the specified options or defaults
168-
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
169+
// DEPRECATED: convenience wrapper around the deprecated explicit-args resolve_service overload above.
170+
// Same behaviour, with default values for unspecified arguments.
169171
template <typename Rep = std::chrono::seconds::rep, typename Period = std::chrono::seconds::period>
170172
inline pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain = {}, const std::set<nmos::api_version>& api_ver = nmos::is04_versions::all, const std::pair<nmos::service_priority, nmos::service_priority>& priorities = { service_priorities::highest_active_priority, service_priorities::no_priority }, const std::set<nmos::service_protocol>& api_proto = nmos::service_protocols::all, const std::set<bool>& api_auth = { false, true }, bool randomize = true, const std::chrono::duration<Rep, Period>& timeout = std::chrono::seconds(mdns::default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none())
171173
{
172174
return resolve_service(discovery, service, browse_domain, api_ver, api_proto, api_auth, randomize, std::chrono::duration_cast<std::chrono::steady_clock::duration>(timeout), token);
173175
}
174176

175-
// helper function for resolving instances of the specified service (API) based on the specified settings
176-
// with the highest version, highest priority instances at the front, and services with the same priority ordered randomly
177+
// Helper function for resolving instances of the specified service (API) based on the specified settings,
178+
// returning a list of base URIs (with the API version path appended), highest version and highest priority
179+
// first, services with the same priority ordered randomly.
180+
// The browse method is selected by the dns_sd_browse_mode setting per TR-10-9 Section 15
181+
// (delegates to resolve_service_, which carries the dual-discovery logic).
177182
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none());
178183

179184
typedef std::pair<api_version, service_priority> api_ver_pri;
180185
typedef std::pair<api_ver_pri, web::uri> resolved_service;
181186

182-
// helper function for resolving instances of the specified service (API)
183-
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
187+
// Helper function for resolving instances of the specified service (API), returning ((api_version, priority), uri)
188+
// tuples so callers can inspect the matched version and priority. Highest version and highest priority first,
189+
// and (by default) services with the same priority ordered randomly.
184190
pplx::task<std::list<resolved_service>> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none());
185191

186-
// helper function for resolving instances of the specified service (API) based on the specified options or defaults
187-
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
192+
// Convenience wrapper around the explicit-args resolve_service_ overload above, with default values
193+
// for unspecified arguments.
188194
template <typename Rep = std::chrono::seconds::rep, typename Period = std::chrono::seconds::period>
189195
inline pplx::task<std::list<resolved_service>> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain = {}, const std::set<nmos::api_version>& api_ver = nmos::is04_versions::all, const std::pair<nmos::service_priority, nmos::service_priority>& priorities = { service_priorities::highest_active_priority, service_priorities::no_priority }, const std::set<nmos::service_protocol>& api_proto = nmos::service_protocols::all, const std::set<bool>& api_auth = { false, true }, bool randomize = true, const std::chrono::duration<Rep, Period>& timeout = std::chrono::seconds(mdns::default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none())
190196
{
191197
return resolve_service_(discovery, service, browse_domain, api_ver, api_proto, api_auth, randomize, std::chrono::duration_cast<std::chrono::steady_clock::duration>(timeout), token);
192198
}
193199

194-
// helper function for resolving instances of the specified service (API) based on the specified settings
195-
// with the highest version, highest priority instances at the front, and services with the same priority ordered randomly
200+
// Helper function for resolving instances of the specified service (API) based on the specified settings,
201+
// returning ((api_version, priority), uri) tuples. Highest version and highest priority first, services
202+
// with the same priority ordered randomly.
203+
// The browse method is selected by the dns_sd_browse_mode setting per TR-10-9 Section 15:
204+
// - both (default): unicast DNS first, mDNS fallback if unsuccessful
205+
// - unicast : unicast DNS only
206+
// - mdns : mDNS only
196207
pplx::task<std::list<resolved_service>> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none());
197208
}
198209
}

Development/nmos/settings.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ namespace nmos
8282
// domain [registry, node]: the domain on which to browse for services or an empty string to use the default domain (specify "local." to explictly select mDNS)
8383
const web::json::field_as_string_or domain{ U("domain"), U("") };
8484

85+
// dns_sd_browse_mode [node]: DNS-SD browse method per TR-10-9 Section 15
86+
// both(0) (default) = unicast DNS first, mDNS fallback if unsuccessful
87+
// unicast(1) = unicast DNS only
88+
// mdns(2) = mDNS only
89+
// Expected resolve behaviour for each (mode, domain) combination:
90+
// mode | domain | behaviour
91+
// --------+-------------+--------------------
92+
// both | example.com | unicast -> mdns
93+
// both | local. | mdns
94+
// unicast | example.com | unicast
95+
// unicast | local. | mdns
96+
// mdns | example.com | mdns
97+
// mdns | local. | mdns
98+
const web::json::field_as_integer_or dns_sd_browse_mode{ U("dns_sd_browse_mode"), 0 };
99+
85100
// host_address/host_addresses [registry, node]: IP addresses used to construct response headers (e.g. 'Link' or 'Location'), and host and URL fields in the data model
86101
const web::json::field_as_string_or host_address{ U("host_address"), U("127.0.0.1") };
87102
const web::json::field_as_array host_addresses{ U("host_addresses") };

0 commit comments

Comments
 (0)