Skip to main content

zentinel_proxy/proxy/
mod.rs

1//! Zentinel Proxy Core Implementation
2//!
3//! This module contains the main ZentinelProxy struct and its implementation,
4//! split across several submodules for maintainability:
5//!
6//! - `context`: Request context maintained throughout the request lifecycle
7//! - `handlers`: Helper methods for handling different route types
8//! - `http_trait`: ProxyHttp trait implementation for Pingora
9
10mod context;
11mod fallback;
12mod fallback_metrics;
13pub(crate) mod filters;
14mod handlers;
15mod http_trait;
16mod listener_addr;
17mod model_routing;
18mod model_routing_metrics;
19
20pub use context::{FallbackReason, RequestContext};
21pub use fallback::{FallbackDecision, FallbackEvaluator};
22pub use fallback_metrics::{get_fallback_metrics, init_fallback_metrics, FallbackMetrics};
23pub use model_routing::{extract_model_from_headers, find_upstream_for_model, ModelRoutingResult};
24pub use model_routing_metrics::{
25    get_model_routing_metrics, init_model_routing_metrics, ModelRoutingMetrics,
26};
27
28use anyhow::{Context, Result};
29use parking_lot::RwLock;
30use pingora::http::ResponseHeader;
31use pingora::prelude::*;
32use std::collections::HashMap;
33use std::sync::Arc;
34use std::time::Duration;
35use tokio::sync::broadcast;
36use tracing::{debug, error, info, warn};
37use uuid::Uuid;
38
39use zentinel_common::ids::{QualifiedId, Scope};
40use zentinel_common::{Registry, ScopedMetrics, ScopedRegistry};
41
42use crate::agents::AgentManager;
43use crate::app::AppState;
44use crate::builtin_handlers::BuiltinHandlerState;
45use crate::cache::{CacheConfig, CacheManager};
46use crate::errors::ErrorHandler;
47use crate::geo_filter::{GeoDatabaseWatcher, GeoFilterManager};
48use crate::health::PassiveHealthChecker;
49use crate::http_helpers;
50use crate::inference::InferenceRateLimitManager;
51use crate::logging::{LogManager, SharedLogManager};
52use crate::rate_limit::{RateLimitConfig, RateLimitManager};
53use crate::reload::{
54    ConfigManager, GracefulReloadCoordinator, ReloadEvent, RouteValidator, UpstreamValidator,
55};
56use crate::routing::RouteMatcher;
57
58use crate::scoped_routing::ScopedRouteMatcher;
59use crate::static_files::StaticFileServer;
60use crate::upstream::{ActiveHealthChecker, HealthCheckRunner, UpstreamPool};
61use crate::validation::SchemaValidator;
62use listener_addr::ListenerMatchers;
63
64use zentinel_common::TraceIdFormat;
65use zentinel_config::{Config, FlattenedConfig};
66
67/// Main proxy service implementing Pingora's ProxyHttp trait
68pub struct ZentinelProxy {
69    /// Configuration manager with hot reload
70    pub config_manager: Arc<ConfigManager>,
71    /// Route matcher (global routes only, for backward compatibility)
72    pub(super) route_matcher: Arc<RwLock<RouteMatcher>>,
73    /// Per-listener route matchers for listeners bound to a namespace route set,
74    /// resolved from the accepted connection's local address. A request arriving
75    /// on one of these listeners is matched only against its namespace's routes
76    /// (isolated from the global set). Listeners absent from this map use
77    /// [`Self::route_matcher`].
78    pub(super) listener_matchers: Arc<RwLock<ListenerMatchers>>,
79    /// Scoped route matcher (namespace/service aware)
80    pub(super) scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
81    /// Upstream pools (keyed by upstream ID, global only)
82    pub(super) upstream_pools: Registry<UpstreamPool>,
83    /// Scoped upstream pools (namespace/service aware)
84    pub(super) scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
85    /// Agent manager for external processing
86    pub(super) agent_manager: Arc<AgentManager>,
87    /// Passive health checker
88    pub(super) passive_health: Arc<PassiveHealthChecker>,
89    /// Metrics collector
90    pub(super) metrics: Arc<zentinel_common::observability::RequestMetrics>,
91    /// Scoped metrics collector (with namespace/service labels)
92    pub(super) scoped_metrics: Arc<ScopedMetrics>,
93    /// Application state
94    pub(super) app_state: Arc<AppState>,
95    /// Graceful reload coordinator
96    pub(super) reload_coordinator: Arc<GracefulReloadCoordinator>,
97    /// Error handlers per route (keyed by route ID)
98    pub(super) error_handlers: Registry<ErrorHandler>,
99    /// API schema validators per route (keyed by route ID)
100    pub(super) validators: Registry<SchemaValidator>,
101    /// Static file servers per route (keyed by route ID)
102    pub(super) static_servers: Registry<StaticFileServer>,
103    /// Builtin handler state
104    pub(super) builtin_state: Arc<BuiltinHandlerState>,
105    /// Log manager for file-based logging
106    pub(super) log_manager: SharedLogManager,
107    /// Trace ID format for request tracing
108    pub(super) trace_id_format: TraceIdFormat,
109    /// Active health check runner
110    pub(super) health_check_runner: Arc<HealthCheckRunner>,
111    /// Rate limit manager
112    pub(super) rate_limit_manager: Arc<RateLimitManager>,
113    /// HTTP cache manager
114    pub(super) cache_manager: Arc<CacheManager>,
115    /// GeoIP filter manager
116    pub(super) geo_filter_manager: Arc<GeoFilterManager>,
117    /// Inference rate limit manager (token-based rate limiting for LLM/AI routes)
118    pub(super) inference_rate_limit_manager: Arc<InferenceRateLimitManager>,
119    /// Warmth tracker for cold model detection on inference routes
120    pub(super) warmth_tracker: Arc<crate::health::WarmthTracker>,
121    /// Guardrail processor for semantic inspection (prompt injection, PII detection)
122    pub(super) guardrail_processor: Arc<crate::inference::GuardrailProcessor>,
123    /// ACME challenge manager for HTTP-01 challenge handling
124    /// Present only when ACME is configured for at least one listener
125    pub acme_challenges: Option<Arc<crate::acme::ChallengeManager>>,
126    /// ACME clients for certificate management
127    /// Present only when ACME is configured
128    pub acme_clients: Vec<Arc<crate::acme::AcmeClient>>,
129}
130
131impl ZentinelProxy {
132    /// Create new proxy instance
133    ///
134    /// If config_path is None, uses the embedded default configuration.
135    /// Note: Tracing must be initialized by the caller before calling this function.
136    pub async fn new(config_path: Option<&str>) -> Result<Self> {
137        info!("Starting Zentinel Proxy");
138
139        // Load initial configuration
140        let (config, effective_config_path) = match config_path {
141            Some(path) => {
142                let cfg = Config::from_file(path).context("Failed to load configuration file")?;
143                (cfg, path.to_string())
144            }
145            None => {
146                let cfg = Config::default_embedded()
147                    .context("Failed to load embedded default configuration")?;
148                // Use a zentinel path to indicate embedded config
149                (cfg, "_embedded_".to_string())
150            }
151        };
152
153        config
154            .validate()
155            .context("Initial configuration validation failed")?;
156
157        // Configure global cache storage (must be done before cache is accessed)
158        if let Some(ref cache_config) = config.cache {
159            info!(
160                max_size_mb = cache_config.max_size_bytes / 1024 / 1024,
161                backend = ?cache_config.backend,
162                "Configuring HTTP cache storage"
163            );
164            crate::cache::configure_cache(cache_config.clone());
165            crate::cache::init_disk_cache_state().await;
166        }
167
168        // Create configuration manager
169        let config_manager =
170            Arc::new(ConfigManager::new(&effective_config_path, config.clone()).await?);
171
172        // Add validators
173        config_manager.add_validator(Box::new(RouteValidator)).await;
174        config_manager
175            .add_validator(Box::new(UpstreamValidator))
176            .await;
177
178        // Create route matcher (global routes only)
179        let route_matcher = Arc::new(RwLock::new(RouteMatcher::with_cache_size(
180            config.routes.clone(),
181            None,
182            config.server.route_cache_size,
183        )?));
184
185        // Build per-listener route matchers for listeners bound to a namespace
186        // route set (empty unless any listener references a namespace).
187        let listener_matchers = Arc::new(RwLock::new(Self::build_listener_matchers(&config)));
188
189        // Flatten config for namespace/service resources
190        let flattened = config.flatten();
191
192        // Create scoped route matcher
193        let scoped_route_matcher = Arc::new(tokio::sync::RwLock::new(
194            ScopedRouteMatcher::from_flattened(&flattened)
195                .await
196                .context("Failed to create scoped route matcher")?,
197        ));
198
199        // Create upstream pools and active health checkers (global only)
200        let mut pools = HashMap::new();
201        let mut health_check_runner = HealthCheckRunner::new();
202
203        for (upstream_id, upstream_config) in &config.upstreams {
204            let mut config_with_id = upstream_config.clone();
205            config_with_id.id = upstream_id.clone();
206            let pool = Arc::new(UpstreamPool::new(config_with_id.clone()).await?);
207            pools.insert(upstream_id.clone(), pool);
208
209            // Create active health checker if health check is configured
210            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
211                health_check_runner.add_checker(checker);
212            }
213        }
214        let upstream_pools = Registry::from_map(pools);
215
216        // Create scoped upstream pools from flattened config
217        let scoped_upstream_pools =
218            Self::create_scoped_upstream_pools(&flattened, &mut health_check_runner).await?;
219
220        // Keep discovery-backed pools up to date. The supervisor reads both
221        // registries on each tick, so it also covers pools installed by a later
222        // config reload; it is a no-op when no upstream uses discovery.
223        crate::upstream::discovery_refresh::spawn(
224            upstream_pools.clone(),
225            scoped_upstream_pools.clone(),
226        );
227
228        let health_check_runner = Arc::new(health_check_runner);
229
230        // Create passive health checker
231        let passive_health = Arc::new(PassiveHealthChecker::new(
232            0.5,  // 50% failure rate threshold
233            100,  // Window size
234            None, // Will be linked to active health checkers
235        ));
236
237        // Create agent manager (per-agent queue isolation)
238        let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
239        agent_manager.initialize().await?;
240
241        // Create metrics collectors
242        let metrics = Arc::new(zentinel_common::observability::RequestMetrics::new()?);
243        let scoped_metrics =
244            Arc::new(ScopedMetrics::new().context("Failed to create scoped metrics collector")?);
245
246        // Create application state
247        let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
248
249        // Create reload coordinator
250        let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
251            Duration::from_secs(30), // Max drain time
252        ));
253
254        // Setup configuration reload subscription
255        Self::setup_reload_handler(
256            config_manager.clone(),
257            route_matcher.clone(),
258            listener_matchers.clone(),
259            upstream_pools.clone(),
260            scoped_route_matcher.clone(),
261            scoped_upstream_pools.clone(),
262        )
263        .await;
264
265        // Initialize service type components
266        let (error_handlers, validators, static_servers) =
267            Self::initialize_route_components(&config).await?;
268
269        // Create builtin handler state
270        let builtin_state = Arc::new(BuiltinHandlerState::new(
271            env!("CARGO_PKG_VERSION").to_string(),
272            app_state.instance_id.clone(),
273        ));
274
275        // Create log manager for file-based logging
276        let log_manager = match LogManager::new(&config.observability.logging) {
277            Ok(manager) => {
278                if manager.access_log_enabled() {
279                    info!("Access logging enabled");
280                }
281                if manager.error_log_enabled() {
282                    info!("Error logging enabled");
283                }
284                if manager.audit_log_enabled() {
285                    info!("Audit logging enabled");
286                }
287                Arc::new(manager)
288            }
289            Err(e) => {
290                warn!(
291                    "Failed to initialize log manager, file logging disabled: {}",
292                    e
293                );
294                Arc::new(LogManager::disabled())
295            }
296        };
297
298        // Register audit reload hook to log configuration changes
299        {
300            use crate::reload::AuditReloadHook;
301            let audit_hook = AuditReloadHook::new(log_manager.clone());
302            config_manager.add_hook(Box::new(audit_hook)).await;
303            debug!("Registered audit reload hook");
304        }
305
306        // Start active health check runner in background
307        if health_check_runner.checker_count() > 0 {
308            let runner = health_check_runner.clone();
309            tokio::spawn(async move {
310                runner.run().await;
311            });
312            info!(
313                "Started active health checking for {} upstreams",
314                health_check_runner.checker_count()
315            );
316        }
317
318        // Initialize rate limit manager
319        let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
320
321        // Initialize inference rate limit manager (for token-based LLM rate limiting)
322        let inference_rate_limit_manager =
323            Arc::new(Self::initialize_inference_rate_limiters(&config));
324
325        // Initialize warmth tracker for cold model detection
326        let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
327
328        // Initialize guardrail processor for semantic inspection
329        let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
330            agent_manager.clone(),
331        ));
332
333        // Initialize geo filter manager
334        let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
335
336        // Start periodic cleanup task for rate limiters and geo caches
337        Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
338
339        // Start geo database file watcher for hot reload
340        Self::spawn_geo_database_watcher(geo_filter_manager.clone());
341
342        // Mark as ready
343        app_state.set_ready(true);
344
345        // Get trace ID format from config
346        let trace_id_format = config.server.trace_id_format;
347
348        // Initialize cache manager
349        let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
350
351        // Initialize fallback metrics (best-effort, log warning if fails)
352        if let Err(e) = init_fallback_metrics() {
353            warn!("Failed to initialize fallback metrics: {}", e);
354        }
355
356        // Initialize model routing metrics (best-effort, log warning if fails)
357        if let Err(e) = init_model_routing_metrics() {
358            warn!("Failed to initialize model routing metrics: {}", e);
359        }
360
361        // Initialize TLS metrics (best-effort, log warning if fails)
362        if let Err(e) = crate::tls_metrics::init_tls_metrics() {
363            warn!("Failed to initialize TLS metrics: {}", e);
364        }
365
366        Ok(Self {
367            config_manager,
368            route_matcher,
369            listener_matchers,
370            scoped_route_matcher,
371            upstream_pools,
372            scoped_upstream_pools,
373            agent_manager,
374            passive_health,
375            metrics,
376            scoped_metrics,
377            app_state,
378            reload_coordinator,
379            error_handlers,
380            validators,
381            static_servers,
382            builtin_state,
383            log_manager,
384            trace_id_format,
385            health_check_runner,
386            rate_limit_manager,
387            cache_manager,
388            geo_filter_manager,
389            inference_rate_limit_manager,
390            warmth_tracker,
391            guardrail_processor,
392            // ACME challenge manager - initialized later if ACME is configured
393            acme_challenges: None,
394            acme_clients: Vec::new(),
395        })
396    }
397
398    /// Shared HTTP cache statistics.
399    ///
400    /// Exposed so the standalone metrics server can include cache counters in
401    /// its Prometheus output, matching the builtin `/metrics` route handler.
402    pub fn http_cache_stats(&self) -> Arc<crate::cache::HttpCacheStats> {
403        self.cache_manager.stats()
404    }
405
406    /// Build per-listener route matchers for listeners that reference a
407    /// namespace route set.
408    ///
409    /// Listeners without a `namespace` reference are omitted (they fall back to
410    /// the global matcher at request time). Unknown namespace references are
411    /// skipped with a warning — config validation rejects them before startup,
412    /// so this only guards against a reload racing a bad config.
413    fn build_listener_matchers(config: &zentinel_config::Config) -> ListenerMatchers {
414        let mut matchers = ListenerMatchers::default();
415        for listener in &config.listeners {
416            let Some(ns_id) = listener.namespace.as_ref() else {
417                continue;
418            };
419            let Some(ns) = config.namespaces.iter().find(|n| &n.id == ns_id) else {
420                warn!(
421                    listener_id = %listener.id,
422                    namespace = %ns_id,
423                    "Listener references unknown namespace; no routes will match on this listener"
424                );
425                continue;
426            };
427            match RouteMatcher::with_cache_size(
428                ns.routes.clone(),
429                None,
430                config.server.route_cache_size,
431            ) {
432                Ok(matcher) => {
433                    info!(
434                        listener_id = %listener.id,
435                        address = %listener.address,
436                        namespace = %ns_id,
437                        routes = ns.routes.len(),
438                        "Listener bound to namespace route set"
439                    );
440                    if !matchers.insert(&listener.address, Arc::new(matcher)) {
441                        error!(
442                            listener_id = %listener.id,
443                            address = %listener.address,
444                            "Listener address is not a socket address; namespace routes                              will not be served on it"
445                        );
446                    }
447                }
448                Err(e) => {
449                    error!(
450                        listener_id = %listener.id,
451                        namespace = %ns_id,
452                        error = %e,
453                        "Failed to compile route matcher for listener namespace"
454                    );
455                }
456            }
457        }
458        matchers
459    }
460
461    /// Setup the configuration reload handler
462    async fn setup_reload_handler(
463        config_manager: Arc<ConfigManager>,
464        route_matcher: Arc<RwLock<RouteMatcher>>,
465        listener_matchers: Arc<RwLock<ListenerMatchers>>,
466        upstream_pools: Registry<UpstreamPool>,
467        scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
468        scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
469    ) {
470        let mut reload_rx = config_manager.subscribe();
471        let config_manager_clone = config_manager.clone();
472
473        tokio::spawn(async move {
474            loop {
475                match reload_rx.recv().await {
476                    Ok(ReloadEvent::Applied { .. }) => {}
477                    Ok(_) => continue,
478                    Err(broadcast::error::RecvError::Lagged(n)) => {
479                        warn!("Reload handler lagged by {n} events, applying latest config");
480                        // Fall through to reload with the latest config
481                    }
482                    Err(broadcast::error::RecvError::Closed) => break,
483                };
484                {
485                    // Reload routes and upstreams
486                    let new_config = config_manager_clone.current();
487                    let flattened = new_config.flatten();
488
489                    // Update route matcher FIRST (most critical for traffic)
490                    match RouteMatcher::new(new_config.routes.clone(), None) {
491                        Ok(new_matcher) => {
492                            *route_matcher.write() = new_matcher;
493                            info!(
494                                routes = new_config.routes.len(),
495                                "Global routes reloaded successfully"
496                            );
497                        }
498                        Err(e) => {
499                            error!(error = %e, "Failed to compile route matcher");
500                        }
501                    }
502
503                    // Rebuild per-listener (namespace-bound) route matchers
504                    *listener_matchers.write() = Self::build_listener_matchers(&new_config);
505
506                    // Update scoped route matcher
507                    if let Err(e) = scoped_route_matcher
508                        .write()
509                        .await
510                        .load_from_flattened(&flattened)
511                        .await
512                    {
513                        error!("Failed to reload scoped routes: {}", e);
514                    }
515
516                    // Update upstream pools with timeout to avoid blocking
517                    // the reload handler on DNS resolution / connection attempts
518                    let pool_update = async {
519                        let mut new_pools = HashMap::new();
520                        for (upstream_id, upstream_config) in &new_config.upstreams {
521                            let mut config_with_id = upstream_config.clone();
522                            config_with_id.id = upstream_id.clone();
523                            match UpstreamPool::new(config_with_id).await {
524                                Ok(pool) => {
525                                    new_pools.insert(upstream_id.clone(), Arc::new(pool));
526                                }
527                                Err(e) => {
528                                    error!("Failed to create upstream pool {}: {}", upstream_id, e);
529                                }
530                            }
531                        }
532                        new_pools
533                    };
534
535                    match tokio::time::timeout(Duration::from_secs(10), pool_update).await {
536                        Ok(new_pools) => {
537                            let old_pools = upstream_pools.replace(new_pools).await;
538
539                            // Update scoped upstream pools
540                            let new_scoped_pools = Self::build_scoped_pools_list(&flattened).await;
541                            let old_scoped_pools =
542                                scoped_upstream_pools.replace_all(new_scoped_pools).await;
543
544                            // Track drain lifecycle for old pools
545                            tokio::spawn(async move {
546                                let tracker = crate::upstream::drain::DrainTracker::default();
547                                tracker.track_pools(old_pools).await;
548                                tracker.track_pools(old_scoped_pools).await;
549                            });
550                        }
551                        Err(_) => {
552                            warn!("Upstream pool update timed out after 10s, routes still updated");
553                        }
554                    }
555                }
556            }
557        });
558    }
559
560    /// Create scoped upstream pools from flattened config
561    async fn create_scoped_upstream_pools(
562        flattened: &FlattenedConfig,
563        health_check_runner: &mut HealthCheckRunner,
564    ) -> Result<ScopedRegistry<UpstreamPool>> {
565        let registry = ScopedRegistry::new();
566
567        for (qid, upstream_config) in &flattened.upstreams {
568            let mut config_with_id = upstream_config.clone();
569            config_with_id.id = qid.canonical();
570
571            let pool = Arc::new(
572                UpstreamPool::new(config_with_id.clone())
573                    .await
574                    .with_context(|| {
575                        format!("Failed to create upstream pool '{}'", qid.canonical())
576                    })?,
577            );
578
579            // Track exports
580            let is_exported = flattened
581                .exported_upstreams
582                .contains_key(&upstream_config.id);
583
584            if is_exported {
585                registry.insert_exported(qid.clone(), pool).await;
586            } else {
587                registry.insert(qid.clone(), pool).await;
588            }
589
590            // Create active health checker if configured
591            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
592                health_check_runner.add_checker(checker);
593            }
594
595            debug!(
596                upstream_id = %qid.canonical(),
597                scope = ?qid.scope,
598                exported = is_exported,
599                "Created scoped upstream pool"
600            );
601        }
602
603        info!("Created {} scoped upstream pools", registry.len().await);
604
605        Ok(registry)
606    }
607
608    /// Build list of scoped pools for atomic replacement
609    async fn build_scoped_pools_list(
610        flattened: &FlattenedConfig,
611    ) -> Vec<(QualifiedId, Arc<UpstreamPool>, bool)> {
612        let mut result = Vec::new();
613
614        for (qid, upstream_config) in &flattened.upstreams {
615            let mut config_with_id = upstream_config.clone();
616            config_with_id.id = qid.canonical();
617
618            match UpstreamPool::new(config_with_id).await {
619                Ok(pool) => {
620                    let is_exported = flattened
621                        .exported_upstreams
622                        .contains_key(&upstream_config.id);
623                    result.push((qid.clone(), Arc::new(pool), is_exported));
624                }
625                Err(e) => {
626                    error!(
627                        "Failed to create scoped upstream pool {}: {}",
628                        qid.canonical(),
629                        e
630                    );
631                }
632            }
633        }
634
635        result
636    }
637
638    /// Initialize route-specific components (error handlers, validators, static servers)
639    async fn initialize_route_components(
640        config: &Config,
641    ) -> Result<(
642        Registry<ErrorHandler>,
643        Registry<SchemaValidator>,
644        Registry<StaticFileServer>,
645    )> {
646        let mut error_handlers_map = HashMap::new();
647        let mut validators_map = HashMap::new();
648        let mut static_servers_map = HashMap::new();
649
650        for route in &config.routes {
651            info!(
652                "Initializing components for route: {} with service type: {:?}",
653                route.id, route.service_type
654            );
655
656            // Initialize error handler for each route
657            if let Some(ref error_config) = route.error_pages {
658                let handler =
659                    ErrorHandler::new(route.service_type.clone(), Some(error_config.clone()));
660                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
661                debug!("Initialized error handler for route: {}", route.id);
662            } else {
663                // Use default error handler for the service type
664                let handler = ErrorHandler::new(route.service_type.clone(), None);
665                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
666            }
667
668            // Initialize schema validator for API routes
669            if route.service_type == zentinel_config::ServiceType::Api {
670                if let Some(ref api_schema) = route.api_schema {
671                    match SchemaValidator::new(api_schema.clone()) {
672                        Ok(validator) => {
673                            validators_map.insert(route.id.clone(), Arc::new(validator));
674                            info!("Initialized schema validator for route: {}", route.id);
675                        }
676                        Err(e) => {
677                            warn!(
678                                "Failed to initialize schema validator for route {}: {}",
679                                route.id, e
680                            );
681                        }
682                    }
683                }
684            }
685
686            // Initialize static file server for static routes
687            if route.service_type == zentinel_config::ServiceType::Static {
688                if let Some(ref static_config) = route.static_files {
689                    let server = StaticFileServer::new(static_config.clone());
690                    static_servers_map.insert(route.id.clone(), Arc::new(server));
691                    info!("Initialized static file server for route: {}", route.id);
692                } else {
693                    warn!(
694                        "Static route {} has no static_files configuration",
695                        route.id
696                    );
697                }
698            }
699        }
700
701        Ok((
702            Registry::from_map(error_handlers_map),
703            Registry::from_map(validators_map),
704            Registry::from_map(static_servers_map),
705        ))
706    }
707
708    /// Get or generate trace ID from session
709    pub(super) fn get_trace_id(&self, session: &pingora::proxy::Session) -> String {
710        http_helpers::get_or_create_trace_id(session, self.trace_id_format)
711    }
712
713    /// Initialize rate limiters from configuration
714    fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
715        use zentinel_config::RateLimitAction;
716
717        // Create manager with global rate limit if configured
718        let manager = if let Some(ref global) = config.rate_limits.global {
719            info!(
720                max_rps = global.max_rps,
721                burst = global.burst,
722                key = ?global.key,
723                "Initializing global rate limiter"
724            );
725            RateLimitManager::with_global_limit(global.max_rps, global.burst)
726        } else {
727            RateLimitManager::new()
728        };
729
730        for route in &config.routes {
731            // Check for rate limit in route policies
732            if let Some(ref rate_limit) = route.policies.rate_limit {
733                let rl_config = RateLimitConfig {
734                    max_rps: rate_limit.requests_per_second,
735                    burst: rate_limit.burst,
736                    key: rate_limit.key.clone(),
737                    action: RateLimitAction::Reject,
738                    status_code: 429,
739                    message: None,
740                    backend: zentinel_config::RateLimitBackend::Local,
741                    max_delay_ms: 5000, // Default for policy-based rate limits
742                    max_keys: crate::rate_limit::DEFAULT_MAX_RATE_LIMIT_KEYS,
743                };
744                manager.register_route(&route.id, rl_config);
745                info!(
746                    route_id = %route.id,
747                    max_rps = rate_limit.requests_per_second,
748                    burst = rate_limit.burst,
749                    key = ?rate_limit.key,
750                    "Registered rate limiter for route"
751                );
752            }
753
754            // Also check for rate limit filters in the filter chain
755            for filter_id in &route.filters {
756                if let Some(filter_config) = config.filters.get(filter_id) {
757                    if let zentinel_config::Filter::RateLimit(ref rl_filter) = filter_config.filter
758                    {
759                        let rl_config = RateLimitConfig {
760                            max_rps: rl_filter.max_rps,
761                            burst: rl_filter.burst,
762                            key: rl_filter.key.clone(),
763                            action: rl_filter.on_limit.clone(),
764                            status_code: rl_filter.status_code,
765                            message: rl_filter.limit_message.clone(),
766                            backend: rl_filter.backend.clone(),
767                            max_delay_ms: rl_filter.max_delay_ms,
768                            max_keys: rl_filter.max_keys,
769                        };
770                        manager.register_route(&route.id, rl_config);
771                        info!(
772                            route_id = %route.id,
773                            filter_id = %filter_id,
774                            max_rps = rl_filter.max_rps,
775                            backend = ?rl_filter.backend,
776                            "Registered rate limiter from filter for route"
777                        );
778                    }
779                }
780            }
781        }
782
783        if manager.route_count() > 0 {
784            info!(
785                route_count = manager.route_count(),
786                "Rate limiting initialized"
787            );
788        }
789
790        manager
791    }
792
793    /// Initialize inference rate limiters from configuration
794    ///
795    /// This creates token-based rate limiters for routes with `service-type "inference"`
796    /// and inference config blocks.
797    fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
798        let manager = InferenceRateLimitManager::new();
799
800        for route in &config.routes {
801            // Only initialize for inference service type routes with inference config
802            if route.service_type == zentinel_config::ServiceType::Inference {
803                if let Some(ref inference_config) = route.inference {
804                    manager.register_route(&route.id, inference_config);
805                }
806            }
807        }
808
809        if manager.route_count() > 0 {
810            info!(
811                route_count = manager.route_count(),
812                "Inference rate limiting initialized"
813            );
814        }
815
816        manager
817    }
818
819    /// Initialize cache manager from configuration
820    fn initialize_cache_manager(config: &Config) -> CacheManager {
821        let manager = CacheManager::new();
822
823        let mut enabled_count = 0;
824
825        for route in &config.routes {
826            // Use per-route cache config if present, otherwise fall back to service-type defaults
827            let cache_config = if let Some(ref rc) = route.policies.cache {
828                // Pre-compile exclude_paths glob patterns into regex at registration time
829                let exclude_paths = rc
830                    .exclude_paths
831                    .iter()
832                    .filter_map(|pattern| {
833                        let regex_str = crate::cache::compile_glob_to_regex(pattern);
834                        match regex::Regex::new(&regex_str) {
835                            Ok(re) => Some(re),
836                            Err(e) => {
837                                warn!(
838                                    route_id = %route.id,
839                                    pattern = %pattern,
840                                    error = %e,
841                                    "Failed to compile cache exclude-path pattern"
842                                );
843                                None
844                            }
845                        }
846                    })
847                    .collect();
848
849                CacheConfig {
850                    enabled: rc.enabled,
851                    default_ttl_secs: rc.default_ttl_secs,
852                    max_size_bytes: rc.max_size_bytes,
853                    cache_private: rc.cache_private,
854                    stale_while_revalidate_secs: rc.stale_while_revalidate_secs,
855                    stale_if_error_secs: rc.stale_if_error_secs,
856                    cacheable_methods: rc.cacheable_methods.clone(),
857                    cacheable_status_codes: rc.cacheable_status_codes.clone(),
858                    exclude_extensions: rc.exclude_extensions.clone(),
859                    exclude_paths,
860                }
861            } else {
862                match route.service_type {
863                    zentinel_config::ServiceType::Static => CacheConfig {
864                        enabled: true,
865                        default_ttl_secs: 3600,
866                        max_size_bytes: 50 * 1024 * 1024, // 50MB for static
867                        stale_while_revalidate_secs: 60,
868                        stale_if_error_secs: 300,
869                        ..Default::default()
870                    },
871                    zentinel_config::ServiceType::Api => CacheConfig {
872                        enabled: false,
873                        default_ttl_secs: 60,
874                        ..Default::default()
875                    },
876                    zentinel_config::ServiceType::Web => CacheConfig {
877                        enabled: false,
878                        default_ttl_secs: 300,
879                        ..Default::default()
880                    },
881                    _ => CacheConfig::default(),
882                }
883            };
884
885            if cache_config.enabled {
886                enabled_count += 1;
887                info!(
888                    route_id = %route.id,
889                    default_ttl_secs = cache_config.default_ttl_secs,
890                    from_config = route.policies.cache.is_some(),
891                    "HTTP caching enabled for route"
892                );
893            }
894            manager.register_route(&route.id, cache_config);
895        }
896
897        if enabled_count > 0 {
898            info!(enabled_routes = enabled_count, "HTTP caching initialized");
899        } else {
900            debug!("HTTP cache manager initialized (no routes with caching enabled)");
901        }
902
903        manager
904    }
905
906    /// Initialize geo filters from configuration
907    fn initialize_geo_filters(config: &Config) -> GeoFilterManager {
908        let manager = GeoFilterManager::new();
909
910        for (filter_id, filter_config) in &config.filters {
911            if let zentinel_config::Filter::Geo(ref geo_filter) = filter_config.filter {
912                match manager.register_filter(filter_id, geo_filter.clone()) {
913                    Ok(_) => {
914                        info!(
915                            filter_id = %filter_id,
916                            database_path = %geo_filter.database_path,
917                            action = ?geo_filter.action,
918                            countries_count = geo_filter.countries.len(),
919                            "Registered geo filter"
920                        );
921                    }
922                    Err(e) => {
923                        error!(
924                            filter_id = %filter_id,
925                            error = %e,
926                            "Failed to register geo filter"
927                        );
928                    }
929                }
930            }
931        }
932
933        let filter_ids = manager.filter_ids();
934        if !filter_ids.is_empty() {
935            info!(
936                filter_count = filter_ids.len(),
937                filter_ids = ?filter_ids,
938                "GeoIP filtering initialized"
939            );
940        }
941
942        manager
943    }
944
945    /// Spawn background task to periodically clean up idle rate limiters and expired geo caches
946    fn spawn_cleanup_task(
947        rate_limit_manager: Arc<RateLimitManager>,
948        geo_filter_manager: Arc<GeoFilterManager>,
949    ) {
950        // Cleanup interval: 5 minutes
951        const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
952
953        tokio::spawn(async move {
954            let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
955            // First tick completes immediately; skip it
956            interval.tick().await;
957
958            loop {
959                interval.tick().await;
960
961                // Clean up rate limiters (removes entries when pool exceeds max size)
962                rate_limit_manager.cleanup();
963
964                // Clean up expired geo filter caches
965                geo_filter_manager.clear_expired_caches();
966
967                debug!("Periodic cleanup completed");
968            }
969        });
970
971        info!(
972            interval_secs = CLEANUP_INTERVAL.as_secs(),
973            "Started periodic cleanup task"
974        );
975    }
976
977    /// Spawn background task to watch geo database files for changes
978    fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
979        let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
980
981        // Try to start watching
982        match watcher.start_watching() {
983            Ok(mut rx) => {
984                let watcher_clone = watcher.clone();
985                tokio::spawn(async move {
986                    // Debounce interval
987                    const DEBOUNCE_MS: u64 = 500;
988
989                    while let Some(path) = rx.recv().await {
990                        // Debounce rapid changes (e.g., temp file then rename)
991                        tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
992
993                        // Drain any additional events for the same path during debounce
994                        while rx.try_recv().is_ok() {}
995
996                        // Handle the change
997                        watcher_clone.handle_change(&path);
998                    }
999                });
1000
1001                info!("Started geo database file watcher");
1002            }
1003            Err(e) => {
1004                warn!(
1005                    error = %e,
1006                    "Failed to start geo database file watcher, auto-reload disabled"
1007                );
1008            }
1009        }
1010    }
1011}
1012
1013#[cfg(test)]
1014mod listener_matcher_tests {
1015    use super::*;
1016    use crate::routing::RequestInfo;
1017    use std::net::SocketAddr;
1018
1019    fn local(s: &str) -> SocketAddr {
1020        s.parse().expect("test address parses")
1021    }
1022
1023    const KDL: &str = r#"
1024        schema-version "1.0"
1025        system { worker-threads 0 }
1026        listeners {
1027            listener "public" { address "0.0.0.0:8080" }
1028            listener "admin" {
1029                address "127.0.0.1:9000"
1030                namespace "ops"
1031            }
1032        }
1033        routes {
1034            route "api" {
1035                matches { path-prefix "/api" }
1036                upstream "backend"
1037            }
1038        }
1039        upstreams {
1040            upstream "backend" { target "127.0.0.1:3000" }
1041        }
1042        namespace "ops" {
1043            routes {
1044                route "metrics" {
1045                    matches { path "/metrics" }
1046                    service-type "builtin"
1047                    builtin-handler "metrics"
1048                }
1049            }
1050        }
1051    "#;
1052
1053    #[test]
1054    fn namespace_listener_matches_only_its_own_routes() {
1055        let config = zentinel_config::Config::from_kdl(KDL).expect("config parses");
1056        let matchers = ZentinelProxy::build_listener_matchers(&config);
1057
1058        // Only the namespace-bound listener gets a dedicated matcher.
1059        assert_eq!(matchers.len(), 1);
1060        assert!(!matchers.contains_configured("0.0.0.0:8080"));
1061        let admin = matchers
1062            .get(local("127.0.0.1:9000"))
1063            .expect("admin listener bound to namespace");
1064
1065        // Isolated: the admin listener serves the namespace route...
1066        assert!(admin
1067            .match_request(&RequestInfo::new("GET", "/metrics", "x"))
1068            .is_some());
1069        // ...but NOT the global route.
1070        assert!(admin
1071            .match_request(&RequestInfo::new("GET", "/api/users", "x"))
1072            .is_none());
1073    }
1074
1075    #[test]
1076    fn no_namespace_listeners_yields_empty_map() {
1077        let kdl = r#"
1078            schema-version "1.0"
1079            system { worker-threads 0 }
1080            listeners {
1081                listener "public" {
1082                    address "0.0.0.0:8080"
1083                }
1084            }
1085            routes {
1086                route "api" {
1087                    matches { path-prefix "/api" }
1088                    upstream "backend"
1089                }
1090            }
1091            upstreams {
1092                upstream "backend" {
1093                    target "127.0.0.1:3000"
1094                }
1095            }
1096        "#;
1097        let config = zentinel_config::Config::from_kdl(kdl).expect("config parses");
1098        let matchers = ZentinelProxy::build_listener_matchers(&config);
1099        assert!(matchers.is_empty());
1100    }
1101
1102    /// A namespaced listener bound to `0.0.0.0` used to be unreachable: the
1103    /// matcher was stored under the configured `0.0.0.0:8080`, but a connection
1104    /// accepted on that bind reports the concrete interface from
1105    /// `getsockname()`, so the lookup always missed and the request silently
1106    /// fell back to the *global* route set.
1107    const WILDCARD_KDL: &str = r#"
1108        schema-version "1.0"
1109        system { worker-threads 0 }
1110        listeners {
1111            listener "public" {
1112                address "0.0.0.0:8080"
1113                namespace "iso"
1114            }
1115        }
1116        routes {
1117            route "global-secret" {
1118                matches { path "/secret" }
1119                service-type "builtin"
1120                builtin-handler "config"
1121            }
1122        }
1123        namespace "iso" {
1124            routes {
1125                route "only" {
1126                    matches { path "/ok" }
1127                    service-type "builtin"
1128                    builtin-handler "health"
1129                }
1130            }
1131        }
1132    "#;
1133
1134    #[test]
1135    fn wildcard_bound_namespace_listener_resolves_from_concrete_local_addr() {
1136        let config = zentinel_config::Config::from_kdl(WILDCARD_KDL).expect("config parses");
1137        let matchers = ZentinelProxy::build_listener_matchers(&config);
1138
1139        // Every interface a `0.0.0.0` bind can accept on must resolve.
1140        for arrival in ["127.0.0.1:8080", "203.0.113.5:8080", "10.0.0.7:8080"] {
1141            let matcher = matchers
1142                .get(local(arrival))
1143                .unwrap_or_else(|| panic!("no matcher for connection arriving on {arrival}"));
1144
1145            // The namespace route is live...
1146            assert!(
1147                matcher
1148                    .match_request(&RequestInfo::new("GET", "/ok", "x"))
1149                    .is_some(),
1150                "namespace route should match on {arrival}"
1151            );
1152            // ...and the global route stays out of reach, which is the
1153            // isolation guarantee `namespace` is documented to provide.
1154            assert!(
1155                matcher
1156                    .match_request(&RequestInfo::new("GET", "/secret", "x"))
1157                    .is_none(),
1158                "global route must not leak into namespace on {arrival}"
1159            );
1160        }
1161    }
1162
1163    #[test]
1164    fn wildcard_listener_does_not_capture_other_ports() {
1165        let config = zentinel_config::Config::from_kdl(WILDCARD_KDL).expect("config parses");
1166        let matchers = ZentinelProxy::build_listener_matchers(&config);
1167        assert!(matchers.get(local("203.0.113.5:9090")).is_none());
1168    }
1169
1170    /// Per-listener timeouts resolve through the same path, and used to be
1171    /// inert on wildcard binds for the same reason.
1172    #[test]
1173    fn per_listener_timeouts_resolve_on_wildcard_bind() {
1174        let kdl = r#"
1175            schema-version "1.0"
1176            system { worker-threads 0 }
1177            listeners {
1178                listener "public" {
1179                    address "0.0.0.0:8080"
1180                    request-timeout-secs 17
1181                    keepalive-timeout-secs 23
1182                }
1183            }
1184            routes {
1185                route "api" {
1186                    matches { path-prefix "/api" }
1187                    upstream "backend"
1188                }
1189            }
1190            upstreams {
1191                upstream "backend" { target "127.0.0.1:3000" }
1192            }
1193        "#;
1194        let config = zentinel_config::Config::from_kdl(kdl).expect("config parses");
1195        let listener =
1196            super::listener_addr::listener_for_addr(&config.listeners, local("203.0.113.5:8080"))
1197                .expect("wildcard listener resolves from a concrete local address");
1198
1199        assert_eq!(listener.id, "public");
1200        assert_eq!(listener.request_timeout_secs, 17);
1201        assert_eq!(listener.keepalive_timeout_secs, 23);
1202    }
1203}