Skip to main content

eggress_runtime/
supervisor.rs

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/// Explicit opt-in for `--sys`: apply the selected local HTTP/SOCKS5 listener
70/// as the OS system proxy after bind, restoring on shutdown.
71///
72/// This is a narrow post-bind runtime hook, not ordinary listener
73/// configuration: pproxy-style startup may bind an ephemeral port whose actual
74/// address is required before OS state can be applied. Native startup passes
75/// `None` (no OS mutation).
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub struct SystemProxyRequest;
78
79/// Minimal residual compatibility surface for runtime/post-bind behavior.
80///
81/// Only state that genuinely requires runtime participation lives here:
82///
83/// - `auth_reuse`: pre-built source-IP auth reuse cache for pproxy `--auth`.
84///   The compatibility facade computes the timeout via
85///   `PproxyArgs::effective_auth_timeout()` and constructs
86///   `AuthReuseCache::new(timeout)` before startup; the generic supervisor
87///   never interprets pproxy argument semantics, it only threads the typed
88///   handle into inbound authentication.
89/// - `system_proxy`: opt-in post-bind OS proxy hook (`--sys`). `None` means
90///   no OS mutation.
91/// - `allow_insecure_ssh_host_keys`: narrow SSH host-key policy resolved by
92///   the compatibility facade from `EGRESS_SSH_INSECURE_HOST_KEYS`. Native
93///   startup never sets it.
94///
95/// Pre-start adapter policy is intentionally absent: `-d`/`-v` log-level
96/// selection is resolved by the CLI facade via
97/// `PproxyArgs::default_log_level()` before supervisor construction with
98/// explicit `RUST_LOG` precedence, and structured compatibility warnings stay
99/// in `eggress-pproxy-compat`. The generic supervisor consumes ordinary
100/// tracing and emits one generic `connection completed` log backed by normal
101/// session reports.
102///
103/// Native startup passes `None`; compatibility startup passes
104/// `Some(hooks)` via `start_from_config_with_compatibility`.
105///
106/// Legacy callers that still construct [`CompatibilityOptions`] should convert
107/// via [`CompatibilityRuntimeHooks::from_legacy_options`]; the legacy DTO never
108/// enters supervisor state.
109#[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    /// Build the canonical pproxy compatibility hooks from already-parsed
131    /// facade decisions.
132    ///
133    /// `auth_timeout` is the fully-resolved reuse interval (typically
134    /// `PproxyArgs::effective_auth_timeout()`); `system_proxy` mirrors `--sys`;
135    /// `allow_insecure_ssh_host_keys` mirrors
136    /// `ssh_insecure_acknowledged()` (explicit `EGRESS_SSH_INSECURE_HOST_KEYS`
137    /// opt-in). This keeps pproxy argument interpretation in the facade while
138    /// giving runtime a single typed construction point.
139    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    /// Canonical conversion from the legacy public facade DTO.
154    ///
155    /// Ownership: `auth_timeout`/`system_proxy`/`compatibility_mode` are
156    /// interpreted here once at the boundary; the supervisor never stores
157    /// [`CompatibilityOptions`].
158    ///
159    /// - `auth_timeout: Some(d)` becomes a bounded process-local
160    ///   `AuthReuseCache`; `None` means no reuse cache. The pproxy CLI's
161    ///   30-day default lives in `PproxyArgs::effective_auth_timeout()` at the
162    ///   facade, not here.
163    /// - `system_proxy: true` becomes `Some(SystemProxyRequest)` (post-bind
164    ///   opt-in); `false` becomes `None` (no OS mutation).
165    /// - `compatibility_mode` is consumed into `allow_insecure_ssh_host_keys`
166    ///   and never becomes a generic runtime mode flag: insecure SSH is
167    ///   permitted only when `compatibility_mode` is true **and**
168    ///   `ssh_insecure_acknowledged()` observes the explicit
169    ///   `EGRESS_SSH_INSECURE_HOST_KEYS` opt-in. Non-compatibility startup
170    ///   stays secure even if the variable is set.
171    /// - `debug`/`verbose_level` are legacy presentation inputs and are
172    ///   intentionally ignored here; logging policy is facade-owned (see
173    ///   `PproxyArgs::default_log_level()`). The legacy startup shim emits a
174    ///   single warning for non-default values.
175    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    /// Whether this hook set carries no compatibility state.
187    ///
188    /// Used by the legacy startup shim to preserve native-equivalent behavior
189    /// for `CompatibilityOptions::default()` instead of fabricating hooks.
190    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/// Legacy public source-compatible facade DTO.
198///
199/// This type exists only so pre-Phase-3 Rust callers keep compiling. It must
200/// not become a field on `ServiceSupervisor`, `RuntimeState`, connection
201/// configuration, or other generic runtime state. New code should use
202/// [`CompatibilityRuntimeHooks`] plus facade-level logging initialization.
203///
204/// - `auth_timeout`/`system_proxy` translate into narrow runtime hooks via
205///   [`CompatibilityRuntimeHooks::from_legacy_options`];
206/// - `compatibility_mode` exists for source compatibility and controls only
207///   the legacy SSH host-key disposition (insecure only with explicit
208///   `EGRESS_SSH_INSECURE_HOST_KEYS` opt-in);
209/// - `debug`/`verbose_level` are legacy presentation inputs that cannot safely
210///   configure an already-initialized process-global tracing subscriber from
211///   deep runtime code and are therefore ignored apart from a single facade
212///   warning in the legacy startup shim.
213#[derive(Debug, Clone, Default)]
214pub struct CompatibilityOptions {
215    /// Legacy compatibility-mode selector. Consumed during conversion into
216    /// the narrow `allow_insecure_ssh_host_keys` hook boolean; never stored
217    /// as a generic runtime mode flag.
218    pub compatibility_mode: bool,
219    /// Optional per-client source-IP auth reuse interval. `None` means no
220    /// reuse cache for low-level legacy callers (the CLI facade applies its
221    /// own `effective_auth_timeout()` default).
222    pub auth_timeout: Option<Duration>,
223    /// Legacy `--sys` selector. Maps only to the narrow post-bind
224    /// `SystemProxyRequest` hook.
225    pub system_proxy: bool,
226    /// Legacy presentation input. Accepted for source compatibility; does not
227    /// configure runtime tracing.
228    pub debug: bool,
229    /// Legacy presentation input. Accepted for source compatibility; does not
230    /// configure runtime tracing.
231    pub verbose_level: u8,
232}
233
234/// Whether the operator explicitly acknowledged unverified SSH host keys.
235///
236/// The compatibility facade calls this before startup; the generic supervisor
237/// consumes only the resulting bool. `1`/`true`/`yes` opts into
238/// `SshSessionCache::new_compatibility()` (MITM risk); anything else keeps
239/// `known_hosts` verification.
240pub 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    /// Start from an already-validated [`RuntimeConfig`] without reading a file.
259    ///
260    /// When `config_path` is provided (file-backed startup), SIGHUP reload
261    /// is enabled. When `None` (in-memory/compatibility startup), SIGHUP
262    /// reload is disabled because there is no stable user-authored config
263    /// file to reload from.
264    ///
265    /// Native startup passes no compatibility state (`None`).
266    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    /// Start from an already-validated [`RuntimeConfig`] with explicit
274    /// compatibility runtime hooks.
275    ///
276    /// Only the pproxy compatibility facades use this path, passing hooks
277    /// built via `CompatibilityRuntimeHooks::from_facade`. Native startup
278    /// uses `start`/`start_from_config` (no compatibility state).
279    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    /// Legacy source-compatible startup shim.
288    ///
289    /// Restored so pre-Phase-3 callers of
290    /// `start_from_config_with_options(RuntimeConfig, Option<String>,
291    /// CompatibilityOptions)` keep compiling. Converts the legacy DTO via
292    /// [`CompatibilityRuntimeHooks::from_legacy_options`] and delegates to the
293    /// canonical Phase 3 startup path without duplicating supervisor
294    /// initialization:
295    ///
296    /// ```text
297    /// start_from_config_with_options(... CompatibilityOptions)
298    ///     -> legacy conversion
299    ///     -> start_from_config_with_compatibility(... hooks)
300    ///     -> startup::init_supervisor(... Some(hooks))
301    /// ```
302    ///
303    /// A default legacy value preserves native-equivalent behavior by
304    /// delegating to `start_from_config()` (no fabricated hooks).
305    /// `debug`/`verbose_level` are accepted for source compatibility but do
306    /// not configure tracing; a single warning explains that logging policy
307    /// is facade-owned. New code should use
308    /// `start_from_config_with_compatibility` with
309    /// [`CompatibilityRuntimeHooks`].
310    #[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    /// Override the TLS client config used for upstream connections (e.g., Trojan).
349    /// Intended for test-only use (e.g., insecure TLS for self-signed certs).
350    #[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    /// Attempt to reload configuration. Encapsulates the full reload transaction:
357    /// 1. Load and compile new config
358    /// 2. Delegate to the canonical [`RuntimeState::apply_compiled_config`]
359    ///    transaction (classification, snapshot build, publish, routing swap,
360    ///    admin publish, health restart, pool invalidation, metrics).
361    /// 3. Update stored rt_config for subsequent reloads on success.
362    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            // Keep the configuration used by the next reload in sync. The
384            // snapshot remains authoritative for classification; this stored
385            // copy only supports file-backed bookkeeping.
386            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            // Start health probes inside the runtime context
452            {
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            // The facade pre-builds the typed reuse handle; runtime only
486            // threads it into inbound authentication (no pproxy arg parsing).
487            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                // Handle Unix domain socket listeners separately
524                #[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                // Handle transparent TCP listeners
576                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                // Standard TCP listener path
700                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            // Store listener addresses for admin snapshot, indexed by config order.
770            // Build a lookup map from all listener types (standard, transparent, unix).
771            {
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                    // Unix domain sockets don't have a meaningful TCP socket address
790                    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            // Spawn transparent listener accept loops
944            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                                // Cancellation is handled by the token select
988                                // above; the inner accept cannot report it.
989                                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            // Spawn Unix domain socket accept loops
1144            #[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                    // Cleanup socket file on shutdown
1312                    unix_listener.cleanup().unwrap_or_else(|e| {
1313                        tracing::warn!("failed to cleanup unix socket: {e}");
1314                    });
1315                });
1316            }
1317
1318            // Spawn standard TCP accept loops
1319            #[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            // All compatibility TCP listeners are bound before this point,
1348            // so --sys can use the actual selected port. Apply before any
1349            // accept loop or admin task is started; an apply failure is
1350            // therefore still a pre-run startup error. Only present when the
1351            // compatibility facade explicitly opted in via
1352            // `CompatibilityRuntimeHooks { system_proxy: Some(..) }`; native
1353            // startup (`None`) never mutates OS proxy state.
1354            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                            // Apply TLS if configured for this listener
1641                            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                            // Generic session reporting: one `connection completed`
1738                            // line backed by the normal session report. pproxy
1739                            // `-d`/`-v` presentation policy is resolved by the
1740                            // CLI facade via `default_log_level()` before
1741                            // startup; no compatibility verbosity state lives
1742                            // in the supervisor.
1743                            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            // Spawn reverse servers and clients
1759            #[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                    // Defense-in-depth: validate the configuration before
1825                    // spawning the task so unsafe configurations fail at
1826                    // startup rather than silently at bind time.
1827                    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            // Pre-bind admin listener before marking readiness so bind failures
1975            // are surfaced as startup errors rather than silent background failures.
1976            #[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 also means signal handling is installed. This
2055                // prevents a reload signal from racing startup.
2056                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                                    // Single canonical transaction owns
2081                                    // classification, snapshot build, publish,
2082                                    // routing/admin/health/pool/metrics side
2083                                    // effects. Metrics are recorded inside.
2084                                    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            // Caller is not inside a tokio runtime; create one and block on it.
2150            let rt = tokio::runtime::Runtime::new()?;
2151            rt.block_on(run_async)
2152        } else {
2153            // Caller is already inside a tokio runtime. Driving a long-lived
2154            // supervisor body via Handle::current().block_on from a worker
2155            // thread would panic or deadlock. Run on a dedicated OS thread
2156            // with its own runtime.
2157            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    // --- Legacy CompatibilityOptions source-compat + conversion (closure) ---
2798
2799    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        // Fails to compile if the pre-Phase-3 public field names/types disappear.
2833        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        // Old runtime entry-point shape must keep type-checking.
2850        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        // Conversion entry point must keep its canonical shape.
2858        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}