1pub mod config;
5pub mod metrics;
6pub mod tls;
7
8use std::convert::Infallible;
9use std::future::Future;
10use std::net::SocketAddr;
11use std::path::Path;
12use std::pin::Pin;
13use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
14use std::sync::{Arc, RwLock};
15use std::task::{Context, Poll};
16use std::time::{Instant, SystemTime};
17
18use tokio::sync::{watch, Notify};
19
20use http_body_util::combinators::BoxBody;
21use http_body_util::{BodyExt, Full};
22use hyper::body::{Body, Bytes, Frame, Incoming};
23use hyper::service::service_fn;
24use hyper::{HeaderMap, Request, Response, Uri};
25use hyper_util::client::legacy::connect::HttpConnector;
26use hyper_util::client::legacy::Client;
27use hyper_util::rt::{TokioExecutor, TokioIo};
28use hyper_util::server::conn::auto;
29use tokio::net::TcpListener;
30use tokio_rustls::TlsAcceptor;
31
32use crate::metrics::{Metrics, Outcome};
33use tls::TlsCertSource;
34use tracing::{debug, error, info, warn};
35
36use waf_core::{
37 ClientIpResolver, Config, FailMode, IpSource, Normalized, RateLimitState, RequestContext,
38 ResilienceConfig, StateStore, WafModule,
39};
40use waf_detection::{
41 crs::CrsModule,
42 evasion::EvasionModule,
43 graphql::GraphqlModule, grpc::GrpcModule, header_injection::HeaderInjectionModule, ldap::LdapModule,
44 lfi_rfi::LfiRfiModule,
45 mail::MailModule, nosql::NosqlModule, path_traversal::PathTraversalModule,
46 rate_limit::RateLimitModule,
47 rce::RceModule, request_smuggling::RequestSmugglingModule, scanner::ScannerModule,
48 sqli::SqliModule, ssi::SsiModule, ssrf::SsrfModule, ssti::SstiModule, xss::XssModule,
49 xxe::XxeModule, ContentPrefilter,
50};
51use waf_normalizer::Normalizer;
52use waf_pipeline::{NoopLogger, Pipeline, PipelineVerdict};
53use waf_wasm::{WasmModule, WasmOptions};
54
55pub type HyperBoxBody = BoxBody<Bytes, hyper::Error>;
56
57pub type ModuleFactory =
64 dyn Fn() -> Result<Vec<Box<dyn WafModule>>, Box<dyn std::error::Error + Send + Sync>>
65 + Send
66 + Sync;
67
68const HOP_BY_HOP: &[&str] = &[
70 "connection",
71 "host", "keep-alive",
73 "proxy-authenticate",
74 "proxy-authorization",
75 "te",
76 "trailers",
77 "transfer-encoding",
78 "upgrade",
79];
80
81static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
82
83fn next_request_id() -> String {
84 let n = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
85 format!("req-{n:016x}")
86}
87
88pub fn full_body(data: impl Into<Bytes>) -> HyperBoxBody {
89 Full::new(data.into())
90 .map_err(|never| match never {})
91 .boxed()
92}
93
94struct FramedBody {
99 data: Option<Bytes>,
100 trailers: Option<HeaderMap>,
101}
102
103impl Body for FramedBody {
104 type Data = Bytes;
105 type Error = Infallible;
106
107 fn poll_frame(
108 mut self: Pin<&mut Self>,
109 _cx: &mut Context<'_>,
110 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
111 if let Some(d) = self.data.take() {
112 return Poll::Ready(Some(Ok(Frame::data(d))));
113 }
114 if let Some(t) = self.trailers.take() {
115 return Poll::Ready(Some(Ok(Frame::trailers(t))));
116 }
117 Poll::Ready(None)
118 }
119}
120
121fn body_with_trailers(data: Bytes, trailers: Option<HeaderMap>) -> HyperBoxBody {
124 match trailers {
125 None => full_body(data),
126 Some(t) => FramedBody { data: Some(data), trailers: Some(t) }
127 .map_err(|never| match never {})
128 .boxed(),
129 }
130}
131
132async fn collect_with_trailers<B>(body: B) -> Result<(Bytes, Option<HeaderMap>), B::Error>
136where
137 B: Body<Data = Bytes>,
138{
139 let collected = body.collect().await?;
140 let trailers = collected.trailers().cloned();
141 Ok((collected.to_bytes(), trailers))
142}
143
144fn is_grpc_request(parts: &hyper::http::request::Parts) -> bool {
148 parts
149 .headers
150 .get(hyper::header::CONTENT_TYPE)
151 .and_then(|v| v.to_str().ok())
152 .map(|ct| ct.trim_start().starts_with("application/grpc"))
153 .unwrap_or(false)
154}
155
156fn parse_cookies(headers: &[(String, String)]) -> Vec<(String, String)> {
157 headers
158 .iter()
159 .filter(|(name, _)| name.eq_ignore_ascii_case("cookie"))
160 .flat_map(|(_, value)| {
161 value.split(';').filter_map(|pair| {
162 let mut parts = pair.splitn(2, '=');
163 let key = parts.next()?.trim().to_string();
164 let val = parts.next().unwrap_or("").trim().to_string();
165 Some((key, val))
166 })
167 })
168 .collect()
169}
170
171fn build_context(
172 parts: &hyper::http::request::Parts,
173 body: &Bytes,
174 client_addr: SocketAddr,
175 ip_resolver: &ClientIpResolver,
176) -> RequestContext {
177 let path = parts.uri.path().to_string();
178 let query = parts.uri.query().map(str::to_string);
179 let method = parts.method.to_string();
180 let http_version = format!("{:?}", parts.version);
181
182 let headers: Vec<(String, String)> = parts
183 .headers
184 .iter()
185 .filter_map(|(name, value)| {
186 value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
187 })
188 .collect();
189
190 let cookies = parse_cookies(&headers);
191
192 let normalized = Normalized::default();
193
194 let request_id = next_request_id();
199 let resolved = ip_resolver.resolve(client_addr.ip(), &headers);
200 match resolved.source {
201 IpSource::FallbackMissingHeader | IpSource::FallbackMalformed => warn!(
202 request_id = %request_id,
203 peer = %client_addr.ip(),
204 source = ?resolved.source,
205 "client-IP resolution fell back to peer address"
206 ),
207 IpSource::DirectPeer | IpSource::TrustedHeader => {}
208 }
209
210 RequestContext {
211 client_ip: resolved.ip,
212 request_id,
213 timestamp: SystemTime::now(),
214 method,
215 path: path.clone(),
216 raw_path: path,
217 query,
218 http_version,
219 headers,
220 cookies,
221 body: body.clone(),
222 normalized,
223 score: 0,
224 score_contributions: vec![],
225 }
226}
227
228struct Reloadable {
232 backend: String,
233 normalizer: Normalizer,
234 pipeline: Pipeline,
235 prefilter: ContentPrefilter,
239 ip_resolver: ClientIpResolver,
240 resilience: ResilienceConfig,
241}
242
243struct StaticState {
250 client: Client<HttpConnector, HyperBoxBody>,
251 grpc_client: Client<HttpConnector, HyperBoxBody>,
255 listen_addr: SocketAddr,
256 rl_state: RateLimitState,
257 current: RwLock<Arc<Reloadable>>,
258 mode: HandlerMode,
259 tls_acceptor: Option<TlsAcceptor>,
263 metrics: Arc<Metrics>,
266 module_factory: Option<Arc<ModuleFactory>>,
271}
272
273#[derive(Clone, Copy)]
280enum HandlerMode {
281 Inspect,
282 Passthrough,
283}
284
285impl StaticState {
286 fn current(&self) -> Arc<Reloadable> {
291 self.current
292 .read()
293 .unwrap_or_else(|poisoned| poisoned.into_inner())
294 .clone()
295 }
296}
297
298#[derive(Clone)]
302pub struct Reloader(Arc<StaticState>);
303
304impl Reloader {
305 pub fn reload_from(&self, path: &Path) -> Result<(), config::LoadError> {
309 let new_cfg = match config::load(path) {
310 Ok(c) => c,
311 Err(e) => {
312 error!(error = %e, "config reload failed; keeping current configuration");
313 return Err(e);
314 }
315 };
316
317 if new_cfg.proxy.listen != self.0.listen_addr {
319 warn!(
320 current = %self.0.listen_addr,
321 requested = %new_cfg.proxy.listen,
322 "proxy.listen change requires a restart; keeping the current bind address"
323 );
324 }
325
326 let extra = match &self.0.module_factory {
333 Some(factory) => match factory() {
334 Ok(modules) => modules,
335 Err(e) => {
336 error!(error = %e, "module factory failed on reload; keeping current configuration");
337 return Err(config::LoadError::ModuleFactory(e.to_string()));
338 }
339 },
340 None => Vec::new(),
341 };
342
343 let new_reloadable = build_reloadable(&new_cfg, self.0.rl_state.clone(), extra);
346
347 *self
351 .0
352 .current
353 .write()
354 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(new_reloadable);
355 info!("configuration reloaded");
356 Ok(())
357 }
358}
359
360fn upstream_error_response(
365 ctx: &RequestContext,
366 resilience: &ResilienceConfig,
367 detail: &str,
368) -> Response<HyperBoxBody> {
369 let (status, body) = match resilience.on_upstream_error {
370 FailMode::FailClosed => (502, "Bad Gateway"),
371 FailMode::FailOpen => (503, "Service Unavailable"),
372 };
373 warn!(
374 request_id = %ctx.request_id,
375 client_ip = %ctx.client_ip,
376 status = status,
377 policy = ?resilience.on_upstream_error,
378 detail = detail,
379 "upstream error: applying on_upstream_error policy"
380 );
381 Response::builder().status(status).body(full_body(body)).unwrap()
382}
383
384fn contributions_json(ctx: &RequestContext) -> String {
390 serde_json::to_string(&ctx.score_contributions).unwrap_or_else(|_| "[]".to_string())
391}
392
393fn deny_response(
396 ctx: &RequestContext,
397 verdict: PipelineVerdict,
398) -> Option<(Response<HyperBoxBody>, Outcome)> {
399 match verdict {
400 PipelineVerdict::Allow => None,
401 PipelineVerdict::Block { rule_id, reason } => {
402 warn!(
403 request_id = %ctx.request_id,
404 rule_id = %rule_id,
405 reason = %reason,
406 score = ctx.score,
407 score_contributions = %contributions_json(ctx),
408 "request blocked"
409 );
410 Some((
411 Response::builder()
412 .status(403)
413 .body(full_body("Forbidden"))
414 .unwrap(),
415 Outcome::Blocked,
416 ))
417 }
418 PipelineVerdict::Reject { rule_id, reason, status, retry_after } => {
419 warn!(
420 request_id = %ctx.request_id,
421 rule_id = %rule_id,
422 reason = %reason,
423 status = status,
424 score_contributions = %contributions_json(ctx),
425 "request rejected"
426 );
427 let (body, outcome) = match status {
430 429 => ("Too Many Requests", Outcome::RateLimited),
431 400 => ("Bad Request", Outcome::BadRequest),
432 _ => ("Rejected", Outcome::BadRequest),
433 };
434 let mut builder = Response::builder().status(status);
435 if let Some(secs) = retry_after {
436 builder = builder.header("retry-after", secs.to_string());
437 }
438 Some((builder.body(full_body(body)).unwrap(), outcome))
439 }
440 }
441}
442
443async fn try_forward(
444 req: Request<Incoming>,
445 state: &StaticState,
446 client_addr: SocketAddr,
447) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
448 let rel = state.current();
451
452 let (parts, body) = req.into_parts();
453 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
456
457 let mut ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
458
459 let connection_verdict = rel.pipeline.run_connection(&mut ctx);
462 if let Some(denied) = deny_response(&ctx, connection_verdict) {
463 return Ok(denied);
464 }
465
466 let normalized_ok = match rel.normalizer.normalize(&mut ctx) {
470 Ok(()) => true,
471 Err(e) => match rel.resilience.on_parser_limit {
472 FailMode::FailClosed => {
473 warn!(
474 request_id = %ctx.request_id,
475 error = %e,
476 policy = ?FailMode::FailClosed,
477 "normalization failed: rejecting (on_parser_limit)"
478 );
479 return Ok((
480 Response::builder()
481 .status(400)
482 .body(full_body("Bad Request"))
483 .unwrap(),
484 Outcome::BadRequest,
485 ));
486 }
487 FailMode::FailOpen => {
488 warn!(
489 request_id = %ctx.request_id,
490 error = %e,
491 policy = ?FailMode::FailOpen,
492 "normalization failed: forwarding UNINSPECTED (on_parser_limit)"
493 );
494 false
495 }
496 },
497 };
498
499 let path_and_query = parts
500 .uri
501 .path_and_query()
502 .map(|pq| pq.as_str())
503 .unwrap_or("/")
504 .to_string();
505
506 info!(
507 request_id = %ctx.request_id,
508 method = %ctx.method,
509 path = %path_and_query,
510 client_ip = %ctx.client_ip,
511 "→ request"
512 );
513
514 if normalized_ok {
517 let inspect = rel.prefilter.is_candidate(&ctx);
523 let inspection_verdict = rel.pipeline.run_inspection_gated(&mut ctx, inspect);
524 if let Some(denied) = deny_response(&ctx, inspection_verdict) {
525 return Ok(denied);
526 }
527 }
528
529 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
530}
531
532#[allow(clippy::too_many_arguments)]
541async fn forward_to_backend(
542 state: &StaticState,
543 rel: &Reloadable,
544 parts: &hyper::http::request::Parts,
545 path_and_query: &str,
546 body_bytes: Bytes,
547 req_trailers: Option<HeaderMap>,
548 client_addr: SocketAddr,
549 ctx: &RequestContext,
550) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
551 let backend_uri: Uri = format!("{}{}", rel.backend, path_and_query).parse()?;
552 let is_grpc = is_grpc_request(parts);
553
554 let mut builder = Request::builder()
555 .method(parts.method.clone())
556 .uri(backend_uri);
557
558 for (name, value) in &parts.headers {
559 if !HOP_BY_HOP.contains(&name.as_str()) {
560 builder = builder.header(name, value);
561 }
562 }
563 builder = builder.header("x-forwarded-for", client_addr.ip().to_string());
566 builder = builder.header("x-request-id", ctx.request_id.as_str());
567 if is_grpc {
570 builder = builder.header("te", "trailers");
571 }
572
573 let (client, fwd_body) = if is_grpc {
576 (&state.grpc_client, body_with_trailers(body_bytes, req_trailers))
577 } else {
578 (&state.client, full_body(body_bytes))
579 };
580 let fwd_req = builder.body(fwd_body)?;
581
582 let upstream = tokio::time::timeout(rel.resilience.upstream_timeout(), async {
586 let resp = client.request(fwd_req).await?;
587 let (resp_parts, resp_body) = resp.into_parts();
588 let (resp_bytes, resp_trailers) = collect_with_trailers(resp_body).await?;
591 Ok::<_, Box<dyn std::error::Error + Send + Sync>>((resp_parts, resp_bytes, resp_trailers))
592 })
593 .await;
594
595 let (resp_parts, resp_bytes, resp_trailers) = match upstream {
596 Ok(Ok(triple)) => triple,
597 Ok(Err(e)) => {
598 return Ok((
599 upstream_error_response(ctx, &rel.resilience, &e.to_string()),
600 Outcome::UpstreamError,
601 ))
602 }
603 Err(_elapsed) => {
604 return Ok((
605 upstream_error_response(ctx, &rel.resilience, "upstream timeout"),
606 Outcome::UpstreamError,
607 ))
608 }
609 };
610
611 info!(
612 request_id = %ctx.request_id,
613 status = %resp_parts.status,
614 score = ctx.score,
615 "← response"
616 );
617
618 Ok((
619 Response::from_parts(resp_parts, body_with_trailers(resp_bytes, resp_trailers)),
620 Outcome::Allowed,
621 ))
622}
623
624async fn try_passthrough(
631 req: Request<Incoming>,
632 state: &StaticState,
633 client_addr: SocketAddr,
634) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
635 let rel = state.current();
636 let (parts, body) = req.into_parts();
637 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
638 let ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
639 let path_and_query = parts
640 .uri
641 .path_and_query()
642 .map(|pq| pq.as_str())
643 .unwrap_or("/")
644 .to_string();
645 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
646}
647
648async fn handle(
649 req: Request<Incoming>,
650 state: Arc<StaticState>,
651 client_addr: SocketAddr,
652) -> Result<Response<HyperBoxBody>, Infallible> {
653 let start = Instant::now();
656 let result = match state.mode {
657 HandlerMode::Inspect => try_forward(req, &state, client_addr).await,
658 HandlerMode::Passthrough => try_passthrough(req, &state, client_addr).await,
659 };
660 let (resp, outcome) = match result {
664 Ok((resp, outcome)) => (resp, outcome),
665 Err(e) => {
666 error!(error = %e, client_ip = %client_addr.ip(), "forwarding error");
667 let resp = Response::builder()
668 .status(502)
669 .body(full_body("Bad Gateway"))
670 .unwrap();
671 (resp, Outcome::InternalError)
672 }
673 };
674 state.metrics.record(outcome, start.elapsed());
675 Ok(resp)
676}
677
678pub struct Proxy {
679 listener: TcpListener,
680 state: Arc<StaticState>,
681 metrics_listener: Option<TcpListener>,
685}
686
687fn build_modules(config: &Config, rl_state: &RateLimitState) -> Vec<Box<dyn WafModule>> {
690 let mut modules: Vec<Box<dyn WafModule>> = vec![Box::new(NoopLogger)];
691 if config.modules.request_smuggling.enabled {
694 modules.push(Box::new(RequestSmugglingModule::new()));
695 }
696 if config.rate_limit.enabled {
697 modules.push(Box::new(RateLimitModule::with_state(rl_state.clone())));
698 }
699 if config.modules.sqli.enabled {
700 modules.push(Box::new(SqliModule::new()));
701 }
702 if config.modules.xss.enabled {
703 modules.push(Box::new(XssModule::new()));
704 }
705 if config.modules.path_traversal.enabled {
706 modules.push(Box::new(PathTraversalModule::new()));
707 }
708 if config.modules.rce.enabled {
709 modules.push(Box::new(RceModule::new()));
710 }
711 if config.modules.lfi_rfi.enabled {
712 modules.push(Box::new(LfiRfiModule::new()));
713 }
714 if config.modules.ssrf.enabled {
715 modules.push(Box::new(SsrfModule::new()));
716 }
717 if config.modules.ldap.enabled {
718 modules.push(Box::new(LdapModule::new()));
719 }
720 if config.modules.nosql.enabled {
721 modules.push(Box::new(NosqlModule::new()));
722 }
723 if config.modules.mail.enabled {
724 modules.push(Box::new(MailModule::new()));
725 }
726 if config.modules.ssti.enabled {
727 modules.push(Box::new(SstiModule::new()));
728 }
729 if config.modules.scanner.enabled {
730 modules.push(Box::new(ScannerModule::new()));
731 }
732 if config.modules.ssi.enabled {
733 modules.push(Box::new(SsiModule::new()));
734 }
735 if config.modules.xxe.enabled {
736 modules.push(Box::new(XxeModule::new()));
737 }
738 if config.modules.header_injection.enabled {
739 modules.push(Box::new(HeaderInjectionModule::new()));
740 }
741 if config.modules.evasion.enabled {
742 modules.push(Box::new(EvasionModule::new()));
743 }
744 if config.modules.graphql.enabled {
745 modules.push(Box::new(GraphqlModule::new()));
746 }
747 if config.modules.grpc.enabled {
748 modules.push(Box::new(GrpcModule::new()));
749 }
750 if config.modules.crs.enabled {
751 modules.push(Box::new(load_crs_module(&config.modules.crs.files)));
752 }
753 if config.modules.wasm.enabled {
754 for plugin in &config.modules.wasm.plugins {
755 if let Some(m) = load_wasm_plugin(plugin, &config.modules.wasm) {
756 modules.push(Box::new(m));
757 }
758 }
759 }
760 modules
761}
762
763fn load_wasm_plugin(
769 plugin: &waf_core::WasmPluginConfig,
770 cfg: &waf_core::WasmConfig,
771) -> Option<WasmModule> {
772 let bytes = match std::fs::read(&plugin.path) {
773 Ok(b) => b,
774 Err(e) => {
775 error!(file = %plugin.path, error = %e, "WASM: cannot read plugin (skipped)");
776 return None;
777 }
778 };
779 let name = plugin_name(&plugin.path);
780 let opts = WasmOptions {
781 pool_size: cfg.pool_size,
782 fuel_per_request: cfg.fuel_per_request,
783 max_memory_bytes: cfg.max_memory_bytes,
784 checkout_timeout: std::time::Duration::from_millis(cfg.checkout_timeout_ms),
785 };
786 let config_bytes = plugin.config.as_deref().unwrap_or("").as_bytes();
787 match WasmModule::from_bytes(&name, &bytes, config_bytes, &opts) {
788 Ok((module, report)) => {
789 info!(plugin = %name, "{}", report.summary());
792 Some(module)
793 }
794 Err(e) => {
795 error!(file = %plugin.path, error = %e, "WASM: plugin failed to load (skipped)");
796 None
797 }
798 }
799}
800
801fn plugin_name(path: &str) -> String {
803 std::path::Path::new(path)
804 .file_stem()
805 .and_then(|s| s.to_str())
806 .unwrap_or("plugin")
807 .to_string()
808}
809
810fn load_crs_module(files: &[String]) -> CrsModule {
817 let mut combined = String::new();
818 for path in files {
819 match std::fs::read_to_string(path) {
820 Ok(text) => {
821 combined.push_str(&text);
822 combined.push('\n');
823 }
824 Err(e) => error!(file = %path, error = %e, "CRS import: cannot read file (skipped)"),
825 }
826 }
827 let module = CrsModule::from_source(&combined);
828 info!(files = files.len(), "{}", module.report());
829 if !module.skipped().is_empty() {
830 warn!(
831 skipped = module.skipped().len(),
832 "CRS import: some rules fall outside the supported subset (see debug logs for reasons)"
833 );
834 for s in module.skipped() {
835 debug!(id = ?s.id, line = s.line_no, reason = %s.reason, "CRS import: rule skipped");
836 }
837 }
838 module
839}
840
841fn build_reloadable(
846 config: &Config,
847 rl_state: RateLimitState,
848 extra: Vec<Box<dyn WafModule>>,
849) -> Reloadable {
850 let mut modules = build_modules(config, &rl_state);
851 modules.extend(extra);
852 let pipeline = Pipeline::new(config, modules);
853
854 if config.waf.paranoia_level > waf_detection::HIGHEST_RULE_PARANOIA {
857 warn!(
858 paranoia_level = config.waf.paranoia_level,
859 highest_rule_paranoia = waf_detection::HIGHEST_RULE_PARANOIA,
860 "paranoia_level exceeds the highest existing rule paranoia: no additional rules are activated"
861 );
862 }
863 let ip_resolver = ClientIpResolver::from_config(&config.network);
864 if ip_resolver.trusted_count() < config.network.trusted_proxies.len() {
865 warn!(
866 configured = config.network.trusted_proxies.len(),
867 valid = ip_resolver.trusted_count(),
868 "some trusted_proxies CIDR entries were invalid and skipped"
869 );
870 }
871
872 Reloadable {
873 backend: config.proxy.backend.trim_end_matches('/').to_string(),
874 normalizer: Normalizer::new(&config.limits),
875 pipeline,
876 prefilter: ContentPrefilter::new(config.waf.paranoia_level),
878 ip_resolver,
879 resilience: config.resilience,
880 }
881}
882
883impl Proxy {
884 pub async fn bind(config: &Config) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
888 Self::builder(config).build().await
889 }
890
891 pub fn builder(config: &Config) -> ProxyBuilder<'_> {
896 ProxyBuilder {
897 config,
898 modules: Vec::new(),
899 state_store: None,
900 cert_source: None,
901 module_factory: None,
902 mode: HandlerMode::Inspect,
903 }
904 }
905
906 #[doc(hidden)]
912 pub async fn bind_with_modules(
913 config: &Config,
914 extra: Vec<Box<dyn WafModule>>,
915 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
916 Self::bind_inner(config, extra, HandlerMode::Inspect, None, None, None).await
917 }
918
919 #[doc(hidden)]
924 pub async fn bind_passthrough(
925 config: &Config,
926 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
927 Self::bind_inner(config, Vec::new(), HandlerMode::Passthrough, None, None, None).await
928 }
929
930 async fn bind_inner(
931 config: &Config,
932 extra: Vec<Box<dyn WafModule>>,
933 mode: HandlerMode,
934 state_store: Option<RateLimitState>,
935 cert_source: Option<Arc<dyn TlsCertSource>>,
936 module_factory: Option<Arc<ModuleFactory>>,
937 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
938 let listener = TcpListener::bind(config.proxy.listen).await?;
939 let listen_addr = listener.local_addr()?;
940 let client: Client<HttpConnector, HyperBoxBody> =
941 Client::builder(TokioExecutor::new()).build(HttpConnector::new());
942 let grpc_client: Client<HttpConnector, HyperBoxBody> =
944 Client::builder(TokioExecutor::new()).http2_only(true).build(HttpConnector::new());
945
946 let tls_acceptor = tls::acceptor_from_source(&config.tls, cert_source)
950 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
951 if tls_acceptor.is_some() {
952 info!(listen = %listen_addr, alpn = ?config.tls.alpn, "TLS termination enabled");
953 }
954
955 let rl_state = state_store
960 .unwrap_or_else(|| RateLimitState::in_memory(config.rate_limit.max_tracked_keys));
961
962 let mut extra_total = extra;
967 if let Some(factory) = &module_factory {
968 extra_total.extend(factory()?);
969 }
970 let reloadable = build_reloadable(config, rl_state.clone(), extra_total);
971
972 let metrics = Arc::new(Metrics::new());
976 let metrics_listener = if config.metrics.enabled {
977 let l = TcpListener::bind(config.metrics.listen).await?;
978 info!(listen = %l.local_addr()?, "metrics endpoint enabled (/metrics)");
979 Some(l)
980 } else {
981 None
982 };
983
984 Ok(Self {
985 listener,
986 state: Arc::new(StaticState {
987 client,
988 grpc_client,
989 listen_addr,
990 rl_state,
991 current: RwLock::new(Arc::new(reloadable)),
992 mode,
993 tls_acceptor,
994 metrics,
995 module_factory,
996 }),
997 metrics_listener,
998 })
999 }
1000
1001 pub fn reloader(&self) -> Reloader {
1005 Reloader(Arc::clone(&self.state))
1006 }
1007
1008 pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
1009 self.listener.local_addr()
1010 }
1011
1012 pub fn metrics_addr(&self) -> Option<SocketAddr> {
1014 self.metrics_listener.as_ref().and_then(|l| l.local_addr().ok())
1015 }
1016
1017 pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1020 self.run_with_shutdown(std::future::pending::<()>()).await
1021 }
1022
1023 pub async fn run_with_shutdown(
1032 self,
1033 shutdown: impl Future<Output = ()> + Send,
1034 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1035 if let Some(metrics_listener) = self.metrics_listener {
1038 let metrics = Arc::clone(&self.state.metrics);
1039 tokio::spawn(serve_metrics(metrics_listener, metrics));
1040 }
1041
1042 let (drain_tx, drain_rx) = watch::channel(false);
1045 let conns = Arc::new(ConnTracker::default());
1046 let mut shutdown = std::pin::pin!(shutdown);
1047
1048 loop {
1049 let (stream, client_addr) = tokio::select! {
1050 accepted = self.listener.accept() => accepted?,
1051 _ = &mut shutdown => break,
1052 };
1053 let state = Arc::clone(&self.state);
1054 let drain_rx = drain_rx.clone();
1055 let conns = Arc::clone(&conns);
1056 conns.enter();
1057
1058 tokio::spawn(async move {
1059 match state.tls_acceptor.clone() {
1064 Some(acceptor) => match acceptor.accept(stream).await {
1065 Ok(tls_stream) => {
1066 serve_connection(TokioIo::new(tls_stream), state, client_addr, drain_rx)
1067 .await;
1068 }
1069 Err(e) => {
1070 warn!(error = %e, client_ip = %client_addr.ip(), "TLS handshake error");
1071 }
1072 },
1073 None => {
1074 serve_connection(TokioIo::new(stream), state, client_addr, drain_rx).await;
1075 }
1076 }
1077 conns.leave();
1078 });
1079 }
1080
1081 info!("shutdown signal received; draining in-flight connections");
1084 drop(self.listener);
1085 let _ = drain_tx.send(true);
1086 conns.wait_idle().await;
1087 info!("drain complete; all connections closed");
1088 Ok(())
1089 }
1090}
1091
1092#[derive(Default)]
1094struct ConnTracker {
1095 count: AtomicUsize,
1096 idle: Notify,
1097}
1098
1099impl ConnTracker {
1100 fn enter(&self) {
1101 self.count.fetch_add(1, Ordering::AcqRel);
1102 }
1103 fn leave(&self) {
1104 if self.count.fetch_sub(1, Ordering::AcqRel) == 1 {
1105 self.idle.notify_waiters();
1106 }
1107 }
1108 async fn wait_idle(&self) {
1109 loop {
1110 let armed = self.idle.notified();
1112 if self.count.load(Ordering::Acquire) == 0 {
1113 return;
1114 }
1115 armed.await;
1116 }
1117 }
1118}
1119
1120pub struct ProxyBuilder<'a> {
1126 config: &'a Config,
1127 modules: Vec<Box<dyn WafModule>>,
1128 state_store: Option<RateLimitState>,
1129 cert_source: Option<Arc<dyn TlsCertSource>>,
1130 module_factory: Option<Arc<ModuleFactory>>,
1131 mode: HandlerMode,
1132}
1133
1134impl<'a> ProxyBuilder<'a> {
1135 pub fn modules(mut self, modules: Vec<Box<dyn WafModule>>) -> Self {
1138 self.modules = modules;
1139 self
1140 }
1141
1142 pub fn add_module(mut self, module: Box<dyn WafModule>) -> Self {
1144 self.modules.push(module);
1145 self
1146 }
1147
1148 pub fn state_store(mut self, store: Arc<dyn StateStore>) -> Self {
1152 self.state_store = Some(RateLimitState::with_store(store));
1153 self
1154 }
1155
1156 pub fn cert_source(mut self, source: Arc<dyn TlsCertSource>) -> Self {
1161 self.cert_source = Some(source);
1162 self
1163 }
1164
1165 pub fn module_factory<F>(mut self, factory: F) -> Self
1173 where
1174 F: Fn() -> Result<Vec<Box<dyn WafModule>>, Box<dyn std::error::Error + Send + Sync>>
1175 + Send
1176 + Sync
1177 + 'static,
1178 {
1179 self.module_factory = Some(Arc::new(factory));
1180 self
1181 }
1182
1183 pub async fn build(self) -> Result<Proxy, Box<dyn std::error::Error + Send + Sync>> {
1185 Proxy::bind_inner(
1186 self.config,
1187 self.modules,
1188 self.mode,
1189 self.state_store,
1190 self.cert_source,
1191 self.module_factory,
1192 )
1193 .await
1194 }
1195}
1196
1197async fn serve_connection<I>(
1201 io: I,
1202 state: Arc<StaticState>,
1203 client_addr: SocketAddr,
1204 mut drain_rx: watch::Receiver<bool>,
1205) where
1206 I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
1207{
1208 let svc = service_fn(move |req| {
1209 let state = Arc::clone(&state);
1210 handle(req, state, client_addr)
1211 });
1212 let builder = auto::Builder::new(TokioExecutor::new());
1213 let conn = builder.serve_connection(io, svc);
1214 let mut conn = std::pin::pin!(conn);
1215 loop {
1216 tokio::select! {
1217 res = conn.as_mut() => {
1218 if let Err(e) = res {
1219 warn!(error = %e, client_ip = %client_addr.ip(), "connection error");
1220 }
1221 break;
1222 }
1223 changed = drain_rx.changed() => {
1226 if changed.is_err() || *drain_rx.borrow() {
1227 conn.as_mut().graceful_shutdown();
1228 if let Err(e) = conn.as_mut().await {
1229 warn!(error = %e, client_ip = %client_addr.ip(), "connection error during drain");
1230 }
1231 break;
1232 }
1233 }
1234 }
1235 }
1236}
1237
1238async fn serve_metrics(listener: TcpListener, metrics: Arc<Metrics>) {
1242 loop {
1243 let Ok((stream, _)) = listener.accept().await else { continue };
1244 let metrics = Arc::clone(&metrics);
1245 tokio::spawn(async move {
1246 let svc = service_fn(move |req: Request<Incoming>| {
1247 let metrics = Arc::clone(&metrics);
1248 async move { Ok::<_, Infallible>(metrics_response(&req, &metrics)) }
1249 });
1250 let _ = hyper::server::conn::http1::Builder::new()
1251 .serve_connection(TokioIo::new(stream), svc)
1252 .await;
1253 });
1254 }
1255}
1256
1257fn metrics_response(req: &Request<Incoming>, metrics: &Metrics) -> Response<HyperBoxBody> {
1259 if req.method() == hyper::Method::GET && req.uri().path() == "/metrics" {
1260 Response::builder()
1261 .status(200)
1262 .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
1263 .body(full_body(metrics.render()))
1264 .unwrap()
1265 } else {
1266 Response::builder().status(404).body(full_body("Not Found")).unwrap()
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use waf_core::WafMode;
1274
1275 #[test]
1276 fn hop_by_hop_includes_connection_and_host() {
1277 assert!(HOP_BY_HOP.contains(&"connection"));
1278 assert!(HOP_BY_HOP.contains(&"host"));
1279 assert!(HOP_BY_HOP.contains(&"transfer-encoding"));
1280 }
1281
1282 #[test]
1283 fn hop_by_hop_excludes_regular_headers() {
1284 assert!(!HOP_BY_HOP.contains(&"content-type"));
1285 assert!(!HOP_BY_HOP.contains(&"authorization"));
1286 assert!(!HOP_BY_HOP.contains(&"x-custom-header"));
1287 }
1288
1289 #[test]
1290 fn config_parses_from_toml() {
1291 let raw = r#"
1292[proxy]
1293listen = "127.0.0.1:8080"
1294backend = "http://localhost:3000"
1295
1296[waf]
1297mode = "detection-only"
1298block_threshold = 10
1299"#;
1300 let config: Config = toml::from_str(raw).unwrap();
1301 assert_eq!(config.proxy.backend, "http://localhost:3000");
1302 assert_eq!(config.waf.mode, WafMode::DetectionOnly);
1303 assert_eq!(config.waf.block_threshold, 10);
1304 }
1305
1306 #[test]
1307 fn config_uses_default_block_threshold_when_omitted() {
1308 let raw = r#"
1309[proxy]
1310listen = "127.0.0.1:8080"
1311backend = "http://localhost:3000"
1312
1313[waf]
1314mode = "detection-only"
1315"#;
1316 let config: Config = toml::from_str(raw).unwrap();
1317 assert_eq!(config.waf.block_threshold, 5);
1318 }
1319
1320 #[test]
1321 fn config_parses_network_section() {
1322 let raw = r#"
1323[proxy]
1324listen = "127.0.0.1:8080"
1325backend = "http://localhost:3000"
1326
1327[waf]
1328mode = "blocking"
1329
1330[network]
1331trusted_proxies = ["10.0.0.0/8", "::1"]
1332client_ip_header = "X-Forwarded-For"
1333trusted_hops = 2
1334"#;
1335 let config: Config = toml::from_str(raw).unwrap();
1336 assert_eq!(config.network.trusted_proxies, vec!["10.0.0.0/8", "::1"]);
1337 assert_eq!(config.network.client_ip_header, "X-Forwarded-For");
1338 assert_eq!(config.network.trusted_hops, 2);
1339 }
1340
1341 #[test]
1342 fn config_network_defaults_to_failsafe_when_absent() {
1343 let raw = r#"
1344[proxy]
1345listen = "127.0.0.1:8080"
1346backend = "http://localhost:3000"
1347
1348[waf]
1349mode = "detection-only"
1350"#;
1351 let config: Config = toml::from_str(raw).unwrap();
1352 assert!(config.network.trusted_proxies.is_empty());
1353 assert_eq!(config.network.trusted_hops, 1);
1354 assert_eq!(config.network.client_ip_header, "x-forwarded-for".to_string());
1355 }
1356
1357 #[test]
1358 fn config_rejects_unknown_mode() {
1359 let raw = r#"
1360[proxy]
1361listen = "127.0.0.1:8080"
1362backend = "http://localhost:3000"
1363
1364[waf]
1365mode = "unknown-mode"
1366"#;
1367 assert!(toml::from_str::<Config>(raw).is_err());
1368 }
1369
1370 #[test]
1371 fn parse_cookies_splits_on_semicolon() {
1372 let headers = vec![("cookie".to_string(), "session=abc; user=123".to_string())];
1373 let cookies = parse_cookies(&headers);
1374 assert_eq!(cookies.len(), 2);
1375 assert!(cookies.contains(&("session".to_string(), "abc".to_string())));
1376 assert!(cookies.contains(&("user".to_string(), "123".to_string())));
1377 }
1378
1379 #[test]
1380 fn parse_cookies_handles_missing_value() {
1381 let headers = vec![("cookie".to_string(), "flag=; token=xyz".to_string())];
1382 let cookies = parse_cookies(&headers);
1383 assert!(cookies.contains(&("flag".to_string(), "".to_string())));
1384 assert!(cookies.contains(&("token".to_string(), "xyz".to_string())));
1385 }
1386
1387 #[test]
1388 fn parse_cookies_handles_empty_header_list() {
1389 assert!(parse_cookies(&[]).is_empty());
1390 }
1391
1392 #[test]
1393 fn request_id_is_unique_per_call() {
1394 let id1 = next_request_id();
1395 let id2 = next_request_id();
1396 assert_ne!(id1, id2);
1397 assert!(id1.starts_with("req-"));
1398 }
1399}