Skip to content

Native plugin filters

Dwara supports two plugin implementation paths, both selected by config and both occupying the same phase slot on a route with no dataplane-visible difference in attachment semantics:

  • Proxy-Wasm plugins -- portable .wasm modules loaded into a sandboxed runtime at startup. The portability/ABI path (see Proxy-Wasm plugins).
  • Native plugin filters -- Rust filters compiled into the gateway binary at build time and linked in directly. The convenience/performance path.

A native filter and a WASM plugin attach identically from config's point of view: both are entries in the top-level plugins list, referenced by name from routes, and both declare the same phase contract. Only the implementation differs -- compiled-in vs sandboxed-and-hot-loaded.

Status

Native filters are a compile-time capability (plugins, default OFF; see Editions) and are not included in the published OSS binaries. The NativeFilter trait, registry, and unified PluginChain are complete and test-covered as library components; dispatching the chain from the gateway's request path is landing iteratively. The plugins: config below (including the native:/wasm: mutual-exclusion validation) parses and validates in any build.

When to use this

Use native plugin filters when you need:

  • Maximum performance (no sandbox overhead, no ABI marshalling).
  • Direct access to Rust types and the dwara-core library.
  • A filter that does not need to be portable across proxy-wasm hosts.

Use proxy-wasm plugins when you need portability (community Kong/Envoy filters run unmodified) or hot-loading without a rebuild.

Enabling

Native filters are compiled into the OSS build (default OFF):

sh
cargo build -p dwara-core

Combine with wasm for both paths:

sh
cargo build -p dwara-core,wasm

When plugins is on but wasm is off, only native filters work. When both are on, both work and share the unified dispatch chain.

Configuration

A plugin is either wasm: or native: (exactly one must be set):

yaml
plugins:
  - name: my-native
    native: add-header
    phases:
      - request_headers
      - response_headers
    config: '{"key": "value"}'

routes:
  - name: api
    service: backend
    match:
      path:
        type: exact
        value: /api
    action:
      type: proxy
    plugins:
      - my-native

Plugin fields

FieldTypeRequiredDescription
namestringyesUnique plugin name (referenced by routes).
wasmstringone of wasm/nativePath to the .wasm module (WASM plugins).
nativestringone of wasm/nativeRegistered native filter implementation name (native plugins).
phaseslistyes (non-empty)Phases the plugin hooks.
configstringnoPlugin-specific config (passed to the factory).
limitsobjectnoResource limits (WASM only: fuel, memory, time).

Validation enforces: exactly one of wasm/native, non-empty phases, no duplicate plugin names, and route plugins references must name a defined plugin.

Phase contract

Plugins hook into the request lifecycle at defined phases:

PhaseDescription
request_headersAfter route resolution, before authn.
request_bodyAfter authn/authz/rate-limit, before upstream.
response_headersAfter upstream response headers arrive, before masking.
response_bodyAfter masking, before compression.

A plugin can hook multiple phases. A native filter can short-circuit with a local response at any phase, exactly as a WASM plugin can via proxy_send_http_response.

Writing a native filter

A native filter is a Rust type implementing the NativeFilter trait. Each method receives the current headers/body by value and returns a FilterOutcome:

rust
use dwara_core::plugins::{NativeFilter, FilterOutcome};

pub struct AddHeaderFilter {
    name: String,
    value: String,
}

impl NativeFilter for AddHeaderFilter {
    fn on_request_headers(
        &mut self,
        mut headers: Vec<(String, String)>,
    ) -> FilterOutcome {
        headers.push((self.name.clone(), self.value.clone()));
        FilterOutcome::Continue { headers, body: Vec::new() }
    }
}

The methods are synchronous, matching the WASM runner's synchronous phase methods. A filter that does not hook a phase returns Continue with the input unchanged (the default trait method does this).

FilterOutcome

  • Continue -- proceed to the next plugin/phase. The (possibly modified) headers/body are threaded through.
  • LocalResponse -- short-circuit with a local response; the proxy returns it immediately.
  • Error -- the filter failed (the proxy returns a 500, mirroring a WASM trap). The message is logged and never leaked to the client.

Registration

Compiled-in filters register themselves at startup via NativeRegistry::register -- a simple function the binary calls (no inventory/linkme, dependency-free):

rust
use dwara_core::plugins::{NativeRegistry, NativeFilterFactory};

let registry = NativeRegistry::new();
registry.register("add-header", Box::new(|_cfg| {
    Ok(Box::new(AddHeaderFilter {
        name: "x-native".into(),
        value: "dwara".into(),
    }))
})).unwrap();

The factory receives the plugin's opaque config string (the same blob a WASM plugin gets via proxy_on_configure); a native filter parses it itself (typically JSON or YAML).

Relationship to proxy-wasm plugins

AspectNative filterWASM plugin
ImplementationRust, compiled in.wasm module, sandboxed
Selectionnative: <name>wasm: <path>
Phase contractidenticalidentical
Sandboxnone (in-process)wasmtime (fuel, memory, time)
Hot-loadno (build-time)yes (startup/reload)
Portabilityno (Rust + dwara-core)yes (community filters)

Both share the same phase slot on a route, selected by config, with no dataplane-visible difference in attachment semantics.

Runnable demo

Run the demo stack: demos/08-extensibility/ in the repository (test script: test-01-native-plugins.sh). The default demo image builds without the plugins cargo feature, so the script documents the native-filter config shape and verifies the proxy path; the README covers prerequisites, custom builds, and teardown.