API reference
The public surface of actix_guard_rs 1.0.0. The full crate documentation
is also in src/lib.rs (build it with cargo doc --open).
Middleware
GuardTransform
The actix-web middleware entry point. GuardTransform implements
Transform<ServiceRequest>; register it with
App::wrap (or a web::scope's wrap) so every request routed after it is
screened before it reaches the wrapped service.
use actix_guard_rs::{default_config, GuardTransform};
use actix_web::{App, web};
let app = App::new().wrap(GuardTransform::new(default_config())).route(
"/",
web::to(|| async { "ok" }),
);
Constructors and builders:
| Method | Description |
|---|---|
GuardTransform::new(config: DetectConfig) |
Build a transform from an engine DetectConfig. The body buffering cap starts at config.max_full_scan_bytes |
GuardTransform::with_defaults() |
Build a transform with default_config() |
.with_body_cap(body_cap: usize) |
Replace the body buffering cap, in bytes. A body larger than the cap is rejected with 413. A cap of 0 rejects every request that carries a non-empty body |
.with_ip_gate(ip_gate: IpGateConfig) |
Install the global IP gate (see below) |
The transform clones into a per-worker GuardService; actix Web builds its
service tree per worker on single-threaded event loops, and the wrapped
service is shared through an Rc, so this never crosses threads.
GuardService
The Service produced by GuardTransform. It screens a ServiceRequest by
running one engine call per request view and, on a clean pass, hands the next
service a rebuilt request whose body is byte-identical to what the client
sent (actix Web consumes the payload as it is read, so the middleware buffers
it under the cap and rebuilds the request).
Configuration
default_config()
Returns the reference default DetectConfig. The engine's DetectConfig
carries no Default impl, so the adapter pins the ecosystem defaults here;
they are the values the engine conformance corpus records:
| Knob | Value |
|---|---|
max_content_length |
10_000 |
max_full_scan_bytes |
262_144 |
preserve_attack_patterns |
true |
semantic_threshold |
0.7 |
threat_score_threshold |
1.0 |
DetectConfig
Re-exported from guard_core_engine::detect. Fields:
| Field | Type | Meaning |
|---|---|---|
max_content_length |
usize |
Semantic budget and truncation budget |
max_full_scan_bytes |
usize |
Preprocessor full-scan cap (also the default body cap) |
preserve_attack_patterns |
bool |
Keep attack patterns in the processed view |
semantic_threshold |
f64 |
Semantic analysis threshold |
threat_score_threshold |
f64 |
Threat score threshold for a verdict |
A custom configuration:
use actix_guard_rs::DetectConfig;
let config = DetectConfig {
max_content_length: 10_000,
max_full_scan_bytes: 262_144,
preserve_attack_patterns: true,
semantic_threshold: 0.7,
threat_score_threshold: 1.0,
};
Behavior
What it inspects
One engine call per request view:
| Request part | Engine context | Notes |
|---|---|---|
| Path | url_path |
Skipped for / |
| Query string | query_param |
Skipped when empty |
| Header values | header |
Skips sec-* and the negotiation/routing headers (Host, User-Agent, Accept, Accept-Encoding, Connection, Origin, Referer) |
| Body | request_body |
Buffered first, capped |
The HTTP method is not scanned.
The IP gate: IpGateConfig
Built with IpGateConfig::new(whitelist, blacklist, exempt_ips), which fails
closed on an invalid entry (IpGateError names the list and the entry):
use actix_guard_rs::{GuardTransform, IpGateConfig};
let gate = IpGateConfig::new(
[] as [&str; 0],
["203.0.113.9"],
["198.51.100.7", "198.51.100.16/28"],
)
.expect("valid lists");
let transform = GuardTransform::new(actix_guard_rs::default_config()).with_ip_gate(gate);
Evaluation order mirrors the reference engine: with a non-empty whitelist,
an IP matching neither the whitelist nor exempt_ips is denied; otherwise a
blacklist hit is denied. Both denials answer 403 Forbidden with
Forbidden before body buffering. The client IP is the resolved client (the
peer address, or the forwarded client behind a trusted proxy, see
GuardTransform::with_client_policy); a request without one is not
attributed, the gate does not run, and detection still screens it (a config
built with from_security_config rejects it under fail_secure). A passed request gets an
IpGateDecision { is_whitelisted, is_exempt } inserted into its request
extensions so downstream handlers can read the skip state.
exempt_ips vs whitelist. exempt_ips is noise reduction for
known-friendly automation (monitoring probes, VPN egress, a partner's
server), not immunity: it sets the same skip state a whitelist match sets but
never adds a deny path and never opens the whitelist gate. The blacklist,
route rules, and detection still apply to exempt IPs - an attack payload from
an exempt IP is still 400 Suspicious activity detected. The stateful stages
(GuardTransform::with_rate_limiting, GuardTransform::with_ip_banning) skip
exactly what the reference skips for a whitelist match
(is_whitelisted || is_exempt): rate limiting, violation counting, and
banning. Detection never skips anything.
The rate limiter: RateLimiter
use actix_guard_rs::{GuardTransform, RateLimitConfig, RateLimiter};
let limiter = RateLimiter::new(RateLimitConfig {
enable_rate_limiting: true,
rate_limit: 30,
rate_limit_window: 10,
..RateLimitConfig::default()
})
.expect("valid config");
let transform =
GuardTransform::new(actix_guard_rs::default_config()).with_rate_limiting(limiter);
The limiter's constructor fails closed on a zero limit or window. Installed
with GuardTransform::with_rate_limiting, it runs after the IP gate and the
ban stage, before body buffering and detection: a crossing answers
429 Too Many Requests with Retry-After: <window seconds>. With
enable_rate_limit_auto_ban on and IP banning configured, every crossing
counts one rate_limit violation toward the auto-ban engine; the response
stays 429 and the ban bites on the next request. Requests without a peer
address cannot be attributed and are not rate limited; detection still
screens them. The limiter is shared across workers through an Arc, and its
handles are cheaply clonable, so out-of-band handles work alongside it.
The ban stage: IpBanManager + IpBanConfig
use actix_guard_rs::{GuardTransform, IpBanConfig, IpBanManager, ThreatBanEntry};
let manager = IpBanManager::new();
let config = IpBanConfig::new(
true,
10,
3600,
[("sqli", ThreatBanEntry { threshold: 3, duration: 1800 })],
)
.expect("valid config");
let transform =
GuardTransform::new(actix_guard_rs::default_config()).with_ip_banning(manager, config);
The config constructor fails closed on an invalid threat_ban_config
entry. Installed with GuardTransform::with_ip_banning, the ban check runs
before the limiter: a live ban answers 403 Forbidden (IP address banned)
and banned traffic never consumes rate budget. Every detected threat counts
its categories per client IP (the reference pipeline's suspicious-activity
stage), and a crossed threat_ban_config entry or the flat
auto_ban_threshold bans on the spot, answering 403 Forbidden
(IP has been banned); without a crossing the block keeps the
400 Bad Request (Suspicious activity detected) shape.
config.enable_ip_banning = false counts violations but never bans.
Whitelisted and exempt IPs are never counted, so they can never be
auto-banned. The store pair is shared across workers through an Arc.
Responses
| Situation | Status | Body |
|---|---|---|
| The IP gate denies the client IP | 403 Forbidden |
Forbidden |
| A live ban on the client IP | 403 Forbidden |
IP address banned |
| Rate limit crossed | 429 Too Many Requests (+ Retry-After: <window>) |
Too many requests |
| Engine flags a view | 400 Bad Request |
Suspicious activity detected |
| Engine flags a view and a crossed auto-ban threshold bans on the spot | 403 Forbidden |
IP has been banned |
| Body exceeds the cap | 413 Payload Too Large |
Payload too large |
| Body read error or engine panic | 500 Internal Server Error |
Security check failed |
The adapter is fail-secure: any failure to complete the security check
answers 500, never an uninspected passthrough. Engine panics are caught
with catch_unwind on the worker thread (note that panic = "abort" in a
release profile disables that recovery).
Constants
Re-exported refusal message bodies:
| Constant | Value |
|---|---|
BLOCKED_MESSAGE |
"Suspicious activity detected" |
FORBIDDEN_MESSAGE |
"Forbidden" |
BANNED_MESSAGE |
"IP address banned" |
ACTIVITY_BANNED_MESSAGE |
"IP has been banned" |
RATE_LIMITED_MESSAGE |
"Too many requests" |
OVERSIZE_MESSAGE |
"Payload too large" |
FAILURE_MESSAGE |
"Security check failed" |
Engine re-exports
DetectConfig, DetectVerdict, and Threat are re-exported from
guard_core_engine::detect.
IpGateConfig, IpGateDecision, IpGateDenial, IpGateError, and
IpGateVerdict are re-exported from guard_core_engine::ip_gate.
RateLimiter, RateLimitConfig, RateLimitConfigError, and
RateLimitDecision are re-exported from guard_core_engine::rate_limit.
IpBanManager, IpBanConfig, IpBanConfigError, BanError, BanRecord,
Clock, ResolvedBan, ThreatBanEntry, and ViolationCounters are
re-exported from guard_core_engine::ip_ban. A DetectVerdict carries is_threat, a
threat_score, and the list of Threat findings (regex or semantic).