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#[derive(Clone)]
20pub struct ReverseServerConfig {
21 pub control_bind: SocketAddr,
23 pub external_bind: Option<SocketAddr>,
25 pub auth_username: Option<String>,
27 pub auth_password: Option<String>,
29 pub max_control_connections: u32,
31 pub read_timeout_ms: u64,
33 pub allow_bind: Option<Vec<SocketAddr>>,
37 pub max_listeners_per_client: u32,
41 pub max_streams_per_listener: u32,
43 pub max_pending_external: u32,
46 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 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 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 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 pub fn validate(&self) -> Result<(), ProtocolError> {
121 if let Some(external) = self.external_bind {
122 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#[derive(Debug, Default)]
163pub struct ReverseServerState {
164 pub active_control: AtomicU32,
166 pub active_streams: AtomicU32,
168 pub pending_external: AtomicU32,
170 pub denied_bind: AtomicU32,
172 pub dropped_stream_limit: AtomicU32,
174 pub dropped_pending_limit: AtomicU32,
176}
177
178impl ReverseServerState {
179 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#[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
203pub 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 pub fn set_metrics(&mut self, metrics: Arc<ReverseMetrics>) {
227 self.metrics = Some(metrics);
228 }
229
230 pub fn state_handle(&self) -> Arc<ReverseServerState> {
232 self.state.clone()
233 }
234
235 pub fn cancel_token(&self) -> CancellationToken {
237 self.cancel.clone()
238 }
239
240 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 pub async fn run(self) -> Result<(), ProtocolError> {
262 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 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 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 crate::ProtocolError::Tls(format!("reverse server TLS build failed: {e}"))
300 })?),
301 None => None,
302 };
303
304 let (control_tx, control_rx) = mpsc::channel::<ControlStream>(256);
306
307 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 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 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 cancel.cancelled().await;
375 let drain_start = Instant::now();
376 info!("reverse server shutting down, draining active streams");
377
378 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 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 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 #[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 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 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 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 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 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 pub fn shutdown(&self) {
710 self.cancel.cancel();
711 }
712}
713
714pub struct ControlStream {
718 pub stream: eggress_core::BoxStream,
719 pub peer_addr: SocketAddr,
720 pub redacted_auth: Option<String>,
721}
722
723pub 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}