use crate::extensions::ssh_remote::{
state::{RemoteConfig, TunnelSpec},
transport,
};
use std::{
collections::BTreeMap,
sync::{Mutex, OnceLock},
};
use tokio::{net::TcpListener, sync::oneshot, task::JoinHandle};
static RUNNING: OnceLock<Mutex<BTreeMap<u16, Running>>> = OnceLock::new();
struct Running {
spec: TunnelSpec,
shutdown: oneshot::Sender<()>,
task: JoinHandle<()>,
}
fn running() -> &'static Mutex<BTreeMap<u16, Running>> {
RUNNING.get_or_init(|| Mutex::new(BTreeMap::new()))
}
pub fn active() -> Vec<TunnelSpec> {
running()
.lock()
.unwrap()
.values()
.map(|r| r.spec.clone())
.collect()
}
pub async fn start(spec: TunnelSpec) -> Result<u16, String> {
if spec.local_port != 0 && running().lock().unwrap().contains_key(&spec.local_port) {
return Err(format!(
"local port {} is already forwarded",
spec.local_port
));
}
let listener = TcpListener::bind(("127.0.0.1", spec.local_port))
.await
.map_err(|e| format!("bind 127.0.0.1:{}: {e}", spec.local_port))?;
let actual = listener
.local_addr()
.map_err(|e| format!("local addr: {e}"))?
.port();
let conn = if spec.host.trim().is_empty() {
transport::resolve_default().await?
} else {
transport::resolve_host(&spec.host).await?
};
let (shutdown, mut stopped) = oneshot::channel::<()>();
let target = (spec.remote_host.clone(), spec.remote_port);
let task = tokio::spawn(async move {
loop {
let accepted = tokio::select! {
_ = &mut stopped => break,
accepted = listener.accept() => accepted,
};
let Ok((mut local, _)) = accepted else {
continue;
};
let handle = conn.handle.clone();
let (remote_host, remote_port) = target.clone();
tokio::spawn(async move {
let Ok(channel) = handle
.channel_open_direct_tcpip(&remote_host, remote_port as u32, "127.0.0.1", 0)
.await
else {
return;
};
let mut stream = channel.into_stream();
_ = tokio::io::copy_bidirectional(&mut local, &mut stream).await;
});
}
});
let mut stored = spec.clone();
stored.local_port = actual;
running().lock().unwrap().insert(
actual,
Running {
spec: stored,
shutdown,
task,
},
);
Ok(actual)
}
pub fn stop(local_port: u16) -> bool {
let Some(r) = running().lock().unwrap().remove(&local_port) else {
return false;
};
_ = r.shutdown.send(());
r.task.abort();
true
}
pub fn stop_all() -> usize {
let items: Vec<u16> = running().lock().unwrap().keys().copied().collect();
let mut count = 0;
for port in items {
if stop(port) {
count += 1;
}
}
count
}
pub async fn restore_all() -> Vec<(String, Result<u16, String>)> {
let cfg = RemoteConfig::load();
let mut out = Vec::new();
for spec in cfg.tunnels {
let text = spec.to_spec_text();
let result = start(spec).await;
out.push((text, result));
}
out
}
pub fn remember(spec: &TunnelSpec) -> Result<(), String> {
let mut cfg = RemoteConfig::load();
if !cfg
.tunnels
.iter()
.any(|t| t.to_spec_text() == spec.to_spec_text())
{
cfg.tunnels.push(spec.clone());
}
cfg.save()
}
pub fn forget(local_port: u16) -> Result<bool, String> {
let mut cfg = RemoteConfig::load();
let before = cfg.tunnels.len();
cfg.tunnels.retain(|t| t.local_port != local_port);
let changed = cfg.tunnels.len() != before;
if changed {
cfg.save()?;
}
Ok(changed)
}
pub fn forget_all() -> Result<usize, String> {
let mut cfg = RemoteConfig::load();
let removed = cfg.tunnels.len();
if removed == 0 {
return Ok(0);
}
cfg.tunnels.clear();
cfg.save()?;
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stop_unknown_port_is_noop() {
assert!(!stop(1));
assert!(active().is_empty());
assert_eq!(stop_all(), 0);
}
#[test]
fn remember_dedups_and_forget_removes() {
let _l = crate::extensions::ssh_remote::state::TEST_ROOT_LOCK.blocking_lock();
let _g = crate::extensions::ssh_remote::state::TestRootGuard::temp();
let spec = TunnelSpec {
host: String::new(),
local_port: 15432,
remote_host: "db".to_string(),
remote_port: 5432,
};
remember(&spec).unwrap();
remember(&spec).unwrap();
let cfg = RemoteConfig::load();
assert_eq!(cfg.tunnels.len(), 1, "同一条转发不应重复写入");
assert!(forget(15432).unwrap());
assert!(!forget(15432).unwrap(), "重复删除返回 false");
assert!(RemoteConfig::load().tunnels.is_empty());
}
#[test]
fn forget_all_clears_every_configured_forward() {
let _l = crate::extensions::ssh_remote::state::TEST_ROOT_LOCK.blocking_lock();
let _g = crate::extensions::ssh_remote::state::TestRootGuard::temp();
let spec = |local_port: u16| TunnelSpec {
host: String::new(),
local_port,
remote_host: "db".to_string(),
remote_port: 5432,
};
remember(&spec(15432)).unwrap();
remember(&spec(15433)).unwrap();
assert_eq!(forget_all().unwrap(), 2);
assert!(RemoteConfig::load().tunnels.is_empty());
assert_eq!(forget_all().unwrap(), 0, "已空时返回 0");
}
}