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}