use bytes::Bytes; use std::{ sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, time::Duration, }; use tokio::{sync::Mutex, time::timeout}; use wgcloak::{ client::supervisor::{self as supervisor, ProbeFuture, TransportProbe, TransportSupervisor}, config::{self, Config, TransportMode}, protocol, transport::{TransportError, TransportFuture, TransportSession}, }; struct Session; impl TransportSession for Session { fn send_datagram<'a>(&'a self, _: Bytes) -> TransportFuture<'a, ()> { Box::pin(async { Ok(()) }) } fn recv_datagram<'a>(&'a self) -> TransportFuture<'a, Bytes> { Box::pin(async { Err(TransportError::Closed) }) } fn close<'a>(&'a self) -> TransportFuture<'a, ()> { Box::pin(async { Ok(()) }) } fn max_datagram_size(&self) -> usize { 1200 } } struct Probe { calls: Arc>>, auth: bool, } impl TransportProbe for Probe { fn connect(&self, mode: TransportMode) -> ProbeFuture { let calls = self.calls.clone(); let auth = self.auth; Box::pin(async move { calls.lock().await.push(mode); if auth { Err(TransportError::AuthenticationRejected( protocol::RejectReason::AuthenticationFailed, )) } else { Err(TransportError::HandshakeTimeout) } }) } } fn config(mode: &str) -> config::ClientConfig { Config::from_str(&format!("[client]\nserver=\"127.0.0.1\"\nid=\"c\"\nsecret=\"{}\"\n[transport]\nmode=\"{}\"\n[transport.auto]\norder=[\"direct_udp\",\"quic\"]\nretries=2\nprobe_timeout_ms=20\n", "A".repeat(64), mode)).unwrap().into_client().unwrap() } #[tokio::test] async fn selection_order_retries_and_auth_stop() { let calls = Arc::new(Mutex::new(Vec::new())); let supervisor = TransportSupervisor::new( config("auto"), Probe { calls: calls.clone(), auth: false, }, ); assert!(matches!( supervisor.connect_once().await, Err(TransportError::HandshakeTimeout) )); assert_eq!( *calls.lock().await, vec![ TransportMode::DirectUdp, TransportMode::DirectUdp, TransportMode::Quic, TransportMode::Quic ] ); let calls = Arc::new(Mutex::new(Vec::new())); let supervisor = TransportSupervisor::new( config("auto"), Probe { calls: calls.clone(), auth: true, }, ); assert!(matches!( supervisor.connect_once().await, Err(TransportError::AuthenticationRejected(_)) )); assert_eq!(*calls.lock().await, vec![TransportMode::DirectUdp]); } #[test] fn backoff_is_bounded_and_resettable() { let mut supervisor = TransportSupervisor::new( config("direct_udp"), Probe { calls: Arc::new(Mutex::new(Vec::new())), auth: false, }, ); for _ in 0..30 { let delay = supervisor.backoff_delay(); assert!((250..=30_000).contains(&delay.as_millis())); } supervisor.reset_backoff(); assert_eq!(supervisor.backoff_delay(), Duration::from_millis(250)); } struct LifecycleProbe { calls: Arc, } impl TransportProbe for LifecycleProbe { fn connect(&self, _mode: TransportMode) -> ProbeFuture { let calls = self.calls.clone(); Box::pin(async move { if calls.fetch_add(1, Ordering::SeqCst) == 0 { Ok(Box::new(Session) as Box) } else { Err(TransportError::HandshakeTimeout) } }) } fn wait_for_session_failure<'a>( &'a self, _session: &'a dyn TransportSession, ) -> supervisor::SessionWatchFuture<'a> { Box::pin(async { Err(TransportError::Closed) }) } } #[tokio::test] async fn established_failure_reprobes_without_reading_datagrams() { let calls = Arc::new(AtomicUsize::new(0)); let supervisor = TransportSupervisor::new( config("direct_udp"), LifecycleProbe { calls: calls.clone(), }, ); let shutdown = supervisor.shutdown_token(); let task = tokio::spawn(supervisor.run()); // The failed established session uses the normal minimum reconnect // backoff before the second probe. tokio::time::sleep(Duration::from_millis(320)).await; shutdown.cancel(); let result = timeout(Duration::from_secs(2), task) .await .expect("supervisor should stop") .unwrap(); assert!(result.is_ok()); assert!(calls.load(Ordering::SeqCst) >= 2); }