1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5#[cfg(feature = "operations")]
6use eggress_admin::AdminSnapshotProvider;
7use eggress_core::listener::{is_listener_cancelled, TcpListener, TcpListenerConfig};
8use eggress_core::ProtocolId;
9use eggress_routing::health::HealthManager;
10use eggress_routing::upstream::UpstreamRuntime;
11use eggress_routing::RouteService;
12use tokio_util::sync::CancellationToken;
13use tokio_util::task::TaskTracker;
14use tracing::Instrument;
15
16use crate::error::RuntimeError;
17use crate::platform::{check_capability, PlatformCapability};
18
19pub(crate) mod accounting;
20pub(crate) mod connection;
21#[cfg(feature = "operations")]
22pub(crate) mod operations;
23pub(crate) mod reload;
24pub(crate) mod shutdown;
25pub(crate) mod startup;
26pub(crate) mod state;
27pub(crate) mod udp_runtime;
28
29pub(crate) use accounting::{handle_accept_error, ActiveConnectionGuard, ListenerConnectionSlot};
30pub(crate) use connection::PreparedListener;
31#[cfg(feature = "quic")]
32pub(crate) use connection::PreparedQuicListener;
33pub(crate) use connection::{
34 build_connection_config, wrap_tls_server, ConnectionBuildParams, InboundSecurity,
35};
36#[cfg(feature = "operations")]
37pub(crate) use operations::RuntimeAdminListenerInfos;
38pub use reload::{classify_reload_config, ReloadResult};
39pub(crate) use shutdown::{shutdown_ordered, ShutdownPlan};
40pub use state::RuntimeState;
41#[allow(unused_imports)]
42pub(crate) use udp_runtime::compute_advertise_ip;
43pub(crate) use udp_runtime::make_udp_service;
44#[cfg(feature = "extended")]
45pub(crate) use udp_runtime::prepare_shadowsocks_udp_relay;
46
47#[allow(dead_code)]
48pub struct ServiceSupervisor {
49 pub(crate) config_path: Option<String>,
50 pub(crate) state: Arc<RuntimeState>,
51 pub(crate) metrics_registry: Arc<eggress_metrics::MetricsRegistry>,
52 pub(crate) cancel: CancellationToken,
53 pub(crate) listener_cancel: CancellationToken,
54 pub(crate) connection_cancel: CancellationToken,
55 pub(crate) health_cancel: CancellationToken,
56 pub(crate) admin_cancel: CancellationToken,
57 pub(crate) health: Arc<Mutex<Option<HealthManager>>>,
58 pub(crate) tasks: TaskTracker,
59 pub(crate) connection_tasks: TaskTracker,
60 pub(crate) admin_tasks: TaskTracker,
61 pub(crate) shutdown_grace: Duration,
62 pub(crate) rt_config: eggress_config::compile::RuntimeConfig,
63 pub(crate) tls_client_config: Option<std::sync::Arc<rustls::ClientConfig>>,
64 #[cfg(feature = "ssh")]
65 pub(crate) ssh_sessions: Arc<eggress_transport_ssh::SshSessionCache>,
66 pub(crate) compatibility_hooks: Option<CompatibilityRuntimeHooks>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub struct SystemProxyRequest;
78
79#[derive(Clone, Default)]
110pub struct CompatibilityRuntimeHooks {
111 pub auth_reuse: Option<Arc<eggress_server::accept::AuthReuseCache>>,
112 pub system_proxy: Option<SystemProxyRequest>,
113 pub allow_insecure_ssh_host_keys: bool,
114}
115
116impl std::fmt::Debug for CompatibilityRuntimeHooks {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("CompatibilityRuntimeHooks")
119 .field("auth_reuse", &self.auth_reuse.is_some())
120 .field("system_proxy", &self.system_proxy)
121 .field(
122 "allow_insecure_ssh_host_keys",
123 &self.allow_insecure_ssh_host_keys,
124 )
125 .finish()
126 }
127}
128
129impl CompatibilityRuntimeHooks {
130 pub fn from_facade(
140 auth_timeout: Duration,
141 system_proxy: bool,
142 allow_insecure_ssh_host_keys: bool,
143 ) -> Self {
144 Self {
145 auth_reuse: Some(Arc::new(eggress_server::accept::AuthReuseCache::new(
146 auth_timeout,
147 ))),
148 system_proxy: system_proxy.then_some(SystemProxyRequest),
149 allow_insecure_ssh_host_keys,
150 }
151 }
152
153 pub fn from_legacy_options(options: &CompatibilityOptions) -> Self {
176 Self {
177 auth_reuse: options
178 .auth_timeout
179 .map(eggress_server::accept::AuthReuseCache::new)
180 .map(Arc::new),
181 system_proxy: options.system_proxy.then_some(SystemProxyRequest),
182 allow_insecure_ssh_host_keys: options.compatibility_mode && ssh_insecure_acknowledged(),
183 }
184 }
185
186 pub fn is_empty(&self) -> bool {
191 self.auth_reuse.is_none()
192 && self.system_proxy.is_none()
193 && !self.allow_insecure_ssh_host_keys
194 }
195}
196
197#[derive(Debug, Clone, Default)]
214pub struct CompatibilityOptions {
215 pub compatibility_mode: bool,
219 pub auth_timeout: Option<Duration>,
223 pub system_proxy: bool,
226 pub debug: bool,
229 pub verbose_level: u8,
232}
233
234pub fn ssh_insecure_acknowledged() -> bool {
241 std::env::var("EGRESS_SSH_INSECURE_HOST_KEYS")
242 .map(|value| matches!(value.as_str(), "1" | "true" | "yes"))
243 .unwrap_or(false)
244}
245
246impl ServiceSupervisor {
247 pub fn start(config_path: &str) -> Result<Self, RuntimeError> {
248 let (rt_config, warnings) = eggress_config::load_and_validate_with_warnings(config_path)
249 .map_err(|e| RuntimeError::Config(e.to_string()))?;
250
251 for warning in &warnings {
252 tracing::warn!("config security warning: {warning}");
253 }
254
255 startup::init_supervisor(rt_config, Some(config_path.to_string()), None)
256 }
257
258 pub fn start_from_config(
267 rt_config: eggress_config::compile::RuntimeConfig,
268 config_path: Option<String>,
269 ) -> Result<Self, RuntimeError> {
270 startup::init_supervisor(rt_config, config_path, None)
271 }
272
273 pub fn start_from_config_with_compatibility(
280 rt_config: eggress_config::compile::RuntimeConfig,
281 config_path: Option<String>,
282 hooks: CompatibilityRuntimeHooks,
283 ) -> Result<Self, RuntimeError> {
284 startup::init_supervisor(rt_config, config_path, Some(hooks))
285 }
286
287 #[deprecated(note = "use start_from_config_with_compatibility with CompatibilityRuntimeHooks")]
311 pub fn start_from_config_with_options(
312 rt_config: eggress_config::compile::RuntimeConfig,
313 config_path: Option<String>,
314 compatibility_options: CompatibilityOptions,
315 ) -> Result<Self, RuntimeError> {
316 if compatibility_options.debug || compatibility_options.verbose_level != 0 {
317 tracing::warn!(
318 "legacy CompatibilityOptions debug/verbose_level are presentation-only \
319 and do not configure runtime tracing; resolve logging at the facade \
320 via PproxyArgs::default_log_level() before startup"
321 );
322 }
323 #[cfg(feature = "ssh")]
324 if compatibility_options.compatibility_mode && !ssh_insecure_acknowledged() {
325 tracing::warn!(
326 "compatibility mode would disable SSH host-key verification; \
327 keeping known_hosts verification enabled. To explicitly \
328 accept unverified SSH host keys (MITM risk), set \
329 EGRESS_SSH_INSECURE_HOST_KEYS=1"
330 );
331 }
332 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&compatibility_options);
333 if hooks.is_empty() {
334 Self::start_from_config(rt_config, config_path)
335 } else {
336 Self::start_from_config_with_compatibility(rt_config, config_path, hooks)
337 }
338 }
339
340 pub fn state(&self) -> &Arc<RuntimeState> {
341 &self.state
342 }
343
344 pub fn shutdown_token(&self) -> CancellationToken {
345 self.cancel.clone()
346 }
347
348 #[allow(dead_code)]
351 pub fn with_tls_client_config(mut self, config: std::sync::Arc<rustls::ClientConfig>) -> Self {
352 self.tls_client_config = Some(config);
353 self
354 }
355
356 pub fn reload_config(&mut self) -> ReloadResult {
363 let config_path = match self.config_path {
364 Some(ref p) => p.clone(),
365 None => {
366 return ReloadResult::Rejected {
367 reason: "no config file path available for reload".to_string(),
368 };
369 }
370 };
371 let new_rt_config = match eggress_config::compile::load_and_compile(&config_path) {
372 Ok(c) => c,
373 Err(e) => {
374 self.state.runtime_metrics.record_reload(false);
375 return ReloadResult::Failed {
376 error: format!("config load: {e}"),
377 };
378 }
379 };
380
381 let result = self.state.apply_compiled_config(&new_rt_config);
382 if matches!(result, ReloadResult::Applied { .. }) {
383 self.rt_config = new_rt_config;
387 }
388 result
389 }
390
391 pub fn run(&mut self) -> Result<(), RuntimeError> {
392 #[allow(unused_variables)]
393 let config_path = self.config_path.clone().unwrap_or_default();
394 let routing = self.state.routing.clone();
395 let listener_cancel = self.listener_cancel.clone();
396 let connection_cancel = self.connection_cancel.clone();
397 let health_cancel = self.health_cancel.clone();
398 let admin_cancel = self.admin_cancel.clone();
399 let cancel = self.cancel.clone();
400 #[allow(unused_variables)]
401 let metrics = self.state.metrics.clone();
402 let runtime_metrics = self.state.runtime_metrics.clone();
403 let readiness = self.state.readiness.clone();
404 #[cfg(feature = "operations")]
405 let admin_state_ref = self.state.clone();
406 let active_connections = self.state.active_connections.clone();
407 let shutdown_grace = self.shutdown_grace;
408 let tasks = self.tasks.clone();
409 let connection_tasks = self.connection_tasks.clone();
410 let admin_tasks = self.admin_tasks.clone();
411 let health_for_run = self.health.clone();
412 let snapshot = self.state.snapshot.clone();
413 let state_ref = self.state.clone();
414 let rt_config = self.rt_config.clone();
415 let tls_client_config = self.tls_client_config.clone();
416 let compatibility_hooks = self.compatibility_hooks.clone();
417 #[cfg(feature = "ssh")]
418 let ssh_sessions = self.ssh_sessions.clone();
419
420 let handshake_timeout = rt_config.timeouts.handshake;
421 let connect_timeout = rt_config.timeouts.connect;
422
423 #[cfg(feature = "operations")]
424 let listener_infos_provider: Arc<RuntimeAdminListenerInfos> =
425 Arc::new(RuntimeAdminListenerInfos {
426 state: admin_state_ref.clone(),
427 });
428
429 #[cfg(feature = "operations")]
430 let metrics_registry_for_admin = self.metrics_registry.clone();
431
432 let run_async = async move {
433 match state_ref.health_runtime.lock() {
434 Ok(mut runtime) => {
435 *runtime = Some(tokio::runtime::Handle::current());
436 }
437 Err(error) => {
438 tracing::warn!("health runtime state was poisoned; resetting it: {error}");
439 let mut runtime = error.into_inner();
440 *runtime = Some(tokio::runtime::Handle::current());
441 state_ref.health_runtime.clear_poison();
442 }
443 }
444 #[cfg(feature = "operations")]
445 let mut compatibility_system_proxy: Option<
446 eggress_system_proxy::AppliedProxy,
447 > = None;
448
449 #[cfg(feature = "operations")]
450 let metrics_registry = metrics_registry_for_admin;
451 {
453 let mut guard = match health_for_run.lock() {
454 Ok(guard) => guard,
455 Err(error) => {
456 tracing::warn!("health manager state was poisoned; resetting it: {error}");
457 let mut guard = error.into_inner();
458 *guard = None;
459 health_for_run.clear_poison();
460 guard
461 }
462 };
463 if let Some(ref mut hm) = *guard {
464 let upstream_runtimes: Vec<Arc<UpstreamRuntime>> =
465 snapshot.load().upstreams.values().cloned().collect();
466 if !upstream_runtimes.is_empty() {
467 hm.start_probes(&upstream_runtimes);
468 }
469 }
470 }
471
472 let current_snapshot = snapshot.load();
473 let listener_configs = current_snapshot.listeners.clone();
474 #[cfg(feature = "operations")]
475 let admin_config = current_snapshot.admin.clone();
476 drop(current_snapshot);
477
478 if listener_configs.is_empty() {
479 tracing::warn!("no listeners configured; the proxy will not accept connections");
480 }
481
482 let mut prepared = Vec::new();
483 #[cfg(feature = "quic")]
484 let mut prepared_quic = Vec::<PreparedQuicListener>::new();
485 let compatibility_auth_reuse = compatibility_hooks
488 .as_ref()
489 .and_then(|hooks| hooks.auth_reuse.clone());
490 #[cfg(unix)]
491 let mut unix_listener_args = Vec::new();
492 let mut transparent_listener_args = Vec::new();
493
494 for lcfg in &listener_configs {
495 let protocols: Vec<ProtocolId> = lcfg.protocols.to_vec();
496
497 let auth = match &lcfg.auth {
498 Some(auth_cfg) => {
499 if auth_cfg.auth_type == "password" {
500 let username = auth_cfg.username.clone().unwrap_or_default();
501 let password = auth_cfg.password.clone().unwrap_or_default();
502 if let Some(reuse) = compatibility_auth_reuse.clone() {
503 eggress_server::accept::InboundAuthentication::UsernamePasswordWithReuse {
504 username,
505 password,
506 reuse,
507 }
508 } else {
509 eggress_server::accept::InboundAuthentication::UsernamePassword {
510 username,
511 password,
512 }
513 }
514 } else {
515 eggress_server::accept::InboundAuthentication::None
516 }
517 }
518 None => eggress_server::accept::InboundAuthentication::None,
519 };
520
521 let connection_limit = lcfg.connection_limit.unwrap_or(1024) as usize;
522
523 #[allow(unused_variables)]
525 if let Some(ref unix_cfg) = lcfg.unix {
526 #[cfg(unix)]
527 {
528 match eggress_server::listener::unix::create_unix_listener(
529 &eggress_server::listener::unix::UnixListenerConfig::from_compiled(
530 &unix_cfg.path,
531 unix_cfg.unlink_existing,
532 Some(unix_cfg.mode),
533 ),
534 ) {
535 Ok(unix_listener) => {
536 tracing::info!(
537 "unix socket listener created at {} ({})",
538 unix_cfg.path.display(),
539 lcfg.name
540 );
541 unix_listener_args.push((
542 lcfg.name.clone(),
543 unix_listener,
544 protocols,
545 auth,
546 handshake_timeout,
547 connection_limit as u64,
548 lcfg.tls.clone(),
549 lcfg.shadowsocks.clone(),
550 lcfg.trojan.clone(),
551 lcfg.udp.clone(),
552 ));
553 continue;
554 }
555 Err(e) => {
556 tracing::error!(
557 "failed to bind unix socket at {} for listener '{}': {e}",
558 unix_cfg.path.display(),
559 lcfg.name
560 );
561 continue;
562 }
563 }
564 }
565 #[cfg(not(unix))]
566 {
567 tracing::error!(
568 "unix socket listener '{}' skipped: not supported on this platform",
569 lcfg.name
570 );
571 continue;
572 }
573 }
574
575 if let Some(ref transparent_cfg) = lcfg.transparent {
577 if transparent_cfg.enabled {
578 let capability = check_capability(PlatformCapability::LinuxOriginalDstIpv4);
579 if capability != crate::platform::CapabilityStatus::Available {
580 #[cfg(feature = "operations")]
581 state_ref
582 .runtime_metrics
583 .record_platform_capability_check_failure();
584 let _cap_span = tracing::info_span!(
585 "capability_check_failed",
586 capability = %PlatformCapability::LinuxOriginalDstIpv4,
587 status = %capability,
588 listener = %lcfg.name,
589 );
590 tracing::warn!(
591 "transparent proxy not available for listener '{}' ({}); \
592 falling back to normal TCP listener",
593 lcfg.name,
594 capability
595 );
596 } else {
597 let bind_addr: std::net::SocketAddr =
598 lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
599 addr: lcfg.bind.clone(),
600 source: std::io::Error::new(
601 std::io::ErrorKind::InvalidInput,
602 e,
603 ),
604 })?;
605
606 let transparent_listener =
607 eggress_server::listener::transparent::TransparentListener::bind(
608 &bind_addr.to_string(),
609 )
610 .await
611 .map_err(|e| {
612 RuntimeError::ListenerBind {
613 addr: lcfg.bind.clone(),
614 source: e,
615 }
616 })?;
617
618 let local_addr = transparent_listener.local_addr().map_err(|e| {
619 RuntimeError::ListenerBind {
620 addr: lcfg.bind.clone(),
621 source: e,
622 }
623 })?;
624
625 tracing::info!(
626 "transparent TCP listener listening on {local_addr} ({})",
627 lcfg.name
628 );
629
630 transparent_listener_args.push((
631 lcfg.name.clone(),
632 transparent_listener,
633 protocols,
634 auth,
635 handshake_timeout,
636 connection_limit as u64,
637 lcfg.tls.clone(),
638 lcfg.shadowsocks.clone(),
639 lcfg.trojan.clone(),
640 lcfg.udp.clone(),
641 ));
642 continue;
643 }
644 }
645 }
646
647 #[cfg(feature = "quic")]
648 if protocols.contains(&ProtocolId::Quic) || protocols.contains(&ProtocolId::Http3) {
649 let tls = lcfg.tls.clone().ok_or_else(|| {
650 RuntimeError::Other(format!(
651 "QUIC/HTTP3 listener '{}' requires certificate and key material",
652 lcfg.name
653 ))
654 })?;
655 let bind_addr: std::net::SocketAddr =
656 lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
657 addr: lcfg.bind.clone(),
658 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
659 })?;
660 let listener = eggress_transport_quic::QuicListener::bind(
661 bind_addr,
662 eggress_transport_quic::QuicServerConfig {
663 certificate_pem: tls.cert_pem.clone(),
664 private_key_pem: tls.key_pem.clone(),
665 idle_timeout: Duration::from_secs(60),
666 max_concurrent_streams: lcfg.connection_limit.unwrap_or(1024).max(1),
667 alpn_protocols: if protocols.contains(&ProtocolId::Http3) {
668 vec![b"h3".to_vec()]
669 } else {
670 Vec::new()
671 },
672 },
673 )
674 .await
675 .map_err(|e| RuntimeError::ListenerBind {
676 addr: lcfg.bind.clone(),
677 source: std::io::Error::other(e.to_string()),
678 })?;
679 let local_addr =
680 listener
681 .local_addr()
682 .map_err(|e| RuntimeError::ListenerBind {
683 addr: lcfg.bind.clone(),
684 source: std::io::Error::other(e.to_string()),
685 })?;
686 tracing::info!("QUIC listening on {local_addr} ({})", lcfg.name);
687 prepared_quic.push(PreparedQuicListener {
688 name: lcfg.name.clone(),
689 protocols,
690 listener,
691 local_addr,
692 auth,
693 handshake_timeout,
694 connection_limit: lcfg.connection_limit.unwrap_or(1024) as u64,
695 });
696 continue;
697 }
698
699 let bind_addr: std::net::SocketAddr =
701 lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
702 addr: lcfg.bind.clone(),
703 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
704 })?;
705
706 let config = TcpListenerConfig {
707 bind_addr,
708 protocols: protocols.clone(),
709 auth_required: false,
710 handshake_timeout,
711 connection_limit,
712 };
713
714 let listener = TcpListener::new_with_reuse_port(
715 &config,
716 listener_cancel.clone(),
717 lcfg.reuse_port.unwrap_or(false),
718 )
719 .await
720 .map_err(|e| RuntimeError::ListenerBind {
721 addr: lcfg.bind.clone(),
722 source: e,
723 })?;
724 let local_addr = listener
725 .local_addr()
726 .map_err(|e| RuntimeError::ListenerBind {
727 addr: lcfg.bind.clone(),
728 source: e,
729 })?;
730 tracing::info!("listening on {local_addr} ({})", lcfg.name);
731
732 prepared.push(PreparedListener {
733 name: lcfg.name.clone(),
734 bind: lcfg.bind.clone(),
735 protocols,
736 listener,
737 local_addr,
738 auth,
739 handshake_timeout,
740 udp: lcfg.udp.clone(),
741 tls: lcfg.tls.clone(),
742 shadowsocks: lcfg.shadowsocks.clone(),
743 trojan: lcfg.trojan.clone(),
744 fixed_target: lcfg.fixed_target.clone(),
745 local_bind: lcfg.local_bind.clone(),
746 });
747 }
748
749 #[cfg(feature = "operations")]
750 {
751 let listener_infos: Vec<eggress_admin::ListenerInfo> = prepared
752 .iter()
753 .map(|p| eggress_admin::ListenerInfo {
754 name: p.name.clone(),
755 bind: p.bind.clone(),
756 local_addr: p.local_addr.to_string(),
757 protocols: p.protocols.iter().map(|p| p.to_string()).collect(),
758 udp_enabled: p.udp.as_ref().is_some_and(|u| u.enabled),
759 mode: Some("standard".to_string()),
760 capability_status: None,
761 original_dst_support: None,
762 unix_socket_path: None,
763 unix_socket_unlink_existing: None,
764 })
765 .collect();
766 drop(listener_infos);
767 }
768
769 {
772 let mut addr_map: std::collections::HashMap<String, Option<std::net::SocketAddr>> =
773 std::collections::HashMap::new();
774 for p in &prepared {
775 addr_map.insert(p.name.clone(), Some(p.local_addr));
776 }
777 #[cfg(feature = "quic")]
778 for p in &prepared_quic {
779 addr_map.insert(p.name.clone(), Some(p.local_addr));
780 }
781 for (name, transparent_listener, _, _, _, _, _, _, _, _) in
782 &transparent_listener_args
783 {
784 let addr = transparent_listener.local_addr().ok();
785 addr_map.insert(name.clone(), addr);
786 }
787 #[cfg(unix)]
788 for (name, _, _, _, _, _, _, _, _, _) in &unix_listener_args {
789 addr_map.insert(name.clone(), None);
791 }
792 let addrs: Vec<Option<std::net::SocketAddr>> = listener_configs
793 .iter()
794 .map(|lcfg| addr_map.get(&lcfg.name).copied().flatten())
795 .collect();
796 let admin_addrs = addrs.clone();
797 match state_ref.listener_addrs.lock() {
798 Ok(mut guard) => *guard = addrs,
799 Err(error) => {
800 tracing::warn!(
801 "listener address state was poisoned; resetting it: {error}"
802 );
803 let mut guard = error.into_inner();
804 *guard = addrs;
805 state_ref.listener_addrs.clear_poison();
806 }
807 }
808 #[cfg(feature = "operations")]
809 state_ref.publish_admin_listener_addrs(state_ref.snapshot.load_full(), admin_addrs);
810 }
811
812 #[cfg(feature = "extended")]
813 let mut shadowsocks_udp_relays = Vec::new();
814 let mut echo_udp_relays = Vec::new();
815
816 for prepared_listener in &prepared {
817 if let Some(ref udp_cfg) = prepared_listener.udp {
818 #[cfg(feature = "extended")]
819 if udp_cfg.mode == eggress_udp::UdpMode::ShadowsocksUdp {
820 shadowsocks_udp_relays.push(
821 prepare_shadowsocks_udp_relay(
822 prepared_listener,
823 udp_cfg,
824 routing.clone(),
825 &state_ref,
826 )
827 .await?,
828 );
829 }
830 if udp_cfg.mode == eggress_udp::UdpMode::Echo {
831 let socket =
832 Arc::new(tokio::net::UdpSocket::bind(udp_cfg.bind).await.map_err(
833 |e| RuntimeError::ListenerBind {
834 addr: udp_cfg.bind.to_string(),
835 source: e,
836 },
837 )?);
838 echo_udp_relays.push((prepared_listener.name.clone(), socket, None));
839 } else if udp_cfg.mode == eggress_udp::UdpMode::FixedTarget {
840 let socket =
841 Arc::new(tokio::net::UdpSocket::bind(udp_cfg.bind).await.map_err(
842 |e| RuntimeError::ListenerBind {
843 addr: udp_cfg.bind.to_string(),
844 source: e,
845 },
846 )?);
847 echo_udp_relays.push((
848 prepared_listener.name.clone(),
849 socket,
850 udp_cfg.fixed_target.clone(),
851 ));
852 }
853 }
854 }
855
856 #[cfg(feature = "extended")]
857 for (socket, relay_config) in shadowsocks_udp_relays {
858 let relay_cancel = cancel.clone();
859 tasks.spawn(async move {
860 let result =
861 eggress_udp::standalone_shadowsocks::shadowsocks_standalone_udp_relay(
862 socket,
863 relay_config,
864 relay_cancel,
865 )
866 .await;
867 if let Err(error) = result {
868 tracing::debug!(
869 %error,
870 "Shadowsocks UDP relay ended with error"
871 );
872 }
873 });
874 }
875
876 for (listener_name, socket, fixed_target) in echo_udp_relays {
877 let relay_cancel = cancel.clone();
878 let max_size = listener_configs
879 .iter()
880 .find(|l| l.name == listener_name)
881 .and_then(|l| l.udp.as_ref())
882 .map(|u| u.max_datagram_size)
883 .unwrap_or(65535);
884 tasks.spawn(async move {
885 let mut buf = vec![0u8; max_size];
886 const ECHO_DNS_TIMEOUT: Duration = Duration::from_secs(2);
887 const ECHO_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
888 const ECHO_RECV_TIMEOUT: Duration = Duration::from_secs(5);
889 let target_socket = if let Some(target) = fixed_target {
890 let lookup = async {
891 match target.host {
892 eggress_core::TargetHost::Ip(ip) => {
893 Ok(std::net::SocketAddr::new(ip, target.port))
894 }
895 eggress_core::TargetHost::Domain(domain) => match tokio::net::lookup_host((domain.as_str(), target.port)).await {
896 Ok(mut addrs) => match addrs.next() {
897 Some(addr) => Ok(addr),
898 None => Err(()),
899 },
900 Err(_) => Err(()),
901 },
902 }
903 };
904 let addr = match tokio::time::timeout(ECHO_DNS_TIMEOUT, lookup).await {
905 Ok(Ok(a)) => a,
906 Ok(Err(())) | Err(_) => return,
907 };
908 let target_socket = match tokio::net::UdpSocket::bind("0.0.0.0:0").await {
909 Ok(s) => s,
910 Err(_) => return,
911 };
912 if tokio::time::timeout(ECHO_CONNECT_TIMEOUT, target_socket.connect(addr))
913 .await
914 .map(|r| r.is_ok())
915 .unwrap_or(false)
916 {
917 Some(target_socket)
918 } else {
919 return;
920 }
921 } else { None };
922 let mut response = vec![0u8; max_size];
923 loop {
924 tokio::select! {
925 result = socket.recv_from(&mut buf) => match result {
926 Ok((n, peer)) => {
927 if let Some(ref target_socket) = target_socket {
928 if target_socket.send(&buf[..n]).await.is_ok() {
929 if let Ok(Ok(m)) = tokio::time::timeout(ECHO_RECV_TIMEOUT, target_socket.recv(&mut response)).await {
930 let _ = socket.send_to(&response[..m], peer).await;
931 }
932 }
933 } else { let _ = socket.send_to(&buf[..n], peer).await; }
934 }
935 Err(_) => break,
936 },
937 _ = relay_cancel.cancelled() => break,
938 }
939 }
940 });
941 }
942
943 for (
945 listener_name,
946 transparent_listener,
947 protocols,
948 auth,
949 hs_timeout,
950 connection_limit,
951 tls_cfg,
952 ss_cfg,
953 trojan_cfg,
954 udp_cfg,
955 ) in transparent_listener_args
956 {
957 let routing = routing.clone();
958 let state = state_ref.clone();
959 let conn_tasks = connection_tasks.clone();
960 let conn_cancel = connection_cancel.clone();
961 let tls_client_config = tls_client_config.clone();
962 let listener_cancel = listener_cancel.clone();
963
964 #[cfg(feature = "ssh")]
965 let listener_ssh_sessions = ssh_sessions.clone();
966
967 tasks.spawn(async move {
968 let proto_slice: Arc<[ProtocolId]> = protocols.clone().into();
969 let transparent_listener_inner = transparent_listener.inner();
970 let listener_active = Arc::new(AtomicU64::new(0));
971
972 let transparent_accepted = state.transparent_accepted_total.clone();
973 let transparent_dst_failed =
974 state.transparent_original_dst_failed_total.clone();
975
976 loop {
977 let accept_result = tokio::select! {
978 result = transparent_listener_inner.accept() => result,
979 _ = listener_cancel.cancelled() => {
980 break;
981 }
982 };
983
984 let (stream, _peer) = match accept_result {
985 Ok(s) => s,
986 Err(e) => {
987 handle_accept_error(
990 &format!("transparent on '{listener_name}'"),
991 &e,
992 )
993 .await;
994 continue;
995 }
996 };
997
998 let connection_slot = match ListenerConnectionSlot::try_acquire(
999 &listener_active,
1000 connection_limit,
1001 ) {
1002 Some(slot) => slot,
1003 None => {
1004 tracing::debug!(
1005 listener = %listener_name,
1006 limit = connection_limit,
1007 "dropping transparent connection: connection limit reached"
1008 );
1009 drop(stream);
1010 continue;
1011 }
1012 };
1013
1014 transparent_accepted.fetch_add(1, Ordering::Relaxed);
1015
1016 let original_dst =
1017 match eggress_server::listener::transparent::get_original_destination(&stream) {
1018 Ok(addr) => addr,
1019 Err(e) => {
1020 drop(connection_slot);
1021 transparent_dst_failed.fetch_add(1, Ordering::Relaxed);
1022 let _span = tracing::info_span!(
1023 "transparent_original_dst_failed",
1024 listener = %listener_name,
1025 error = %e,
1026 );
1027 tracing::warn!(
1028 "failed to get original destination for transparent connection on '{}': {e}",
1029 listener_name
1030 );
1031 continue;
1032 }
1033 };
1034
1035 let peer = stream
1036 .peer_addr()
1037 .unwrap_or_else(|_| {
1038 std::net::SocketAddr::new(
1039 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
1040 0,
1041 )
1042 });
1043
1044 let routing = routing.clone();
1045 let tls_client_config = tls_client_config.clone();
1046 let listener_str = listener_name.clone();
1047 let conn_id = state
1048 .connection_counter
1049 .fetch_add(1, Ordering::Relaxed);
1050 let conn_protocols = proto_slice.clone();
1051 let conn_auth = auth.clone();
1052 let conn_metrics = state.metrics.clone();
1053 #[cfg(feature = "extended")]
1054 let conn_ss_metrics = state.shadowsocks_metrics.clone();
1055 let active = state.active_connections.clone();
1056 let conn_cancel = conn_cancel.child_token();
1057 let generation = state.snapshot.load().generation;
1058 let tls_config = tls_cfg.clone();
1059 let ss_config = ss_cfg.clone();
1060 let trojan_config = trojan_cfg.clone();
1061
1062 let udp_svc =
1063 make_udp_service(&state, &routing, &listener_name, udp_cfg.as_ref());
1064
1065 #[cfg(feature = "ssh")]
1066 let conn_ssh_sessions = listener_ssh_sessions.clone();
1067 conn_tasks.spawn(async move {
1068 let _active_guard = ActiveConnectionGuard::new(active);
1069 let _connection_slot = connection_slot;
1070 let started = std::time::Instant::now();
1071
1072 let Some(stream) = wrap_tls_server(
1073 Box::new(stream),
1074 tls_config.as_ref(),
1075 peer,
1076 )
1077 .await
1078 else {
1079 return;
1080 };
1081
1082 let config = build_connection_config(ConnectionBuildParams {
1083 routing: routing as Arc<dyn RouteService>,
1084 listener: listener_str.clone(),
1085 peer: Some(peer),
1086 generation,
1087 handshake_timeout: hs_timeout,
1088 connect_timeout,
1089 protocols: conn_protocols,
1090 authentication: conn_auth,
1091 metrics: conn_metrics,
1092 udp: udp_svc,
1093 tls_client_config,
1094 security: InboundSecurity {
1095 shadowsocks: ss_config,
1096 trojan: trojan_config,
1097 },
1098 fixed_target: None,
1099 local_bind: None,
1100 #[cfg(feature = "extended")]
1101 shadowsocks_metrics: conn_ss_metrics,
1102 #[cfg(feature = "ssh")]
1103 ssh_sessions: Some(conn_ssh_sessions),
1104 });
1105
1106 let report = tokio::select! {
1107 report = eggress_server::serve_connection(stream, config)
1108 .instrument(tracing::info_span!(
1109 "conn",
1110 id = conn_id,
1111 peer = %peer,
1112 original_dst = %original_dst,
1113 listener_type = "transparent",
1114 listener = %listener_str,
1115 )) => {
1116 report
1117 }
1118 _ = conn_cancel.cancelled() => {
1119 eggress_server::SessionReport::cancelled(
1120 None,
1121 None,
1122 String::new(),
1123 )
1124 }
1125 };
1126
1127 tracing::info!(
1128 protocol = ?report.protocol,
1129 target = ?report.target,
1130 original_dst = %original_dst,
1131 route = %report.route,
1132 outcome = ?report.outcome,
1133 bytes_upstream = report.bytes_upstream,
1134 bytes_downstream = report.bytes_downstream,
1135 duration_ms = started.elapsed().as_millis() as u64,
1136 "transparent connection completed",
1137 );
1138 });
1139 }
1140 });
1141 }
1142
1143 #[cfg(unix)]
1145 for (
1146 listener_name,
1147 unix_listener,
1148 protocols,
1149 auth,
1150 hs_timeout,
1151 connection_limit,
1152 tls_cfg,
1153 ss_cfg,
1154 trojan_cfg,
1155 udp_cfg,
1156 ) in unix_listener_args
1157 {
1158 let routing = routing.clone();
1159 let state = state_ref.clone();
1160 let conn_tasks = connection_tasks.clone();
1161 let conn_cancel = connection_cancel.clone();
1162 let tls_client_config = tls_client_config.clone();
1163 let listener_cancel = listener_cancel.clone();
1164
1165 let socket_path = unix_listener.path().display().to_string();
1166
1167 #[cfg(feature = "ssh")]
1168 let listener_ssh_sessions = ssh_sessions.clone();
1169 tasks.spawn(async move {
1170 let proto_slice: Arc<[ProtocolId]> = protocols.clone().into();
1171 let listener_active = Arc::new(AtomicU64::new(0));
1172
1173 let _accept_loop_span = tracing::info_span!(
1174 "unix_accept_loop",
1175 listener = %listener_name,
1176 socket_path = %socket_path,
1177 );
1178
1179 loop {
1180 let (stream, _peer_addr) = tokio::select! {
1181 result = unix_listener.accept() => match result {
1182 Ok(r) => r,
1183 Err(e) => {
1184 handle_accept_error("unix", &e).await;
1185 continue;
1186 }
1187 },
1188 _ = listener_cancel.cancelled() => {
1189 break;
1190 }
1191 };
1192
1193 let connection_slot = match ListenerConnectionSlot::try_acquire(
1194 &listener_active,
1195 connection_limit,
1196 ) {
1197 Some(slot) => slot,
1198 None => {
1199 tracing::debug!(
1200 listener = %listener_name,
1201 limit = connection_limit,
1202 "dropping Unix connection: connection limit reached"
1203 );
1204 drop(stream);
1205 continue;
1206 }
1207 };
1208
1209 state
1210 .runtime_metrics
1211 .record_unix_listener_connection_accepted();
1212
1213 let routing = routing.clone();
1214 let tls_client_config = tls_client_config.clone();
1215 let listener_str = listener_name.clone();
1216 let conn_id = state.connection_counter.fetch_add(1, Ordering::Relaxed);
1217 let conn_protocols = proto_slice.clone();
1218 let conn_auth = auth.clone();
1219 let conn_metrics = state.metrics.clone();
1220 #[cfg(feature = "extended")]
1221 let conn_ss_metrics = state.shadowsocks_metrics.clone();
1222 let active = state.active_connections.clone();
1223 let conn_cancel = conn_cancel.child_token();
1224 let generation = state.snapshot.load().generation;
1225
1226 let tls_config = tls_cfg.clone();
1227 let ss_config = ss_cfg.clone();
1228 let trojan_config = trojan_cfg.clone();
1229 let socket_path_clone = socket_path.clone();
1230 let listener_str_for_span = listener_str.clone();
1231
1232 let udp_svc =
1233 make_udp_service(&state, &routing, &listener_name, udp_cfg.as_ref());
1234
1235 #[cfg(feature = "ssh")]
1236 let conn_ssh_sessions = listener_ssh_sessions.clone();
1237 conn_tasks.spawn(async move {
1238 let _active_guard = ActiveConnectionGuard::new(active);
1239 let _connection_slot = connection_slot;
1240 let started = std::time::Instant::now();
1241
1242 let peer = std::net::SocketAddr::new(
1243 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
1244 0,
1245 );
1246
1247 let Some(stream) =
1248 wrap_tls_server(Box::new(stream), tls_config.as_ref(), peer).await
1249 else {
1250 return;
1251 };
1252
1253 let config = build_connection_config(ConnectionBuildParams {
1254 routing: routing as Arc<dyn RouteService>,
1255 listener: listener_str,
1256 peer: Some(peer),
1257 generation,
1258 handshake_timeout: hs_timeout,
1259 connect_timeout,
1260 protocols: conn_protocols,
1261 authentication: conn_auth,
1262 metrics: conn_metrics,
1263 udp: udp_svc,
1264 tls_client_config,
1265 security: InboundSecurity {
1266 shadowsocks: ss_config,
1267 trojan: trojan_config,
1268 },
1269 fixed_target: None,
1270 local_bind: None,
1271 #[cfg(feature = "extended")]
1272 shadowsocks_metrics: conn_ss_metrics,
1273 #[cfg(feature = "ssh")]
1274 ssh_sessions: Some(conn_ssh_sessions),
1275 });
1276
1277 let report = tokio::select! {
1278 report = eggress_server::serve_connection(stream, config)
1279 .instrument(tracing::info_span!(
1280 "conn",
1281 id = conn_id,
1282 peer = %peer,
1283 listener_type = "unix",
1284 listener = %listener_str_for_span,
1285 socket_path = %socket_path_clone,
1286 )) => {
1287 report
1288 }
1289 _ = conn_cancel.cancelled() => {
1290 eggress_server::SessionReport::cancelled(
1291 None,
1292 None,
1293 String::new(),
1294 )
1295 }
1296 };
1297
1298 tracing::info!(
1299 protocol = ?report.protocol,
1300 target = ?report.target,
1301 route = %report.route,
1302 outcome = ?report.outcome,
1303 bytes_upstream = report.bytes_upstream,
1304 bytes_downstream = report.bytes_downstream,
1305 duration_ms = started.elapsed().as_millis() as u64,
1306 "unix connection completed",
1307 );
1308 });
1309 }
1310
1311 unix_listener.cleanup().unwrap_or_else(|e| {
1313 tracing::warn!("failed to cleanup unix socket: {e}");
1314 });
1315 });
1316 }
1317
1318 #[cfg(feature = "operations")]
1320 let compatibility_proxy_selection = prepared
1321 .iter()
1322 .find(|listener| {
1323 listener.protocols.contains(&ProtocolId::Socks5)
1324 && listener.local_addr.port() != 0
1325 })
1326 .map(|listener| {
1327 (
1328 eggress_system_proxy::CompatibilityProxyKind::Socks5,
1329 listener.local_addr.port(),
1330 )
1331 })
1332 .or_else(|| {
1333 prepared
1334 .iter()
1335 .find(|listener| {
1336 listener.protocols.contains(&ProtocolId::Http)
1337 && listener.local_addr.port() != 0
1338 })
1339 .map(|listener| {
1340 (
1341 eggress_system_proxy::CompatibilityProxyKind::Http,
1342 listener.local_addr.port(),
1343 )
1344 })
1345 });
1346
1347 if compatibility_hooks
1355 .as_ref()
1356 .and_then(|hooks| hooks.system_proxy)
1357 .is_some()
1358 {
1359 #[cfg(feature = "operations")]
1360 {
1361 let selected = compatibility_proxy_selection.ok_or_else(|| {
1362 RuntimeError::Other(
1363 "--sys requires a usable local HTTP or SOCKS5 listener".to_string(),
1364 )
1365 })?;
1366 let address = std::net::SocketAddr::new(
1367 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
1368 selected.1,
1369 );
1370 compatibility_system_proxy = Some(
1371 eggress_system_proxy::apply_compatibility_proxy(selected.0, address)
1372 .map_err(RuntimeError::Other)?,
1373 );
1374 }
1375 #[cfg(not(feature = "operations"))]
1376 {
1377 return Err(RuntimeError::Other(
1378 "--sys requires the operations feature".to_string(),
1379 ));
1380 }
1381 }
1382
1383 #[cfg(feature = "quic")]
1384 for prepared_listener in prepared_quic {
1385 let listener_name = prepared_listener.name.clone();
1386 let listener_protocols: Arc<[ProtocolId]> = prepared_listener
1387 .protocols
1388 .iter()
1389 .copied()
1390 .filter(|protocol| !matches!(protocol, ProtocolId::Quic | ProtocolId::Http3))
1391 .collect::<Vec<_>>()
1392 .into();
1393 let routing = routing.clone();
1394 let state = state_ref.clone();
1395 let conn_tasks = connection_tasks.clone();
1396 let conn_cancel = connection_cancel.clone();
1397 let listener_cancel = listener_cancel.clone();
1398 let is_h3 = prepared_listener.protocols.contains(&ProtocolId::Http3);
1399 let auth = prepared_listener.auth.clone();
1400 let handshake_timeout_for_listener = prepared_listener.handshake_timeout;
1401 let connection_limit = prepared_listener.connection_limit;
1402
1403 #[cfg(feature = "ssh")]
1404 let listener_ssh_sessions = ssh_sessions.clone();
1405
1406 if is_h3 {
1407 let listener = prepared_listener.listener.clone();
1408 let tls_client_config_for_listener = tls_client_config.clone();
1409 tasks.spawn(async move {
1410 let active_streams = Arc::new(AtomicU64::new(0));
1411 loop {
1412 let connection = match listener.accept_connection(&listener_cancel).await {
1413 Ok(Some(connection)) => connection,
1414 Ok(None) => break,
1415 Err(error) => {
1416 tracing::debug!(%error, listener = %listener_name, "H3 connection failed");
1417 continue;
1418 }
1419 };
1420 let routing = routing.clone();
1421 let state = state.clone();
1422 let conn_tasks = conn_tasks.clone();
1423 let conn_cancel = conn_cancel.child_token();
1424 let listener_name = listener_name.clone();
1425 let auth = auth.clone();
1426 let authorization = match &auth {
1427 eggress_server::accept::InboundAuthentication::None => None,
1428 eggress_server::accept::InboundAuthentication::UsernamePassword { username, password }
1429 | eggress_server::accept::InboundAuthentication::UsernamePasswordWithReuse { username, password, .. } => {
1430 Some((username.clone(), password.clone()))
1431 }
1432 };
1433 let active_streams = active_streams.clone();
1434 let protocols = listener_protocols.clone();
1435 let connection_limit = connection_limit;
1436 let tls_client_config_for_connection = tls_client_config_for_listener.clone();
1437 #[cfg(feature = "ssh")]
1438 let listener_ssh_sessions_for_connection = listener_ssh_sessions.clone();
1439 conn_tasks.spawn(async move {
1440 let result = eggress_protocol_h3::serve_connection(
1441 connection,
1442 conn_cancel.clone(),
1443 authorization,
1444 move |request, stream, peer| {
1445 let routing = routing.clone();
1446 let state = state.clone();
1447 let listener_name = listener_name.clone();
1448 let auth = auth.clone();
1449 let protocols = protocols.clone();
1450 let active_streams = active_streams.clone();
1451 let tls_client_config = tls_client_config_for_connection.clone();
1452 #[cfg(feature = "ssh")]
1453 let ssh_sessions = listener_ssh_sessions_for_connection.clone();
1454 async move {
1455 let slot = match ListenerConnectionSlot::try_acquire(&active_streams, connection_limit) {
1456 Some(slot) => slot,
1457 None => return,
1458 };
1459 let target = match request.target() {
1460 Ok(target) => target,
1461 Err(error) => {
1462 tracing::debug!(%error, "invalid H3 CONNECT authority");
1463 drop(slot);
1464 return;
1465 }
1466 };
1467 let generation = state.snapshot.load().generation;
1468 let config = eggress_server::ConnectionConfig {
1469 routing: routing as Arc<dyn RouteService>,
1470 context: eggress_server::ConnectionContext {
1471 source: Some(peer),
1472 listener: listener_name,
1473 generation,
1474 },
1475 handshake_timeout: handshake_timeout_for_listener,
1476 connect_timeout,
1477 protocols,
1478 authentication: auth,
1479 metrics: Some(state.metrics.clone()),
1480 udp: None,
1481 tls_client_config,
1482 shadowsocks: None,
1483 #[cfg(feature = "extended")]
1484 shadowsocks_metrics: Some(state.shadowsocks_metrics.clone()),
1485 #[cfg(not(feature = "extended"))]
1486 shadowsocks_metrics: None,
1487 trojan: None,
1488 fixed_target: None,
1489 local_bind: None,
1490 #[cfg(feature = "ssh")]
1491 ssh_sessions: Some(ssh_sessions),
1492 };
1493 let pending = eggress_server::accept::PendingTunnel {
1494 target: target.clone(),
1495 client: stream,
1496 protocol: eggress_server::accept::TunnelProtocol::Http3,
1497 reply_context: eggress_server::accept::ReplyContext::Http3,
1498 identity: eggress_core::ClientIdentity::Anonymous,
1499 };
1500 state.metrics.record_session_start();
1501 let report = eggress_server::execute::execute(
1502 eggress_server::accept::AcceptedSession::Tunnel(pending),
1503 &config,
1504 ).await;
1505 state.metrics.record_session(&report);
1506 drop(slot);
1507 }
1508 },
1509 ).await;
1510 if let Err(error) = result {
1511 tracing::debug!(%error, "H3 connection ended");
1512 }
1513 });
1514 }
1515 });
1516 } else {
1517 let listener = prepared_listener.listener.clone();
1518 let auth = prepared_listener.auth.clone();
1519 let tls_client_config_for_listener = tls_client_config.clone();
1520 tasks.spawn(async move {
1521 let active_streams = Arc::new(AtomicU64::new(0));
1522 let listener_name_for_handler = listener_name.clone();
1523 let protocols_for_handler = listener_protocols.clone();
1524 let routing_for_handler = routing.clone();
1525 let state_for_handler = state.clone();
1526 let auth_for_handler = auth.clone();
1527 #[cfg(feature = "ssh")]
1528 let listener_ssh_sessions_for_handler = Some(listener_ssh_sessions.clone());
1529 let result = listener
1530 .run(listener_cancel, move |stream, peer| {
1531 let routing = routing_for_handler.clone();
1532 let state = state_for_handler.clone();
1533 let listener_name = listener_name_for_handler.clone();
1534 let protocols = protocols_for_handler.clone();
1535 let auth = auth_for_handler.clone();
1536 let active_streams = active_streams.clone();
1537 #[cfg(feature = "ssh")]
1538 let ssh_sessions = listener_ssh_sessions_for_handler.clone();
1539 let tls_client_config = tls_client_config_for_listener.clone();
1540 async move {
1541 let Some(slot) = ListenerConnectionSlot::try_acquire(
1542 &active_streams,
1543 connection_limit,
1544 ) else {
1545 return;
1546 };
1547 let generation = state.snapshot.load().generation;
1548 let config = build_connection_config(ConnectionBuildParams {
1549 routing: routing as Arc<dyn RouteService>,
1550 listener: listener_name,
1551 peer: Some(peer),
1552 generation,
1553 handshake_timeout: handshake_timeout_for_listener,
1554 connect_timeout,
1555 protocols,
1556 authentication: auth,
1557 metrics: state.metrics.clone(),
1558 udp: None,
1559 tls_client_config,
1560 security: InboundSecurity {
1561 shadowsocks: None,
1562 trojan: None,
1563 },
1564 fixed_target: None,
1565 local_bind: None,
1566 #[cfg(feature = "extended")]
1567 shadowsocks_metrics: state.shadowsocks_metrics.clone(),
1568 #[cfg(feature = "ssh")]
1569 ssh_sessions,
1570 });
1571 let _ = eggress_server::serve_connection(stream, config).await;
1572 drop(slot);
1573 }
1574 })
1575 .await;
1576 if let Err(error) = result {
1577 tracing::debug!(%error, "QUIC listener ended");
1578 }
1579 });
1580 }
1581 }
1582
1583 for prepared_listener in prepared {
1584 let routing = routing.clone();
1585 let state = state_ref.clone();
1586 let conn_tasks = connection_tasks.clone();
1587 let conn_cancel = connection_cancel.clone();
1588 let tls_client_config = tls_client_config.clone();
1589
1590 #[cfg(feature = "ssh")]
1591 let listener_ssh_sessions = ssh_sessions.clone();
1592 tasks.spawn(async move {
1593 let proto_slice: Arc<[ProtocolId]> = prepared_listener.protocols.clone().into();
1594
1595 loop {
1596 let conn = match prepared_listener.listener.accept().await {
1597 Ok(c) => c,
1598 Err(e) => {
1599 if is_listener_cancelled(&e) {
1600 break;
1601 }
1602 handle_accept_error("tcp", &e).await;
1603 continue;
1604 }
1605 };
1606
1607 let routing = routing.clone();
1608 let tls_client_config = tls_client_config.clone();
1609 let peer = conn.peer_addr;
1610 let listener_str = prepared_listener.name.clone();
1611 let conn_id = state.connection_counter.fetch_add(1, Ordering::Relaxed);
1612 let conn_protocols = proto_slice.clone();
1613 let conn_auth = prepared_listener.auth.clone();
1614 let conn_metrics = state.metrics.clone();
1615 #[cfg(feature = "extended")]
1616 let conn_ss_metrics = state.shadowsocks_metrics.clone();
1617 let active = state.active_connections.clone();
1618 let conn_cancel = conn_cancel.child_token();
1619 let generation = state.snapshot.load().generation;
1620
1621 let tls_config = prepared_listener.tls.clone();
1622 let ss_config = prepared_listener.shadowsocks.clone();
1623 let trojan_config = prepared_listener.trojan.clone();
1624 let fixed_target = prepared_listener.fixed_target.clone();
1625 let local_bind = prepared_listener.local_bind.clone();
1626
1627 let udp_svc = make_udp_service(
1628 &state,
1629 &routing,
1630 &prepared_listener.name,
1631 prepared_listener.udp.as_ref(),
1632 );
1633 #[cfg(feature = "ssh")]
1634 let conn_ssh_sessions = listener_ssh_sessions.clone();
1635 let stream_tasks = conn_tasks.clone();
1636 conn_tasks.spawn(async move {
1637 let _active_guard = ActiveConnectionGuard::new(active);
1638 let started = std::time::Instant::now();
1639
1640 let Some(stream) =
1642 wrap_tls_server(Box::new(conn.stream), tls_config.as_ref(), peer)
1643 .await
1644 else {
1645 return;
1646 };
1647
1648 #[cfg(feature = "extended")]
1649 let advanced_protocol = conn_protocols.first().copied();
1650 #[cfg(feature = "extended")]
1651 let advanced_is_single = conn_protocols.len() == 1;
1652 #[cfg(feature = "extended")]
1653 let advanced_fixed_target = fixed_target.clone();
1654 let config = build_connection_config(ConnectionBuildParams {
1655 routing: routing as Arc<dyn RouteService>,
1656 listener: listener_str,
1657 peer: Some(peer),
1658 generation,
1659 handshake_timeout: prepared_listener.handshake_timeout,
1660 connect_timeout,
1661 protocols: conn_protocols,
1662 authentication: conn_auth,
1663 metrics: conn_metrics,
1664 udp: udp_svc,
1665 tls_client_config: tls_client_config.clone(),
1666 security: InboundSecurity {
1667 shadowsocks: ss_config,
1668 trojan: trojan_config,
1669 },
1670 fixed_target,
1671 local_bind,
1672 #[cfg(feature = "extended")]
1673 shadowsocks_metrics: conn_ss_metrics,
1674 #[cfg(feature = "ssh")]
1675 ssh_sessions: Some(conn_ssh_sessions),
1676 });
1677
1678 #[cfg(feature = "extended")]
1679 if matches!(
1680 advanced_protocol,
1681 Some(ProtocolId::Http2 | ProtocolId::WebSocket)
1682 ) && advanced_is_single
1683 {
1684 let advanced_result = match advanced_protocol {
1685 Some(ProtocolId::Http2) => {
1686 eggress_server::advanced::serve_h2_connection(
1687 stream,
1688 config,
1689 &stream_tasks,
1690 conn_cancel.clone(),
1691 )
1692 .await
1693 }
1694 Some(ProtocolId::WebSocket) => match advanced_fixed_target {
1695 Some(target) => {
1696 eggress_server::advanced::serve_websocket_connection(
1697 stream, config, target,
1698 )
1699 .await
1700 }
1701 None => Err("WebSocket listener requires a fixed target"
1702 .to_string()),
1703 },
1704 None => {
1705 Err("advanced listener has no configured protocol"
1706 .to_string())
1707 }
1708 Some(_) => {
1709 Err("advanced listener protocol is not supported here"
1710 .to_string())
1711 }
1712 };
1713 if let Err(error) = advanced_result {
1714 tracing::debug!(%peer, %error, "advanced listener ended");
1715 }
1716 return;
1717 }
1718
1719 let report = tokio::select! {
1720 report = eggress_server::serve_connection(stream, config)
1721 .instrument(tracing::info_span!(
1722 "conn",
1723 id = conn_id,
1724 peer = %peer,
1725 )) => {
1726 report
1727 }
1728 _ = conn_cancel.cancelled() => {
1729 eggress_server::SessionReport::cancelled(
1730 None,
1731 None,
1732 String::new(),
1733 )
1734 }
1735 };
1736
1737 tracing::info!(
1744 protocol = ?report.protocol,
1745 target = ?report.target,
1746 route = %report.route,
1747 outcome = ?report.outcome,
1748 bytes_upstream = report.bytes_upstream,
1749 bytes_downstream = report.bytes_downstream,
1750 duration_ms = started.elapsed().as_millis() as u64,
1751 "connection completed",
1752 );
1753 });
1754 }
1755 });
1756 }
1757
1758 #[cfg(feature = "reverse")]
1760 {
1761 let current_snapshot = snapshot.load();
1762 let reverse_servers = current_snapshot.reverse_servers.clone();
1763 let reverse_clients = current_snapshot.reverse_clients.clone();
1764 drop(current_snapshot);
1765
1766 for rs_cfg in reverse_servers {
1767 if rs_cfg.pproxy_compat {
1768 let server_config =
1769 eggress_protocol_reverse::compat_pproxy::PproxyBackwardServerConfig {
1770 control_bind: rs_cfg.control_bind,
1771 external_bind: rs_cfg.external_bind,
1772 auth: eggress_protocol_reverse::compat_pproxy::raw_auth(
1773 rs_cfg.auth_username.as_deref(),
1774 rs_cfg.auth_password.as_deref(),
1775 ),
1776 max_control_connections: rs_cfg.max_control_connections as usize,
1777 max_pending_external: rs_cfg.max_pending_external as usize,
1778 read_timeout_ms: rs_cfg.read_timeout_ms,
1779 socks5_target: None,
1780 client_framing:
1781 eggress_protocol_reverse::compat_pproxy::PproxyBackwardFraming::Raw,
1782 };
1783 let server =
1784 eggress_protocol_reverse::compat_pproxy::PproxyBackwardServer::new(
1785 server_config,
1786 );
1787 let server_cancel = server.cancel_token();
1788 let cancel_clone = cancel.clone();
1789 tasks.spawn(async move {
1790 let result = tokio::select! {
1791 r = server.run() => r,
1792 _ = cancel_clone.cancelled() => {
1793 server_cancel.cancel();
1794 Ok(())
1795 }
1796 };
1797 if let Err(e) = result {
1798 tracing::error!(error = %e, "pproxy backward server error");
1799 }
1800 });
1801 continue;
1802 }
1803 let server_tls = rs_cfg.tls.as_ref().map(|t| {
1804 eggress_protocol_reverse::tls::ReverseServerTlsConfig {
1805 cert_pem: t.cert_pem.clone(),
1806 key_pem: t.key_pem.clone(),
1807 client_ca_pem: t.client_ca_pem.clone(),
1808 require_client_cert: t.require_client_cert,
1809 }
1810 });
1811 let server_config = eggress_protocol_reverse::server::ReverseServerConfig {
1812 control_bind: rs_cfg.control_bind,
1813 external_bind: Some(rs_cfg.external_bind),
1814 auth_username: rs_cfg.auth_username.clone(),
1815 auth_password: rs_cfg.auth_password.clone(),
1816 max_control_connections: rs_cfg.max_control_connections,
1817 read_timeout_ms: rs_cfg.read_timeout_ms,
1818 allow_bind: rs_cfg.allow_bind.clone(),
1819 max_listeners_per_client: rs_cfg.max_listeners_per_client,
1820 max_streams_per_listener: rs_cfg.max_streams_per_listener,
1821 max_pending_external: rs_cfg.max_pending_external,
1822 tls: server_tls,
1823 };
1824 if let Err(e) = server_config.validate() {
1828 tracing::error!(
1829 server_id = %rs_cfg.id,
1830 error = %e,
1831 "reverse server configuration validation failed; skipping",
1832 );
1833 continue;
1834 }
1835 let mut server =
1836 eggress_protocol_reverse::server::ReverseServer::new(server_config);
1837 server.set_metrics(state_ref.reverse_metrics.clone());
1838 let server_state = server.state_handle();
1839 let server_cancel = server.cancel_token();
1840
1841 state_ref
1842 .reverse_registry
1843 .register(eggress_admin::ReverseServerEntry {
1844 id: eggress_admin::ReverseServerId::from(rs_cfg.id.as_str()),
1845 control_bind: rs_cfg.control_bind.to_string(),
1846 state: server_state,
1847 });
1848
1849 let cancel_clone = cancel.clone();
1850 tasks.spawn(async move {
1851 let result = tokio::select! {
1852 r = server.run() => r,
1853 _ = cancel_clone.cancelled() => {
1854 server_cancel.cancel();
1855 Ok(())
1856 }
1857 };
1858 if let Err(e) = result {
1859 tracing::error!(error = %e, "reverse server error");
1860 }
1861 });
1862 }
1863
1864 for rc_cfg in reverse_clients {
1865 let host = rc_cfg
1866 .default_target_host
1867 .clone()
1868 .unwrap_or_else(|| "127.0.0.1".to_string());
1869 let port = rc_cfg.default_target_port.unwrap_or(0);
1870
1871 let parallel = rc_cfg.parallel_connections.max(1);
1872 for conn_idx in 0..parallel {
1873 if rc_cfg.pproxy_compat {
1874 let client_config = eggress_protocol_reverse::compat_pproxy::PproxyBackwardClientConfig {
1875 server_addr: rc_cfg.server_addr,
1876 server_chain: rc_cfg.server_chain.clone(),
1877 auth: eggress_protocol_reverse::compat_pproxy::raw_auth(
1878 rc_cfg.auth_username.as_deref(),
1879 rc_cfg.auth_password.as_deref(),
1880 ),
1881 reconnect_initial_ms: rc_cfg.reconnect_initial_ms,
1882 reconnect_max_ms: rc_cfg.reconnect_max_ms,
1883 read_timeout_ms: rc_cfg.read_timeout_ms,
1884 target_connect_timeout_ms: 10_000,
1885 server_framing:
1886 eggress_protocol_reverse::compat_pproxy::PproxyBackwardFraming::Raw,
1887 };
1888 let client =
1889 eggress_protocol_reverse::compat_pproxy::PproxyBackwardClient::new(
1890 client_config,
1891 std::sync::Arc::new(
1892 crate::reverse::RouteEngineTargetResolver::new(
1893 routing.clone(),
1894 host.clone(),
1895 port,
1896 std::sync::Arc::from(rc_cfg.id.as_str()),
1897 Some(rc_cfg.server_addr),
1898 ),
1899 ),
1900 );
1901 let cancel_clone = cancel.clone();
1902 let client_cancel = client.cancel_token();
1903 let client_id = rc_cfg.id.clone();
1904 let server_addr = rc_cfg.server_addr;
1905 tasks.spawn(async move {
1906 let result = tokio::select! {
1907 r = client.run() => r,
1908 _ = cancel_clone.cancelled() => {
1909 client_cancel.cancel();
1910 Ok(())
1911 }
1912 };
1913 if let Err(e) = result {
1914 tracing::error!(error = %e, client_id = %client_id, server = %server_addr, conn = conn_idx, "pproxy backward client error");
1915 }
1916 });
1917 continue;
1918 }
1919 let client_tls = rc_cfg.tls.as_ref().map(|t| {
1920 eggress_protocol_reverse::tls::ReverseClientTlsConfig {
1921 ca_pem: t.ca_pem.clone(),
1922 server_name: t.server_name.clone(),
1923 client_cert_pem: t.client_cert_pem.clone(),
1924 client_key_pem: t.client_key_pem.clone(),
1925 }
1926 });
1927 let client_config = eggress_protocol_reverse::client::ReverseClientConfig {
1928 server_addr: rc_cfg.server_addr,
1929 auth_username: rc_cfg.auth_username.clone(),
1930 auth_password: rc_cfg.auth_password.clone(),
1931 reconnect_initial_ms: rc_cfg.reconnect_initial_ms,
1932 reconnect_max_ms: rc_cfg.reconnect_max_ms,
1933 default_target_host: rc_cfg.default_target_host.clone(),
1934 default_target_port: rc_cfg.default_target_port,
1935 read_timeout_ms: rc_cfg.read_timeout_ms,
1936 drain_grace_ms: rc_cfg.drain_grace_ms,
1937 target_connect_timeout_ms: 10_000,
1938 tls: client_tls,
1939 };
1940 let mut client =
1941 eggress_protocol_reverse::client::ReverseClient::new(client_config);
1942 client.set_metrics(state_ref.reverse_metrics.clone());
1943
1944 let resolver = crate::reverse::RouteEngineTargetResolver::new(
1945 routing.clone(),
1946 host.clone(),
1947 port,
1948 std::sync::Arc::from(rc_cfg.id.as_str()),
1949 Some(rc_cfg.server_addr),
1950 );
1951 client.set_resolver(std::sync::Arc::new(resolver));
1952
1953 let cancel_clone = cancel.clone();
1954 let client_cancel = client.cancel_token();
1955 let client_id = rc_cfg.id.clone();
1956 let server_addr = rc_cfg.server_addr;
1957
1958 tasks.spawn(async move {
1959 let result = tokio::select! {
1960 r = client.run() => r,
1961 _ = cancel_clone.cancelled() => {
1962 client_cancel.cancel();
1963 Ok(())
1964 }
1965 };
1966 if let Err(e) = result {
1967 tracing::error!(error = %e, client_id = %client_id, server = %server_addr, conn = conn_idx, "reverse client error");
1968 }
1969 });
1970 }
1971 }
1972 }
1973
1974 #[cfg(feature = "operations")]
1977 let pre_bound_admin = if let Some(ref admin_cfg) = admin_config {
1978 if admin_cfg.enabled {
1979 let bind = admin_cfg.bind.clone();
1980 let admin_cancel_token = admin_cancel.clone();
1981 match eggress_admin::AdminServer::new(&bind, admin_cancel_token).await {
1982 Ok(s) => Some(s),
1983 Err(e) => {
1984 return Err(RuntimeError::ListenerBind {
1985 addr: bind,
1986 source: std::io::Error::new(
1987 std::io::ErrorKind::AddrInUse,
1988 e.to_string(),
1989 ),
1990 });
1991 }
1992 }
1993 } else {
1994 None
1995 }
1996 } else {
1997 None
1998 };
1999
2000 #[cfg(feature = "operations")]
2001 if let (Some(server), Some(admin_cfg)) = (pre_bound_admin, admin_config.as_ref()) {
2002 let metrics_enabled = admin_cfg.metrics;
2003 let state_ref = state_ref.clone();
2004 let provider: Arc<dyn AdminSnapshotProvider> = listener_infos_provider.clone();
2005 if let Ok(addr) = server.local_addr() {
2006 match state_ref.admin_local_addr.lock() {
2007 Ok(mut guard) => *guard = Some(addr),
2008 Err(error) => {
2009 tracing::warn!(
2010 "admin listener address state was poisoned; resetting it: {error}"
2011 );
2012 let mut guard = error.into_inner();
2013 *guard = Some(addr);
2014 state_ref.admin_local_addr.clear_poison();
2015 }
2016 }
2017 }
2018 let admin_auth = admin_cfg.auth.clone();
2019 admin_tasks.spawn(async move {
2020 let admin_state = eggress_admin::AdminState {
2021 metrics: metrics_registry.clone(),
2022 start_time: state_ref.start_time,
2023 readiness: state_ref.readiness.clone(),
2024 active_connections: Some(state_ref.active_connections.clone()),
2025 provider,
2026 udp_registry: state_ref.udp_registry.clone(),
2027 #[cfg(feature = "reverse")]
2028 reverse_registry: state_ref.reverse_registry.clone(),
2029 #[cfg(not(feature = "reverse"))]
2030 reverse_registry: std::sync::Arc::new(eggress_admin::ReverseRegistry::new()),
2031 metrics_enabled,
2032 auth: admin_auth,
2033 };
2034 if let Err(e) = server.run(admin_state).await {
2035 tracing::error!("admin server error: {e}");
2036 }
2037 });
2038 }
2039
2040 #[cfg(unix)]
2041 {
2042 let mut sigterm =
2043 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate());
2044 let mut sighup =
2045 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup());
2046
2047 if let Err(ref e) = sigterm {
2048 tracing::warn!("failed to register SIGTERM handler: {e}");
2049 }
2050 if let Err(ref e) = sighup {
2051 tracing::warn!("failed to register SIGHUP handler: {e}");
2052 }
2053
2054 readiness.store(true, Ordering::Release);
2057
2058 loop {
2059 tokio::select! {
2060 _ = cancel.cancelled() => {
2061 tracing::info!("shutdown requested via cancel token");
2062 break;
2063 }
2064 _ = tokio::signal::ctrl_c() => {
2065 tracing::info!("shutdown signal received");
2066 break;
2067 }
2068 _ = async { sigterm.as_mut().ok()?.recv().await }, if sigterm.is_ok() => {
2069 tracing::info!("shutdown signal received");
2070 break;
2071 }
2072 _ = async { sighup.as_mut().ok()?.recv().await }, if sighup.is_ok() && !config_path.is_empty() => {
2073 tracing::info!("reload signal received, reloading config from {config_path}");
2074 let config_path_clone = config_path.clone();
2075 let load_result = tokio::task::spawn_blocking(move || {
2076 eggress_config::compile::load_and_compile(&config_path_clone)
2077 }).await;
2078 match load_result {
2079 Ok(Ok(new_rt_config)) => {
2080 match state_ref.apply_compiled_config(&new_rt_config) {
2085 ReloadResult::Applied { generation: gen, upstreams: upstream_count } => {
2086 tracing::info!(
2087 generation = gen,
2088 upstreams = upstream_count,
2089 "config reloaded successfully"
2090 );
2091 }
2092 ReloadResult::Rejected { reason } => {
2093 tracing::error!("reload rejected: {reason}");
2094 }
2095 ReloadResult::Failed { error } => {
2096 tracing::error!("reload failed (snapshot build): {error}");
2097 }
2098 }
2099 }
2100 Ok(Err(e)) => {
2101 runtime_metrics.record_reload(false);
2102 tracing::error!("reload failed (config load): {e}");
2103 }
2104 Err(join_err) => {
2105 runtime_metrics.record_reload(false);
2106 tracing::error!("reload task panicked: {join_err}");
2107 }
2108 }
2109 }
2110 }
2111 }
2112 }
2113
2114 #[cfg(not(unix))]
2115 {
2116 readiness.store(true, Ordering::Release);
2117 tokio::select! {
2118 _ = cancel.cancelled() => {
2119 tracing::info!("shutdown requested via cancel token");
2120 }
2121 _ = tokio::signal::ctrl_c() => {
2122 tracing::info!("shutdown signal received");
2123 }
2124 }
2125 }
2126
2127 shutdown_ordered(ShutdownPlan {
2128 readiness: readiness.clone(),
2129 listener_cancel: listener_cancel.clone(),
2130 health_cancel: health_cancel.clone(),
2131 connection_cancel: connection_cancel.clone(),
2132 admin_cancel: admin_cancel.clone(),
2133 state: state_ref.clone(),
2134 tasks: tasks.clone(),
2135 connection_tasks: connection_tasks.clone(),
2136 admin_tasks: admin_tasks.clone(),
2137 active_connections: active_connections.clone(),
2138 shutdown_grace,
2139 #[cfg(feature = "ssh")]
2140 ssh_sessions: ssh_sessions.clone(),
2141 #[cfg(feature = "operations")]
2142 compatibility_system_proxy,
2143 })
2144 .await?;
2145 Ok::<_, RuntimeError>(())
2146 };
2147
2148 let result = if tokio::runtime::Handle::try_current().is_err() {
2149 let rt = tokio::runtime::Runtime::new()?;
2151 rt.block_on(run_async)
2152 } else {
2153 std::thread::Builder::new()
2158 .name("eggress-supervisor".to_string())
2159 .spawn(move || -> Result<(), RuntimeError> {
2160 let rt = tokio::runtime::Builder::new_multi_thread()
2161 .enable_all()
2162 .build()
2163 .map_err(RuntimeError::RuntimeInit)?;
2164 rt.block_on(run_async)
2165 })
2166 .map_err(RuntimeError::RuntimeInit)?
2167 .join()
2168 .map_err(|payload| {
2169 let message = payload
2170 .downcast_ref::<String>()
2171 .map(String::as_str)
2172 .or_else(|| payload.downcast_ref::<&'static str>().copied())
2173 .unwrap_or("unknown panic payload");
2174 RuntimeError::Other(format!("supervisor thread panicked: {message}"))
2175 })?
2176 };
2177
2178 match &result {
2179 Ok(()) => tracing::info!("eggress stopped"),
2180 Err(e) => tracing::error!(error = %e, "eggress stopped with error"),
2181 }
2182
2183 result
2184 }
2185}
2186
2187#[cfg(test)]
2188mod tests {
2189 use super::*;
2190 use std::io::Write;
2191 use std::sync::atomic::AtomicBool;
2192 use tempfile::NamedTempFile;
2193
2194 use crate::snapshot::compile_runtime_snapshot;
2195 use eggress_config::compile::{GroupFallback, ProcessConfig, RuntimeConfig, TimeoutConfig};
2196 use eggress_routing::scheduler::SchedulerKind;
2197 use eggress_routing::{MatchExpr, RouteActionSpec, RuleId, UpstreamGroupId};
2198
2199 fn write_config(content: &str) -> NamedTempFile {
2200 let mut f = NamedTempFile::new().unwrap();
2201 f.write_all(content.as_bytes()).unwrap();
2202 f.flush().unwrap();
2203 f
2204 }
2205
2206 #[test]
2207 fn build_router_direct_only() {
2208 let rt_config = RuntimeConfig {
2209 process: ProcessConfig::default(),
2210 timeouts: TimeoutConfig::default(),
2211 listeners: vec![],
2212 upstreams: vec![],
2213 groups: vec![],
2214 rules: vec![],
2215 default_action: RouteActionSpec::Direct,
2216 admin: None,
2217 reverse_servers: vec![],
2218 reverse_clients: vec![],
2219 };
2220 let snap = compile_runtime_snapshot(&rt_config, None).unwrap();
2221 assert!(snap.router.rules().is_empty());
2222 }
2223
2224 #[test]
2225 fn build_router_with_group_references_unknown_upstream() {
2226 let rt_config = RuntimeConfig {
2227 process: ProcessConfig::default(),
2228 timeouts: TimeoutConfig::default(),
2229 listeners: vec![],
2230 upstreams: vec![],
2231 groups: vec![eggress_config::compile::UpstreamGroupConfig {
2232 id: UpstreamGroupId(Arc::from("main")),
2233 scheduler: SchedulerKind::RoundRobin,
2234 members: vec!["nonexistent".to_string()],
2235 fallback: GroupFallback::Reject,
2236 }],
2237 rules: vec![],
2238 default_action: RouteActionSpec::Direct,
2239 admin: None,
2240 reverse_servers: vec![],
2241 reverse_clients: vec![],
2242 };
2243 let result = compile_runtime_snapshot(&rt_config, None);
2244 assert!(result.is_err(), "expected error, got Ok");
2245 let err_msg = result.err().unwrap().to_string();
2246 assert!(err_msg.contains("nonexistent"));
2247 }
2248
2249 #[test]
2250 fn build_router_with_valid_group() {
2251 let rt_config = RuntimeConfig {
2252 process: ProcessConfig::default(),
2253 timeouts: TimeoutConfig::default(),
2254 listeners: vec![],
2255 upstreams: vec![eggress_config::compile::UpstreamConfig {
2256 id: "proxy1".to_string(),
2257 chain: eggress_uri::ProxyChainSpec { hops: vec![] },
2258 health: eggress_routing::health::HealthConfig::default(),
2259 h2: None,
2260 }],
2261 groups: vec![eggress_config::compile::UpstreamGroupConfig {
2262 id: UpstreamGroupId(Arc::from("main")),
2263 scheduler: SchedulerKind::RoundRobin,
2264 members: vec!["proxy1".to_string()],
2265 fallback: GroupFallback::Reject,
2266 }],
2267 rules: vec![],
2268 default_action: RouteActionSpec::Direct,
2269 admin: None,
2270 reverse_servers: vec![],
2271 reverse_clients: vec![],
2272 };
2273 let snap = compile_runtime_snapshot(&rt_config, None).unwrap();
2274 assert!(snap.router.rules().is_empty());
2275 }
2276
2277 #[test]
2278 fn build_router_rule_references_unknown_group() {
2279 let rt_config = RuntimeConfig {
2280 process: ProcessConfig::default(),
2281 timeouts: TimeoutConfig::default(),
2282 listeners: vec![],
2283 upstreams: vec![],
2284 groups: vec![],
2285 rules: vec![eggress_routing::CompiledRule {
2286 id: RuleId(Arc::from("r1")),
2287 matcher: MatchExpr::Any,
2288 action: RouteActionSpec::UpstreamGroup(UpstreamGroupId(Arc::from("missing"))),
2289 }],
2290 default_action: RouteActionSpec::Direct,
2291 admin: None,
2292 reverse_servers: vec![],
2293 reverse_clients: vec![],
2294 };
2295 let result = compile_runtime_snapshot(&rt_config, None);
2296 assert!(result.is_err(), "expected error, got Ok");
2297 let err_msg = result.err().unwrap().to_string();
2298 assert!(err_msg.contains("missing"));
2299 }
2300
2301 #[tokio::test]
2302 async fn load_config_start_supervisor() {
2303 let config = r#"
2304version = 1
2305
2306[[listeners]]
2307name = "test"
2308bind = "127.0.0.1:0"
2309protocols = ["http"]
2310"#;
2311 let f = write_config(config);
2312 let path = f.path().to_str().unwrap();
2313 let result = ServiceSupervisor::start(path);
2314 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
2315 }
2316
2317 #[test]
2318 fn active_connections_counter_increments_and_decrements() {
2319 let active = Arc::new(AtomicU64::new(0));
2320 assert_eq!(active.load(Ordering::Acquire), 0);
2321 active.fetch_add(1, Ordering::AcqRel);
2322 assert_eq!(active.load(Ordering::Acquire), 1);
2323 active.fetch_add(1, Ordering::AcqRel);
2324 assert_eq!(active.load(Ordering::Acquire), 2);
2325 active.fetch_sub(1, Ordering::Release);
2326 assert_eq!(active.load(Ordering::Acquire), 1);
2327 active.fetch_sub(1, Ordering::Release);
2328 assert_eq!(active.load(Ordering::Acquire), 0);
2329 }
2330
2331 #[test]
2332 fn active_connections_guard_releases_on_panic() {
2333 let active = Arc::new(AtomicU64::new(0));
2334 let result = std::panic::catch_unwind({
2335 let active = active.clone();
2336 move || {
2337 let _guard = ActiveConnectionGuard::new(active);
2338 panic!("connection task panic");
2339 }
2340 });
2341 assert!(result.is_err());
2342 assert_eq!(active.load(Ordering::Acquire), 0);
2343 }
2344
2345 #[test]
2346 fn readiness_flag_controls_ready_endpoint() {
2347 let readiness = Arc::new(AtomicBool::new(true));
2348 assert!(readiness.load(Ordering::Relaxed));
2349 readiness.store(false, Ordering::Relaxed);
2350 assert!(!readiness.load(Ordering::Relaxed));
2351 readiness.store(true, Ordering::Relaxed);
2352 assert!(readiness.load(Ordering::Relaxed));
2353 }
2354
2355 #[test]
2356 fn reload_rejects_listener_name_change() {
2357 let config1 = r#"
2358version = 1
2359
2360[[listeners]]
2361name = "http-in"
2362bind = "127.0.0.1:8080"
2363protocols = ["http"]
2364"#;
2365 let config2 = r#"
2366version = 1
2367
2368[[listeners]]
2369name = "http-changed"
2370bind = "127.0.0.1:8080"
2371protocols = ["http"]
2372"#;
2373 let f1 = write_config(config1);
2374 let f2 = write_config(config2);
2375 let path1 = f1.path().to_str().unwrap();
2376 let path2 = f2.path().to_str().unwrap();
2377
2378 let sup = ServiceSupervisor::start(path1).unwrap();
2379 let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
2380 let snap = sup.state.snapshot.load();
2381 let result = classify_reload_config(
2382 &snap.listeners,
2383 &snap.timeouts,
2384 snap.admin.as_ref(),
2385 &new_config,
2386 );
2387 assert!(result.is_err(), "listener name change should be rejected");
2388 assert!(result.unwrap_err().contains("name changed"));
2389 }
2390
2391 #[test]
2392 fn reload_rejects_listener_bind_change() {
2393 let config1 = r#"
2394version = 1
2395
2396[[listeners]]
2397name = "http-in"
2398bind = "127.0.0.1:8080"
2399protocols = ["http"]
2400"#;
2401 let config2 = r#"
2402version = 1
2403
2404[[listeners]]
2405name = "http-in"
2406bind = "127.0.0.1:9090"
2407protocols = ["http"]
2408"#;
2409 let f1 = write_config(config1);
2410 let f2 = write_config(config2);
2411 let path1 = f1.path().to_str().unwrap();
2412 let path2 = f2.path().to_str().unwrap();
2413
2414 let sup = ServiceSupervisor::start(path1).unwrap();
2415 let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
2416 let snap = sup.state.snapshot.load();
2417 let result = classify_reload_config(
2418 &snap.listeners,
2419 &snap.timeouts,
2420 snap.admin.as_ref(),
2421 &new_config,
2422 );
2423 assert!(result.is_err(), "listener bind change should be rejected");
2424 assert!(result.unwrap_err().contains("bind"));
2425 }
2426
2427 #[test]
2428 fn reload_accepts_unchanged_listeners() {
2429 let config = r#"
2430version = 1
2431
2432[[listeners]]
2433name = "http-in"
2434bind = "127.0.0.1:8080"
2435protocols = ["http"]
2436"#;
2437 let f = write_config(config);
2438 let path = f.path().to_str().unwrap();
2439
2440 let sup = ServiceSupervisor::start(path).unwrap();
2441 let new_config = eggress_config::compile::load_and_compile(path).unwrap();
2442 let snap = sup.state.snapshot.load();
2443 let result = classify_reload_config(
2444 &snap.listeners,
2445 &snap.timeouts,
2446 snap.admin.as_ref(),
2447 &new_config,
2448 );
2449 assert!(result.is_ok(), "unchanged listeners should be accepted");
2450 }
2451
2452 #[test]
2453 fn reload_rejects_udp_topology_changes() {
2454 let config_without_udp = r#"
2455version = 1
2456
2457[[listeners]]
2458name = "socks-in"
2459bind = "127.0.0.1:8080"
2460protocols = ["socks5"]
2461"#;
2462 let config_with_udp = r#"
2463version = 1
2464
2465[[listeners]]
2466name = "socks-in"
2467bind = "127.0.0.1:8080"
2468protocols = ["socks5"]
2469
2470[listeners.udp]
2471enabled = true
2472bind = "127.0.0.1:0"
2473"#;
2474 let f1 = write_config(config_without_udp);
2475 let f2 = write_config(config_with_udp);
2476 let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
2477 let new_config =
2478 eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
2479
2480 let snap = sup.state.snapshot.load();
2481 let result = classify_reload_config(
2482 &snap.listeners,
2483 &snap.timeouts,
2484 snap.admin.as_ref(),
2485 &new_config,
2486 );
2487 assert!(result.is_err());
2488 assert!(result.unwrap_err().contains("UDP"));
2489 }
2490
2491 #[test]
2492 fn reload_rejects_timeout_change() {
2493 let config1 = r#"
2494version = 1
2495
2496[timeouts]
2497handshake = "10s"
2498
2499[[listeners]]
2500name = "http-in"
2501bind = "127.0.0.1:8080"
2502protocols = ["http"]
2503"#;
2504 let config2 = r#"
2505version = 1
2506
2507[timeouts]
2508handshake = "5s"
2509
2510[[listeners]]
2511name = "http-in"
2512bind = "127.0.0.1:8080"
2513protocols = ["http"]
2514"#;
2515 let f1 = write_config(config1);
2516 let f2 = write_config(config2);
2517 let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
2518 let new_config =
2519 eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
2520
2521 let snap = sup.state.snapshot.load();
2522 let result = classify_reload_config(
2523 &snap.listeners,
2524 &snap.timeouts,
2525 snap.admin.as_ref(),
2526 &new_config,
2527 );
2528 assert!(result.is_err());
2529 assert!(result.unwrap_err().contains("timeout"));
2530 }
2531
2532 #[test]
2533 fn reload_rejects_admin_bind_change() {
2534 let config1 = r#"
2535version = 1
2536
2537[[listeners]]
2538name = "http-in"
2539bind = "127.0.0.1:8080"
2540protocols = ["http"]
2541
2542[admin]
2543bind = "127.0.0.1:9090"
2544enabled = false
2545"#;
2546 let config2 = r#"
2547version = 1
2548
2549[[listeners]]
2550name = "http-in"
2551bind = "127.0.0.1:8080"
2552protocols = ["http"]
2553
2554[admin]
2555bind = "127.0.0.1:9091"
2556enabled = false
2557"#;
2558 let f1 = write_config(config1);
2559 let f2 = write_config(config2);
2560 let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
2561 let new_config =
2562 eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
2563
2564 let snap = sup.state.snapshot.load();
2565 let result = classify_reload_config(
2566 &snap.listeners,
2567 &snap.timeouts,
2568 snap.admin.as_ref(),
2569 &new_config,
2570 );
2571 assert!(result.is_err(), "admin bind change should be rejected");
2572 assert!(result.unwrap_err().contains("admin"));
2573 }
2574
2575 #[test]
2576 fn reload_rejects_listener_count_change() {
2577 let config1 = r#"
2578version = 1
2579
2580[[listeners]]
2581name = "http-in"
2582bind = "127.0.0.1:8080"
2583protocols = ["http"]
2584"#;
2585 let config2 = r#"
2586version = 1
2587
2588[[listeners]]
2589name = "http-in"
2590bind = "127.0.0.1:8080"
2591protocols = ["http"]
2592
2593[[listeners]]
2594name = "socks-in"
2595bind = "127.0.0.1:1080"
2596protocols = ["socks5"]
2597"#;
2598 let f1 = write_config(config1);
2599 let f2 = write_config(config2);
2600 let path1 = f1.path().to_str().unwrap();
2601 let path2 = f2.path().to_str().unwrap();
2602
2603 let sup = ServiceSupervisor::start(path1).unwrap();
2604 let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
2605 let snap = sup.state.snapshot.load();
2606 let result = classify_reload_config(
2607 &snap.listeners,
2608 &snap.timeouts,
2609 snap.admin.as_ref(),
2610 &new_config,
2611 );
2612 assert!(result.is_err(), "listener count change should be rejected");
2613 assert!(result.unwrap_err().contains("listener count"));
2614 }
2615
2616 #[test]
2617 fn reload_rejects_transparent_enabled_change() {
2618 let config1 = r#"
2619version = 1
2620
2621[[listeners]]
2622name = "http-in"
2623bind = "127.0.0.1:8080"
2624protocols = ["http"]
2625"#;
2626 let config2 = r#"
2627version = 1
2628
2629[[listeners]]
2630name = "http-in"
2631bind = "127.0.0.1:8080"
2632protocols = ["http"]
2633
2634[listeners.transparent]
2635enabled = true
2636"#;
2637 let f1 = write_config(config1);
2638 let f2 = write_config(config2);
2639 let path1 = f1.path().to_str().unwrap();
2640 let path2 = f2.path().to_str().unwrap();
2641
2642 let sup = ServiceSupervisor::start(path1).unwrap();
2643 let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
2644 let snap = sup.state.snapshot.load();
2645 let result = classify_reload_config(
2646 &snap.listeners,
2647 &snap.timeouts,
2648 snap.admin.as_ref(),
2649 &new_config,
2650 );
2651 assert!(
2652 result.is_err(),
2653 "transparent enabled change should be rejected"
2654 );
2655 assert!(result.unwrap_err().contains("transparent"));
2656 }
2657
2658 #[test]
2659 fn reload_rejects_unix_path_change() {
2660 let config1 = r#"
2661version = 1
2662
2663[[listeners]]
2664name = "http-in"
2665bind = "127.0.0.1:8080"
2666protocols = ["http"]
2667
2668[listeners.unix]
2669path = "/tmp/eggress.sock"
2670"#;
2671 let config2 = r#"
2672version = 1
2673
2674[[listeners]]
2675name = "http-in"
2676bind = "127.0.0.1:8080"
2677protocols = ["http"]
2678
2679[listeners.unix]
2680path = "/tmp/eggress-new.sock"
2681"#;
2682 let f1 = write_config(config1);
2683 let f2 = write_config(config2);
2684 let path1 = f1.path().to_str().unwrap();
2685 let path2 = f2.path().to_str().unwrap();
2686
2687 let sup = ServiceSupervisor::start(path1).unwrap();
2688 let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
2689 let snap = sup.state.snapshot.load();
2690 let result = classify_reload_config(
2691 &snap.listeners,
2692 &snap.timeouts,
2693 snap.admin.as_ref(),
2694 &new_config,
2695 );
2696 assert!(result.is_err(), "unix path change should be rejected");
2697 assert!(result.unwrap_err().contains("unix socket path"));
2698 }
2699
2700 #[test]
2701 fn compute_advertise_explicit() {
2702 let result = compute_advertise_ip(
2703 Some("10.0.0.1".parse().unwrap()),
2704 "0.0.0.0".parse().unwrap(),
2705 Some("127.0.0.1:5000".parse().unwrap()),
2706 );
2707 assert_eq!(
2708 result.unwrap(),
2709 std::net::IpAddr::V4("10.0.0.1".parse().unwrap())
2710 );
2711 }
2712
2713 #[test]
2714 fn compute_advertise_bind_ip() {
2715 let result = compute_advertise_ip(
2716 None,
2717 "192.168.1.1".parse().unwrap(),
2718 Some("127.0.0.1:5000".parse().unwrap()),
2719 );
2720 assert_eq!(
2721 result.unwrap(),
2722 std::net::IpAddr::V4("192.168.1.1".parse().unwrap())
2723 );
2724 }
2725
2726 #[test]
2727 fn compute_advertise_loopback_fallback() {
2728 let result = compute_advertise_ip(
2729 None,
2730 "0.0.0.0".parse().unwrap(),
2731 Some("127.0.0.1:5000".parse().unwrap()),
2732 );
2733 assert_eq!(
2734 result.unwrap(),
2735 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
2736 );
2737 }
2738
2739 #[test]
2740 fn compute_advertise_unspecified_non_loopback_rejected() {
2741 let result = compute_advertise_ip(
2742 None,
2743 "0.0.0.0".parse().unwrap(),
2744 Some("192.168.1.10:5000".parse().unwrap()),
2745 );
2746 assert!(
2747 result.is_err(),
2748 "non-loopback with unspecified bind should fail"
2749 );
2750 }
2751
2752 #[test]
2753 fn compute_advertise_without_tcp_peer_is_rejected() {
2754 let result = compute_advertise_ip(None, "0.0.0.0".parse().unwrap(), None);
2755 assert!(result.is_err());
2756 }
2757
2758 #[test]
2759 fn compute_advertise_ipv6_loopback() {
2760 let result = compute_advertise_ip(
2761 None,
2762 "::".parse().unwrap(),
2763 Some("[::1]:5000".parse().unwrap()),
2764 );
2765 assert_eq!(
2766 result.unwrap(),
2767 std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
2768 );
2769 }
2770
2771 #[test]
2772 fn compute_advertise_preserves_unspecified_bind_family() {
2773 let result = compute_advertise_ip(
2774 None,
2775 "0.0.0.0".parse().unwrap(),
2776 Some("[::ffff:127.0.0.1]:5000".parse().unwrap()),
2777 );
2778 assert_eq!(
2779 result.unwrap(),
2780 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
2781 );
2782 }
2783
2784 #[test]
2785 fn compute_advertise_explicit_overrides_bind() {
2786 let result = compute_advertise_ip(
2787 Some("10.0.0.1".parse().unwrap()),
2788 "192.168.1.1".parse().unwrap(),
2789 Some("127.0.0.1:5000".parse().unwrap()),
2790 );
2791 assert_eq!(
2792 result.unwrap(),
2793 std::net::IpAddr::V4("10.0.0.1".parse().unwrap())
2794 );
2795 }
2796
2797 static LEGACY_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2800
2801 fn with_legacy_env_reset(f: impl FnOnce()) {
2802 let _guard = LEGACY_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2803 let saved = std::env::var("EGRESS_SSH_INSECURE_HOST_KEYS").ok();
2804 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
2805 match saved {
2806 Some(v) => std::env::set_var("EGRESS_SSH_INSECURE_HOST_KEYS", v),
2807 None => std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS"),
2808 }
2809 if let Err(e) = result {
2810 std::panic::resume_unwind(e);
2811 }
2812 }
2813
2814 fn minimal_runtime_config() -> eggress_config::compile::RuntimeConfig {
2815 eggress_config::compile::RuntimeConfig {
2816 process: eggress_config::compile::ProcessConfig::default(),
2817 timeouts: eggress_config::compile::TimeoutConfig::default(),
2818 listeners: vec![],
2819 upstreams: vec![],
2820 groups: vec![],
2821 rules: vec![],
2822 default_action: RouteActionSpec::Direct,
2823 admin: None,
2824 reverse_servers: vec![],
2825 reverse_clients: vec![],
2826 }
2827 }
2828
2829 #[test]
2830 #[allow(deprecated)]
2831 fn legacy_compatibility_options_source_surface_type_checks() {
2832 let options = crate::CompatibilityOptions {
2834 compatibility_mode: true,
2835 auth_timeout: Some(Duration::from_secs(60)),
2836 system_proxy: false,
2837 debug: true,
2838 verbose_level: 2,
2839 };
2840 assert!(options.compatibility_mode);
2841 assert_eq!(options.auth_timeout, Some(Duration::from_secs(60)));
2842 assert!(!options.system_proxy);
2843 assert!(options.debug);
2844 assert_eq!(options.verbose_level, 2);
2845 let cloned = options.clone();
2846 let _debug = format!("{cloned:?}");
2847 let _default = crate::CompatibilityOptions::default();
2848
2849 let _method: fn(
2851 eggress_config::compile::RuntimeConfig,
2852 Option<String>,
2853 crate::CompatibilityOptions,
2854 ) -> Result<ServiceSupervisor, crate::RuntimeError> =
2855 ServiceSupervisor::start_from_config_with_options;
2856
2857 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2859 assert!(hooks.auth_reuse.is_some());
2860 }
2861
2862 #[test]
2863 fn legacy_default_options_convert_to_empty_hooks() {
2864 with_legacy_env_reset(|| {
2865 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2866 let hooks = CompatibilityRuntimeHooks::from_legacy_options(
2867 &crate::CompatibilityOptions::default(),
2868 );
2869 assert!(hooks.auth_reuse.is_none());
2870 assert!(hooks.system_proxy.is_none());
2871 assert!(!hooks.allow_insecure_ssh_host_keys);
2872 assert!(hooks.is_empty());
2873 });
2874 }
2875
2876 #[test]
2877 fn legacy_auth_timeout_maps_to_cache() {
2878 with_legacy_env_reset(|| {
2879 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2880 let options = crate::CompatibilityOptions {
2881 auth_timeout: Some(Duration::from_secs(30)),
2882 ..Default::default()
2883 };
2884 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2885 assert!(hooks.auth_reuse.is_some());
2886 assert!(hooks.system_proxy.is_none());
2887 });
2888 }
2889
2890 #[test]
2891 fn legacy_system_proxy_maps_to_narrow_hook() {
2892 with_legacy_env_reset(|| {
2893 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2894 let options = crate::CompatibilityOptions {
2895 system_proxy: true,
2896 ..Default::default()
2897 };
2898 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2899 assert!(hooks.system_proxy.is_some());
2900 assert!(hooks.auth_reuse.is_none());
2901 });
2902 }
2903
2904 #[test]
2905 fn legacy_logging_fields_do_not_enter_hooks() {
2906 with_legacy_env_reset(|| {
2907 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2908 let options = crate::CompatibilityOptions {
2909 debug: true,
2910 verbose_level: 3,
2911 ..Default::default()
2912 };
2913 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2914 assert!(hooks.is_empty());
2915 });
2916 }
2917
2918 #[test]
2919 fn legacy_compat_mode_without_env_stays_secure() {
2920 with_legacy_env_reset(|| {
2921 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2922 let options = crate::CompatibilityOptions {
2923 compatibility_mode: true,
2924 ..Default::default()
2925 };
2926 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2927 assert!(!hooks.allow_insecure_ssh_host_keys);
2928 });
2929 }
2930
2931 #[test]
2932 fn legacy_compat_mode_with_false_env_stays_secure() {
2933 with_legacy_env_reset(|| {
2934 std::env::set_var("EGRESS_SSH_INSECURE_HOST_KEYS", "0");
2935 let options = crate::CompatibilityOptions {
2936 compatibility_mode: true,
2937 ..Default::default()
2938 };
2939 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2940 assert!(!hooks.allow_insecure_ssh_host_keys);
2941 });
2942 }
2943
2944 #[test]
2945 fn legacy_compat_mode_with_accepted_env_allows_insecure() {
2946 for accepted in ["1", "true", "yes"] {
2947 with_legacy_env_reset(|| {
2948 std::env::set_var("EGRESS_SSH_INSECURE_HOST_KEYS", accepted);
2949 let options = crate::CompatibilityOptions {
2950 compatibility_mode: true,
2951 ..Default::default()
2952 };
2953 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2954 assert!(
2955 hooks.allow_insecure_ssh_host_keys,
2956 "accepted value {accepted:?} should allow insecure SSH"
2957 );
2958 });
2959 }
2960 }
2961
2962 #[test]
2963 fn legacy_non_compat_mode_with_accepted_env_stays_secure() {
2964 with_legacy_env_reset(|| {
2965 std::env::set_var("EGRESS_SSH_INSECURE_HOST_KEYS", "1");
2966 let options = crate::CompatibilityOptions {
2967 compatibility_mode: false,
2968 ..Default::default()
2969 };
2970 let hooks = CompatibilityRuntimeHooks::from_legacy_options(&options);
2971 assert!(!hooks.allow_insecure_ssh_host_keys);
2972 });
2973 }
2974
2975 #[test]
2976 #[allow(deprecated)]
2977 fn legacy_start_shim_default_preserves_native_hooks() {
2978 with_legacy_env_reset(|| {
2979 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2980 let sup = ServiceSupervisor::start_from_config_with_options(
2981 minimal_runtime_config(),
2982 None,
2983 crate::CompatibilityOptions::default(),
2984 )
2985 .unwrap();
2986 assert!(sup.compatibility_hooks.is_none());
2987 });
2988 }
2989
2990 #[test]
2991 #[allow(deprecated)]
2992 fn legacy_start_shim_nonempty_produces_hooks() {
2993 with_legacy_env_reset(|| {
2994 std::env::remove_var("EGRESS_SSH_INSECURE_HOST_KEYS");
2995 let sup = ServiceSupervisor::start_from_config_with_options(
2996 minimal_runtime_config(),
2997 None,
2998 crate::CompatibilityOptions {
2999 auth_timeout: Some(Duration::from_secs(5)),
3000 ..Default::default()
3001 },
3002 )
3003 .unwrap();
3004 let hooks = sup.compatibility_hooks.as_ref().unwrap();
3005 assert!(hooks.auth_reuse.is_some());
3006 });
3007 }
3008}