Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WASM Plugin Sandboxing

Extend the gateway with custom logic written in any language that compiles to WebAssembly — Rust, Go, AssemblyScript, C/C++, Zig, and more — while keeping the router process completely safe from faulty plugins.

Feature flag: Enable with cargo build --features wasm

Architecture

┌──────────────────────────────────────────────────────────────────┐
│                        Gateway Process                          │
│                                                                  │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐        │
│  │  WASM Plugin  │   │  WASM Plugin  │   │  WASM Plugin  │        │
│  │  (Rust .wasm) │   │  (Go .wasm)   │   │  (AS .wasm)   │        │
│  │              │   │              │   │              │        │
│  │ ┌──────────┐ │   │ ┌──────────┐ │   │ ┌──────────┐ │        │
│  │ │  Memory   │ │   │ │  Memory   │ │   │ │  Memory   │ │        │
│  │ │(sandboxed)│ │   │ │(sandboxed)│ │   │ │(sandboxed)│ │        │
│  │ └──────────┘ │   │ └──────────┘ │   │ └──────────┘ │        │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘        │
│         │                  │                  │                │
│         └──────────────────┼──────────────────┘                │
│                            │                                    │
│                    ┌───────▼───────┐                            │
│                    │   Host ABI    │                            │
│                    │  (log, headers│                            │
│                    │   metadata,   │                            │
│                    │    config)    │                            │
│                    └───────────────┘                            │
└──────────────────────────────────────────────────────────────────┘

Each plugin runs in its own isolated WebAssembly linear memory. A crashing, infinite-looping, or memory-hungry plugin cannot affect the host process or other plugins.

Security Model

ProtectionMechanismDefault
CPU BudgetFuel metering (instruction counting)1,000,000 fuel per invocation
Memory LimitResourceLimiter on wasmtime Store16 MB per plugin
No System AccessWASM has no WASI — no filesystem, network, or OS accessAlways enforced
Crash IsolationFresh Store per invocation; traps become Error::WasmPluginAlways enforced
Table LimitsBounded indirect function tables10,000 elements

Quick Start

1. Build with WASM support

cargo build --release --features wasm

2. Configure plugins in router.yaml

wasm_plugins:
  plugins:
    - name: "auth-handler"
      path: "plugins/auth.wasm"
      max_memory_bytes: 16777216   # 16 MB
      max_fuel: 1000000            # CPU budget
      config:
        api_key_header: "X-API-Key"
        allowed_origins: ["https://app.example.com"]

3. Register via GatewayBuilder (programmatic)

use grpc_graphql_gateway::{Gateway, WasmPluginConfig};

let gateway = Gateway::builder()
    .register_wasm_plugin(WasmPluginConfig {
        name: "auth-handler".into(),
        path: "plugins/auth.wasm".into(),
        max_memory_bytes: 16 * 1024 * 1024,
        max_fuel: 1_000_000,
        config: serde_json::json!({"api_key_header": "X-API-Key"}),
    })?
    // ... other configuration
    .build()?;

4. Auto-discover plugins from a directory

use grpc_graphql_gateway::{Gateway, WasmResourceLimits};

let gateway = Gateway::builder()
    .load_wasm_plugins_from_dir("./plugins", WasmResourceLimits::default())?
    .build()?;

Guest ABI

WASM plugins interact with the gateway through a well-defined ABI. Your module must export specific functions and may call host functions.

Required Guest Exports

ExportSignatureDescription
memoryMemoryLinear memory (auto-exported by most compilers)
alloc(size: i32) -> i32Allocate size bytes, return pointer

Optional Lifecycle Hooks

ExportSignatureDescription
on_request(ptr: i32, len: i32) -> i32Called before request processing. Return 0 = allow, non-zero = reject
on_response(ptr: i32, len: i32) -> i32Called after response generation
on_subgraph_request(ptr: i32, len: i32) -> i32Called before each subgraph gRPC call

The ptr and len arguments point to a JSON payload in guest memory:

on_request payload:

{
  "request_id": "abc-123",
  "client_ip": "192.168.1.1",
  "query": "query { users { id name } }",
  "operation_name": "GetUsers"
}

on_subgraph_request payload:

{
  "service_name": "users-service",
  "headers": {
    "authorization": "Bearer ...",
    "x-request-id": "abc-123"
  }
}

Host Functions Available to Guests

All host functions are in the "env" module:

FunctionSignatureDescription
host_log(level: i32, ptr: i32, len: i32)Log a message (0=trace, 1=debug, 2=info, 3=warn, 4=error)
host_get_header(key_ptr: i32, key_len: i32) -> i64Read a request header. Returns (ptr << 32) | len or 0 if not found
host_set_header(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32)Set a response header / metadata
host_get_metadata(key_ptr: i32, key_len: i32) -> i64Read metadata
host_set_metadata(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32)Set metadata (propagated to gRPC MetadataMap)
host_get_config() -> i64Get plugin config JSON. Returns (ptr << 32) | len

Writing a Plugin (Rust)

// lib.rs — compile with: cargo build --target wasm32-unknown-unknown --release

use std::alloc::{alloc, Layout};

#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
    let layout = Layout::from_size_align(size as usize, 1).unwrap();
    unsafe { alloc(layout) as i32 }
}

// Import host functions
extern "C" {
    fn host_log(level: i32, ptr: i32, len: i32);
    fn host_get_header(key_ptr: i32, key_len: i32) -> i64;
    fn host_set_metadata(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32);
}

fn log_info(msg: &str) {
    unsafe { host_log(2, msg.as_ptr() as i32, msg.len() as i32) }
}

#[no_mangle]
pub extern "C" fn on_request(ptr: i32, len: i32) -> i32 {
    log_info("Auth plugin: checking request");

    // Check for API key header
    let key = b"x-api-key";
    let result = unsafe { host_get_header(key.as_ptr() as i32, key.len() as i32) };

    if result == 0 {
        log_info("Auth plugin: missing API key, rejecting");
        return 1; // Reject
    }

    log_info("Auth plugin: API key present, allowing");
    0 // Allow
}

Build it:

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/my_plugin.wasm plugins/

Writing a Plugin (AssemblyScript)

// assembly/index.ts

@external("env", "host_log")
declare function host_log(level: i32, ptr: i32, len: i32): void;

@external("env", "host_get_header")
declare function host_get_header(key_ptr: i32, key_len: i32): i64;

export function alloc(size: i32): i32 {
  return heap.alloc(size as usize) as i32;
}

export function on_request(ptr: i32, len: i32): i32 {
  // Log
  const msg = "AS plugin: checking request";
  const msgBuf = String.UTF8.encode(msg);
  host_log(2, changetype<i32>(msgBuf), msgBuf.byteLength);

  // Check header
  const key = "authorization";
  const keyBuf = String.UTF8.encode(key);
  const result = host_get_header(changetype<i32>(keyBuf), keyBuf.byteLength);

  return result == 0 ? 1 : 0; // Reject if no auth header
}

Configuration Reference

WasmPluginConfig

FieldTypeDefaultDescription
nameStringrequiredHuman-readable plugin name
pathPathBufrequiredPath to .wasm file
max_memory_bytesusize16777216 (16 MB)Maximum linear memory
max_fuelu641000000CPU instruction budget per invocation
configserde_json::ValuenullArbitrary JSON config passed to plugin

WasmResourceLimits

FieldTypeDefaultDescription
max_memory_bytesusize16777216Memory limit
max_fuelu641000000Fuel limit
max_tablesusize4Max indirect call tables
max_table_elementsusize10000Max table entries
max_instancesu321Max instances per plugin

Error Handling

WASM plugin errors produce a distinct error code WASM_PLUGIN_ERROR:

{
  "errors": [{
    "message": "Plugin execution error",
    "extensions": {
      "code": "WASM_PLUGIN_ERROR"
    }
  }]
}

In non-production mode, the full error details are included:

{
  "errors": [{
    "message": "WASM plugin exceeded CPU budget (fuel exhausted) in 'on_request'",
    "extensions": {
      "code": "WASM_PLUGIN_ERROR"
    }
  }]
}

Production Recommendations

  1. Set conservative fuel limits — Start with 500K fuel and increase as needed. Monitor logs for fuel exhaustion.
  2. Limit memory — 8-16 MB is sufficient for most plugins. Only increase for data-heavy transformations.
  3. Pre-compile modules — Wasmtime compiles WASM to native code at load time. This is a one-time cost; instantiation is fast.
  4. Monitor plugin performance — Check fuel_used in debug logs to right-size your fuel budgets.
  5. Use directory auto-loading for GitOps workflows — drop .wasm files into ./plugins/ and restart.
  6. Test plugins in staging before production — a plugin returning non-zero from on_request will reject all requests.