Add native local port forwarding
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-13 15:36:10 -04:00
parent f324a7627f
commit 6aa81d0ce3
4 changed files with 844 additions and 55 deletions
+102 -1
View File
@@ -1,6 +1,6 @@
use std::fs;
use std::io::{Read, Write};
use std::net::UdpSocket;
use std::net::{TcpListener, TcpStream, UdpSocket};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
@@ -24,6 +24,11 @@ fn free_udp_port() -> u16 {
socket.local_addr().unwrap().port()
}
fn free_tcp_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
}
fn write_server_config(dir: &tempfile::TempDir, port: u16) -> std::path::PathBuf {
write_server_config_with_timeout(dir, port, 30)
}
@@ -143,6 +148,50 @@ fn native_attach_once_with_agent(
.unwrap()
}
fn connect_with_retry(port: u16, timeout: Duration, child: &mut Child) -> TcpStream {
let deadline = std::time::Instant::now() + timeout;
loop {
if let Some(status) = child.try_wait().unwrap() {
let mut stderr = String::new();
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
panic!(
"client exited before local forward was ready: status={status}; stderr={stderr}"
);
}
match TcpStream::connect(("127.0.0.1", port)) {
Ok(stream) => return stream,
Err(err) if std::time::Instant::now() < deadline => {
let _ = err;
thread::sleep(Duration::from_millis(50));
}
Err(err) => panic!("connect to local forward 127.0.0.1:{port}: {err}"),
}
}
}
fn start_echo_server() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 1024];
loop {
match stream.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if stream.write_all(&buf[..n]).is_err() {
break;
}
}
}
}
}
});
port
}
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
let ssh_dir = dir.path().join(".ssh");
fs::create_dir_all(&ssh_dir).unwrap();
@@ -495,6 +544,58 @@ fn native_attach_only_smoke_uses_ssh_agent() {
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
}
#[test]
fn native_local_forward_echo_smoke() {
let dir = tempfile::tempdir().unwrap();
let port = free_udp_port();
let config = write_server_config(&dir, port);
write_native_client_auth(&dir, &config);
let echo_port = start_echo_server();
let local_port = free_tcp_port();
let mut server = start_server(&dir, &config);
let client_bin = env!("CARGO_BIN_EXE_dosh-client");
let mut client = Command::new(client_bin)
.arg("--auth")
.arg("native")
.arg("--no-cache")
.arg("-N")
.arg("-L")
.arg(format!("{local_port}:127.0.0.1:{echo_port}"))
.arg("--session")
.arg("default")
.arg("--dosh-host")
.arg("127.0.0.1")
.arg("--dosh-port")
.arg(port.to_string())
.arg("local")
.env("HOME", dir.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stream = connect_with_retry(local_port, Duration::from_secs(5), &mut client);
stream
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
stream.write_all(b"dosh-forward-ping").unwrap();
let mut buf = [0u8; 64];
let n = stream.read(&mut buf).unwrap();
let _ = client.kill();
let client_output = client.wait_with_output().unwrap();
let _ = server.kill();
let _ = server.wait();
assert_eq!(&buf[..n], b"dosh-forward-ping");
assert!(
client_output.status.success() || client_output.status.code().is_none(),
"stderr={}",
String::from_utf8_lossy(&client_output.stderr)
);
}
#[test]
fn ticket_attach_after_server_restart_smoke() {
let dir = tempfile::tempdir().unwrap();