1mod 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
67pub struct ZentinelProxy {
69 pub config_manager: Arc<ConfigManager>,
71 pub(super) route_matcher: Arc<RwLock<RouteMatcher>>,
73 pub(super) listener_matchers: Arc<RwLock<ListenerMatchers>>,
79 pub(super) scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
81 pub(super) upstream_pools: Registry<UpstreamPool>,
83 pub(super) scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
85 pub(super) agent_manager: Arc<AgentManager>,
87 pub(super) passive_health: Arc<PassiveHealthChecker>,
89 pub(super) metrics: Arc<zentinel_common::observability::RequestMetrics>,
91 pub(super) scoped_metrics: Arc<ScopedMetrics>,
93 pub(super) app_state: Arc<AppState>,
95 pub(super) reload_coordinator: Arc<GracefulReloadCoordinator>,
97 pub(super) error_handlers: Registry<ErrorHandler>,
99 pub(super) validators: Registry<SchemaValidator>,
101 pub(super) static_servers: Registry<StaticFileServer>,
103 pub(super) builtin_state: Arc<BuiltinHandlerState>,
105 pub(super) log_manager: SharedLogManager,
107 pub(super) trace_id_format: TraceIdFormat,
109 pub(super) health_check_runner: Arc<HealthCheckRunner>,
111 pub(super) rate_limit_manager: Arc<RateLimitManager>,
113 pub(super) cache_manager: Arc<CacheManager>,
115 pub(super) geo_filter_manager: Arc<GeoFilterManager>,
117 pub(super) inference_rate_limit_manager: Arc<InferenceRateLimitManager>,
119 pub(super) warmth_tracker: Arc<crate::health::WarmthTracker>,
121 pub(super) guardrail_processor: Arc<crate::inference::GuardrailProcessor>,
123 pub acme_challenges: Option<Arc<crate::acme::ChallengeManager>>,
126 pub acme_clients: Vec<Arc<crate::acme::AcmeClient>>,
129}
130
131impl ZentinelProxy {
132 pub async fn new(config_path: Option<&str>) -> Result<Self> {
137 info!("Starting Zentinel Proxy");
138
139 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 (cfg, "_embedded_".to_string())
150 }
151 };
152
153 config
154 .validate()
155 .context("Initial configuration validation failed")?;
156
157 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 let config_manager =
170 Arc::new(ConfigManager::new(&effective_config_path, config.clone()).await?);
171
172 config_manager.add_validator(Box::new(RouteValidator)).await;
174 config_manager
175 .add_validator(Box::new(UpstreamValidator))
176 .await;
177
178 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 let listener_matchers = Arc::new(RwLock::new(Self::build_listener_matchers(&config)));
188
189 let flattened = config.flatten();
191
192 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 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 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 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 let passive_health = Arc::new(PassiveHealthChecker::new(
224 0.5, 100, None, ));
228
229 let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
231 agent_manager.initialize().await?;
232
233 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 let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
240
241 let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
243 Duration::from_secs(30), ));
245
246 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 let (error_handlers, validators, static_servers) =
259 Self::initialize_route_components(&config).await?;
260
261 let builtin_state = Arc::new(BuiltinHandlerState::new(
263 env!("CARGO_PKG_VERSION").to_string(),
264 app_state.instance_id.clone(),
265 ));
266
267 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 {
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 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 let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
312
313 let inference_rate_limit_manager =
315 Arc::new(Self::initialize_inference_rate_limiters(&config));
316
317 let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
319
320 let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
322 agent_manager.clone(),
323 ));
324
325 let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
327
328 Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
330
331 Self::spawn_geo_database_watcher(geo_filter_manager.clone());
333
334 app_state.set_ready(true);
336
337 let trace_id_format = config.server.trace_id_format;
339
340 let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
342
343 if let Err(e) = init_fallback_metrics() {
345 warn!("Failed to initialize fallback metrics: {}", e);
346 }
347
348 if let Err(e) = init_model_routing_metrics() {
350 warn!("Failed to initialize model routing metrics: {}", e);
351 }
352
353 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_challenges: None,
386 acme_clients: Vec::new(),
387 })
388 }
389
390 pub fn http_cache_stats(&self) -> Arc<crate::cache::HttpCacheStats> {
395 self.cache_manager.stats()
396 }
397
398 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 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 }
474 Err(broadcast::error::RecvError::Closed) => break,
475 };
476 {
477 let new_config = config_manager_clone.current();
479 let flattened = new_config.flatten();
480
481 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 *listener_matchers.write() = Self::build_listener_matchers(&new_config);
497
498 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 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 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 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 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 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 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 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 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 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 let handler = ErrorHandler::new(route.service_type.clone(), None);
657 error_handlers_map.insert(route.id.clone(), Arc::new(handler));
658 }
659
660 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 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 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 fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
707 use zentinel_config::RateLimitAction;
708
709 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 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, 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 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 fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
790 let manager = InferenceRateLimitManager::new();
791
792 for route in &config.routes {
793 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 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 let cache_config = if let Some(ref rc) = route.policies.cache {
820 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(®ex_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, 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 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 fn spawn_cleanup_task(
939 rate_limit_manager: Arc<RateLimitManager>,
940 geo_filter_manager: Arc<GeoFilterManager>,
941 ) {
942 const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
944
945 tokio::spawn(async move {
946 let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
947 interval.tick().await;
949
950 loop {
951 interval.tick().await;
952
953 rate_limit_manager.cleanup();
955
956 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 fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
971 let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
972
973 match watcher.start_watching() {
975 Ok(mut rx) => {
976 let watcher_clone = watcher.clone();
977 tokio::spawn(async move {
978 const DEBOUNCE_MS: u64 = 500;
980
981 while let Some(path) = rx.recv().await {
982 tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
984
985 while rx.try_recv().is_ok() {}
987
988 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 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 assert!(admin
1059 .match_request(&RequestInfo::new("GET", "/metrics", "x"))
1060 .is_some());
1061 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 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 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 assert!(
1139 matcher
1140 .match_request(&RequestInfo::new("GET", "/ok", "x"))
1141 .is_some(),
1142 "namespace route should match on {arrival}"
1143 );
1144 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 #[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}