#[cfg(any(feature = "wasm", feature = "native"))]
use crate::arena;
use crate::engine_access::EngineAccess;
#[cfg(feature = "wasm")]
use crate::engine_access::NullEngine;
use crate::error::HookError;
use crate::hooks::HookContext;
#[cfg(feature = "wasm")]
use crate::host_fns;
use crate::program::{LoadedProgram, ProgramBackend};
use crate::result::{ExecuteResult, HookResult, Verdict};
#[cfg(feature = "wasm")]
use crate::runtime::{StoreData, WasmRuntime};
#[cfg(feature = "wasm")]
use wasmtime::{Linker, Store};
#[cfg(feature = "wasm")]
const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
pub struct HookManager {
#[cfg(feature = "wasm")]
runtime: WasmRuntime,
#[cfg(feature = "wasm")]
linker: Linker<StoreData>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookBackend {
Wasm,
Native,
Builtin,
}
impl HookBackend {
pub fn as_str(self) -> &'static str {
match self {
HookBackend::Wasm => "wasm",
HookBackend::Native => "native",
HookBackend::Builtin => "builtin",
}
}
}
impl std::str::FromStr for HookBackend {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"wasm" => Ok(HookBackend::Wasm),
"native" | "dylib" | "dynamic" => Ok(HookBackend::Native),
"builtin" | "built-in" | "static" => Ok(HookBackend::Builtin),
other => Err(format!("unknown hook type '{}'", other)),
}
}
}
impl HookManager {
pub fn new() -> Result<Self, HookError> {
#[cfg(feature = "wasm")]
{
let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
let mut linker = Linker::new(runtime.engine());
host_fns::register_host_functions(&mut linker)
.map_err(|e| HookError::CompileError(e.to_string()))?;
Ok(HookManager { runtime, linker })
}
#[cfg(not(feature = "wasm"))]
{
Ok(HookManager {})
}
}
pub fn compile(
&self,
name: String,
bytes: &[u8],
priority: i32,
) -> Result<LoadedProgram, HookError> {
#[cfg(not(feature = "wasm"))]
{
let _ = (name, bytes, priority);
return Err(HookError::CompileError(
"WASM hook backend not enabled".to_string(),
));
}
#[cfg(feature = "wasm")]
{
let module = self
.runtime
.compile(bytes)
.map_err(|e| HookError::CompileError(e.to_string()))?;
self.validate_abi_version(&name, &module)?;
Ok(LoadedProgram::new(name, module, priority))
}
}
#[cfg(feature = "wasm")]
fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
let has_export = module.exports().any(|e| e.name() == "__rns_abi_version");
if !has_export {
return Err(HookError::AbiVersionMismatch {
hook_name: name.to_string(),
expected: HOST_ABI_VERSION,
found: None,
});
}
static NULL_ENGINE: NullEngine = NullEngine;
let mut store = Store::new(
self.runtime.engine(),
StoreData {
engine_access: &NULL_ENGINE as *const dyn EngineAccess,
now: 0.0,
injected_actions: Vec::new(),
log_messages: Vec::new(),
provider_events: Vec::new(),
provider_events_enabled: false,
},
);
store
.set_fuel(self.runtime.fuel())
.map_err(|e| HookError::CompileError(e.to_string()))?;
let instance = self
.linker
.instantiate(&mut store, module)
.map_err(|e| HookError::InstantiationError(e.to_string()))?;
let func = instance
.get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
.map_err(|e| {
HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
})?;
let version = func
.call(&mut store, ())
.map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
if version != HOST_ABI_VERSION {
return Err(HookError::AbiVersionMismatch {
hook_name: name.to_string(),
expected: HOST_ABI_VERSION,
found: Some(version),
});
}
Ok(())
}
pub fn load_file(
&self,
name: String,
path: &std::path::Path,
priority: i32,
) -> Result<LoadedProgram, HookError> {
let bytes = std::fs::read(path)?;
self.compile(name, &bytes, priority)
}
pub fn load_file_backend(
&self,
name: String,
path: &std::path::Path,
priority: i32,
backend: HookBackend,
) -> Result<LoadedProgram, HookError> {
match backend {
HookBackend::Wasm => self.load_file(name, path, priority),
HookBackend::Builtin => Err(HookError::CompileError(
"built-in hooks are loaded by ID, not file path".to_string(),
)),
HookBackend::Native => {
#[cfg(feature = "native")]
{
let native = crate::native::NativeProgram::load(path)?;
Ok(LoadedProgram::new_native(name, native, priority))
}
#[cfg(not(feature = "native"))]
{
let _ = (name, path, priority);
Err(HookError::CompileError(
"native hook backend not enabled".to_string(),
))
}
}
}
}
pub fn load_builtin(
&self,
name: String,
id: impl Into<String>,
priority: i32,
) -> Result<LoadedProgram, HookError> {
let builtin = crate::builtin::BuiltinProgram::load(id)?;
Ok(LoadedProgram::new_builtin(name, builtin, priority))
}
pub fn execute_program(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
self.execute_program_with_provider_events(
program,
ctx,
engine_access,
now,
false,
data_override,
)
}
pub fn execute_program_with_provider_events(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
if !program.enabled {
return None;
}
if matches!(program.backend, ProgramBackend::Builtin(_)) {
let result = match &program.backend {
ProgramBackend::Builtin(builtin) => builtin.execute(
ctx,
data_override,
engine_access,
now,
provider_events_enabled,
),
#[cfg(feature = "wasm")]
ProgramBackend::Wasm(_) => unreachable!(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
return match result {
Ok(mut exec) => {
program.record_success();
for event in &mut exec.provider_events {
event.hook_name = program.name.clone();
}
Some(exec)
}
Err(e) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"built-in hook '{}' auto-disabled after {} consecutive errors",
program.name,
program.consecutive_traps
);
} else {
log::warn!("built-in hook '{}' failed: {}", program.name, e);
}
None
}
};
}
#[cfg(feature = "native")]
if matches!(program.backend, ProgramBackend::Native(_)) {
let ctx_bytes = match arena::context_to_bytes(ctx, data_override) {
Ok(bytes) => bytes,
Err(e) => {
log::warn!(
"failed to encode native hook context '{}': {}",
program.name,
e
);
program.record_trap();
return None;
}
};
let result = match &program.backend {
ProgramBackend::Native(native) => {
native.execute(&ctx_bytes, engine_access, provider_events_enabled)
}
ProgramBackend::Builtin(_) => unreachable!(),
#[cfg(feature = "wasm")]
ProgramBackend::Wasm(_) => unreachable!(),
};
return match result {
Ok(mut exec) => {
program.record_success();
for event in &mut exec.provider_events {
event.hook_name = program.name.clone();
}
Some(exec)
}
Err(e) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"native hook '{}' auto-disabled after {} consecutive errors",
program.name,
program.consecutive_traps
);
} else {
log::warn!("native hook '{}' failed: {}", program.name, e);
}
None
}
};
}
#[cfg(not(feature = "wasm"))]
{
let _ = (
ctx,
engine_access,
now,
provider_events_enabled,
data_override,
);
return None;
}
#[cfg(feature = "wasm")]
{
self.execute_wasm_program(
program,
ctx,
engine_access,
now,
provider_events_enabled,
data_override,
)
}
}
#[cfg(feature = "wasm")]
fn execute_wasm_program(
&self,
program: &mut LoadedProgram,
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
data_override: Option<&[u8]>,
) -> Option<ExecuteResult> {
let engine_access_ptr: *const dyn EngineAccess =
unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
let cached = match &mut program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.cached.take(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let (mut store, instance) = if let Some(cached) = cached {
let (mut s, i) = cached;
s.data_mut()
.reset_per_call(engine_access_ptr, now, provider_events_enabled);
if let Err(e) = s.set_fuel(self.runtime.fuel()) {
log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
self.cache_wasm_instance(program, s, i);
return None;
}
(s, i)
} else {
let store_data = StoreData {
engine_access: engine_access_ptr,
now,
injected_actions: Vec::new(),
log_messages: Vec::new(),
provider_events: Vec::new(),
provider_events_enabled,
};
let mut store = Store::new(self.runtime.engine(), store_data);
if let Err(e) = store.set_fuel(self.runtime.fuel()) {
log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
return None;
}
let module = match &program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.module.clone(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let instance = match self.linker.instantiate(&mut store, &module) {
Ok(inst) => inst,
Err(e) => {
log::warn!("failed to instantiate hook '{}': {}", program.name, e);
program.record_trap();
return None;
}
};
(store, instance)
};
let memory = match instance.get_memory(&mut store, "memory") {
Some(mem) => mem,
None => {
log::warn!("hook '{}' has no exported memory", program.name);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
};
if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
log::warn!("failed to write context for hook '{}': {}", program.name, e);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
if let Some(override_data) = data_override {
if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
log::warn!(
"failed to write data override for hook '{}': {}",
program.name,
e
);
}
}
let export_name = match &program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.export_name.clone(),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
};
let func = match instance.get_typed_func::<i32, i32>(&mut store, &export_name) {
Ok(f) => f,
Err(e) => {
log::warn!(
"hook '{}' missing export '{}': {}",
program.name,
export_name,
e
);
program.record_trap();
self.cache_wasm_instance(program, store, instance);
return None;
}
};
let result_offset = match func.call(&mut store, arena::ARENA_BASE as i32) {
Ok(offset) => offset,
Err(e) => {
let auto_disabled = program.record_trap();
if auto_disabled {
log::error!(
"hook '{}' auto-disabled after {} consecutive traps",
program.name,
program.consecutive_traps
);
} else {
log::warn!("hook '{}' trapped: {}", program.name, e);
}
self.cache_wasm_instance(program, store, instance);
return None;
}
};
let ret = match arena::read_result(&memory, &store, result_offset as usize) {
Ok(result) => {
program.record_success();
let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
arena::read_modified_data(&memory, &store, &result)
} else {
None
};
let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
let provider_events = std::mem::take(&mut store.data_mut().provider_events)
.into_iter()
.map(|event| crate::result::EmittedProviderEvent {
hook_name: program.name.clone(),
payload_type: event.payload_type,
payload: event.payload,
})
.collect();
Some(ExecuteResult {
hook_result: Some(result),
injected_actions,
provider_events,
modified_data,
})
}
Err(e) => {
log::warn!("hook '{}' returned invalid result: {}", program.name, e);
program.record_trap();
None
}
};
self.cache_wasm_instance(program, store, instance);
ret
}
#[cfg(feature = "wasm")]
fn cache_wasm_instance(
&self,
program: &mut LoadedProgram,
store: Store<StoreData>,
instance: wasmtime::Instance,
) {
match &mut program.backend {
ProgramBackend::Builtin(_) => unreachable!(),
ProgramBackend::Wasm(wasm) => wasm.cached = Some((store, instance)),
#[cfg(feature = "native")]
ProgramBackend::Native(_) => unreachable!(),
}
}
pub fn run_chain(
&self,
programs: &mut [LoadedProgram],
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
) -> Option<ExecuteResult> {
self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
}
pub fn run_chain_with_provider_events(
&self,
programs: &mut [LoadedProgram],
ctx: &HookContext,
engine_access: &dyn EngineAccess,
now: f64,
provider_events_enabled: bool,
) -> Option<ExecuteResult> {
let mut accumulated_actions = Vec::new();
let mut accumulated_provider_events = Vec::new();
let mut last_result: Option<HookResult> = None;
let mut last_modified_data: Option<Vec<u8>> = None;
let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
for program in programs.iter_mut() {
if !program.enabled {
continue;
}
let override_ref = if is_packet_ctx {
last_modified_data.as_deref()
} else {
None
};
if let Some(exec_result) = self.execute_program_with_provider_events(
program,
ctx,
engine_access,
now,
provider_events_enabled,
override_ref,
) {
accumulated_actions.extend(exec_result.injected_actions);
accumulated_provider_events.extend(exec_result.provider_events);
if let Some(ref result) = exec_result.hook_result {
let verdict = Verdict::from_u32(result.verdict);
match verdict {
Some(Verdict::Drop) | Some(Verdict::Halt) => {
return Some(ExecuteResult {
hook_result: exec_result.hook_result,
injected_actions: accumulated_actions,
provider_events: accumulated_provider_events,
modified_data: exec_result.modified_data.or(last_modified_data),
});
}
Some(Verdict::Modify) => {
last_result = exec_result.hook_result;
if is_packet_ctx {
if let Some(data) = exec_result.modified_data {
last_modified_data = Some(data);
}
}
}
_ => {} }
}
}
}
if last_result.is_some()
|| !accumulated_actions.is_empty()
|| !accumulated_provider_events.is_empty()
{
Some(ExecuteResult {
hook_result: last_result,
injected_actions: accumulated_actions,
provider_events: accumulated_provider_events,
modified_data: last_modified_data,
})
} else {
None
}
}
}
#[cfg(all(test, feature = "wasm"))]
mod tests {
use super::*;
use crate::engine_access::NullEngine;
fn make_manager() -> HookManager {
HookManager::new().expect("failed to create HookManager")
}
const WAT_CONTINUE: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
;; Write HookResult at offset 0x2000
;; verdict = 0 (Continue)
(i32.store (i32.const 0x2000) (i32.const 0))
;; modified_data_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
;; modified_data_len = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
;; inject_actions_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
;; inject_actions_count = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
;; log_offset = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
;; log_len = 0
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_DROP: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 1))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_TRAP: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
unreachable
)
)
"#;
const WAT_INFINITE: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(loop $inf (br $inf))
(i32.const 0)
)
)
"#;
const WAT_FUEL_RECOVERY: &str = r#"
(module
(memory (export "memory") 1)
(global $calls (mut i32) (i32.const 0))
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(global.set $calls (i32.add (global.get $calls) (i32.const 1)))
(if (i32.eq (global.get $calls) (i32.const 1))
(then (loop $inf (br $inf))))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.const 0x2004) (i32.const 0))
(i32.store (i32.const 0x2008) (i32.const 0))
(i32.store (i32.const 0x200c) (i32.const 0))
(i32.store (i32.const 0x2010) (i32.const 0))
(i32.store (i32.const 0x2014) (i32.const 0))
(i32.store (i32.const 0x2018) (i32.const 0))
(i32.const 0x2000))
)
"#;
const WAT_BULK_MEMORY: &str = r#"
(module
(memory (export "memory") 2)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(memory.fill (i32.const 0x10000) (i32.const 0x5a) (i32.const 0x4000))
(memory.copy (i32.const 0x14000) (i32.const 0x10000) (i32.const 0x4000))
(i32.store (i32.const 0x2000) (i32.const 2))
(i32.store (i32.const 0x2004) (i32.const 0x14000))
(i32.store (i32.const 0x2008) (i32.const 0x4000))
(i32.store (i32.const 0x200c) (i32.const 0))
(i32.store (i32.const 0x2010) (i32.const 0))
(i32.store (i32.const 0x2014) (i32.const 0))
(i32.store (i32.const 0x2018) (i32.const 0))
(i32.const 0x2000))
)
"#;
const WAT_HOST_HAS_PATH: &str = r#"
(module
(import "env" "host_has_path" (func $has_path (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Check if path exists for a 16-byte dest at offset 0x3000
;; (we'll write the dest hash there in the test)
(if (call $has_path (i32.const 0x3000))
(then
;; Drop
(i32.store (i32.const 0x2000) (i32.const 1))
)
(else
;; Continue
(i32.store (i32.const 0x2000) (i32.const 0))
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn pass_through() {
let mgr = make_manager();
let mut prog = mgr
.compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Continue as u32);
}
#[test]
fn drop_hook() {
let mgr = make_manager();
let mut prog = mgr
.compile("dropper".into(), WAT_DROP.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn trap_failopen() {
let mgr = make_manager();
let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
assert!(result.is_none());
assert_eq!(prog.consecutive_traps, 1);
assert!(prog.enabled);
}
#[test]
fn auto_disable() {
let mgr = make_manager();
let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
for _ in 0..10 {
let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
}
assert!(!prog.enabled);
assert_eq!(prog.consecutive_traps, 10);
}
#[test]
fn fuel_exhaustion() {
let mgr = make_manager();
let mut prog = mgr
.compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
assert!(result.is_none());
assert_eq!(prog.consecutive_traps, 1);
}
#[test]
fn fuel_exhaustion_recovers_cached_store_on_next_call() {
let mgr = make_manager();
let mut prog = mgr
.compile("fuel-recovery".into(), WAT_FUEL_RECOVERY.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
assert!(mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.is_none());
assert_eq!(prog.consecutive_traps, 1);
let recovered = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.expect("fuel must be reset before reusing the cached store");
assert_eq!(
recovered.hook_result.unwrap().verdict,
Verdict::Continue as u32
);
assert_eq!(prog.consecutive_traps, 0);
}
#[test]
fn bulk_memory_fill_and_copy_preserve_all_bytes() {
let mgr = make_manager();
let mut prog = mgr
.compile("bulk-memory".into(), WAT_BULK_MEMORY.as_bytes(), 0)
.unwrap();
let result = mgr
.execute_program(&mut prog, &HookContext::Tick, &NullEngine, 0.0, None)
.unwrap();
let copied = result.modified_data.unwrap();
assert_eq!(copied.len(), 0x4000);
assert!(copied.iter().all(|byte| *byte == 0x5a));
}
#[test]
fn chain_ordering() {
let mgr = make_manager();
let high = mgr
.compile("high".into(), WAT_DROP.as_bytes(), 100)
.unwrap();
let low = mgr
.compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
.unwrap();
let mut programs = vec![high, low];
programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
let ctx = HookContext::Tick;
let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn attach_detach() {
use crate::hooks::HookSlot;
let mgr = make_manager();
let mut slot = HookSlot {
programs: Vec::new(),
runner: crate::hooks::hook_noop,
};
let p1 = mgr
.compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
.unwrap();
let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
slot.attach(p1);
assert_eq!(slot.programs.len(), 1);
assert!(!std::ptr::eq(
slot.runner as *const (),
crate::hooks::hook_noop as *const (),
));
slot.attach(p2);
assert_eq!(slot.programs.len(), 2);
assert_eq!(slot.programs[0].name, "beta");
assert_eq!(slot.programs[1].name, "alpha");
let removed = slot.detach("beta");
assert!(removed.is_some());
assert_eq!(slot.programs.len(), 1);
assert_eq!(slot.programs[0].name, "alpha");
let removed2 = slot.detach("alpha");
assert!(removed2.is_some());
assert!(slot.programs.is_empty());
assert_eq!(
slot.runner as *const () as usize,
crate::hooks::hook_noop as *const () as usize
);
}
#[test]
fn host_has_path() {
use crate::engine_access::EngineAccess;
struct MockEngine;
impl EngineAccess for MockEngine {
fn has_path(&self, _dest: &[u8; 16]) -> bool {
true
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
None
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
None
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
false
}
fn interface_name(&self, _: u64) -> Option<String> {
None
}
fn interface_mode(&self, _: u64) -> Option<u8> {
None
}
fn identity_hash(&self) -> Option<[u8; 16]> {
None
}
fn announce_rate(&self, _: u64) -> Option<i32> {
None
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
None
}
}
let mgr = make_manager();
let mut prog = mgr
.compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert!(r.is_drop());
}
#[test]
fn host_has_path_null_engine() {
let mgr = make_manager();
let mut prog = mgr
.compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
let exec = result.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Continue as u32);
}
struct MockEngineCustom {
announce_rate_val: Option<i32>,
link_state_val: Option<u8>,
}
impl EngineAccess for MockEngineCustom {
fn has_path(&self, _: &[u8; 16]) -> bool {
false
}
fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
None
}
fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
None
}
fn is_blackholed(&self, _: &[u8; 16]) -> bool {
false
}
fn interface_name(&self, _: u64) -> Option<String> {
None
}
fn interface_mode(&self, _: u64) -> Option<u8> {
None
}
fn identity_hash(&self) -> Option<[u8; 16]> {
None
}
fn announce_rate(&self, _: u64) -> Option<i32> {
self.announce_rate_val
}
fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
self.link_state_val
}
}
const WAT_ANNOUNCE_RATE: &str = r#"
(module
(import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
(if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
(then
(i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
)
(else
(i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_get_announce_rate_found() {
let engine = MockEngineCustom {
announce_rate_val: Some(1500),
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
}
#[test]
fn host_get_announce_rate_not_found() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
}
const WAT_LINK_STATE: &str = r#"
(module
(import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
(if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
(then
(i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
)
(else
(i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
)
)
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_get_link_state_active() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: Some(2),
};
let mgr = make_manager();
let mut prog = mgr
.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
}
#[test]
fn host_get_link_state_not_found() {
let engine = MockEngineCustom {
announce_rate_val: None,
link_state_val: None,
};
let mgr = make_manager();
let mut prog = mgr
.compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &engine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
}
const WAT_INJECT_ACTION: &str = r#"
(module
(import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Write the data payload at 0x3100
(i32.store8 (i32.const 0x3100) (i32.const 0xDE))
(i32.store8 (i32.const 0x3101) (i32.const 0xAD))
(i32.store8 (i32.const 0x3102) (i32.const 0xBE))
(i32.store8 (i32.const 0x3103) (i32.const 0xEF))
;; Write ActionWire at 0x3000:
;; byte 0: tag = 0 (SendOnInterface)
(i32.store8 (i32.const 0x3000) (i32.const 0))
;; bytes 1-8: interface = 1 (u64 LE)
(i64.store (i32.const 0x3001) (i64.const 1))
;; bytes 9-12: data_offset = 0x3100 (u32 LE)
(i32.store (i32.const 0x3009) (i32.const 0x3100))
;; bytes 13-16: data_len = 4 (u32 LE)
(i32.store (i32.const 0x300D) (i32.const 4))
;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
(drop (call $inject (i32.const 0x3000) (i32.const 17)))
;; Return Continue
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn host_inject_action_send() {
let mgr = make_manager();
let mut prog = mgr
.compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
assert_eq!(exec.injected_actions.len(), 1);
match &exec.injected_actions[0] {
crate::wire::ActionWire::SendOnInterface { interface, raw } => {
assert_eq!(*interface, 1);
assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
}
other => panic!("expected SendOnInterface, got {:?}", other),
}
}
const WAT_MODIFY: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param $ctx_ptr i32) (result i32)
;; Write modified data at 0x2100
(i32.store8 (i32.const 0x2100) (i32.const 0xAA))
(i32.store8 (i32.const 0x2101) (i32.const 0xBB))
(i32.store8 (i32.const 0x2102) (i32.const 0xCC))
(i32.store8 (i32.const 0x2103) (i32.const 0xDD))
;; verdict = 2 (Modify)
(i32.store (i32.const 0x2000) (i32.const 2))
;; modified_data_offset = 0x2100
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
;; modified_data_len = 4
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn modify_extracts_data() {
let mgr = make_manager();
let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
let ctx = HookContext::Tick;
let exec = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
let r = exec.hook_result.unwrap();
assert_eq!(r.verdict, Verdict::Modify as u32);
let data = exec.modified_data.unwrap();
assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn chain_accumulates_injected_actions() {
let mgr = make_manager();
let injector = mgr
.compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
.unwrap();
let dropper = mgr
.compile("dropper".into(), WAT_DROP.as_bytes(), 0)
.unwrap();
let mut programs = vec![injector, dropper];
programs.sort_by_key(|program| std::cmp::Reverse(program.priority));
let ctx = HookContext::Tick;
let exec = mgr
.run_chain(&mut programs, &ctx, &NullEngine, 0.0)
.unwrap();
assert!(exec.hook_result.unwrap().is_drop());
assert_eq!(exec.injected_actions.len(), 1);
}
const WAT_COUNTER: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(global $counter (mut i32) (i32.const 0))
(func (export "on_hook") (param i32) (result i32)
;; Increment counter
(global.set $counter (i32.add (global.get $counter) (i32.const 1)))
;; Write counter value at 0x3000 (scratch area)
(i32.store (i32.const 0x3000) (global.get $counter))
;; Return Continue with the counter stashed in modified_data region
;; verdict = 2 (Modify) so we can extract the counter via modified_data
(i32.store (i32.const 0x2000) (i32.const 2))
;; modified_data_offset = 0x3000
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
;; modified_data_len = 4
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
fn extract_counter(exec: &ExecuteResult) -> u32 {
let data = exec.modified_data.as_ref().expect("no modified data");
assert_eq!(data.len(), 4);
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
}
#[test]
fn instance_persistence_counter() {
let mgr = make_manager();
let mut prog = mgr
.compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
let exec1 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec1), 1);
let exec2 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec2), 2);
let exec3 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec3), 3);
}
#[test]
fn instance_persistence_resets_on_drop_cache() {
let mgr = make_manager();
let mut prog = mgr
.compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
.unwrap();
let ctx = HookContext::Tick;
mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
let exec2 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec2), 2);
prog.drop_cache();
let exec3 = mgr
.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
.unwrap();
assert_eq!(extract_counter(&exec3), 1);
}
const WAT_NO_ABI_VERSION: &str = r#"
(module
(memory (export "memory") 1)
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
const WAT_WRONG_ABI_VERSION: &str = r#"
(module
(memory (export "memory") 1)
(func (export "__rns_abi_version") (result i32) (i32.const 9999))
(func (export "on_hook") (param i32) (result i32)
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
#[test]
fn rejects_missing_abi_version() {
let mgr = make_manager();
let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
match result {
Err(HookError::AbiVersionMismatch {
hook_name,
expected,
found,
}) => {
assert_eq!(hook_name, "no_abi");
assert_eq!(expected, HOST_ABI_VERSION);
assert_eq!(found, None);
}
other => panic!(
"expected AbiVersionMismatch with found=None, got {:?}",
other.err()
),
}
}
#[test]
fn rejects_wrong_abi_version() {
let mgr = make_manager();
let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
match result {
Err(HookError::AbiVersionMismatch {
hook_name,
expected,
found,
}) => {
assert_eq!(hook_name, "bad_abi");
assert_eq!(expected, HOST_ABI_VERSION);
assert_eq!(found, Some(9999));
}
other => panic!(
"expected AbiVersionMismatch with found=Some(9999), got {:?}",
other.err()
),
}
}
#[test]
fn accepts_correct_abi_version() {
let mgr = make_manager();
let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
assert!(
result.is_ok(),
"compile should succeed with correct ABI version"
);
}
#[test]
fn host_emit_event_collects_provider_event() {
let mgr = make_manager();
let wat = r#"
(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0x3000) "packet")
(data (i32.const 0x3010) "\01\02\03")
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
let mut prog = LoadedProgram::new("emit".into(), module, 0);
let ctx = HookContext::Tick;
let exec = mgr
.execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
.unwrap();
assert_eq!(exec.provider_events.len(), 1);
assert_eq!(exec.provider_events[0].hook_name, "emit");
assert_eq!(exec.provider_events[0].payload_type, "packet");
assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
}
#[test]
fn host_emit_event_is_ignored_when_disabled() {
let mgr = make_manager();
let wat = r#"
(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0x3000) "packet")
(data (i32.const 0x3010) "\01\02\03")
(func (export "__rns_abi_version") (result i32) (i32.const 1))
(func (export "on_hook") (param i32) (result i32)
(drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
(i32.store (i32.const 0x2000) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
(i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
(i32.const 0x2000)
)
)
"#;
let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
let mut prog = LoadedProgram::new("emit".into(), module, 0);
let ctx = HookContext::Tick;
let exec = mgr
.execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
.unwrap();
assert!(exec.provider_events.is_empty());
}
#[test]
fn run_chain_returns_continue_only_provider_events() {
let manager = make_manager();
let wasm = wat::parse_str(
r#"(module
(import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 4096) "\00\00\00\00")
(data (i32.const 8192) "packet")
(data (i32.const 8208) "\01\02\03")
(func (export "__rns_abi_version") (result i32) i32.const 1)
(func (export "on_hook") (param i32) (result i32)
i32.const 8192
i32.const 6
i32.const 8208
i32.const 3
call $emit
drop
i32.const 4096
)
)"#,
)
.unwrap();
let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
let pkt_ctx = crate::PacketContext {
flags: 0,
hops: 0,
destination_hash: [0; 16],
context: 0,
packet_hash: [0; 32],
interface_id: 1,
data_offset: 0,
data_len: 0,
};
let ctx = crate::HookContext::Packet {
ctx: &pkt_ctx,
raw: &[],
};
let exec = manager
.run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
.expect("expected provider event result");
assert!(exec.hook_result.is_none());
assert!(exec.injected_actions.is_empty());
assert_eq!(exec.provider_events.len(), 1);
assert_eq!(exec.provider_events[0].payload_type, "packet");
assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
}
}