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