use std::process::{Child, Command, Stdio};
pub struct SshTunnel {
pub local_port: u16,
pub remote: String,
child: Child,
}
impl SshTunnel {
pub fn new(local_port: u16, remote_host: &str, remote_port: u16) -> Result<Self, String> {
let forward_spec = format!("{local_port}:localhost:{remote_port}");
let child = Command::new("ssh")
.args(["-N", "-L", &forward_spec, remote_host])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("failed to start ssh: {e}"))?;
Ok(Self {
local_port,
remote: format!("{remote_host}:{remote_port}"),
child,
})
}
pub fn summary(&self) -> String {
format!("localhost:{} → {}", self.local_port, self.remote)
}
pub fn is_alive(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(None))
}
pub fn kill(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Drop for SshTunnel {
fn drop(&mut self) {
self.kill();
}
}
pub struct ForwardManager {
pub tunnels: Vec<SshTunnel>,
}
impl Default for ForwardManager {
fn default() -> Self {
Self::new()
}
}
impl ForwardManager {
pub fn new() -> Self {
Self {
tunnels: Vec::new(),
}
}
pub fn add(
&mut self,
local_port: u16,
remote_host: &str,
remote_port: u16,
) -> Result<usize, String> {
let tunnel = SshTunnel::new(local_port, remote_host, remote_port)?;
self.tunnels.push(tunnel);
Ok(self.tunnels.len() - 1)
}
pub fn cleanup(&mut self) {
self.tunnels.retain_mut(|t| t.is_alive());
}
pub fn kill_all(&mut self) {
for tunnel in &mut self.tunnels {
tunnel.kill();
}
self.tunnels.clear();
}
pub fn count(&self) -> usize {
self.tunnels.len()
}
pub fn summaries(&self) -> Vec<String> {
self.tunnels.iter().map(|t| t.summary()).collect()
}
}
impl Drop for ForwardManager {
fn drop(&mut self) {
self.kill_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forward_manager_new_is_empty() {
let fm = ForwardManager::new();
assert_eq!(fm.count(), 0);
}
#[test]
fn forward_manager_default_is_empty() {
let fm = ForwardManager::default();
assert_eq!(fm.count(), 0);
}
}