CloFix WebAssembly Guide
Write WebAssembly modules to inspect, filter, and control every HTTP request flowing through CloFix WAF — with full access to request context, threat intelligence, built-in host functions, and cross-request state via shared dictionaries. Choose your language: Rust, AssemblyScript, TinyGo, or C/C++.
01 Introduction
CloFix WAF executes WebAssembly modules on every HTTP request before it reaches your backend. WASM modules are sandboxed, fast (near-native performance), and can be written in multiple languages including Rust, AssemblyScript, TinyGo, and C/C++.
02 Why WebAssembly?
03 Supported Languages
04 Quick Start
Step 1: Install Language Toolchain
# Rust (recommended) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-unknown-unknown # AssemblyScript npm install -g assemblyscript # TinyGo wget https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo_0.30.0_amd64.deb sudo dpkg -i tinygo_0.30.0_amd64.deb
Step 2: Create Modules Directory
sudo mkdir -p /etc/clofix/example.com/wasm_modules
Step 3: Write a Simple Module
use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] struct Request { path: String, } #[derive(Debug, Serialize)] struct Response { allowed: bool, action: String, } #[no_mangle] pub extern "C" fn process_request( req_ptr: *const u8, req_len: usize, resp_ptr: *mut u8, resp_cap: usize, ) -> usize { // Your logic here let response = Response { allowed: true, action: "allow".to_string() }; // ... serialize and return }
Step 4: Build and Deploy
# Build Rust module cargo build --target wasm32-unknown-unknown --release cp target/wasm32-unknown-unknown/release/my_module.wasm /etc/clofix/example.com/wasm_modules/ # Reload module curl -X POST https://example.com/api/wasm/reload -H "Content-Type: application/json" -d '{"domain":"example.com"}'
05 Directory Structure
/etc/clofix/
├── example.com/
│ └── wasm_modules/
│ ├── rate_limiter.wasm
│ ├── geo_blocker.wasm
│ └── sql_injection.wasm
├── api.example.com/
│ └── wasm_modules/
│ └── api_auth.wasm
└── wasm_global/
├── common_rules.wasm
└── threat_intel.wasm
allowed: false stops the pipeline.
06 Domain Configuration
# /etc/clofix/conf/example.com.conf
domain example.com {
backend https://localhost:8080;
wasm {
enabled on;
modules_path /etc/clofix/example.com/wasm_modules;
modules rate_limiter.wasm, geo_blocker.wasm;
timeout 150ms;
max_memory_mb 64;
fail_open off;
cache_modules on;
}
}
07 Module Requirements
A valid WASM module must export three functions:
wasm_alloc(size: usize) -> *mut u8— allocate memory in the WASM linear memorywasm_free(ptr: *mut u8, size: usize)— free allocated memoryprocess_request(req_ptr: *const u8, req_len: usize, resp_ptr: *mut u8, resp_cap: usize) -> usize— main entry point
#[no_mangle] pub extern "C" fn wasm_alloc(size: usize) -> *mut u8 { let mut buf = Vec::with_capacity(size); let ptr = buf.as_mut_ptr(); std::mem::forget(buf); ptr } #[no_mangle] pub extern "C" fn wasm_free(ptr: *mut u8, size: usize) { unsafe { let _ = Vec::from_raw_parts(ptr, 0, size); } }
08 Request Object
The WASM module receives a JSON-serialized request object with the following structure:
{
"method": "GET",
"path": "/admin",
"query": "id=1",
"headers": { "user-agent": "Mozilla/5.0..." },
"cookies": { "session_id": "abc123" },
"body": "",
"client_ip": "203.0.113.5",
"user_agent": "Mozilla/5.0...",
"host": "example.com",
"country": "US",
"is_tor": false,
"is_vpn": false,
"bot_score": 25,
"ja3": "...",
"ja4": "...",
"ip_reputation": "clean",
"timestamp": 1709123456
}
09 Response Format
Modules must return a JSON-serialized response:
{
"allowed": false, // true = allow, false = block
"status_code": 403, // HTTP status code
"action": "block", // allow, block, challenge, redirect, rate_limit
"redirect_url": "", // URL for redirect action
"message": "Access denied", // Human-readable reason
"score": 100, // Threat score (0-100)
"tags": ["admin_block"] // Tags for logging
}
10 Host Functions
WASM modules can call host functions exposed by the CloFix WAF engine via the clofix namespace.
11 Rust Example — Admin Path Blocker
[package] name = "admin_blocker" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [profile.release] opt-level = "z" lto = true codegen-units = 1 strip = true
use serde::{Deserialize, Serialize}; use std::slice; #[derive(Debug, Deserialize)] struct Request { path: String, method: String, client_ip: String, } #[derive(Debug, Serialize)] struct Response { allowed: bool, status_code: u16, action: String, message: String, score: u8, tags: Vec<String>, } #[no_mangle] pub extern "C" fn wasm_alloc(size: usize) -> *mut u8 { let mut buf = Vec::with_capacity(size); let ptr = buf.as_mut_ptr(); std::mem::forget(buf); ptr } #[no_mangle] pub extern "C" fn wasm_free(ptr: *mut u8, size: usize) { unsafe { let _ = Vec::from_raw_parts(ptr, 0, size); } } #[no_mangle] pub extern "C" fn process_request( req_ptr: *const u8, req_len: usize, resp_ptr: *mut u8, resp_cap: usize, ) -> usize { // Read request JSON let req_data = unsafe { slice::from_raw_parts(req_ptr, req_len) }; let request: Request = serde_json::from_slice(req_data).unwrap(); // Block /admin-wasm path if request.path == "/admin-wasm" || request.path.starts_with("/admin-wasm/") { let response = Response { allowed: false, status_code: 403, action: "block".to_string(), message: "Access to /admin-wasm is blocked by WASM module".to_string(), score: 100, tags: vec!["admin_block".to_string(), "path_block".to_string()], }; return write_response(response, resp_ptr, resp_cap); } // Block other admin paths let blocked_paths = vec!["/admin", "/wp-admin", "/administrator", "/admin.php"]; for bp in blocked_paths { if request.path == bp || request.path.starts_with(&format!("{}/", bp)) { let response = Response { allowed: false, status_code: 403, action: "block".to_string(), message: format!("Access to {} is blocked", bp), score: 90, tags: vec!["path_block".to_string(), "scanner".to_string()], }; return write_response(response, resp_ptr, resp_cap); } } // Allow all other requests let response = Response { allowed: true, status_code: 200, action: "allow".to_string(), message: "Request allowed".to_string(), score: 0, tags: vec!["allowed".to_string()], }; write_response(response, resp_ptr, resp_cap) } fn write_response(response: Response, resp_ptr: *mut u8, resp_cap: usize) -> usize { let json_data = serde_json::to_vec(&response).unwrap(); if json_data.len() > resp_cap { return 0; } unsafe { std::ptr::copy_nonoverlapping(json_data.as_ptr(), resp_ptr, json_data.len()); } json_data.len() }
12 AssemblyScript Example
// Memory allocation export function wasm_alloc(size: usize): usize { return heap.alloc(size); } export function wasm_free(ptr: usize, size: usize): void { heap.free(ptr); } // Request interface interface Request { path: string; method: string; client_ip: string; } // Response interface interface Response { allowed: boolean; status_code: u16; action: string; message: string; score: u8; tags: string[]; } export function process_request( req_ptr: usize, req_len: usize, resp_ptr: usize, resp_cap: usize ): usize { // Read request const reqData = load<Uint8Array>(req_ptr, req_len); const reqStr = String.UTF8.decode(reqData); const request = JSON.parse<Request>(reqStr); // Block /admin-wasm path if (request.path == "/admin-wasm" || request.path.startsWith("/admin-wasm/")) { const response: Response = { allowed: false, status_code: 403, action: "block", message: "Access to /admin-wasm is blocked", score: 100, tags: ["admin_block", "path_block"] }; return writeResponse(response, resp_ptr, resp_cap); } // Allow all others const response: Response = { allowed: true, status_code: 200, action: "allow", message: "Request allowed", score: 0, tags: ["allowed"] }; return writeResponse(response, resp_ptr, resp_cap); } function writeResponse(response: Response, resp_ptr: usize, resp_cap: usize): usize { const jsonStr = JSON.stringify(response); const jsonData = String.UTF8.encode(jsonStr); if (jsonData.length > resp_cap) { return 0; } store<Uint8Array>(resp_ptr, jsonData, jsonData.length); return jsonData.length; }
Build Command
asc admin_blocker.ts --target release -o admin_blocker.wasm
13 TinyGo Example
package main import ( "encoding/json" "strings" "unsafe" ) type Request struct { Path string `json:"path"` Method string `json:"method"` ClientIP string `json:"client_ip"` } type Response struct { Allowed bool `json:"allowed"` StatusCode int `json:"status_code"` Action string `json:"action"` Message string `json:"message"` Score int `json:"score"` Tags []string `json:"tags"` } //export wasm_alloc func wasm_alloc(size uint32) *byte { buf := make([]byte, size) return &buf[0] } //export wasm_free func wasm_free(ptr *byte, size uint32) {} //export process_request func process_request(req_ptr *byte, req_len uint32, resp_ptr *byte, resp_cap uint32) uint32 { // Read request reqData := make([]byte, req_len) for i := uint32(0); i < req_len; i++ { reqData[i] = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(req_ptr)) + uintptr(i))) } var request Request json.Unmarshal(reqData, &request) // Block /admin-wasm path if request.Path == "/admin-wasm" || strings.HasPrefix(request.Path, "/admin-wasm/") { response := Response{ Allowed: false, StatusCode: 403, Action: "block", Message: "Access to /admin-wasm is blocked by WASM module", Score: 100, Tags: []string{"admin_block", "path_block"}, } return writeResponse(response, resp_ptr, resp_cap) } // Allow all others response := Response{ Allowed: true, StatusCode: 200, Action: "allow", Message: "Request allowed", Score: 0, Tags: []string{"allowed"}, } return writeResponse(response, resp_ptr, resp_cap) } func writeResponse(response Response, resp_ptr *byte, resp_cap uint32) uint32 { respData, _ := json.Marshal(response) if uint32(len(respData)) > resp_cap { return 0 } for i := 0; i < len(respData); i++ { *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(resp_ptr)) + uintptr(i))) = respData[i] } return uint32(len(respData)) } func main() {}
Build Command
tinygo build -o admin_blocker.wasm -target wasm ./admin_blocker.go
14 Security Examples
Rate Limiter Module
fn process_request(...) -> usize { let ip = request.client_ip; // Call host function for rate limiting let allowed = clofix::rate_limit(ip, 100, 60); if allowed == 0 { let response = Response { allowed: false, status_code: 429, action: "rate_limit", message: "Too many requests", score: 50, tags: vec!["rate_limit"], }; return write_response(response, resp_ptr, resp_cap); } // ... rest of logic }
Geo Block Module
fn process_request(...) -> usize { let blocked_countries = vec!["XX", "YY"]; if blocked_countries.contains(&request.country) { let response = Response { allowed: false, status_code: 403, action: "block", message: format!("Access from {} is blocked", request.country), score: 80, tags: vec!["geo_block"], }; return write_response(response, resp_ptr, resp_cap); } // ... rest }
SQL Injection Detector
fn process_request(...) -> usize { let sql_patterns = vec![ r"(?i)union.*select", r"(?i)or\s+1\s*=\s*1", r"(?i)sleep\s*\(", r"(?i)select.*from.*information_schema", ]; let norm = clofix::normalize_payload(&request.query); for pattern in sql_patterns { if regex::is_match(pattern, &norm) { let response = Response { allowed: false, status_code: 403, action: "block", message: "SQL injection detected", score: 100, tags: vec!["sql_injection"], }; return write_response(response, resp_ptr, resp_cap); } } // ... rest }
Bot Detection Module
fn process_request(...) -> usize { // Use pre-computed bot score from WAF if request.bot_score > 80 { let response = Response { allowed: false, status_code: 403, action: "block", message: "Bot detected", score: 90, tags: vec!["bot"], }; return write_response(response, resp_ptr, resp_cap); } // Check Tor exit nodes if request.is_tor { let response = Response { allowed: false, status_code: 403, action: "block", message: "Tor exit node blocked", score: 70, tags: vec!["tor"], }; return write_response(response, resp_ptr, resp_cap); } // ... rest }
Admin Path Protection
fn process_request(...) -> usize { let admin_paths = vec![ "/admin", "/administrator", "/wp-admin", "/admin.php", "/admin/login", "/admin-panel", ]; for path in admin_paths { if request.path == path || request.path.starts_with(&format!("{}/", path)) { let allowed_ips = vec!["192.168.1.100", "10.0.0.50"]; if !allowed_ips.contains(&request.client_ip) { let response = Response { allowed: false, status_code: 403, action: "block", message: format!("Access to {} from {} is not allowed", path, request.client_ip), score: 100, tags: vec!["admin_block", "unauthorized"], }; return write_response(response, resp_ptr, resp_cap); } } } // ... rest }
15 Testing
Test Commands
# Watch WAF logs sudo tail -f /var/log/clofix/waf.log | grep -i '\[WASM\]' # Test normal request (should allow) curl https://example.com/ # Test blocked admin path (should return 403) curl https://example.com/admin-wasm # Test with custom header curl -H "X-API-Key: test" https://example.com/api/endpoint # View module stats curl https://example.com/api/wasm/stats?domain=example.com # Reload all modules curl -X POST https://example.com/api/wasm/reload -H "Content-Type: application/json" -d '{"domain":"example.com"}' # Upload new module curl -X POST https://example.com/api/wasm/upload \ -F "wasm_file=@my_module.wasm" \ -F "domain=example.com"
16 Troubleshooting
| Error / Symptom | Cause | Fix |
|---|---|---|
| Module not loading | Missing required exports | Ensure wasm_alloc, wasm_free, process_request are exported |
| Memory allocation failed | Module exceeds memory limit | Increase max_memory_mb in config or optimize module |
| Execution timeout (503) | Module exceeds timeout limit | Increase timeout or optimize code |
| JSON parse error | Invalid response format | Ensure response follows required JSON schema |
| Host function not found | Function not registered | Check clofix.* function name spelling |
| Changes not taking effect | Module not reloaded | Call /api/wasm/reload endpoint or restart WAF |
17 Best Practices
- Keep modules small — Compile with optimizations (
opt-level="z", LTO, strip) for minimal binary size - Use Rust for production — Best performance, memory safety, and smallest binary size
- Leverage host functions — Use clofix.rate_limit(), clofix.get_country(), etc. instead of implementing yourself
- Return early — Check block-list lookups first before expensive operations
- Set appropriate timeouts — 150ms default is good for most modules; increase for complex regex operations
- Test with curl — Always test modules with curl before deploying to production
- Monitor dashboard — Use the WASM dashboard to track module execution stats and errors
- Hot reload for updates — Use the reload API to update modules without restarting the WAF
- Use shared dictionaries — For cross-request state like rate limiting counters
- Log attacks — Use clofix.log_attack() for structured attack logging with full request context