1use std::net::IpAddr;
5use std::time::SystemTime;
6
7use serde::Deserialize;
8
9pub use bytes::Bytes;
10
11pub mod network;
12pub use network::{ClientIpResolver, IpSource, ResolvedClientIp};
13
14pub mod state;
15pub use state::{
16 Acquired, BucketParams, Clock, InMemoryStateStore, ManualClock, RateLimitState, StateStore,
17 SystemClock,
18};
19
20#[cfg(feature = "testkit")]
21pub mod testkit;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Decision {
27 Allow,
28 Block { rule_id: String, reason: String },
29 Monitor { rule_id: String },
30 Score { rule_id: String, points: u32 },
33 Scores(Vec<ScoreItem>),
38 Reject {
43 rule_id: String,
44 reason: String,
45 status: u16,
46 retry_after: Option<u64>,
47 },
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ScoreItem {
55 pub rule_id: String,
56 pub severity: Severity,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum Severity {
63 Critical,
64 Error,
65 Warning,
66 Notice,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum Phase {
71 Connection,
72 RequestLine,
73 Headers,
74 Body,
75 Response,
76}
77
78pub trait WafModule: Send + Sync {
79 fn id(&self) -> &str;
80 fn phase(&self) -> Phase;
81 fn init(&mut self, cfg: &Config);
83 fn inspect(&self, ctx: &RequestContext) -> Decision;
85 fn structural(&self) -> bool {
91 false
92 }
93}
94
95#[derive(Debug, Clone, Deserialize)]
105#[non_exhaustive]
106pub struct Config {
107 pub proxy: ProxyConfig,
108 pub waf: WafConfig,
109 #[serde(default)]
110 pub limits: LimitsConfig,
111 #[serde(default)]
112 pub modules: ModulesConfig,
113 #[serde(default)]
114 pub rate_limit: RateLimitConfig,
115 #[serde(default)]
118 pub network: NetworkConfig,
119 #[serde(default)]
121 pub resilience: ResilienceConfig,
122 #[serde(default)]
126 pub tls: TlsConfig,
127 #[serde(default)]
130 pub metrics: MetricsConfig,
131}
132
133impl Default for Config {
134 fn default() -> Self {
140 Self {
141 proxy: ProxyConfig::default(),
142 waf: WafConfig::default(),
143 limits: LimitsConfig::default(),
144 modules: ModulesConfig::default(),
145 rate_limit: RateLimitConfig::default(),
146 network: NetworkConfig::default(),
147 resilience: ResilienceConfig::default(),
148 tls: TlsConfig::default(),
149 metrics: MetricsConfig::default(),
150 }
151 }
152}
153
154#[derive(Debug, Clone, Deserialize)]
160pub struct MetricsConfig {
161 #[serde(default)]
163 pub enabled: bool,
164 #[serde(default = "default_metrics_listen")]
166 pub listen: std::net::SocketAddr,
167}
168
169fn default_metrics_listen() -> std::net::SocketAddr {
170 "127.0.0.1:9090".parse().expect("valid loopback addr")
171}
172
173impl Default for MetricsConfig {
174 fn default() -> Self {
175 Self { enabled: false, listen: default_metrics_listen() }
176 }
177}
178
179#[derive(Debug, Clone, Deserialize)]
189#[non_exhaustive]
190pub struct TlsConfig {
191 #[serde(default)]
195 pub enabled: bool,
196 #[serde(default)]
198 pub cert_path: String,
199 #[serde(default)]
201 pub key_path: String,
202 #[serde(default = "default_tls_alpn")]
206 pub alpn: Vec<String>,
207}
208
209fn default_tls_alpn() -> Vec<String> {
210 vec!["h2".to_string(), "http/1.1".to_string()]
211}
212
213impl Default for TlsConfig {
214 fn default() -> Self {
215 Self {
216 enabled: false,
217 cert_path: String::new(),
218 key_path: String::new(),
219 alpn: default_tls_alpn(),
220 }
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum FailMode {
232 FailOpen,
234 FailClosed,
236}
237
238#[derive(Debug, Clone, Copy, Deserialize)]
241pub struct ResilienceConfig {
242 #[serde(default = "default_on_upstream_error")]
246 pub on_upstream_error: FailMode,
247 #[serde(default = "default_on_internal_error")]
250 pub on_internal_error: FailMode,
251 #[serde(default = "default_on_config_error")]
254 pub on_config_error: FailMode,
255 #[serde(default = "default_on_parser_limit")]
258 pub on_parser_limit: FailMode,
259 #[serde(default = "default_upstream_timeout_ms")]
262 pub upstream_timeout_ms: u64,
263}
264
265fn default_on_upstream_error() -> FailMode { FailMode::FailClosed }
266fn default_on_internal_error() -> FailMode { FailMode::FailOpen }
267fn default_on_config_error() -> FailMode { FailMode::FailOpen }
268fn default_on_parser_limit() -> FailMode { FailMode::FailClosed }
269fn default_upstream_timeout_ms() -> u64 { 30_000 }
270
271impl Default for ResilienceConfig {
272 fn default() -> Self {
273 Self {
274 on_upstream_error: default_on_upstream_error(),
275 on_internal_error: default_on_internal_error(),
276 on_config_error: default_on_config_error(),
277 on_parser_limit: default_on_parser_limit(),
278 upstream_timeout_ms: default_upstream_timeout_ms(),
279 }
280 }
281}
282
283impl ResilienceConfig {
284 pub fn upstream_timeout(&self) -> std::time::Duration {
286 std::time::Duration::from_millis(self.upstream_timeout_ms)
287 }
288}
289
290#[derive(Debug, Clone, Deserialize)]
296#[non_exhaustive]
297pub struct NetworkConfig {
298 #[serde(default)]
301 pub trusted_proxies: Vec<String>,
302 #[serde(default = "default_client_ip_header")]
304 pub client_ip_header: String,
305 #[serde(default = "default_trusted_hops")]
308 pub trusted_hops: usize,
309}
310
311fn default_client_ip_header() -> String { "x-forwarded-for".to_string() }
312fn default_trusted_hops() -> usize { 1 }
313
314impl Default for NetworkConfig {
315 fn default() -> Self {
316 Self {
317 trusted_proxies: Vec::new(),
318 client_ip_header: default_client_ip_header(),
319 trusted_hops: default_trusted_hops(),
320 }
321 }
322}
323
324pub const MAX_PARANOIA_LEVEL: u8 = 4;
330pub const MAX_TRUSTED_HOPS: usize = 10;
333
334#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum ConfigError {
338 InvalidBackend(String),
339 BlockThresholdZero,
340 ParanoiaOutOfRange(u8),
341 SeverityWeightZero(&'static str),
342 LimitZero(&'static str),
343 RateLimitValueZero(&'static str),
344 TrustedHopsOutOfRange(usize),
345 InvalidCidr(String),
346 EmptyClientIpHeader,
347 ResilienceTimeoutZero,
348 GraphqlCapZero(&'static str),
349 GrpcCapZero(&'static str),
350 TlsPathEmpty(&'static str),
351 TlsAlpnInvalid,
352 CrsNoFiles,
353 WasmNoPlugins,
354 WasmPluginPathEmpty,
355}
356
357impl std::fmt::Display for ConfigError {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 Self::InvalidBackend(b) => write!(
361 f,
362 "proxy.backend must be an absolute http(s) URL with a host, got {b:?}"
363 ),
364 Self::BlockThresholdZero =>
365 write!(f, "waf.block_threshold must be >= 1 (0 would block everything)"),
366 Self::ParanoiaOutOfRange(p) =>
367 write!(f, "waf.paranoia_level must be in 1..={MAX_PARANOIA_LEVEL}, got {p}"),
368 Self::SeverityWeightZero(name) =>
369 write!(f, "waf.severity_scores.{name} must be >= 1 (a 0 weight makes the rule contribute nothing)"),
370 Self::LimitZero(name) =>
371 write!(f, "limits.{name} must be >= 1"),
372 Self::RateLimitValueZero(name) =>
373 write!(f, "rate_limit.{name} must be >= 1 when rate limiting is enabled"),
374 Self::TrustedHopsOutOfRange(h) =>
375 write!(f, "network.trusted_hops must be in 1..={MAX_TRUSTED_HOPS}, got {h}"),
376 Self::InvalidCidr(c) =>
377 write!(f, "network.trusted_proxies contains an invalid CIDR: {c:?}"),
378 Self::EmptyClientIpHeader =>
379 write!(f, "network.client_ip_header must not be empty"),
380 Self::ResilienceTimeoutZero =>
381 write!(f, "resilience.upstream_timeout_ms must be >= 1"),
382 Self::GraphqlCapZero(name) =>
383 write!(f, "modules.graphql.{name} must be >= 1 when the graphql module is enabled"),
384 Self::GrpcCapZero(name) =>
385 write!(f, "modules.grpc.{name} must be >= 1 when the grpc module is enabled"),
386 Self::TlsPathEmpty(name) =>
387 write!(f, "tls.{name} must be set (a PEM file path) when tls is enabled"),
388 Self::TlsAlpnInvalid =>
389 write!(f, "tls.alpn must be a non-empty list of non-empty protocol ids (e.g. \"h2\", \"http/1.1\")"),
390 Self::CrsNoFiles =>
391 write!(f, "modules.crs.files must list at least one file when the crs module is enabled"),
392 Self::WasmNoPlugins =>
393 write!(f, "modules.wasm.plugins must list at least one plugin when the wasm module is enabled"),
394 Self::WasmPluginPathEmpty =>
395 write!(f, "modules.wasm.plugins[].path must be a non-empty .wasm file path"),
396 }
397 }
398}
399
400impl std::error::Error for ConfigError {}
401
402impl Config {
403 pub fn validate(&self) -> Result<(), ConfigError> {
407 let backend = self.proxy.backend.trim();
409 let authority = backend
410 .strip_prefix("http://")
411 .or_else(|| backend.strip_prefix("https://"));
412 match authority {
413 Some(a) if !a.is_empty() && !a.starts_with('/') => {}
414 _ => return Err(ConfigError::InvalidBackend(self.proxy.backend.clone())),
415 }
416
417 if self.waf.block_threshold == 0 {
419 return Err(ConfigError::BlockThresholdZero);
420 }
421 if !(1..=MAX_PARANOIA_LEVEL).contains(&self.waf.paranoia_level) {
422 return Err(ConfigError::ParanoiaOutOfRange(self.waf.paranoia_level));
423 }
424 let s = &self.waf.severity_scores;
425 for (name, v) in [
426 ("critical", s.critical),
427 ("error", s.error),
428 ("warning", s.warning),
429 ("notice", s.notice),
430 ] {
431 if v == 0 {
432 return Err(ConfigError::SeverityWeightZero(name));
433 }
434 }
435
436 let l = &self.limits;
438 for (name, v) in [
439 ("max_body_size", l.max_body_size),
440 ("max_header_size", l.max_header_size),
441 ("max_headers", l.max_headers),
442 ("max_params", l.max_params),
443 ("max_cookies", l.max_cookies),
444 ("max_json_depth", l.max_json_depth),
445 ] {
446 if v == 0 {
447 return Err(ConfigError::LimitZero(name));
448 }
449 }
450
451 if self.rate_limit.enabled {
453 let r = &self.rate_limit;
454 if r.requests == 0 {
455 return Err(ConfigError::RateLimitValueZero("requests"));
456 }
457 if r.window_seconds == 0 {
458 return Err(ConfigError::RateLimitValueZero("window_seconds"));
459 }
460 if matches!(r.burst, Some(0)) {
461 return Err(ConfigError::RateLimitValueZero("burst"));
462 }
463 if r.max_tracked_keys == 0 {
464 return Err(ConfigError::RateLimitValueZero("max_tracked_keys"));
465 }
466 if r.action == RateLimitAction::Score && r.score == 0 {
467 return Err(ConfigError::RateLimitValueZero("score"));
468 }
469 }
470
471 if !(1..=MAX_TRUSTED_HOPS).contains(&self.network.trusted_hops) {
473 return Err(ConfigError::TrustedHopsOutOfRange(self.network.trusted_hops));
474 }
475 for cidr in &self.network.trusted_proxies {
476 if !network::is_valid_cidr(cidr) {
477 return Err(ConfigError::InvalidCidr(cidr.clone()));
478 }
479 }
480 if self.network.client_ip_header.trim().is_empty() {
481 return Err(ConfigError::EmptyClientIpHeader);
482 }
483
484 if self.resilience.upstream_timeout_ms == 0 {
486 return Err(ConfigError::ResilienceTimeoutZero);
487 }
488
489 if self.tls.enabled {
493 if self.tls.cert_path.trim().is_empty() {
494 return Err(ConfigError::TlsPathEmpty("cert_path"));
495 }
496 if self.tls.key_path.trim().is_empty() {
497 return Err(ConfigError::TlsPathEmpty("key_path"));
498 }
499 if self.tls.alpn.is_empty() || self.tls.alpn.iter().any(|p| p.trim().is_empty()) {
500 return Err(ConfigError::TlsAlpnInvalid);
501 }
502 }
503
504 if self.modules.graphql.enabled {
506 let g = &self.modules.graphql;
507 for (name, v) in [
508 ("max_depth", g.max_depth),
509 ("max_aliases", g.max_aliases),
510 ("max_fields", g.max_fields),
511 ("max_directives", g.max_directives),
512 ("max_batch", g.max_batch),
513 ] {
514 if v == 0 {
515 return Err(ConfigError::GraphqlCapZero(name));
516 }
517 }
518 }
519
520 if self.modules.grpc.enabled {
522 let g = &self.modules.grpc;
523 for (name, zero) in [
524 ("max_message_bytes", g.max_message_bytes == 0),
525 ("max_fields", g.max_fields == 0),
526 ("max_depth", g.max_depth == 0),
527 ] {
528 if zero {
529 return Err(ConfigError::GrpcCapZero(name));
530 }
531 }
532 }
533
534 if self.modules.crs.enabled && self.modules.crs.files.is_empty() {
537 return Err(ConfigError::CrsNoFiles);
538 }
539
540 if self.modules.wasm.enabled {
543 if self.modules.wasm.plugins.is_empty() {
544 return Err(ConfigError::WasmNoPlugins);
545 }
546 if self.modules.wasm.plugins.iter().any(|p| p.path.trim().is_empty()) {
547 return Err(ConfigError::WasmPluginPathEmpty);
548 }
549 }
550
551 Ok(())
552 }
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
560#[serde(rename_all = "snake_case")]
561pub enum RateLimitKey {
562 #[default]
563 ClientIp,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
568#[serde(rename_all = "snake_case")]
569pub enum RateLimitAction {
570 #[default]
572 Block,
573 Score,
575}
576
577#[derive(Debug, Clone, Deserialize)]
578pub struct RateLimitConfig {
579 #[serde(default)]
581 pub enabled: bool,
582 #[serde(default)]
583 pub key: RateLimitKey,
584 #[serde(default = "default_rl_requests")]
586 pub requests: u32,
587 #[serde(default = "default_rl_window")]
588 pub window_seconds: u64,
589 #[serde(default)]
591 pub burst: Option<u32>,
592 #[serde(default)]
593 pub action: RateLimitAction,
594 #[serde(default = "default_rl_score")]
596 pub score: u32,
597 #[serde(default = "default_rl_max_keys")]
600 pub max_tracked_keys: usize,
601}
602
603fn default_rl_requests() -> u32 { 100 }
604fn default_rl_window() -> u64 { 60 }
605fn default_rl_score() -> u32 { 5 }
606fn default_rl_max_keys() -> usize { 100_000 }
607
608impl Default for RateLimitConfig {
609 fn default() -> Self {
610 Self {
611 enabled: false,
612 key: RateLimitKey::ClientIp,
613 requests: default_rl_requests(),
614 window_seconds: default_rl_window(),
615 burst: None,
616 action: RateLimitAction::Block,
617 score: default_rl_score(),
618 max_tracked_keys: default_rl_max_keys(),
619 }
620 }
621}
622
623#[derive(Debug, Clone, Deserialize, Default)]
624pub struct ModulesConfig {
625 #[serde(default)]
626 pub sqli: ModuleConfig,
627 #[serde(default)]
628 pub xss: ModuleConfig,
629 #[serde(default)]
630 pub path_traversal: ModuleConfig,
631 #[serde(default)]
632 pub rce: ModuleConfig,
633 #[serde(default)]
634 pub lfi_rfi: ModuleConfig,
635 #[serde(default)]
636 pub ssrf: ModuleConfig,
637 #[serde(default)]
638 pub ldap: ModuleConfig,
639 #[serde(default)]
640 pub nosql: ModuleConfig,
641 #[serde(default)]
642 pub mail: ModuleConfig,
643 #[serde(default)]
644 pub ssti: ModuleConfig,
645 #[serde(default)]
646 pub scanner: ModuleConfig,
647 #[serde(default)]
648 pub ssi: ModuleConfig,
649 #[serde(default)]
650 pub xxe: ModuleConfig,
651 #[serde(default)]
652 pub header_injection: ModuleConfig,
653 #[serde(default)]
656 pub request_smuggling: ModuleConfig,
657 #[serde(default)]
661 pub graphql: GraphqlConfig,
662 #[serde(default)]
667 pub grpc: GrpcConfig,
668 #[serde(default)]
671 pub crs: CrsConfig,
672 #[serde(default)]
675 pub wasm: WasmConfig,
676}
677
678#[derive(Debug, Clone, Default, Deserialize)]
683pub struct CrsConfig {
684 #[serde(default)]
686 pub enabled: bool,
687 #[serde(default)]
690 pub files: Vec<String>,
691}
692
693#[derive(Debug, Clone, Deserialize)]
698pub struct WasmConfig {
699 #[serde(default)]
701 pub enabled: bool,
702 #[serde(default = "default_wasm_pool_size")]
705 pub pool_size: usize,
706 #[serde(default = "default_wasm_fuel")]
709 pub fuel_per_request: u64,
710 #[serde(default = "default_wasm_max_memory")]
712 pub max_memory_bytes: usize,
713 #[serde(default = "default_wasm_checkout_timeout_ms")]
715 pub checkout_timeout_ms: u64,
716 #[serde(default)]
718 pub plugins: Vec<WasmPluginConfig>,
719}
720
721fn default_wasm_pool_size() -> usize { 4 }
722fn default_wasm_fuel() -> u64 { 10_000_000 }
723fn default_wasm_max_memory() -> usize { 16 * 1024 * 1024 }
724fn default_wasm_checkout_timeout_ms() -> u64 { 100 }
725
726impl Default for WasmConfig {
727 fn default() -> Self {
728 WasmConfig {
729 enabled: false,
730 pool_size: default_wasm_pool_size(),
731 fuel_per_request: default_wasm_fuel(),
732 max_memory_bytes: default_wasm_max_memory(),
733 checkout_timeout_ms: default_wasm_checkout_timeout_ms(),
734 plugins: Vec::new(),
735 }
736 }
737}
738
739#[derive(Debug, Clone, Default, Deserialize)]
742pub struct WasmPluginConfig {
743 pub path: String,
744 #[serde(default)]
745 pub config: Option<String>,
746}
747
748#[derive(Debug, Clone, Deserialize)]
749pub struct ModuleConfig {
750 #[serde(default = "default_true")]
751 pub enabled: bool,
752}
753
754fn default_true() -> bool { true }
755
756impl Default for ModuleConfig {
757 fn default() -> Self {
758 Self { enabled: true }
759 }
760}
761
762#[derive(Debug, Clone, Deserialize)]
766pub struct GraphqlConfig {
767 #[serde(default)]
769 pub enabled: bool,
770 #[serde(default = "default_graphql_paths")]
775 pub paths: Vec<String>,
776 #[serde(default = "default_graphql_max_depth")]
778 pub max_depth: u32,
779 #[serde(default = "default_graphql_max_aliases")]
781 pub max_aliases: u32,
782 #[serde(default = "default_graphql_max_fields")]
784 pub max_fields: u32,
785 #[serde(default = "default_graphql_max_directives")]
787 pub max_directives: u32,
788 #[serde(default = "default_graphql_max_batch")]
790 pub max_batch: u32,
791 #[serde(default)]
793 pub block_introspection: bool,
794}
795
796fn default_graphql_paths() -> Vec<String> { vec!["/graphql".to_string()] }
797fn default_graphql_max_depth() -> u32 { 15 }
798fn default_graphql_max_aliases() -> u32 { 30 }
799fn default_graphql_max_fields() -> u32 { 1000 }
800fn default_graphql_max_directives() -> u32 { 50 }
801fn default_graphql_max_batch() -> u32 { 10 }
802
803impl Default for GraphqlConfig {
804 fn default() -> Self {
805 Self {
806 enabled: false,
807 paths: default_graphql_paths(),
808 max_depth: default_graphql_max_depth(),
809 max_aliases: default_graphql_max_aliases(),
810 max_fields: default_graphql_max_fields(),
811 max_directives: default_graphql_max_directives(),
812 max_batch: default_graphql_max_batch(),
813 block_introspection: false,
814 }
815 }
816}
817
818#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
824#[serde(rename_all = "snake_case")]
825pub enum CompressedPolicy {
826 Reject,
827 Passthrough,
828}
829
830#[derive(Debug, Clone, Deserialize)]
836pub struct GrpcConfig {
837 #[serde(default)]
839 pub enabled: bool,
840 #[serde(default = "default_grpc_max_message_bytes")]
842 pub max_message_bytes: u64,
843 #[serde(default = "default_grpc_max_fields")]
845 pub max_fields: u32,
846 #[serde(default = "default_grpc_max_depth")]
848 pub max_depth: u32,
849 #[serde(default = "default_grpc_on_compressed")]
851 pub on_compressed: CompressedPolicy,
852}
853
854fn default_grpc_max_message_bytes() -> u64 { 4 * 1024 * 1024 }
855fn default_grpc_max_fields() -> u32 { 4096 }
856fn default_grpc_max_depth() -> u32 { 16 }
857fn default_grpc_on_compressed() -> CompressedPolicy { CompressedPolicy::Reject }
858
859impl Default for GrpcConfig {
860 fn default() -> Self {
861 Self {
862 enabled: false,
863 max_message_bytes: default_grpc_max_message_bytes(),
864 max_fields: default_grpc_max_fields(),
865 max_depth: default_grpc_max_depth(),
866 on_compressed: default_grpc_on_compressed(),
867 }
868 }
869}
870
871#[derive(Debug, Clone, Copy, Deserialize)]
874pub struct SeverityScores {
875 #[serde(default = "default_critical_score")]
876 pub critical: u32,
877 #[serde(default = "default_error_score")]
878 pub error: u32,
879 #[serde(default = "default_warning_score")]
880 pub warning: u32,
881 #[serde(default = "default_notice_score")]
882 pub notice: u32,
883}
884
885fn default_critical_score() -> u32 { 6 }
890fn default_error_score() -> u32 { 4 }
891fn default_warning_score() -> u32 { 3 }
892fn default_notice_score() -> u32 { 2 }
893
894impl Default for SeverityScores {
895 fn default() -> Self {
896 Self {
897 critical: default_critical_score(),
898 error: default_error_score(),
899 warning: default_warning_score(),
900 notice: default_notice_score(),
901 }
902 }
903}
904
905impl SeverityScores {
906 pub fn points_for(&self, severity: Severity) -> u32 {
908 match severity {
909 Severity::Critical => self.critical,
910 Severity::Error => self.error,
911 Severity::Warning => self.warning,
912 Severity::Notice => self.notice,
913 }
914 }
915}
916
917#[derive(Debug, Clone, Deserialize)]
918pub struct ProxyConfig {
919 pub listen: std::net::SocketAddr,
920 pub backend: String,
921}
922
923#[derive(Debug, Clone, Deserialize)]
924pub struct WafConfig {
925 pub mode: WafMode,
926 #[serde(default = "default_block_threshold")]
930 pub block_threshold: u32,
931 #[serde(default = "default_paranoia_level")]
933 pub paranoia_level: u8,
934 #[serde(default)]
936 pub severity_scores: SeverityScores,
937}
938
939fn default_block_threshold() -> u32 {
940 5
941}
942
943fn default_paranoia_level() -> u8 {
944 1
945}
946
947impl Default for ProxyConfig {
948 fn default() -> Self {
951 Self {
952 listen: "127.0.0.1:8080".parse().expect("valid loopback addr"),
953 backend: "http://localhost:8080".to_string(),
954 }
955 }
956}
957
958impl Default for WafConfig {
959 fn default() -> Self {
962 Self {
963 mode: WafMode::DetectionOnly,
964 block_threshold: default_block_threshold(),
965 paranoia_level: default_paranoia_level(),
966 severity_scores: SeverityScores::default(),
967 }
968 }
969}
970
971#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
972#[serde(rename_all = "kebab-case")]
973pub enum WafMode {
974 DetectionOnly,
975 Blocking,
976}
977
978#[derive(Debug, Clone, Deserialize)]
980#[non_exhaustive]
981pub struct LimitsConfig {
982 #[serde(default = "default_max_body_size")]
983 pub max_body_size: usize,
984 #[serde(default = "default_max_header_size")]
985 pub max_header_size: usize,
986 #[serde(default = "default_max_headers")]
987 pub max_headers: usize,
988 #[serde(default = "default_max_params")]
989 pub max_params: usize,
990 #[serde(default = "default_max_cookies")]
991 pub max_cookies: usize,
992 #[serde(default = "default_max_json_depth")]
993 pub max_json_depth: usize,
994}
995
996fn default_max_body_size() -> usize { 1_048_576 } fn default_max_header_size() -> usize { 8_192 } fn default_max_headers() -> usize { 100 }
999fn default_max_params() -> usize { 100 }
1000fn default_max_cookies() -> usize { 50 }
1001fn default_max_json_depth() -> usize { 20 }
1002
1003impl Default for LimitsConfig {
1004 fn default() -> Self {
1005 Self {
1006 max_body_size: default_max_body_size(),
1007 max_header_size: default_max_header_size(),
1008 max_headers: default_max_headers(),
1009 max_params: default_max_params(),
1010 max_cookies: default_max_cookies(),
1011 max_json_depth: default_max_json_depth(),
1012 }
1013 }
1014}
1015
1016#[derive(Debug, Clone, Default)]
1020pub enum ParsedBody {
1021 #[default]
1022 None,
1023 FormUrlEncoded(Vec<(String, String)>),
1024 Multipart(Vec<MultipartField>),
1025 JsonFlattened(Vec<(String, String)>),
1027 Raw(Bytes),
1028}
1029
1030#[derive(Debug, Clone)]
1031pub struct MultipartField {
1032 pub name: String,
1033 pub filename: Option<String>,
1034 pub content_type: Option<String>,
1035 pub data: Bytes,
1036}
1037
1038#[derive(Debug, Clone, Default)]
1042pub struct Normalized {
1043 pub path: String,
1045 pub query: Option<String>,
1047 pub query_params: Vec<(String, String)>,
1049 pub cookies: Vec<(String, String)>,
1051 pub headers: Vec<(String, String)>,
1053 pub body: ParsedBody,
1054 pub double_encoding_detected: bool,
1056 pub derived_decoded: Vec<String>,
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Eq)]
1070pub struct ScoreContribution {
1071 pub module: String,
1073 pub rule_id: String,
1075 pub severity: Option<Severity>,
1078 pub points: u32,
1080}
1081
1082#[derive(Debug)]
1085pub struct RequestContext {
1086 pub client_ip: IpAddr,
1088 pub request_id: String,
1089 pub timestamp: SystemTime,
1090 pub method: String,
1092 pub path: String,
1093 pub raw_path: String,
1094 pub query: Option<String>,
1095 pub http_version: String,
1096 pub headers: Vec<(String, String)>,
1098 pub cookies: Vec<(String, String)>,
1099 pub body: Bytes,
1101 pub normalized: Normalized,
1103 pub score: u32,
1105 pub score_contributions: Vec<ScoreContribution>,
1107}
1108
1109#[cfg(test)]
1112mod config_validation_tests {
1113 use super::*;
1114
1115 fn valid() -> Config {
1116 Config {
1117 proxy: ProxyConfig {
1118 listen: "127.0.0.1:8080".parse().unwrap(),
1119 backend: "http://localhost:3000".to_string(),
1120 },
1121 waf: WafConfig {
1122 mode: WafMode::Blocking,
1123 block_threshold: 5,
1124 paranoia_level: 2,
1125 severity_scores: SeverityScores::default(),
1126 },
1127 limits: LimitsConfig::default(),
1128 modules: ModulesConfig::default(),
1129 rate_limit: RateLimitConfig::default(),
1130 network: NetworkConfig::default(),
1131 resilience: ResilienceConfig::default(),
1132 tls: TlsConfig::default(),
1133 metrics: MetricsConfig::default(),
1134 }
1135 }
1136
1137 #[test]
1138 fn valid_config_passes() {
1139 assert!(valid().validate().is_ok());
1140 }
1141
1142 #[test]
1143 fn paranoia_4_is_legal_forward_compatible() {
1144 let mut c = valid();
1145 c.waf.paranoia_level = MAX_PARANOIA_LEVEL; assert!(c.validate().is_ok());
1147 }
1148
1149 #[test]
1150 fn paranoia_out_of_range_rejected() {
1151 let mut c = valid();
1152 c.waf.paranoia_level = 0;
1153 assert_eq!(c.validate(), Err(ConfigError::ParanoiaOutOfRange(0)));
1154 c.waf.paranoia_level = 5;
1155 assert_eq!(c.validate(), Err(ConfigError::ParanoiaOutOfRange(5)));
1156 }
1157
1158 #[test]
1159 fn block_threshold_zero_rejected() {
1160 let mut c = valid();
1161 c.waf.block_threshold = 0;
1162 assert_eq!(c.validate(), Err(ConfigError::BlockThresholdZero));
1163 }
1164
1165 #[test]
1166 fn grpc_disabled_ignores_caps() {
1167 assert!(valid().validate().is_ok());
1169 }
1170
1171 #[test]
1172 fn grpc_enabled_rejects_zero_caps() {
1173 let mut c = valid();
1174 c.modules.grpc.enabled = true;
1175 assert!(c.validate().is_ok()); c.modules.grpc.max_depth = 0;
1177 assert_eq!(c.validate(), Err(ConfigError::GrpcCapZero("max_depth")));
1178 c.modules.grpc.max_depth = 16;
1179 c.modules.grpc.max_message_bytes = 0;
1180 assert_eq!(c.validate(), Err(ConfigError::GrpcCapZero("max_message_bytes")));
1181 }
1182
1183 #[test]
1184 fn tls_disabled_ignores_empty_paths() {
1185 assert!(valid().validate().is_ok());
1187 }
1188
1189 #[test]
1190 fn tls_enabled_requires_cert_and_key_paths() {
1191 let mut c = valid();
1192 c.tls.enabled = true;
1193 assert_eq!(c.validate(), Err(ConfigError::TlsPathEmpty("cert_path")));
1194 c.tls.cert_path = "cert.pem".to_string();
1195 assert_eq!(c.validate(), Err(ConfigError::TlsPathEmpty("key_path")));
1196 c.tls.key_path = "key.pem".to_string();
1197 assert!(c.validate().is_ok());
1198 }
1199
1200 #[test]
1201 fn tls_enabled_rejects_empty_alpn() {
1202 let mut c = valid();
1203 c.tls.enabled = true;
1204 c.tls.cert_path = "cert.pem".to_string();
1205 c.tls.key_path = "key.pem".to_string();
1206 c.tls.alpn = vec![];
1207 assert_eq!(c.validate(), Err(ConfigError::TlsAlpnInvalid));
1208 c.tls.alpn = vec!["h2".to_string(), " ".to_string()];
1209 assert_eq!(c.validate(), Err(ConfigError::TlsAlpnInvalid));
1210 }
1211
1212 #[test]
1213 fn crs_enabled_requires_files() {
1214 let mut c = valid();
1215 c.modules.crs.enabled = true;
1216 assert_eq!(c.validate(), Err(ConfigError::CrsNoFiles));
1217 c.modules.crs.files = vec!["rules.conf".to_string()];
1218 assert!(c.validate().is_ok());
1219 }
1220
1221 #[test]
1222 fn crs_disabled_ignores_empty_files() {
1223 assert!(valid().validate().is_ok());
1225 }
1226
1227 #[test]
1228 fn wasm_enabled_requires_plugins_with_paths() {
1229 let mut c = valid();
1230 c.modules.wasm.enabled = true;
1231 assert_eq!(c.validate(), Err(ConfigError::WasmNoPlugins));
1232 c.modules.wasm.plugins = vec![WasmPluginConfig { path: " ".into(), config: None }];
1233 assert_eq!(c.validate(), Err(ConfigError::WasmPluginPathEmpty));
1234 c.modules.wasm.plugins = vec![WasmPluginConfig { path: "f.wasm".into(), config: None }];
1235 assert!(c.validate().is_ok());
1236 }
1237
1238 #[test]
1239 fn wasm_disabled_ignores_empty_plugins() {
1240 assert!(valid().validate().is_ok());
1242 }
1243
1244 #[test]
1245 fn zero_severity_weight_rejected() {
1246 let mut c = valid();
1247 c.waf.severity_scores.warning = 0;
1248 assert_eq!(c.validate(), Err(ConfigError::SeverityWeightZero("warning")));
1249 }
1250
1251 #[test]
1252 fn zero_limit_rejected() {
1253 let mut c = valid();
1254 c.limits.max_json_depth = 0;
1255 assert_eq!(c.validate(), Err(ConfigError::LimitZero("max_json_depth")));
1256 }
1257
1258 #[test]
1259 fn invalid_backend_rejected() {
1260 let mut c = valid();
1261 c.proxy.backend = "localhost:3000".to_string(); assert!(matches!(c.validate(), Err(ConfigError::InvalidBackend(_))));
1263 c.proxy.backend = "http://".to_string(); assert!(matches!(c.validate(), Err(ConfigError::InvalidBackend(_))));
1265 }
1266
1267 #[test]
1268 fn rate_limit_values_checked_only_when_enabled() {
1269 let mut c = valid();
1270 c.rate_limit.enabled = false;
1272 c.rate_limit.requests = 0;
1273 assert!(c.validate().is_ok());
1274 c.rate_limit.enabled = true;
1276 assert_eq!(c.validate(), Err(ConfigError::RateLimitValueZero("requests")));
1277 }
1278
1279 #[test]
1280 fn rate_limit_score_action_requires_positive_score() {
1281 let mut c = valid();
1282 c.rate_limit.enabled = true;
1283 c.rate_limit.action = RateLimitAction::Score;
1284 c.rate_limit.score = 0;
1285 assert_eq!(c.validate(), Err(ConfigError::RateLimitValueZero("score")));
1286 }
1287
1288 #[test]
1289 fn trusted_hops_out_of_range_rejected() {
1290 let mut c = valid();
1291 c.network.trusted_hops = 0;
1292 assert_eq!(c.validate(), Err(ConfigError::TrustedHopsOutOfRange(0)));
1293 c.network.trusted_hops = MAX_TRUSTED_HOPS + 1;
1294 assert_eq!(
1295 c.validate(),
1296 Err(ConfigError::TrustedHopsOutOfRange(MAX_TRUSTED_HOPS + 1))
1297 );
1298 }
1299
1300 #[test]
1301 fn invalid_cidr_in_trusted_proxies_rejected() {
1302 let mut c = valid();
1303 c.network.trusted_proxies = vec!["10.0.0.0/8".to_string(), "999.0.0.0/8".to_string()];
1304 assert_eq!(
1305 c.validate(),
1306 Err(ConfigError::InvalidCidr("999.0.0.0/8".to_string()))
1307 );
1308 }
1309
1310 #[test]
1311 fn valid_cidrs_pass() {
1312 let mut c = valid();
1313 c.network.trusted_proxies = vec!["10.0.0.0/8".to_string(), "::1".to_string()];
1314 assert!(c.validate().is_ok());
1315 }
1316
1317 #[test]
1318 fn empty_client_ip_header_rejected() {
1319 let mut c = valid();
1320 c.network.client_ip_header = " ".to_string();
1321 assert_eq!(c.validate(), Err(ConfigError::EmptyClientIpHeader));
1322 }
1323
1324 #[test]
1325 fn zero_upstream_timeout_rejected() {
1326 let mut c = valid();
1327 c.resilience.upstream_timeout_ms = 0;
1328 assert_eq!(c.validate(), Err(ConfigError::ResilienceTimeoutZero));
1329 }
1330
1331 #[test]
1332 fn resilience_defaults_match_documented_posture() {
1333 let r = ResilienceConfig::default();
1334 assert_eq!(r.on_internal_error, FailMode::FailOpen);
1335 assert_eq!(r.on_upstream_error, FailMode::FailClosed);
1336 assert_eq!(r.on_config_error, FailMode::FailOpen);
1337 assert_eq!(r.on_parser_limit, FailMode::FailClosed);
1338 }
1339}