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
53const HOP_BY_HOP: &[&str] = &[
55 "connection",
56 "host", "keep-alive",
58 "proxy-authenticate",
59 "proxy-authorization",
60 "te",
61 "trailers",
62 "transfer-encoding",
63 "upgrade",
64];
65
66static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
67
68fn next_request_id() -> String {
69 let n = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
70 format!("req-{n:016x}")
71}
72
73pub fn full_body(data: impl Into<Bytes>) -> HyperBoxBody {
74 Full::new(data.into())
75 .map_err(|never| match never {})
76 .boxed()
77}
78
79struct FramedBody {
84 data: Option<Bytes>,
85 trailers: Option<HeaderMap>,
86}
87
88impl Body for FramedBody {
89 type Data = Bytes;
90 type Error = Infallible;
91
92 fn poll_frame(
93 mut self: Pin<&mut Self>,
94 _cx: &mut Context<'_>,
95 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
96 if let Some(d) = self.data.take() {
97 return Poll::Ready(Some(Ok(Frame::data(d))));
98 }
99 if let Some(t) = self.trailers.take() {
100 return Poll::Ready(Some(Ok(Frame::trailers(t))));
101 }
102 Poll::Ready(None)
103 }
104}
105
106fn body_with_trailers(data: Bytes, trailers: Option<HeaderMap>) -> HyperBoxBody {
109 match trailers {
110 None => full_body(data),
111 Some(t) => FramedBody { data: Some(data), trailers: Some(t) }
112 .map_err(|never| match never {})
113 .boxed(),
114 }
115}
116
117async fn collect_with_trailers<B>(body: B) -> Result<(Bytes, Option<HeaderMap>), B::Error>
121where
122 B: Body<Data = Bytes>,
123{
124 let collected = body.collect().await?;
125 let trailers = collected.trailers().cloned();
126 Ok((collected.to_bytes(), trailers))
127}
128
129fn is_grpc_request(parts: &hyper::http::request::Parts) -> bool {
133 parts
134 .headers
135 .get(hyper::header::CONTENT_TYPE)
136 .and_then(|v| v.to_str().ok())
137 .map(|ct| ct.trim_start().starts_with("application/grpc"))
138 .unwrap_or(false)
139}
140
141fn parse_cookies(headers: &[(String, String)]) -> Vec<(String, String)> {
142 headers
143 .iter()
144 .filter(|(name, _)| name.eq_ignore_ascii_case("cookie"))
145 .flat_map(|(_, value)| {
146 value.split(';').filter_map(|pair| {
147 let mut parts = pair.splitn(2, '=');
148 let key = parts.next()?.trim().to_string();
149 let val = parts.next().unwrap_or("").trim().to_string();
150 Some((key, val))
151 })
152 })
153 .collect()
154}
155
156fn build_context(
157 parts: &hyper::http::request::Parts,
158 body: &Bytes,
159 client_addr: SocketAddr,
160 ip_resolver: &ClientIpResolver,
161) -> RequestContext {
162 let path = parts.uri.path().to_string();
163 let query = parts.uri.query().map(str::to_string);
164 let method = parts.method.to_string();
165 let http_version = format!("{:?}", parts.version);
166
167 let headers: Vec<(String, String)> = parts
168 .headers
169 .iter()
170 .filter_map(|(name, value)| {
171 value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
172 })
173 .collect();
174
175 let cookies = parse_cookies(&headers);
176
177 let normalized = Normalized::default();
178
179 let request_id = next_request_id();
184 let resolved = ip_resolver.resolve(client_addr.ip(), &headers);
185 match resolved.source {
186 IpSource::FallbackMissingHeader | IpSource::FallbackMalformed => warn!(
187 request_id = %request_id,
188 peer = %client_addr.ip(),
189 source = ?resolved.source,
190 "client-IP resolution fell back to peer address"
191 ),
192 IpSource::DirectPeer | IpSource::TrustedHeader => {}
193 }
194
195 RequestContext {
196 client_ip: resolved.ip,
197 request_id,
198 timestamp: SystemTime::now(),
199 method,
200 path: path.clone(),
201 raw_path: path,
202 query,
203 http_version,
204 headers,
205 cookies,
206 body: body.clone(),
207 normalized,
208 score: 0,
209 score_contributions: vec![],
210 }
211}
212
213struct Reloadable {
217 backend: String,
218 normalizer: Normalizer,
219 pipeline: Pipeline,
220 prefilter: ContentPrefilter,
224 ip_resolver: ClientIpResolver,
225 resilience: ResilienceConfig,
226}
227
228struct StaticState {
235 client: Client<HttpConnector, HyperBoxBody>,
236 grpc_client: Client<HttpConnector, HyperBoxBody>,
240 listen_addr: SocketAddr,
241 rl_state: RateLimitState,
242 current: RwLock<Arc<Reloadable>>,
243 mode: HandlerMode,
244 tls_acceptor: Option<TlsAcceptor>,
248 metrics: Arc<Metrics>,
251}
252
253#[derive(Clone, Copy)]
260enum HandlerMode {
261 Inspect,
262 Passthrough,
263}
264
265impl StaticState {
266 fn current(&self) -> Arc<Reloadable> {
271 self.current
272 .read()
273 .unwrap_or_else(|poisoned| poisoned.into_inner())
274 .clone()
275 }
276}
277
278#[derive(Clone)]
282pub struct Reloader(Arc<StaticState>);
283
284impl Reloader {
285 pub fn reload_from(&self, path: &Path) -> Result<(), config::LoadError> {
289 let new_cfg = match config::load(path) {
290 Ok(c) => c,
291 Err(e) => {
292 error!(error = %e, "config reload failed; keeping current configuration");
293 return Err(e);
294 }
295 };
296
297 if new_cfg.proxy.listen != self.0.listen_addr {
299 warn!(
300 current = %self.0.listen_addr,
301 requested = %new_cfg.proxy.listen,
302 "proxy.listen change requires a restart; keeping the current bind address"
303 );
304 }
305
306 let new_reloadable = build_reloadable(&new_cfg, self.0.rl_state.clone(), Vec::new());
309
310 *self
314 .0
315 .current
316 .write()
317 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(new_reloadable);
318 info!("configuration reloaded");
319 Ok(())
320 }
321}
322
323fn upstream_error_response(
328 ctx: &RequestContext,
329 resilience: &ResilienceConfig,
330 detail: &str,
331) -> Response<HyperBoxBody> {
332 let (status, body) = match resilience.on_upstream_error {
333 FailMode::FailClosed => (502, "Bad Gateway"),
334 FailMode::FailOpen => (503, "Service Unavailable"),
335 };
336 warn!(
337 request_id = %ctx.request_id,
338 client_ip = %ctx.client_ip,
339 status = status,
340 policy = ?resilience.on_upstream_error,
341 detail = detail,
342 "upstream error: applying on_upstream_error policy"
343 );
344 Response::builder().status(status).body(full_body(body)).unwrap()
345}
346
347fn deny_response(
350 ctx: &RequestContext,
351 verdict: PipelineVerdict,
352) -> Option<(Response<HyperBoxBody>, Outcome)> {
353 match verdict {
354 PipelineVerdict::Allow => None,
355 PipelineVerdict::Block { rule_id, reason } => {
356 warn!(
357 request_id = %ctx.request_id,
358 rule_id = %rule_id,
359 reason = %reason,
360 score = ctx.score,
361 "request blocked"
362 );
363 Some((
364 Response::builder()
365 .status(403)
366 .body(full_body("Forbidden"))
367 .unwrap(),
368 Outcome::Blocked,
369 ))
370 }
371 PipelineVerdict::Reject { rule_id, reason, status, retry_after } => {
372 warn!(
373 request_id = %ctx.request_id,
374 rule_id = %rule_id,
375 reason = %reason,
376 status = status,
377 "request rejected"
378 );
379 let (body, outcome) = match status {
382 429 => ("Too Many Requests", Outcome::RateLimited),
383 400 => ("Bad Request", Outcome::BadRequest),
384 _ => ("Rejected", Outcome::BadRequest),
385 };
386 let mut builder = Response::builder().status(status);
387 if let Some(secs) = retry_after {
388 builder = builder.header("retry-after", secs.to_string());
389 }
390 Some((builder.body(full_body(body)).unwrap(), outcome))
391 }
392 }
393}
394
395async fn try_forward(
396 req: Request<Incoming>,
397 state: &StaticState,
398 client_addr: SocketAddr,
399) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
400 let rel = state.current();
403
404 let (parts, body) = req.into_parts();
405 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
408
409 let mut ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
410
411 let connection_verdict = rel.pipeline.run_connection(&mut ctx);
414 if let Some(denied) = deny_response(&ctx, connection_verdict) {
415 return Ok(denied);
416 }
417
418 let normalized_ok = match rel.normalizer.normalize(&mut ctx) {
422 Ok(()) => true,
423 Err(e) => match rel.resilience.on_parser_limit {
424 FailMode::FailClosed => {
425 warn!(
426 request_id = %ctx.request_id,
427 error = %e,
428 policy = ?FailMode::FailClosed,
429 "normalization failed: rejecting (on_parser_limit)"
430 );
431 return Ok((
432 Response::builder()
433 .status(400)
434 .body(full_body("Bad Request"))
435 .unwrap(),
436 Outcome::BadRequest,
437 ));
438 }
439 FailMode::FailOpen => {
440 warn!(
441 request_id = %ctx.request_id,
442 error = %e,
443 policy = ?FailMode::FailOpen,
444 "normalization failed: forwarding UNINSPECTED (on_parser_limit)"
445 );
446 false
447 }
448 },
449 };
450
451 let path_and_query = parts
452 .uri
453 .path_and_query()
454 .map(|pq| pq.as_str())
455 .unwrap_or("/")
456 .to_string();
457
458 info!(
459 request_id = %ctx.request_id,
460 method = %ctx.method,
461 path = %path_and_query,
462 client_ip = %ctx.client_ip,
463 "→ request"
464 );
465
466 if normalized_ok {
469 let inspect = rel.prefilter.is_candidate(&ctx);
475 let inspection_verdict = rel.pipeline.run_inspection_gated(&mut ctx, inspect);
476 if let Some(denied) = deny_response(&ctx, inspection_verdict) {
477 return Ok(denied);
478 }
479 }
480
481 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
482}
483
484#[allow(clippy::too_many_arguments)]
493async fn forward_to_backend(
494 state: &StaticState,
495 rel: &Reloadable,
496 parts: &hyper::http::request::Parts,
497 path_and_query: &str,
498 body_bytes: Bytes,
499 req_trailers: Option<HeaderMap>,
500 client_addr: SocketAddr,
501 ctx: &RequestContext,
502) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
503 let backend_uri: Uri = format!("{}{}", rel.backend, path_and_query).parse()?;
504 let is_grpc = is_grpc_request(parts);
505
506 let mut builder = Request::builder()
507 .method(parts.method.clone())
508 .uri(backend_uri);
509
510 for (name, value) in &parts.headers {
511 if !HOP_BY_HOP.contains(&name.as_str()) {
512 builder = builder.header(name, value);
513 }
514 }
515 builder = builder.header("x-forwarded-for", client_addr.ip().to_string());
518 builder = builder.header("x-request-id", ctx.request_id.as_str());
519 if is_grpc {
522 builder = builder.header("te", "trailers");
523 }
524
525 let (client, fwd_body) = if is_grpc {
528 (&state.grpc_client, body_with_trailers(body_bytes, req_trailers))
529 } else {
530 (&state.client, full_body(body_bytes))
531 };
532 let fwd_req = builder.body(fwd_body)?;
533
534 let upstream = tokio::time::timeout(rel.resilience.upstream_timeout(), async {
538 let resp = client.request(fwd_req).await?;
539 let (resp_parts, resp_body) = resp.into_parts();
540 let (resp_bytes, resp_trailers) = collect_with_trailers(resp_body).await?;
543 Ok::<_, Box<dyn std::error::Error + Send + Sync>>((resp_parts, resp_bytes, resp_trailers))
544 })
545 .await;
546
547 let (resp_parts, resp_bytes, resp_trailers) = match upstream {
548 Ok(Ok(triple)) => triple,
549 Ok(Err(e)) => {
550 return Ok((
551 upstream_error_response(ctx, &rel.resilience, &e.to_string()),
552 Outcome::UpstreamError,
553 ))
554 }
555 Err(_elapsed) => {
556 return Ok((
557 upstream_error_response(ctx, &rel.resilience, "upstream timeout"),
558 Outcome::UpstreamError,
559 ))
560 }
561 };
562
563 info!(
564 request_id = %ctx.request_id,
565 status = %resp_parts.status,
566 score = ctx.score,
567 "← response"
568 );
569
570 Ok((
571 Response::from_parts(resp_parts, body_with_trailers(resp_bytes, resp_trailers)),
572 Outcome::Allowed,
573 ))
574}
575
576async fn try_passthrough(
583 req: Request<Incoming>,
584 state: &StaticState,
585 client_addr: SocketAddr,
586) -> Result<(Response<HyperBoxBody>, Outcome), Box<dyn std::error::Error + Send + Sync>> {
587 let rel = state.current();
588 let (parts, body) = req.into_parts();
589 let (body_bytes, req_trailers) = collect_with_trailers(body).await?;
590 let ctx = build_context(&parts, &body_bytes, client_addr, &rel.ip_resolver);
591 let path_and_query = parts
592 .uri
593 .path_and_query()
594 .map(|pq| pq.as_str())
595 .unwrap_or("/")
596 .to_string();
597 forward_to_backend(state, &rel, &parts, &path_and_query, body_bytes, req_trailers, client_addr, &ctx).await
598}
599
600async fn handle(
601 req: Request<Incoming>,
602 state: Arc<StaticState>,
603 client_addr: SocketAddr,
604) -> Result<Response<HyperBoxBody>, Infallible> {
605 let start = Instant::now();
608 let result = match state.mode {
609 HandlerMode::Inspect => try_forward(req, &state, client_addr).await,
610 HandlerMode::Passthrough => try_passthrough(req, &state, client_addr).await,
611 };
612 let (resp, outcome) = match result {
616 Ok((resp, outcome)) => (resp, outcome),
617 Err(e) => {
618 error!(error = %e, client_ip = %client_addr.ip(), "forwarding error");
619 let resp = Response::builder()
620 .status(502)
621 .body(full_body("Bad Gateway"))
622 .unwrap();
623 (resp, Outcome::InternalError)
624 }
625 };
626 state.metrics.record(outcome, start.elapsed());
627 Ok(resp)
628}
629
630pub struct Proxy {
631 listener: TcpListener,
632 state: Arc<StaticState>,
633 metrics_listener: Option<TcpListener>,
637}
638
639fn build_modules(config: &Config, rl_state: &RateLimitState) -> Vec<Box<dyn WafModule>> {
642 let mut modules: Vec<Box<dyn WafModule>> = vec![Box::new(NoopLogger)];
643 if config.modules.request_smuggling.enabled {
646 modules.push(Box::new(RequestSmugglingModule::new()));
647 }
648 if config.rate_limit.enabled {
649 modules.push(Box::new(RateLimitModule::with_state(rl_state.clone())));
650 }
651 if config.modules.sqli.enabled {
652 modules.push(Box::new(SqliModule::new()));
653 }
654 if config.modules.xss.enabled {
655 modules.push(Box::new(XssModule::new()));
656 }
657 if config.modules.path_traversal.enabled {
658 modules.push(Box::new(PathTraversalModule::new()));
659 }
660 if config.modules.rce.enabled {
661 modules.push(Box::new(RceModule::new()));
662 }
663 if config.modules.lfi_rfi.enabled {
664 modules.push(Box::new(LfiRfiModule::new()));
665 }
666 if config.modules.ssrf.enabled {
667 modules.push(Box::new(SsrfModule::new()));
668 }
669 if config.modules.ldap.enabled {
670 modules.push(Box::new(LdapModule::new()));
671 }
672 if config.modules.nosql.enabled {
673 modules.push(Box::new(NosqlModule::new()));
674 }
675 if config.modules.mail.enabled {
676 modules.push(Box::new(MailModule::new()));
677 }
678 if config.modules.ssti.enabled {
679 modules.push(Box::new(SstiModule::new()));
680 }
681 if config.modules.scanner.enabled {
682 modules.push(Box::new(ScannerModule::new()));
683 }
684 if config.modules.ssi.enabled {
685 modules.push(Box::new(SsiModule::new()));
686 }
687 if config.modules.xxe.enabled {
688 modules.push(Box::new(XxeModule::new()));
689 }
690 if config.modules.header_injection.enabled {
691 modules.push(Box::new(HeaderInjectionModule::new()));
692 }
693 if config.modules.graphql.enabled {
694 modules.push(Box::new(GraphqlModule::new()));
695 }
696 if config.modules.grpc.enabled {
697 modules.push(Box::new(GrpcModule::new()));
698 }
699 if config.modules.crs.enabled {
700 modules.push(Box::new(load_crs_module(&config.modules.crs.files)));
701 }
702 if config.modules.wasm.enabled {
703 for plugin in &config.modules.wasm.plugins {
704 if let Some(m) = load_wasm_plugin(plugin, &config.modules.wasm) {
705 modules.push(Box::new(m));
706 }
707 }
708 }
709 modules
710}
711
712fn load_wasm_plugin(
718 plugin: &waf_core::WasmPluginConfig,
719 cfg: &waf_core::WasmConfig,
720) -> Option<WasmModule> {
721 let bytes = match std::fs::read(&plugin.path) {
722 Ok(b) => b,
723 Err(e) => {
724 error!(file = %plugin.path, error = %e, "WASM: cannot read plugin (skipped)");
725 return None;
726 }
727 };
728 let name = plugin_name(&plugin.path);
729 let opts = WasmOptions {
730 pool_size: cfg.pool_size,
731 fuel_per_request: cfg.fuel_per_request,
732 max_memory_bytes: cfg.max_memory_bytes,
733 checkout_timeout: std::time::Duration::from_millis(cfg.checkout_timeout_ms),
734 };
735 let config_bytes = plugin.config.as_deref().unwrap_or("").as_bytes();
736 match WasmModule::from_bytes(&name, &bytes, config_bytes, &opts) {
737 Ok((module, report)) => {
738 info!(plugin = %name, "{}", report.summary());
741 Some(module)
742 }
743 Err(e) => {
744 error!(file = %plugin.path, error = %e, "WASM: plugin failed to load (skipped)");
745 None
746 }
747 }
748}
749
750fn plugin_name(path: &str) -> String {
752 std::path::Path::new(path)
753 .file_stem()
754 .and_then(|s| s.to_str())
755 .unwrap_or("plugin")
756 .to_string()
757}
758
759fn load_crs_module(files: &[String]) -> CrsModule {
766 let mut combined = String::new();
767 for path in files {
768 match std::fs::read_to_string(path) {
769 Ok(text) => {
770 combined.push_str(&text);
771 combined.push('\n');
772 }
773 Err(e) => error!(file = %path, error = %e, "CRS import: cannot read file (skipped)"),
774 }
775 }
776 let module = CrsModule::from_source(&combined);
777 info!(files = files.len(), "{}", module.report());
778 if !module.skipped().is_empty() {
779 warn!(
780 skipped = module.skipped().len(),
781 "CRS import: some rules fall outside the supported subset (see debug logs for reasons)"
782 );
783 for s in module.skipped() {
784 debug!(id = ?s.id, line = s.line_no, reason = %s.reason, "CRS import: rule skipped");
785 }
786 }
787 module
788}
789
790fn build_reloadable(
795 config: &Config,
796 rl_state: RateLimitState,
797 extra: Vec<Box<dyn WafModule>>,
798) -> Reloadable {
799 let mut modules = build_modules(config, &rl_state);
800 modules.extend(extra);
801 let pipeline = Pipeline::new(config, modules);
802
803 if config.waf.paranoia_level > waf_detection::HIGHEST_RULE_PARANOIA {
806 warn!(
807 paranoia_level = config.waf.paranoia_level,
808 highest_rule_paranoia = waf_detection::HIGHEST_RULE_PARANOIA,
809 "paranoia_level exceeds the highest existing rule paranoia: no additional rules are activated"
810 );
811 }
812 let ip_resolver = ClientIpResolver::from_config(&config.network);
813 if ip_resolver.trusted_count() < config.network.trusted_proxies.len() {
814 warn!(
815 configured = config.network.trusted_proxies.len(),
816 valid = ip_resolver.trusted_count(),
817 "some trusted_proxies CIDR entries were invalid and skipped"
818 );
819 }
820
821 Reloadable {
822 backend: config.proxy.backend.trim_end_matches('/').to_string(),
823 normalizer: Normalizer::new(&config.limits),
824 pipeline,
825 prefilter: ContentPrefilter::new(config.waf.paranoia_level),
827 ip_resolver,
828 resilience: config.resilience,
829 }
830}
831
832impl Proxy {
833 pub async fn bind(config: &Config) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
837 Self::builder(config).build().await
838 }
839
840 pub fn builder(config: &Config) -> ProxyBuilder<'_> {
845 ProxyBuilder {
846 config,
847 modules: Vec::new(),
848 state_store: None,
849 cert_source: None,
850 mode: HandlerMode::Inspect,
851 }
852 }
853
854 #[doc(hidden)]
860 pub async fn bind_with_modules(
861 config: &Config,
862 extra: Vec<Box<dyn WafModule>>,
863 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
864 Self::bind_inner(config, extra, HandlerMode::Inspect, None, None).await
865 }
866
867 #[doc(hidden)]
872 pub async fn bind_passthrough(
873 config: &Config,
874 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
875 Self::bind_inner(config, Vec::new(), HandlerMode::Passthrough, None, None).await
876 }
877
878 async fn bind_inner(
879 config: &Config,
880 extra: Vec<Box<dyn WafModule>>,
881 mode: HandlerMode,
882 state_store: Option<RateLimitState>,
883 cert_source: Option<Arc<dyn TlsCertSource>>,
884 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
885 let listener = TcpListener::bind(config.proxy.listen).await?;
886 let listen_addr = listener.local_addr()?;
887 let client: Client<HttpConnector, HyperBoxBody> =
888 Client::builder(TokioExecutor::new()).build(HttpConnector::new());
889 let grpc_client: Client<HttpConnector, HyperBoxBody> =
891 Client::builder(TokioExecutor::new()).http2_only(true).build(HttpConnector::new());
892
893 let tls_acceptor = tls::acceptor_from_source(&config.tls, cert_source)
897 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
898 if tls_acceptor.is_some() {
899 info!(listen = %listen_addr, alpn = ?config.tls.alpn, "TLS termination enabled");
900 }
901
902 let rl_state = state_store
907 .unwrap_or_else(|| RateLimitState::in_memory(config.rate_limit.max_tracked_keys));
908 let reloadable = build_reloadable(config, rl_state.clone(), extra);
909
910 let metrics = Arc::new(Metrics::new());
914 let metrics_listener = if config.metrics.enabled {
915 let l = TcpListener::bind(config.metrics.listen).await?;
916 info!(listen = %l.local_addr()?, "metrics endpoint enabled (/metrics)");
917 Some(l)
918 } else {
919 None
920 };
921
922 Ok(Self {
923 listener,
924 state: Arc::new(StaticState {
925 client,
926 grpc_client,
927 listen_addr,
928 rl_state,
929 current: RwLock::new(Arc::new(reloadable)),
930 mode,
931 tls_acceptor,
932 metrics,
933 }),
934 metrics_listener,
935 })
936 }
937
938 pub fn reloader(&self) -> Reloader {
942 Reloader(Arc::clone(&self.state))
943 }
944
945 pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
946 self.listener.local_addr()
947 }
948
949 pub fn metrics_addr(&self) -> Option<SocketAddr> {
951 self.metrics_listener.as_ref().and_then(|l| l.local_addr().ok())
952 }
953
954 pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
955 if let Some(metrics_listener) = self.metrics_listener {
958 let metrics = Arc::clone(&self.state.metrics);
959 tokio::spawn(serve_metrics(metrics_listener, metrics));
960 }
961 loop {
962 let (stream, client_addr) = self.listener.accept().await?;
963 let state = Arc::clone(&self.state);
964
965 tokio::spawn(async move {
966 match state.tls_acceptor.clone() {
971 Some(acceptor) => match acceptor.accept(stream).await {
972 Ok(tls_stream) => {
973 serve_connection(TokioIo::new(tls_stream), state, client_addr).await;
974 }
975 Err(e) => {
976 warn!(error = %e, client_ip = %client_addr.ip(), "TLS handshake error");
977 }
978 },
979 None => {
980 serve_connection(TokioIo::new(stream), state, client_addr).await;
981 }
982 }
983 });
984 }
985 }
986}
987
988pub struct ProxyBuilder<'a> {
994 config: &'a Config,
995 modules: Vec<Box<dyn WafModule>>,
996 state_store: Option<RateLimitState>,
997 cert_source: Option<Arc<dyn TlsCertSource>>,
998 mode: HandlerMode,
999}
1000
1001impl<'a> ProxyBuilder<'a> {
1002 pub fn modules(mut self, modules: Vec<Box<dyn WafModule>>) -> Self {
1005 self.modules = modules;
1006 self
1007 }
1008
1009 pub fn add_module(mut self, module: Box<dyn WafModule>) -> Self {
1011 self.modules.push(module);
1012 self
1013 }
1014
1015 pub fn state_store(mut self, store: Arc<dyn StateStore>) -> Self {
1019 self.state_store = Some(RateLimitState::with_store(store));
1020 self
1021 }
1022
1023 pub fn cert_source(mut self, source: Arc<dyn TlsCertSource>) -> Self {
1028 self.cert_source = Some(source);
1029 self
1030 }
1031
1032 pub async fn build(self) -> Result<Proxy, Box<dyn std::error::Error + Send + Sync>> {
1034 Proxy::bind_inner(self.config, self.modules, self.mode, self.state_store, self.cert_source)
1035 .await
1036 }
1037}
1038
1039async fn serve_connection<I>(io: I, state: Arc<StaticState>, client_addr: SocketAddr)
1043where
1044 I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
1045{
1046 let svc = service_fn(move |req| {
1047 let state = Arc::clone(&state);
1048 handle(req, state, client_addr)
1049 });
1050 if let Err(e) = auto::Builder::new(TokioExecutor::new())
1051 .serve_connection(io, svc)
1052 .await
1053 {
1054 warn!(error = %e, client_ip = %client_addr.ip(), "connection error");
1055 }
1056}
1057
1058async fn serve_metrics(listener: TcpListener, metrics: Arc<Metrics>) {
1062 loop {
1063 let Ok((stream, _)) = listener.accept().await else { continue };
1064 let metrics = Arc::clone(&metrics);
1065 tokio::spawn(async move {
1066 let svc = service_fn(move |req: Request<Incoming>| {
1067 let metrics = Arc::clone(&metrics);
1068 async move { Ok::<_, Infallible>(metrics_response(&req, &metrics)) }
1069 });
1070 let _ = hyper::server::conn::http1::Builder::new()
1071 .serve_connection(TokioIo::new(stream), svc)
1072 .await;
1073 });
1074 }
1075}
1076
1077fn metrics_response(req: &Request<Incoming>, metrics: &Metrics) -> Response<HyperBoxBody> {
1079 if req.method() == hyper::Method::GET && req.uri().path() == "/metrics" {
1080 Response::builder()
1081 .status(200)
1082 .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
1083 .body(full_body(metrics.render()))
1084 .unwrap()
1085 } else {
1086 Response::builder().status(404).body(full_body("Not Found")).unwrap()
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use waf_core::WafMode;
1094
1095 #[test]
1096 fn hop_by_hop_includes_connection_and_host() {
1097 assert!(HOP_BY_HOP.contains(&"connection"));
1098 assert!(HOP_BY_HOP.contains(&"host"));
1099 assert!(HOP_BY_HOP.contains(&"transfer-encoding"));
1100 }
1101
1102 #[test]
1103 fn hop_by_hop_excludes_regular_headers() {
1104 assert!(!HOP_BY_HOP.contains(&"content-type"));
1105 assert!(!HOP_BY_HOP.contains(&"authorization"));
1106 assert!(!HOP_BY_HOP.contains(&"x-custom-header"));
1107 }
1108
1109 #[test]
1110 fn config_parses_from_toml() {
1111 let raw = r#"
1112[proxy]
1113listen = "127.0.0.1:8080"
1114backend = "http://localhost:3000"
1115
1116[waf]
1117mode = "detection-only"
1118block_threshold = 10
1119"#;
1120 let config: Config = toml::from_str(raw).unwrap();
1121 assert_eq!(config.proxy.backend, "http://localhost:3000");
1122 assert_eq!(config.waf.mode, WafMode::DetectionOnly);
1123 assert_eq!(config.waf.block_threshold, 10);
1124 }
1125
1126 #[test]
1127 fn config_uses_default_block_threshold_when_omitted() {
1128 let raw = r#"
1129[proxy]
1130listen = "127.0.0.1:8080"
1131backend = "http://localhost:3000"
1132
1133[waf]
1134mode = "detection-only"
1135"#;
1136 let config: Config = toml::from_str(raw).unwrap();
1137 assert_eq!(config.waf.block_threshold, 5);
1138 }
1139
1140 #[test]
1141 fn config_parses_network_section() {
1142 let raw = r#"
1143[proxy]
1144listen = "127.0.0.1:8080"
1145backend = "http://localhost:3000"
1146
1147[waf]
1148mode = "blocking"
1149
1150[network]
1151trusted_proxies = ["10.0.0.0/8", "::1"]
1152client_ip_header = "X-Forwarded-For"
1153trusted_hops = 2
1154"#;
1155 let config: Config = toml::from_str(raw).unwrap();
1156 assert_eq!(config.network.trusted_proxies, vec!["10.0.0.0/8", "::1"]);
1157 assert_eq!(config.network.client_ip_header, "X-Forwarded-For");
1158 assert_eq!(config.network.trusted_hops, 2);
1159 }
1160
1161 #[test]
1162 fn config_network_defaults_to_failsafe_when_absent() {
1163 let raw = r#"
1164[proxy]
1165listen = "127.0.0.1:8080"
1166backend = "http://localhost:3000"
1167
1168[waf]
1169mode = "detection-only"
1170"#;
1171 let config: Config = toml::from_str(raw).unwrap();
1172 assert!(config.network.trusted_proxies.is_empty());
1173 assert_eq!(config.network.trusted_hops, 1);
1174 assert_eq!(config.network.client_ip_header, "x-forwarded-for".to_string());
1175 }
1176
1177 #[test]
1178 fn config_rejects_unknown_mode() {
1179 let raw = r#"
1180[proxy]
1181listen = "127.0.0.1:8080"
1182backend = "http://localhost:3000"
1183
1184[waf]
1185mode = "unknown-mode"
1186"#;
1187 assert!(toml::from_str::<Config>(raw).is_err());
1188 }
1189
1190 #[test]
1191 fn parse_cookies_splits_on_semicolon() {
1192 let headers = vec![("cookie".to_string(), "session=abc; user=123".to_string())];
1193 let cookies = parse_cookies(&headers);
1194 assert_eq!(cookies.len(), 2);
1195 assert!(cookies.contains(&("session".to_string(), "abc".to_string())));
1196 assert!(cookies.contains(&("user".to_string(), "123".to_string())));
1197 }
1198
1199 #[test]
1200 fn parse_cookies_handles_missing_value() {
1201 let headers = vec![("cookie".to_string(), "flag=; token=xyz".to_string())];
1202 let cookies = parse_cookies(&headers);
1203 assert!(cookies.contains(&("flag".to_string(), "".to_string())));
1204 assert!(cookies.contains(&("token".to_string(), "xyz".to_string())));
1205 }
1206
1207 #[test]
1208 fn parse_cookies_handles_empty_header_list() {
1209 assert!(parse_cookies(&[]).is_empty());
1210 }
1211
1212 #[test]
1213 fn request_id_is_unique_per_call() {
1214 let id1 = next_request_id();
1215 let id2 = next_request_id();
1216 assert_ne!(id1, id2);
1217 assert!(id1.starts_with("req-"));
1218 }
1219}