Skip to main content

dlep_ext/
lib.rs

1//! DLEP extension plug-in API.
2//!
3//! Extensions declare the `ExtensionId`s they advertise in Session
4//! Initialization, and provide hooks to handle unknown data items and
5//! messages, or to observe session/destination state transitions.
6//!
7//! The trait lives in its own crate (depending only on `dlep-core`) so
8//! third-party extensions do not pull the daemon runtime.
9
10#![allow(dead_code)]
11
12use std::any::Any;
13use std::sync::Arc;
14
15use dlep_core::{
16    DataItem, ExtensionId, LinkMetrics, MacAddress, Message, MessageType, RawDataItem, StatusCode,
17};
18
19/// Opaque session identifier produced by the runtime. `Copy`-cheap for
20/// passing into hooks. The runtime mints these per-daemon (not
21/// process-wide), so two daemons sharing one process have independent
22/// id-spaces.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
24pub struct SessionId(pub u64);
25
26/// Which side of the DLEP protocol a session is on. Set explicitly by the
27/// daemon when spawning a session task so extensions can reason about
28/// directionality without deriving it from the initial FSM event.
29#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
30pub enum Role {
31    Router,
32    Modem,
33}
34
35impl Role {
36    pub fn is_router(self) -> bool {
37        matches!(self, Role::Router)
38    }
39    pub fn is_modem(self) -> bool {
40        matches!(self, Role::Modem)
41    }
42}
43
44/// Context handed to every hook. Extensions may queue outbound messages and
45/// emit opaque application-level events, but cannot mutate FSM state.
46pub trait ExtensionCtx {
47    fn session_id(&self) -> SessionId;
48    fn is_router_side(&self) -> bool;
49    fn send_message(&mut self, msg: Message);
50    fn emit_event(&mut self, ev: Arc<dyn Any + Send + Sync>);
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum ExtHandled {
55    Handled,
56    Passthrough,
57}
58
59/// Session lifecycle state exposed to negotiated extensions.
60#[derive(Clone, Copy, Debug)]
61pub struct SessionStateSnapshot {
62    pub up: bool,
63}
64
65#[derive(Clone, Copy, Debug)]
66pub struct DestinationStateSnapshot {
67    pub up: bool,
68    /// Last status the FSM saw for this destination. `SUCCESS` on `Up`;
69    /// the inbound reason on `Down`.
70    pub last_status: StatusCode,
71    /// Wire-reported link metrics. `Some(_)` on `Up`; `None` on `Down`
72    /// (RFC 8175's `Destination_Down` carries no metric Data Items).
73    /// Extensions that want richer per-destination context (addresses,
74    /// subnets) should subscribe to `DaemonEvent::Destination` directly.
75    pub metrics: Option<LinkMetrics>,
76}
77
78/// Extension plug-in trait. All hooks have default empty implementations so a
79/// real extension only overrides what it cares about.
80pub trait DlepExtension: Send + Sync + 'static {
81    /// Extension IDs this plug-in advertises in Session Initialization.
82    fn advertised_ids(&self) -> &[ExtensionId];
83
84    /// Invoked after parsing the peer's Session Init / Session Init Response.
85    /// Return `false` to opt out of this session (extension stays inert).
86    fn on_negotiated(&self, remote_ids: &[ExtensionId]) -> bool {
87        !self.advertised_ids().is_empty()
88            && self
89                .advertised_ids()
90                .iter()
91                .all(|id| remote_ids.contains(id))
92    }
93
94    /// Called when the core codec could not map a data item to a typed
95    /// variant. Consuming the item (`ExtHandled::Handled`) hides it from the
96    /// core FSM; returning `Passthrough` leaves it in the message's
97    /// `unknown_items` list.
98    fn on_unknown_data_item(
99        &self,
100        in_message: MessageType,
101        item: &RawDataItem,
102        ctx: &mut dyn ExtensionCtx,
103    ) -> ExtHandled {
104        let _ = (in_message, item, ctx);
105        ExtHandled::Passthrough
106    }
107
108    /// Called for unknown `MessageType` values arriving on the session
109    /// channel (after typed data items have been parsed).
110    fn on_unknown_message(
111        &self,
112        message_type: MessageType,
113        items: &[DataItem],
114        ctx: &mut dyn ExtensionCtx,
115    ) -> ExtHandled {
116        let _ = (message_type, items, ctx);
117        ExtHandled::Passthrough
118    }
119
120    fn on_session_state(&self, state: SessionStateSnapshot, ctx: &mut dyn ExtensionCtx) {
121        let _ = (state, ctx);
122    }
123
124    fn on_destination_state(
125        &self,
126        mac: MacAddress,
127        state: DestinationStateSnapshot,
128        ctx: &mut dyn ExtensionCtx,
129    ) {
130        let _ = (mac, state, ctx);
131    }
132}
133
134/// Registry of configured extensions. Cheap to clone (holds `Arc`s).
135#[derive(Clone, Default)]
136pub struct ExtensionRegistry {
137    extensions: Vec<Arc<dyn DlepExtension>>,
138}
139
140impl ExtensionRegistry {
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    pub fn register(&mut self, ext: Arc<dyn DlepExtension>) {
146        self.extensions.push(ext);
147    }
148
149    /// Union of all advertised IDs, for Session Initialization.
150    pub fn advertised(&self) -> Vec<ExtensionId> {
151        let mut out = Vec::new();
152        for e in &self.extensions {
153            out.extend_from_slice(e.advertised_ids());
154        }
155        out.sort();
156        out.dedup();
157        out
158    }
159
160    /// Given the peer's advertised IDs, return extensions that accepted
161    /// negotiation. Only these receive subsequent hook calls.
162    pub fn negotiate(&self, remote: &[ExtensionId]) -> Vec<Arc<dyn DlepExtension>> {
163        self.extensions
164            .iter()
165            .filter(|e| {
166                !e.advertised_ids().is_empty()
167                    && e.advertised_ids().iter().all(|id| remote.contains(id))
168                    && e.on_negotiated(remote)
169            })
170            .cloned()
171            .collect()
172    }
173
174    pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn DlepExtension>> {
175        self.extensions.iter()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    struct DefaultExtension;
183    impl DlepExtension for DefaultExtension {
184        fn advertised_ids(&self) -> &[ExtensionId] {
185            &[ExtensionId(65000)]
186        }
187    }
188    struct PermissiveExtension;
189    impl DlepExtension for PermissiveExtension {
190        fn advertised_ids(&self) -> &[ExtensionId] {
191            &[ExtensionId(65000), ExtensionId(65001)]
192        }
193        fn on_negotiated(&self, _: &[ExtensionId]) -> bool {
194            true
195        }
196    }
197    #[test]
198    fn negotiation_requires_mutual_support_even_for_permissive_plugins() {
199        let mut r = ExtensionRegistry::new();
200        r.register(Arc::new(DefaultExtension));
201        r.register(Arc::new(PermissiveExtension));
202        assert!(r.negotiate(&[]).is_empty());
203        assert_eq!(r.negotiate(&[ExtensionId(65000)]).len(), 1);
204        assert_eq!(
205            r.negotiate(&[ExtensionId(65000), ExtensionId(65001)]).len(),
206            2
207        );
208    }
209    struct SelectiveExtension {
210        ids: Vec<ExtensionId>,
211        accept: bool,
212    }
213    impl DlepExtension for SelectiveExtension {
214        fn advertised_ids(&self) -> &[ExtensionId] {
215            &self.ids
216        }
217        fn on_negotiated(&self, _: &[ExtensionId]) -> bool {
218            assert!(!self.ids.is_empty(), "unadvertised plugins must stay inert");
219            self.accept
220        }
221    }
222
223    #[test]
224    fn advertisement_is_sorted_unique_and_negotiation_preserves_plugin_order() {
225        let mut registry = ExtensionRegistry::new();
226        for ids in [vec![65002, 65000, 65002], vec![65001], vec![65000]] {
227            registry.register(Arc::new(SelectiveExtension {
228                ids: ids.into_iter().map(ExtensionId).collect(),
229                accept: true,
230            }));
231        }
232        let advertised = vec![ExtensionId(65000), ExtensionId(65001), ExtensionId(65002)];
233        assert_eq!(registry.advertised(), advertised);
234        let negotiated = registry.negotiate(&advertised);
235        assert_eq!(negotiated.len(), 3);
236        for (actual, registered) in negotiated.iter().zip(registry.iter()) {
237            assert!(Arc::ptr_eq(actual, registered));
238        }
239    }
240
241    #[test]
242    fn plugins_can_opt_out_and_plugins_without_ids_never_negotiate() {
243        let mut registry = ExtensionRegistry::new();
244        registry.register(Arc::new(SelectiveExtension {
245            ids: vec![],
246            accept: true,
247        }));
248        registry.register(Arc::new(SelectiveExtension {
249            ids: vec![ExtensionId(65000)],
250            accept: false,
251        }));
252        assert!(registry.negotiate(&[ExtensionId(65000)]).is_empty());
253    }
254}