diff --git a/crates/tuic-tests/src/lib.rs b/crates/tuic-tests/src/lib.rs index 511806d..e85988e 100644 --- a/crates/tuic-tests/src/lib.rs +++ b/crates/tuic-tests/src/lib.rs @@ -48,8 +48,54 @@ pub fn quiche_server_config( cfg } +/// Build a `tuic-server` config that uses the default quinn backend +/// (`wind-tuic`) with a self-signed certificate. +pub fn quinn_server_config( + server: SocketAddr, + data_dir: PathBuf, + uuid: Uuid, + password: &str, + zero_rtt: bool, +) -> tuic_server::Config { + // Default `BackendMode` is `Quinn`, so leave `backend.mode` untouched. On the + // quinn backend `zero_rtt_handshake` flows into the inbound's + // `max_early_data_size`/`into_0rtt()` accept path (see wind-tuic + // quinn::inbound). + tuic_server::Config { + log_level: tuic_server::config::LogLevel::Debug, + server, + users: { + let mut users = HashMap::new(); + users.insert(uuid, password.to_string()); + users + }, + tls: tuic_server::config::TlsConfig { + self_sign: true, + hostname: "localhost".to_string(), + // The quinn backend passes `tls.alpn` straight through to the QUIC + // server config (unlike the quiche backend, which forces `h3`), so it + // must be set explicitly or ALPN negotiation fails against the client's + // `h3`. + alpn: vec!["h3".to_string()], + ..Default::default() + }, + data_dir, + zero_rtt_handshake: zero_rtt, + experimental: tuic_server::config::ExperimentalConfig { + // Echo servers run on 127.0.0.1, so loopback must be allowed. + drop_loopback: false, + drop_private: false, + }, + ..Default::default() + } +} + /// Build a `tuic-client` config (quinn) pointing at a local server. -pub fn quiche_client_config( +/// +/// The client is always quinn-based regardless of the *server's* backend, so +/// this builder is shared by both [`start_quiche_pair`] and +/// [`start_quinn_pair`]. +pub fn tuic_client_config( server_port: u16, socks_port: u16, uuid: Uuid, @@ -116,7 +162,7 @@ pub async fn start_quiche_pair(server_port: u16, socks_port: u16, zero_rtt: bool }); tokio::time::sleep(Duration::from_secs(1)).await; - let ccfg = quiche_client_config(server_port, socks_port, uuid, password, zero_rtt); + let ccfg = tuic_client_config(server_port, socks_port, uuid, password, zero_rtt); tokio::spawn(async move { match timeout(Duration::from_secs(20), tuic_client::run(ccfg)).await { Ok(Ok(())) => info!("[quiche test] client exited ok"), @@ -129,6 +175,44 @@ pub async fn start_quiche_pair(server_port: u16, socks_port: u16, zero_rtt: bool format!("127.0.0.1:{socks_port}") } +/// Start a quinn-backed `tuic-server` plus a `tuic-client`, waiting for the +/// client's SOCKS5 proxy to come up. Returns the SOCKS5 address. Mirrors +/// [`start_quiche_pair`] but exercises the default quinn backend. +/// +/// NOTE: `tuic_client::run` installs a **process-global** connection +/// (`OnceCell`), so at most one client may run per test process — keep to one +/// client-starting test per `tests/*.rs` file. +pub async fn start_quinn_pair(server_port: u16, socks_port: u16, zero_rtt: bool) -> String { + install_crypto_provider(); + + let uuid = Uuid::new_v4(); + let password = "test_password"; + let server_addr: SocketAddr = format!("127.0.0.1:{server_port}").parse().unwrap(); + let data_dir = std::env::temp_dir().join(format!("wind-tuic-quinn-test-{server_port}")); + + let scfg = quinn_server_config(server_addr, data_dir, uuid, password, zero_rtt); + tokio::spawn(async move { + match timeout(Duration::from_secs(20), tuic_server::run(scfg)).await { + Ok(Ok(())) => info!("[quinn test] server exited ok"), + Ok(Err(e)) => error!("[quinn test] server error: {e}"), + Err(_) => info!("[quinn test] server timed out (expected at test end)"), + } + }); + tokio::time::sleep(Duration::from_secs(1)).await; + + let ccfg = tuic_client_config(server_port, socks_port, uuid, password, zero_rtt); + tokio::spawn(async move { + match timeout(Duration::from_secs(20), tuic_client::run(ccfg)).await { + Ok(Ok(())) => info!("[quinn test] client exited ok"), + Ok(Err(e)) => error!("[quinn test] client error: {e}"), + Err(_) => info!("[quinn test] client timed out (expected at test end)"), + } + }); + tokio::time::sleep(Duration::from_secs(2)).await; + + format!("127.0.0.1:{socks_port}") +} + pub async fn run_tcp_echo_server(bind_addr: &str, test_name: &str) -> (tokio::task::JoinHandle<()>, std::net::SocketAddr) { use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, diff --git a/crates/tuic-tests/tests/quiche_zero_rtt.rs b/crates/tuic-tests/tests/quiche_zero_rtt.rs index ca08a79..7154061 100644 --- a/crates/tuic-tests/tests/quiche_zero_rtt.rs +++ b/crates/tuic-tests/tests/quiche_zero_rtt.rs @@ -3,7 +3,8 @@ //! Runs in its own test binary (separate process) because `tuic_client::run` //! installs a process-global connection. 0-RTT early data is enabled on both //! the server (`enable_early_data`) and the client (`zero_rtt_handshake`); the -//! test verifies the 0-RTT-enabled config path handshakes and relays correctly. +//! test verifies the 0-RTT-enabled config path handshakes and relays both TCP +//! and UDP correctly (mirrors `quinn_zero_rtt.rs` for backend parity). //! //! It does not assert that early data was actually replayed on a resumed //! handshake — that would require a custom resumption client; the first @@ -14,29 +15,47 @@ // quiche backend itself now builds on 32-bit too (see patches/tokio-quiche). #![cfg(target_pointer_width = "64")] -use std::time::Duration; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; use serial_test::serial; use tokio::time::timeout; -use tuic_tests::{run_tcp_echo_server, start_quiche_pair, test_tcp_through_socks5}; +use tuic_tests::{ + run_tcp_echo_server, run_udp_echo_server, start_quiche_pair, test_tcp_through_socks5, test_udp_through_socks5, +}; #[tokio::test] #[serial] #[tracing_test::traced_test] -async fn quiche_zero_rtt_tcp_relay() -> eyre::Result<()> { +async fn quiche_zero_rtt_tcp_and_udp_relay() -> eyre::Result<()> { let socks = start_quiche_pair(8464, 1094, true).await; - let (tcp_echo, tcp_addr) = run_tcp_echo_server("127.0.0.1:0", "Quiche 0-RTT").await; + // --- TCP relay --- + let (tcp_echo, tcp_addr) = run_tcp_echo_server("127.0.0.1:0", "Quiche 0-RTT TCP").await; tokio::time::sleep(Duration::from_millis(200)).await; - - let ok = timeout( + let tcp_ok = timeout( Duration::from_secs(10), - test_tcp_through_socks5(&socks, tcp_addr, b"hello 0-rtt over quiche", "Quiche 0-RTT"), + test_tcp_through_socks5(&socks, tcp_addr, b"hello 0-rtt over quiche", "Quiche 0-RTT TCP"), ) .await .expect("0-RTT TCP relay timed out"); tcp_echo.abort(); + assert!(tcp_ok, "TCP echo through the 0-RTT quiche backend did not round-trip"); + + // --- UDP relay (native datagram mode) --- + let (udp_echo, udp_addr, _srv) = run_udp_echo_server("127.0.0.1:0", "Quiche 0-RTT UDP").await; + tokio::time::sleep(Duration::from_millis(200)).await; + let bind = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + let udp_ok = timeout( + Duration::from_secs(10), + test_udp_through_socks5(&socks, udp_addr, b"hello udp 0-rtt over quiche", "Quiche 0-RTT UDP", bind), + ) + .await + .expect("0-RTT UDP relay timed out"); + udp_echo.abort(); + assert!(udp_ok, "UDP echo through the 0-RTT quiche backend did not round-trip"); - assert!(ok, "TCP echo through the 0-RTT quiche backend did not round-trip"); Ok(()) } diff --git a/crates/tuic-tests/tests/quinn_zero_rtt.rs b/crates/tuic-tests/tests/quinn_zero_rtt.rs new file mode 100644 index 0000000..5705ac7 --- /dev/null +++ b/crates/tuic-tests/tests/quinn_zero_rtt.rs @@ -0,0 +1,59 @@ +//! 0-RTT integration test for the quinn (`wind-tuic`) backend — the default +//! backend, which until now had no 0-RTT coverage (only the quiche backend +//! did). +//! +//! Runs in its own test binary (separate process) because `tuic_client::run` +//! installs a process-global connection. 0-RTT early data is enabled on the +//! server via `zero_rtt_handshake`, which wires into the inbound's +//! `max_early_data_size` and the `into_0rtt()` accept path (see +//! `wind_tuic::quinn::inbound`). The test verifies the 0-RTT-enabled config +//! path handshakes and relays TCP and UDP correctly. +//! +//! Like the quiche 0-RTT test, it does not assert that early data was actually +//! replayed on a *resumed* handshake — that would require a custom resumption +//! client; the first connection is always 1-RTT. + +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; + +use serial_test::serial; +use tokio::time::timeout; +use tuic_tests::{ + run_tcp_echo_server, run_udp_echo_server, start_quinn_pair, test_tcp_through_socks5, test_udp_through_socks5, +}; + +#[tokio::test] +#[serial] +#[tracing_test::traced_test] +async fn quinn_zero_rtt_tcp_and_udp_relay() -> eyre::Result<()> { + let socks = start_quinn_pair(8466, 1096, true).await; + + // --- TCP relay --- + let (tcp_echo, tcp_addr) = run_tcp_echo_server("127.0.0.1:0", "Quinn 0-RTT TCP").await; + tokio::time::sleep(Duration::from_millis(200)).await; + let tcp_ok = timeout( + Duration::from_secs(10), + test_tcp_through_socks5(&socks, tcp_addr, b"hello 0-rtt over quinn", "Quinn 0-RTT TCP"), + ) + .await + .expect("0-RTT TCP relay timed out"); + tcp_echo.abort(); + assert!(tcp_ok, "TCP echo through the 0-RTT quinn backend did not round-trip"); + + // --- UDP relay (native datagram mode) --- + let (udp_echo, udp_addr, _srv) = run_udp_echo_server("127.0.0.1:0", "Quinn 0-RTT UDP").await; + tokio::time::sleep(Duration::from_millis(200)).await; + let bind = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + let udp_ok = timeout( + Duration::from_secs(10), + test_udp_through_socks5(&socks, udp_addr, b"hello udp 0-rtt over quinn", "Quinn 0-RTT UDP", bind), + ) + .await + .expect("0-RTT UDP relay timed out"); + udp_echo.abort(); + assert!(udp_ok, "UDP echo through the 0-RTT quinn backend did not round-trip"); + + Ok(()) +}