ndn_discovery/config.rs
1//! Discovery configuration — deployment profiles and per-parameter tuning.
2//!
3//! [`DiscoveryProfile`] captures deployment intent; [`DiscoveryConfig`] holds
4//! the concrete numeric parameters. Callers pick a profile and optionally
5//! override individual fields.
6
7use std::time::Duration;
8
9use ndn_packet::Name;
10
11// ─── HelloStrategyKind ────────────────────────────────────────────────────────
12
13/// Which probe-scheduling algorithm a discovery protocol builds when
14/// constructed from a [`DiscoveryConfig`].
15///
16/// This controls *when* hellos are sent; the state machine (face creation,
17/// FIB wiring, neighbor table) is independent and stays in the protocol impl.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum HelloStrategyKind {
20 /// Exponential backoff with jitter. Default for most deployments.
21 Backoff,
22 /// Event-driven only — no timer. Sends hellos only on topology events
23 /// (face up, forwarding failure, neighbor going stale).
24 Reactive,
25 /// Passive MAC overhearing. Sends hellos only when unknown source MACs
26 /// are observed; falls back to occasional backoff probing when quiet.
27 Passive,
28 /// SWIM-style direct + indirect probing. Not yet fully implemented;
29 /// falls back to [`Backoff`](Self::Backoff) until `strategy/swim.rs` is complete.
30 Swim,
31}
32
33// ─── PrefixAnnouncementMode ───────────────────────────────────────────────────
34
35/// How this node announces its own prefixes to neighbours.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum PrefixAnnouncementMode {
38 /// Static configuration only; no automatic announcements.
39 Static,
40 /// Prefix list carried in the `SERVED-PREFIX` fields of every Hello Data.
41 InHello,
42 /// LSA-style routing (NLSR adapter, future work).
43 NlsrLsa,
44}
45
46// ─── DiscoveryConfig ──────────────────────────────────────────────────────────
47
48/// Concrete discovery parameters.
49///
50/// Obtain via [`DiscoveryConfig::for_profile`] and adjust as needed,
51/// or construct from scratch for fully custom deployments.
52#[derive(Clone, Debug)]
53pub struct DiscoveryConfig {
54 /// Probe-scheduling algorithm.
55 pub hello_strategy: HelloStrategyKind,
56 /// Initial hello interval (fast bootstrap).
57 pub hello_interval_base: Duration,
58 /// Maximum hello interval after full exponential backoff.
59 pub hello_interval_max: Duration,
60 /// Fractional jitter applied to each hello interval (0.0–0.5).
61 /// `0.25` means ±25 % of the current interval is added as random noise.
62 pub hello_jitter: f32,
63 /// How long without a hello response before `Established → Stale`.
64 pub liveness_timeout: Duration,
65 /// Consecutive missed hellos before `Stale → Absent` (face/FIB removal).
66 pub liveness_miss_count: u32,
67 /// How long to wait for a hello response before declaring a probe lost.
68 pub probe_timeout: Duration,
69 /// SWIM indirect-probe fanout K (0 = SWIM disabled).
70 pub swim_indirect_fanout: u32,
71 /// Emergency gossip-broadcast fanout K (0 = disabled).
72 /// When a neighbor goes Stale, K unicast hellos are sent to other
73 /// established peers so they can independently verify the failure.
74 pub gossip_fanout: u32,
75 /// Prefix announcement mode.
76 pub prefix_announcement: PrefixAnnouncementMode,
77 /// Automatically create unicast faces for discovered peers.
78 pub auto_create_faces: bool,
79 /// How often the engine calls `DiscoveryProtocol::on_tick`.
80 /// Smaller values improve responsiveness at the cost of CPU overhead.
81 /// Default: 100 ms.
82 pub tick_interval: Duration,
83}
84
85impl DiscoveryConfig {
86 /// Build the default config for the given deployment profile.
87 pub fn for_profile(profile: &DiscoveryProfile) -> Self {
88 match profile {
89 DiscoveryProfile::Static => Self::static_routes(),
90 DiscoveryProfile::Lan => Self::lan(),
91 DiscoveryProfile::Campus => Self::campus(),
92 DiscoveryProfile::Mobile => Self::mobile(),
93 DiscoveryProfile::HighMobility => Self::high_mobility(),
94 DiscoveryProfile::Asymmetric => Self::asymmetric(),
95 DiscoveryProfile::Custom(c) => c.clone(),
96 }
97 }
98
99 /// Static routing — no hello traffic at all.
100 fn static_routes() -> Self {
101 Self {
102 hello_strategy: HelloStrategyKind::Backoff,
103 hello_interval_base: Duration::from_secs(3600),
104 hello_interval_max: Duration::from_secs(3600),
105 hello_jitter: 0.0,
106 liveness_timeout: Duration::MAX,
107 liveness_miss_count: u32::MAX,
108 probe_timeout: Duration::from_secs(5),
109 swim_indirect_fanout: 0,
110 gossip_fanout: 0,
111 prefix_announcement: PrefixAnnouncementMode::Static,
112 auto_create_faces: false,
113 tick_interval: Duration::from_secs(1),
114 }
115 }
116
117 /// Link-local LAN: stable topology, low overhead.
118 ///
119 /// # Liveness invariant
120 ///
121 /// `liveness_timeout` (30 s) must exceed `hello_interval_max × (1 + jitter)`
122 /// so that a healthy peer at full backoff never triggers a false Stale
123 /// transition. With `hello_interval_max = 20 s` and `jitter = 0.25`:
124 /// `20 × 1.25 = 25 s < 30 s` ✓
125 ///
126 /// Failure detection: `liveness_timeout × liveness_miss_count = 90 s`
127 /// from the last received hello.
128 fn lan() -> Self {
129 Self {
130 hello_strategy: HelloStrategyKind::Backoff,
131 hello_interval_base: Duration::from_secs(5),
132 hello_interval_max: Duration::from_secs(20),
133 hello_jitter: 0.25,
134 liveness_timeout: Duration::from_secs(30),
135 liveness_miss_count: 3,
136 probe_timeout: Duration::from_secs(5),
137 swim_indirect_fanout: 0,
138 gossip_fanout: 0,
139 prefix_announcement: PrefixAnnouncementMode::InHello,
140 auto_create_faces: true,
141 tick_interval: Duration::from_millis(500),
142 }
143 }
144
145 /// Campus / enterprise: mix of stable and dynamic peers.
146 ///
147 /// # Liveness invariant
148 ///
149 /// `liveness_timeout` (120 s) must exceed `hello_interval_max × (1 + jitter)`.
150 /// With `hello_interval_max = 100 s` and `jitter = 0.10`:
151 /// `100 × 1.10 = 110 s < 120 s` ✓
152 ///
153 /// Failure detection: `120 s × 3 = 360 s` (~6 min).
154 fn campus() -> Self {
155 Self {
156 hello_strategy: HelloStrategyKind::Backoff,
157 hello_interval_base: Duration::from_secs(30),
158 hello_interval_max: Duration::from_secs(100),
159 hello_jitter: 0.10,
160 liveness_timeout: Duration::from_secs(120),
161 liveness_miss_count: 3,
162 probe_timeout: Duration::from_secs(10),
163 swim_indirect_fanout: 3,
164 gossip_fanout: 3,
165 prefix_announcement: PrefixAnnouncementMode::NlsrLsa,
166 auto_create_faces: true,
167 tick_interval: Duration::from_millis(500),
168 }
169 }
170
171 /// Mobile / vehicular: topology changes at human-movement timescales.
172 ///
173 /// # Liveness invariant
174 ///
175 /// `liveness_timeout` (3 s) must exceed `hello_interval_max × (1 + jitter)`.
176 /// With `hello_interval_max = 2 s` and `jitter = 0.15`:
177 /// `2 × 1.15 = 2.3 s < 3 s` ✓
178 ///
179 /// Failure detection: `3 s × 5 = 15 s`.
180 fn mobile() -> Self {
181 Self {
182 hello_strategy: HelloStrategyKind::Reactive,
183 hello_interval_base: Duration::from_millis(200),
184 hello_interval_max: Duration::from_secs(2),
185 hello_jitter: 0.15,
186 liveness_timeout: Duration::from_secs(3),
187 liveness_miss_count: 5,
188 probe_timeout: Duration::from_millis(500),
189 swim_indirect_fanout: 3,
190 gossip_fanout: 5,
191 prefix_announcement: PrefixAnnouncementMode::InHello,
192 auto_create_faces: true,
193 tick_interval: Duration::from_millis(50),
194 }
195 }
196
197 /// High-mobility (drones, V2X): sub-second topology changes.
198 ///
199 /// # Liveness invariant
200 ///
201 /// `liveness_timeout` (750 ms) must exceed `hello_interval_max × (1 + jitter)`.
202 /// With `hello_interval_max = 500 ms` and `jitter = 0.10`:
203 /// `500 × 1.10 = 550 ms < 750 ms` ✓
204 ///
205 /// Failure detection: `750 ms × 3 = 2.25 s`.
206 fn high_mobility() -> Self {
207 Self {
208 hello_strategy: HelloStrategyKind::Passive,
209 hello_interval_base: Duration::from_millis(50),
210 hello_interval_max: Duration::from_millis(500),
211 hello_jitter: 0.10,
212 liveness_timeout: Duration::from_millis(750),
213 liveness_miss_count: 3,
214 probe_timeout: Duration::from_millis(200),
215 swim_indirect_fanout: 5,
216 gossip_fanout: 5,
217 prefix_announcement: PrefixAnnouncementMode::InHello,
218 auto_create_faces: true,
219 tick_interval: Duration::from_millis(20),
220 }
221 }
222
223 /// Asymmetric / unidirectional link (Wifibroadcast, satellite downlink).
224 fn asymmetric() -> Self {
225 Self {
226 hello_strategy: HelloStrategyKind::Passive,
227 hello_interval_base: Duration::from_secs(5),
228 hello_interval_max: Duration::from_secs(30),
229 hello_jitter: 0.10,
230 liveness_timeout: Duration::from_secs(60),
231 liveness_miss_count: 3,
232 probe_timeout: Duration::from_secs(10),
233 swim_indirect_fanout: 0,
234 gossip_fanout: 0,
235 prefix_announcement: PrefixAnnouncementMode::Static,
236 auto_create_faces: false,
237 tick_interval: Duration::from_millis(500),
238 }
239 }
240}
241
242impl Default for DiscoveryConfig {
243 fn default() -> Self {
244 Self::lan()
245 }
246}
247
248// ─── DiscoveryProfile ─────────────────────────────────────────────────────────
249
250/// High-level deployment profiles mapping to tuned [`DiscoveryConfig`] sets.
251#[derive(Clone, Debug, Default)]
252pub enum DiscoveryProfile {
253 /// No discovery. FIB and faces configured statically.
254 Static,
255 /// Link-local LAN (home, small office).
256 #[default]
257 Lan,
258 /// Campus or enterprise network.
259 Campus,
260 /// Mobile / vehicular network.
261 Mobile,
262 /// High-mobility (drones, V2X).
263 HighMobility,
264 /// Asymmetric unidirectional link (Wifibroadcast, satellite downlink).
265 Asymmetric,
266 /// Fully custom parameters.
267 Custom(DiscoveryConfig),
268}
269
270// ─── ServiceDiscoveryConfig ───────────────────────────────────────────────────
271
272/// Scope at which service records are consumed or propagated.
273#[derive(Clone, Debug, PartialEq, Eq)]
274pub enum DiscoveryScope {
275 /// `/ndn/local/` — never forwarded beyond the local link.
276 LinkLocal,
277 /// `/ndn/site/` — distributed within an administrative domain.
278 Site,
279 /// `/ndn/global/` — federated global registry.
280 Global,
281}
282
283/// Validation policy for incoming service records.
284#[derive(Clone, Debug, PartialEq, Eq)]
285pub enum ServiceValidationPolicy {
286 /// No validation. Accept any record. Fast; for closed networks.
287 Skip,
288 /// Log unsigned/unverified records but act on them anyway.
289 WarnOnly,
290 /// Drop unsigned records; only auto-populate FIB from verified Data.
291 Required,
292}
293
294/// Configuration for the service-discovery layer (`/ndn/local/sd/`).
295#[derive(Clone, Debug)]
296pub struct ServiceDiscoveryConfig {
297 /// Automatically add FIB entries when service records arrive.
298 pub auto_populate_fib: bool,
299 /// Restrict auto-population to this scope.
300 pub auto_populate_scope: DiscoveryScope,
301 /// Route cost for auto-populated FIB entries (should exceed manual routes).
302 pub auto_fib_cost: u32,
303 /// Auto-populated entries expire after `freshness_period × multiplier`.
304 pub auto_fib_ttl_multiplier: f32,
305 /// Only auto-populate for these prefixes (empty = accept any).
306 pub auto_populate_prefix_filter: Vec<Name>,
307 /// Maximum service records per scope prefix.
308 pub max_records_per_scope: usize,
309 /// Max registrations per producer per time window (rate limiting).
310 pub max_registrations_per_producer: u32,
311 /// Time window for the per-producer rate limit.
312 pub max_registrations_window: Duration,
313 /// Whether to relay service records received from peers.
314 pub relay_records: bool,
315 /// Validation policy for incoming service records.
316 pub validation: ServiceValidationPolicy,
317}
318
319impl Default for ServiceDiscoveryConfig {
320 fn default() -> Self {
321 Self {
322 auto_populate_fib: true,
323 auto_populate_scope: DiscoveryScope::LinkLocal,
324 auto_fib_cost: 100,
325 auto_fib_ttl_multiplier: 2.0,
326 auto_populate_prefix_filter: Vec::new(),
327 max_records_per_scope: 1000,
328 max_registrations_per_producer: 10,
329 max_registrations_window: Duration::from_secs(60),
330 relay_records: false,
331 validation: ServiceValidationPolicy::Skip,
332 }
333 }
334}
335
336// ─── Tests ────────────────────────────────────────────────────────────────────
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn lan_profile_has_backoff() {
344 let cfg = DiscoveryConfig::for_profile(&DiscoveryProfile::Lan);
345 assert_eq!(cfg.hello_strategy, HelloStrategyKind::Backoff);
346 assert!(cfg.auto_create_faces);
347 assert!(cfg.hello_interval_base < cfg.hello_interval_max);
348 }
349
350 #[test]
351 fn mobile_profile_is_reactive() {
352 let cfg = DiscoveryConfig::for_profile(&DiscoveryProfile::Mobile);
353 assert_eq!(cfg.hello_strategy, HelloStrategyKind::Reactive);
354 assert!(cfg.hello_interval_base < Duration::from_secs(1));
355 }
356
357 #[test]
358 fn custom_profile_roundtrips() {
359 let mut custom = DiscoveryConfig::for_profile(&DiscoveryProfile::Lan);
360 custom.liveness_miss_count = 7;
361 let profile = DiscoveryProfile::Custom(custom.clone());
362 let out = DiscoveryConfig::for_profile(&profile);
363 assert_eq!(out.liveness_miss_count, 7);
364 }
365
366 #[test]
367 fn static_profile_never_expires() {
368 let cfg = DiscoveryConfig::for_profile(&DiscoveryProfile::Static);
369 assert!(!cfg.auto_create_faces);
370 assert_eq!(cfg.liveness_miss_count, u32::MAX);
371 }
372}