WASM SDK
v1.0 — WebAssembly Security Modules

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++.

process_request entry point JSON action returns Threat scoring engine Host function API Rate limiting API JA3 / JA4 fingerprint GeoIP & threat fields Shared dictionaries Multi-language support

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++.

⚙️ CloFix WAF WebAssembly Architecture
HTTP Request WAF Engine
(IP, Geo, TLS, Threat Intel)
WASM VM
(Sandboxed per module)
Shared Dicts
(State / Rate Limit)
Action / Backend
Important WASM modules run in a secure sandbox with memory limits (64MB default) and execution timeouts (150ms default). Modules that exceed limits are terminated automatically.

02 Why WebAssembly?

Performance
Near-native execution speed — 10-100x faster than Lua or JavaScript for compute-heavy operations like regex matching and string parsing.
🔒 Security
Memory-safe sandbox with capability-based security. Modules cannot access the filesystem, network, or system resources unless explicitly allowed.
🌍 Multi-Language
Write modules in Rust, AssemblyScript, TinyGo, C/C++, Zig, or any language that compiles to WASM — use your team's preferred language.
🔄 Hot Reload
Update security rules without restarting the WAF. Upload new .wasm files and reload modules instantly via the dashboard.

03 Supported Languages

🦀 Rust RECOMMENDED
Best performance, memory safety, zero-cost abstractions. Smallest binary size (10-50KB). Production-ready.
✅ Fastest execution | ✅ Memory safe | ✅ Small binary | ✅ Excellent tooling
⚠️ Steep learning curve | ⚠️ Longer compile times
📘 AssemblyScript TypeScript-like
Perfect for JavaScript/TypeScript developers. Familiar syntax, good tooling, easy to learn.
✅ Easy to learn | ✅ TypeScript syntax | ✅ Good tooling | ✅ Fast compilation
⚠️ Slower than Rust | ⚠️ Larger binary (100-300KB)
🐹 TinyGo Go-compatible
Go developers can write WASM modules with familiar syntax. Good performance, easy concurrency.
✅ Familiar for Go devs | ✅ Good concurrency | ✅ Easy deployment
⚠️ Larger binary (500KB-2MB) | ⚠️ Limited standard library
⚙️ C/C++ Maximum Control
Traditional systems programming with maximum performance and fine-grained control.
✅ Maximum performance | ✅ Fine-grained control | ✅ Small binary
⚠️ Memory unsafe | ⚠️ Complex build process | ⚠️ Manual memory management

04 Quick Start

Step 1: Install Language Toolchain

bash
# 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

bash
sudo mkdir -p /etc/clofix/example.com/wasm_modules

Step 3: Write a Simple Module

rust (lib.rs)
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

bash
# 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

filesystem
/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
Execution Order Modules execute in the order they are loaded. Use the dashboard to control module priority. The first module that returns allowed: false stops the pipeline.

06 Domain Configuration

nginx config
# /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 memory
  • wasm_free(ptr: *mut u8, size: usize) — free allocated memory
  • process_request(req_ptr: *const u8, req_len: usize, resp_ptr: *mut u8, resp_cap: usize) -> usize — main entry point
rust — memory functions
#[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:

json
{
  "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:

json
{
  "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.

clofix.log(level, msg)
Log a message (level: info, warn, error)
clofix.log("info", "Request processed")
clofix.get_header(name)
Get HTTP request header value
let ua = clofix.get_header("User-Agent")
clofix.get_client_ip()
Get client IP address
let ip = clofix.get_client_ip()
clofix.get_country()
Get country code from client IP
let country = clofix.get_country()
clofix.rate_limit(key, limit, window_secs)
Check rate limit (returns 1 if allowed)
if clofix.rate_limit(ip, 100, 60) == 0 { block() }
clofix.is_tor(ip)
Check if IP is a Tor exit node
if clofix.is_tor(client_ip) == 1 { block() }
clofix.get_reputation(ip)
Get IP reputation score (0-100)
let rep = clofix.get_reputation(ip)
clofix.set_response_header(name, value)
Set response header
clofix.set_response_header("X-Custom", "value")
clofix.sha256(data)
Compute SHA256 hash
let hash = clofix.sha256(body)
clofix.base64_encode(data)
Base64 encode data
let b64 = clofix.base64_encode(raw)
clofix.now()
Get current Unix timestamp
let ts = clofix.now()
clofix.url_decode(str)
URL decode a string
let decoded = clofix.url_decode(payload)

11 Rust Example — Admin Path Blocker

rust (Cargo.toml)
[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
rust (src/lib.rs)
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

typescript (admin_blocker.ts)
// 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

bash
asc admin_blocker.ts --target release -o admin_blocker.wasm

13 TinyGo Example

go (admin_blocker.go)
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

bash
tinygo build -o admin_blocker.wasm -target wasm ./admin_blocker.go

14 Security Examples

Rate Limiter Module

rust
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

rust
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

rust
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

rust
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

rust
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

bash
# 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 / SymptomCauseFix
Module not loadingMissing required exportsEnsure wasm_alloc, wasm_free, process_request are exported
Memory allocation failedModule exceeds memory limitIncrease max_memory_mb in config or optimize module
Execution timeout (503)Module exceeds timeout limitIncrease timeout or optimize code
JSON parse errorInvalid response formatEnsure response follows required JSON schema
Host function not foundFunction not registeredCheck clofix.* function name spelling
Changes not taking effectModule not reloadedCall /api/wasm/reload endpoint or restart WAF

17 Best Practices

  1. Keep modules small — Compile with optimizations (opt-level="z", LTO, strip) for minimal binary size
  2. Use Rust for production — Best performance, memory safety, and smallest binary size
  3. Leverage host functions — Use clofix.rate_limit(), clofix.get_country(), etc. instead of implementing yourself
  4. Return early — Check block-list lookups first before expensive operations
  5. Set appropriate timeouts — 150ms default is good for most modules; increase for complex regex operations
  6. Test with curl — Always test modules with curl before deploying to production
  7. Monitor dashboard — Use the WASM dashboard to track module execution stats and errors
  8. Hot reload for updates — Use the reload API to update modules without restarting the WAF
  9. Use shared dictionaries — For cross-request state like rate limiting counters
  10. Log attacks — Use clofix.log_attack() for structured attack logging with full request context
Need Help? For CloFix WAF support, WebAssembly module development questions, or to report issues, visit the Support section or contact us.