Files
shoes/tests/cases/privileged/tun_kernel.rs
T
cfal fbcc5f9d46 Add Clash API and runtime proxy selectors (#213)
Expose runtime proxy selection and connection visibility without restarting Shoes. This adds an opt-in authenticated Clash-compatible TCP/Unix API, persistent selector choices, traffic and connection statistics, delay probes, live logs, and per-flow cancellation, with reload-aware integration across proxy protocols, DNS, and TUN.
2026-10-23 17:50:36 -04:00

800 lines
31 KiB
Rust

//! Linux kernel-TUN coverage; requires /dev/net/tun, IPv6 loopback and passwordless sudo.
//! Run with --features privileged-tests --test privileged tun_kernel:: -- --test-threads=1.
#[path = "tun_kernel/offload_failure.rs"]
mod offload_failure;
#[path = "tun_kernel/clash_api.rs"]
mod clash_api;
use shoes_test_support::test_fixture::{
ProcessGuard, RouteGuard, add_route_via_device, start_shoes_server_with_sudo,
};
use shoes_test_support::test_servers::{
start_tcp_stream_echo_server, start_udp_echo_server_with_suffix,
};
use std::io;
use std::net::{IpAddr, SocketAddr};
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpSocket, TcpStream, UdpSocket};
use tokio::sync::{Barrier, watch};
use tokio::task::JoinSet;
use tokio::time::timeout;
const TEST_TIMEOUT: Duration = Duration::from_secs(60);
struct KernelTun {
_route: RouteGuard,
_process: ProcessGuard,
_config: tempfile::NamedTempFile,
name: String,
source: IpAddr,
target: IpAddr,
}
fn run_ip(args: &[&str]) -> io::Result<String> {
let output = Command::new("sudo")
.args(["-n", "ip"])
.args(args)
.output()?;
if !output.status.success() {
return Err(io::Error::other(format!(
"sudo -n ip {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
impl KernelTun {
async fn start(ipv6: bool) -> io::Result<Self> {
Self::start_options(ipv6, None, 1500, 32768).await
}
async fn start_reloadable(ipv6: bool, offload: bool) -> io::Result<Self> {
Self::start_with(ipv6, Some(offload), 1500, 4096, |config| {
let file = tempfile::NamedTempFile::new()?;
std::fs::write(file.path(), config)?;
let process = shoes_test_support::process::spawn_privileged_process(
std::path::Path::new(env!("CARGO_BIN_EXE_shoes")),
"reloadable-tun",
|command| {
command.args(["-t", "2"]).arg(file.path());
},
)?;
Ok((process, file))
})
.await
}
async fn start_options(
ipv6: bool,
offload: Option<bool>,
mtu: u16,
tcp_buffer_size: usize,
) -> io::Result<Self> {
Self::start_with(
ipv6,
offload,
mtu,
tcp_buffer_size,
start_shoes_server_with_sudo,
)
.await
}
async fn start_with(
ipv6: bool,
offload: Option<bool>,
mtu: u16,
tcp_buffer_size: usize,
start: impl FnOnce(&str) -> io::Result<(ProcessGuard, tempfile::NamedTempFile)>,
) -> io::Result<Self> {
std::fs::metadata("/dev/net/tun")?;
let id: u32 = rand::random();
let name = format!("shcov{id:08x}");
let (source, target, management, destination, loopback) = if ipv6 {
let prefix = format!("fdab:{:x}:{:x}", id >> 16, id & 0xffff);
(
format!("{prefix}::1"),
format!("{prefix}::2"),
"10.203.254.1".to_string(),
format!("{prefix}::2/128"),
// NetLocation's YAML syntax uses unbracketed IPv6 with a final port.
"::1:0",
)
} else {
let subnet = (id % 250 + 1) as u8;
(
format!("10.203.{subnet}.1"),
format!("10.203.{subnet}.2"),
format!("10.203.{subnet}.1"),
format!("10.203.{subnet}.2/32"),
"127.0.0.1:0",
)
};
let config = format!(
r#"
- device_name: {name}
address: "{management}"
netmask: 255.255.255.255
mtu: {mtu}
segmentation_offload: {offload}
resource_limits:
tcp_buffer_size: {tcp_buffer_size}
rules:
- masks: "{destination}"
action: allow
override_address: "{loopback}"
client_chain:
- protocol:
type: direct
"#,
offload = offload.map_or("null", |enabled| if enabled { "true" } else { "false" }),
);
let (process, config) = start(&config)?;
timeout(Duration::from_secs(10), async {
loop {
if let Ok(addresses) = run_ip(&["-j", "-4", "address", "show", "dev", &name]) {
let devices: serde_json::Value = serde_json::from_str(&addresses).unwrap();
if devices[0]["addr_info"].as_array().is_some_and(|addresses| {
addresses
.iter()
.any(|address| address["local"] == management)
}) {
return;
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "TUN interface did not appear"))?;
if ipv6 {
run_ip(&[
"-6",
"addr",
"add",
&format!("{source}/128"),
"dev",
&name,
"nodad",
])?;
}
let route = add_route_via_device(&destination, &name)?;
let family = if ipv6 { "-6" } else { "-4" };
let lookup = run_ip(&["-j", family, "route", "get", &target, "from", &source])?;
let routes: serde_json::Value = serde_json::from_str(&lookup)?;
assert_eq!(routes[0]["dev"], name, "wrong TUN route: {lookup}");
assert_ne!(routes[0]["type"], "local", "test traffic would bypass TUN");
let tun = Self {
_route: route,
_process: process,
_config: config,
name,
source: source.parse().unwrap(),
target: target.parse().unwrap(),
};
// An interface address alone does not establish that the handlers are ready.
let peer =
start_udp_echo_server_with_suffix(if ipv6 { "::" } else { "0.0.0.0" }, 0, b"").await?;
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
udp_exchange(
&socket,
SocketAddr::new(tun.target, peer.local_addr().port()),
b"ready",
)
.await?;
Ok(tun)
}
async fn connect_tcp(&self, port: u16) -> io::Result<TcpStream> {
let socket = if self.source.is_ipv6() {
TcpSocket::new_v6()?
} else {
TcpSocket::new_v4()?
};
socket.bind(SocketAddr::new(self.source, 0))?;
socket.connect(SocketAddr::new(self.target, port)).await
}
}
async fn bulk_echo(mut stream: TcpStream, bytes: usize, flow: usize) -> io::Result<()> {
let data = payload(flow, 0, bytes);
let (mut reader, mut writer) = stream.split();
tokio::try_join!(
async {
writer.write_all(&data).await?;
writer.shutdown().await
},
async {
let mut received = vec![0; bytes];
reader.read_exact(&mut received).await?;
assert_eq!(received, data);
assert_eq!(reader.read(&mut [0]).await?, 0);
io::Result::Ok(())
}
)?;
Ok(())
}
#[tokio::test]
async fn offload_on_off_bulk_dual_stack_and_mtu_variants() -> io::Result<()> {
timeout(Duration::from_secs(120), async {
for ipv6 in [false, true] {
let peer = start_tcp_stream_echo_server(if ipv6 { "::" } else { "0.0.0.0" }, 0).await?;
for offload in [false, true] {
for (mtu, buffer) in [(1280, 4096), (1500, 32768), (9000, 131072), (65535, 131072)]
{
let tun = KernelTun::start_options(ipv6, Some(offload), mtu, buffer).await?;
let mut flows = JoinSet::new();
for flow in 0..4 {
let stream = tun.connect_tcp(peer.local_addr().port()).await?;
flows.spawn(bulk_echo(stream, 1_000_007, flow));
}
while let Some(result) = flows.join_next().await {
result??;
}
// A reset must not poison the next flow or leave a stuck writer.
let reset = tun.connect_tcp(peer.local_addr().port()).await?;
socket2::SockRef::from(&reset).set_linger(Some(Duration::ZERO))?;
drop(reset);
bulk_echo(tun.connect_tcp(peer.local_addr().port()).await?, 4097, 17).await?;
}
}
}
Ok(())
})
.await?
}
#[tokio::test]
async fn server_half_close_preserves_client_upload() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
for ipv6 in [false, true] {
for offload in [false, true] {
let peer = TcpListener::bind(if ipv6 { "[::1]:0" } else { "127.0.0.1:0" }).await?;
let tun = KernelTun::start_options(ipv6, Some(offload), 1280, 4096).await?;
let mut client = tun.connect_tcp(peer.local_addr()?.port()).await?;
let data = payload(3, 7, 256 * 1024 + 1);
let ((), ()) = tokio::try_join!(
async {
let (mut stream, _) = peer.accept().await?;
stream.write_all(b"banner").await?;
stream.shutdown().await?;
let mut received = Vec::new();
stream.read_to_end(&mut received).await?;
assert_eq!(received, data, "ipv6={ipv6}, offload={offload}");
io::Result::Ok(())
},
async {
let mut received = Vec::new();
client.read_to_end(&mut received).await?;
assert_eq!(received, b"banner");
// Upload starts after EOF, exercising the still-open direction.
client.write_all(&data).await?;
client.shutdown().await
}
)?;
}
}
Ok(())
})
.await?
}
#[tokio::test]
async fn reload_removes_tun_and_active_upstreams_without_exiting() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
for ipv6 in [false, true] {
for offload in [false, true] {
let listener =
TcpListener::bind(if ipv6 { "[::1]:0" } else { "127.0.0.1:0" }).await?;
let tun = KernelTun::start_reloadable(ipv6, offload).await?;
let mut client = tun.connect_tcp(listener.local_addr()?.port()).await?;
client.write_all(b"active").await?;
let (mut upstream, _) = listener.accept().await?;
let mut data = [0; 6];
upstream.read_exact(&mut data).await?;
assert_eq!(&data, b"active");
upstream.write_all(b"reply").await?;
client.read_exact(&mut data[..5]).await?;
assert_eq!(&data[..5], b"reply");
let interface = std::path::Path::new("/sys/class/net").join(&tun.name);
assert!(interface.try_exists()?);
let reservation = std::net::TcpListener::bind("127.0.0.1:0")?;
let address = reservation.local_addr()?;
let replacement = format!(
"- address: '{address}'\n protocol: {{type: socks}}\n rules: allow-all-direct\n"
);
drop(reservation);
std::fs::write(tun._config.path(), replacement)?;
timeout(Duration::from_secs(10), async {
while interface.try_exists()? {
tokio::time::sleep(Duration::from_millis(20)).await;
}
io::Result::Ok(())
})
.await??;
match upstream.read(&mut data).await {
Ok(0) => {}
Err(error) if error.kind() == io::ErrorKind::ConnectionReset => {}
result => panic!("upstream not closed on shutdown: {result:?}"),
}
let mut replacement = timeout(Duration::from_secs(5), async {
loop {
if let Ok(stream) = TcpStream::connect(address).await {
return io::Result::Ok(stream);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
replacement.write_all(&[5, 1, 0]).await?;
let mut greeting = [0; 2];
replacement.read_exact(&mut greeting).await?;
assert_eq!(greeting, [5, 0]);
}
}
Ok(())
})
.await?
}
#[tokio::test]
async fn reload_recreates_same_named_tun_and_routes_new_connections() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
for ipv6 in [false, true] {
for offload in [false, true] {
let listener = TcpListener::bind(if ipv6 { "[::]:0" } else { "0.0.0.0:0" }).await?;
let tun = KernelTun::start_reloadable(ipv6, offload).await?;
let mut client = tun.connect_tcp(listener.local_addr()?.port()).await?;
client.write_all(b"before").await?;
let (mut upstream, _) = listener.accept().await?;
let mut data = [0; 6];
upstream.read_exact(&mut data).await?;
assert_eq!(&data, b"before");
let before: serde_json::Value =
serde_json::from_str(&run_ip(&["-j", "link", "show", "dev", &tun.name])?)?;
let mut config: serde_yaml::Value =
serde_yaml::from_str(&std::fs::read_to_string(tun._config.path())?)
.map_err(io::Error::other)?;
config[0]["mtu"] = 1280.into();
std::fs::write(
tun._config.path(),
serde_yaml::to_string(&config).map_err(io::Error::other)?,
)?;
timeout(Duration::from_secs(10), async {
loop {
if let Ok(addresses) =
run_ip(&["-j", "-4", "address", "show", "dev", &tun.name])
{
let devices: serde_json::Value = serde_json::from_str(&addresses)?;
if devices[0]["ifindex"] != before[0]["ifindex"]
&& devices[0]["mtu"] == 1280
&& devices[0]["flags"]
.as_array()
.is_some_and(|flags| flags.iter().any(|flag| flag == "UP"))
&& devices[0]["addr_info"]
.as_array()
.is_some_and(|addresses| !addresses.is_empty())
{
return io::Result::Ok(());
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
match upstream.read(&mut data).await {
Ok(0) => {}
Err(error) if error.kind() == io::ErrorKind::ConnectionReset => {}
result => panic!("upstream not closed on TUN replacement: {result:?}"),
}
drop(client);
// Recreating the device removes addresses and routes installed by the fixture.
if ipv6 {
run_ip(&[
"-6",
"address",
"add",
&format!("{}/128", tun.source),
"dev",
&tun.name,
"nodad",
])?;
}
run_ip(&[
if ipv6 { "-6" } else { "-4" },
"route",
"add",
&format!("{}/{}", tun.target, if ipv6 { 128 } else { 32 }),
"dev",
&tun.name,
])?;
let echo =
start_tcp_stream_echo_server(if ipv6 { "::" } else { "0.0.0.0" }, 0).await?;
bulk_echo(tun.connect_tcp(echo.local_addr().port()).await?, 65537, 9).await?;
let peer =
start_udp_echo_server_with_suffix(if ipv6 { "::" } else { "0.0.0.0" }, 0, b"")
.await?;
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
udp_exchange(
&socket,
SocketAddr::new(tun.target, peer.local_addr().port()),
b"after reload",
)
.await?;
}
}
Ok(())
})
.await?
}
#[tokio::test]
async fn link_down_drops_replies_and_recovers_without_restarting() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
for ipv6 in [false, true] {
for offload in [false, true] {
let listener =
TcpListener::bind(if ipv6 { "[::1]:0" } else { "127.0.0.1:0" }).await?;
let tun = KernelTun::start_options(ipv6, Some(offload), 1500, 4096).await?;
let mut client = tun.connect_tcp(listener.local_addr()?.port()).await?;
let (mut upstream, _) = listener.accept().await?;
upstream.write_all(b"before").await?;
let mut data = [0; 6];
client.read_exact(&mut data).await?;
assert_eq!(&data, b"before");
let dropped_path = std::path::Path::new("/sys/class/net")
.join(&tun.name)
.join("statistics/rx_dropped");
let dropped = || -> io::Result<u64> {
std::fs::read_to_string(&dropped_path)?
.trim()
.parse()
.map_err(io::Error::other)
};
let before = dropped()?;
run_ip(&["link", "set", &tun.name, "down"])?;
upstream.write_all(b"during").await?;
// The kernel increments rx_dropped before returning EIO for a down TUN.
timeout(Duration::from_secs(5), async {
while dropped()? == before {
tokio::time::sleep(Duration::from_millis(20)).await;
}
io::Result::Ok(())
})
.await??;
run_ip(&["link", "set", &tun.name, "up"])?;
// Link-down flushes the fixture's route and IPv6 address.
if ipv6 {
run_ip(&[
"-6",
"address",
"replace",
&format!("{}/128", tun.source),
"dev",
&tun.name,
"nodad",
])?;
}
run_ip(&[
if ipv6 { "-6" } else { "-4" },
"route",
"replace",
&format!("{}/{}", tun.target, if ipv6 { 128 } else { 32 }),
"dev",
&tun.name,
])?;
client.read_exact(&mut data).await?;
assert_eq!(&data, b"during");
client.write_all(b"after").await?;
upstream.read_exact(&mut data[..5]).await?;
assert_eq!(&data[..5], b"after");
}
}
Ok(())
})
.await?
}
fn process_group_cpu_seconds(process: &ProcessGuard) -> io::Result<f64> {
let group = process
.process_group_id()
.expect("privileged test process group");
let mut ticks = 0u64;
for entry in std::fs::read_dir("/proc")? {
let path = entry?.path();
if !path
.file_name()
.unwrap()
.to_string_lossy()
.bytes()
.all(|b| b.is_ascii_digit())
{
continue;
}
let Ok(stat) = std::fs::read_to_string(path.join("stat")) else {
continue;
};
let Some((_, fields)) = stat.rsplit_once(')') else {
continue;
};
let fields: Vec<_> = fields.split_whitespace().collect();
if fields.get(2).and_then(|s| s.parse::<i32>().ok()) == Some(group) {
ticks += fields[11].parse::<u64>().unwrap() + fields[12].parse::<u64>().unwrap();
}
}
Ok(ticks as f64 / unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64)
}
#[tokio::test]
#[ignore = "alternating kernel TUN A/B and idle-socket scaling benchmark; run alone in release mode"]
async fn benchmark_offload_and_idle_scaling() -> io::Result<()> {
let peer = start_tcp_stream_echo_server("0.0.0.0", 0).await?;
for idle in [0, 100, 500, 1000] {
for round in 0..6 {
let offload = round % 2 == 1;
let tun = KernelTun::start_options(false, Some(offload), 1500, 32768).await?;
let mut idle_sockets = Vec::new();
for _ in 0..idle {
let mut stream = tun.connect_tcp(peer.local_addr().port()).await?;
tcp_exchange(&mut stream, b"idle").await?;
idle_sockets.push(stream);
}
for flows in [1, 8] {
let mut streams = Vec::new();
for _ in 0..flows {
streams.push(tun.connect_tcp(peer.local_addr().port()).await?);
}
let cpu = process_group_cpu_seconds(&tun._process)?;
let start = std::time::Instant::now();
let mut jobs = JoinSet::new();
let bytes = 64 * 1024 * 1024;
for (flow, stream) in streams.into_iter().enumerate() {
jobs.spawn(bulk_echo(stream, bytes / flows, flow));
}
timeout(Duration::from_secs(120), async {
while let Some(result) = jobs.join_next().await {
result??;
}
io::Result::Ok(())
})
.await??;
let elapsed = start.elapsed().as_secs_f64();
let cpu = process_group_cpu_seconds(&tun._process)? - cpu;
eprintln!(
"tun_bench offload={offload} idle={idle} flows={flows} seconds={elapsed:.6} cpu_seconds={cpu:.6} bytes_per_direction={bytes}"
);
}
for mut stream in idle_sockets {
tcp_exchange(&mut stream, b"reactivated").await?;
tcp_eof(&mut stream).await?;
}
}
}
Ok(())
}
fn payload(flow: usize, sequence: usize, size: usize) -> Vec<u8> {
let mut data = vec![0; size];
for (offset, chunk) in data.chunks_mut(8).enumerate() {
let value = ((flow as u64) << 48) | ((sequence as u64) << 32) | offset as u64;
chunk.copy_from_slice(&value.to_be_bytes()[..chunk.len()]);
}
if size >= 16 {
data[..8].copy_from_slice(&(flow as u64).to_be_bytes());
data[8..16].copy_from_slice(&(sequence as u64).to_be_bytes());
}
data
}
async fn udp_exchange(socket: &UdpSocket, target: SocketAddr, data: &[u8]) -> io::Result<()> {
assert_eq!(socket.send_to(data, target).await?, data.len());
let mut received = vec![0; data.len() + 1];
let (length, source) =
timeout(Duration::from_secs(5), socket.recv_from(&mut received)).await??;
assert_eq!(source, target);
assert_eq!(&received[..length], data);
Ok(())
}
async fn tcp_exchange(stream: &mut TcpStream, data: &[u8]) -> io::Result<()> {
stream.write_all(data).await?;
let mut received = vec![0; data.len()];
stream.read_exact(&mut received).await?;
assert_eq!(received, data);
Ok(())
}
async fn tcp_eof(stream: &mut TcpStream) -> io::Result<()> {
stream.shutdown().await?;
assert_eq!(
stream.read(&mut [0]).await?,
0,
"surplus TCP data after half-close"
);
Ok(())
}
#[tokio::test]
async fn ipv6_udp_and_tcp_roundtrip_through_kernel_tun() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
let udp_peer = start_udp_echo_server_with_suffix("::", 0, b"").await?;
let tcp_peer = start_tcp_stream_echo_server("::", 0).await?;
let tun = KernelTun::start(true).await?;
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
let target = SocketAddr::new(tun.target, udp_peer.local_addr().port());
for (sequence, length) in [0, 1, 64, 1200, 0, 512].into_iter().enumerate() {
udp_exchange(&socket, target, &payload(1, sequence, length)).await?;
}
let mut stream = tun.connect_tcp(tcp_peer.local_addr().port()).await?;
for sequence in 0..16 {
tcp_exchange(&mut stream, &payload(2, sequence, 32 * 1024)).await?;
}
tcp_eof(&mut stream).await
})
.await?
}
#[tokio::test]
async fn sustained_udp_preserves_all_flow_ids() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
let peer = start_udp_echo_server_with_suffix("0.0.0.0", 0, b"").await?;
let tun = KernelTun::start(false).await?;
let target = SocketAddr::new(tun.target, peer.local_addr().port());
let mut flows = JoinSet::new();
for flow in 0..16 {
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
flows.spawn(async move {
for sequence in 0..512 {
let length = [64, 256, 1200][sequence % 3];
udp_exchange(&socket, target, &payload(flow, sequence, length)).await?;
}
io::Result::Ok(512)
});
}
let mut received = 0;
while let Some(result) = flows.join_next().await {
received += result??;
}
assert_eq!(received, 16 * 512);
Ok(())
})
.await?
}
#[tokio::test]
async fn tcp_and_udp_make_progress_during_overlapping_load() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
const TCP_FLOWS: usize = 8;
const UDP_FLOWS: usize = 16;
let tcp_peer = start_tcp_stream_echo_server("0.0.0.0", 0).await?;
let udp_peer = start_udp_echo_server_with_suffix("0.0.0.0", 0, b"").await?;
let tun = KernelTun::start(false).await?;
let start = Arc::new(Barrier::new(TCP_FLOWS + UDP_FLOWS + 1));
let (stop_tx, stop_rx) = watch::channel(false);
let udp_in_flight = Arc::new(AtomicUsize::new(0));
let udp_midpoint = Arc::new(Barrier::new(UDP_FLOWS));
let tcp_progress: Arc<Vec<AtomicUsize>> =
Arc::new((0..TCP_FLOWS).map(|_| AtomicUsize::new(0)).collect());
let mut tcp = JoinSet::new();
for flow in 0..TCP_FLOWS {
let mut stream = tun.connect_tcp(tcp_peer.local_addr().port()).await?;
tcp_exchange(&mut stream, &payload(flow, 0, 256)).await?;
let start = start.clone();
let stop = stop_rx.clone();
let in_flight = udp_in_flight.clone();
let tcp_progress = tcp_progress.clone();
tcp.spawn(async move {
start.wait().await;
let mut sequence = 1;
while !*stop.borrow() {
tcp_exchange(&mut stream, &payload(flow, sequence, 16 * 1024)).await?;
if in_flight.load(Ordering::Acquire) != 0 {
tcp_progress[flow].fetch_add(1, Ordering::Release);
}
sequence += 1;
}
tcp_eof(&mut stream).await
});
}
let mut udp = JoinSet::new();
for flow in 0..UDP_FLOWS {
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
let target = SocketAddr::new(tun.target, udp_peer.local_addr().port());
let start = start.clone();
let udp_midpoint = udp_midpoint.clone();
let in_flight = udp_in_flight.clone();
udp.spawn(async move {
start.wait().await;
for sequence in 0..256 {
let data = payload(flow, sequence, 256);
let second_phase = sequence >= 128;
if second_phase {
in_flight.fetch_add(1, Ordering::AcqRel);
}
let result = udp_exchange(&socket, target, &data).await;
if second_phase {
in_flight.fetch_sub(1, Ordering::AcqRel);
}
result?;
if sequence == 127 {
udp_midpoint.wait().await;
}
}
io::Result::Ok(())
});
}
start.wait().await;
while let Some(result) = udp.join_next().await {
result??;
}
assert_eq!(udp_in_flight.load(Ordering::Acquire), 0);
for (flow, count) in tcp_progress.iter().enumerate() {
assert!(
count.load(Ordering::Acquire) > 0,
"TCP flow {flow} made no progress with second-phase UDP exchanges in flight"
);
}
stop_tx.send(true).unwrap();
while let Some(result) = tcp.join_next().await {
result??;
}
Ok(())
})
.await?
}
#[tokio::test]
async fn udp_progresses_while_a_tcp_reader_is_stalled() -> io::Result<()> {
timeout(TEST_TIMEOUT, async {
let tcp_peer = start_tcp_stream_echo_server("0.0.0.0", 0).await?;
let udp_peer = start_udp_echo_server_with_suffix("0.0.0.0", 0, b"").await?;
let tun = KernelTun::start(false).await?;
let mut stream = tun.connect_tcp(tcp_peer.local_addr().port()).await?;
tcp_exchange(&mut stream, b"ready").await?;
socket2::SockRef::from(&stream).set_recv_buffer_size(16 * 1024)?;
let socket = UdpSocket::bind(SocketAddr::new(tun.source, 0)).await?;
let target = SocketAddr::new(tun.target, udp_peer.local_addr().port());
let data = payload(1, 0, 4 * 1024 * 1024);
let (write_stalled_tx, write_stalled_rx) = tokio::sync::oneshot::channel();
let (mut reader, mut writer) = stream.split();
let sending = async {
writer.write_all(&data[..64 * 1024]).await?;
let remaining_write = writer.write_all(&data[64 * 1024..]);
tokio::pin!(remaining_write);
assert!(
futures::poll!(remaining_write.as_mut()).is_pending(),
"TCP write must stall before starting UDP"
);
let _ = write_stalled_tx.send(());
remaining_write.await?;
writer.shutdown().await
};
let receiving = async {
write_stalled_rx.await.map_err(io::Error::other)?;
for sequence in 0..128 {
udp_exchange(&socket, target, &payload(2, sequence, 1200)).await?;
}
let mut received = vec![0; data.len()];
reader.read_exact(&mut received).await?;
assert_eq!(received, data);
assert_eq!(reader.read(&mut [0]).await?, 0);
io::Result::Ok(())
};
tokio::try_join!(sending, receiving)?;
Ok(())
})
.await?
}