Envoy’s WebAssembly filter mechanism allows you to inject custom logic into the request/response lifecycle without recompiling Envoy itself.
Let’s see it in action. Imagine you want to add a simple header to every incoming request.
# envoy.yaml
admin:
access_log_path: /tmp/envoy.access.log
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: http_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin_service
# This is where we add our Wasm filter
typed_per_filter_config:
envoy.filters.http.wasm:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
vm:
runtime: envoy.wasm.runtime.v8 # Or envoy.wasm.runtime.null
# If using runtime.null, the code will be embedded directly.
# For other runtimes, you specify the root ID.
# vm_id: "my_wasm_vm"
# Code can be provided inline or via a ConfigMap/filesystem
code:
local:
filename: /etc/envoy/filters/add_header.wasm
# Specify the root filter configuration
root_id: "add_header_root"
# Optional: Specify filters for specific phases (e.g., request header, response header)
# phase: 1 # Envoy Wasm filter phases: 1 (headers), 2 (body), 3 (trailers)
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
vm:
runtime: envoy.wasm.runtime.v8
# vm_id: "my_wasm_vm" # Must match the root_id if not using runtime.null
code:
local:
filename: /etc/envoy/filters/add_header.wasm
root_id: "add_header_root"
clusters:
- name: httpbin_service
connect_timeout: 0.25s
type: LOGICAL_DNS
# Use the built-in httpbin service for testing
# If running locally, you might use a local IP and port
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin.org
port_value: 80
Here’s a simple add_header.wasm filter written in Rust and compiled to WebAssembly:
// src/lib.rs
use proxy_wasm_std::hostcalls;
use proxy_wasm_std::types::*;
#[no_mangle]
pub extern "C" fn _start() {
proxy_wasm_std::set_log_level(LogLevel::Trace);
proxy_wasm_std::register_plugin(|_context_id| -> Box<dyn Plugin> {
Box::new(AddHeaderFilter {})
});
}
struct AddHeaderFilter {}
impl Plugin for AddHeaderFilter {
fn on_request_headers(&mut self, _num_headers: usize) -> Action {
hostcalls::add_header("X-Custom-Header", "Hello-Wasm").unwrap();
Action::Continue
}
}
When Envoy starts with this configuration and the compiled add_header.wasm file at /etc/envoy/filters/add_header.wasm, and you send a request to http://localhost:8080/anything, you’ll see X-Custom-Header: Hello-Wasm in the output from httpbin.org.
The core problem WebAssembly filters solve is the desire to extend Envoy’s functionality with custom, dynamic logic without the operational burden of building and deploying a new Envoy binary. This is particularly useful for tasks like custom authentication, request/response transformation, rate limiting enforcement, or injecting request IDs.
Internally, Envoy uses a WebAssembly runtime (like Wasmtime, WasmEdge, or the V8 JavaScript engine compiled for Wasm) to execute your compiled .wasm module. The proxy-wasm SDK (available for Rust, C++, Go, and AssemblyScript) provides a standardized API that your Wasm module uses to interact with Envoy. When a wasm HTTP filter is configured, Envoy loads the Wasm VM, loads your compiled module into it, and then calls into your module at specific points in the HTTP request/response lifecycle (defined by phase in the config, or implicitly by the filter’s capabilities). Your Wasm code then uses the proxy-wasm API to inspect or modify the request/response.
The most surprising thing most people miss is how granularly you can control the Wasm execution flow. While you can simply return Action::Continue from a callback, you can also return Action::Pause and then resume processing later via host calls, or Action::Stop to terminate the request processing immediately. This allows for complex asynchronous operations or early termination based on dynamic conditions. For instance, you could pause a request, perform an external lookup using Envoy’s http_call host function, and then resume processing with the result.
The next concept to explore is how to implement more complex logic, such as dynamic routing or custom authentication, using Wasm filters.