Skip to main content

eggress_protocol_reverse/
server.rs

1use crate::metrics::ReverseMetrics;
2use crate::{redact_auth, server_auth_handshake, ControlState, ProtocolError};
3use std::net::{IpAddr, SocketAddr};
4use std::sync::atomic::{AtomicU32, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::net::{TcpListener, TcpStream};
8use tokio::sync::mpsc;
9use tokio::task::JoinSet;
10use tokio_util::sync::CancellationToken;
11use tracing::{debug, error, info, warn};
12
13const AUTH_FAILURE_DELAY: Duration = Duration::from_millis(100);
14
15/// Configuration for a reverse proxy server (acceptor side).
16///
17/// The server accepts control connections from remote clients and dispatches
18/// externally-accepted connections back through the control channel.
19#[derive(Clone)]
20pub struct ReverseServerConfig {
21    /// Address to bind the control listener on.
22    pub control_bind: SocketAddr,
23    /// Address to bind the external listener on (for clients to connect to).
24    pub external_bind: Option<SocketAddr>,
25    /// Optional username for authentication.
26    pub auth_username: Option<String>,
27    /// Optional password for authentication.
28    pub auth_password: Option<String>,
29    /// Maximum concurrent control connections.
30    pub max_control_connections: u32,
31    /// Read timeout in milliseconds (for idle control connections).
32    pub read_timeout_ms: u64,
33    /// Optional list of allowed external bind addresses. When `Some` and
34    /// non-empty, the server rejects bind addresses not in the list.
35    /// When `None` or empty, no allowlist enforcement is applied.
36    pub allow_bind: Option<Vec<SocketAddr>>,
37    /// Maximum number of external listeners per control client. Currently
38    /// pproxy supports one external listener per control connection, so
39    /// defaults to 1.
40    pub max_listeners_per_client: u32,
41    /// Maximum concurrent streams per external listener.
42    pub max_streams_per_listener: u32,
43    /// Maximum number of concurrent external clients queued while waiting
44    /// for a control connection. Excess clients are dropped.
45    pub max_pending_external: u32,
46    /// Optional TLS for the control channel. When present, the TCP control
47    /// stream is wrapped with Rustls before reverse framing/authentication.
48    /// External listener traffic remains plaintext TCP.
49    pub tls: Option<crate::tls::ReverseServerTlsConfig>,
50}
51
52impl Default for ReverseServerConfig {
53    fn default() -> Self {
54        Self {
55            control_bind: "127.0.0.1:0".parse().unwrap(),
56            external_bind: None,
57            auth_username: None,
58            auth_password: None,
59            max_control_connections: 256,
60            read_timeout_ms: 300_000,
61            allow_bind: None,
62            max_listeners_per_client: 1,
63            max_streams_per_listener: 1024,
64            max_pending_external: 1024,
65            tls: None,
66        }
67    }
68}
69
70impl std::fmt::Debug for ReverseServerConfig {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        // auth_password and TLS key material are never printed.
73        f.debug_struct("ReverseServerConfig")
74            .field("control_bind", &self.control_bind)
75            .field("external_bind", &self.external_bind)
76            .field("auth_username", &self.auth_username)
77            .field(
78                "auth_password",
79                &self.auth_password.as_deref().map(|_| "****"),
80            )
81            .field("max_control_connections", &self.max_control_connections)
82            .field("read_timeout_ms", &self.read_timeout_ms)
83            .field("allow_bind", &self.allow_bind)
84            .field("max_listeners_per_client", &self.max_listeners_per_client)
85            .field("max_streams_per_listener", &self.max_streams_per_listener)
86            .field("max_pending_external", &self.max_pending_external)
87            .field("tls", &self.tls)
88            .finish()
89    }
90}
91
92impl ReverseServerConfig {
93    /// Returns true if the supplied external bind address is allowed by the
94    /// configured `allow_bind` policy. When `allow_bind` is `None` or empty,
95    /// all addresses are allowed.
96    pub fn is_bind_allowed(&self, addr: SocketAddr) -> bool {
97        match &self.allow_bind {
98            None => true,
99            Some(list) if list.is_empty() => true,
100            Some(list) => list.iter().any(|allowed| same_bind(allowed, &addr)),
101        }
102    }
103
104    /// Returns true if the address is loopback (127.0.0.0/8 or ::1).
105    pub fn is_loopback(addr: SocketAddr) -> bool {
106        match addr.ip() {
107            IpAddr::V4(v4) => v4.is_loopback(),
108            IpAddr::V6(v6) => v6.is_loopback(),
109        }
110    }
111
112    /// Validate this configuration. Returns an error if the configuration is
113    /// unsafe (e.g. external bind on a non-loopback address without
114    /// authentication and without an explicit `allow_bind` allowlist).
115    ///
116    /// This is a defense-in-depth check: it catches misconfigurations that
117    /// would otherwise expose the reverse proxy to unauthenticated network
118    /// clients. TLS material is validated here as well so impossible
119    /// combinations fail before any socket is bound.
120    pub fn validate(&self) -> Result<(), ProtocolError> {
121        if let Some(external) = self.external_bind {
122            // Non-loopback external bind requires BOTH authentication
123            // credentials AND a non-empty `allow_bind` allowlist. This
124            // prevents accidentally exposing the reverse proxy to the
125            // local network without operator intent.
126            if !Self::is_loopback(external) {
127                let has_auth = self.auth_username.as_deref().is_some_and(|s| !s.is_empty())
128                    && self.auth_password.as_deref().is_some_and(|s| !s.is_empty());
129                let has_allowlist = matches!(&self.allow_bind, Some(list) if !list.is_empty());
130                if !has_auth {
131                    return Err(ProtocolError::ConfigInvalid(format!(
132                        "reverse server external_bind={external} is non-loopback but no \
133                         authentication is configured; set auth_username/auth_password or \
134                         bind to loopback"
135                    )));
136                }
137                if !has_allowlist {
138                    return Err(ProtocolError::ConfigInvalid(format!(
139                        "reverse server external_bind={external} is non-loopback but \
140                         allow_bind is empty; configure an explicit allowlist"
141                    )));
142                }
143            }
144        }
145        if let Some(ref tls) = self.tls {
146            tls.validate()?;
147        }
148        Ok(())
149    }
150}
151
152fn same_bind(a: &SocketAddr, b: &SocketAddr) -> bool {
153    a.port() == b.port()
154        && match (a.ip(), b.ip()) {
155            (IpAddr::V4(a4), IpAddr::V4(b4)) => a4 == b4,
156            (IpAddr::V6(a6), IpAddr::V6(b6)) => a6 == b6,
157            _ => false,
158        }
159}
160
161/// Active state of the reverse server, exposed for tests and admin hooks.
162#[derive(Debug, Default)]
163pub struct ReverseServerState {
164    /// Number of currently-active (accepted, awaiting use) control connections.
165    pub active_control: AtomicU32,
166    /// Number of currently-active external streams being relayed.
167    pub active_streams: AtomicU32,
168    /// Number of external clients waiting for a control connection.
169    pub pending_external: AtomicU32,
170    /// Number of listeners denied because of allow_bind.
171    pub denied_bind: AtomicU32,
172    /// Number of streams dropped because max_streams_per_listener was reached.
173    pub dropped_stream_limit: AtomicU32,
174    /// Number of external clients dropped because max_pending_external was reached.
175    pub dropped_pending_limit: AtomicU32,
176}
177
178impl ReverseServerState {
179    /// Snapshot of the counters for admin/log display.
180    pub fn snapshot(&self) -> ReverseServerStateSnapshot {
181        ReverseServerStateSnapshot {
182            active_control: self.active_control.load(Ordering::Relaxed),
183            active_streams: self.active_streams.load(Ordering::Relaxed),
184            pending_external: self.pending_external.load(Ordering::Relaxed),
185            denied_bind: self.denied_bind.load(Ordering::Relaxed),
186            dropped_stream_limit: self.dropped_stream_limit.load(Ordering::Relaxed),
187            dropped_pending_limit: self.dropped_pending_limit.load(Ordering::Relaxed),
188        }
189    }
190}
191
192/// Plain-data snapshot of [`ReverseServerState`].
193#[derive(Debug, Clone, serde::Serialize)]
194pub struct ReverseServerStateSnapshot {
195    pub active_control: u32,
196    pub active_streams: u32,
197    pub pending_external: u32,
198    pub denied_bind: u32,
199    pub dropped_stream_limit: u32,
200    pub dropped_pending_limit: u32,
201}
202
203/// The reverse proxy server (acceptor side).
204///
205/// Accepts control connections from reverse clients and external clients,
206/// relaying traffic between them. Each control connection carries exactly
207/// one proxy session (matching pproxy's backward model).
208pub struct ReverseServer {
209    config: ReverseServerConfig,
210    cancel: CancellationToken,
211    metrics: Option<Arc<ReverseMetrics>>,
212    state: Arc<ReverseServerState>,
213}
214
215impl ReverseServer {
216    pub fn new(config: ReverseServerConfig) -> Self {
217        Self {
218            config,
219            cancel: CancellationToken::new(),
220            metrics: None,
221            state: Arc::new(ReverseServerState::default()),
222        }
223    }
224
225    /// Attach metrics to this server instance.
226    pub fn set_metrics(&mut self, metrics: Arc<ReverseMetrics>) {
227        self.metrics = Some(metrics);
228    }
229
230    /// Get a handle to the active server state.
231    pub fn state_handle(&self) -> Arc<ReverseServerState> {
232        self.state.clone()
233    }
234
235    /// Get a cancel token for external shutdown.
236    pub fn cancel_token(&self) -> CancellationToken {
237        self.cancel.clone()
238    }
239
240    /// Validate the configured bind address against `allow_bind` before
241    /// binding. Returns the resolved listener or an error.
242    async fn bind_external_listener(
243        config: &ReverseServerConfig,
244        state: &ReverseServerState,
245    ) -> Result<Option<TcpListener>, ProtocolError> {
246        let external_bind = match config.external_bind {
247            Some(addr) => addr,
248            None => return Ok(None),
249        };
250        if !config.is_bind_allowed(external_bind) {
251            state.denied_bind.fetch_add(1, Ordering::Relaxed);
252            return Err(ProtocolError::BindDenied(external_bind));
253        }
254        let listener = TcpListener::bind(external_bind).await?;
255        let addr = listener.local_addr()?;
256        info!(addr = %addr, "reverse server listening for external clients");
257        Ok(Some(listener))
258    }
259
260    /// Start the reverse server.
261    pub async fn run(self) -> Result<(), ProtocolError> {
262        // Defense-in-depth validation: catch unsafe configurations (e.g.
263        // non-loopback external_bind without auth or allow_bind allowlist)
264        // before binding any sockets.
265        self.config.validate()?;
266
267        if self.config.auth_username.is_none() || self.config.auth_password.is_none() {
268            warn!(
269                control_bind = %self.config.control_bind,
270                "reverse server control channel has no authentication configured"
271            );
272        }
273
274        // Enforce the allow_bind policy up-front so misconfiguration is loud.
275        if let Some(external_bind) = self.config.external_bind {
276            if !self.config.is_bind_allowed(external_bind) {
277                self.state.denied_bind.fetch_add(1, Ordering::Relaxed);
278                return Err(ProtocolError::BindDenied(external_bind));
279            }
280        }
281
282        let control_listener = TcpListener::bind(&self.config.control_bind).await?;
283        let control_addr = control_listener.local_addr()?;
284        info!(addr = %control_addr, "reverse server listening for control connections");
285
286        let external_listener = Self::bind_external_listener(&self.config, &self.state).await?;
287
288        let config = Arc::new(self.config);
289        let cancel = self.cancel.clone();
290        let state = self.state.clone();
291        let metrics = self.metrics.clone();
292
293        // Build TLS server config once when configured; reconnects and
294        // per-connection handshakes reuse the immutable Arc.
295        let tls_server_config: Option<Arc<rustls::ServerConfig>> = match config.tls.as_ref() {
296            Some(tls) => Some(tls.build_server_config().map_err(|e| {
297                // Validation already ran in `validate`, but build can still
298                // fail on malformed PEM that validation deferred.
299                crate::ProtocolError::Tls(format!("reverse server TLS build failed: {e}"))
300            })?),
301            None => None,
302        };
303
304        // Channel for available control connections
305        let (control_tx, control_rx) = mpsc::channel::<ControlStream>(256);
306
307        // Spawn control connection acceptor
308        let config_clone = config.clone();
309        let cancel_clone = cancel.clone();
310        let control_tx_clone = control_tx.clone();
311        let metrics_clone = metrics.clone();
312        let state_clone = state.clone();
313        let tls_clone = tls_server_config.clone();
314        let control_task = tokio::spawn(async move {
315            Self::accept_control_connections(
316                control_listener,
317                config_clone,
318                cancel_clone,
319                control_tx_clone,
320                metrics_clone,
321                state_clone,
322                tls_clone,
323            )
324            .await;
325        });
326
327        // Spawn external client acceptor
328        let external_task = if let Some(external_listener) = external_listener {
329            let config_clone = config.clone();
330            let cancel_clone = cancel.clone();
331            let metrics_clone = metrics.clone();
332            let state_clone = state.clone();
333            Some(tokio::spawn(async move {
334                Self::accept_external_clients(
335                    external_listener,
336                    config_clone,
337                    cancel_clone,
338                    control_rx,
339                    metrics_clone,
340                    state_clone,
341                )
342                .await;
343            }))
344        } else {
345            // No external listener: drain the control channel so the
346            // counter accurately reflects connections that have not yet
347            // been paired with an external client. Each received stream
348            // is closed and the active_control counter is decremented.
349            let state_clone = state.clone();
350            let metrics_clone = metrics.clone();
351            let cancel_clone = cancel.clone();
352            Some(tokio::spawn(async move {
353                let mut control_rx = control_rx;
354                loop {
355                    tokio::select! {
356                        Some(ctrl) = control_rx.recv() => {
357                            debug!(
358                                control_peer = %ctrl.peer_addr,
359                                "dropping control connection: no external listener"
360                            );
361                            drop(ctrl.stream);
362                            state_clone.active_control.fetch_sub(1, Ordering::Relaxed);
363                            if let Some(m) = metrics_clone.as_deref() {
364                                m.record_control_closed();
365                            }
366                        }
367                        _ = cancel_clone.cancelled() => break,
368                    }
369                }
370            }))
371        };
372
373        // Wait for shutdown
374        cancel.cancelled().await;
375        let drain_start = Instant::now();
376        info!("reverse server shutting down, draining active streams");
377
378        // Stop the accept loops before returning. The external accept loop
379        // also aborts and joins every in-flight relay task it owns.
380        let _ = control_task.await;
381        if let Some(task) = external_task {
382            let _ = task.await;
383        }
384        let drain_ms = drain_start.elapsed().as_millis() as u64;
385        if let Some(ref m) = metrics {
386            m.record_drain(drain_ms);
387        }
388        info!(drain_ms, "reverse server drain complete");
389        Ok(())
390    }
391
392    /// Accept control connections, authenticate, and add to available pool.
393    async fn accept_control_connections(
394        listener: TcpListener,
395        config: Arc<ReverseServerConfig>,
396        cancel: CancellationToken,
397        control_tx: mpsc::Sender<ControlStream>,
398        metrics: Option<Arc<ReverseMetrics>>,
399        state: Arc<ReverseServerState>,
400        tls_config: Option<Arc<rustls::ServerConfig>>,
401    ) {
402        loop {
403            tokio::select! {
404                result = listener.accept() => {
405                    match result {
406                        Ok((stream, peer_addr)) => {
407                            // Enforce the per-server control connection cap.
408                            // Atomically increment then check to avoid TOCTOU race.
409                            let prev = state.active_control.fetch_add(1, Ordering::AcqRel);
410                            if prev >= config.max_control_connections {
411                                state.active_control.fetch_sub(1, Ordering::Relaxed);
412                                warn!(
413                                    peer = %peer_addr,
414                                    max = config.max_control_connections,
415                                    "rejecting control connection: max reached"
416                                );
417                                if let Some(ref m) = metrics {
418                                    m.record_control_rejected(peer_addr, "max_control_connections");
419                                }
420                                drop(stream);
421                                continue;
422                            }
423
424                            let config = config.clone();
425                            let control_tx = control_tx.clone();
426                            let metrics = metrics.clone();
427                            let state = state.clone();
428                            let tls_config = tls_config.clone();
429                            let cancel = cancel.clone();
430                            tokio::spawn(async move {
431                                if let Err(e) = Self::handle_control_connection(
432                                    stream,
433                                    peer_addr,
434                                    config,
435                                    control_tx,
436                                    metrics.as_deref(),
437                                    state.clone(),
438                                    tls_config,
439                                    cancel,
440                                ).await {
441                                    state.active_control.fetch_sub(1, Ordering::Relaxed);
442                                    debug!(peer = %peer_addr, error = %e, "control connection handler error");
443                                }
444                            });
445                        }
446                        Err(e) => {
447                            error!(error = %e, "failed to accept control connection");
448                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
449                        }
450                    }
451                }
452                _ = cancel.cancelled() => {
453                    break;
454                }
455            }
456        }
457    }
458
459    /// Handle a single control connection: TLS (if configured), authenticate,
460    /// and add to pool. TLS wraps the TCP stream before reverse framing so
461    /// credentials never cross in plaintext when configured.
462    #[allow(clippy::too_many_arguments)]
463    async fn handle_control_connection(
464        stream: TcpStream,
465        peer_addr: SocketAddr,
466        config: Arc<ReverseServerConfig>,
467        control_tx: mpsc::Sender<ControlStream>,
468        metrics: Option<&ReverseMetrics>,
469        state: Arc<ReverseServerState>,
470        tls_config: Option<Arc<rustls::ServerConfig>>,
471        cancel: CancellationToken,
472    ) -> Result<(), ProtocolError> {
473        info!(peer = %peer_addr, state = ?ControlState::Connecting, "new control connection");
474
475        // Wrap with TLS before any reverse bytes when configured. The
476        // handshake is cancel-aware so shutdown interrupts pending
477        // handshakes cleanly.
478        let mut boxed: eggress_core::BoxStream = if let Some(tls_cfg) = tls_config {
479            let tcp_boxed: eggress_core::BoxStream = Box::new(stream);
480            let handshake = eggress_transport_tls::tls_accept(tcp_boxed, tls_cfg);
481            tokio::select! {
482                result = handshake => {
483                    result.map_err(|e| {
484                        let msg = format!("reverse control TLS handshake failed: {e}");
485                        if let Some(m) = metrics {
486                            m.record_error(&msg);
487                        }
488                        ProtocolError::Tls(msg)
489                    })?
490                }
491                _ = cancel.cancelled() => {
492                    return Err(ProtocolError::ConnectionClosed);
493                }
494            }
495        } else {
496            Box::new(stream)
497        };
498
499        // Authenticate if configured
500        let redacted = if config.auth_username.is_some() && config.auth_password.is_some() {
501            let authenticating_start = Instant::now();
502            let result = server_auth_handshake(
503                &mut boxed,
504                config.auth_username.as_deref(),
505                config.auth_password.as_deref(),
506            )
507            .await;
508            let elapsed = authenticating_start.elapsed().as_millis() as u64;
509
510            match result {
511                Ok(redacted) => {
512                    info!(
513                        peer = %peer_addr,
514                        auth = %redacted,
515                        duration_ms = elapsed,
516                        state = ?ControlState::Authenticating,
517                        "control connection authenticated"
518                    );
519                    if let Some(m) = metrics {
520                        m.record_control_accepted(peer_addr);
521                        m.record_state_duration(ControlState::Authenticating, elapsed);
522                    }
523                    Some(redacted)
524                }
525                Err(e) => {
526                    warn!(
527                        peer = %peer_addr,
528                        error = %e,
529                        duration_ms = elapsed,
530                        state = ?ControlState::Authenticating,
531                        "control connection auth failed"
532                    );
533                    if let Some(m) = metrics {
534                        m.record_auth_failure(peer_addr, &e.to_string());
535                    }
536                    tokio::time::sleep(AUTH_FAILURE_DELAY).await;
537                    return Err(e);
538                }
539            }
540        } else {
541            // No auth configured: send accept handshake
542            crate::write_handshake_accept(&mut boxed).await?;
543            info!(
544                peer = %peer_addr,
545                state = ?ControlState::Authenticating,
546                "control connection accepted (no auth)"
547            );
548            if let Some(m) = metrics {
549                m.record_control_accepted(peer_addr);
550            }
551            None
552        };
553
554        let ctrl = ControlStream {
555            stream: boxed,
556            peer_addr,
557            redacted_auth: redacted,
558        };
559        if control_tx.try_send(ctrl).is_err() {
560            state.active_control.fetch_sub(1, Ordering::Relaxed);
561            if let Some(m) = metrics {
562                m.record_control_closed();
563            }
564            warn!(peer = %peer_addr, "control channel closed, cannot add to pool");
565        }
566
567        Ok(())
568    }
569
570    /// Accept external clients and relay them through available control connections.
571    async fn accept_external_clients(
572        listener: TcpListener,
573        config: Arc<ReverseServerConfig>,
574        cancel: CancellationToken,
575        mut control_rx: mpsc::Receiver<ControlStream>,
576        metrics: Option<Arc<ReverseMetrics>>,
577        state: Arc<ReverseServerState>,
578    ) {
579        let mut relay_tasks = JoinSet::new();
580
581        loop {
582            tokio::select! {
583                result = listener.accept() => {
584                    match result {
585                        Ok((external_stream, peer_addr)) => {
586                            match state.active_streams.fetch_update(
587                                Ordering::AcqRel,
588                                Ordering::Acquire,
589                                |current| {
590                                    (current < config.max_streams_per_listener)
591                                        .then_some(current + 1)
592                                },
593                            ) {
594                                Ok(_) => {}
595                                Err(current) => {
596                                    warn!(
597                                        peer = %peer_addr,
598                                        active = current,
599                                        max = config.max_streams_per_listener,
600                                        "dropping external client: max_streams_per_listener reached"
601                                    );
602                                    state.dropped_stream_limit.fetch_add(1, Ordering::Relaxed);
603                                    drop(external_stream);
604                                    continue;
605                                }
606                            }
607
608                            match state.pending_external.fetch_update(
609                                Ordering::AcqRel,
610                                Ordering::Acquire,
611                                |current| {
612                                    (current < config.max_pending_external)
613                                        .then_some(current + 1)
614                                },
615                            ) {
616                                Ok(_) => {}
617                                Err(current) => {
618                                    state.active_streams.fetch_sub(1, Ordering::Release);
619                                warn!(
620                                    peer = %peer_addr,
621                                    pending = current,
622                                    max = config.max_pending_external,
623                                    "dropping external client: max_pending_external reached"
624                                );
625                                    state.dropped_pending_limit.fetch_add(1, Ordering::Relaxed);
626                                    drop(external_stream);
627                                    continue;
628                                }
629                            }
630                            // Get an available control connection
631                            let control = tokio::select! {
632                                control = control_rx.recv() => control,
633                                _ = cancel.cancelled() => {
634                                    state.pending_external.fetch_sub(1, Ordering::Release);
635                                    state.active_streams.fetch_sub(1, Ordering::Release);
636                                    drop(external_stream);
637                                    break;
638                                }
639                            };
640                            match control {
641                                Some(control) => {
642                                    state.pending_external.fetch_sub(1, Ordering::Release);
643                                    let metrics = metrics.clone();
644                                    let state = state.clone();
645                                    let idle_timeout = (config.read_timeout_ms > 0).then(|| {
646                                        std::time::Duration::from_millis(config.read_timeout_ms)
647                                    });
648                                    state.active_control.fetch_sub(1, Ordering::Relaxed);
649                                    relay_tasks.spawn(async move {
650                                        info!(
651                                            peer = %peer_addr,
652                                            control_peer = %control.peer_addr,
653                                            "relaying external client through control connection"
654                                        );
655                                        if let Some(m) = metrics.as_deref() {
656                                            m.record_stream_opened();
657                                            m.record_state_duration(ControlState::Ready, 0);
658                                        }
659                                        let external_boxed: eggress_core::BoxStream =
660                                            Box::new(external_stream);
661                                        let relay_result =
662                                            crate::relay_bidirectional_boxed(
663                                                external_boxed,
664                                                control.stream,
665                                                idle_timeout,
666                                            )
667                                            .await;
668                                        match relay_result {
669                                            Ok(()) => {
670                                                debug!(peer = %peer_addr, "relay finished cleanly");
671                                            }
672                                            Err(e) => {
673                                                debug!(peer = %peer_addr, error = %e, "relay ended");
674                                            }
675                                        }
676                                        if let Some(m) = metrics.as_deref() {
677                                            m.record_stream_closed(0);
678                                            m.record_control_closed();
679                                        }
680                                        state.active_streams.fetch_sub(1, Ordering::Release);
681                                        debug!(peer = %peer_addr, "relay finished");
682                                    });
683                                }
684                                None => {
685                                    state.pending_external.fetch_sub(1, Ordering::Release);
686                                    state.active_streams.fetch_sub(1, Ordering::Release);
687                                    warn!(peer = %peer_addr, "no control connections available, rejecting external client");
688                                    drop(external_stream);
689                                }
690                            }
691                        }
692                        Err(e) => {
693                            error!(error = %e, "failed to accept external client");
694                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
695                        }
696                    }
697                }
698                _ = cancel.cancelled() => {
699                    break;
700                }
701            }
702        }
703
704        relay_tasks.abort_all();
705        while relay_tasks.join_next().await.is_some() {}
706    }
707
708    /// Shut down the reverse server.
709    pub fn shutdown(&self) {
710        self.cancel.cancel();
711    }
712}
713
714/// A control stream paired with metadata, used when handing the stream off
715/// from the auth phase to the relay phase. The stream is boxed so plaintext
716/// TCP and TLS-wrapped control channels share the relay path.
717pub struct ControlStream {
718    pub stream: eggress_core::BoxStream,
719    pub peer_addr: SocketAddr,
720    pub redacted_auth: Option<String>,
721}
722
723/// Helper that exposes the redacted auth form for tests and admin code.
724pub fn format_auth_redacted(auth: &str) -> String {
725    redact_auth(auth)
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn is_bind_allowed_with_none() {
734        let cfg = ReverseServerConfig {
735            allow_bind: None,
736            ..Default::default()
737        };
738        assert!(cfg.is_bind_allowed("127.0.0.1:8080".parse().unwrap()));
739    }
740
741    #[test]
742    fn is_bind_allowed_with_empty() {
743        let cfg = ReverseServerConfig {
744            allow_bind: Some(vec![]),
745            ..Default::default()
746        };
747        assert!(cfg.is_bind_allowed("127.0.0.1:8080".parse().unwrap()));
748    }
749
750    #[test]
751    fn is_bind_allowed_match() {
752        let cfg = ReverseServerConfig {
753            allow_bind: Some(vec!["127.0.0.1:8080".parse().unwrap()]),
754            ..Default::default()
755        };
756        assert!(cfg.is_bind_allowed("127.0.0.1:8080".parse().unwrap()));
757    }
758
759    #[test]
760    fn is_bind_allowed_mismatch() {
761        let cfg = ReverseServerConfig {
762            allow_bind: Some(vec!["127.0.0.1:8080".parse().unwrap()]),
763            ..Default::default()
764        };
765        assert!(!cfg.is_bind_allowed("0.0.0.0:8080".parse().unwrap()));
766        assert!(!cfg.is_bind_allowed("127.0.0.1:9090".parse().unwrap()));
767    }
768
769    #[test]
770    fn state_snapshot_round_trip() {
771        let s = ReverseServerState::default();
772        s.active_control.fetch_add(3, Ordering::Relaxed);
773        s.active_streams.fetch_add(2, Ordering::Relaxed);
774        s.pending_external.fetch_add(1, Ordering::Relaxed);
775        s.denied_bind.fetch_add(1, Ordering::Relaxed);
776        s.dropped_stream_limit.fetch_add(4, Ordering::Relaxed);
777        s.dropped_pending_limit.fetch_add(5, Ordering::Relaxed);
778        let snap = s.snapshot();
779        assert_eq!(snap.active_control, 3);
780        assert_eq!(snap.active_streams, 2);
781        assert_eq!(snap.pending_external, 1);
782        assert_eq!(snap.denied_bind, 1);
783        assert_eq!(snap.dropped_stream_limit, 4);
784        assert_eq!(snap.dropped_pending_limit, 5);
785    }
786
787    #[test]
788    fn format_auth_redacted_basic() {
789        assert_eq!(format_auth_redacted("user:pass"), "user:****");
790    }
791
792    #[test]
793    fn same_bind_v4() {
794        let a: SocketAddr = "127.0.0.1:8080".parse().unwrap();
795        let b: SocketAddr = "127.0.0.1:8080".parse().unwrap();
796        assert!(same_bind(&a, &b));
797    }
798
799    #[test]
800    fn same_bind_different_port() {
801        let a: SocketAddr = "127.0.0.1:8080".parse().unwrap();
802        let b: SocketAddr = "127.0.0.1:9090".parse().unwrap();
803        assert!(!same_bind(&a, &b));
804    }
805
806    #[test]
807    fn validate_loopback_ok() {
808        let cfg = ReverseServerConfig {
809            control_bind: "127.0.0.1:0".parse().unwrap(),
810            external_bind: Some("127.0.0.1:0".parse().unwrap()),
811            ..Default::default()
812        };
813        assert!(cfg.validate().is_ok());
814    }
815
816    #[test]
817    fn validate_no_external_bind_ok() {
818        let cfg = ReverseServerConfig {
819            control_bind: "127.0.0.1:0".parse().unwrap(),
820            external_bind: None,
821            ..Default::default()
822        };
823        assert!(cfg.validate().is_ok());
824    }
825
826    #[test]
827    fn validate_non_loopback_without_auth_rejected() {
828        let cfg = ReverseServerConfig {
829            control_bind: "127.0.0.1:0".parse().unwrap(),
830            external_bind: Some("0.0.0.0:9000".parse().unwrap()),
831            auth_username: None,
832            auth_password: None,
833            ..Default::default()
834        };
835        let err = cfg.validate().unwrap_err();
836        assert!(
837            matches!(err, ProtocolError::ConfigInvalid(_)),
838            "got: {err:?}"
839        );
840    }
841
842    #[test]
843    fn validate_non_loopback_with_auth_but_no_allowlist_rejected() {
844        let cfg = ReverseServerConfig {
845            control_bind: "127.0.0.1:0".parse().unwrap(),
846            external_bind: Some("0.0.0.0:9000".parse().unwrap()),
847            auth_username: Some("user".to_string()),
848            auth_password: Some("pass".to_string()),
849            allow_bind: None,
850            ..Default::default()
851        };
852        let err = cfg.validate().unwrap_err();
853        assert!(
854            matches!(err, ProtocolError::ConfigInvalid(_)),
855            "got: {err:?}"
856        );
857    }
858
859    #[test]
860    fn validate_non_loopback_with_auth_and_allowlist_ok() {
861        let cfg = ReverseServerConfig {
862            control_bind: "127.0.0.1:0".parse().unwrap(),
863            external_bind: Some("0.0.0.0:9000".parse().unwrap()),
864            auth_username: Some("user".to_string()),
865            auth_password: Some("pass".to_string()),
866            allow_bind: Some(vec!["0.0.0.0:9000".parse().unwrap()]),
867            ..Default::default()
868        };
869        assert!(cfg.validate().is_ok());
870    }
871
872    #[test]
873    fn validate_ipv6_loopback_ok() {
874        let cfg = ReverseServerConfig {
875            control_bind: "127.0.0.1:0".parse().unwrap(),
876            external_bind: Some("[::1]:9000".parse().unwrap()),
877            ..Default::default()
878        };
879        assert!(cfg.validate().is_ok());
880    }
881
882    #[test]
883    fn validate_ipv6_non_loopback_without_auth_rejected() {
884        let cfg = ReverseServerConfig {
885            control_bind: "127.0.0.1:0".parse().unwrap(),
886            external_bind: Some("[2001:db8::1]:9000".parse().unwrap()),
887            ..Default::default()
888        };
889        let err = cfg.validate().unwrap_err();
890        assert!(
891            matches!(err, ProtocolError::ConfigInvalid(_)),
892            "got: {err:?}"
893        );
894    }
895}