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 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 let passive_health = Arc::new(PassiveHealthChecker::new(
232 0.5, 100, None, ));
236
237 let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
239 agent_manager.initialize().await?;
240
241 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 let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
248
249 let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
251 Duration::from_secs(30), ));
253
254 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 let (error_handlers, validators, static_servers) =
267 Self::initialize_route_components(&config).await?;
268
269 let builtin_state = Arc::new(BuiltinHandlerState::new(
271 env!("CARGO_PKG_VERSION").to_string(),
272 app_state.instance_id.clone(),
273 ));
274
275 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 {
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 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 let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
320
321 let inference_rate_limit_manager =
323 Arc::new(Self::initialize_inference_rate_limiters(&config));
324
325 let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
327
328 let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
330 agent_manager.clone(),
331 ));
332
333 let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
335
336 Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
338
339 Self::spawn_geo_database_watcher(geo_filter_manager.clone());
341
342 app_state.set_ready(true);
344
345 let trace_id_format = config.server.trace_id_format;
347
348 let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
350
351 if let Err(e) = init_fallback_metrics() {
353 warn!("Failed to initialize fallback metrics: {}", e);
354 }
355
356 if let Err(e) = init_model_routing_metrics() {
358 warn!("Failed to initialize model routing metrics: {}", e);
359 }
360
361 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_challenges: None,
394 acme_clients: Vec::new(),
395 })
396 }
397
398 pub fn http_cache_stats(&self) -> Arc<crate::cache::HttpCacheStats> {
403 self.cache_manager.stats()
404 }
405
406 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 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 }
482 Err(broadcast::error::RecvError::Closed) => break,
483 };
484 {
485 let new_config = config_manager_clone.current();
487 let flattened = new_config.flatten();
488
489 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 *listener_matchers.write() = Self::build_listener_matchers(&new_config);
505
506 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 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 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 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 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 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 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 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 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 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 let handler = ErrorHandler::new(route.service_type.clone(), None);
665 error_handlers_map.insert(route.id.clone(), Arc::new(handler));
666 }
667
668 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 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 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 fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
715 use zentinel_config::RateLimitAction;
716
717 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 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, 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 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 fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
798 let manager = InferenceRateLimitManager::new();
799
800 for route in &config.routes {
801 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 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 let cache_config = if let Some(ref rc) = route.policies.cache {
828 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(®ex_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, 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 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 fn spawn_cleanup_task(
947 rate_limit_manager: Arc<RateLimitManager>,
948 geo_filter_manager: Arc<GeoFilterManager>,
949 ) {
950 const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
952
953 tokio::spawn(async move {
954 let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
955 interval.tick().await;
957
958 loop {
959 interval.tick().await;
960
961 rate_limit_manager.cleanup();
963
964 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 fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
979 let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
980
981 match watcher.start_watching() {
983 Ok(mut rx) => {
984 let watcher_clone = watcher.clone();
985 tokio::spawn(async move {
986 const DEBOUNCE_MS: u64 = 500;
988
989 while let Some(path) = rx.recv().await {
990 tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
992
993 while rx.try_recv().is_ok() {}
995
996 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 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 assert!(admin
1067 .match_request(&RequestInfo::new("GET", "/metrics", "x"))
1068 .is_some());
1069 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 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 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 assert!(
1147 matcher
1148 .match_request(&RequestInfo::new("GET", "/ok", "x"))
1149 .is_some(),
1150 "namespace route should match on {arrival}"
1151 );
1152 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 #[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}