#![forbid(unsafe_code)]
use super::TunnelStats;
use crate::errors::SshCliError;
use crate::output;
use crate::ssh::client::SshClientTrait;
use anyhow::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub struct ReverseServe {
pub vps_name: String,
pub remote_bind: String,
pub remote_port: u16,
pub local_host: String,
pub local_port: u16,
pub timeout_ms: u64,
pub json: bool,
}
pub async fn serve(
params: ReverseServe,
client: Box<dyn SshClientTrait>,
bound_flag: Option<Arc<AtomicBool>>,
stats: Option<Arc<TunnelStats>>,
) -> Result<()> {
let stats = stats.unwrap_or_default();
let client: Arc<dyn SshClientTrait> = Arc::from(client);
let ReverseServe {
vps_name,
remote_bind,
remote_port,
local_host,
local_port,
timeout_ms,
json,
} = params;
let allocated = client
.request_remote_forward(&remote_bind, remote_port)
.await?;
stats
.effective_port
.store(u32::from(allocated), Ordering::Release);
if let Some(flag) = bound_flag.as_ref() {
flag.store(true, Ordering::Release);
}
tracing::info!(
vps = %vps_name,
remote_bind = %remote_bind,
allocated,
requested = remote_port,
local_host = %local_host,
local_port,
"remote listener established"
);
if json {
output::print_tunnel_listening_json(
&vps_name,
allocated,
&local_host,
local_port,
timeout_ms,
&remote_bind,
"reverse",
)?;
} else {
let banner = crate::i18n::t(crate::i18n::Message::TunnelReverseListening {
remote_bind: remote_bind.clone(),
remote_port: allocated,
local_host: local_host.clone(),
local_port,
vps: vps_name.clone(),
timeout_ms,
});
tracing::info!("{banner}");
output::print_human_banner(&banner);
}
let mut forwards = tokio::task::JoinSet::new();
let forward_limit = crate::concurrency::effective_limit();
let forward_sem = crate::concurrency::semaphore(forward_limit);
loop {
if crate::signals::should_stop() {
tracing::info!("reverse tunnel cancelled by signal");
stats.stopped_by_signal.store(true, Ordering::Release);
break;
}
tokio::select! {
incoming = client.accept_forwarded_channel() => {
let Some(channel) = incoming else {
tracing::warn!("SSH session closed; no further forwarded channels");
stats.stopped_by_accept_error.store(true, Ordering::Release);
break;
};
let permit = match forward_sem.clone().try_acquire_owned() {
Ok(p) => p,
Err(_) => {
let prior = stats.capacity_waits.fetch_add(1, Ordering::Relaxed);
if prior == 0 {
tracing::warn!(
max_concurrency = forward_limit,
"reverse forward concurrency saturated; new channels are queuing"
);
}
crate::concurrency::acquire_owned(&forward_sem).await
}
};
let host = local_host.clone();
let served = Arc::clone(&stats);
forwards.spawn(async move {
let _permit = permit;
served.forwards_served.fetch_add(1, Ordering::Relaxed);
if let Err(e) = deliver(channel, &host, local_port).await {
tracing::warn!(err = %e, "reverse forward failed");
}
});
}
Some(joined) = forwards.join_next() => {
if let Err(e) = joined {
tracing::debug!(err = %e, "reverse forward task ended with join error");
}
}
_ = tokio::time::sleep(Duration::from_millis(
crate::constants::TUNNEL_SIGNAL_POLL_INTERVAL_MS,
)) => {}
}
}
if let Err(e) = client.cancel_remote_forward(&remote_bind, allocated).await {
tracing::debug!(err = %e, "cancel-tcpip-forward failed during teardown");
}
super::drain_forwards(&mut forwards).await;
let _ = client.disconnect().await;
Ok(())
}
async fn deliver(
channel: Box<dyn crate::ssh::client::TunnelChannel>,
local_host: &str,
local_port: u16,
) -> Result<()> {
let target = format!("{local_host}:{local_port}");
let socket = tokio::net::TcpStream::connect(&target)
.await
.map_err(SshCliError::Io)?;
if let Err(e) = socket.set_nodelay(true) {
tracing::debug!(err = %e, "reverse forward set_nodelay failed");
}
super::pump(socket, channel, local_host, local_port).await
}