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
21const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(100);
25
26async 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
39struct 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
64struct 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#[derive(Debug)]
96pub enum ReloadResult {
97 Applied { generation: u64, upstreams: usize },
99 Rejected { reason: String },
101 Failed { error: String },
103}
104
105#[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
200fn 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 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 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)] 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
506fn 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 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 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#[derive(Debug, Clone, Default)]
777pub struct CompatibilityOptions {
778 pub compatibility_mode: bool,
780 pub auth_timeout: Option<Duration>,
781 pub system_proxy: bool,
782 pub debug: bool,
784 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 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 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 #[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 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 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 self.rt_config = new_rt_config;
1088
1089 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 {
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 #[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 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 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 {
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 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 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 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 #[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 unix_listener.cleanup().unwrap_or_else(|e| {
2117 tracing::warn!("failed to cleanup unix socket: {e}");
2118 });
2119 });
2120 }
2121
2122 #[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 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 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 #[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 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 #[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.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 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 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 readiness.store(false, Ordering::Release);
3028
3029 listener_cancel.cancel();
3031
3032 health_cancel.cancel();
3034
3035 state_ref.udp_registry.close_all().await;
3037
3038 state_ref.udp_tasks.close();
3040 let _ = tokio::time::timeout(shutdown_grace, state_ref.udp_tasks.wait()).await;
3041
3042 tasks.close();
3045 tasks.wait().await;
3046
3047 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 connection_tasks.close();
3069 connection_tasks.wait().await;
3070
3071 #[cfg(feature = "ssh")]
3072 ssh_sessions.shutdown().await;
3073
3074 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 let rt = tokio::runtime::Runtime::new()?;
3092 rt.block_on(run_async)
3093 } else {
3094 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}