A native HTTP filter is Rust code compiled into Arion itself — the same mechanism behind the built-in RBAC, JWT, CORS, and MCP gateway filters. This page maps the seams a contributor touches to add one; it teaches the shape, not a full tutorial.
Why it matters
Native filters beat WASM where the feature belongs to everyone: always-on behaviour, zero serialization across a guest boundary, direct access to internals such as metrics, access logging, and streaming bodies. The price is that shipping one means changing Arion — a contribution and a rebuild, not a config change.
The config side
A filter's configuration lives in arion-configuration, in src/config/network_filters/http_connection_manager/http_filters/. Each filter is one module with a serde config struct, plus a variant on the HttpFilterType enum:
#[serde(rename_all = "snake_case", tag = "filter_type", content = "filter_settings")]
pub enum HttpFilterType {
Wasm(WasmConfig),
Rbac(HttpRbac),
RateLimit(LocalRateLimit),
ExternalProcessor(ExternalProcessor),
// ... Cors, JwtAuthentication, McpGateway, UserRateLimit, CedarPolicy
}
The Envoy dialect reaches this enum through the envoy_conversions module in http_filters.rs, where TryFrom<Any> for SupportedEnvoyFilter maps a @type URL to a protobuf decoder. Arion-original filters register URLs under the arion. namespace:
"type.googleapis.com/arion.extensions.filters.http.cedar.cedar_policy.v3.CedarPolicy" => {
ProtoCedarPolicy::decode(typed_config.value.as_slice()).map(Self::CedarPolicy)
},
Each config struct implements TryFrom for its Envoy proto, destructuring every field and passing unhandled ones through unsupported_field!. That macro is the compatibility promise in code: a field the filter does not implement makes the config refuse to load rather than being silently dropped.
The runtime side
The executable filter lives in arion-lib, under src/listeners/http_connection_manager/, and gets a variant on HttpFilterValue in src/listeners/http_filters.rs. TryFrom<HttpFilterConfig> builds the runtime filter from its config once at load; per request, FilterFactory::new_from produces a request-local instance. The chain then dispatches:
pub enum FilterDecision {
Continue,
Reroute,
DirectResponse(Box<Response<ArionResponseBody>>),
}
impl HttpFilterValue {
pub async fn apply_request(&mut self, request: &mut Request<ArionRequestBody>, ctx: &RequestCtx) -> FilterDecision {
match self {
HttpFilterValue::Rbac(rbac) => apply_authorization_rules(rbac, request),
HttpFilterValue::ExternalProcessor(ext_proc) => ext_proc.apply_request(request, ctx).await,
// ...
}
}
}
A filter implements apply_request, and apply_response if it acts on the response path. Continue passes the message on, DirectResponse answers the client and stops the chain, and Reroute re-matches the route table and resumes with the next filter. Request filters run in configured order; response filters run in reverse. Filters that skip a path — RBAC and rate limiting have no response behaviour — return Continue from the unused hook.
Per-route overrides
Route-level control flows through typed_per_filter_config, keyed by the filter's chain name. On the config side that is the FilterConfigOverride enum (today: local rate limit, RBAC, ext_proc) plus the generic FilterConfig wrapper for disabling any filter; at load time per_route_http_filters precomputes the effective filter list for every route, so request handling only does a lookup. A new filter that wants per-route settings adds a variant there and handles it in HttpFilterValue::from_filter_override.
Where tests live
End-to-end coverage sits in arion-e2e-tests/tests/ (filters_ext_proc.rs, filters_wasm.rs, filters_rate_limit.rs, ...), with configs composed by the builders in arion-e2e-tests/src/config_builder/ — a new filter adds a builder there so tests can express it.
Key terms
HttpFilterType— the serde config enum; one variant per filter.SupportedEnvoyFilter— the@typeURL registry for the Envoy dialect.HttpFilterValue— the runtime enum whoseapply_request/apply_responserun on the hot path.FilterDecision— what a hook returns: continue, re-route, or answer directly.
Next
Read the filter chain for how the chain is assembled, or start from Extending Arion if you are still choosing an extension mechanism.