1use crate::arena;
2use crate::engine_access::{EngineAccess, NullEngine};
3use crate::error::HookError;
4use crate::hooks::HookContext;
5use crate::host_fns;
6use crate::program::LoadedProgram;
7use crate::result::{ExecuteResult, HookResult, Verdict};
8use crate::runtime::{StoreData, WasmRuntime};
9use wasmtime::{Linker, Store};
10
11const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
13
14pub struct HookManager {
19 runtime: WasmRuntime,
20 linker: Linker<StoreData>,
21}
22
23impl HookManager {
24 pub fn new() -> Result<Self, HookError> {
25 let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
26 let mut linker = Linker::new(runtime.engine());
27 host_fns::register_host_functions(&mut linker)
28 .map_err(|e| HookError::CompileError(e.to_string()))?;
29 Ok(HookManager { runtime, linker })
30 }
31
32 pub fn compile(
37 &self,
38 name: String,
39 bytes: &[u8],
40 priority: i32,
41 ) -> Result<LoadedProgram, HookError> {
42 let module = self
43 .runtime
44 .compile(bytes)
45 .map_err(|e| HookError::CompileError(e.to_string()))?;
46 self.validate_abi_version(&name, &module)?;
47 Ok(LoadedProgram::new(name, module, priority))
48 }
49
50 fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
53 let has_export = module.exports().any(|e| e.name() == "__rns_abi_version");
55 if !has_export {
56 return Err(HookError::AbiVersionMismatch {
57 hook_name: name.to_string(),
58 expected: HOST_ABI_VERSION,
59 found: None,
60 });
61 }
62
63 static NULL_ENGINE: NullEngine = NullEngine;
65 let mut store = Store::new(
66 self.runtime.engine(),
67 StoreData {
68 engine_access: &NULL_ENGINE as *const dyn EngineAccess,
69 now: 0.0,
70 injected_actions: Vec::new(),
71 log_messages: Vec::new(),
72 provider_events: Vec::new(),
73 provider_events_enabled: false,
74 },
75 );
76 store
77 .set_fuel(self.runtime.fuel())
78 .map_err(|e| HookError::CompileError(e.to_string()))?;
79
80 let instance = self
81 .linker
82 .instantiate(&mut store, module)
83 .map_err(|e| HookError::InstantiationError(e.to_string()))?;
84
85 let func = instance
86 .get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
87 .map_err(|e| {
88 HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
89 })?;
90
91 let version = func
92 .call(&mut store, ())
93 .map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
94
95 if version != HOST_ABI_VERSION {
96 return Err(HookError::AbiVersionMismatch {
97 hook_name: name.to_string(),
98 expected: HOST_ABI_VERSION,
99 found: Some(version),
100 });
101 }
102
103 Ok(())
104 }
105
106 pub fn load_file(
108 &self,
109 name: String,
110 path: &std::path::Path,
111 priority: i32,
112 ) -> Result<LoadedProgram, HookError> {
113 let bytes = std::fs::read(path)?;
114 self.compile(name, &bytes, priority)
115 }
116
117 pub fn execute_program(
130 &self,
131 program: &mut LoadedProgram,
132 ctx: &HookContext,
133 engine_access: &dyn EngineAccess,
134 now: f64,
135 data_override: Option<&[u8]>,
136 ) -> Option<ExecuteResult> {
137 self.execute_program_with_provider_events(
138 program,
139 ctx,
140 engine_access,
141 now,
142 false,
143 data_override,
144 )
145 }
146
147 pub fn execute_program_with_provider_events(
148 &self,
149 program: &mut LoadedProgram,
150 ctx: &HookContext,
151 engine_access: &dyn EngineAccess,
152 now: f64,
153 provider_events_enabled: bool,
154 data_override: Option<&[u8]>,
155 ) -> Option<ExecuteResult> {
156 if !program.enabled {
157 return None;
158 }
159
160 let engine_access_ptr: *const dyn EngineAccess =
163 unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
164
165 let (mut store, instance) = if let Some(cached) = program.cached.take() {
168 let (mut s, i) = cached;
169 s.data_mut()
171 .reset_per_call(engine_access_ptr, now, provider_events_enabled);
172 if let Err(e) = s.set_fuel(self.runtime.fuel()) {
173 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
174 program.cached = Some((s, i));
175 return None;
176 }
177 (s, i)
178 } else {
179 let store_data = StoreData {
180 engine_access: engine_access_ptr,
181 now,
182 injected_actions: Vec::new(),
183 log_messages: Vec::new(),
184 provider_events: Vec::new(),
185 provider_events_enabled,
186 };
187
188 let mut store = Store::new(self.runtime.engine(), store_data);
189 if let Err(e) = store.set_fuel(self.runtime.fuel()) {
190 log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
191 return None;
192 }
193
194 let instance = match self.linker.instantiate(&mut store, &program.module) {
195 Ok(inst) => inst,
196 Err(e) => {
197 log::warn!("failed to instantiate hook '{}': {}", program.name, e);
198 program.record_trap();
199 return None;
200 }
201 };
202
203 (store, instance)
204 };
205
206 let memory = match instance.get_memory(&mut store, "memory") {
208 Some(mem) => mem,
209 None => {
210 log::warn!("hook '{}' has no exported memory", program.name);
211 program.record_trap();
212 program.cached = Some((store, instance));
213 return None;
214 }
215 };
216
217 if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
218 log::warn!("failed to write context for hook '{}': {}", program.name, e);
219 program.record_trap();
220 program.cached = Some((store, instance));
221 return None;
222 }
223
224 if let Some(override_data) = data_override {
226 if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
227 log::warn!(
228 "failed to write data override for hook '{}': {}",
229 program.name,
230 e
231 );
232 }
234 }
235
236 let func = match instance.get_typed_func::<i32, i32>(&mut store, &program.export_name) {
238 Ok(f) => f,
239 Err(e) => {
240 log::warn!(
241 "hook '{}' missing export '{}': {}",
242 program.name,
243 program.export_name,
244 e
245 );
246 program.record_trap();
247 program.cached = Some((store, instance));
248 return None;
249 }
250 };
251
252 let result_offset = match func.call(&mut store, arena::ARENA_BASE as i32) {
253 Ok(offset) => offset,
254 Err(e) => {
255 let auto_disabled = program.record_trap();
257 if auto_disabled {
258 log::error!(
259 "hook '{}' auto-disabled after {} consecutive traps",
260 program.name,
261 program.consecutive_traps
262 );
263 } else {
264 log::warn!("hook '{}' trapped: {}", program.name, e);
265 }
266 program.cached = Some((store, instance));
267 return None;
268 }
269 };
270
271 let ret = match arena::read_result(&memory, &store, result_offset as usize) {
273 Ok(result) => {
274 program.record_success();
275
276 let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
278 arena::read_modified_data(&memory, &store, &result)
279 } else {
280 None
281 };
282
283 let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
285 let provider_events = std::mem::take(&mut store.data_mut().provider_events)
286 .into_iter()
287 .map(|event| crate::result::EmittedProviderEvent {
288 hook_name: program.name.clone(),
289 payload_type: event.payload_type,
290 payload: event.payload,
291 })
292 .collect();
293
294 Some(ExecuteResult {
295 hook_result: Some(result),
296 injected_actions,
297 provider_events,
298 modified_data,
299 })
300 }
301 Err(e) => {
302 log::warn!("hook '{}' returned invalid result: {}", program.name, e);
303 program.record_trap();
304 None
305 }
306 };
307
308 program.cached = Some((store, instance));
310 ret
311 }
312
313 pub fn run_chain(
320 &self,
321 programs: &mut [LoadedProgram],
322 ctx: &HookContext,
323 engine_access: &dyn EngineAccess,
324 now: f64,
325 ) -> Option<ExecuteResult> {
326 self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
327 }
328
329 pub fn run_chain_with_provider_events(
330 &self,
331 programs: &mut [LoadedProgram],
332 ctx: &HookContext,
333 engine_access: &dyn EngineAccess,
334 now: f64,
335 provider_events_enabled: bool,
336 ) -> Option<ExecuteResult> {
337 let mut accumulated_actions = Vec::new();
338 let mut accumulated_provider_events = Vec::new();
339 let mut last_result: Option<HookResult> = None;
340 let mut last_modified_data: Option<Vec<u8>> = None;
341 let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
342
343 for program in programs.iter_mut() {
344 if !program.enabled {
345 continue;
346 }
347 let override_ref = if is_packet_ctx {
348 last_modified_data.as_deref()
349 } else {
350 None
351 };
352 if let Some(exec_result) = self.execute_program_with_provider_events(
353 program,
354 ctx,
355 engine_access,
356 now,
357 provider_events_enabled,
358 override_ref,
359 ) {
360 accumulated_actions.extend(exec_result.injected_actions);
361 accumulated_provider_events.extend(exec_result.provider_events);
362
363 if let Some(ref result) = exec_result.hook_result {
364 let verdict = Verdict::from_u32(result.verdict);
365 match verdict {
366 Some(Verdict::Drop) | Some(Verdict::Halt) => {
367 return Some(ExecuteResult {
368 hook_result: exec_result.hook_result,
369 injected_actions: accumulated_actions,
370 provider_events: accumulated_provider_events,
371 modified_data: exec_result.modified_data.or(last_modified_data),
372 });
373 }
374 Some(Verdict::Modify) => {
375 last_result = exec_result.hook_result;
376 if is_packet_ctx {
377 if let Some(data) = exec_result.modified_data {
378 last_modified_data = Some(data);
379 }
380 }
381 }
382 _ => {} }
384 }
385 }
386 }
387
388 if last_result.is_some()
389 || !accumulated_actions.is_empty()
390 || !accumulated_provider_events.is_empty()
391 {
392 Some(ExecuteResult {
393 hook_result: last_result,
394 injected_actions: accumulated_actions,
395 provider_events: accumulated_provider_events,
396 modified_data: last_modified_data,
397 })
398 } else {
399 None
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::engine_access::NullEngine;
408
409 fn make_manager() -> HookManager {
410 HookManager::new().expect("failed to create HookManager")
411 }
412
413 const WAT_CONTINUE: &str = r#"
415 (module
416 (memory (export "memory") 1)
417 (func (export "__rns_abi_version") (result i32) (i32.const 1))
418 (func (export "on_hook") (param i32) (result i32)
419 ;; Write HookResult at offset 0x2000
420 ;; verdict = 0 (Continue)
421 (i32.store (i32.const 0x2000) (i32.const 0))
422 ;; modified_data_offset = 0
423 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
424 ;; modified_data_len = 0
425 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
426 ;; inject_actions_offset = 0
427 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
428 ;; inject_actions_count = 0
429 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
430 ;; log_offset = 0
431 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
432 ;; log_len = 0
433 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
434 (i32.const 0x2000)
435 )
436 )
437 "#;
438
439 const WAT_DROP: &str = r#"
441 (module
442 (memory (export "memory") 1)
443 (func (export "__rns_abi_version") (result i32) (i32.const 1))
444 (func (export "on_hook") (param i32) (result i32)
445 (i32.store (i32.const 0x2000) (i32.const 1))
446 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
447 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
448 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
449 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
450 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
451 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
452 (i32.const 0x2000)
453 )
454 )
455 "#;
456
457 const WAT_TRAP: &str = r#"
459 (module
460 (memory (export "memory") 1)
461 (func (export "__rns_abi_version") (result i32) (i32.const 1))
462 (func (export "on_hook") (param i32) (result i32)
463 unreachable
464 )
465 )
466 "#;
467
468 const WAT_INFINITE: &str = r#"
470 (module
471 (memory (export "memory") 1)
472 (func (export "__rns_abi_version") (result i32) (i32.const 1))
473 (func (export "on_hook") (param i32) (result i32)
474 (loop $inf (br $inf))
475 (i32.const 0)
476 )
477 )
478 "#;
479
480 const WAT_HOST_HAS_PATH: &str = r#"
482 (module
483 (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
484 (memory (export "memory") 1)
485 (func (export "__rns_abi_version") (result i32) (i32.const 1))
486 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
487 ;; Check if path exists for a 16-byte dest at offset 0x3000
488 ;; (we'll write the dest hash there in the test)
489 (if (call $has_path (i32.const 0x3000))
490 (then
491 ;; Drop
492 (i32.store (i32.const 0x2000) (i32.const 1))
493 )
494 (else
495 ;; Continue
496 (i32.store (i32.const 0x2000) (i32.const 0))
497 )
498 )
499 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
500 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
501 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
502 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
503 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
504 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
505 (i32.const 0x2000)
506 )
507 )
508 "#;
509
510 #[test]
511 fn pass_through() {
512 let mgr = make_manager();
513 let mut prog = mgr
514 .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
515 .unwrap();
516 let ctx = HookContext::Tick;
517 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
518 let exec = result.unwrap();
520 let r = exec.hook_result.unwrap();
521 assert_eq!(r.verdict, Verdict::Continue as u32);
522 }
523
524 #[test]
525 fn drop_hook() {
526 let mgr = make_manager();
527 let mut prog = mgr
528 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
529 .unwrap();
530 let ctx = HookContext::Tick;
531 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
532 let exec = result.unwrap();
533 let r = exec.hook_result.unwrap();
534 assert!(r.is_drop());
535 }
536
537 #[test]
538 fn trap_failopen() {
539 let mgr = make_manager();
540 let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
541 let ctx = HookContext::Tick;
542 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
543 assert!(result.is_none());
544 assert_eq!(prog.consecutive_traps, 1);
545 assert!(prog.enabled);
546 }
547
548 #[test]
549 fn auto_disable() {
550 let mgr = make_manager();
551 let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
552 let ctx = HookContext::Tick;
553 for _ in 0..10 {
554 let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
555 }
556 assert!(!prog.enabled);
557 assert_eq!(prog.consecutive_traps, 10);
558 }
559
560 #[test]
561 fn fuel_exhaustion() {
562 let mgr = make_manager();
563 let mut prog = mgr
564 .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
565 .unwrap();
566 let ctx = HookContext::Tick;
567 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
568 assert!(result.is_none());
570 assert_eq!(prog.consecutive_traps, 1);
571 }
572
573 #[test]
574 fn chain_ordering() {
575 let mgr = make_manager();
576 let high = mgr
577 .compile("high".into(), WAT_DROP.as_bytes(), 100)
578 .unwrap();
579 let low = mgr
580 .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
581 .unwrap();
582 let mut programs = vec![high, low];
584 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
586
587 let ctx = HookContext::Tick;
588 let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
589 let exec = result.unwrap();
591 let r = exec.hook_result.unwrap();
592 assert!(r.is_drop());
593 }
594
595 #[test]
596 fn attach_detach() {
597 use crate::hooks::HookSlot;
598
599 let mgr = make_manager();
600 let mut slot = HookSlot {
601 programs: Vec::new(),
602 runner: crate::hooks::hook_noop,
603 };
604
605 let p1 = mgr
606 .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
607 .unwrap();
608 let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
609
610 slot.attach(p1);
611 assert_eq!(slot.programs.len(), 1);
612 assert!(!std::ptr::eq(
613 slot.runner as *const (),
614 crate::hooks::hook_noop as *const (),
615 ));
616
617 slot.attach(p2);
618 assert_eq!(slot.programs.len(), 2);
619 assert_eq!(slot.programs[0].name, "beta");
621 assert_eq!(slot.programs[1].name, "alpha");
622
623 let removed = slot.detach("beta");
624 assert!(removed.is_some());
625 assert_eq!(slot.programs.len(), 1);
626 assert_eq!(slot.programs[0].name, "alpha");
627
628 let removed2 = slot.detach("alpha");
629 assert!(removed2.is_some());
630 assert!(slot.programs.is_empty());
631 assert_eq!(
632 slot.runner as *const () as usize,
633 crate::hooks::hook_noop as *const () as usize
634 );
635 }
636
637 #[test]
638 fn host_has_path() {
639 use crate::engine_access::EngineAccess;
640
641 struct MockEngine;
642 impl EngineAccess for MockEngine {
643 fn has_path(&self, _dest: &[u8; 16]) -> bool {
644 true
645 }
646 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
647 None
648 }
649 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
650 None
651 }
652 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
653 false
654 }
655 fn interface_name(&self, _: u64) -> Option<String> {
656 None
657 }
658 fn interface_mode(&self, _: u64) -> Option<u8> {
659 None
660 }
661 fn identity_hash(&self) -> Option<[u8; 16]> {
662 None
663 }
664 fn announce_rate(&self, _: u64) -> Option<i32> {
665 None
666 }
667 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
668 None
669 }
670 }
671
672 let mgr = make_manager();
673 let mut prog = mgr
674 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
675 .unwrap();
676 let ctx = HookContext::Tick;
677 let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
678 let exec = result.unwrap();
680 let r = exec.hook_result.unwrap();
681 assert!(r.is_drop());
682 }
683
684 #[test]
685 fn host_has_path_null_engine() {
686 let mgr = make_manager();
688 let mut prog = mgr
689 .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
690 .unwrap();
691 let ctx = HookContext::Tick;
692 let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
693 let exec = result.unwrap();
694 let r = exec.hook_result.unwrap();
695 assert_eq!(r.verdict, Verdict::Continue as u32);
696 }
697
698 struct MockEngineCustom {
702 announce_rate_val: Option<i32>,
703 link_state_val: Option<u8>,
704 }
705
706 impl EngineAccess for MockEngineCustom {
707 fn has_path(&self, _: &[u8; 16]) -> bool {
708 false
709 }
710 fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
711 None
712 }
713 fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
714 None
715 }
716 fn is_blackholed(&self, _: &[u8; 16]) -> bool {
717 false
718 }
719 fn interface_name(&self, _: u64) -> Option<String> {
720 None
721 }
722 fn interface_mode(&self, _: u64) -> Option<u8> {
723 None
724 }
725 fn identity_hash(&self) -> Option<[u8; 16]> {
726 None
727 }
728 fn announce_rate(&self, _: u64) -> Option<i32> {
729 self.announce_rate_val
730 }
731 fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
732 self.link_state_val
733 }
734 }
735
736 const WAT_ANNOUNCE_RATE: &str = r#"
738 (module
739 (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
740 (memory (export "memory") 1)
741 (func (export "__rns_abi_version") (result i32) (i32.const 1))
742 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
743 (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
744 (then
745 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
746 )
747 (else
748 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
749 )
750 )
751 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
752 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
753 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
754 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
755 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
756 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
757 (i32.const 0x2000)
758 )
759 )
760 "#;
761
762 #[test]
763 fn host_get_announce_rate_found() {
764 let engine = MockEngineCustom {
766 announce_rate_val: Some(1500),
767 link_state_val: None,
768 };
769 let mgr = make_manager();
770 let mut prog = mgr
771 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
772 .unwrap();
773 let ctx = HookContext::Tick;
774 let exec = mgr
775 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
776 .unwrap();
777 assert!(exec.hook_result.unwrap().is_drop());
778 }
779
780 #[test]
781 fn host_get_announce_rate_not_found() {
782 let engine = MockEngineCustom {
784 announce_rate_val: None,
785 link_state_val: None,
786 };
787 let mgr = make_manager();
788 let mut prog = mgr
789 .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
790 .unwrap();
791 let ctx = HookContext::Tick;
792 let exec = mgr
793 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
794 .unwrap();
795 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
796 }
797
798 const WAT_LINK_STATE: &str = r#"
801 (module
802 (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
803 (memory (export "memory") 1)
804 (func (export "__rns_abi_version") (result i32) (i32.const 1))
805 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
806 (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
807 (then
808 (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
809 )
810 (else
811 (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
812 )
813 )
814 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
815 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
816 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
817 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
818 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
819 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
820 (i32.const 0x2000)
821 )
822 )
823 "#;
824
825 #[test]
826 fn host_get_link_state_active() {
827 let engine = MockEngineCustom {
829 announce_rate_val: None,
830 link_state_val: Some(2),
831 };
832 let mgr = make_manager();
833 let mut prog = mgr
834 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
835 .unwrap();
836 let ctx = HookContext::Tick;
837 let exec = mgr
838 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
839 .unwrap();
840 assert!(exec.hook_result.unwrap().is_drop());
841 }
842
843 #[test]
844 fn host_get_link_state_not_found() {
845 let engine = MockEngineCustom {
847 announce_rate_val: None,
848 link_state_val: None,
849 };
850 let mgr = make_manager();
851 let mut prog = mgr
852 .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
853 .unwrap();
854 let ctx = HookContext::Tick;
855 let exec = mgr
856 .execute_program(&mut prog, &ctx, &engine, 0.0, None)
857 .unwrap();
858 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
859 }
860
861 const WAT_INJECT_ACTION: &str = r#"
866 (module
867 (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
868 (memory (export "memory") 1)
869 (func (export "__rns_abi_version") (result i32) (i32.const 1))
870 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
871 ;; Write the data payload at 0x3100
872 (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
873 (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
874 (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
875 (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
876
877 ;; Write ActionWire at 0x3000:
878 ;; byte 0: tag = 0 (SendOnInterface)
879 (i32.store8 (i32.const 0x3000) (i32.const 0))
880 ;; bytes 1-8: interface = 1 (u64 LE)
881 (i64.store (i32.const 0x3001) (i64.const 1))
882 ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
883 (i32.store (i32.const 0x3009) (i32.const 0x3100))
884 ;; bytes 13-16: data_len = 4 (u32 LE)
885 (i32.store (i32.const 0x300D) (i32.const 4))
886
887 ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
888 (drop (call $inject (i32.const 0x3000) (i32.const 17)))
889
890 ;; Return Continue
891 (i32.store (i32.const 0x2000) (i32.const 0))
892 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
893 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
894 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
895 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
896 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
897 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
898 (i32.const 0x2000)
899 )
900 )
901 "#;
902
903 #[test]
904 fn host_inject_action_send() {
905 let mgr = make_manager();
906 let mut prog = mgr
907 .compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
908 .unwrap();
909 let ctx = HookContext::Tick;
910 let exec = mgr
911 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
912 .unwrap();
913 assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
914 assert_eq!(exec.injected_actions.len(), 1);
915 match &exec.injected_actions[0] {
916 crate::wire::ActionWire::SendOnInterface { interface, raw } => {
917 assert_eq!(*interface, 1);
918 assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
919 }
920 other => panic!("expected SendOnInterface, got {:?}", other),
921 }
922 }
923
924 const WAT_MODIFY: &str = r#"
926 (module
927 (memory (export "memory") 1)
928 (func (export "__rns_abi_version") (result i32) (i32.const 1))
929 (func (export "on_hook") (param $ctx_ptr i32) (result i32)
930 ;; Write modified data at 0x2100
931 (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
932 (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
933 (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
934 (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
935
936 ;; verdict = 2 (Modify)
937 (i32.store (i32.const 0x2000) (i32.const 2))
938 ;; modified_data_offset = 0x2100
939 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
940 ;; modified_data_len = 4
941 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
942 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
943 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
944 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
945 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
946 (i32.const 0x2000)
947 )
948 )
949 "#;
950
951 #[test]
952 fn modify_extracts_data() {
953 let mgr = make_manager();
954 let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
955 let ctx = HookContext::Tick;
956 let exec = mgr
957 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
958 .unwrap();
959 let r = exec.hook_result.unwrap();
960 assert_eq!(r.verdict, Verdict::Modify as u32);
961 let data = exec.modified_data.unwrap();
962 assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
963 }
964
965 #[test]
966 fn chain_accumulates_injected_actions() {
967 let mgr = make_manager();
970 let injector = mgr
971 .compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
972 .unwrap();
973 let dropper = mgr
974 .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
975 .unwrap();
976 let mut programs = vec![injector, dropper];
977 programs.sort_by(|a, b| b.priority.cmp(&a.priority));
978
979 let ctx = HookContext::Tick;
980 let exec = mgr
981 .run_chain(&mut programs, &ctx, &NullEngine, 0.0)
982 .unwrap();
983 assert!(exec.hook_result.unwrap().is_drop());
985 assert_eq!(exec.injected_actions.len(), 1);
987 }
988
989 const WAT_COUNTER: &str = r#"
995 (module
996 (memory (export "memory") 1)
997 (func (export "__rns_abi_version") (result i32) (i32.const 1))
998 (global $counter (mut i32) (i32.const 0))
999 (func (export "on_hook") (param i32) (result i32)
1000 ;; Increment counter
1001 (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
1002 ;; Write counter value at 0x3000 (scratch area)
1003 (i32.store (i32.const 0x3000) (global.get $counter))
1004 ;; Return Continue with the counter stashed in modified_data region
1005 ;; verdict = 2 (Modify) so we can extract the counter via modified_data
1006 (i32.store (i32.const 0x2000) (i32.const 2))
1007 ;; modified_data_offset = 0x3000
1008 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
1009 ;; modified_data_len = 4
1010 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1011 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1012 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1013 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1014 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1015 (i32.const 0x2000)
1016 )
1017 )
1018 "#;
1019
1020 fn extract_counter(exec: &ExecuteResult) -> u32 {
1021 let data = exec.modified_data.as_ref().expect("no modified data");
1022 assert_eq!(data.len(), 4);
1023 u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1024 }
1025
1026 #[test]
1027 fn instance_persistence_counter() {
1028 let mgr = make_manager();
1029 let mut prog = mgr
1030 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1031 .unwrap();
1032 let ctx = HookContext::Tick;
1033
1034 let exec1 = mgr
1036 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1037 .unwrap();
1038 assert_eq!(extract_counter(&exec1), 1);
1039
1040 let exec2 = mgr
1041 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1042 .unwrap();
1043 assert_eq!(extract_counter(&exec2), 2);
1044
1045 let exec3 = mgr
1046 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1047 .unwrap();
1048 assert_eq!(extract_counter(&exec3), 3);
1049 }
1050
1051 #[test]
1052 fn instance_persistence_resets_on_drop_cache() {
1053 let mgr = make_manager();
1054 let mut prog = mgr
1055 .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1056 .unwrap();
1057 let ctx = HookContext::Tick;
1058
1059 mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1061 .unwrap();
1062 let exec2 = mgr
1063 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1064 .unwrap();
1065 assert_eq!(extract_counter(&exec2), 2);
1066
1067 prog.drop_cache();
1069
1070 let exec3 = mgr
1072 .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1073 .unwrap();
1074 assert_eq!(extract_counter(&exec3), 1);
1075 }
1076
1077 const WAT_NO_ABI_VERSION: &str = r#"
1081 (module
1082 (memory (export "memory") 1)
1083 (func (export "on_hook") (param i32) (result i32)
1084 (i32.store (i32.const 0x2000) (i32.const 0))
1085 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1086 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1087 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1088 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1089 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1090 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1091 (i32.const 0x2000)
1092 )
1093 )
1094 "#;
1095
1096 const WAT_WRONG_ABI_VERSION: &str = r#"
1098 (module
1099 (memory (export "memory") 1)
1100 (func (export "__rns_abi_version") (result i32) (i32.const 9999))
1101 (func (export "on_hook") (param i32) (result i32)
1102 (i32.store (i32.const 0x2000) (i32.const 0))
1103 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1104 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1105 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1106 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1107 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1108 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1109 (i32.const 0x2000)
1110 )
1111 )
1112 "#;
1113
1114 #[test]
1115 fn rejects_missing_abi_version() {
1116 let mgr = make_manager();
1117 let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
1118 match result {
1119 Err(HookError::AbiVersionMismatch {
1120 hook_name,
1121 expected,
1122 found,
1123 }) => {
1124 assert_eq!(hook_name, "no_abi");
1125 assert_eq!(expected, HOST_ABI_VERSION);
1126 assert_eq!(found, None);
1127 }
1128 other => panic!(
1129 "expected AbiVersionMismatch with found=None, got {:?}",
1130 other.err()
1131 ),
1132 }
1133 }
1134
1135 #[test]
1136 fn rejects_wrong_abi_version() {
1137 let mgr = make_manager();
1138 let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1139 match result {
1140 Err(HookError::AbiVersionMismatch {
1141 hook_name,
1142 expected,
1143 found,
1144 }) => {
1145 assert_eq!(hook_name, "bad_abi");
1146 assert_eq!(expected, HOST_ABI_VERSION);
1147 assert_eq!(found, Some(9999));
1148 }
1149 other => panic!(
1150 "expected AbiVersionMismatch with found=Some(9999), got {:?}",
1151 other.err()
1152 ),
1153 }
1154 }
1155
1156 #[test]
1157 fn accepts_correct_abi_version() {
1158 let mgr = make_manager();
1159 let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1160 assert!(
1161 result.is_ok(),
1162 "compile should succeed with correct ABI version"
1163 );
1164 }
1165
1166 #[test]
1167 fn host_emit_event_collects_provider_event() {
1168 let mgr = make_manager();
1169 let wat = r#"
1170 (module
1171 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1172 (memory (export "memory") 1)
1173 (data (i32.const 0x3000) "packet")
1174 (data (i32.const 0x3010) "\01\02\03")
1175 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1176 (func (export "on_hook") (param i32) (result i32)
1177 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1178 (i32.store (i32.const 0x2000) (i32.const 0))
1179 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1180 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1181 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1182 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1183 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1184 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1185 (i32.const 0x2000)
1186 )
1187 )
1188 "#;
1189 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1190 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1191 let ctx = HookContext::Tick;
1192
1193 let exec = mgr
1194 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
1195 .unwrap();
1196 assert_eq!(exec.provider_events.len(), 1);
1197 assert_eq!(exec.provider_events[0].hook_name, "emit");
1198 assert_eq!(exec.provider_events[0].payload_type, "packet");
1199 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1200 }
1201
1202 #[test]
1203 fn host_emit_event_is_ignored_when_disabled() {
1204 let mgr = make_manager();
1205 let wat = r#"
1206 (module
1207 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1208 (memory (export "memory") 1)
1209 (data (i32.const 0x3000) "packet")
1210 (data (i32.const 0x3010) "\01\02\03")
1211 (func (export "__rns_abi_version") (result i32) (i32.const 1))
1212 (func (export "on_hook") (param i32) (result i32)
1213 (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1214 (i32.store (i32.const 0x2000) (i32.const 0))
1215 (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1216 (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1217 (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1218 (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1219 (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1220 (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1221 (i32.const 0x2000)
1222 )
1223 )
1224 "#;
1225 let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1226 let mut prog = LoadedProgram::new("emit".into(), module, 0);
1227 let ctx = HookContext::Tick;
1228
1229 let exec = mgr
1230 .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
1231 .unwrap();
1232 assert!(exec.provider_events.is_empty());
1233 }
1234
1235 #[test]
1236 fn run_chain_returns_continue_only_provider_events() {
1237 let manager = make_manager();
1238 let wasm = wat::parse_str(
1239 r#"(module
1240 (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1241 (memory (export "memory") 1)
1242 (data (i32.const 4096) "\00\00\00\00")
1243 (data (i32.const 8192) "packet")
1244 (data (i32.const 8208) "\01\02\03")
1245 (func (export "__rns_abi_version") (result i32) i32.const 1)
1246 (func (export "on_hook") (param i32) (result i32)
1247 i32.const 8192
1248 i32.const 6
1249 i32.const 8208
1250 i32.const 3
1251 call $emit
1252 drop
1253 i32.const 4096
1254 )
1255 )"#,
1256 )
1257 .unwrap();
1258
1259 let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
1260 let pkt_ctx = crate::PacketContext {
1261 flags: 0,
1262 hops: 0,
1263 destination_hash: [0; 16],
1264 context: 0,
1265 packet_hash: [0; 32],
1266 interface_id: 1,
1267 data_offset: 0,
1268 data_len: 0,
1269 };
1270 let ctx = crate::HookContext::Packet {
1271 ctx: &pkt_ctx,
1272 raw: &[],
1273 };
1274
1275 let exec = manager
1276 .run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
1277 .expect("expected provider event result");
1278 assert!(exec.hook_result.is_none());
1279 assert!(exec.injected_actions.is_empty());
1280 assert_eq!(exec.provider_events.len(), 1);
1281 assert_eq!(exec.provider_events[0].payload_type, "packet");
1282 assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1283 }
1284}