| Crates.io | sentinel-agent-wasm |
| lib.rs | sentinel-agent-wasm |
| version | 0.1.0 |
| created_at | 2026-01-13 16:37:52.396633+00 |
| updated_at | 2026-01-13 16:37:52.396633+00 |
| description | WebAssembly agent for Sentinel reverse proxy - run Wasm modules for request/response processing |
| homepage | https://sentinel.raskell.io |
| repository | https://github.com/raskell-io/sentinel-agent-wasm |
| max_upload_size | |
| id | 2040582 |
| size | 165,825 |
WebAssembly agent for Sentinel reverse proxy. Execute custom Wasm modules for request/response processing.
cargo install sentinel-agent-wasm
git clone https://github.com/raskell-io/sentinel-agent-wasm
cd sentinel-agent-wasm
cargo build --release
sentinel-wasm-agent --socket /var/run/sentinel/wasm.sock \
--module /etc/sentinel/modules/security.wasm
| Option | Environment Variable | Description | Default |
|---|---|---|---|
--socket |
AGENT_SOCKET |
Unix socket path | /tmp/sentinel-wasm.sock |
--module |
WASM_MODULE |
Wasm module file (.wasm) | (required) |
--pool-size |
WASM_POOL_SIZE |
Instance pool size | 4 |
--verbose |
WASM_VERBOSE |
Enable debug logging | false |
--fail-open |
FAIL_OPEN |
Allow requests on module errors | false |
Modules must export the following functions:
// Memory allocation (required)
alloc(size: i32) -> i32 // Allocate `size` bytes, return pointer
dealloc(ptr: i32, size: i32) // Free memory at `ptr`
// Request/Response handlers (at least one required)
on_request_headers(ptr: i32, len: i32) -> i64 // Returns (result_ptr << 32) | result_len
on_response_headers(ptr: i32, len: i32) -> i64 // Returns (result_ptr << 32) | result_len
The host passes JSON data to handlers and expects JSON back.
{
"method": "GET",
"uri": "/api/users?page=1",
"client_ip": "192.168.1.100",
"correlation_id": "abc123",
"headers": {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0..."
}
}
{
"status": 200,
"correlation_id": "abc123",
"headers": {
"Content-Type": "application/json",
"X-Custom": "value"
}
}
{
"decision": "allow",
"status": 403,
"body": "Forbidden",
"add_request_headers": {"X-Processed": "true"},
"remove_request_headers": ["X-Debug"],
"add_response_headers": {"X-Frame-Options": "DENY"},
"remove_response_headers": ["Server"],
"tags": ["processed", "logged"]
}
| Decision | Description |
|---|---|
allow |
Allow the request/response to proceed |
block |
Block with given status (default: 403) and body |
deny |
Same as block |
redirect |
Redirect to URL in body field (default: 302) |
See examples/wasm-module/ for a complete example. Key parts:
use serde::{Deserialize, Serialize};
use std::alloc::{alloc as heap_alloc, dealloc as heap_dealloc, Layout};
#[derive(Deserialize)]
struct Request {
method: String,
uri: String,
client_ip: String,
headers: std::collections::HashMap<String, String>,
}
#[derive(Serialize, Default)]
struct Result {
decision: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
}
#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { heap_alloc(layout) as i32 }
}
#[no_mangle]
pub extern "C" fn dealloc(ptr: i32, size: i32) {
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { heap_dealloc(ptr as *mut u8, layout) }
}
#[no_mangle]
pub extern "C" fn on_request_headers(ptr: i32, len: i32) -> i64 {
// Read input JSON from memory
let input = unsafe {
let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
std::str::from_utf8(slice).unwrap()
};
// Parse request
let request: Request = serde_json::from_str(input).unwrap();
// Apply security rules
let result = if request.uri.contains("/admin") {
Result {
decision: "block".to_string(),
status: Some(403),
body: Some("Forbidden".to_string()),
}
} else {
Result {
decision: "allow".to_string(),
..Default::default()
}
};
// Serialize and return result
let output = serde_json::to_string(&result).unwrap();
let bytes = output.as_bytes();
let len = bytes.len() as i32;
let ptr = alloc(len);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
}
((ptr as i64) << 32) | (len as i64)
}
Build with:
cargo build --target wasm32-unknown-unknown --release
cd examples/wasm-module
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
The module will be at examples/wasm-module/target/wasm32-unknown-unknown/release/example_wasm_module.wasm.
The agent maintains a pool of pre-initialized Wasm instances for performance. Configure with --pool-size:
agents {
agent "wasm" {
type "custom"
transport "unix_socket" {
path "/var/run/sentinel/wasm.sock"
}
events ["request_headers", "response_headers"]
timeout-ms 50
failure-mode "open"
}
}
When --fail-open is enabled, module errors will:
wasm-error and fail-open tags to audit metadataWhen --fail-open is disabled (default), module errors will:
wasm-error tag to audit metadata| Feature | sentinel-agent-wasm | sentinel-agent-js | sentinel-agent-lua |
|---|---|---|---|
| Language | Any (Rust, Go, C, etc.) | JavaScript | Lua |
| Runtime | wasmtime | QuickJS | mlua |
| Performance | Fastest | Fast | Fast |
| Sandboxing | Strong (Wasm isolation) | Basic | Comprehensive |
| Ecosystem | Wasm-compatible libraries | Limited | Lua libraries |
| Complexity | Higher (compilation required) | Lower | Lower |
Use sentinel-agent-wasm for:
# Run tests (requires example module)
cd examples/wasm-module && cargo build --target wasm32-unknown-unknown --release && cd ../..
cargo test
# Run with debug logging
RUST_LOG=debug cargo run -- --socket /tmp/test.sock --module ./examples/wasm-module/target/wasm32-unknown-unknown/release/example_wasm_module.wasm --verbose
Apache-2.0