use std::{
net::{SocketAddr, TcpListener, TcpStream},
process::{Child, Command, Stdio},
thread::sleep,
time::{Duration, Instant},
};
use anyhow::{Context, Result, bail};
const READY_TIMEOUT: Duration = Duration::from_secs(15);
const POLL: Duration = Duration::from_millis(100);
pub struct Tunnel {
child: Child,
addr: SocketAddr,
}
impl Tunnel {
pub fn addr(&self) -> SocketAddr {
self.addr
}
}
impl Drop for Tunnel {
fn drop(&mut self) {
if let Err(e) = self.child.kill() {
eprintln!("smon: could not stop the ssh tunnel: {e}");
}
if let Err(e) = self.child.wait() {
eprintln!("smon: could not reap the ssh tunnel: {e}");
}
}
}
pub fn open(host: &str, remote: SocketAddr) -> Result<Tunnel> {
let local = free_port().context("finding a local port for the ssh tunnel")?;
let forward = format!("{local}:127.0.0.1:{}", remote.port());
let child = Command::new("ssh")
.args([
"-N",
"-o",
"ExitOnForwardFailure=yes",
"-L",
&forward,
host,
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
.with_context(|| format!("starting ssh to {host}"))?;
let addr = SocketAddr::from(([127, 0, 0, 1], local));
let mut tunnel = Tunnel { child, addr };
let deadline = Instant::now() + READY_TIMEOUT;
while Instant::now() < deadline {
if let Some(status) = tunnel.child.try_wait()? {
bail!("ssh to {host} exited with {status}");
}
if TcpStream::connect_timeout(&addr, POLL).is_ok() {
return Ok(tunnel);
}
sleep(POLL);
}
bail!("no answer through the ssh tunnel to {host} within 15s, is smon running there")
}
fn free_port() -> Result<u16> {
let listener = TcpListener::bind("127.0.0.1:0")?;
Ok(listener.local_addr()?.port())
}