feat: complete wgcloak v1 transports

This commit is contained in:
h3cx committed 2026-09-19 19:47:16 +02:00
1 parent 49ec9d8c33
commit cedab4910e
47 files changed
+2849 -400

No files matched your search

+165 -1
View File
@@ -29,6 +29,37 @@ impl TransportSession for Session {
1200
}
}
struct EchoSession {
payload: Mutex<Option<Bytes>>,
}
impl TransportSession for EchoSession {
fn send_datagram<'a>(&'a self, data: Bytes) -> TransportFuture<'a, ()> {
Box::pin(async move {
*self.payload.lock().await = Some(data);
Ok(())
})
}
fn recv_datagram<'a>(&'a self) -> TransportFuture<'a, Bytes> {
Box::pin(async move {
self.payload
.lock()
.await
.take()
.ok_or(TransportError::Closed)
})
}
fn close<'a>(&'a self) -> TransportFuture<'a, ()> {
Box::pin(async { Ok(()) })
}
fn max_datagram_size(&self) -> usize {
1200
}
}
struct Probe {
calls: Arc<Mutex<Vec<TransportMode>>>,
auth: bool,
@@ -48,9 +79,113 @@ impl TransportProbe for Probe {
}
})
}
fn wait_for_session_failure<'a>(
&'a self,
_session: &'a dyn TransportSession,
) -> supervisor::SessionWatchFuture<'a> {
Box::pin(async {
Err(TransportError::Unsupported(
"test probe has no session lifecycle".into(),
))
})
}
}
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()
config_with_order(mode, "\"direct_udp\",\"quic\"")
}
fn config_with_order(mode: &str, order: &str) -> config::ClientConfig {
Config::from_str(&format!("[client]\nserver=\"127.0.0.1\"\nid=\"c\"\nsecret=\"{}\"\n[transport]\nmode=\"{}\"\n[transport.auto]\norder=[{}]\nretries=2\nprobe_timeout_ms=20\n", "A".repeat(64), mode, order)).unwrap().into_client().unwrap()
}
struct SelectingProbe {
calls: Arc<Mutex<Vec<TransportMode>>>,
selected: TransportMode,
}
impl TransportProbe for SelectingProbe {
fn connect(&self, mode: TransportMode) -> ProbeFuture {
let calls = self.calls.clone();
let selected = self.selected;
Box::pin(async move {
calls.lock().await.push(mode);
if mode == selected {
Ok(Box::new(EchoSession {
payload: Mutex::new(None),
}) as Box<dyn TransportSession>)
} else {
Err(TransportError::HandshakeTimeout)
}
})
}
fn wait_for_session_failure<'a>(
&'a self,
_session: &'a dyn TransportSession,
) -> supervisor::SessionWatchFuture<'a> {
Box::pin(async { Err(TransportError::Unsupported("test probe".into())) })
}
}
async fn assert_selected_with_roundtrip(
config: config::ClientConfig,
selected: TransportMode,
expected_calls: Vec<TransportMode>,
) {
let calls = Arc::new(Mutex::new(Vec::new()));
let supervisor = TransportSupervisor::new(
config,
SelectingProbe {
calls: calls.clone(),
selected,
},
);
let session = supervisor.connect_once().await.unwrap();
assert_eq!(*calls.lock().await, expected_calls);
let status = supervisor.status().await;
assert_eq!(status.state, supervisor::SupervisorState::Connected);
assert_eq!(status.selected_transport, Some(selected));
session
.send_datagram(Bytes::from_static(b"auto-payload"))
.await
.unwrap();
assert_eq!(
session.recv_datagram().await.unwrap(),
Bytes::from_static(b"auto-payload")
);
}
#[tokio::test]
async fn auto_selects_quic_after_direct_udp_is_unavailable() {
assert_selected_with_roundtrip(
config_with_order("auto", "\"direct_udp\",\"quic\""),
TransportMode::Quic,
vec![
TransportMode::DirectUdp,
TransportMode::DirectUdp,
TransportMode::Quic,
],
)
.await;
}
#[tokio::test]
async fn auto_selects_tls_tcp_after_udp_carriers_are_unavailable() {
assert_selected_with_roundtrip(
config_with_order("auto", "\"direct_udp\",\"quic\",\"http3\",\"tls_tcp\""),
TransportMode::TlsTcp,
vec![
TransportMode::DirectUdp,
TransportMode::DirectUdp,
TransportMode::Quic,
TransportMode::Quic,
TransportMode::Http3,
TransportMode::Http3,
TransportMode::TlsTcp,
],
)
.await;
}
#[tokio::test]
@@ -89,6 +224,10 @@ async fn selection_order_retries_and_auth_stop() {
Err(TransportError::AuthenticationRejected(_))
));
assert_eq!(*calls.lock().await, vec![TransportMode::DirectUdp]);
assert_eq!(
supervisor.status().await.state,
supervisor::SupervisorState::AuthenticationRejected
);
}
#[test]
@@ -154,3 +293,28 @@ async fn established_failure_reprobes_without_reading_datagrams() {
assert!(result.is_ok());
assert!(calls.load(Ordering::SeqCst) >= 2);
}
struct UnsupportedLifecycleProbe;
impl TransportProbe for UnsupportedLifecycleProbe {
fn connect(&self, _mode: TransportMode) -> ProbeFuture {
Box::pin(async { Ok(Box::new(Session) as Box<dyn TransportSession>) })
}
fn wait_for_session_failure<'a>(
&'a self,
_session: &'a dyn TransportSession,
) -> supervisor::SessionWatchFuture<'a> {
Box::pin(async { Err(TransportError::Unsupported("lifecycle unavailable".into())) })
}
}
#[tokio::test]
async fn unsupported_session_lifecycle_is_explicitly_terminal() {
let supervisor = TransportSupervisor::new(config("direct_udp"), UnsupportedLifecycleProbe);
let result = timeout(Duration::from_secs(1), supervisor.run())
.await
.expect("unsupported lifecycle should not hang")
.unwrap_err();
assert!(matches!(result, TransportError::Unsupported(_)));
}