//! L7 Routing Engine //! //! Evaluates L7 policies and rules to determine request routing. use axum::extract::Request; use axum::http::{HeaderMap, Uri}; use std::sync::Arc; use crate::metadata::LbMetadataStore; use fiberlb_types::{ L7CompareType, L7Policy, L7PolicyAction, L7Rule, L7RuleType, ListenerId, PoolId, }; /// Request information extracted for routing (Send + Sync safe) #[derive(Debug, Clone)] pub struct RequestInfo { pub headers: HeaderMap, pub uri: Uri, pub sni_hostname: Option, } impl RequestInfo { /// Extract routing info from request pub fn from_request(request: &Request) -> Self { Self { headers: request.headers().clone(), uri: request.uri().clone(), sni_hostname: request.extensions().get::().map(|s| s.0.clone()), } } } /// Routing decision result #[derive(Debug, Clone)] pub enum RoutingResult { /// Route to a specific pool Pool(PoolId), /// HTTP redirect to URL Redirect { url: String, status: u32 }, /// Reject with status code Reject { status: u32 }, /// Use default pool (no policy matched) Default, } /// L7 routing engine pub struct L7Router { metadata: Arc, } impl L7Router { /// Create a new L7 router pub fn new(metadata: Arc) -> Self { Self { metadata } } /// Evaluate policies for a request pub async fn evaluate( &self, listener_id: &ListenerId, request_info: &RequestInfo, ) -> RoutingResult { // Load policies ordered by position let policies = match self.metadata.list_l7_policies(listener_id).await { Ok(p) => p, Err(e) => { tracing::warn!("Failed to load L7 policies: {}", e); return RoutingResult::Default; } }; // Iterate through policies in order for policy in policies.iter().filter(|p| p.enabled) { // Load rules for this policy let rules = match self.metadata.list_l7_rules(&policy.id).await { Ok(r) => r, Err(e) => { tracing::warn!("Failed to load L7 rules for policy {}: {}", policy.id, e); continue; } }; // All rules must match (AND logic) let all_match = rules.iter().all(|rule| self.evaluate_rule(rule, request_info)); if all_match { return self.apply_policy_action(policy); } } RoutingResult::Default } /// Evaluate a single rule fn evaluate_rule(&self, rule: &L7Rule, info: &RequestInfo) -> bool { let value = match rule.rule_type { L7RuleType::HostName => { // Extract from Host header info.headers .get("host") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) } L7RuleType::Path => { // Extract from request URI Some(info.uri.path().to_string()) } L7RuleType::FileType => { // Extract file extension from path info.uri .path() .rsplit('.') .next() .filter(|ext| !ext.is_empty() && !ext.contains('/')) .map(|s| format!(".{}", s)) } L7RuleType::Header => { // Extract specific header by key rule.key.as_ref().and_then(|key| { info.headers .get(key) .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) }) } L7RuleType::Cookie => { // Extract cookie value by key self.extract_cookie(info, rule.key.as_deref()) } L7RuleType::SslConnHasSni => { // SNI extracted during TLS handshake (Phase 3) info.sni_hostname.clone() } }; let matched = match value { Some(v) => self.compare(&v, &rule.value, rule.compare_type), None => false, }; // Apply invert logic if rule.invert { !matched } else { matched } } /// Compare a value against a pattern fn compare(&self, value: &str, pattern: &str, compare_type: L7CompareType) -> bool { match compare_type { L7CompareType::EqualTo => value == pattern, L7CompareType::StartsWith => value.starts_with(pattern), L7CompareType::EndsWith => value.ends_with(pattern), L7CompareType::Contains => value.contains(pattern), L7CompareType::Regex => { // Compile regex on-the-fly (production should cache) regex::Regex::new(pattern) .map(|r| r.is_match(value)) .unwrap_or(false) } } } /// Extract cookie value from request fn extract_cookie(&self, info: &RequestInfo, cookie_name: Option<&str>) -> Option { let name = cookie_name?; info.headers .get("cookie") .and_then(|v| v.to_str().ok()) .and_then(|cookies| { cookies.split(';').find_map(|c| { let parts: Vec<_> = c.trim().splitn(2, '=').collect(); if parts.len() == 2 && parts[0] == name { Some(parts[1].to_string()) } else { None } }) }) } /// Apply policy action fn apply_policy_action(&self, policy: &L7Policy) -> RoutingResult { match policy.action { L7PolicyAction::RedirectToPool => { if let Some(pool_id) = &policy.redirect_pool_id { RoutingResult::Pool(*pool_id) } else { tracing::warn!( policy_id = %policy.id, "RedirectToPool action but no pool_id configured" ); RoutingResult::Default } } L7PolicyAction::RedirectToUrl => { if let Some(url) = &policy.redirect_url { let status = policy.redirect_http_status_code.unwrap_or(302) as u32; RoutingResult::Redirect { url: url.clone(), status, } } else { tracing::warn!( policy_id = %policy.id, "RedirectToUrl action but no URL configured" ); RoutingResult::Default } } L7PolicyAction::Reject => { let status = policy.redirect_http_status_code.unwrap_or(403) as u32; RoutingResult::Reject { status } } } } } /// SNI hostname extension (for TLS connections) #[derive(Debug, Clone)] pub struct SniHostname(pub String);