ndn_discovery/scope.rs
1//! Namespace isolation — well-known prefix constants and scope enforcement.
2//!
3//! All discovery and local management traffic lives under `/ndn/local/`, which
4//! **must never be forwarded beyond the local link**. This mirrors IPv6
5//! link-local address semantics (`fe80::/10`).
6//!
7//! ## Reserved sub-namespaces
8//!
9//! | Prefix | Purpose |
10//! |--------|---------|
11//! | `/ndn/local/nd/hello` | Neighbor discovery hello Interest/Data |
12//! | `/ndn/local/nd/probe/direct` | SWIM direct liveness probe |
13//! | `/ndn/local/nd/probe/via` | SWIM indirect liveness probe |
14//! | `/ndn/local/nd/peers` | Demand-driven neighbor queries |
15//! | `/ndn/local/sd/services` | Service discovery records |
16//! | `/ndn/local/sd/updates` | Service discovery SVS sync group |
17//! | `/ndn/local/routing/lsa` | Link-state advertisements (NLSR adapter) |
18//! | `/ndn/local/routing/prefix` | Prefix announcements |
19//! | `/ndn/local/mgmt` | Management protocol |
20//!
21//! Third-party or experimental protocols must use:
22//! `/ndn/local/x/<owner-name>/v=<version>/...`
23//!
24//! ## Scope roots for service discovery
25//!
26//! | [`DiscoveryScope`](crate::config::DiscoveryScope) | Root prefix |
27//! |----------------------------------------------------|-------------|
28//! | `LinkLocal` | `/ndn/local` |
29//! | `Site` | `/ndn/site` |
30//! | `Global` | `/ndn/global`|
31
32use std::str::FromStr;
33use std::sync::OnceLock;
34
35use ndn_packet::Name;
36
37use crate::config::DiscoveryScope;
38
39// ─── Macro helper ─────────────────────────────────────────────────────────────
40
41/// Build and cache a `Name` from a string literal.
42///
43/// Well-known names are parsed once at first use and stored as `&'static Name`.
44macro_rules! cached_name {
45 ($vis:vis fn $fn:ident() -> $s:literal) => {
46 $vis fn $fn() -> &'static Name {
47 static CELL: OnceLock<Name> = OnceLock::new();
48 CELL.get_or_init(|| {
49 Name::from_str($s).expect(concat!("invalid well-known name: ", $s))
50 })
51 }
52 };
53}
54
55// ─── Link-local root ──────────────────────────────────────────────────────────
56
57cached_name!(pub fn ndn_local() -> "/ndn/local");
58
59// ─── Neighbor discovery sub-prefixes ──────────────────────────────────────────
60
61cached_name!(pub fn nd_root() -> "/ndn/local/nd");
62cached_name!(pub fn hello_prefix() -> "/ndn/local/nd/hello");
63cached_name!(pub fn probe_direct() -> "/ndn/local/nd/probe/direct");
64cached_name!(pub fn probe_via() -> "/ndn/local/nd/probe/via");
65cached_name!(pub fn peers_prefix() -> "/ndn/local/nd/peers");
66cached_name!(pub fn gossip_prefix() -> "/ndn/local/nd/gossip");
67
68// ─── Service discovery sub-prefixes ───────────────────────────────────────────
69
70cached_name!(pub fn sd_root() -> "/ndn/local/sd");
71cached_name!(pub fn sd_services() -> "/ndn/local/sd/services");
72cached_name!(pub fn sd_updates() -> "/ndn/local/sd/updates");
73
74// ─── Routing sub-prefixes ─────────────────────────────────────────────────────
75
76cached_name!(pub fn routing_lsa() -> "/ndn/local/routing/lsa");
77cached_name!(pub fn routing_prefix() -> "/ndn/local/routing/prefix");
78
79// ─── Management sub-prefix ───────────────────────────────────────────────────
80
81cached_name!(pub fn mgmt_prefix() -> "/ndn/local/mgmt");
82
83// ─── Scope roots ─────────────────────────────────────────────────────────────
84
85cached_name!(pub fn site_root() -> "/ndn/site");
86cached_name!(pub fn global_root() -> "/ndn/global");
87
88/// Return the root prefix for the given [`DiscoveryScope`].
89///
90/// - `LinkLocal` → `/ndn/local`
91/// - `Site` → `/ndn/site`
92/// - `Global` → `/ndn/global`
93pub fn scope_root(scope: &DiscoveryScope) -> &'static Name {
94 match scope {
95 DiscoveryScope::LinkLocal => ndn_local(),
96 DiscoveryScope::Site => site_root(),
97 DiscoveryScope::Global => global_root(),
98 }
99}
100
101// ─── Predicates ──────────────────────────────────────────────────────────────
102
103/// Return `true` if `name` is under `/ndn/local/` (link-local scope).
104///
105/// Any packet whose name matches this predicate **must not** be forwarded
106/// beyond the local link. The engine enforces this by dropping outbound
107/// Interest and Data packets whose name is link-local when the outbound face
108/// is not a local-scope face.
109#[inline]
110pub fn is_link_local(name: &Name) -> bool {
111 name.has_prefix(ndn_local())
112}
113
114/// Return `true` if `name` falls under the neighbor-discovery sub-tree
115/// (`/ndn/local/nd/`).
116#[inline]
117pub fn is_nd_packet(name: &Name) -> bool {
118 name.has_prefix(nd_root())
119}
120
121/// Return `true` if `name` falls under the service-discovery sub-tree
122/// (`/ndn/local/sd/`).
123#[inline]
124pub fn is_sd_packet(name: &Name) -> bool {
125 name.has_prefix(sd_root())
126}
127
128// ─── Tests ────────────────────────────────────────────────────────────────────
129
130#[cfg(test)]
131mod tests {
132 use std::str::FromStr;
133
134 use ndn_packet::Name;
135
136 use super::*;
137
138 fn n(s: &str) -> Name {
139 Name::from_str(s).unwrap()
140 }
141
142 #[test]
143 fn hello_prefix_is_link_local() {
144 assert!(is_link_local(hello_prefix()));
145 }
146
147 #[test]
148 fn nd_root_is_nd_packet() {
149 assert!(is_nd_packet(&n("/ndn/local/nd/hello/abc")));
150 assert!(!is_nd_packet(&n("/ndn/local/sd/services")));
151 }
152
153 #[test]
154 fn sd_root_is_sd_packet() {
155 assert!(is_sd_packet(&n("/ndn/local/sd/services/foo")));
156 assert!(!is_sd_packet(&n("/ndn/local/nd/hello/abc")));
157 }
158
159 #[test]
160 fn non_local_is_not_link_local() {
161 assert!(!is_link_local(&n("/ndn/edu/ucla/cs")));
162 }
163
164 #[test]
165 fn scope_root_returns_correct_prefix() {
166 assert_eq!(scope_root(&DiscoveryScope::LinkLocal), ndn_local());
167 assert_eq!(scope_root(&DiscoveryScope::Site), site_root());
168 assert_eq!(scope_root(&DiscoveryScope::Global), global_root());
169 }
170
171 #[test]
172 fn nd_and_sd_are_disjoint() {
173 // Neither is a prefix of the other.
174 assert!(!nd_root().has_prefix(sd_root()));
175 assert!(!sd_root().has_prefix(nd_root()));
176 }
177}