Skip to content

Your first Wasm filter

Write a WebAssembly HTTP filter with the Arion Wasm SDK, build it, and run it in the request path of a listener.

Request path

What you will build

ARION 1listener2my_filter.wasm3router client cluster app

This guide takes you from an empty crate to your own WebAssembly filter running inside Arion's HTTP filter chain. Use it when the built-in filters do not cover your logic and you want custom code — header rewrites, auth decisions, counters — in the request path without forking the proxy.

Outcome

  • A Rust crate compiled to WebAssembly that sets a header on every request and every response.
  • Arion loading the module from disk and running it ahead of the router.
  • curl showing the header your filter added.

Prerequisites

  • Rust with the Wasm target: rustup target add wasm32-unknown-unknown.
  • The Arion source checkout — the SDK crates live in the repo and are referenced by path.
  • An Arion binary built with the wasm feature; a stock build refuses to start on a config that contains a Wasm filter.

requires: wasm

cargo build -p arion-proxy --features wasm

The architecture

  • Your filter is a .wasm module implementing the SDK's Plugin trait; only the hooks you write are exported.
  • Arion's Wasm host (Wasmtime) loads the module named in the filter config and calls those hooks at each phase of the HTTP transaction.
  • In the chain the module behaves like any built-in HTTP filter: it runs before the router and can mutate the request, the response, or short-circuit with a direct response.

The complete config

# arion.yaml, composed from 4 features.
logging:
  log_level: info
envoy_bootstrap:
  static_resources:
    listeners:
      - name: http_8080
        address:
          socket_address: { address: 0.0.0.0, port_value: 8080 }
        filter_chains:
          - name: http_8080_chain
            filters:
              - name: http_gateway
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                  http_filters:
                    - name: arion.filters.http.wasm.my_filter
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                        config:
                          name: my_filter
                          vm_config:
                            code:
                              local:
                                filename: /etc/arion/wasm/my_filter.wasm
                    - name: envoy.filters.http.router
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                  route_config:
                    name: http_8080_routes
                    virtual_hosts:
                      - name: backend
                        domains: ["*"]
                        routes:
                          - match:
                              prefix: /
                            route:
                              cluster: app
    clusters:
      - name: app
        connect_timeout: 5s
        type: STATIC
        load_assignment:
          cluster_name: app
          endpoints:
            - lb_endpoints:
                - endpoint:
                    address:
                      socket_address: { address: 10.0.5.11, port_value: 8080 }

Walkthrough

1. Scaffold the crate

Create a library crate. The package name becomes the module filename, so call it my_filter to match the config:

[package]
name = "my_filter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
arion_wasm_sdk = { path = "../arion/arion-wasm-sdk" }
tracing = "0.1"
http = "1.0"

crate-type = ["cdylib"] is required — the host loads a C-ABI dynamic library. The SDK's package name is arion_wasm_sdk (with underscores); point the path at arion-wasm-sdk/ in your checkout.

2. Write the filter

The plugin struct must implement Default. The #[arion_plugin] macro generates the FFI exports for exactly the hooks you implement:

use arion_wasm_sdk::{
    arion_plugin, init_tracing, FilterAction, HttpHeaders, Plugin, RequestHandle, ResponseHandle,
};

#[derive(Default)]
struct MyFilter;

#[arion_plugin]
impl Plugin for MyFilter {
    fn on_plugin_start(&mut self) {
        let _ = init_tracing();
        tracing::info!("my_filter loaded");
    }

    fn on_request_headers(&mut self, ctx: &RequestHandle<HttpHeaders>) -> FilterAction {
        let _ = ctx.set_header(
            http::header::HeaderName::from_static("x-custom-set"),
            http::header::HeaderValue::from_static("set-value"),
        );
        FilterAction::Continue
    }

    fn on_response_headers(&mut self, ctx: &ResponseHandle<HttpHeaders>) -> FilterAction {
        let _ = ctx.set_header(
            http::header::HeaderName::from_static("x-response-set"),
            http::header::HeaderValue::from_static("res-set-value"),
        );
        FilterAction::Continue
    }
}

Returning FilterAction::Continue hands the request on; the other actions buffer the body or short-circuit with a direct response — see the Rust SDK.

3. Build the module

cargo build --target wasm32-unknown-unknown --release

The module lands at target/wasm32-unknown-unknown/release/my_filter.wasm. Copy it where the config expects it:

sudo cp target/wasm32-unknown-unknown/release/my_filter.wasm /etc/arion/wasm/my_filter.wasm

4. Attach it to the listener

In the config above, the filter chain is the Wasm filter followed by the router. The typed_config is Envoy's Wasm filter type: config.name names the plugin instance and vm_config.code.local.filename points at the module on disk. Field-level detail is on the Wasm filters page.

Run and verify

The recipe proxies / to the app cluster at 10.0.5.11:8080; for a local test, change that endpoint to 127.0.0.1:5678 and run any HTTP server there. Then start the feature-enabled binary:

target/debug/arion --config arion.yaml

Call the listener:

curl -i http://127.0.0.1:8080/

The response includes the header the filter set on the response path:

HTTP/1.1 200 OK
x-response-set: res-set-value

The request-side header travels upstream: if your backend echoes headers, you will see x-custom-set: set-value arrive with every proxied request.

Production notes

  • Instances are pooled. The host keeps a pool of warm module instances per filter and reuses them across requests, so treat fields on your plugin struct as per-instance scratch space, not global state.
  • For state that must be consistent across workers — counters, shared lists — use the SDK's shared atomics and blobs, and for decisions that need another service use the HTTP/gRPC callouts; both are covered in callouts and shared state.
  • Keep modules small: build with --release, implement only the hooks you need (unimplemented phases are never called), and avoid heavyweight dependencies.
  • init_tracing() bridges the tracing macros to Arion's logs, so your filter's diagnostics appear alongside the proxy's own.

Next

Next

Take the configuration further

Start typing to search the site.

↑ ↓ to move Enter to open Esc to close