Skip to main content

rns_hooks/
hooks.rs

1use crate::program::LoadedProgram;
2use crate::result::HookResult;
3
4/// All hook points in the transport pipeline.
5#[repr(usize)]
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum HookPoint {
8    // Packet lifecycle
9    PreIngress = 0,
10    PreDispatch = 1,
11
12    // Announce processing
13    AnnounceReceived = 2,
14    PathUpdated = 3,
15    AnnounceRetransmit = 4,
16
17    // Link lifecycle
18    LinkRequestReceived = 5,
19    LinkEstablished = 6,
20    LinkClosed = 7,
21
22    // Interface lifecycle
23    InterfaceUp = 8,
24    InterfaceDown = 9,
25    InterfaceConfigChanged = 10,
26
27    // Per-action hooks
28    SendOnInterface = 11,
29    BroadcastOnAllInterfaces = 12,
30    DeliverLocal = 13,
31    TunnelSynthesize = 14,
32
33    // Periodic
34    Tick = 15,
35}
36
37impl HookPoint {
38    /// Total number of hook points.
39    pub const COUNT: usize = 16;
40}
41
42/// Context passed to hook functions (host-side only, NOT `repr(C)`).
43///
44/// This enum carries the relevant data for each hook invocation.
45/// The WASM runtime serializes this into the guest's linear memory.
46pub enum HookContext<'a> {
47    Packet { ctx: &'a crate::context::PacketContext, raw: &'a [u8] },
48    Interface { interface_id: u64 },
49    Tick,
50    Announce {
51        destination_hash: [u8; 16],
52        hops: u8,
53        interface_id: u64,
54    },
55    Link {
56        link_id: [u8; 16],
57        interface_id: u64,
58    },
59}
60
61/// Function pointer type for hook slot runners.
62pub type HookFn = fn(&HookSlot, &HookContext) -> Option<HookResult>;
63
64/// No-op hook runner — returns `None` immediately.
65///
66/// This is the default for all hook points when no programs are attached.
67pub fn hook_noop(_slot: &HookSlot, _ctx: &HookContext) -> Option<HookResult> {
68    None
69}
70
71/// Placeholder runner for slots that have programs attached.
72///
73/// This function is swapped in when programs are present, serving as a
74/// "something to do" signal. Actual execution goes through `HookManager`.
75fn hook_has_programs(_slot: &HookSlot, _ctx: &HookContext) -> Option<HookResult> {
76    // Execution is handled externally by HookManager.run_chain().
77    // This runner just signals that the slot is active.
78    None
79}
80
81/// A slot for a single hook point, holding its attached programs and runner.
82pub struct HookSlot {
83    pub programs: Vec<LoadedProgram>,
84    /// Function pointer — points to [`hook_noop`] when empty,
85    /// [`hook_has_programs`] when programs are attached.
86    pub runner: HookFn,
87}
88
89impl HookSlot {
90    /// Update the runner pointer based on whether programs are present.
91    pub fn update_runner(&mut self) {
92        if self.programs.is_empty() {
93            self.runner = hook_noop;
94        } else {
95            self.runner = hook_has_programs;
96        }
97    }
98
99    /// Attach a program to this slot. Maintains descending priority order.
100    pub fn attach(&mut self, program: LoadedProgram) {
101        self.programs.push(program);
102        self.programs.sort_by(|a, b| b.priority.cmp(&a.priority));
103        self.update_runner();
104    }
105
106    /// Detach a program by name. Returns the removed program, if found.
107    pub fn detach(&mut self, name: &str) -> Option<LoadedProgram> {
108        let pos = self.programs.iter().position(|p| p.name == name)?;
109        let prog = self.programs.remove(pos);
110        self.update_runner();
111        Some(prog)
112    }
113
114    /// Returns true if this slot has programs attached (fast check).
115    pub fn has_programs(&self) -> bool {
116        self.runner as *const () as usize != hook_noop as *const () as usize
117    }
118}
119
120/// Create an array of [`HookSlot`]s, one per [`HookPoint`], all initialized
121/// with empty program lists and the [`hook_noop`] runner.
122pub fn create_hook_slots() -> [HookSlot; HookPoint::COUNT] {
123    std::array::from_fn(|_| HookSlot {
124        programs: Vec::new(),
125        runner: hook_noop,
126    })
127}
128
129/// Dispatch a hook call through the slot's function pointer.
130///
131/// When the `rns-hooks` feature is enabled in `rns-net`, this macro is used
132/// at each integration point. When no programs are attached, the noop runner
133/// returns `None` immediately.
134///
135/// When `rns-hooks` is **not** enabled, the call site wraps this in
136/// `#[cfg(feature = "rns-hooks")]` so it compiles to nothing.
137#[macro_export]
138macro_rules! run_hook {
139    ($driver:expr, $point:expr, $ctx:expr) => {{
140        ($driver.hook_slots[$point as usize].runner)(
141            &$driver.hook_slots[$point as usize],
142            &$ctx,
143        )
144    }};
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::result::Verdict;
151
152    #[test]
153    fn test_hook_point_count() {
154        assert_eq!(HookPoint::COUNT, 16);
155        // Verify the last variant matches COUNT - 1
156        assert_eq!(HookPoint::Tick as usize, 15);
157    }
158
159    #[test]
160    fn test_hook_noop_returns_none() {
161        let slot = HookSlot {
162            programs: Vec::new(),
163            runner: hook_noop,
164        };
165        let ctx = HookContext::Tick;
166        assert!(hook_noop(&slot, &ctx).is_none());
167    }
168
169    #[test]
170    fn test_create_hook_slots() {
171        let slots = create_hook_slots();
172        assert_eq!(slots.len(), HookPoint::COUNT);
173        for slot in &slots {
174            assert!(slot.programs.is_empty());
175            // Verify runner is hook_noop by calling it
176            let ctx = HookContext::Tick;
177            assert!((slot.runner)(slot, &ctx).is_none());
178        }
179    }
180
181    #[test]
182    fn test_run_hook_macro() {
183        struct FakeDriver {
184            hook_slots: [HookSlot; HookPoint::COUNT],
185        }
186        let driver = FakeDriver {
187            hook_slots: create_hook_slots(),
188        };
189        let ctx = HookContext::Tick;
190        let result = run_hook!(driver, HookPoint::Tick, ctx);
191        assert!(result.is_none());
192
193        let ctx2 = HookContext::Interface { interface_id: 42 };
194        let result2 = run_hook!(driver, HookPoint::InterfaceUp, ctx2);
195        assert!(result2.is_none());
196    }
197
198    #[test]
199    fn test_verdict_values() {
200        assert_eq!(Verdict::Continue as u32, 0);
201        assert_eq!(Verdict::Drop as u32, 1);
202        assert_eq!(Verdict::Modify as u32, 2);
203        assert_eq!(Verdict::Halt as u32, 3);
204    }
205
206    #[test]
207    fn test_hook_result_helpers() {
208        let drop_r = HookResult::drop_result();
209        assert!(drop_r.is_drop());
210        assert_eq!(drop_r.verdict, Verdict::Drop as u32);
211
212        let cont_r = HookResult::continue_result();
213        assert!(!cont_r.is_drop());
214        assert_eq!(cont_r.verdict, Verdict::Continue as u32);
215        assert_eq!(cont_r.modified_data_len, 0);
216        assert_eq!(cont_r.inject_actions_count, 0);
217        assert_eq!(cont_r.log_len, 0);
218    }
219}