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}