2026-03-10 23:38:19 +00:00
|
|
|
use tokio::io::copy_bidirectional;
|
2026-03-10 23:38:19 +00:00
|
|
|
use tokio::net::TcpStream;
|
|
|
|
|
|
|
|
|
|
use crate::dual_stack::DualStackTcpListener;
|
2026-03-10 23:38:19 +00:00
|
|
|
|
|
|
|
|
/// Listens on `listen` and proxies every TCP connection to `backend`.
|
|
|
|
|
/// Runs forever; intended to be spawned on a dedicated OS thread + Tokio runtime,
|
|
|
|
|
/// matching the pattern used for the cert/ingress watcher.
|
|
|
|
|
pub async fn run_tcp_proxy(listen: &str, backend: &str) {
|
2026-03-10 23:38:19 +00:00
|
|
|
// Parse the listen address to determine if it's IPv6 or IPv4
|
|
|
|
|
let ipv6_addr = if listen.starts_with('[') {
|
|
|
|
|
listen.to_string()
|
|
|
|
|
} else {
|
2026-03-10 23:38:20 +00:00
|
|
|
format!("[::]:{}", listen.split(':').next_back().unwrap_or("22"))
|
2026-03-10 23:38:19 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let ipv4_addr = if listen.contains(':') {
|
|
|
|
|
// Extract port from the original address
|
2026-03-10 23:38:20 +00:00
|
|
|
let port = listen.split(':').next_back().unwrap_or("22");
|
2026-03-10 23:38:19 +00:00
|
|
|
format!("0.0.0.0:{}", port)
|
|
|
|
|
} else {
|
|
|
|
|
"0.0.0.0:22".to_string()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let listener = match DualStackTcpListener::bind(&ipv6_addr, &ipv4_addr).await {
|
2026-03-10 23:38:19 +00:00
|
|
|
Ok(l) => {
|
2026-03-10 23:38:19 +00:00
|
|
|
tracing::info!(%listen, %backend, "SSH TCP proxy listening (dual-stack)");
|
2026-03-10 23:38:19 +00:00
|
|
|
l
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!(error = %e, %listen, "SSH TCP proxy: bind failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
match listener.accept().await {
|
|
|
|
|
Ok((mut socket, peer_addr)) => {
|
|
|
|
|
let backend = backend.to_string();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
match TcpStream::connect(&backend).await {
|
|
|
|
|
Ok(mut upstream) => {
|
|
|
|
|
if let Err(e) = copy_bidirectional(&mut socket, &mut upstream).await {
|
|
|
|
|
tracing::debug!(error = %e, %peer_addr, "ssh: session ended");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!(error = %e, %peer_addr, %backend, "ssh: upstream connect failed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!(error = %e, "ssh: accept failed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|