mod common;
use std::process::Stdio;
use std::time::Duration;
use common::{get_available_port, init_crypto, server_config};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::time::timeout;
const STEP_TIMEOUT: Duration = Duration::from_secs(10);
async fn spawn_echo_server() -> std::net::SocketAddr {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let (mut r, mut w) = sock.split();
let _ = tokio::io::copy(&mut r, &mut w).await;
let _ = w.shutdown().await;
});
addr
}
#[tokio::test]
async fn stdio_forward_pipes_stdin_stdout_through_tunnel() {
init_crypto();
let server_port = get_available_port();
let echo_addr = spawn_echo_server().await;
let sc = server_config(server_port, false);
let server_handle = tokio::spawn(async move {
let _ = rusnel::server::run_async(sc).await;
});
tokio::time::sleep(common::STARTUP_DELAY).await;
let bin = env!("CARGO_BIN_EXE_rusnel");
let server_url = format!("127.0.0.1:{server_port}");
let stdio_remote = format!("stdio:127.0.0.1:{}", echo_addr.port());
let mut child = Command::new(bin)
.args([
"client",
"--insecure",
"--max-retry-count",
"0",
&server_url,
&stdio_remote,
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true)
.spawn()
.expect("failed to spawn rusnel client subprocess");
let mut stdin = child.stdin.take().expect("captured stdin");
let stdout = child.stdout.take().expect("captured stdout");
let mut reader = BufReader::new(stdout);
let messages = ["hello\n", "stdio tunnel\n", "third frame\n"];
for msg in messages {
timeout(STEP_TIMEOUT, stdin.write_all(msg.as_bytes()))
.await
.expect("write to child stdin timed out")
.expect("write to child stdin failed");
timeout(STEP_TIMEOUT, stdin.flush())
.await
.expect("flush child stdin timed out")
.expect("flush child stdin failed");
let mut got = String::new();
timeout(STEP_TIMEOUT, reader.read_line(&mut got))
.await
.expect("read from child stdout timed out")
.expect("read from child stdout failed");
assert_eq!(got, msg, "echo mismatch (sent vs got)");
}
drop(stdin);
let mut tail = Vec::new();
let _ = timeout(STEP_TIMEOUT, reader.read_to_end(&mut tail)).await;
let status = timeout(STEP_TIMEOUT, child.wait())
.await
.expect("child did not exit after stdin EOF")
.expect("child wait failed");
assert!(
status.success(),
"rusnel client exited with non-zero status: {status:?}"
);
server_handle.abort();
}