Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions crates/tuic-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand All @@ -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},
Expand Down
37 changes: 28 additions & 9 deletions crates/tuic-tests/tests/quiche_zero_rtt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(())
}
59 changes: 59 additions & 0 deletions crates/tuic-tests/tests/quinn_zero_rtt.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading