1pub mod config;
5pub mod metrics;
6pub mod tls;
7
8use std::convert::Infallible;
9use std::net::SocketAddr;
10use std::path::Path;
11use std::pin::Pin;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, RwLock};
14use std::task::{Context, Poll};
15use std::time::{Instant, SystemTime};
16
17use http_body_util::combinators::BoxBody;
18use http_body_util::{BodyExt, Full};
19use hyper::body::{Body, Bytes, Frame, Incoming};
20use hyper::service::service_fn;
21use hyper::{HeaderMap, Request, Response, Uri};
22use hyper_util::client::legacy::connect::HttpConnector;
23use hyper_util::client::legacy::Client;
24use hyper_util::rt::{TokioExecutor, TokioIo};
25use hyper_util::server::conn::auto;
26use tokio::net::TcpListener;
27use tokio_rustls::TlsAcceptor;
28
29use crate::metrics::{Metrics, Outcome};
30use tls::TlsCertSource;
31use tracing::{debug, error, info, warn};
32
33use waf_core::{
34 ClientIpResolver, Config, FailMode, IpSource, Normalized, RateLimitState, RequestContext,
35 ResilienceConfig, StateStore, WafModule,
36};
37use waf_detection::{
38 crs::CrsModule,
39 graphql::GraphqlModule, grpc::GrpcModule, header_injection::HeaderInjectionModule, ldap::LdapModule,
40 lfi_rfi::LfiRfiModule,
41 mail::MailModule, nosql::NosqlModule, path_traversal::PathTraversalModule,
42 rate_limit::RateLimitModule,
43 rce::RceModule, request_smuggling::RequestSmugglingModule, scanner::ScannerModule,
44 sqli::SqliModule, ssi::SsiModule, ssrf::SsrfModule, ssti::SstiModule, xss::XssModule,
45 xxe::XxeModule, ContentPrefilter,
46};
47use waf_normalizer::Normalizer;
48use waf_pipeline::{NoopLogger, Pipeline, PipelineVerdict};
49use waf_wasm::{WasmModule, WasmOptions};
50
51pub type HyperBoxBody = BoxBody<Bytes, hyper::Error>;
52
53pub type ModuleFactory =
60 dyn Fn() -> Result<Vec<Box<dyn WafModule>>, Box<dyn std::error::Error + Send + Sync>>
61 + Send
62 + Sync;
63
64const HOP_BY_HOP: &[&str] = &[
66 "connection",
67 "host", "keep-alive",
69 "proxy-authenticate",
70 "proxy-authorization",
71 "te",
72 "trailers",
73 "transfer-encoding",
74 "upgrade",
75];
76
77static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
78
79fn next_request_id() -> String {
80 let n = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
81 format!("req-{n:016x}")
82}
83
84pub fn full_body(data: impl Into<Bytes>) -> HyperBoxBody {
85 Full::new(data.into())
86 .map_err(|never| match never {})
87 .boxed()
88}
89
90struct FramedBody {
95 data: Option<Bytes>,
96 trailers: Option<HeaderMap>,
97}
98
99impl Body for FramedBody {
100 type Data = Bytes;
101 type Error = Infallible;
102
103 fn poll_frame(
104 mut self: Pin<&mut Self>,
105 _cx: &mut Context<'_>,
106 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
107 if let Some(d) = self.data.take() {
108 return Poll::Ready(Some(Ok(Frame::data(d))));
109 }
110 if let Some(t) = self.trailers.take() {
111 return Poll::Ready(Some(Ok(Frame::trailers(t))));
112 }
113 Poll::Ready(None)
114 }
115}
116
117fn body_with_trailers(data: Bytes, trailers: Option<HeaderMap>) -> HyperBoxBody {
120 match trailers {
121 None => full_body(data),
122 Some(t) => FramedBody { data: Some(data), trailers: Some(t) }
123 .map_err(|never| match never {})
124 .boxed(),
125 }
126}
127
128async fn collect_with_trailers<B>(body: B) -> Result<(Bytes, Option<HeaderMap>), B::Error>
132where
133 B: Body<Data = Bytes>,
134{
135 let collected = body.collect().await?;
136 let trailers = collected.trailers().cloned();
137 Ok((collected.to_bytes(), trailers))
138}
139
140fn is_grpc_request(parts: &hyper::http::request::Parts) -> bool {
144 parts
145 .headers
146 .get(hyper::header::CONTENT_TYPE)
147 .and_then(|v| v.to_str().ok())
148 .map(|ct| ct.trim_start().starts_with("application/grpc"))
149 .unwrap_or(false)
150}
151
152fn parse_cookies(headers: &[(String, String)]) -> Vec<(String, String)> {
153 headers
154 .iter()
155 .filter(|(name, _)| name.eq_ignore_ascii_case("cookie"))
156 .flat_map(|(_, value)| {
157 value.split(';').filter_map(|pair| {
158 let mut parts = pair.splitn(2, '=');
159 let key = parts.next()?.trim().to_string();
160 let val = parts.next().unwrap_or("").trim().to_string();
161 Some((key, val))
162 })
163 })
164 .collect()
165}
166
167fn build_context(
168 parts: &hyper::http::request::Parts,
169 body: &Bytes,
170 client_addr: SocketAddr,
171 ip_resolver: &ClientIpResolver,
172) -> RequestContext {
173 let path = parts.uri.path().to_string();
174 let query = parts.uri.query().map(str::to_string);
175 let method = parts.method.to_string();
176 let http_version = format!("{:?}", parts.version);
177
178 let headers: Vec<(String, String)> = parts
179 .headers
180 .iter()
181 .filter_map(|(name, value)| {
182 value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
183 })
184 .collect();
185
186 let cookies = parse_cookies(&headers);
187
188 let normalized = Normalized::default();
189
190 let request_id = next_request_id();
195 let resolved = ip_resolver.resolve(client_addr.ip(), &headers);
196 match resolved.source {
197 IpSource::FallbackMissingHeader | IpSource::FallbackMalformed => warn!(
198 request_id = %request_id,
199 peer = %client_addr.ip(),
200 source = ?resolved.source,
201 "client-IP resolution fell back to peer address"
202 ),
203 IpSource::DirectPeer | IpSource::TrustedHeader => {}
204 }
205
206 RequestContext {
207 client_ip: resolved.ip,
208 request_id,
209 timestamp: SystemTime::now(),
210 method,
211 path: path.clone(),
212 raw_path: path,
213 query,
214 http_version,
215 headers,
216 cookies,
217 body: body.clone(),
218 normalized,
219 score: 0,
220 score_contributions: vec![],
221 }
222}
223
224struct Reloadable {
228 backend: String,
229 normalizer: Normalizer,
230 pipeline: Pipeline,
231 prefilter: ContentPrefilter,
235 ip_resolver: ClientIpResolver,
236 resilience: ResilienceConfig,
237}
238
239struct StaticState {
246 client: Client<HttpConnector, HyperBoxBody>,
247 grpc_client: Client<HttpConnector, HyperBoxBody>,
251 listen_addr: SocketAddr,
252 rl_state: RateLimitState,
253 current: RwLock<Arc<Reloadable>>,
254 mode: HandlerMode,
255 tls_acceptor: Option<TlsAcceptor>,
259 metrics: Arc<Metrics>,
262 module_factory: Option<Arc<ModuleFactory>>,
267}
268
269#[derive(Clone, Copy)]
276enum HandlerMode {
277 Inspect,
278 Passthrough,
279}
280
281impl StaticState {
282 fn current(&self) -> Arc<Reloadable> {
287 self.current
288 .read()
289 .unwrap_or_else(|poisoned| poisoned.into_inner())
290 .clone()
291 }
292}
293
294#[derive(Clone)]
298pub struct Reloader(Arc<StaticState>);
299
300impl Reloader {
301 pub fn reload_from(&self, path: &Path) -> Result<(), config::LoadError> {
305 let new_cfg = match config::load(path) {
306 Ok(c) => c,
307 Err(e) => {
308 error!(error = %e, "config reload failed; keeping current configuration");
309 return Err(e);
310 }
311 };
312
313 if new_cfg.proxy.listen != self.0.listen_addr {
315 warn!(
316 current = %self.0.listen_addr,
317 requested = %new_cfg.proxy.listen,
318 "proxy.listen change requires a restart; keeping the current bind address"
319 );
320 }
321
322 let extra = match &self.0.module_factory {
329 Some(factory) => match factory() {
330 Ok(modules) => modules,
331 Err(e) => {
332 error!(error = %e, "module factory failed on reload; keeping current configuration");
333 return Err(config::LoadError::ModuleFactory(e.to_string()));
334 }
335 },
336 None => Vec::new(),
337 };
338
339 let new_reloadable = build_reloadable(&new_cfg, self.0.rl_state.clone(), extra);
342
343 *self
347 .0
348 .current
349 .write()
350 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(new_reloadable);
351 info!("configuration reloaded");
352 Ok(())
353 }
354}
355
356fn upstream_error_response(
361 ctx: &RequestContext,
362 resilience: &ResilienceConfig,
363 detail: &str,
364) -> Response<HyperBoxBody> {
365 let (status, body) = match resilience.on_upstream_error {
366 FailMode::FailClosed => (502, "Bad Gateway"),
367 FailMode::FailOpen => (503, "Service Unavailable"),
368 };
369 warn!(
370 request_id = %ctx.request_id,
371 client_ip = %ctx.client_ip,
372 status = status,
373 policy = ?resilience.on_upstream_error,
374 detail = detail,
375 "upstream error: applying on_upstream_error policy"
376 );
377 Response::builder().status(status).body(full_body(body)).unwrap()
378}
379
380fn deny_response(
383 ctx: &RequestContext,
384 verdict: PipelineVerdict,
385) -> Option<(Response<HyperBoxBody>, Outcome)> {
386 match verdict {
387 PipelineVerdict::Allow => None,
388 PipelineVerdict::Block { rule_id, reason } => {
389 warn!(
390 request_id = %ctx.request_id,
391 rule_id = %rule_id,
392 reason = %reason,
393 score = ctx.score,
394 "request blocked"
395 );
396 Some((
397 Response::builder()
398 .status(403)
399 .body(full_body("Forbidden"))
400 .unwrap(),
401 Outcome::Blocked,
402 ))
403 }
404 PipelineVerdict::Reject { rule_id, reason, status, retry_after } => {
405 warn!(
406 request_id = %ctx.request_id,
407 rule_id = %rule_id,
408 reason = %reason,
409 status = status,
410 "request rejected"
411 );
412 let (body, outcome) = match status {
415 429 => ("Too Many Requests", Outcome::RateLimited),
416 400 => ("Bad Request", Outcome::BadRequest),
417 _ => ("Rejected", Outcome::BadRequest),
418 };
419 let mut builder = Response::builder().status(status);
420 if let Some(secs) = retry_after {
421 builder = builder.header("retry-after", secs.to_string());
422 }
423 Some((builder.body(full_body(body)).unwrap(), outcome))
424 }
425 }
426}
427
428async fn try_forward(
429 req: Request<Incoming>,
430 state: &StaticState,
431 client_addr: SocketAddr,
432) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
433 let rel = state.current();
436
437 let (parts, body) = req.into_parts();
438 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
441
442 let mut ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
443
444 let connection_verdict = rel.pipeline.run_connection(&mut ctx);
447 if let Some(denied) = deny_response(&ctx, connection_verdict) {
448 return Ok(denied);
449 }
450
451 let normalized_ok = match rel.normalizer.normalize(&mut ctx) {
455 Ok(()) => true,
456 Err(e) => match rel.resilience.on_parser_limit {
457 FailMode::FailClosed => {
458 warn!(
459 request_id = %ctx.request_id,
460 error = %e,
461 policy = ?FailMode::FailClosed,
462 "normalization failed: rejecting (on_parser_limit)"
463 );
464 return Ok((
465 Response::builder()
466 .status(400)
467 .body(full_body("Bad Request"))
468 .unwrap(),
469 Outcome::BadRequest,
470 ));
471 }
472 FailMode::FailOpen => {
473 warn!(
474 request_id = %ctx.request_id,
475 error = %e,
476 policy = ?FailMode::FailOpen,
477 "normalization failed: forwarding UNINSPECTED (on_parser_limit)"
478 );
479 false
480 }
481 },
482 };
483
484 let path_and_query = parts
485 .uri
486 .path_and_query()
487 .map(|pq| pq.as_str())
488 .unwrap_or("/")
489 .to_string();
490
491 info!(
492 request_id = %ctx.request_id,
493 method = %ctx.method,
494 path = %path_and_query,
495 client_ip = %ctx.client_ip,
496 "→ request"
497 );
498
499 if normalized_ok {
502 let inspect = rel.prefilter.is_candidate(&ctx);
508 let inspection_verdict = rel.pipeline.run_inspection_gated(&mut ctx, inspect);
509 if let Some(denied) = deny_response(&ctx, inspection_verdict) {
510 return Ok(denied);
511 }
512 }
513
514 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
515}
516
517#[allow(clippy::too_many_arguments)]
526async fn forward_to_backend(
527 state: &StaticState,
528 rel: &Reloadable,
529 parts: &hyper::http::request::Parts,
530 path_and_query: &str,
531 body_bytes: Bytes,
532 req_trailers: Option<HeaderMap>,
533 client_addr: SocketAddr,
534 ctx: &RequestContext,
535) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
536 let backend_uri: Uri = format!("{}{}", rel.backend, path_and_query).parse()?;
537 let is_grpc = is_grpc_request(parts);
538
539 let mut builder = Request::builder()
540 .method(parts.method.clone())
541 .uri(backend_uri);
542
543 for (name, value) in &parts.headers {
544 if !HOP_BY_HOP.contains(&name.as_str()) {
545 builder = builder.header(name, value);
546 }
547 }
548 builder = builder.header("x-forwarded-for", client_addr.ip().to_string());
551 builder = builder.header("x-request-id", ctx.request_id.as_str());
552 if is_grpc {
555 builder = builder.header("te", "trailers");
556 }
557
558 let (client, fwd_body) = if is_grpc {
561 (&state.grpc_client, body_with_trailers(body_bytes, req_trailers))
562 } else {
563 (&state.client, full_body(body_bytes))
564 };
565 let fwd_req = builder.body(fwd_body)?;
566
567 let upstream = tokio::time::timeout(rel.resilience.upstream_timeout(), async {
571 let resp = client.request(fwd_req).await?;
572 let (resp_parts, resp_body) = resp.into_parts();
573 let (resp_bytes, resp_trailers) = collect_with_trailers(resp_body).await?;
576 Ok::<_, Box<dyn std::error::Error + Send + Sync>>((resp_parts, resp_bytes, resp_trailers))
577 })
578 .await;
579
580 let (resp_parts, resp_bytes, resp_trailers) = match upstream {
581 Ok(Ok(triple)) => triple,
582 Ok(Err(e)) => {
583 return Ok((
584 upstream_error_response(ctx, &rel.resilience, &e.to_string()),
585 Outcome::UpstreamError,
586 ))
587 }
588 Err(_elapsed) => {
589 return Ok((
590 upstream_error_response(ctx, &rel.resilience, "upstream timeout"),
591 Outcome::UpstreamError,
592 ))
593 }
594 };
595
596 info!(
597 request_id = %ctx.request_id,
598 status = %resp_parts.status,
599 score = ctx.score,
600 "← response"
601 );
602
603 Ok((
604 Response::from_parts(resp_parts, body_with_trailers(resp_bytes, resp_trailers)),
605 Outcome::Allowed,
606 ))
607}
608
609async fn try_passthrough(
616 req: Request<Incoming>,
617 state: &StaticState,
618 client_addr: SocketAddr,
619) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
620 let rel = state.current();
621 let (parts, body) = req.into_parts();
622 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
623 let ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
624 let path_and_query = parts
625 .uri
626 .path_and_query()
627 .map(|pq| pq.as_str())
628 .unwrap_or("/")
629 .to_string();
630 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
631}
632
633async fn handle(
634 req: Request<Incoming>,
635 state: Arc<StaticState>,
636 client_addr: SocketAddr,
637) -> Result<Response<HyperBoxBody>, Infallible> {
638 let start = Instant::now();
641 let result = match state.mode {
642 HandlerMode::Inspect => try_forward(req, &state, client_addr).await,
643 HandlerMode::Passthrough => try_passthrough(req, &state, client_addr).await,
644 };
645 let (resp, outcome) = match result {
649 Ok((resp, outcome)) => (resp, outcome),
650 Err(e) => {
651 error!(error = %e, client_ip = %client_addr.ip(), "forwarding error");
652 let resp = Response::builder()
653 .status(502)
654 .body(full_body("Bad Gateway"))
655 .unwrap();
656 (resp, Outcome::InternalError)
657 }
658 };
659 state.metrics.record(outcome, start.elapsed());
660 Ok(resp)
661}
662
663pub struct Proxy {
664 listener: TcpListener,
665 state: Arc<StaticState>,
666 metrics_listener: Option<TcpListener>,
670}
671
672fn build_modules(config: &Config, rl_state: &RateLimitState) -> Vec<Box<dyn WafModule>> {
675 let mut modules: Vec<Box<dyn WafModule>> = vec![Box::new(NoopLogger)];
676 if config.modules.request_smuggling.enabled {
679 modules.push(Box::new(RequestSmugglingModule::new()));
680 }
681 if config.rate_limit.enabled {
682 modules.push(Box::new(RateLimitModule::with_state(rl_state.clone())));
683 }
684 if config.modules.sqli.enabled {
685 modules.push(Box::new(SqliModule::new()));
686 }
687 if config.modules.xss.enabled {
688 modules.push(Box::new(XssModule::new()));
689 }
690 if config.modules.path_traversal.enabled {
691 modules.push(Box::new(PathTraversalModule::new()));
692 }
693 if config.modules.rce.enabled {
694 modules.push(Box::new(RceModule::new()));
695 }
696 if config.modules.lfi_rfi.enabled {
697 modules.push(Box::new(LfiRfiModule::new()));
698 }
699 if config.modules.ssrf.enabled {
700 modules.push(Box::new(SsrfModule::new()));
701 }
702 if config.modules.ldap.enabled {
703 modules.push(Box::new(LdapModule::new()));
704 }
705 if config.modules.nosql.enabled {
706 modules.push(Box::new(NosqlModule::new()));
707 }
708 if config.modules.mail.enabled {
709 modules.push(Box::new(MailModule::new()));
710 }
711 if config.modules.ssti.enabled {
712 modules.push(Box::new(SstiModule::new()));
713 }
714 if config.modules.scanner.enabled {
715 modules.push(Box::new(ScannerModule::new()));
716 }
717 if config.modules.ssi.enabled {
718 modules.push(Box::new(SsiModule::new()));
719 }
720 if config.modules.xxe.enabled {
721 modules.push(Box::new(XxeModule::new()));
722 }
723 if config.modules.header_injection.enabled {
724 modules.push(Box::new(HeaderInjectionModule::new()));
725 }
726 if config.modules.graphql.enabled {
727 modules.push(Box::new(GraphqlModule::new()));
728 }
729 if config.modules.grpc.enabled {
730 modules.push(Box::new(GrpcModule::new()));
731 }
732 if config.modules.crs.enabled {
733 modules.push(Box::new(load_crs_module(&config.modules.crs.files)));
734 }
735 if config.modules.wasm.enabled {
736 for plugin in &config.modules.wasm.plugins {
737 if let Some(m) = load_wasm_plugin(plugin, &config.modules.wasm) {
738 modules.push(Box::new(m));
739 }
740 }
741 }
742 modules
743}
744
745fn load_wasm_plugin(
751 plugin: &waf_core::WasmPluginConfig,
752 cfg: &waf_core::WasmConfig,
753) -> Option<WasmModule> {
754 let bytes = match std::fs::read(&plugin.path) {
755 Ok(b) => b,
756 Err(e) => {
757 error!(file = %plugin.path, error = %e, "WASM: cannot read plugin (skipped)");
758 return None;
759 }
760 };
761 let name = plugin_name(&plugin.path);
762 let opts = WasmOptions {
763 pool_size: cfg.pool_size,
764 fuel_per_request: cfg.fuel_per_request,
765 max_memory_bytes: cfg.max_memory_bytes,
766 checkout_timeout: std::time::Duration::from_millis(cfg.checkout_timeout_ms),
767 };
768 let config_bytes = plugin.config.as_deref().unwrap_or("").as_bytes();
769 match WasmModule::from_bytes(&name, &bytes, config_bytes, &opts) {
770 Ok((module, report)) => {
771 info!(plugin = %name, "{}", report.summary());
774 Some(module)
775 }
776 Err(e) => {
777 error!(file = %plugin.path, error = %e, "WASM: plugin failed to load (skipped)");
778 None
779 }
780 }
781}
782
783fn plugin_name(path: &str) -> String {
785 std::path::Path::new(path)
786 .file_stem()
787 .and_then(|s| s.to_str())
788 .unwrap_or("plugin")
789 .to_string()
790}
791
792fn load_crs_module(files: &[String]) -> CrsModule {
799 let mut combined = String::new();
800 for path in files {
801 match std::fs::read_to_string(path) {
802 Ok(text) => {
803 combined.push_str(&text);
804 combined.push('\n');
805 }
806 Err(e) => error!(file = %path, error = %e, "CRS import: cannot read file (skipped)"),
807 }
808 }
809 let module = CrsModule::from_source(&combined);
810 info!(files = files.len(), "{}", module.report());
811 if !module.skipped().is_empty() {
812 warn!(
813 skipped = module.skipped().len(),
814 "CRS import: some rules fall outside the supported subset (see debug logs for reasons)"
815 );
816 for s in module.skipped() {
817 debug!(id = ?s.id, line = s.line_no, reason = %s.reason, "CRS import: rule skipped");
818 }
819 }
820 module
821}
822
823fn build_reloadable(
828 config: &Config,
829 rl_state: RateLimitState,
830 extra: Vec<Box<dyn WafModule>>,
831) -> Reloadable {
832 let mut modules = build_modules(config, &rl_state);
833 modules.extend(extra);
834 let pipeline = Pipeline::new(config, modules);
835
836 if config.waf.paranoia_level > waf_detection::HIGHEST_RULE_PARANOIA {
839 warn!(
840 paranoia_level = config.waf.paranoia_level,
841 highest_rule_paranoia = waf_detection::HIGHEST_RULE_PARANOIA,
842 "paranoia_level exceeds the highest existing rule paranoia: no additional rules are activated"
843 );
844 }
845 let ip_resolver = ClientIpResolver::from_config(&config.network);
846 if ip_resolver.trusted_count() < config.network.trusted_proxies.len() {
847 warn!(
848 configured = config.network.trusted_proxies.len(),
849 valid = ip_resolver.trusted_count(),
850 "some trusted_proxies CIDR entries were invalid and skipped"
851 );
852 }
853
854 Reloadable {
855 backend: config.proxy.backend.trim_end_matches('/').to_string(),
856 normalizer: Normalizer::new(&config.limits),
857 pipeline,
858 prefilter: ContentPrefilter::new(config.waf.paranoia_level),
860 ip_resolver,
861 resilience: config.resilience,
862 }
863}
864
865impl Proxy {
866 pub async fn bind(config: &Config) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
870 Self::builder(config).build().await
871 }
872
873 pub fn builder(config: &Config) -> ProxyBuilder<'_> {
878 ProxyBuilder {
879 config,
880 modules: Vec::new(),
881 state_store: None,
882 cert_source: None,
883 module_factory: None,
884 mode: HandlerMode::Inspect,
885 }
886 }
887
888 #[doc(hidden)]
894 pub async fn bind_with_modules(
895 config: &Config,
896 extra: Vec<Box<dyn WafModule>>,
897 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
898 Self::bind_inner(config, extra, HandlerMode::Inspect, None, None, None).await
899 }
900
901 #[doc(hidden)]
906 pub async fn bind_passthrough(
907 config: &Config,
908 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
909 Self::bind_inner(config, Vec::new(), HandlerMode::Passthrough, None, None, None).await
910 }
911
912 async fn bind_inner(
913 config: &Config,
914 extra: Vec<Box<dyn WafModule>>,
915 mode: HandlerMode,
916 state_store: Option<RateLimitState>,
917 cert_source: Option<Arc<dyn TlsCertSource>>,
918 module_factory: Option<Arc<ModuleFactory>>,
919 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
920 let listener = TcpListener::bind(config.proxy.listen).await?;
921 let listen_addr = listener.local_addr()?;
922 let client: Client<HttpConnector, HyperBoxBody> =
923 Client::builder(TokioExecutor::new()).build(HttpConnector::new());
924 let grpc_client: Client<HttpConnector, HyperBoxBody> =
926 Client::builder(TokioExecutor::new()).http2_only(true).build(HttpConnector::new());
927
928 let tls_acceptor = tls::acceptor_from_source(&config.tls, cert_source)
932 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
933 if tls_acceptor.is_some() {
934 info!(listen = %listen_addr, alpn = ?config.tls.alpn, "TLS termination enabled");
935 }
936
937 let rl_state = state_store
942 .unwrap_or_else(|| RateLimitState::in_memory(config.rate_limit.max_tracked_keys));
943
944 let mut extra_total = extra;
949 if let Some(factory) = &module_factory {
950 extra_total.extend(factory()?);
951 }
952 let reloadable = build_reloadable(config, rl_state.clone(), extra_total);
953
954 let metrics = Arc::new(Metrics::new());
958 let metrics_listener = if config.metrics.enabled {
959 let l = TcpListener::bind(config.metrics.listen).await?;
960 info!(listen = %l.local_addr()?, "metrics endpoint enabled (/metrics)");
961 Some(l)
962 } else {
963 None
964 };
965
966 Ok(Self {
967 listener,
968 state: Arc::new(StaticState {
969 client,
970 grpc_client,
971 listen_addr,
972 rl_state,
973 current: RwLock::new(Arc::new(reloadable)),
974 mode,
975 tls_acceptor,
976 metrics,
977 module_factory,
978 }),
979 metrics_listener,
980 })
981 }
982
983 pub fn reloader(&self) -> Reloader {
987 Reloader(Arc::clone(&self.state))
988 }
989
990 pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
991 self.listener.local_addr()
992 }
993
994 pub fn metrics_addr(&self) -> Option<SocketAddr> {
996 self.metrics_listener.as_ref().and_then(|l| l.local_addr().ok())
997 }
998
999 pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1000 if let Some(metrics_listener) = self.metrics_listener {
1003 let metrics = Arc::clone(&self.state.metrics);
1004 tokio::spawn(serve_metrics(metrics_listener, metrics));
1005 }
1006 loop {
1007 let (stream, client_addr) = self.listener.accept().await?;
1008 let state = Arc::clone(&self.state);
1009
1010 tokio::spawn(async move {
1011 match state.tls_acceptor.clone() {
1016 Some(acceptor) => match acceptor.accept(stream).await {
1017 Ok(tls_stream) => {
1018 serve_connection(TokioIo::new(tls_stream), state, client_addr).await;
1019 }
1020 Err(e) => {
1021 warn!(error = %e, client_ip = %client_addr.ip(), "TLS handshake error");
1022 }
1023 },
1024 None => {
1025 serve_connection(TokioIo::new(stream), state, client_addr).await;
1026 }
1027 }
1028 });
1029 }
1030 }
1031}
1032
1033pub struct ProxyBuilder<'a> {
1039 config: &'a Config,
1040 modules: Vec<Box<dyn WafModule>>,
1041 state_store: Option<RateLimitState>,
1042 cert_source: Option<Arc<dyn TlsCertSource>>,
1043 module_factory: Option<Arc<ModuleFactory>>,
1044 mode: HandlerMode,
1045}
1046
1047impl<'a> ProxyBuilder<'a> {
1048 pub fn modules(mut self, modules: Vec<Box<dyn WafModule>>) -> Self {
1051 self.modules = modules;
1052 self
1053 }
1054
1055 pub fn add_module(mut self, module: Box<dyn WafModule>) -> Self {
1057 self.modules.push(module);
1058 self
1059 }
1060
1061 pub fn state_store(mut self, store: Arc<dyn StateStore>) -> Self {
1065 self.state_store = Some(RateLimitState::with_store(store));
1066 self
1067 }
1068
1069 pub fn cert_source(mut self, source: Arc<dyn TlsCertSource>) -> Self {
1074 self.cert_source = Some(source);
1075 self
1076 }
1077
1078 pub fn module_factory<F>(mut self, factory: F) -> Self
1086 where
1087 F: Fn() -> Result<Vec<Box<dyn WafModule>>, Box<dyn std::error::Error + Send + Sync>>
1088 + Send
1089 + Sync
1090 + 'static,
1091 {
1092 self.module_factory = Some(Arc::new(factory));
1093 self
1094 }
1095
1096 pub async fn build(self) -> Result<Proxy, Box<dyn std::error::Error + Send + Sync>> {
1098 Proxy::bind_inner(
1099 self.config,
1100 self.modules,
1101 self.mode,
1102 self.state_store,
1103 self.cert_source,
1104 self.module_factory,
1105 )
1106 .await
1107 }
1108}
1109
1110async fn serve_connection<I>(io: I, state: Arc<StaticState>, client_addr: SocketAddr)
1114where
1115 I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
1116{
1117 let svc = service_fn(move |req| {
1118 let state = Arc::clone(&state);
1119 handle(req, state, client_addr)
1120 });
1121 if let Err(e) = auto::Builder::new(TokioExecutor::new())
1122 .serve_connection(io, svc)
1123 .await
1124 {
1125 warn!(error = %e, client_ip = %client_addr.ip(), "connection error");
1126 }
1127}
1128
1129async fn serve_metrics(listener: TcpListener, metrics: Arc<Metrics>) {
1133 loop {
1134 let Ok((stream, _)) = listener.accept().await else { continue };
1135 let metrics = Arc::clone(&metrics);
1136 tokio::spawn(async move {
1137 let svc = service_fn(move |req: Request<Incoming>| {
1138 let metrics = Arc::clone(&metrics);
1139 async move { Ok::<_, Infallible>(metrics_response(&req, &metrics)) }
1140 });
1141 let _ = hyper::server::conn::http1::Builder::new()
1142 .serve_connection(TokioIo::new(stream), svc)
1143 .await;
1144 });
1145 }
1146}
1147
1148fn metrics_response(req: &Request<Incoming>, metrics: &Metrics) -> Response<HyperBoxBody> {
1150 if req.method() == hyper::Method::GET && req.uri().path() == "/metrics" {
1151 Response::builder()
1152 .status(200)
1153 .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
1154 .body(full_body(metrics.render()))
1155 .unwrap()
1156 } else {
1157 Response::builder().status(404).body(full_body("Not Found")).unwrap()
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164 use waf_core::WafMode;
1165
1166 #[test]
1167 fn hop_by_hop_includes_connection_and_host() {
1168 assert!(HOP_BY_HOP.contains(&"connection"));
1169 assert!(HOP_BY_HOP.contains(&"host"));
1170 assert!(HOP_BY_HOP.contains(&"transfer-encoding"));
1171 }
1172
1173 #[test]
1174 fn hop_by_hop_excludes_regular_headers() {
1175 assert!(!HOP_BY_HOP.contains(&"content-type"));
1176 assert!(!HOP_BY_HOP.contains(&"authorization"));
1177 assert!(!HOP_BY_HOP.contains(&"x-custom-header"));
1178 }
1179
1180 #[test]
1181 fn config_parses_from_toml() {
1182 let raw = r#"
1183[proxy]
1184listen = "127.0.0.1:8080"
1185backend = "http://localhost:3000"
1186
1187[waf]
1188mode = "detection-only"
1189block_threshold = 10
1190"#;
1191 let config: Config = toml::from_str(raw).unwrap();
1192 assert_eq!(config.proxy.backend, "http://localhost:3000");
1193 assert_eq!(config.waf.mode, WafMode::DetectionOnly);
1194 assert_eq!(config.waf.block_threshold, 10);
1195 }
1196
1197 #[test]
1198 fn config_uses_default_block_threshold_when_omitted() {
1199 let raw = r#"
1200[proxy]
1201listen = "127.0.0.1:8080"
1202backend = "http://localhost:3000"
1203
1204[waf]
1205mode = "detection-only"
1206"#;
1207 let config: Config = toml::from_str(raw).unwrap();
1208 assert_eq!(config.waf.block_threshold, 5);
1209 }
1210
1211 #[test]
1212 fn config_parses_network_section() {
1213 let raw = r#"
1214[proxy]
1215listen = "127.0.0.1:8080"
1216backend = "http://localhost:3000"
1217
1218[waf]
1219mode = "blocking"
1220
1221[network]
1222trusted_proxies = ["10.0.0.0/8", "::1"]
1223client_ip_header = "X-Forwarded-For"
1224trusted_hops = 2
1225"#;
1226 let config: Config = toml::from_str(raw).unwrap();
1227 assert_eq!(config.network.trusted_proxies, vec!["10.0.0.0/8", "::1"]);
1228 assert_eq!(config.network.client_ip_header, "X-Forwarded-For");
1229 assert_eq!(config.network.trusted_hops, 2);
1230 }
1231
1232 #[test]
1233 fn config_network_defaults_to_failsafe_when_absent() {
1234 let raw = r#"
1235[proxy]
1236listen = "127.0.0.1:8080"
1237backend = "http://localhost:3000"
1238
1239[waf]
1240mode = "detection-only"
1241"#;
1242 let config: Config = toml::from_str(raw).unwrap();
1243 assert!(config.network.trusted_proxies.is_empty());
1244 assert_eq!(config.network.trusted_hops, 1);
1245 assert_eq!(config.network.client_ip_header, "x-forwarded-for".to_string());
1246 }
1247
1248 #[test]
1249 fn config_rejects_unknown_mode() {
1250 let raw = r#"
1251[proxy]
1252listen = "127.0.0.1:8080"
1253backend = "http://localhost:3000"
1254
1255[waf]
1256mode = "unknown-mode"
1257"#;
1258 assert!(toml::from_str::<Config>(raw).is_err());
1259 }
1260
1261 #[test]
1262 fn parse_cookies_splits_on_semicolon() {
1263 let headers = vec![("cookie".to_string(), "session=abc; user=123".to_string())];
1264 let cookies = parse_cookies(&headers);
1265 assert_eq!(cookies.len(), 2);
1266 assert!(cookies.contains(&("session".to_string(), "abc".to_string())));
1267 assert!(cookies.contains(&("user".to_string(), "123".to_string())));
1268 }
1269
1270 #[test]
1271 fn parse_cookies_handles_missing_value() {
1272 let headers = vec![("cookie".to_string(), "flag=; token=xyz".to_string())];
1273 let cookies = parse_cookies(&headers);
1274 assert!(cookies.contains(&("flag".to_string(), "".to_string())));
1275 assert!(cookies.contains(&("token".to_string(), "xyz".to_string())));
1276 }
1277
1278 #[test]
1279 fn parse_cookies_handles_empty_header_list() {
1280 assert!(parse_cookies(&[]).is_empty());
1281 }
1282
1283 #[test]
1284 fn request_id_is_unique_per_call() {
1285 let id1 = next_request_id();
1286 let id2 = next_request_id();
1287 assert_ne!(id1, id2);
1288 assert!(id1.starts_with("req-"));
1289 }
1290}