Skip to main content

eggress_runtime/
supervisor.rs

1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::{Arc, Mutex};
3use std::time::{Duration, Instant};
4
5use arc_swap::ArcSwap;
6#[cfg(feature = "operations")]
7use eggress_admin::{AdminSnapshot, AdminSnapshotProvider, ListenerInfo};
8use eggress_core::listener::{is_listener_cancelled, TcpListener, TcpListenerConfig};
9use eggress_core::ProtocolId;
10use eggress_routing::health::HealthManager;
11use eggress_routing::upstream::UpstreamRuntime;
12use eggress_routing::{RouteService, SharedRoutingService};
13use tokio_util::sync::CancellationToken;
14use tokio_util::task::TaskTracker;
15use tracing::Instrument;
16
17use crate::error::RuntimeError;
18use crate::platform::{check_capability, PlatformCapability};
19use crate::snapshot::{compile_runtime_snapshot, CompiledRuntimeSnapshot};
20
21/// Pause between retries when a listener's `accept()` keeps failing (for
22/// example fd exhaustion), so the loop does not tight-spin while the system
23/// is already resource-starved.
24const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(100);
25
26/// Log an accept failure, backing off briefly for persistent error classes so
27/// repeated failures cannot hot-spin the accept loop. Transient races (a
28/// queued connection vanishing before `accept`) retry immediately.
29async fn handle_accept_error(context: &str, error: &std::io::Error) {
30    match error.kind() {
31        std::io::ErrorKind::WouldBlock
32        | std::io::ErrorKind::Interrupted
33        | std::io::ErrorKind::ConnectionAborted => {}
34        _ => tokio::time::sleep(ACCEPT_ERROR_BACKOFF).await,
35    }
36    tracing::error!("{context} accept error: {error}");
37}
38
39/// Per-listener connection slot for listener implementations that cannot use
40/// the core TCP listener wrapper (transparent and Unix sockets).
41struct ListenerConnectionSlot {
42    active: Arc<AtomicU64>,
43}
44
45impl ListenerConnectionSlot {
46    fn try_acquire(active: &Arc<AtomicU64>, limit: u64) -> Option<Self> {
47        active
48            .fetch_update(Ordering::AcqRel, Ordering::Relaxed, |current| {
49                (current < limit).then_some(current + 1)
50            })
51            .ok()
52            .map(|_| Self {
53                active: active.clone(),
54            })
55    }
56}
57
58impl Drop for ListenerConnectionSlot {
59    fn drop(&mut self) {
60        self.active.fetch_sub(1, Ordering::Release);
61    }
62}
63
64/// Guard for active-connection accounting on non-TCP listeners.
65///
66/// TCP listeners account via `eggress_core::listener::PermitStream` (semaphore
67/// permit held for the whole session). Transparent and Unix listeners use this
68/// counter instead. The two mechanisms are **exclusive** — a connection must
69/// use exactly one, never both, otherwise the `active_connections` metric
70/// double-counts. Call sites below (transparent/Unix/QUIC paths) ensure this
71/// invariant; TCP accept paths must not also create this guard.
72///
73/// Ordering: `AcqRel` on inc pairs with `Release` on dec for visibility of
74/// the counter to the metrics reader. `Relaxed` would also be correct for a
75/// metrics-only counter, but `AcqRel` is kept for consistency with the
76/// previous implementation.
77struct ActiveConnectionGuard {
78    active: Arc<AtomicU64>,
79}
80
81impl ActiveConnectionGuard {
82    fn new(active: Arc<AtomicU64>) -> Self {
83        active.fetch_add(1, Ordering::AcqRel);
84        Self { active }
85    }
86}
87
88impl Drop for ActiveConnectionGuard {
89    fn drop(&mut self) {
90        self.active.fetch_sub(1, Ordering::Release);
91    }
92}
93
94/// Result of a reload attempt.
95#[derive(Debug)]
96pub enum ReloadResult {
97    /// Reload was applied successfully.
98    Applied { generation: u64, upstreams: usize },
99    /// Reload was rejected due to unsupported changes.
100    Rejected { reason: String },
101    /// Reload failed due to a compile or build error.
102    Failed { error: String },
103}
104
105/// Adapter that exposes the runtime's compiled snapshot to the admin server.
106///
107/// Implements `AdminSnapshotProvider` so that admin handlers see live data:
108/// each request reads the current `ArcSwap<CompiledRuntimeSnapshot>` rather
109/// than a startup-captured copy. Reloads take effect on the next request.
110#[cfg(feature = "operations")]
111pub struct RuntimeAdminListenerInfos {
112    state: Arc<RuntimeState>,
113}
114
115#[cfg(feature = "operations")]
116struct RuntimeAdminState {
117    snapshot: Arc<CompiledRuntimeSnapshot>,
118    listener_addrs: Vec<Option<std::net::SocketAddr>>,
119}
120
121#[cfg(feature = "operations")]
122impl AdminSnapshotProvider for RuntimeAdminListenerInfos {
123    fn generation(&self) -> u64 {
124        self.state.admin_snapshot.load().snapshot.generation
125    }
126
127    fn snapshot(&self) -> AdminSnapshot {
128        let admin_state = self.state.admin_snapshot.load();
129        let snap = &admin_state.snapshot;
130        let addrs = &admin_state.listener_addrs;
131        let listeners: Vec<ListenerInfo> = snap
132            .listeners
133            .iter()
134            .enumerate()
135            .map(|(idx, lcfg)| {
136                let mode = if lcfg.transparent.as_ref().is_some_and(|t| t.enabled) {
137                    Some("transparent".to_string())
138                } else if lcfg.unix.is_some() {
139                    Some("unix".to_string())
140                } else {
141                    Some("standard".to_string())
142                };
143
144                let (capability_status, original_dst_support) =
145                    if lcfg.transparent.as_ref().is_some_and(|t| t.enabled) {
146                        let cap = crate::platform::check_capability(
147                            crate::platform::PlatformCapability::LinuxOriginalDstIpv4,
148                        );
149                        (
150                            Some(cap.to_string()),
151                            Some(cap == crate::platform::CapabilityStatus::Available),
152                        )
153                    } else {
154                        (None, None)
155                    };
156
157                let (unix_socket_path, unix_socket_unlink_existing) =
158                    if let Some(ref unix_cfg) = lcfg.unix {
159                        (
160                            Some(unix_cfg.path.display().to_string()),
161                            Some(unix_cfg.unlink_existing),
162                        )
163                    } else {
164                        (None, None)
165                    };
166
167                ListenerInfo {
168                    name: lcfg.name.clone(),
169                    bind: lcfg.bind.clone(),
170                    local_addr: addrs
171                        .get(idx)
172                        .and_then(|a| *a)
173                        .map(|a| a.to_string())
174                        .or_else(|| unix_socket_path.clone())
175                        .unwrap_or_default(),
176                    protocols: lcfg.protocols.iter().map(|p| p.to_string()).collect(),
177                    udp_enabled: lcfg.udp.as_ref().is_some_and(|u| u.enabled),
178                    mode,
179                    capability_status,
180                    original_dst_support,
181                    unix_socket_path,
182                    unix_socket_unlink_existing,
183                }
184            })
185            .collect();
186        AdminSnapshot {
187            generation: snap.generation,
188            router: snap.router.clone(),
189            pac: snap.admin.as_ref().and_then(|a| a.pac.clone()),
190            static_routes: snap
191                .admin
192                .as_ref()
193                .map(|a| a.static_content.clone())
194                .unwrap_or_default(),
195            listeners,
196        }
197    }
198}
199
200/// What is and isn't reloaded on SIGHUP:
201///
202/// **Reloaded (hot-swap, no downtime):**
203/// - Upstream chains and health config (with Arc reuse for unchanged upstreams)
204/// - Upstream groups, schedulers, and fallback policies
205/// - Routing rules and default action
206/// - Listener configuration metadata (name, bind, protocols, auth)
207/// - Admin PAC and static content configuration
208///
209/// **NOT reloaded (requires full restart):**
210/// - Listener socket bindings (bound before readiness)
211/// - Process-level settings (log format, log level, shutdown grace)
212/// - Timeout configuration
213/// - Admin bind address
214///
215/// **UDP-specific reload semantics:**
216/// - UDP limits apply to new associations only; existing associations keep their limits.
217/// - UDP bind changes are restart-required.
218/// - UDP advertise address changes are restart-required if socket bind changes.
219/// - Route changes apply immediately to future UDP packets.
220///
221/// Classify whether a reload is supported given old and new listener
222/// configs. Returns `Ok(())` if the reload is safe, or `Err(reason)`
223/// if it should be rejected.
224fn classify_listeners(
225    old_listeners: &[eggress_config::compile::ListenerConfig],
226    new_listeners: &[eggress_config::compile::ListenerConfig],
227) -> Result<(), String> {
228    if old_listeners.len() != new_listeners.len() {
229        return Err(format!(
230            "listener count changed ({} -> {}); restart required",
231            old_listeners.len(),
232            new_listeners.len()
233        ));
234    }
235
236    for (old, new) in old_listeners.iter().zip(new_listeners.iter()) {
237        if old.name != new.name {
238            return Err(format!(
239                "listener name changed ('{}' -> '{}'); restart required",
240                old.name, new.name
241            ));
242        }
243        if old.bind != new.bind {
244            return Err(format!(
245                "listener bind address changed for '{}': '{}' -> '{}'; restart required",
246                old.name, old.bind, new.bind
247            ));
248        }
249        match (&old.udp, &new.udp) {
250            (Some(old_udp), Some(new_udp)) => {
251                if old_udp.bind != new_udp.bind
252                    || old_udp.enabled != new_udp.enabled
253                    || old_udp.mode != new_udp.mode
254                {
255                    return Err(format!(
256                        "UDP listener configuration changed for '{}'; restart required",
257                        old.name
258                    ));
259                }
260            }
261            (None, Some(new_udp)) => {
262                return Err(format!(
263                    "UDP configuration added for '{}': '{}'; restart required",
264                    new.name, new_udp.bind
265                ));
266            }
267            (Some(_old_udp), None) => {
268                return Err(format!(
269                    "UDP configuration removed for '{}'; restart required",
270                    old.name
271                ));
272            }
273            (None, None) => {}
274        }
275
276        match (&old.transparent, &new.transparent) {
277            (Some(old_t), Some(new_t)) => {
278                if old_t.enabled != new_t.enabled {
279                    return Err(format!(
280                        "transparent config changed for '{}': enabled {} -> {}; restart required",
281                        old.name, old_t.enabled, new_t.enabled
282                    ));
283                }
284            }
285            (None, Some(new_t)) => {
286                if new_t.enabled {
287                    return Err(format!(
288                        "transparent proxy enabled for '{}'; restart required",
289                        new.name
290                    ));
291                }
292            }
293            (Some(old_t), None) if old_t.enabled => {
294                return Err(format!(
295                    "transparent proxy configuration removed for '{}'; restart required",
296                    old.name
297                ));
298            }
299            (Some(_old_t), None) => {}
300            (None, None) => {}
301        }
302
303        match (&old.unix, &new.unix) {
304            (Some(old_u), Some(new_u)) => {
305                if old_u.path != new_u.path {
306                    return Err(format!(
307                        "unix socket path changed for '{}': '{}' -> '{}'; restart required",
308                        old.name,
309                        old_u.path.display(),
310                        new_u.path.display()
311                    ));
312                }
313            }
314            (None, Some(_new_u)) => {
315                return Err(format!(
316                    "unix socket added for '{}'; restart required",
317                    new.name
318                ));
319            }
320            (Some(_old_u), None) => {
321                return Err(format!(
322                    "unix socket removed for '{}'; restart required",
323                    old.name
324                ));
325            }
326            (None, None) => {}
327        }
328
329        // Hot-swappable listener metadata: log material changes so reload
330        // observability is not silent. Socket topology is unchanged.
331        if old.protocols != new.protocols {
332            tracing::info!(
333                listener = %old.name,
334                old_protocols = ?old.protocols,
335                new_protocols = ?new.protocols,
336                "listener protocols changed via hot reload"
337            );
338        }
339        // Auth material: compare presence and non-secret fields only.
340        let old_auth_present = old.auth.is_some();
341        let new_auth_present = new.auth.is_some();
342        if old_auth_present != new_auth_present {
343            tracing::info!(
344                listener = %old.name,
345                old_has_auth = old_auth_present,
346                new_has_auth = new_auth_present,
347                "listener auth presence changed via hot reload"
348            );
349        } else if let (Some(old_a), Some(new_a)) = (&old.auth, &new.auth) {
350            if old_a.auth_type != new_a.auth_type || old_a.username != new_a.username {
351                tracing::info!(
352                    listener = %old.name,
353                    "listener auth material changed via hot reload"
354                );
355            }
356        }
357        if old.tls.is_some() != new.tls.is_some() {
358            tracing::info!(
359                listener = %old.name,
360                old_has_tls = old.tls.is_some(),
361                new_has_tls = new.tls.is_some(),
362                "listener TLS presence changed via hot reload"
363            );
364        }
365        if old.shadowsocks.is_some() != new.shadowsocks.is_some()
366            || old.trojan.is_some() != new.trojan.is_some()
367        {
368            tracing::info!(
369                listener = %old.name,
370                "listener shadowsocks/trojan presence changed via hot reload"
371            );
372        }
373        if old.connection_limit != new.connection_limit
374            || old.fixed_target != new.fixed_target
375            || old.local_bind != new.local_bind
376        {
377            tracing::info!(
378                listener = %old.name,
379                "listener connection_limit/fixed_target/local_bind changed via hot reload"
380            );
381        }
382    }
383
384    Ok(())
385}
386
387pub fn classify_reload_config(
388    old_listeners: &[eggress_config::compile::ListenerConfig],
389    old_timeouts: &eggress_config::compile::TimeoutConfig,
390    old_admin: Option<&eggress_config::compile::AdminConfig>,
391    new_config: &eggress_config::compile::RuntimeConfig,
392) -> Result<(), String> {
393    classify_listeners(old_listeners, &new_config.listeners)?;
394    if old_timeouts != &new_config.timeouts {
395        return Err("timeout configuration changed; restart required".to_string());
396    }
397
398    let old_admin_endpoint = old_admin.map(|admin| (admin.enabled, admin.bind.as_str()));
399    let new_admin_endpoint = new_config
400        .admin
401        .as_ref()
402        .map(|admin| (admin.enabled, admin.bind.as_str()));
403    if old_admin_endpoint != new_admin_endpoint {
404        return Err("admin endpoint bind configuration changed; restart required".to_string());
405    }
406
407    Ok(())
408}
409
410struct PreparedListener {
411    name: String,
412    #[allow(dead_code)] // used only with `operations` feature
413    bind: String,
414    protocols: Vec<ProtocolId>,
415    listener: TcpListener,
416    local_addr: std::net::SocketAddr,
417    auth: eggress_server::accept::InboundAuthentication,
418    handshake_timeout: Duration,
419    udp: Option<eggress_config::compile::CompiledListenerUdpConfig>,
420    tls: Option<eggress_config::compile::CompiledListenerTlsConfig>,
421    shadowsocks: Option<eggress_config::model::ShadowsocksListenerConfig>,
422    trojan: Option<eggress_config::model::ListenerTrojanConfig>,
423    fixed_target: Option<eggress_core::TargetAddr>,
424    local_bind: Option<String>,
425}
426
427#[cfg(feature = "quic")]
428struct PreparedQuicListener {
429    name: String,
430    protocols: Vec<ProtocolId>,
431    listener: Arc<eggress_transport_quic::QuicListener>,
432    local_addr: std::net::SocketAddr,
433    auth: eggress_server::accept::InboundAuthentication,
434    handshake_timeout: Duration,
435    connection_limit: u64,
436}
437
438#[cfg(feature = "extended")]
439type PreparedShadowsocksUdpRelay = (
440    Arc<tokio::net::UdpSocket>,
441    eggress_udp::standalone_shadowsocks::ShadowsocksStandaloneUdpConfig,
442);
443
444#[cfg(feature = "extended")]
445async fn prepare_shadowsocks_udp_relay(
446    prepared_listener: &PreparedListener,
447    udp_cfg: &eggress_config::compile::CompiledListenerUdpConfig,
448    routing: Arc<dyn RouteService>,
449    state: &RuntimeState,
450) -> Result<PreparedShadowsocksUdpRelay, RuntimeError> {
451    let ss = prepared_listener.shadowsocks.as_ref().ok_or_else(|| {
452        RuntimeError::Other(format!(
453            "listener '{}' shadowsocks_udp mode requires shadowsocks config",
454            prepared_listener.name
455        ))
456    })?;
457    let method =
458        eggress_protocol_shadowsocks::CipherMethod::parse_method(&ss.method).map_err(|e| {
459            RuntimeError::Other(format!(
460                "listener '{}' has invalid shadowsocks method '{}': {}",
461                prepared_listener.name, ss.method, e
462            ))
463        })?;
464    let socket = Arc::new(
465        tokio::net::UdpSocket::bind(udp_cfg.bind)
466            .await
467            .map_err(|e| RuntimeError::ListenerBind {
468                addr: udp_cfg.bind.to_string(),
469                source: e,
470            })?,
471    );
472    let local_addr = socket
473        .local_addr()
474        .map_err(|e| RuntimeError::ListenerBind {
475            addr: udp_cfg.bind.to_string(),
476            source: e,
477        })?;
478    tracing::info!(
479        "shadowsocks UDP relay listening on {local_addr} ({})",
480        prepared_listener.name
481    );
482
483    let relay_config = eggress_udp::standalone_shadowsocks::ShadowsocksStandaloneUdpConfig {
484        routing,
485        udp_metrics: state.udp_metrics.clone(),
486        shadowsocks_metrics: Some(state.shadowsocks_metrics.clone()),
487        limits: eggress_udp::limits::UdpLimits::from_listener_config(
488            udp_cfg.max_associations_global,
489            udp_cfg.max_associations,
490            udp_cfg.max_targets_per_association,
491            udp_cfg.max_datagram_size,
492            udp_cfg.idle_timeout,
493            udp_cfg.client_pin,
494            udp_cfg.target_idle_timeout,
495        ),
496        listener: prepared_listener.name.clone(),
497        generation: state.snapshot.load().generation,
498        method,
499        password: ss.password.clone(),
500        allow_private_egress: udp_cfg.allow_private_egress,
501    };
502
503    Ok((socket, relay_config))
504}
505
506/// Compute the advertised IP for the SOCKS5 UDP ASSOCIATE reply.
507///
508/// Derivation rules:
509/// 1. If `advertise` is configured, use it.
510/// 2. Else if UDP bind IP is not unspecified, use UDP bind IP.
511/// 3. Else if TCP peer is loopback, use loopback matching address family.
512/// 4. Else return a config error requiring explicit `advertise`.
513fn compute_advertise_ip(
514    configured_advertise: Option<std::net::IpAddr>,
515    udp_bind_ip: std::net::IpAddr,
516    tcp_peer: Option<std::net::SocketAddr>,
517) -> Result<std::net::IpAddr, eggress_udp::error::UdpError> {
518    if let Some(ip) = configured_advertise {
519        return Ok(ip);
520    }
521
522    if !udp_bind_ip.is_unspecified() {
523        return Ok(udp_bind_ip);
524    }
525
526    if let Some(tcp_peer) = tcp_peer {
527        let peer_is_loopback = tcp_peer.ip().is_loopback()
528            || matches!(
529                tcp_peer.ip(),
530                std::net::IpAddr::V6(ipv6)
531                    if ipv6
532                        .to_ipv4_mapped()
533                        .is_some_and(|ipv4| ipv4.is_loopback())
534            );
535        if peer_is_loopback {
536            // Preserve the family selected by the unspecified UDP bind. This
537            // matters for dual-stack sockets and IPv4-mapped IPv6 peers.
538            match udp_bind_ip {
539                std::net::IpAddr::V4(_) => {
540                    return Ok(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
541                }
542                std::net::IpAddr::V6(_) => {
543                    return Ok(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
544                }
545            }
546        }
547    }
548
549    Err(eggress_udp::error::UdpError::Other(
550        "UDP relay requires explicit advertise address when bind is unspecified and client is not loopback".to_string()
551    ))
552}
553
554struct RuntimeUdpService {
555    _listener_name: String,
556    udp_config: eggress_config::compile::CompiledListenerUdpConfig,
557    registry: Arc<eggress_udp::registry::UdpAssociationRegistry>,
558    metrics: Arc<dyn eggress_server::SessionMetrics>,
559    udp_metrics: Arc<eggress_udp::metrics::UdpMetrics>,
560    routing: Arc<SharedRoutingService>,
561    udp_tasks: TaskTracker,
562}
563
564impl eggress_server::UdpService for RuntimeUdpService {
565    fn create_association(
566        &self,
567        listener: &str,
568        client_tcp_peer: Option<std::net::SocketAddr>,
569        identity: eggress_core::ClientIdentity,
570        generation: u64,
571    ) -> std::pin::Pin<
572        Box<
573            dyn std::future::Future<
574                    Output = Result<
575                        eggress_server::UdpAssociationHandle,
576                        eggress_udp::error::UdpError,
577                    >,
578                > + Send
579                + 'static,
580        >,
581    > {
582        let registry = self.registry.clone();
583        let metrics = self.metrics.clone();
584        let udp_metrics = self.udp_metrics.clone();
585        let routing = self.routing.clone();
586        let udp_tasks = self.udp_tasks.clone();
587        let udp_config = self.udp_config.clone();
588        let listener = listener.to_string();
589        Box::pin(async move {
590            let assoc = registry
591                .create_association(&listener, client_tcp_peer, identity, generation)
592                .await?;
593            metrics.record_udp_association_created();
594
595            let relay_socket =
596                std::sync::Arc::new(tokio::net::UdpSocket::bind(udp_config.bind).await?);
597            let local_addr = relay_socket.local_addr()?;
598
599            let advertised_ip =
600                compute_advertise_ip(udp_config.advertise, local_addr.ip(), client_tcp_peer)?;
601            let relay_addr = std::net::SocketAddr::new(advertised_ip, local_addr.port());
602
603            let relay_config = eggress_udp::relay::RelayConfig {
604                routing: routing as Arc<dyn RouteService>,
605                udp_metrics: udp_metrics.clone(),
606                limits: eggress_udp::limits::UdpLimits::from_listener_config(
607                    udp_config.max_associations_global,
608                    udp_config.max_associations,
609                    udp_config.max_targets_per_association,
610                    udp_config.max_datagram_size,
611                    udp_config.idle_timeout,
612                    udp_config.client_pin,
613                    udp_config.target_idle_timeout,
614                ),
615                listener: listener.clone(),
616                generation,
617                identity: assoc.meta.identity.clone(),
618                client_tcp_peer,
619                registry: registry.clone(),
620                allow_private_egress: udp_config.allow_private_egress,
621                upstream_connect_timeout: udp_config.upstream_connect_timeout,
622                upstream_udp_bind: udp_config.upstream_udp_bind,
623            };
624
625            let relay_assoc = assoc.clone();
626            let relay_cancel = assoc.cancel.clone();
627            let assoc_id = assoc.id;
628            let relay_udp_metrics = udp_metrics.clone();
629            udp_tasks.spawn(async move {
630                let result = eggress_udp::relay::udp_relay_loop(
631                    relay_socket,
632                    relay_assoc,
633                    relay_config,
634                    relay_cancel,
635                )
636                .await;
637                if let Err(error) = result {
638                    relay_udp_metrics.record_association_failure();
639                    tracing::warn!(
640                        %error,
641                        association_id = ?assoc_id,
642                        "UDP relay ended with error"
643                    );
644                }
645            });
646
647            Ok(eggress_server::UdpAssociationHandle {
648                id: assoc.id,
649                relay_addr,
650                cancel: assoc.cancel.clone(),
651            })
652        })
653    }
654
655    fn is_enabled(&self) -> bool {
656        self.udp_config.enabled
657    }
658
659    fn active_count(
660        &self,
661    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = usize> + Send + 'static>> {
662        let registry = self.registry.clone();
663        Box::pin(async move { registry.active_count().await })
664    }
665}
666
667pub struct RuntimeState {
668    pub snapshot: Arc<ArcSwap<CompiledRuntimeSnapshot>>,
669    pub routing: Arc<SharedRoutingService>,
670    pub metrics: Arc<dyn eggress_server::SessionMetrics>,
671    pub readiness: Arc<AtomicBool>,
672    pub start_time: Instant,
673    pub active_connections: Arc<AtomicU64>,
674    pub connection_counter: Arc<AtomicU64>,
675    pub admin_local_addr: Arc<Mutex<Option<std::net::SocketAddr>>>,
676    pub listener_addrs: Arc<Mutex<Vec<Option<std::net::SocketAddr>>>>,
677    #[cfg(feature = "operations")]
678    admin_snapshot: Arc<ArcSwap<RuntimeAdminState>>,
679    pub health: Arc<Mutex<Option<HealthManager>>>,
680    pub health_cancel: CancellationToken,
681    pub health_runtime: Mutex<Option<tokio::runtime::Handle>>,
682    pub udp_registry: Arc<eggress_udp::registry::UdpAssociationRegistry>,
683    pub udp_metrics: Arc<eggress_udp::metrics::UdpMetrics>,
684    #[cfg(feature = "extended")]
685    pub shadowsocks_metrics: Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>,
686    pub udp_tasks: TaskTracker,
687    pub transparent_accepted_total: Arc<AtomicU64>,
688    pub transparent_original_dst_failed_total: Arc<AtomicU64>,
689    #[cfg(feature = "reverse")]
690    pub reverse_registry: Arc<eggress_admin::ReverseRegistry>,
691    #[cfg(feature = "reverse")]
692    pub reverse_metrics: Arc<eggress_protocol_reverse::metrics::ReverseMetrics>,
693}
694
695impl RuntimeState {
696    pub fn generation(&self) -> u64 {
697        self.snapshot.load().generation
698    }
699
700    #[cfg(feature = "operations")]
701    fn publish_admin_snapshot(&self, snapshot: Arc<CompiledRuntimeSnapshot>) {
702        let listener_addrs = self.admin_snapshot.load().listener_addrs.clone();
703        self.admin_snapshot.store(Arc::new(RuntimeAdminState {
704            snapshot,
705            listener_addrs,
706        }));
707    }
708
709    #[cfg(feature = "operations")]
710    fn publish_admin_listener_addrs(
711        &self,
712        snapshot: Arc<CompiledRuntimeSnapshot>,
713        listener_addrs: Vec<Option<std::net::SocketAddr>>,
714    ) {
715        self.admin_snapshot.store(Arc::new(RuntimeAdminState {
716            snapshot,
717            listener_addrs,
718        }));
719    }
720
721    /// Restart health probes for the upstreams in the current snapshot.
722    pub fn restart_health_probes(&self) {
723        let mut guard = self.health.lock().unwrap_or_else(|error| {
724            tracing::warn!("health manager state was poisoned; resetting it: {error}");
725            let mut guard = error.into_inner();
726            *guard = None;
727            self.health.clear_poison();
728            guard
729        });
730        if let Some(ref mut health) = *guard {
731            health.stop_all();
732        }
733        let upstreams: Vec<Arc<UpstreamRuntime>> =
734            self.snapshot.load().upstreams.values().cloned().collect();
735        if !upstreams.is_empty() {
736            let mut health = HealthManager::new(self.health_cancel.clone());
737            if let Some(handle) = self
738                .health_runtime
739                .lock()
740                .unwrap_or_else(|error| error.into_inner())
741                .clone()
742            {
743                health.start_probes_on(&handle, &upstreams);
744            }
745            *guard = Some(health);
746        } else {
747            *guard = None;
748        }
749    }
750}
751
752#[allow(dead_code)]
753pub struct ServiceSupervisor {
754    config_path: Option<String>,
755    state: Arc<RuntimeState>,
756    metrics_registry: Arc<eggress_metrics::MetricsRegistry>,
757    cancel: CancellationToken,
758    listener_cancel: CancellationToken,
759    connection_cancel: CancellationToken,
760    health_cancel: CancellationToken,
761    admin_cancel: CancellationToken,
762    health: Arc<Mutex<Option<HealthManager>>>,
763    tasks: TaskTracker,
764    connection_tasks: TaskTracker,
765    admin_tasks: TaskTracker,
766    shutdown_grace: Duration,
767    rt_config: eggress_config::compile::RuntimeConfig,
768    tls_client_config: Option<std::sync::Arc<rustls::ClientConfig>>,
769    #[cfg(feature = "ssh")]
770    ssh_sessions: Arc<eggress_transport_ssh::SshSessionCache>,
771    compatibility_options: CompatibilityOptions,
772}
773
774/// Explicit options used only by the pproxy compatibility executable.
775/// Native configuration startup always uses the default value.
776#[derive(Debug, Clone, Default)]
777pub struct CompatibilityOptions {
778    /// Whether the caller is the explicitly opt-in pproxy compatibility executable.
779    pub compatibility_mode: bool,
780    pub auth_timeout: Option<Duration>,
781    pub system_proxy: bool,
782    /// Compatibility-only debug propagation. Native Eggress never enables it.
783    pub debug: bool,
784    /// Compatibility verbosity count, used for human-readable session and
785    /// traffic events backed by the normal session reports.
786    pub verbose_level: u8,
787}
788
789impl ServiceSupervisor {
790    pub fn start(config_path: &str) -> Result<Self, RuntimeError> {
791        let (rt_config, warnings) = eggress_config::load_and_validate_with_warnings(config_path)
792            .map_err(|e| RuntimeError::Config(e.to_string()))?;
793
794        for warning in &warnings {
795            tracing::warn!("config security warning: {warning}");
796        }
797
798        Self::init_with_config(
799            rt_config,
800            Some(config_path.to_string()),
801            CompatibilityOptions::default(),
802        )
803    }
804
805    /// Start from an already-validated [`RuntimeConfig`] without reading a file.
806    ///
807    /// When `config_path` is provided (file-backed startup), SIGHUP reload
808    /// is enabled. When `None` (in-memory/compatibility startup), SIGHUP
809    /// reload is disabled because there is no stable user-authored config
810    /// file to reload from.
811    pub fn start_from_config(
812        rt_config: eggress_config::compile::RuntimeConfig,
813        config_path: Option<String>,
814    ) -> Result<Self, RuntimeError> {
815        Self::init_with_config(rt_config, config_path, CompatibilityOptions::default())
816    }
817
818    pub fn start_from_config_with_options(
819        rt_config: eggress_config::compile::RuntimeConfig,
820        config_path: Option<String>,
821        compatibility_options: CompatibilityOptions,
822    ) -> Result<Self, RuntimeError> {
823        Self::init_with_config(rt_config, config_path, compatibility_options)
824    }
825
826    fn init_with_config(
827        rt_config: eggress_config::compile::RuntimeConfig,
828        config_path: Option<String>,
829        compatibility_options: CompatibilityOptions,
830    ) -> Result<Self, RuntimeError> {
831        #[cfg(not(feature = "reverse"))]
832        if !rt_config.reverse_servers.is_empty() || !rt_config.reverse_clients.is_empty() {
833            return Err(RuntimeError::Other(
834                "reverse proxy support not included in this build".to_string(),
835            ));
836        }
837
838        #[cfg(not(feature = "operations"))]
839        if rt_config.admin.as_ref().is_some_and(|a| a.enabled) {
840            return Err(RuntimeError::Other(
841                "admin server support not included in this build; \
842                 enable the 'operations' feature or remove [admin] from config"
843                    .to_string(),
844            ));
845        }
846
847        for lcfg in &rt_config.listeners {
848            if lcfg.unix.is_none() {
849                let _bind_addr: std::net::SocketAddr =
850                    lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
851                        addr: lcfg.bind.clone(),
852                        source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
853                    })?;
854            }
855        }
856
857        let udp_metrics = Arc::new(eggress_udp::metrics::UdpMetrics::new());
858        #[cfg(feature = "extended")]
859        let shadowsocks_metrics = Arc::new(eggress_protocol_shadowsocks::ShadowsocksMetrics::new());
860
861        let metrics_registry = Arc::new(eggress_metrics::MetricsRegistry::new());
862        metrics_registry.set_udp_metrics(udp_metrics.clone());
863        #[cfg(feature = "extended")]
864        {
865            metrics_registry.set_shadowsocks_metrics(shadowsocks_metrics.clone());
866        }
867        let metrics: Arc<dyn eggress_server::SessionMetrics> = metrics_registry.clone();
868        let readiness = Arc::new(AtomicBool::new(false));
869
870        let snapshot = compile_runtime_snapshot(&rt_config, None)
871            .map_err(|e| RuntimeError::Config(e.to_string()))?;
872        let snapshot = Arc::new(ArcSwap::from_pointee(snapshot));
873
874        let routing = Arc::new(SharedRoutingService::new_arc(
875            snapshot.load().router.clone(),
876        ));
877
878        let active_connections = Arc::new(AtomicU64::new(0));
879        let connection_counter = Arc::new(AtomicU64::new(1));
880
881        let mut udp_global_limit: Option<usize> = None;
882        for listener in &rt_config.listeners {
883            if let Some(udp) = &listener.udp {
884                let value = udp.max_associations_global;
885                match udp_global_limit {
886                    None => udp_global_limit = Some(value),
887                    Some(existing) if existing != value => {
888                        tracing::warn!(
889                            listener = %listener.name,
890                            existing,
891                            other = value,
892                            "multiple listeners specify udp.max_associations_global with different values; using first"
893                        );
894                    }
895                    Some(_) => {}
896                }
897            }
898        }
899        let udp_global_limit = udp_global_limit.unwrap_or(1024);
900
901        let udp_registry = Arc::new(eggress_udp::registry::UdpAssociationRegistry::new(
902            eggress_udp::limits::UdpLimits {
903                max_associations_global: udp_global_limit,
904                ..Default::default()
905            },
906        ));
907
908        let cancel = CancellationToken::new();
909        let listener_cancel = CancellationToken::new();
910        let connection_cancel = CancellationToken::new();
911        let health_cancel = CancellationToken::new();
912        let admin_cancel = CancellationToken::new();
913        let health = Arc::new(Mutex::new(if snapshot.load().upstreams.is_empty() {
914            None
915        } else {
916            Some(HealthManager::new(health_cancel.clone()))
917        }));
918
919        #[cfg(feature = "reverse")]
920        let reverse_metrics = Arc::new(eggress_protocol_reverse::metrics::ReverseMetrics::new());
921        let udp_tasks = TaskTracker::new();
922
923        let state = Arc::new(RuntimeState {
924            snapshot: snapshot.clone(),
925            routing: routing.clone(),
926            metrics: metrics.clone(),
927            readiness,
928            start_time: Instant::now(),
929            active_connections,
930            connection_counter,
931            admin_local_addr: Arc::new(Mutex::new(None)),
932            listener_addrs: Arc::new(Mutex::new(Vec::new())),
933            #[cfg(feature = "operations")]
934            admin_snapshot: Arc::new(ArcSwap::from_pointee(RuntimeAdminState {
935                snapshot: snapshot.load_full(),
936                listener_addrs: Vec::new(),
937            })),
938            health: health.clone(),
939            health_cancel: health_cancel.clone(),
940            health_runtime: Mutex::new(None),
941            udp_registry,
942            udp_metrics,
943            #[cfg(feature = "extended")]
944            shadowsocks_metrics,
945            udp_tasks: udp_tasks.clone(),
946            transparent_accepted_total: Arc::new(AtomicU64::new(0)),
947            transparent_original_dst_failed_total: Arc::new(AtomicU64::new(0)),
948            #[cfg(feature = "reverse")]
949            reverse_registry: Arc::new(eggress_admin::ReverseRegistry::new()),
950            #[cfg(feature = "reverse")]
951            reverse_metrics,
952        });
953
954        // Bridge transparent proxy atomics to MetricsRegistry for /metrics
955        metrics_registry.set_transparent_counters(
956            state.transparent_accepted_total.clone(),
957            state.transparent_original_dst_failed_total.clone(),
958        );
959
960        #[cfg(feature = "ssh")]
961        let ssh_sessions = Arc::new(if compatibility_options.compatibility_mode {
962            let insecure_acknowledged = std::env::var("EGRESS_SSH_INSECURE_HOST_KEYS")
963                .map(|value| matches!(value.as_str(), "1" | "true" | "yes"))
964                .unwrap_or(false);
965            if insecure_acknowledged {
966                eggress_transport_ssh::SshSessionCache::new_compatibility()
967            } else {
968                tracing::warn!(
969                    "compatibility mode would disable SSH host-key verification; \
970                     keeping known_hosts verification enabled. To explicitly \
971                     accept unverified SSH host keys (MITM risk), set \
972                     EGRESS_SSH_INSECURE_HOST_KEYS=1"
973                );
974                eggress_transport_ssh::SshSessionCache::new()
975            }
976        } else {
977            eggress_transport_ssh::SshSessionCache::new()
978        });
979
980        let tasks = TaskTracker::new();
981        let connection_tasks = TaskTracker::new();
982
983        let shutdown_grace = rt_config.process.shutdown_grace;
984
985        Ok(ServiceSupervisor {
986            config_path,
987            state,
988            metrics_registry,
989            cancel,
990            listener_cancel,
991            connection_cancel,
992            health_cancel,
993            admin_cancel,
994            health: health.clone(),
995            tasks,
996            connection_tasks,
997            admin_tasks: TaskTracker::new(),
998            shutdown_grace,
999            rt_config,
1000            tls_client_config: None,
1001            #[cfg(feature = "ssh")]
1002            ssh_sessions,
1003            compatibility_options,
1004        })
1005    }
1006
1007    pub fn state(&self) -> &Arc<RuntimeState> {
1008        &self.state
1009    }
1010
1011    pub fn shutdown_token(&self) -> CancellationToken {
1012        self.cancel.clone()
1013    }
1014
1015    /// Override the TLS client config used for upstream connections (e.g., Trojan).
1016    /// Intended for test-only use (e.g., insecure TLS for self-signed certs).
1017    #[allow(dead_code)]
1018    pub fn with_tls_client_config(mut self, config: std::sync::Arc<rustls::ClientConfig>) -> Self {
1019        self.tls_client_config = Some(config);
1020        self
1021    }
1022
1023    /// Classify whether a reload is supported given old and new listener configs.
1024    /// Returns `Ok(())` if the reload is safe, or `Err(reason)` if it should be rejected.
1025    ///
1026    /// UDP-specific reload semantics:
1027    /// - UDP bind changes are restart-required.
1028    /// - UDP advertise address changes are restart-required if socket bind changes.
1029    /// - UDP limits apply to new associations only; existing ones keep their limits.
1030    /// - Route changes apply immediately to future UDP packets.
1031    fn classify_reload(
1032        &self,
1033        new_config: &eggress_config::compile::RuntimeConfig,
1034    ) -> Result<(), String> {
1035        classify_reload_config(
1036            &self.rt_config.listeners,
1037            &self.rt_config.timeouts,
1038            self.rt_config.admin.as_ref(),
1039            new_config,
1040        )
1041    }
1042
1043    /// Attempt to reload configuration. Encapsulates the full reload transaction:
1044    /// 1. Load and compile new config
1045    /// 2. Classify unsupported changes (reject if listener topology changed)
1046    /// 3. Build new snapshot with previous snapshot for Arc reuse
1047    /// 4. Update stored rt_config for subsequent reloads
1048    /// 5. Atomically swap routing and snapshot
1049    pub fn reload_config(&mut self) -> ReloadResult {
1050        let config_path = match self.config_path {
1051            Some(ref p) => p.clone(),
1052            None => {
1053                return ReloadResult::Rejected {
1054                    reason: "no config file path available for reload".to_string(),
1055                };
1056            }
1057        };
1058        let new_rt_config = match eggress_config::compile::load_and_compile(&config_path) {
1059            Ok(c) => c,
1060            Err(e) => {
1061                return ReloadResult::Failed {
1062                    error: format!("config load: {e}"),
1063                };
1064            }
1065        };
1066
1067        if let Err(reason) = self.classify_reload(&new_rt_config) {
1068            return ReloadResult::Rejected { reason };
1069        }
1070
1071        let prev_snapshot = self.state.snapshot.load();
1072        let prev_ref: Option<&CompiledRuntimeSnapshot> = Some(&prev_snapshot);
1073        let new_snapshot = match compile_runtime_snapshot(&new_rt_config, prev_ref) {
1074            Ok(s) => s,
1075            Err(e) => {
1076                return ReloadResult::Failed {
1077                    error: format!("snapshot build: {e}"),
1078                };
1079            }
1080        };
1081
1082        let upstream_count = new_snapshot.upstreams.len();
1083        let gen = new_snapshot.generation;
1084
1085        // Keep the configuration used by the next reload in sync before making
1086        // the new snapshot visible to readers.
1087        self.rt_config = new_rt_config;
1088
1089        // Snapshot must be published before the router swap. Readers that observe
1090        // the new generation via `snapshot.load()` pull the router from that
1091        // same snapshot Arc, so any reader seeing the new generation also sees
1092        // the router that belongs to it.
1093        let new_snapshot = Arc::new(new_snapshot);
1094        self.state.snapshot.store(new_snapshot.clone());
1095        self.state.routing.swap_arc(new_snapshot.router.clone());
1096        #[cfg(feature = "operations")]
1097        self.state.publish_admin_snapshot(new_snapshot.clone());
1098
1099        self.state.restart_health_probes();
1100        eggress_protocol_http::H2_POOL_REGISTRY.clear();
1101
1102        ReloadResult::Applied {
1103            generation: gen,
1104            upstreams: upstream_count,
1105        }
1106    }
1107
1108    pub fn run(&mut self) -> Result<(), RuntimeError> {
1109        #[allow(unused_variables)]
1110        let config_path = self.config_path.clone().unwrap_or_default();
1111        let routing = self.state.routing.clone();
1112        let listener_cancel = self.listener_cancel.clone();
1113        let connection_cancel = self.connection_cancel.clone();
1114        let health_cancel = self.health_cancel.clone();
1115        let admin_cancel = self.admin_cancel.clone();
1116        let cancel = self.cancel.clone();
1117        #[allow(unused_variables)]
1118        let metrics = self.state.metrics.clone();
1119        let readiness = self.state.readiness.clone();
1120        #[cfg(feature = "operations")]
1121        let admin_state_ref = self.state.clone();
1122        let active_connections = self.state.active_connections.clone();
1123        let shutdown_grace = self.shutdown_grace;
1124        let tasks = self.tasks.clone();
1125        let connection_tasks = self.connection_tasks.clone();
1126        let admin_tasks = self.admin_tasks.clone();
1127        let health_for_run = self.health.clone();
1128        let health_clone = health_for_run.clone();
1129        let snapshot = self.state.snapshot.clone();
1130        let state_ref = self.state.clone();
1131        let rt_config = self.rt_config.clone();
1132        let tls_client_config = self.tls_client_config.clone();
1133        let compatibility_options = self.compatibility_options.clone();
1134        #[cfg(feature = "ssh")]
1135        let ssh_sessions = self.ssh_sessions.clone();
1136
1137        let handshake_timeout = rt_config.timeouts.handshake;
1138        let connect_timeout = rt_config.timeouts.connect;
1139
1140        #[cfg(feature = "operations")]
1141        let listener_infos_provider: Arc<RuntimeAdminListenerInfos> =
1142            Arc::new(RuntimeAdminListenerInfos {
1143                state: admin_state_ref.clone(),
1144            });
1145
1146        #[cfg(feature = "operations")]
1147        let metrics_registry_for_admin = self.metrics_registry.clone();
1148
1149        let run_async = async move {
1150            if let Ok(mut runtime) = state_ref.health_runtime.lock() {
1151                *runtime = Some(tokio::runtime::Handle::current());
1152            }
1153            #[cfg(feature = "operations")]
1154            let mut compatibility_system_proxy: Option<
1155                eggress_system_proxy::AppliedProxy,
1156            > = None;
1157
1158            #[cfg(feature = "operations")]
1159            let metrics_registry = metrics_registry_for_admin;
1160            // Start health probes inside the runtime context
1161            {
1162                let mut guard = match health_for_run.lock() {
1163                    Ok(guard) => guard,
1164                    Err(error) => {
1165                        tracing::warn!("health manager state was poisoned; resetting it: {error}");
1166                        let mut guard = error.into_inner();
1167                        *guard = None;
1168                        health_for_run.clear_poison();
1169                        guard
1170                    }
1171                };
1172                if let Some(ref mut hm) = *guard {
1173                    let upstream_runtimes: Vec<Arc<UpstreamRuntime>> =
1174                        snapshot.load().upstreams.values().cloned().collect();
1175                    if !upstream_runtimes.is_empty() {
1176                        hm.start_probes(&upstream_runtimes);
1177                    }
1178                }
1179            }
1180
1181            let current_snapshot = snapshot.load();
1182            let listener_configs = current_snapshot.listeners.clone();
1183            #[cfg(feature = "operations")]
1184            let admin_config = current_snapshot.admin.clone();
1185            drop(current_snapshot);
1186
1187            if listener_configs.is_empty() {
1188                tracing::warn!("no listeners configured; the proxy will not accept connections");
1189            }
1190
1191            let mut prepared = Vec::new();
1192            #[cfg(feature = "quic")]
1193            let mut prepared_quic = Vec::<PreparedQuicListener>::new();
1194            let compatibility_auth_reuse = compatibility_options
1195                .auth_timeout
1196                .map(eggress_server::accept::AuthReuseCache::new)
1197                .map(Arc::new);
1198            #[cfg(unix)]
1199            let mut unix_listener_args = Vec::new();
1200            let mut transparent_listener_args = Vec::new();
1201
1202            for lcfg in &listener_configs {
1203                let protocols: Vec<ProtocolId> = lcfg.protocols.to_vec();
1204
1205                let auth = match &lcfg.auth {
1206                    Some(auth_cfg) => {
1207                        if auth_cfg.auth_type == "password" {
1208                            let username = auth_cfg.username.clone().unwrap_or_default();
1209                            let password = auth_cfg.password.clone().unwrap_or_default();
1210                            if let Some(reuse) = compatibility_auth_reuse.clone() {
1211                                eggress_server::accept::InboundAuthentication::UsernamePasswordWithReuse {
1212                                    username,
1213                                    password,
1214                                    reuse,
1215                                }
1216                            } else {
1217                                eggress_server::accept::InboundAuthentication::UsernamePassword {
1218                                    username,
1219                                    password,
1220                                }
1221                            }
1222                        } else {
1223                            eggress_server::accept::InboundAuthentication::None
1224                        }
1225                    }
1226                    None => eggress_server::accept::InboundAuthentication::None,
1227                };
1228
1229                let connection_limit = lcfg.connection_limit.unwrap_or(1024) as usize;
1230
1231                // Handle Unix domain socket listeners separately
1232                #[allow(unused_variables)]
1233                if let Some(ref unix_cfg) = lcfg.unix {
1234                    #[cfg(unix)]
1235                    {
1236                        match eggress_server::listener::unix::create_unix_listener(
1237                            &eggress_server::listener::unix::UnixListenerConfig::from_compiled(
1238                                &unix_cfg.path,
1239                                unix_cfg.unlink_existing,
1240                                Some(unix_cfg.mode),
1241                            ),
1242                        ) {
1243                            Ok(unix_listener) => {
1244                                tracing::info!(
1245                                    "unix socket listener created at {} ({})",
1246                                    unix_cfg.path.display(),
1247                                    lcfg.name
1248                                );
1249                                unix_listener_args.push((
1250                                    lcfg.name.clone(),
1251                                    unix_listener,
1252                                    protocols,
1253                                    auth,
1254                                    handshake_timeout,
1255                                    connection_limit as u64,
1256                                    lcfg.tls.clone(),
1257                                    lcfg.shadowsocks.clone(),
1258                                    lcfg.trojan.clone(),
1259                                    lcfg.udp.clone(),
1260                                ));
1261                                continue;
1262                            }
1263                            Err(e) => {
1264                                tracing::error!(
1265                                    "failed to bind unix socket at {} for listener '{}': {e}",
1266                                    unix_cfg.path.display(),
1267                                    lcfg.name
1268                                );
1269                                continue;
1270                            }
1271                        }
1272                    }
1273                    #[cfg(not(unix))]
1274                    {
1275                        tracing::error!(
1276                            "unix socket listener '{}' skipped: not supported on this platform",
1277                            lcfg.name
1278                        );
1279                        continue;
1280                    }
1281                }
1282
1283                // Handle transparent TCP listeners
1284                if let Some(ref transparent_cfg) = lcfg.transparent {
1285                    if transparent_cfg.enabled {
1286                        let capability = check_capability(PlatformCapability::LinuxOriginalDstIpv4);
1287                        if capability != crate::platform::CapabilityStatus::Available {
1288                            #[cfg(feature = "operations")]
1289                            state_ref.metrics.record_platform_capability_check_failure();
1290                            let _cap_span = tracing::info_span!(
1291                                "capability_check_failed",
1292                                capability = %PlatformCapability::LinuxOriginalDstIpv4,
1293                                status = %capability,
1294                                listener = %lcfg.name,
1295                            );
1296                            tracing::warn!(
1297                                "transparent proxy not available for listener '{}' ({}); \
1298                                 falling back to normal TCP listener",
1299                                lcfg.name,
1300                                capability
1301                            );
1302                        } else {
1303                            let bind_addr: std::net::SocketAddr =
1304                                lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
1305                                    addr: lcfg.bind.clone(),
1306                                    source: std::io::Error::new(
1307                                        std::io::ErrorKind::InvalidInput,
1308                                        e,
1309                                    ),
1310                                })?;
1311
1312                            let transparent_listener =
1313                                eggress_server::listener::transparent::TransparentListener::bind(
1314                                    &bind_addr.to_string(),
1315                                )
1316                                .await
1317                                .map_err(|e| {
1318                                    RuntimeError::ListenerBind {
1319                                        addr: lcfg.bind.clone(),
1320                                        source: e,
1321                                    }
1322                                })?;
1323
1324                            let local_addr = transparent_listener.local_addr().map_err(|e| {
1325                                RuntimeError::ListenerBind {
1326                                    addr: lcfg.bind.clone(),
1327                                    source: e,
1328                                }
1329                            })?;
1330
1331                            tracing::info!(
1332                                "transparent TCP listener listening on {local_addr} ({})",
1333                                lcfg.name
1334                            );
1335
1336                            transparent_listener_args.push((
1337                                lcfg.name.clone(),
1338                                transparent_listener,
1339                                protocols,
1340                                auth,
1341                                handshake_timeout,
1342                                connection_limit as u64,
1343                                lcfg.tls.clone(),
1344                                lcfg.shadowsocks.clone(),
1345                                lcfg.trojan.clone(),
1346                                lcfg.udp.clone(),
1347                            ));
1348                            continue;
1349                        }
1350                    }
1351                }
1352
1353                #[cfg(feature = "quic")]
1354                if protocols.contains(&ProtocolId::Quic) || protocols.contains(&ProtocolId::Http3) {
1355                    let tls = lcfg.tls.clone().ok_or_else(|| {
1356                        RuntimeError::Other(format!(
1357                            "QUIC/HTTP3 listener '{}' requires certificate and key material",
1358                            lcfg.name
1359                        ))
1360                    })?;
1361                    let bind_addr: std::net::SocketAddr =
1362                        lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
1363                            addr: lcfg.bind.clone(),
1364                            source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
1365                        })?;
1366                    let listener = eggress_transport_quic::QuicListener::bind(
1367                        bind_addr,
1368                        eggress_transport_quic::QuicServerConfig {
1369                            certificate_pem: tls.cert_pem.clone(),
1370                            private_key_pem: tls.key_pem.clone(),
1371                            idle_timeout: Duration::from_secs(60),
1372                            max_concurrent_streams: lcfg.connection_limit.unwrap_or(1024).max(1),
1373                            alpn_protocols: if protocols.contains(&ProtocolId::Http3) {
1374                                vec![b"h3".to_vec()]
1375                            } else {
1376                                Vec::new()
1377                            },
1378                        },
1379                    )
1380                    .await
1381                    .map_err(|e| RuntimeError::ListenerBind {
1382                        addr: lcfg.bind.clone(),
1383                        source: std::io::Error::other(e.to_string()),
1384                    })?;
1385                    let local_addr =
1386                        listener
1387                            .local_addr()
1388                            .map_err(|e| RuntimeError::ListenerBind {
1389                                addr: lcfg.bind.clone(),
1390                                source: std::io::Error::other(e.to_string()),
1391                            })?;
1392                    tracing::info!("QUIC listening on {local_addr} ({})", lcfg.name);
1393                    prepared_quic.push(PreparedQuicListener {
1394                        name: lcfg.name.clone(),
1395                        protocols,
1396                        listener,
1397                        local_addr,
1398                        auth,
1399                        handshake_timeout,
1400                        connection_limit: lcfg.connection_limit.unwrap_or(1024) as u64,
1401                    });
1402                    continue;
1403                }
1404
1405                // Standard TCP listener path
1406                let bind_addr: std::net::SocketAddr =
1407                    lcfg.bind.parse().map_err(|e| RuntimeError::ListenerBind {
1408                        addr: lcfg.bind.clone(),
1409                        source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
1410                    })?;
1411
1412                let config = TcpListenerConfig {
1413                    bind_addr,
1414                    protocols: protocols.clone(),
1415                    auth_required: false,
1416                    handshake_timeout,
1417                    connection_limit,
1418                };
1419
1420                let listener = TcpListener::new_with_reuse_port(
1421                    &config,
1422                    listener_cancel.clone(),
1423                    lcfg.reuse_port.unwrap_or(false),
1424                )
1425                .await
1426                .map_err(|e| RuntimeError::ListenerBind {
1427                    addr: lcfg.bind.clone(),
1428                    source: e,
1429                })?;
1430                let local_addr = listener
1431                    .local_addr()
1432                    .map_err(|e| RuntimeError::ListenerBind {
1433                        addr: lcfg.bind.clone(),
1434                        source: e,
1435                    })?;
1436                tracing::info!("listening on {local_addr} ({})", lcfg.name);
1437
1438                prepared.push(PreparedListener {
1439                    name: lcfg.name.clone(),
1440                    bind: lcfg.bind.clone(),
1441                    protocols,
1442                    listener,
1443                    local_addr,
1444                    auth,
1445                    handshake_timeout,
1446                    udp: lcfg.udp.clone(),
1447                    tls: lcfg.tls.clone(),
1448                    shadowsocks: lcfg.shadowsocks.clone(),
1449                    trojan: lcfg.trojan.clone(),
1450                    fixed_target: lcfg.fixed_target.clone(),
1451                    local_bind: lcfg.local_bind.clone(),
1452                });
1453            }
1454
1455            #[cfg(feature = "operations")]
1456            {
1457                let listener_infos: Vec<eggress_admin::ListenerInfo> = prepared
1458                    .iter()
1459                    .map(|p| eggress_admin::ListenerInfo {
1460                        name: p.name.clone(),
1461                        bind: p.bind.clone(),
1462                        local_addr: p.local_addr.to_string(),
1463                        protocols: p.protocols.iter().map(|p| p.to_string()).collect(),
1464                        udp_enabled: p.udp.as_ref().is_some_and(|u| u.enabled),
1465                        mode: Some("standard".to_string()),
1466                        capability_status: None,
1467                        original_dst_support: None,
1468                        unix_socket_path: None,
1469                        unix_socket_unlink_existing: None,
1470                    })
1471                    .collect();
1472                drop(listener_infos);
1473            }
1474
1475            // Store listener addresses for admin snapshot, indexed by config order.
1476            // Build a lookup map from all listener types (standard, transparent, unix).
1477            {
1478                let mut addr_map: std::collections::HashMap<String, Option<std::net::SocketAddr>> =
1479                    std::collections::HashMap::new();
1480                for p in &prepared {
1481                    addr_map.insert(p.name.clone(), Some(p.local_addr));
1482                }
1483                #[cfg(feature = "quic")]
1484                for p in &prepared_quic {
1485                    addr_map.insert(p.name.clone(), Some(p.local_addr));
1486                }
1487                for (name, transparent_listener, _, _, _, _, _, _, _, _) in
1488                    &transparent_listener_args
1489                {
1490                    let addr = transparent_listener.local_addr().ok();
1491                    addr_map.insert(name.clone(), addr);
1492                }
1493                #[cfg(unix)]
1494                for (name, _, _, _, _, _, _, _, _, _) in &unix_listener_args {
1495                    // Unix domain sockets don't have a meaningful TCP socket address
1496                    addr_map.insert(name.clone(), None);
1497                }
1498                let addrs: Vec<Option<std::net::SocketAddr>> = listener_configs
1499                    .iter()
1500                    .map(|lcfg| addr_map.get(&lcfg.name).copied().flatten())
1501                    .collect();
1502                let admin_addrs = addrs.clone();
1503                match state_ref.listener_addrs.lock() {
1504                    Ok(mut guard) => *guard = addrs,
1505                    Err(error) => {
1506                        tracing::warn!(
1507                            "listener address state was poisoned; resetting it: {error}"
1508                        );
1509                        let mut guard = error.into_inner();
1510                        *guard = addrs;
1511                        state_ref.listener_addrs.clear_poison();
1512                    }
1513                }
1514                #[cfg(feature = "operations")]
1515                state_ref.publish_admin_listener_addrs(state_ref.snapshot.load_full(), admin_addrs);
1516            }
1517
1518            #[cfg(feature = "extended")]
1519            let mut shadowsocks_udp_relays = Vec::new();
1520            let mut echo_udp_relays = Vec::new();
1521
1522            for prepared_listener in &prepared {
1523                if let Some(ref udp_cfg) = prepared_listener.udp {
1524                    #[cfg(feature = "extended")]
1525                    if udp_cfg.mode == eggress_udp::UdpMode::ShadowsocksUdp {
1526                        shadowsocks_udp_relays.push(
1527                            prepare_shadowsocks_udp_relay(
1528                                prepared_listener,
1529                                udp_cfg,
1530                                routing.clone(),
1531                                &state_ref,
1532                            )
1533                            .await?,
1534                        );
1535                    }
1536                    if udp_cfg.mode == eggress_udp::UdpMode::Echo {
1537                        let socket =
1538                            Arc::new(tokio::net::UdpSocket::bind(udp_cfg.bind).await.map_err(
1539                                |e| RuntimeError::ListenerBind {
1540                                    addr: udp_cfg.bind.to_string(),
1541                                    source: e,
1542                                },
1543                            )?);
1544                        echo_udp_relays.push((prepared_listener.name.clone(), socket, None));
1545                    } else if udp_cfg.mode == eggress_udp::UdpMode::FixedTarget {
1546                        let socket =
1547                            Arc::new(tokio::net::UdpSocket::bind(udp_cfg.bind).await.map_err(
1548                                |e| RuntimeError::ListenerBind {
1549                                    addr: udp_cfg.bind.to_string(),
1550                                    source: e,
1551                                },
1552                            )?);
1553                        echo_udp_relays.push((
1554                            prepared_listener.name.clone(),
1555                            socket,
1556                            udp_cfg.fixed_target.clone(),
1557                        ));
1558                    }
1559                }
1560            }
1561
1562            #[cfg(feature = "extended")]
1563            for (socket, relay_config) in shadowsocks_udp_relays {
1564                let relay_cancel = cancel.clone();
1565                tasks.spawn(async move {
1566                    let result =
1567                        eggress_udp::standalone_shadowsocks::shadowsocks_standalone_udp_relay(
1568                            socket,
1569                            relay_config,
1570                            relay_cancel,
1571                        )
1572                        .await;
1573                    if let Err(error) = result {
1574                        tracing::debug!(
1575                            %error,
1576                            "Shadowsocks UDP relay ended with error"
1577                        );
1578                    }
1579                });
1580            }
1581
1582            for (listener_name, socket, fixed_target) in echo_udp_relays {
1583                let relay_cancel = cancel.clone();
1584                let max_size = listener_configs
1585                    .iter()
1586                    .find(|l| l.name == listener_name)
1587                    .and_then(|l| l.udp.as_ref())
1588                    .map(|u| u.max_datagram_size)
1589                    .unwrap_or(65535);
1590                tasks.spawn(async move {
1591                    let mut buf = vec![0u8; max_size];
1592                    const ECHO_DNS_TIMEOUT: Duration = Duration::from_secs(2);
1593                    const ECHO_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
1594                    const ECHO_RECV_TIMEOUT: Duration = Duration::from_secs(5);
1595                    let target_socket = if let Some(target) = fixed_target {
1596                        let lookup = async {
1597                            match target.host {
1598                                eggress_core::TargetHost::Ip(ip) => {
1599                                    Ok(std::net::SocketAddr::new(ip, target.port))
1600                                }
1601                                eggress_core::TargetHost::Domain(domain) => match tokio::net::lookup_host((domain.as_str(), target.port)).await {
1602                                    Ok(mut addrs) => match addrs.next() {
1603                                        Some(addr) => Ok(addr),
1604                                        None => Err(()),
1605                                    },
1606                                    Err(_) => Err(()),
1607                                },
1608                            }
1609                        };
1610                        let addr = match tokio::time::timeout(ECHO_DNS_TIMEOUT, lookup).await {
1611                            Ok(Ok(a)) => a,
1612                            Ok(Err(())) | Err(_) => return,
1613                        };
1614                        let target_socket = match tokio::net::UdpSocket::bind("0.0.0.0:0").await {
1615                            Ok(s) => s,
1616                            Err(_) => return,
1617                        };
1618                        if tokio::time::timeout(ECHO_CONNECT_TIMEOUT, target_socket.connect(addr))
1619                            .await
1620                            .map(|r| r.is_ok())
1621                            .unwrap_or(false)
1622                        {
1623                            Some(target_socket)
1624                        } else {
1625                            return;
1626                        }
1627                    } else { None };
1628                    let mut response = vec![0u8; max_size];
1629                    loop {
1630                        tokio::select! {
1631                            result = socket.recv_from(&mut buf) => match result {
1632                                Ok((n, peer)) => {
1633                                    if let Some(ref target_socket) = target_socket {
1634                                        if target_socket.send(&buf[..n]).await.is_ok() {
1635                                            if let Ok(Ok(m)) = tokio::time::timeout(ECHO_RECV_TIMEOUT, target_socket.recv(&mut response)).await {
1636                                                let _ = socket.send_to(&response[..m], peer).await;
1637                                            }
1638                                        }
1639                                    } else { let _ = socket.send_to(&buf[..n], peer).await; }
1640                                }
1641                                Err(_) => break,
1642                            },
1643                            _ = relay_cancel.cancelled() => break,
1644                        }
1645                    }
1646                });
1647            }
1648
1649            // Spawn transparent listener accept loops
1650            for (
1651                listener_name,
1652                transparent_listener,
1653                protocols,
1654                auth,
1655                hs_timeout,
1656                connection_limit,
1657                tls_cfg,
1658                ss_cfg,
1659                trojan_cfg,
1660                udp_cfg,
1661            ) in transparent_listener_args
1662            {
1663                let routing = routing.clone();
1664                let state = state_ref.clone();
1665                let conn_tasks = connection_tasks.clone();
1666                let conn_cancel = connection_cancel.clone();
1667                let tls_client_config = tls_client_config.clone();
1668                let listener_cancel = listener_cancel.clone();
1669
1670                #[cfg(feature = "ssh")]
1671                let listener_ssh_sessions = ssh_sessions.clone();
1672
1673                tasks.spawn(async move {
1674                    let proto_slice: Arc<[ProtocolId]> = protocols.clone().into();
1675                    let transparent_listener_inner = transparent_listener.inner();
1676                    let listener_active = Arc::new(AtomicU64::new(0));
1677
1678                    let transparent_accepted = state.transparent_accepted_total.clone();
1679                    let transparent_dst_failed =
1680                        state.transparent_original_dst_failed_total.clone();
1681
1682                    loop {
1683                        let accept_result = tokio::select! {
1684                            result = transparent_listener_inner.accept() => result,
1685                            _ = listener_cancel.cancelled() => {
1686                                break;
1687                            }
1688                        };
1689
1690                        let (stream, _peer) = match accept_result {
1691                            Ok(s) => s,
1692                            Err(e) => {
1693                                // Cancellation is handled by the token select
1694                                // above; the inner accept cannot report it.
1695                                handle_accept_error(
1696                                    &format!("transparent on '{listener_name}'"),
1697                                    &e,
1698                                )
1699                                .await;
1700                                continue;
1701                            }
1702                        };
1703
1704                        let connection_slot = match ListenerConnectionSlot::try_acquire(
1705                            &listener_active,
1706                            connection_limit,
1707                        ) {
1708                            Some(slot) => slot,
1709                            None => {
1710                                tracing::debug!(
1711                                    listener = %listener_name,
1712                                    limit = connection_limit,
1713                                    "dropping transparent connection: connection limit reached"
1714                                );
1715                                drop(stream);
1716                                continue;
1717                            }
1718                        };
1719
1720                        transparent_accepted.fetch_add(1, Ordering::Relaxed);
1721
1722                        let original_dst =
1723                            match eggress_server::listener::transparent::get_original_destination(&stream) {
1724                                Ok(addr) => addr,
1725                                Err(e) => {
1726                                    drop(connection_slot);
1727                                    transparent_dst_failed.fetch_add(1, Ordering::Relaxed);
1728                                    let _span = tracing::info_span!(
1729                                        "transparent_original_dst_failed",
1730                                        listener = %listener_name,
1731                                        error = %e,
1732                                    );
1733                                    tracing::warn!(
1734                                        "failed to get original destination for transparent connection on '{}': {e}",
1735                                        listener_name
1736                                    );
1737                                    continue;
1738                                }
1739                            };
1740
1741                        let peer = stream
1742                            .peer_addr()
1743                            .unwrap_or_else(|_| {
1744                                std::net::SocketAddr::new(
1745                                    std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
1746                                    0,
1747                                )
1748                            });
1749
1750                        let routing = routing.clone();
1751                        let tls_client_config = tls_client_config.clone();
1752                        let listener_str = listener_name.clone();
1753                        let conn_id = state
1754                            .connection_counter
1755                            .fetch_add(1, Ordering::Relaxed);
1756                        let conn_protocols = proto_slice.clone();
1757                        let conn_auth = auth.clone();
1758                        let conn_metrics = state.metrics.clone();
1759                        #[cfg(feature = "extended")]
1760                        let conn_ss_metrics = state.shadowsocks_metrics.clone();
1761                        let active = state.active_connections.clone();
1762                        let conn_cancel = conn_cancel.child_token();
1763                        let generation = state.snapshot.load().generation;
1764                        let tls_config = tls_cfg.clone();
1765                        let ss_config = ss_cfg.clone();
1766                        let trojan_config = trojan_cfg.clone();
1767
1768                        let udp_svc = udp_cfg.as_ref().map(|udp_config| {
1769                            Arc::new(RuntimeUdpService {
1770                                _listener_name: listener_name.clone(),
1771                                udp_config: udp_config.clone(),
1772                                registry: state.udp_registry.clone(),
1773                                metrics: state.metrics.clone(),
1774                                udp_metrics: state.udp_metrics.clone(),
1775                                routing: routing.clone(),
1776                                udp_tasks: state.udp_tasks.clone(),
1777                            }) as Arc<dyn eggress_server::UdpService>
1778                        });
1779
1780                        #[cfg(feature = "ssh")]
1781                        let conn_ssh_sessions = listener_ssh_sessions.clone();
1782                        conn_tasks.spawn(async move {
1783                            let _active_guard = ActiveConnectionGuard::new(active);
1784                            let _connection_slot = connection_slot;
1785                            let started = std::time::Instant::now();
1786
1787                            let stream: eggress_core::BoxStream =
1788                                if let Some(ref tls_cfg) = tls_config {
1789                                    let server_config = match eggress_transport_tls::TlsServerConfigBuilder::new()
1790                                        .with_certificate_pem(&tls_cfg.cert_pem)
1791                                        .and_then(|b| b.with_key_pem(&tls_cfg.key_pem))
1792                                        .and_then(|b| {
1793                                            let b = if tls_cfg.alpn.is_empty() { b } else { b.with_alpn(tls_cfg.alpn.clone()) };
1794                                            b.build()
1795                                        }) {
1796                                            Ok(c) => c,
1797                                            Err(e) => {
1798                                                tracing::error!(%peer, "TLS config error: {e}");
1799                                                return;
1800                                            }
1801                                        };
1802                                    match eggress_transport_tls::tls_accept(Box::new(stream), server_config).await {
1803                                        Ok(s) => s,
1804                                        Err(e) => {
1805                                            tracing::debug!(%peer, "TLS accept failed: {e}");
1806                                            return;
1807                                        }
1808                                    }
1809                                } else {
1810                                    Box::new(stream)
1811                                };
1812
1813                            let config = eggress_server::ConnectionConfig {
1814                                routing: routing as Arc<dyn RouteService>,
1815                                context: eggress_server::ConnectionContext {
1816                                    source: Some(peer),
1817                                    listener: listener_str.clone(),
1818                                    generation,
1819                                },
1820                                handshake_timeout: hs_timeout,
1821                                connect_timeout,
1822                                protocols: conn_protocols,
1823                                authentication: conn_auth,
1824                                metrics: Some(conn_metrics),
1825                                udp: udp_svc,
1826                                tls_client_config,
1827                                shadowsocks: ss_config.map(
1828                                    |ss| eggress_server::accept::InboundShadowsocksConfig {
1829                                        method: ss.method.clone(),
1830                                        password: ss.password.clone(),
1831                                        #[cfg(feature = "pproxy-legacy")]
1832                                        auth_prefix: ss.auth_prefix.clone().map(String::into_bytes),
1833                                        #[cfg(feature = "pproxy-legacy")]
1834                                        plugins: ss.plugins.clone(),
1835                                    },
1836                                ),
1837                                #[cfg(feature = "extended")]
1838                                shadowsocks_metrics: Some(conn_ss_metrics),
1839                                #[cfg(not(feature = "extended"))]
1840                                shadowsocks_metrics: None,
1841                                trojan: trojan_config.map(
1842                                    |t| eggress_server::accept::InboundTrojanConfig {
1843                                        password: t.password.clone(),
1844                                        fallback: t.fallback.clone(),
1845                                    },
1846                                ),
1847                                fixed_target: None,
1848                                local_bind: None,
1849                                #[cfg(feature = "ssh")]
1850                                ssh_sessions: Some(conn_ssh_sessions),
1851                            };
1852
1853                            let report = tokio::select! {
1854                                report = eggress_server::serve_connection(stream, config)
1855                                    .instrument(tracing::info_span!(
1856                                        "conn",
1857                                        id = conn_id,
1858                                        peer = %peer,
1859                                        original_dst = %original_dst,
1860                                        listener_type = "transparent",
1861                                        listener = %listener_str,
1862                                    )) => {
1863                                report
1864                            }
1865                                _ = conn_cancel.cancelled() => {
1866                                    eggress_server::SessionReport::cancelled(
1867                                        None,
1868                                        None,
1869                                        String::new(),
1870                                    )
1871                                }
1872                            };
1873
1874                            tracing::info!(
1875                                protocol = ?report.protocol,
1876                                target = ?report.target,
1877                                original_dst = %original_dst,
1878                                route = %report.route,
1879                                outcome = ?report.outcome,
1880                                bytes_upstream = report.bytes_upstream,
1881                                bytes_downstream = report.bytes_downstream,
1882                                duration_ms = started.elapsed().as_millis() as u64,
1883                                "transparent connection completed",
1884                            );
1885                        });
1886                    }
1887                });
1888            }
1889
1890            // Spawn Unix domain socket accept loops
1891            #[cfg(unix)]
1892            for (
1893                listener_name,
1894                unix_listener,
1895                protocols,
1896                auth,
1897                hs_timeout,
1898                connection_limit,
1899                tls_cfg,
1900                ss_cfg,
1901                trojan_cfg,
1902                udp_cfg,
1903            ) in unix_listener_args
1904            {
1905                let routing = routing.clone();
1906                let state = state_ref.clone();
1907                let conn_tasks = connection_tasks.clone();
1908                let conn_cancel = connection_cancel.clone();
1909                let tls_client_config = tls_client_config.clone();
1910                let listener_cancel = listener_cancel.clone();
1911
1912                let socket_path = unix_listener.path().display().to_string();
1913
1914                #[cfg(feature = "ssh")]
1915                let listener_ssh_sessions = ssh_sessions.clone();
1916                tasks.spawn(async move {
1917                    let proto_slice: Arc<[ProtocolId]> = protocols.clone().into();
1918                    let listener_active = Arc::new(AtomicU64::new(0));
1919
1920                    let _accept_loop_span = tracing::info_span!(
1921                        "unix_accept_loop",
1922                        listener = %listener_name,
1923                        socket_path = %socket_path,
1924                    );
1925
1926                    loop {
1927                        let (stream, _peer_addr) = tokio::select! {
1928                            result = unix_listener.accept() => match result {
1929                                Ok(r) => r,
1930                                Err(e) => {
1931                                    handle_accept_error("unix", &e).await;
1932                                    continue;
1933                                }
1934                            },
1935                            _ = listener_cancel.cancelled() => {
1936                                break;
1937                            }
1938                        };
1939
1940                        let connection_slot = match ListenerConnectionSlot::try_acquire(
1941                            &listener_active,
1942                            connection_limit,
1943                        ) {
1944                            Some(slot) => slot,
1945                            None => {
1946                                tracing::debug!(
1947                                    listener = %listener_name,
1948                                    limit = connection_limit,
1949                                    "dropping Unix connection: connection limit reached"
1950                                );
1951                                drop(stream);
1952                                continue;
1953                            }
1954                        };
1955
1956                        state.metrics.record_unix_listener_connection_accepted();
1957
1958                        let routing = routing.clone();
1959                        let tls_client_config = tls_client_config.clone();
1960                        let listener_str = listener_name.clone();
1961                        let conn_id = state.connection_counter.fetch_add(1, Ordering::Relaxed);
1962                        let conn_protocols = proto_slice.clone();
1963                        let conn_auth = auth.clone();
1964                        let conn_metrics = state.metrics.clone();
1965                        #[cfg(feature = "extended")]
1966                        let conn_ss_metrics = state.shadowsocks_metrics.clone();
1967                        let active = state.active_connections.clone();
1968                        let conn_cancel = conn_cancel.child_token();
1969                        let generation = state.snapshot.load().generation;
1970
1971                        let tls_config = tls_cfg.clone();
1972                        let ss_config = ss_cfg.clone();
1973                        let trojan_config = trojan_cfg.clone();
1974                        let socket_path_clone = socket_path.clone();
1975                        let listener_str_for_span = listener_str.clone();
1976
1977                        let udp_svc = udp_cfg.as_ref().map(|udp_config| {
1978                            Arc::new(RuntimeUdpService {
1979                                _listener_name: listener_name.clone(),
1980                                udp_config: udp_config.clone(),
1981                                registry: state.udp_registry.clone(),
1982                                metrics: state.metrics.clone(),
1983                                udp_metrics: state.udp_metrics.clone(),
1984                                routing: routing.clone(),
1985                                udp_tasks: state.udp_tasks.clone(),
1986                            }) as Arc<dyn eggress_server::UdpService>
1987                        });
1988
1989                        #[cfg(feature = "ssh")]
1990                        let conn_ssh_sessions = listener_ssh_sessions.clone();
1991                        conn_tasks.spawn(async move {
1992                            let _active_guard = ActiveConnectionGuard::new(active);
1993                            let _connection_slot = connection_slot;
1994                            let started = std::time::Instant::now();
1995
1996                            let stream: eggress_core::BoxStream =
1997                                if let Some(ref tls_cfg) = tls_config {
1998                                    let server_config =
1999                                        match eggress_transport_tls::TlsServerConfigBuilder::new()
2000                                            .with_certificate_pem(&tls_cfg.cert_pem)
2001                                            .and_then(|b| b.with_key_pem(&tls_cfg.key_pem))
2002                                            .and_then(|b| {
2003                                                let b = if tls_cfg.alpn.is_empty() {
2004                                                    b
2005                                                } else {
2006                                                    b.with_alpn(tls_cfg.alpn.clone())
2007                                                };
2008                                                b.build()
2009                                            }) {
2010                                            Ok(c) => c,
2011                                            Err(e) => {
2012                                                tracing::error!(
2013                                                    "TLS config error for unix connection: {e}"
2014                                                );
2015                                                return;
2016                                            }
2017                                        };
2018                                    match eggress_transport_tls::tls_accept(
2019                                        Box::new(stream),
2020                                        server_config,
2021                                    )
2022                                    .await
2023                                    {
2024                                        Ok(s) => s,
2025                                        Err(e) => {
2026                                            tracing::debug!(
2027                                                "TLS accept failed for unix connection: {e}"
2028                                            );
2029                                            return;
2030                                        }
2031                                    }
2032                                } else {
2033                                    Box::new(stream)
2034                                };
2035
2036                            let peer = std::net::SocketAddr::new(
2037                                std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
2038                                0,
2039                            );
2040
2041                            let config = eggress_server::ConnectionConfig {
2042                                routing: routing as Arc<dyn RouteService>,
2043                                context: eggress_server::ConnectionContext {
2044                                    source: Some(peer),
2045                                    listener: listener_str,
2046                                    generation,
2047                                },
2048                                handshake_timeout: hs_timeout,
2049                                connect_timeout,
2050                                protocols: conn_protocols,
2051                                authentication: conn_auth,
2052                                metrics: Some(conn_metrics),
2053                                udp: udp_svc,
2054                                tls_client_config,
2055                                shadowsocks: ss_config.map(|ss| {
2056                                    eggress_server::accept::InboundShadowsocksConfig {
2057                                        method: ss.method.clone(),
2058                                        password: ss.password.clone(),
2059                                        #[cfg(feature = "pproxy-legacy")]
2060                                        auth_prefix: ss.auth_prefix.clone().map(String::into_bytes),
2061                                        #[cfg(feature = "pproxy-legacy")]
2062                                        plugins: ss.plugins.clone(),
2063                                    }
2064                                }),
2065                                #[cfg(feature = "extended")]
2066                                shadowsocks_metrics: Some(conn_ss_metrics),
2067                                #[cfg(not(feature = "extended"))]
2068                                shadowsocks_metrics: None,
2069                                trojan: trojan_config.map(|t| {
2070                                    eggress_server::accept::InboundTrojanConfig {
2071                                        password: t.password.clone(),
2072                                        fallback: t.fallback.clone(),
2073                                    }
2074                                }),
2075                                fixed_target: None,
2076                                local_bind: None,
2077                                #[cfg(feature = "ssh")]
2078                                ssh_sessions: Some(conn_ssh_sessions),
2079                            };
2080
2081                            let report = tokio::select! {
2082                                report = eggress_server::serve_connection(stream, config)
2083                                    .instrument(tracing::info_span!(
2084                                        "conn",
2085                                        id = conn_id,
2086                                        peer = %peer,
2087                                        listener_type = "unix",
2088                                        listener = %listener_str_for_span,
2089                                        socket_path = %socket_path_clone,
2090                                    )) => {
2091                                    report
2092                                }
2093                                _ = conn_cancel.cancelled() => {
2094                                    eggress_server::SessionReport::cancelled(
2095                                        None,
2096                                        None,
2097                                        String::new(),
2098                                    )
2099                                }
2100                            };
2101
2102                            tracing::info!(
2103                                protocol = ?report.protocol,
2104                                target = ?report.target,
2105                                route = %report.route,
2106                                outcome = ?report.outcome,
2107                                bytes_upstream = report.bytes_upstream,
2108                                bytes_downstream = report.bytes_downstream,
2109                                duration_ms = started.elapsed().as_millis() as u64,
2110                                "unix connection completed",
2111                            );
2112                        });
2113                    }
2114
2115                    // Cleanup socket file on shutdown
2116                    unix_listener.cleanup().unwrap_or_else(|e| {
2117                        tracing::warn!("failed to cleanup unix socket: {e}");
2118                    });
2119                });
2120            }
2121
2122            // Spawn standard TCP accept loops
2123            #[cfg(feature = "operations")]
2124            let compatibility_proxy_selection = prepared
2125                .iter()
2126                .find(|listener| {
2127                    listener.protocols.contains(&ProtocolId::Socks5)
2128                        && listener.local_addr.port() != 0
2129                })
2130                .map(|listener| {
2131                    (
2132                        eggress_system_proxy::CompatibilityProxyKind::Socks5,
2133                        listener.local_addr.port(),
2134                    )
2135                })
2136                .or_else(|| {
2137                    prepared
2138                        .iter()
2139                        .find(|listener| {
2140                            listener.protocols.contains(&ProtocolId::Http)
2141                                && listener.local_addr.port() != 0
2142                        })
2143                        .map(|listener| {
2144                            (
2145                                eggress_system_proxy::CompatibilityProxyKind::Http,
2146                                listener.local_addr.port(),
2147                            )
2148                        })
2149                });
2150
2151            // All compatibility TCP listeners are bound before this point,
2152            // so --sys can use the actual selected port. Apply before any
2153            // accept loop or admin task is started; an apply failure is
2154            // therefore still a pre-run startup error.
2155            if compatibility_options.system_proxy {
2156                #[cfg(feature = "operations")]
2157                {
2158                    let selected = compatibility_proxy_selection.ok_or_else(|| {
2159                        RuntimeError::Other(
2160                            "--sys requires a usable local HTTP or SOCKS5 listener".to_string(),
2161                        )
2162                    })?;
2163                    let address = std::net::SocketAddr::new(
2164                        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
2165                        selected.1,
2166                    );
2167                    compatibility_system_proxy = Some(
2168                        eggress_system_proxy::apply_compatibility_proxy(selected.0, address)
2169                            .map_err(RuntimeError::Other)?,
2170                    );
2171                }
2172                #[cfg(not(feature = "operations"))]
2173                {
2174                    return Err(RuntimeError::Other(
2175                        "--sys requires the operations feature".to_string(),
2176                    ));
2177                }
2178            }
2179
2180            #[cfg(feature = "quic")]
2181            for prepared_listener in prepared_quic {
2182                let listener_name = prepared_listener.name.clone();
2183                let listener_protocols: Arc<[ProtocolId]> = prepared_listener
2184                    .protocols
2185                    .iter()
2186                    .copied()
2187                    .filter(|protocol| !matches!(protocol, ProtocolId::Quic | ProtocolId::Http3))
2188                    .collect::<Vec<_>>()
2189                    .into();
2190                let routing = routing.clone();
2191                let state = state_ref.clone();
2192                let conn_tasks = connection_tasks.clone();
2193                let conn_cancel = connection_cancel.clone();
2194                let listener_cancel = listener_cancel.clone();
2195                let is_h3 = prepared_listener.protocols.contains(&ProtocolId::Http3);
2196                let auth = prepared_listener.auth.clone();
2197                let handshake_timeout_for_listener = prepared_listener.handshake_timeout;
2198                let connection_limit = prepared_listener.connection_limit;
2199
2200                #[cfg(feature = "ssh")]
2201                let listener_ssh_sessions = ssh_sessions.clone();
2202
2203                if is_h3 {
2204                    let listener = prepared_listener.listener.clone();
2205                    let tls_client_config_for_listener = tls_client_config.clone();
2206                    tasks.spawn(async move {
2207                        let active_streams = Arc::new(AtomicU64::new(0));
2208                        loop {
2209                            let connection = match listener.accept_connection(&listener_cancel).await {
2210                                Ok(Some(connection)) => connection,
2211                                Ok(None) => break,
2212                                Err(error) => {
2213                                    tracing::debug!(%error, listener = %listener_name, "H3 connection failed");
2214                                    continue;
2215                                }
2216                            };
2217                            let routing = routing.clone();
2218                            let state = state.clone();
2219                            let conn_tasks = conn_tasks.clone();
2220                            let conn_cancel = conn_cancel.child_token();
2221                            let listener_name = listener_name.clone();
2222                            let auth = auth.clone();
2223                            let authorization = match &auth {
2224                                eggress_server::accept::InboundAuthentication::None => None,
2225                                eggress_server::accept::InboundAuthentication::UsernamePassword { username, password }
2226                                | eggress_server::accept::InboundAuthentication::UsernamePasswordWithReuse { username, password, .. } => {
2227                                    Some((username.clone(), password.clone()))
2228                                }
2229                            };
2230                            let active_streams = active_streams.clone();
2231                            let protocols = listener_protocols.clone();
2232                            let connection_limit = connection_limit;
2233                            let tls_client_config_for_connection = tls_client_config_for_listener.clone();
2234                            #[cfg(feature = "ssh")]
2235                            let listener_ssh_sessions_for_connection = listener_ssh_sessions.clone();
2236                            conn_tasks.spawn(async move {
2237                                let result = eggress_protocol_h3::serve_connection(
2238                                    connection,
2239                                    conn_cancel.clone(),
2240                                    authorization,
2241                                    move |request, stream, peer| {
2242                                        let routing = routing.clone();
2243                                        let state = state.clone();
2244                                        let listener_name = listener_name.clone();
2245                                        let auth = auth.clone();
2246                                        let protocols = protocols.clone();
2247                                        let active_streams = active_streams.clone();
2248                                        let tls_client_config = tls_client_config_for_connection.clone();
2249                                        #[cfg(feature = "ssh")]
2250                                        let ssh_sessions = listener_ssh_sessions_for_connection.clone();
2251                                        async move {
2252                                            let slot = match ListenerConnectionSlot::try_acquire(&active_streams, connection_limit) {
2253                                                Some(slot) => slot,
2254                                                None => return,
2255                                            };
2256                                            let target = match request.target() {
2257                                                Ok(target) => target,
2258                                                Err(error) => {
2259                                                    tracing::debug!(%error, "invalid H3 CONNECT authority");
2260                                                    drop(slot);
2261                                                    return;
2262                                                }
2263                                            };
2264                                            let generation = state.snapshot.load().generation;
2265                                            let config = eggress_server::ConnectionConfig {
2266                                                routing: routing as Arc<dyn RouteService>,
2267                                                context: eggress_server::ConnectionContext {
2268                                                    source: Some(peer),
2269                                                    listener: listener_name,
2270                                                    generation,
2271                                                },
2272                                                handshake_timeout: handshake_timeout_for_listener,
2273                                                connect_timeout,
2274                                                protocols,
2275                                                authentication: auth,
2276                                                metrics: Some(state.metrics.clone()),
2277                                                udp: None,
2278                                                tls_client_config,
2279                                                shadowsocks: None,
2280                                                #[cfg(feature = "extended")]
2281                                                shadowsocks_metrics: Some(state.shadowsocks_metrics.clone()),
2282                                                #[cfg(not(feature = "extended"))]
2283                                                shadowsocks_metrics: None,
2284                                                trojan: None,
2285                                                fixed_target: None,
2286                                                local_bind: None,
2287                                                #[cfg(feature = "ssh")]
2288                                                ssh_sessions: Some(ssh_sessions),
2289                                            };
2290                                            let pending = eggress_server::accept::PendingTunnel {
2291                                                target: target.clone(),
2292                                                client: stream,
2293                                                protocol: eggress_server::accept::TunnelProtocol::Http3,
2294                                                reply_context: eggress_server::accept::ReplyContext::Http3,
2295                                                identity: eggress_core::ClientIdentity::Anonymous,
2296                                            };
2297                                            state.metrics.record_session_start();
2298                                            let report = eggress_server::execute::execute(
2299                                                eggress_server::accept::AcceptedSession::Tunnel(pending),
2300                                                &config,
2301                                            ).await;
2302                                            state.metrics.record_session(&report);
2303                                            drop(slot);
2304                                        }
2305                                    },
2306                                ).await;
2307                                if let Err(error) = result {
2308                                    tracing::debug!(%error, "H3 connection ended");
2309                                }
2310                            });
2311                        }
2312                    });
2313                } else {
2314                    let listener = prepared_listener.listener.clone();
2315                    let auth = prepared_listener.auth.clone();
2316                    let tls_client_config_for_listener = tls_client_config.clone();
2317                    tasks.spawn(async move {
2318                        let active_streams = Arc::new(AtomicU64::new(0));
2319                        let listener_name_for_handler = listener_name.clone();
2320                        let protocols_for_handler = listener_protocols.clone();
2321                        let routing_for_handler = routing.clone();
2322                        let state_for_handler = state.clone();
2323                        let auth_for_handler = auth.clone();
2324                        #[cfg(feature = "ssh")]
2325                        let listener_ssh_sessions_for_handler = Some(listener_ssh_sessions.clone());
2326                        let result = listener
2327                            .run(listener_cancel, move |stream, peer| {
2328                                let routing = routing_for_handler.clone();
2329                                let state = state_for_handler.clone();
2330                                let listener_name = listener_name_for_handler.clone();
2331                                let protocols = protocols_for_handler.clone();
2332                                let auth = auth_for_handler.clone();
2333                                let active_streams = active_streams.clone();
2334                                #[cfg(feature = "ssh")]
2335                                let ssh_sessions = listener_ssh_sessions_for_handler.clone();
2336                                let tls_client_config = tls_client_config_for_listener.clone();
2337                                async move {
2338                                    let Some(slot) = ListenerConnectionSlot::try_acquire(
2339                                        &active_streams,
2340                                        connection_limit,
2341                                    ) else {
2342                                        return;
2343                                    };
2344                                    let generation = state.snapshot.load().generation;
2345                                    let config = eggress_server::ConnectionConfig {
2346                                        routing: routing as Arc<dyn RouteService>,
2347                                        context: eggress_server::ConnectionContext {
2348                                            source: Some(peer),
2349                                            listener: listener_name,
2350                                            generation,
2351                                        },
2352                                        handshake_timeout: handshake_timeout_for_listener,
2353                                        connect_timeout,
2354                                        protocols,
2355                                        authentication: auth,
2356                                        metrics: Some(state.metrics.clone()),
2357                                        udp: None,
2358                                        tls_client_config,
2359                                        shadowsocks: None,
2360                                        #[cfg(feature = "extended")]
2361                                        shadowsocks_metrics: Some(
2362                                            state.shadowsocks_metrics.clone(),
2363                                        ),
2364                                        #[cfg(not(feature = "extended"))]
2365                                        shadowsocks_metrics: None,
2366                                        trojan: None,
2367                                        fixed_target: None,
2368                                        local_bind: None,
2369                                        #[cfg(feature = "ssh")]
2370                                        ssh_sessions,
2371                                    };
2372                                    let _ = eggress_server::serve_connection(stream, config).await;
2373                                    drop(slot);
2374                                }
2375                            })
2376                            .await;
2377                        if let Err(error) = result {
2378                            tracing::debug!(%error, "QUIC listener ended");
2379                        }
2380                    });
2381                }
2382            }
2383
2384            for prepared_listener in prepared {
2385                let routing = routing.clone();
2386                let state = state_ref.clone();
2387                let conn_tasks = connection_tasks.clone();
2388                let conn_cancel = connection_cancel.clone();
2389                let tls_client_config = tls_client_config.clone();
2390
2391                #[cfg(feature = "ssh")]
2392                let listener_ssh_sessions = ssh_sessions.clone();
2393                tasks.spawn(async move {
2394                    let proto_slice: Arc<[ProtocolId]> = prepared_listener.protocols.clone().into();
2395
2396                    loop {
2397                        let conn = match prepared_listener.listener.accept().await {
2398                            Ok(c) => c,
2399                            Err(e) => {
2400                                if is_listener_cancelled(&e) {
2401                                    break;
2402                                }
2403                                handle_accept_error("tcp", &e).await;
2404                                continue;
2405                            }
2406                        };
2407
2408                        let routing = routing.clone();
2409                        let tls_client_config = tls_client_config.clone();
2410                        let peer = conn.peer_addr;
2411                        let listener_str = prepared_listener.name.clone();
2412                        let conn_id = state.connection_counter.fetch_add(1, Ordering::Relaxed);
2413                        let conn_protocols = proto_slice.clone();
2414                        let conn_auth = prepared_listener.auth.clone();
2415                        let conn_metrics = state.metrics.clone();
2416                        #[cfg(feature = "extended")]
2417                        let conn_ss_metrics = state.shadowsocks_metrics.clone();
2418                        let active = state.active_connections.clone();
2419                        let conn_cancel = conn_cancel.child_token();
2420                        let generation = state.snapshot.load().generation;
2421
2422                        let tls_config = prepared_listener.tls.clone();
2423                        let ss_config = prepared_listener.shadowsocks.clone();
2424                        let trojan_config = prepared_listener.trojan.clone();
2425                        let fixed_target = prepared_listener.fixed_target.clone();
2426                        let local_bind = prepared_listener.local_bind.clone();
2427
2428                        let udp_svc = if let Some(ref udp_config) = prepared_listener.udp {
2429                            Some(Arc::new(RuntimeUdpService {
2430                                _listener_name: prepared_listener.name.clone(),
2431                                udp_config: udp_config.clone(),
2432                                registry: state.udp_registry.clone(),
2433                                metrics: state.metrics.clone(),
2434                                udp_metrics: state.udp_metrics.clone(),
2435                                routing: routing.clone(),
2436                                udp_tasks: state.udp_tasks.clone(),
2437                            })
2438                                as Arc<dyn eggress_server::UdpService>)
2439                        } else {
2440                            None
2441                        };
2442                        #[cfg(feature = "ssh")]
2443                        let conn_ssh_sessions = listener_ssh_sessions.clone();
2444                        let stream_tasks = conn_tasks.clone();
2445                        conn_tasks.spawn(async move {
2446                            let _active_guard = ActiveConnectionGuard::new(active);
2447                            let started = std::time::Instant::now();
2448
2449                            // Apply TLS if configured for this listener
2450                            let stream: eggress_core::BoxStream =
2451                                if let Some(ref tls_cfg) = tls_config {
2452                                    let server_config =
2453                                        match eggress_transport_tls::TlsServerConfigBuilder::new()
2454                                            .with_certificate_pem(&tls_cfg.cert_pem)
2455                                            .and_then(|b| b.with_key_pem(&tls_cfg.key_pem))
2456                                            .and_then(|b| {
2457                                                let b = if tls_cfg.alpn.is_empty() {
2458                                                    b
2459                                                } else {
2460                                                    b.with_alpn(tls_cfg.alpn.clone())
2461                                                };
2462                                                b.build()
2463                                            }) {
2464                                            Ok(c) => c,
2465                                            Err(e) => {
2466                                                tracing::error!(%peer, "TLS config error: {e}");
2467                                                return;
2468                                            }
2469                                        };
2470                                    match eggress_transport_tls::tls_accept(
2471                                        Box::new(conn.stream),
2472                                        server_config,
2473                                    )
2474                                    .await
2475                                    {
2476                                        Ok(s) => s,
2477                                        Err(e) => {
2478                                            tracing::debug!(%peer, "TLS accept failed: {e}");
2479                                            return;
2480                                        }
2481                                    }
2482                                } else {
2483                                    Box::new(conn.stream)
2484                                };
2485
2486                            #[cfg(feature = "extended")]
2487                            let advanced_protocol = conn_protocols.first().copied();
2488                            #[cfg(feature = "extended")]
2489                            let advanced_is_single = conn_protocols.len() == 1;
2490                            #[cfg(feature = "extended")]
2491                            let advanced_fixed_target = fixed_target.clone();
2492                            let config = eggress_server::ConnectionConfig {
2493                                routing: routing as Arc<dyn RouteService>,
2494                                context: eggress_server::ConnectionContext {
2495                                    source: Some(peer),
2496                                    listener: listener_str,
2497                                    generation,
2498                                },
2499                                handshake_timeout: prepared_listener.handshake_timeout,
2500                                connect_timeout,
2501                                protocols: conn_protocols,
2502                                authentication: conn_auth,
2503                                metrics: Some(conn_metrics),
2504                                udp: udp_svc,
2505                                tls_client_config: tls_client_config.clone(),
2506                                shadowsocks: ss_config.map(|ss| {
2507                                    eggress_server::accept::InboundShadowsocksConfig {
2508                                        method: ss.method.clone(),
2509                                        password: ss.password.clone(),
2510                                        #[cfg(feature = "pproxy-legacy")]
2511                                        auth_prefix: ss.auth_prefix.clone().map(String::into_bytes),
2512                                        #[cfg(feature = "pproxy-legacy")]
2513                                        plugins: ss.plugins.clone(),
2514                                    }
2515                                }),
2516                                #[cfg(feature = "extended")]
2517                                shadowsocks_metrics: Some(conn_ss_metrics),
2518                                #[cfg(not(feature = "extended"))]
2519                                shadowsocks_metrics: None,
2520                                trojan: trojan_config.map(|t| {
2521                                    eggress_server::accept::InboundTrojanConfig {
2522                                        password: t.password.clone(),
2523                                        fallback: t.fallback.clone(),
2524                                    }
2525                                }),
2526                                fixed_target,
2527                                local_bind,
2528                                #[cfg(feature = "ssh")]
2529                                ssh_sessions: Some(conn_ssh_sessions),
2530                            };
2531
2532                            #[cfg(feature = "extended")]
2533                            if matches!(
2534                                advanced_protocol,
2535                                Some(ProtocolId::Http2 | ProtocolId::WebSocket)
2536                            ) && advanced_is_single
2537                            {
2538                                let advanced_result = match advanced_protocol {
2539                                    Some(ProtocolId::Http2) => {
2540                                        eggress_server::advanced::serve_h2_connection(
2541                                            stream,
2542                                            config,
2543                                            &stream_tasks,
2544                                            conn_cancel.clone(),
2545                                        )
2546                                        .await
2547                                    }
2548                                    Some(ProtocolId::WebSocket) => match advanced_fixed_target {
2549                                        Some(target) => {
2550                                            eggress_server::advanced::serve_websocket_connection(
2551                                                stream, config, target,
2552                                            )
2553                                            .await
2554                                        }
2555                                        None => Err("WebSocket listener requires a fixed target"
2556                                            .to_string()),
2557                                    },
2558                                    None => {
2559                                        Err("advanced listener has no configured protocol"
2560                                            .to_string())
2561                                    }
2562                                    Some(_) => {
2563                                        Err("advanced listener protocol is not supported here"
2564                                            .to_string())
2565                                    }
2566                                };
2567                                if let Err(error) = advanced_result {
2568                                    tracing::debug!(%peer, %error, "advanced listener ended");
2569                                }
2570                                return;
2571                            }
2572
2573                            let report = tokio::select! {
2574                                report = eggress_server::serve_connection(stream, config)
2575                                    .instrument(tracing::info_span!(
2576                                        "conn",
2577                                        id = conn_id,
2578                                        peer = %peer,
2579                                    )) => {
2580                                    report
2581                                }
2582                                _ = conn_cancel.cancelled() => {
2583                                    eggress_server::SessionReport::cancelled(
2584                                        None,
2585                                        None,
2586                                        String::new(),
2587                                    )
2588                                }
2589                            };
2590
2591                            if compatibility_options.debug && report.failure.is_some() {
2592                                tracing::error!(
2593                                    protocol = ?report.protocol,
2594                                    target = ?report.target,
2595                                    route = %report.route,
2596                                    outcome = ?report.outcome,
2597                                    failure = ?report.failure,
2598                                    "pproxy debug connection failure",
2599                                );
2600                            } else {
2601                                tracing::info!(
2602                                    protocol = ?report.protocol,
2603                                    target = ?report.target,
2604                                    route = %report.route,
2605                                    outcome = ?report.outcome,
2606                                    bytes_upstream = report.bytes_upstream,
2607                                    bytes_downstream = report.bytes_downstream,
2608                                    duration_ms = started.elapsed().as_millis() as u64,
2609                                    "connection completed",
2610                                );
2611                            }
2612                            if compatibility_options.verbose_level >= 1 {
2613                                tracing::info!(
2614                                    protocol = ?report.protocol,
2615                                    target = ?report.target,
2616                                    route = %report.route,
2617                                    "pproxy connection event",
2618                                );
2619                            }
2620                            if compatibility_options.verbose_level >= 2 {
2621                                tracing::info!(
2622                                    bytes_upstream = report.bytes_upstream,
2623                                    bytes_downstream = report.bytes_downstream,
2624                                    "pproxy traffic stats",
2625                                );
2626                            }
2627                        });
2628                    }
2629                });
2630            }
2631
2632            // Spawn reverse servers and clients
2633            #[cfg(feature = "reverse")]
2634            {
2635                let current_snapshot = snapshot.load();
2636                let reverse_servers = current_snapshot.reverse_servers.clone();
2637                let reverse_clients = current_snapshot.reverse_clients.clone();
2638                drop(current_snapshot);
2639
2640                for rs_cfg in reverse_servers {
2641                    if rs_cfg.pproxy_compat {
2642                        let server_config =
2643                            eggress_protocol_reverse::compat_pproxy::PproxyBackwardServerConfig {
2644                                control_bind: rs_cfg.control_bind,
2645                                external_bind: rs_cfg.external_bind,
2646                                auth: eggress_protocol_reverse::compat_pproxy::raw_auth(
2647                                    rs_cfg.auth_username.as_deref(),
2648                                    rs_cfg.auth_password.as_deref(),
2649                                ),
2650                                max_control_connections: rs_cfg.max_control_connections as usize,
2651                                max_pending_external: rs_cfg.max_pending_external as usize,
2652                                read_timeout_ms: rs_cfg.read_timeout_ms,
2653                                socks5_target: None,
2654                                client_framing:
2655                                    eggress_protocol_reverse::compat_pproxy::PproxyBackwardFraming::Raw,
2656                            };
2657                        let server =
2658                            eggress_protocol_reverse::compat_pproxy::PproxyBackwardServer::new(
2659                                server_config,
2660                            );
2661                        let server_cancel = server.cancel_token();
2662                        let cancel_clone = cancel.clone();
2663                        tasks.spawn(async move {
2664                            let result = tokio::select! {
2665                                r = server.run() => r,
2666                                _ = cancel_clone.cancelled() => {
2667                                    server_cancel.cancel();
2668                                    Ok(())
2669                                }
2670                            };
2671                            if let Err(e) = result {
2672                                tracing::error!(error = %e, "pproxy backward server error");
2673                            }
2674                        });
2675                        continue;
2676                    }
2677                    let server_config = eggress_protocol_reverse::server::ReverseServerConfig {
2678                        control_bind: rs_cfg.control_bind,
2679                        external_bind: Some(rs_cfg.external_bind),
2680                        auth_username: rs_cfg.auth_username.clone(),
2681                        auth_password: rs_cfg.auth_password.clone(),
2682                        max_control_connections: rs_cfg.max_control_connections,
2683                        read_timeout_ms: rs_cfg.read_timeout_ms,
2684                        allow_bind: rs_cfg.allow_bind.clone(),
2685                        max_listeners_per_client: rs_cfg.max_listeners_per_client,
2686                        max_streams_per_listener: rs_cfg.max_streams_per_listener,
2687                        max_pending_external: rs_cfg.max_pending_external,
2688                    };
2689                    // Defense-in-depth: validate the configuration before
2690                    // spawning the task so unsafe configurations fail at
2691                    // startup rather than silently at bind time.
2692                    if let Err(e) = server_config.validate() {
2693                        tracing::error!(
2694                            server_id = %rs_cfg.id,
2695                            error = %e,
2696                            "reverse server configuration validation failed; skipping",
2697                        );
2698                        continue;
2699                    }
2700                    let mut server =
2701                        eggress_protocol_reverse::server::ReverseServer::new(server_config);
2702                    server.set_metrics(state_ref.reverse_metrics.clone());
2703                    let server_state = server.state_handle();
2704                    let server_cancel = server.cancel_token();
2705
2706                    state_ref
2707                        .reverse_registry
2708                        .register(eggress_admin::ReverseServerEntry {
2709                            id: eggress_admin::ReverseServerId::from(rs_cfg.id.as_str()),
2710                            control_bind: rs_cfg.control_bind.to_string(),
2711                            state: server_state,
2712                        });
2713
2714                    let cancel_clone = cancel.clone();
2715                    tasks.spawn(async move {
2716                        let result = tokio::select! {
2717                            r = server.run() => r,
2718                            _ = cancel_clone.cancelled() => {
2719                                server_cancel.cancel();
2720                                Ok(())
2721                            }
2722                        };
2723                        if let Err(e) = result {
2724                            tracing::error!(error = %e, "reverse server error");
2725                        }
2726                    });
2727                }
2728
2729                for rc_cfg in reverse_clients {
2730                    let host = rc_cfg
2731                        .default_target_host
2732                        .clone()
2733                        .unwrap_or_else(|| "127.0.0.1".to_string());
2734                    let port = rc_cfg.default_target_port.unwrap_or(0);
2735
2736                    let parallel = rc_cfg.parallel_connections.max(1);
2737                    for conn_idx in 0..parallel {
2738                        if rc_cfg.pproxy_compat {
2739                            let client_config = eggress_protocol_reverse::compat_pproxy::PproxyBackwardClientConfig {
2740                                server_addr: rc_cfg.server_addr,
2741                                server_chain: rc_cfg.server_chain.clone(),
2742                                auth: eggress_protocol_reverse::compat_pproxy::raw_auth(
2743                                    rc_cfg.auth_username.as_deref(),
2744                                    rc_cfg.auth_password.as_deref(),
2745                                ),
2746                                reconnect_initial_ms: rc_cfg.reconnect_initial_ms,
2747                                reconnect_max_ms: rc_cfg.reconnect_max_ms,
2748                                read_timeout_ms: rc_cfg.read_timeout_ms,
2749                                target_connect_timeout_ms: 10_000,
2750                                server_framing:
2751                                    eggress_protocol_reverse::compat_pproxy::PproxyBackwardFraming::Raw,
2752                            };
2753                            let client =
2754                                eggress_protocol_reverse::compat_pproxy::PproxyBackwardClient::new(
2755                                    client_config,
2756                                    std::sync::Arc::new(
2757                                        crate::reverse::RouteEngineTargetResolver::new(
2758                                            routing.clone(),
2759                                            host.clone(),
2760                                            port,
2761                                            std::sync::Arc::from(rc_cfg.id.as_str()),
2762                                            Some(rc_cfg.server_addr),
2763                                        ),
2764                                    ),
2765                                );
2766                            let cancel_clone = cancel.clone();
2767                            let client_cancel = client.cancel_token();
2768                            let client_id = rc_cfg.id.clone();
2769                            let server_addr = rc_cfg.server_addr;
2770                            tasks.spawn(async move {
2771                                let result = tokio::select! {
2772                                    r = client.run() => r,
2773                                    _ = cancel_clone.cancelled() => {
2774                                        client_cancel.cancel();
2775                                        Ok(())
2776                                    }
2777                                };
2778                                if let Err(e) = result {
2779                                    tracing::error!(error = %e, client_id = %client_id, server = %server_addr, conn = conn_idx, "pproxy backward client error");
2780                                }
2781                            });
2782                            continue;
2783                        }
2784                        let client_config = eggress_protocol_reverse::client::ReverseClientConfig {
2785                            server_addr: rc_cfg.server_addr,
2786                            auth_username: rc_cfg.auth_username.clone(),
2787                            auth_password: rc_cfg.auth_password.clone(),
2788                            reconnect_initial_ms: rc_cfg.reconnect_initial_ms,
2789                            reconnect_max_ms: rc_cfg.reconnect_max_ms,
2790                            default_target_host: rc_cfg.default_target_host.clone(),
2791                            default_target_port: rc_cfg.default_target_port,
2792                            read_timeout_ms: rc_cfg.read_timeout_ms,
2793                            drain_grace_ms: rc_cfg.drain_grace_ms,
2794                            ..Default::default()
2795                        };
2796                        let mut client =
2797                            eggress_protocol_reverse::client::ReverseClient::new(client_config);
2798                        client.set_metrics(state_ref.reverse_metrics.clone());
2799
2800                        let resolver = crate::reverse::RouteEngineTargetResolver::new(
2801                            routing.clone(),
2802                            host.clone(),
2803                            port,
2804                            std::sync::Arc::from(rc_cfg.id.as_str()),
2805                            Some(rc_cfg.server_addr),
2806                        );
2807                        client.set_resolver(std::sync::Arc::new(resolver));
2808
2809                        let cancel_clone = cancel.clone();
2810                        let client_cancel = client.cancel_token();
2811                        let client_id = rc_cfg.id.clone();
2812                        let server_addr = rc_cfg.server_addr;
2813
2814                        tasks.spawn(async move {
2815                            let result = tokio::select! {
2816                                r = client.run() => r,
2817                                _ = cancel_clone.cancelled() => {
2818                                    client_cancel.cancel();
2819                                    Ok(())
2820                                }
2821                            };
2822                            if let Err(e) = result {
2823                                tracing::error!(error = %e, client_id = %client_id, server = %server_addr, conn = conn_idx, "reverse client error");
2824                            }
2825                        });
2826                    }
2827                }
2828            }
2829
2830            // Pre-bind admin listener before marking readiness so bind failures
2831            // are surfaced as startup errors rather than silent background failures.
2832            #[cfg(feature = "operations")]
2833            let pre_bound_admin = if let Some(ref admin_cfg) = admin_config {
2834                if admin_cfg.enabled {
2835                    let bind = admin_cfg.bind.clone();
2836                    let admin_cancel_token = admin_cancel.clone();
2837                    match eggress_admin::AdminServer::new(&bind, admin_cancel_token).await {
2838                        Ok(s) => Some(s),
2839                        Err(e) => {
2840                            return Err(RuntimeError::ListenerBind {
2841                                addr: bind,
2842                                source: std::io::Error::new(
2843                                    std::io::ErrorKind::AddrInUse,
2844                                    e.to_string(),
2845                                ),
2846                            });
2847                        }
2848                    }
2849                } else {
2850                    None
2851                }
2852            } else {
2853                None
2854            };
2855
2856            #[cfg(feature = "operations")]
2857            if let (Some(server), Some(admin_cfg)) = (pre_bound_admin, admin_config.as_ref()) {
2858                let metrics_enabled = admin_cfg.metrics;
2859                let state_ref = state_ref.clone();
2860                let provider: Arc<dyn AdminSnapshotProvider> = listener_infos_provider.clone();
2861                if let Ok(addr) = server.local_addr() {
2862                    match state_ref.admin_local_addr.lock() {
2863                        Ok(mut guard) => *guard = Some(addr),
2864                        Err(error) => {
2865                            tracing::warn!(
2866                                "admin listener address state was poisoned; resetting it: {error}"
2867                            );
2868                            let mut guard = error.into_inner();
2869                            *guard = Some(addr);
2870                            state_ref.admin_local_addr.clear_poison();
2871                        }
2872                    }
2873                }
2874                let admin_auth = admin_cfg.auth.clone();
2875                admin_tasks.spawn(async move {
2876                    let admin_state = eggress_admin::AdminState {
2877                        metrics: metrics_registry.clone(),
2878                        start_time: state_ref.start_time,
2879                        readiness: state_ref.readiness.clone(),
2880                        active_connections: Some(state_ref.active_connections.clone()),
2881                        provider,
2882                        udp_registry: state_ref.udp_registry.clone(),
2883                        #[cfg(feature = "reverse")]
2884                        reverse_registry: state_ref.reverse_registry.clone(),
2885                        #[cfg(not(feature = "reverse"))]
2886                        reverse_registry: std::sync::Arc::new(eggress_admin::ReverseRegistry::new()),
2887                        metrics_enabled,
2888                        auth: admin_auth,
2889                    };
2890                    if let Err(e) = server.run(admin_state).await {
2891                        tracing::error!("admin server error: {e}");
2892                    }
2893                });
2894            }
2895
2896            #[cfg(unix)]
2897            {
2898                let mut sigterm =
2899                    tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate());
2900                let mut sighup =
2901                    tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup());
2902
2903                if let Err(ref e) = sigterm {
2904                    tracing::warn!("failed to register SIGTERM handler: {e}");
2905                }
2906                if let Err(ref e) = sighup {
2907                    tracing::warn!("failed to register SIGHUP handler: {e}");
2908                }
2909
2910                // Readiness also means signal handling is installed. This
2911                // prevents a reload signal from racing startup.
2912                readiness.store(true, Ordering::Release);
2913
2914                loop {
2915                    tokio::select! {
2916                        _ = cancel.cancelled() => {
2917                            tracing::info!("shutdown requested via cancel token");
2918                            break;
2919                        }
2920                        _ = tokio::signal::ctrl_c() => {
2921                            tracing::info!("shutdown signal received");
2922                            break;
2923                        }
2924                        _ = async { sigterm.as_mut().ok()?.recv().await }, if sigterm.is_ok() => {
2925                            tracing::info!("shutdown signal received");
2926                            break;
2927                        }
2928                        _ = async { sighup.as_mut().ok()?.recv().await }, if sighup.is_ok() && !config_path.is_empty() => {
2929                            tracing::info!("reload signal received, reloading config from {config_path}");
2930                            let prev_snapshot = snapshot.load();
2931                            let prev_ref: Option<&CompiledRuntimeSnapshot> = Some(&prev_snapshot);
2932                            let config_path_clone = config_path.clone();
2933                            let load_result = tokio::task::spawn_blocking(move || {
2934                                eggress_config::compile::load_and_compile(&config_path_clone)
2935                            }).await;
2936                            match load_result {
2937                                Ok(Ok(new_rt_config)) => {
2938                                    // Classify unsupported changes before building a new snapshot.
2939                                    if let Err(reason) = classify_reload_config(
2940                                        &prev_snapshot.listeners,
2941                                        &prev_snapshot.timeouts,
2942                                        prev_snapshot.admin.as_ref(),
2943                                        &new_rt_config,
2944                                    ) {
2945                                        tracing::error!("reload rejected: {reason}");
2946                                        metrics.record_reload(false);
2947                                        continue;
2948                                    }
2949                                    match compile_runtime_snapshot(&new_rt_config, prev_ref) {
2950                                        Ok(new_snapshot) => {
2951                                            let upstream_count = new_snapshot.upstreams.len();
2952                                            let gen = new_snapshot.generation;
2953
2954                                            // Snapshot must be published before the router swap so any reader
2955                                            // observing the new generation via
2956                                            // `snapshot.load()` also sees the
2957                                            // matching router.
2958                                            let new_snapshot = Arc::new(new_snapshot);
2959                                            snapshot.store(new_snapshot.clone());
2960                                            routing.swap_arc(new_snapshot.router.clone());
2961                                            #[cfg(feature = "operations")]
2962                                            state_ref.publish_admin_snapshot(new_snapshot.clone());
2963
2964                                            metrics.set_config_generation(gen);
2965                                            metrics.record_reload(true);
2966                                            eggress_protocol_http::H2_POOL_REGISTRY.clear();
2967
2968                                            if let Ok(mut guard) = health_clone.lock() {
2969                                                if let Some(ref mut hm) = *guard {
2970                                                    hm.stop_all();
2971                                                }
2972                                                let upstream_runtimes: Vec<Arc<UpstreamRuntime>> = snapshot
2973                                                    .load()
2974                                                    .upstreams
2975                                                    .values()
2976                                                    .cloned()
2977                                                    .collect();
2978                                                if !upstream_runtimes.is_empty() {
2979                                                    let mut hm = HealthManager::new(health_cancel.clone());
2980                                                    hm.start_probes(&upstream_runtimes);
2981                                                    *guard = Some(hm);
2982                                                } else {
2983                                                    *guard = None;
2984                                                }
2985                                            }
2986
2987                                            tracing::info!(
2988                                                generation = gen,
2989                                                upstreams = upstream_count,
2990                                                "config reloaded successfully"
2991                                            );
2992                                        }
2993                                        Err(e) => {
2994                                            metrics.record_reload(false);
2995                                            tracing::error!("reload failed (snapshot build): {e}");
2996                                        }
2997                                    }
2998                                }
2999                                Ok(Err(e)) => {
3000                                    metrics.record_reload(false);
3001                                    tracing::error!("reload failed (config load): {e}");
3002                                }
3003                                Err(join_err) => {
3004                                    metrics.record_reload(false);
3005                                    tracing::error!("reload task panicked: {join_err}");
3006                                }
3007                            }
3008                        }
3009                    }
3010                }
3011            }
3012
3013            #[cfg(not(unix))]
3014            {
3015                readiness.store(true, Ordering::Release);
3016                tokio::select! {
3017                    _ = cancel.cancelled() => {
3018                        tracing::info!("shutdown requested via cancel token");
3019                    }
3020                    _ = tokio::signal::ctrl_c() => {
3021                        tracing::info!("shutdown signal received");
3022                    }
3023                }
3024            }
3025
3026            // 1. Set readiness false (admin /-/ready will report 503 during drain)
3027            readiness.store(false, Ordering::Release);
3028
3029            // 2. Stop listeners (no new connections accepted)
3030            listener_cancel.cancel();
3031
3032            // 3. Stop health probes
3033            health_cancel.cancel();
3034
3035            // 4. Close all UDP associations
3036            state_ref.udp_registry.close_all().await;
3037
3038            // 5. Wait for UDP relay tasks to complete
3039            state_ref.udp_tasks.close();
3040            let _ = tokio::time::timeout(shutdown_grace, state_ref.udp_tasks.wait()).await;
3041
3042            // 6. Wait for listener accept loops to exit so they cannot hand
3043            //    new connections to the connection tracker.
3044            tasks.close();
3045            tasks.wait().await;
3046
3047            // 7. Drain active connections within the grace period; force-cancel
3048            //    afterwards. Admin stays up through this window so operators
3049            //    can observe drain progress via /-/ready, /-/status, /metrics.
3050            tracing::info!("draining active connections");
3051
3052            let deadline = tokio::time::Instant::now() + shutdown_grace;
3053            loop {
3054                let active = active_connections.load(Ordering::Acquire);
3055                if active == 0 {
3056                    tracing::info!("all connections drained");
3057                    break;
3058                }
3059                if tokio::time::Instant::now() >= deadline {
3060                    tracing::warn!(active, "drain timeout reached, forcing shutdown");
3061                    connection_cancel.cancel();
3062                    break;
3063                }
3064                tokio::time::sleep(Duration::from_millis(100)).await;
3065            }
3066
3067            // 8. Wait for connection tasks (either drained naturally or force-cancelled)
3068            connection_tasks.close();
3069            connection_tasks.wait().await;
3070
3071            #[cfg(feature = "ssh")]
3072            ssh_sessions.shutdown().await;
3073
3074            // 9. Now that the proxy has fully stopped accepting and serving
3075            //    traffic, stop the admin server. /-/ready has been reporting
3076            //    503 since step 1.
3077            admin_cancel.cancel();
3078            admin_tasks.close();
3079            admin_tasks.wait().await;
3080
3081            #[cfg(feature = "operations")]
3082            if let Some(mut proxy) = compatibility_system_proxy {
3083                proxy.restore().map_err(RuntimeError::Other)?;
3084            }
3085
3086            Ok::<_, RuntimeError>(())
3087        };
3088
3089        let result = if tokio::runtime::Handle::try_current().is_err() {
3090            // Caller is not inside a tokio runtime; create one and block on it.
3091            let rt = tokio::runtime::Runtime::new()?;
3092            rt.block_on(run_async)
3093        } else {
3094            // Caller is already inside a tokio runtime. Driving a long-lived
3095            // supervisor body via Handle::current().block_on from a worker
3096            // thread would panic or deadlock. Run on a dedicated OS thread
3097            // with its own runtime.
3098            std::thread::Builder::new()
3099                .name("eggress-supervisor".to_string())
3100                .spawn(move || -> Result<(), RuntimeError> {
3101                    let rt = tokio::runtime::Builder::new_multi_thread()
3102                        .enable_all()
3103                        .build()
3104                        .map_err(RuntimeError::RuntimeInit)?;
3105                    rt.block_on(run_async)
3106                })
3107                .map_err(RuntimeError::RuntimeInit)?
3108                .join()
3109                .map_err(|payload| {
3110                    let message = payload
3111                        .downcast_ref::<String>()
3112                        .map(String::as_str)
3113                        .or_else(|| payload.downcast_ref::<&'static str>().copied())
3114                        .unwrap_or("unknown panic payload");
3115                    RuntimeError::Other(format!("supervisor thread panicked: {message}"))
3116                })?
3117        };
3118
3119        match &result {
3120            Ok(()) => tracing::info!("eggress stopped"),
3121            Err(e) => tracing::error!(error = %e, "eggress stopped with error"),
3122        }
3123
3124        result
3125    }
3126}
3127
3128#[cfg(test)]
3129mod tests {
3130    use super::*;
3131    use std::io::Write;
3132    use tempfile::NamedTempFile;
3133
3134    use crate::snapshot::compile_runtime_snapshot;
3135    use eggress_config::compile::{GroupFallback, ProcessConfig, RuntimeConfig, TimeoutConfig};
3136    use eggress_routing::scheduler::SchedulerKind;
3137    use eggress_routing::{MatchExpr, RouteActionSpec, RuleId, UpstreamGroupId};
3138
3139    fn write_config(content: &str) -> NamedTempFile {
3140        let mut f = NamedTempFile::new().unwrap();
3141        f.write_all(content.as_bytes()).unwrap();
3142        f.flush().unwrap();
3143        f
3144    }
3145
3146    #[test]
3147    fn build_router_direct_only() {
3148        let rt_config = RuntimeConfig {
3149            process: ProcessConfig::default(),
3150            timeouts: TimeoutConfig::default(),
3151            listeners: vec![],
3152            upstreams: vec![],
3153            groups: vec![],
3154            rules: vec![],
3155            default_action: RouteActionSpec::Direct,
3156            admin: None,
3157            reverse_servers: vec![],
3158            reverse_clients: vec![],
3159        };
3160        let snap = compile_runtime_snapshot(&rt_config, None).unwrap();
3161        assert!(snap.router.rules().is_empty());
3162    }
3163
3164    #[test]
3165    fn build_router_with_group_references_unknown_upstream() {
3166        let rt_config = RuntimeConfig {
3167            process: ProcessConfig::default(),
3168            timeouts: TimeoutConfig::default(),
3169            listeners: vec![],
3170            upstreams: vec![],
3171            groups: vec![eggress_config::compile::UpstreamGroupConfig {
3172                id: UpstreamGroupId(Arc::from("main")),
3173                scheduler: SchedulerKind::RoundRobin,
3174                members: vec!["nonexistent".to_string()],
3175                fallback: GroupFallback::Reject,
3176            }],
3177            rules: vec![],
3178            default_action: RouteActionSpec::Direct,
3179            admin: None,
3180            reverse_servers: vec![],
3181            reverse_clients: vec![],
3182        };
3183        let result = compile_runtime_snapshot(&rt_config, None);
3184        assert!(result.is_err(), "expected error, got Ok");
3185        let err_msg = result.err().unwrap().to_string();
3186        assert!(err_msg.contains("nonexistent"));
3187    }
3188
3189    #[test]
3190    fn build_router_with_valid_group() {
3191        let rt_config = RuntimeConfig {
3192            process: ProcessConfig::default(),
3193            timeouts: TimeoutConfig::default(),
3194            listeners: vec![],
3195            upstreams: vec![eggress_config::compile::UpstreamConfig {
3196                id: "proxy1".to_string(),
3197                chain: eggress_uri::ProxyChainSpec { hops: vec![] },
3198                health: eggress_routing::health::HealthConfig::default(),
3199                h2: None,
3200            }],
3201            groups: vec![eggress_config::compile::UpstreamGroupConfig {
3202                id: UpstreamGroupId(Arc::from("main")),
3203                scheduler: SchedulerKind::RoundRobin,
3204                members: vec!["proxy1".to_string()],
3205                fallback: GroupFallback::Reject,
3206            }],
3207            rules: vec![],
3208            default_action: RouteActionSpec::Direct,
3209            admin: None,
3210            reverse_servers: vec![],
3211            reverse_clients: vec![],
3212        };
3213        let snap = compile_runtime_snapshot(&rt_config, None).unwrap();
3214        assert!(snap.router.rules().is_empty());
3215    }
3216
3217    #[test]
3218    fn build_router_rule_references_unknown_group() {
3219        let rt_config = RuntimeConfig {
3220            process: ProcessConfig::default(),
3221            timeouts: TimeoutConfig::default(),
3222            listeners: vec![],
3223            upstreams: vec![],
3224            groups: vec![],
3225            rules: vec![eggress_routing::CompiledRule {
3226                id: RuleId(Arc::from("r1")),
3227                matcher: MatchExpr::Any,
3228                action: RouteActionSpec::UpstreamGroup(UpstreamGroupId(Arc::from("missing"))),
3229            }],
3230            default_action: RouteActionSpec::Direct,
3231            admin: None,
3232            reverse_servers: vec![],
3233            reverse_clients: vec![],
3234        };
3235        let result = compile_runtime_snapshot(&rt_config, None);
3236        assert!(result.is_err(), "expected error, got Ok");
3237        let err_msg = result.err().unwrap().to_string();
3238        assert!(err_msg.contains("missing"));
3239    }
3240
3241    #[tokio::test]
3242    async fn load_config_start_supervisor() {
3243        let config = r#"
3244version = 1
3245
3246[[listeners]]
3247name = "test"
3248bind = "127.0.0.1:0"
3249protocols = ["http"]
3250"#;
3251        let f = write_config(config);
3252        let path = f.path().to_str().unwrap();
3253        let result = ServiceSupervisor::start(path);
3254        assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
3255    }
3256
3257    #[test]
3258    fn active_connections_counter_increments_and_decrements() {
3259        let active = Arc::new(AtomicU64::new(0));
3260        assert_eq!(active.load(Ordering::Acquire), 0);
3261        active.fetch_add(1, Ordering::AcqRel);
3262        assert_eq!(active.load(Ordering::Acquire), 1);
3263        active.fetch_add(1, Ordering::AcqRel);
3264        assert_eq!(active.load(Ordering::Acquire), 2);
3265        active.fetch_sub(1, Ordering::Release);
3266        assert_eq!(active.load(Ordering::Acquire), 1);
3267        active.fetch_sub(1, Ordering::Release);
3268        assert_eq!(active.load(Ordering::Acquire), 0);
3269    }
3270
3271    #[test]
3272    fn active_connections_guard_releases_on_panic() {
3273        let active = Arc::new(AtomicU64::new(0));
3274        let result = std::panic::catch_unwind({
3275            let active = active.clone();
3276            move || {
3277                let _guard = ActiveConnectionGuard::new(active);
3278                panic!("connection task panic");
3279            }
3280        });
3281        assert!(result.is_err());
3282        assert_eq!(active.load(Ordering::Acquire), 0);
3283    }
3284
3285    #[test]
3286    fn readiness_flag_controls_ready_endpoint() {
3287        let readiness = Arc::new(AtomicBool::new(true));
3288        assert!(readiness.load(Ordering::Relaxed));
3289        readiness.store(false, Ordering::Relaxed);
3290        assert!(!readiness.load(Ordering::Relaxed));
3291        readiness.store(true, Ordering::Relaxed);
3292        assert!(readiness.load(Ordering::Relaxed));
3293    }
3294
3295    #[test]
3296    fn reload_rejects_listener_name_change() {
3297        let config1 = r#"
3298version = 1
3299
3300[[listeners]]
3301name = "http-in"
3302bind = "127.0.0.1:8080"
3303protocols = ["http"]
3304"#;
3305        let config2 = r#"
3306version = 1
3307
3308[[listeners]]
3309name = "http-changed"
3310bind = "127.0.0.1:8080"
3311protocols = ["http"]
3312"#;
3313        let f1 = write_config(config1);
3314        let f2 = write_config(config2);
3315        let path1 = f1.path().to_str().unwrap();
3316        let path2 = f2.path().to_str().unwrap();
3317
3318        let sup = ServiceSupervisor::start(path1).unwrap();
3319        let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
3320        let result = sup.classify_reload(&new_config);
3321        assert!(result.is_err(), "listener name change should be rejected");
3322        assert!(result.unwrap_err().contains("name changed"));
3323    }
3324
3325    #[test]
3326    fn reload_rejects_listener_bind_change() {
3327        let config1 = r#"
3328version = 1
3329
3330[[listeners]]
3331name = "http-in"
3332bind = "127.0.0.1:8080"
3333protocols = ["http"]
3334"#;
3335        let config2 = r#"
3336version = 1
3337
3338[[listeners]]
3339name = "http-in"
3340bind = "127.0.0.1:9090"
3341protocols = ["http"]
3342"#;
3343        let f1 = write_config(config1);
3344        let f2 = write_config(config2);
3345        let path1 = f1.path().to_str().unwrap();
3346        let path2 = f2.path().to_str().unwrap();
3347
3348        let sup = ServiceSupervisor::start(path1).unwrap();
3349        let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
3350        let result = sup.classify_reload(&new_config);
3351        assert!(result.is_err(), "listener bind change should be rejected");
3352        assert!(result.unwrap_err().contains("bind"));
3353    }
3354
3355    #[test]
3356    fn reload_accepts_unchanged_listeners() {
3357        let config = r#"
3358version = 1
3359
3360[[listeners]]
3361name = "http-in"
3362bind = "127.0.0.1:8080"
3363protocols = ["http"]
3364"#;
3365        let f = write_config(config);
3366        let path = f.path().to_str().unwrap();
3367
3368        let sup = ServiceSupervisor::start(path).unwrap();
3369        let new_config = eggress_config::compile::load_and_compile(path).unwrap();
3370        let result = sup.classify_reload(&new_config);
3371        assert!(result.is_ok(), "unchanged listeners should be accepted");
3372    }
3373
3374    #[test]
3375    fn reload_rejects_udp_topology_changes() {
3376        let config_without_udp = r#"
3377version = 1
3378
3379[[listeners]]
3380name = "socks-in"
3381bind = "127.0.0.1:8080"
3382protocols = ["socks5"]
3383"#;
3384        let config_with_udp = r#"
3385version = 1
3386
3387[[listeners]]
3388name = "socks-in"
3389bind = "127.0.0.1:8080"
3390protocols = ["socks5"]
3391
3392[listeners.udp]
3393enabled = true
3394bind = "127.0.0.1:0"
3395"#;
3396        let f1 = write_config(config_without_udp);
3397        let f2 = write_config(config_with_udp);
3398        let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
3399        let new_config =
3400            eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
3401
3402        let result = sup.classify_reload(&new_config);
3403        assert!(result.is_err());
3404        assert!(result.unwrap_err().contains("UDP"));
3405    }
3406
3407    #[test]
3408    fn reload_rejects_timeout_change() {
3409        let config1 = r#"
3410version = 1
3411
3412[timeouts]
3413handshake = "10s"
3414
3415[[listeners]]
3416name = "http-in"
3417bind = "127.0.0.1:8080"
3418protocols = ["http"]
3419"#;
3420        let config2 = r#"
3421version = 1
3422
3423[timeouts]
3424handshake = "5s"
3425
3426[[listeners]]
3427name = "http-in"
3428bind = "127.0.0.1:8080"
3429protocols = ["http"]
3430"#;
3431        let f1 = write_config(config1);
3432        let f2 = write_config(config2);
3433        let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
3434        let new_config =
3435            eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
3436
3437        let result = sup.classify_reload(&new_config);
3438        assert!(result.is_err());
3439        assert!(result.unwrap_err().contains("timeout"));
3440    }
3441
3442    #[test]
3443    fn reload_rejects_admin_bind_change() {
3444        let config1 = r#"
3445version = 1
3446
3447[[listeners]]
3448name = "http-in"
3449bind = "127.0.0.1:8080"
3450protocols = ["http"]
3451
3452[admin]
3453bind = "127.0.0.1:9090"
3454enabled = false
3455"#;
3456        let config2 = r#"
3457version = 1
3458
3459[[listeners]]
3460name = "http-in"
3461bind = "127.0.0.1:8080"
3462protocols = ["http"]
3463
3464[admin]
3465bind = "127.0.0.1:9091"
3466enabled = false
3467"#;
3468        let f1 = write_config(config1);
3469        let f2 = write_config(config2);
3470        let sup = ServiceSupervisor::start(f1.path().to_str().unwrap()).unwrap();
3471        let new_config =
3472            eggress_config::compile::load_and_compile(f2.path().to_str().unwrap()).unwrap();
3473
3474        let result = sup.classify_reload(&new_config);
3475        assert!(result.is_err(), "admin bind change should be rejected");
3476        assert!(result.unwrap_err().contains("admin"));
3477    }
3478
3479    #[test]
3480    fn reload_rejects_listener_count_change() {
3481        let config1 = r#"
3482version = 1
3483
3484[[listeners]]
3485name = "http-in"
3486bind = "127.0.0.1:8080"
3487protocols = ["http"]
3488"#;
3489        let config2 = r#"
3490version = 1
3491
3492[[listeners]]
3493name = "http-in"
3494bind = "127.0.0.1:8080"
3495protocols = ["http"]
3496
3497[[listeners]]
3498name = "socks-in"
3499bind = "127.0.0.1:1080"
3500protocols = ["socks5"]
3501"#;
3502        let f1 = write_config(config1);
3503        let f2 = write_config(config2);
3504        let path1 = f1.path().to_str().unwrap();
3505        let path2 = f2.path().to_str().unwrap();
3506
3507        let sup = ServiceSupervisor::start(path1).unwrap();
3508        let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
3509        let result = sup.classify_reload(&new_config);
3510        assert!(result.is_err(), "listener count change should be rejected");
3511        assert!(result.unwrap_err().contains("listener count"));
3512    }
3513
3514    #[test]
3515    fn reload_rejects_transparent_enabled_change() {
3516        let config1 = r#"
3517version = 1
3518
3519[[listeners]]
3520name = "http-in"
3521bind = "127.0.0.1:8080"
3522protocols = ["http"]
3523"#;
3524        let config2 = r#"
3525version = 1
3526
3527[[listeners]]
3528name = "http-in"
3529bind = "127.0.0.1:8080"
3530protocols = ["http"]
3531
3532[listeners.transparent]
3533enabled = true
3534"#;
3535        let f1 = write_config(config1);
3536        let f2 = write_config(config2);
3537        let path1 = f1.path().to_str().unwrap();
3538        let path2 = f2.path().to_str().unwrap();
3539
3540        let sup = ServiceSupervisor::start(path1).unwrap();
3541        let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
3542        let result = sup.classify_reload(&new_config);
3543        assert!(
3544            result.is_err(),
3545            "transparent enabled change should be rejected"
3546        );
3547        assert!(result.unwrap_err().contains("transparent"));
3548    }
3549
3550    #[test]
3551    fn reload_rejects_unix_path_change() {
3552        let config1 = r#"
3553version = 1
3554
3555[[listeners]]
3556name = "http-in"
3557bind = "127.0.0.1:8080"
3558protocols = ["http"]
3559
3560[listeners.unix]
3561path = "/tmp/eggress.sock"
3562"#;
3563        let config2 = r#"
3564version = 1
3565
3566[[listeners]]
3567name = "http-in"
3568bind = "127.0.0.1:8080"
3569protocols = ["http"]
3570
3571[listeners.unix]
3572path = "/tmp/eggress-new.sock"
3573"#;
3574        let f1 = write_config(config1);
3575        let f2 = write_config(config2);
3576        let path1 = f1.path().to_str().unwrap();
3577        let path2 = f2.path().to_str().unwrap();
3578
3579        let sup = ServiceSupervisor::start(path1).unwrap();
3580        let new_config = eggress_config::compile::load_and_compile(path2).unwrap();
3581        let result = sup.classify_reload(&new_config);
3582        assert!(result.is_err(), "unix path change should be rejected");
3583        assert!(result.unwrap_err().contains("unix socket path"));
3584    }
3585
3586    #[test]
3587    fn compute_advertise_explicit() {
3588        let result = compute_advertise_ip(
3589            Some("10.0.0.1".parse().unwrap()),
3590            "0.0.0.0".parse().unwrap(),
3591            Some("127.0.0.1:5000".parse().unwrap()),
3592        );
3593        assert_eq!(
3594            result.unwrap(),
3595            std::net::IpAddr::V4("10.0.0.1".parse().unwrap())
3596        );
3597    }
3598
3599    #[test]
3600    fn compute_advertise_bind_ip() {
3601        let result = compute_advertise_ip(
3602            None,
3603            "192.168.1.1".parse().unwrap(),
3604            Some("127.0.0.1:5000".parse().unwrap()),
3605        );
3606        assert_eq!(
3607            result.unwrap(),
3608            std::net::IpAddr::V4("192.168.1.1".parse().unwrap())
3609        );
3610    }
3611
3612    #[test]
3613    fn compute_advertise_loopback_fallback() {
3614        let result = compute_advertise_ip(
3615            None,
3616            "0.0.0.0".parse().unwrap(),
3617            Some("127.0.0.1:5000".parse().unwrap()),
3618        );
3619        assert_eq!(
3620            result.unwrap(),
3621            std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
3622        );
3623    }
3624
3625    #[test]
3626    fn compute_advertise_unspecified_non_loopback_rejected() {
3627        let result = compute_advertise_ip(
3628            None,
3629            "0.0.0.0".parse().unwrap(),
3630            Some("192.168.1.10:5000".parse().unwrap()),
3631        );
3632        assert!(
3633            result.is_err(),
3634            "non-loopback with unspecified bind should fail"
3635        );
3636    }
3637
3638    #[test]
3639    fn compute_advertise_without_tcp_peer_is_rejected() {
3640        let result = compute_advertise_ip(None, "0.0.0.0".parse().unwrap(), None);
3641        assert!(result.is_err());
3642    }
3643
3644    #[test]
3645    fn compute_advertise_ipv6_loopback() {
3646        let result = compute_advertise_ip(
3647            None,
3648            "::".parse().unwrap(),
3649            Some("[::1]:5000".parse().unwrap()),
3650        );
3651        assert_eq!(
3652            result.unwrap(),
3653            std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
3654        );
3655    }
3656
3657    #[test]
3658    fn compute_advertise_preserves_unspecified_bind_family() {
3659        let result = compute_advertise_ip(
3660            None,
3661            "0.0.0.0".parse().unwrap(),
3662            Some("[::ffff:127.0.0.1]:5000".parse().unwrap()),
3663        );
3664        assert_eq!(
3665            result.unwrap(),
3666            std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
3667        );
3668    }
3669
3670    #[test]
3671    fn compute_advertise_explicit_overrides_bind() {
3672        let result = compute_advertise_ip(
3673            Some("10.0.0.1".parse().unwrap()),
3674            "192.168.1.1".parse().unwrap(),
3675            Some("127.0.0.1:5000".parse().unwrap()),
3676        );
3677        assert_eq!(
3678            result.unwrap(),
3679            std::net::IpAddr::V4("10.0.0.1".parse().unwrap())
3680        );
3681    }
3682}