Service Mesh (ASM) supports Wasm plug-ins in Envoy proxies for custom request processing. This tutorial walks through writing a Wasm plug-in in Rust that inspects HTTP request headers and either allows or blocks requests based on the result.
The plug-in checks whether an incoming request contains the header allow: true. If the header is missing or set to any other value, the plug-in returns HTTP 403 with a custom error message. If the header is present and set to true, the request passes through to the upstream service.
How it works
A Wasm plug-in runs inside the Envoy proxy as a sandboxed module. The Proxy-Wasm specification defines a standard Application Binary Interface (ABI) for communication between the proxy host and Wasm modules. Because Proxy-Wasm is proxy-agnostic, plug-ins built against this ABI are portable across any proxy that implements the specification.
The Proxy-Wasm Rust SDK organizes plug-in logic around three trait types:
| Trait | Role |
|---|---|
RootContext | Manages plug-in lifecycle and configuration. Creates new HTTP or stream contexts for each request. |
HttpContext | Handles a single HTTP request/response cycle. Implement callbacks such as on_http_request_headers to inspect or modify traffic. |
Context | Provides shared utility functions (property access, timers, and others) inherited by both root and HTTP contexts. |
When Envoy receives an HTTP request, it creates a new HttpContext instance through the RootContext. The HttpContext callbacks fire at each stage of request processing, giving the plug-in full control over headers, body, and trailers.
Background information
Wasm provides near-native execution performance and runs in a memory-safe sandbox, which makes it well suited for extending Envoy without recompiling the proxy binary. However, languages with built-in garbage collection may introduce performance overhead in Wasm. For this reason, languages with manual memory management -- C++ and Rust -- are the recommended choices.
Compared to C++, Rust offers a simpler compilation and build workflow, though it has a steeper learning curve. Choose based on your team's familiarity with each language.
Prerequisites
Before you begin, make sure that you have:
A cluster added to an ASM instance version 1.18 or later. For more information, see Add a cluster to an ASM instance
Sidecar injection enabled. For more information, see Configure sidecar injection policy
An ingress gateway created. For more information, see Create an ingress gateway
The HTTPBin application deployed and accessible. For more information, see Deploy the HTTPBin application
A Container Registry Enterprise Edition instance created. For more information, see Create a Container Registry Enterprise Edition instance
Workflow overview
The end-to-end process has five steps:
Set up the development environment -- Install the Rust toolchain and the Wasm compilation target.
Write the plug-in -- Create a Rust library project, define dependencies, and implement the request header inspection logic.
Build and package -- Compile the Rust code to a Wasm binary and package it as an OCI image.
Deploy -- Push the image to Container Registry and apply the Wasm plug-in resource in ASM.
Verify -- Send test requests to confirm the plug-in blocks and allows traffic as expected.
Step 1: Set up the development environment
Install the Rust toolchain through rustup. For instructions, see Install Rust.
Add the
wasm32-wasicompilation target: Thewasm32-wasitarget compiles Rust code to WebAssembly with access to the WASI system interface, which Envoy's Wasm runtime expects. If Rust is already installed, update it first:rustup target add wasm32-wasirustup update
Step 2: Write the plug-in
This step covers three parts: creating the project, configuring dependencies, and implementing the plug-in logic.
Create the project
Create a directory named rust-example, switch to it, and initialize a new Rust library:
mkdir rust-example && cd rust-example
cargo init --libConfigure dependencies
Replace the contents of Cargo.toml with the following:
[package]
name = "rust-example"
version = "0.1.0"
edition = "2021"
[lib]
# Build as a C-compatible dynamic library so Envoy can load it
crate-type = ["cdylib"]
[dependencies]
log = "0.4.8"
proxy-wasm = "0.2.2"The cdylib crate type produces a dynamic library compatible with the Wasm runtime. The two dependencies are:
log-- Standard Rust logging facade for structured log output.proxy-wasm-- The Proxy-Wasm Rust SDK, which provides the trait definitions and host function bindings.
Implement the plug-in logic
The plug-in needs to:
Register a
RootContextthat tells Envoy this plug-in handles HTTP traffic.For each incoming HTTP request, create an
HttpContextthat inspects request headers.In the
on_http_request_headerscallback, check for theallow: trueheader. Block the request with HTTP 403 if the header is missing, or let it pass through if present.
Replace the contents of src/lib.rs with:
use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
proxy_wasm::main! {{
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) });
}}
struct HttpHeadersRoot;
// Inherit shared utility functions (property access, timers, etc.)
impl Context for HttpHeadersRoot {}
impl RootContext for HttpHeadersRoot {
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(HttpHeaders { context_id }))
}
}
struct HttpHeaders {
context_id: u32,
}
impl Context for HttpHeaders {}
impl HttpContext for HttpHeaders {
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
info!("#{} wasm-rust: on_http_request_headers", self.context_id);
match self.get_http_request_header("allow") {
Some(allow) if allow == "true" => {
Action::Continue
}
_ => {
info!("#{} wasm-rust: allow header not found or is not true, deny by default", self.context_id);
self.send_http_response(
403,
vec![("Content-Type", "text/plain")],
Some(b"Forbidden by ASM Wasm Plugin, rust version\n"),
);
Action::Pause
}
}
}
}The code breaks down as follows:
| Section | Purpose |
|---|---|
proxy_wasm::main! | Entry point macro. Sets the log level to Trace and registers HttpHeadersRoot as the root context. |
HttpHeadersRoot + RootContext | Returns ContextType::HttpContext to tell Envoy this plug-in processes HTTP traffic. Creates a new HttpHeaders instance for each request. |
HttpHeaders + HttpContext | Implements on_http_request_headers. Reads the allow header: returns Action::Continue if the value is "true", otherwise sends an HTTP 403 response and returns Action::Pause to stop the request. |
Compile the plug-in
Build the plug-in with the wasm32-wasi target:
cargo build --target wasm32-wasi --releaseAfter a successful build, the Wasm binary is at:
target/wasm32-wasi/release/rust_example.wasmStep 3: Build an OCI image and push it to Container Registry
Package the Wasm binary as an OCI image so ASM can pull and load it.
Create a
Dockerfilein the project root:FROM scratch # Copy the compiled Wasm binary into the image as plugin.wasm ADD target/wasm32-wasi/release/rust_example.wasm ./plugin.wasmBuild and push the image. For detailed steps, see Create an OCI image of the Wasm plug-in and push it to the Container Registry Enterprise Edition instance. Replace
<your-registry>and<tag>with your actual registry address and image tag:docker build -t <your-registry>/wasm-rust-example:<tag> . docker push <your-registry>/wasm-rust-example:<tag>
Step 4: Apply the Wasm plug-in to the ingress gateway
Configure the WasmPlugin resource in ASM to load the plug-in into the ingress gateway's Envoy proxy.
For the full procedure, see Apply the Wasm plug-in to the ingress gateway. Make sure the url field points to the image you pushed in Step 3.
Step 5: Verify the plug-in
After deploying the plug-in, test it by sending requests with and without the allow: true header.
Enable debug logging
Use the kubeconfig of the data plane cluster to enable Wasm debug logs on the gateway Pod:
kubectl -n istio-system exec <gateway-pod-name> -c istio-proxy -- \
curl -XPOST "localhost:15000/logging?wasm=debug"Replace <gateway-pod-name> with the actual name of your gateway Pod.
Test without the allow header
Send a request without the allow header:
curl <ASM-gateway-IP>/status/418Expected output:
Forbidden by ASM Wasm Plugin, rust versionThe plug-in blocks the request and returns HTTP 403.
Check gateway logs
Inspect the gateway Pod logs for entries similar to the following:
2024-09-05T08:33:31.079869Z info envoy wasm wasm log istio-system.header-authorization: #2 wasm-rust: on_http_request_headers
2024-09-05T08:33:31.079943Z info envoy wasm wasm log istio-system.header-authorization: #2 wasm-rust: allow header not found or is not true, deny by default
{"authority_for":"xx.xx.xx.xx","bytes_received":"0","bytes_sent":"43","downstream_local_address":"xx.xx.xx.xx:80","downstream_remote_address":"xx.xx.xx.xx:xxxxx","duration":"0","istio_policy_status":"-","method":"GET","path":"/status/418","protocol":"HTTP/1.1","request_id":"d5250d1a-54b3-406d-8bea-5a51b617b579","requested_server_name":"-","response_code":"403","response_flags":"-","route_name":"httpbin","start_time":"2024-09-05T08:33:31.079Z","trace_id":"-","upstream_cluster":"outbound|8000||httpbin.default.svc.cluster.local","upstream_host":"-","upstream_local_address":"-","upstream_response_time":"-","upstream_service_time":"-","upstream_transport_failure_reason":"-","user_agent":"curl/8.9.0-DEV","x_forwarded_for":"xx.xx.xx.xx"}These log lines confirm the plug-in executed and denied the request.
Test with the allow header
Send a request with allow: true:
curl <ASM-gateway-IP>/status/418 -H "allow: true"Expected output:
-=[ teapot ]=-
_...._
.' _ _ `.
| ."` ^ `". _,
\_;`"---"`|//
| ;/
\_ _/
`"""`The request passes through to HTTPBin, which returns the standard HTTP 418 teapot response. The plug-in is working correctly.
What's next
To write a Wasm plug-in in Go instead, see the Go version of this tutorial.
Explore more plug-in examples in the proxy-wasm Rust SDK repository, including HTTP response body modification, configuration loading, and gRPC authentication.