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