Skip to content

Plugin System Overview

Dwaar’s plugin system decouples optional features from the core proxy engine. Each feature — bot detection, rate limiting, compression, security headers, authentication — is a DwaarPlugin that hooks into three Pingora proxy phases. Plugins are composed into a PluginChain, sorted once by priority at startup, and reused lock-free across all worker threads for every request.


Request hooks run in ascending priority order. If any plugin returns Respond, the chain stops and that response is sent immediately — the upstream is never contacted. Skip stops the chain without sending a response. Response and body hooks always run in the same ascending order; they can modify headers and transform body chunks but cannot short-circuit (the response is already being built).


pub trait DwaarPlugin: Send + Sync {
/// Human-readable name for logging and diagnostics.
fn name(&self) -> &'static str;
/// Execution priority — lower values run first.
fn priority(&self) -> u16;
/// Called during `request_filter()`. Inspect request headers, populate
/// context, or short-circuit with a response.
fn on_request(&self, _req: &RequestHeader, _ctx: &mut PluginCtx) -> PluginAction {
PluginAction::Continue
}
/// Called during `response_filter()`. Modify response headers or set
/// up per-request state for body processing.
fn on_response(&self, _resp: &mut ResponseHeader, _ctx: &mut PluginCtx) -> PluginAction {
PluginAction::Continue
}
/// Called during `response_body_filter()` for each body chunk.
/// Transform the body in-place (e.g., compression).
fn on_body(
&self,
_body: &mut Option<Bytes>,
_end_of_stream: bool,
_ctx: &mut PluginCtx,
) -> PluginAction {
PluginAction::Continue
}
}

All hooks have default implementations that return Continue. Override only the hooks your plugin needs.

Hooks are synchronous — they run inline on Pingora’s worker threads. Keep them fast. Use BackgroundService for any work that requires async I/O.


PluginCtx is per-request state shared across all plugins. The proxy engine populates it before the chain runs; plugins read and write it to communicate.

FieldTypePopulated byDescription
client_ipOption<IpAddr>proxy engineClient TCP address
hostOption<CompactString>proxy engineHost header value
methodCompactStringproxy engineHTTP method (GET, POST, …)
pathCompactStringproxy engineRequest path
is_tlsboolproxy enginetrue if the downstream connection used TLS
accept_encodingCompactStringproxy engineAccept-Encoding header value
rate_limit_rpsOption<u32>proxy enginePer-route requests-per-second limit; None means no limit
route_domainOption<CompactString>proxy engineCanonical domain from matched route; used as rate-limiter key
under_attackboolproxy enginetrue when Under Attack Mode is active for this route
ip_filterOption<Arc<IpFilterConfig>>proxy enginePer-route IP filter config; None means no filtering
is_botboolBotDetectPlugintrue when the client is a known bot
bot_categoryOption<BotCategory>BotDetectPluginBot category (search, scraper, malicious, …)
countryOption<CompactString>GeoIP / pluginISO 3166-1 alpha-2 country code
compressorOption<ResponseCompressor>CompressPluginNegotiated compression algorithm for this response
rate_limitedboolRateLimitPlugintrue when this request was rejected by rate limiting

String fields use CompactString, which stores up to 24 bytes inline with no heap allocation. HTTP methods, hostnames, country codes, and most header values fit inline.


Every hook returns a PluginAction that tells the chain what to do next.

VariantEffect on run_requestEffect on run_response / run_body
ContinuePass to the next pluginPass to the next plugin
Respond(PluginResponse)Stop chain; send this response to the clientStop chain; response already committed
SkipStop chain; continue normal proxy flow (no response sent)Stop chain

PluginResponse carries a status code (u16), a list of (&'static str, String) headers, and a Bytes body. Use it to return synthetic responses like 429 Too Many Requests or 403 Forbidden.


Lower priority values run first. The built-in plugins are spaced to leave room for custom plugins at any point in the chain.

PriorityPluginPhase(s)
5UnderAttackPluginrequest
8IpFilterPluginrequest
10BotDetectPluginrequest
15UnderAttackPlugin (challenge check)request
20RateLimitPluginrequest
90CompressPluginresponse, body
100SecurityHeadersPluginresponse

WASM plugins are assigned a priority at load time via the priority field in your Dwaarfile route block. Use any value in the gaps above to interleave native and WASM plugins.


PluginDescriptionDoc
UnderAttackPluginPresents a JS challenge page when Under Attack Mode is active for a routenative-plugins.md
IpFilterPluginAllows or blocks requests based on per-route IP allowlists and denylistsnative-plugins.md
BotDetectPluginClassifies clients by User-Agent into search, scraper, malicious, and human categoriesnative-plugins.md
RateLimitPluginEnforces per-route requests-per-second limits; applies stricter limits to botsnative-plugins.md
CompressPluginNegotiates and applies Brotli, Gzip, or Deflate compression for compressible responsesnative-plugins.md
SecurityHeadersPluginAdds X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and related security headersnative-plugins.md

BasicAuthConfig and ForwardAuthConfig are route-level middleware that wrap into request-phase logic. They are not registered in the PluginChain but follow the same pattern of reading PluginCtx and returning early responses.


TypeLanguageOverheadIsolationUse case
Native RustRustZero — inline on the hot pathNone (shares process)High-throughput built-in features
WASMAny wasm32-wasip2 targetWasmtime overhead per callFull sandboxCustom business logic, third-party extensions

Native plugins are compiled into the Dwaar binary and share memory with the proxy engine. WASM plugins are loaded from .wasm files at startup, sandboxed by Wasmtime, and communicate with the host via a WIT-defined interface.

See Native Plugin Development and WASM Plugins for implementation guides.