#![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;
use tokio::net::TcpListener;
#[derive(Debug, Clone)]
pub enum ForwardKind {
Tcp {
host: String,
port: u16,
},
Socks5,
StreamLocal {
socket_path: String,
},
}
impl ForwardKind {
#[must_use]
pub fn mode_label(&self) -> &'static str {
match self {
Self::Tcp { .. } => "local",
Self::Socks5 => "socks5",
Self::StreamLocal { .. } => "streamlocal",
}
}
#[must_use]
pub fn event_host(&self) -> String {
match self {
Self::Tcp { host, .. } => host.clone(),
Self::Socks5 => "*".to_string(),
Self::StreamLocal { socket_path } => socket_path.clone(),
}
}
#[must_use]
pub fn event_port(&self) -> u16 {
match self {
Self::Tcp { port, .. } => *port,
Self::Socks5 | Self::StreamLocal { .. } => 0,
}
}
}
pub struct LocalServe {
pub vps_name: String,
pub local_port: u16,
pub bind_addr: String,
pub timeout_ms: u64,
pub json: bool,
pub kind: ForwardKind,
}
pub async fn serve(
params: LocalServe,
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 LocalServe {
vps_name,
local_port,
bind_addr,
timeout_ms,
json,
kind,
} = params;
let bind_target = format!("{bind_addr}:{local_port}");
let listener = TcpListener::bind(&bind_target).await.map_err(|e| {
let kind = e.kind();
match kind {
std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::InvalidInput => {
SshCliError::InvalidArgument(format!("cannot bind {bind_target}: {e}"))
}
_ => SshCliError::Io(e),
}
})?;
let effective_port = listener
.local_addr()
.map(|a| a.port())
.unwrap_or(local_port);
stats
.effective_port
.store(u32::from(effective_port), Ordering::Release);
if let Some(flag) = bound_flag.as_ref() {
flag.store(true, Ordering::Release);
}
tracing::info!(
port = %effective_port,
requested = %local_port,
vps = %vps_name,
mode = kind.mode_label(),
"local TCP listener started"
);
if json {
output::print_tunnel_listening_json(
&vps_name,
effective_port,
&kind.event_host(),
kind.event_port(),
timeout_ms,
&bind_addr,
kind.mode_label(),
)?;
} else {
let banner = crate::i18n::t(match &kind {
ForwardKind::Tcp { host, port } => crate::i18n::Message::TunnelLocalListening {
bind: bind_addr.clone(),
port: effective_port,
remote_host: host.clone(),
remote_port: *port,
vps: vps_name.clone(),
timeout_ms,
},
ForwardKind::Socks5 => crate::i18n::Message::TunnelSocks5Listening {
bind: bind_addr.clone(),
port: effective_port,
vps: vps_name.clone(),
timeout_ms,
},
ForwardKind::StreamLocal { socket_path } => {
crate::i18n::Message::TunnelStreamLocalListening {
bind: bind_addr.clone(),
port: effective_port,
socket_path: socket_path.clone(),
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);
tracing::debug!(
max_concurrency = forward_limit,
"tunnel forward admission gate ready"
);
loop {
if crate::signals::should_stop() {
tracing::info!(
force = crate::signals::is_force_exit(),
"tunnel cancelled by signal"
);
stats.stopped_by_signal.store(true, Ordering::Release);
break;
}
tokio::select! {
accept_result = listener.accept() => {
match accept_result {
Ok((socket, addr)) => {
tracing::debug!(address = %addr, "new local connection");
if let Err(e) = socket.set_nodelay(true) {
tracing::debug!(err = %e, %addr, "tunnel set_nodelay failed");
}
let kind_c = kind.clone();
let client_c = Arc::clone(&client);
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,
"tunnel forward concurrency saturated; new connections are queuing"
);
}
tokio::select! {
p = crate::concurrency::acquire_owned(&forward_sem) => p,
Some(joined) = forwards.join_next() => {
if let Err(e) = joined {
tracing::debug!(err = %e, "tunnel forward task ended with join error");
}
crate::concurrency::acquire_owned(&forward_sem).await
}
}
}
};
let served = Arc::clone(&stats);
forwards.spawn(async move {
let _permit = permit; served.forwards_served.fetch_add(1, Ordering::Relaxed);
if let Err(e) = handle_connection(socket, client_c, &kind_c).await {
tracing::warn!(err = %e, "tunnel forwarding failed");
}
});
}
Err(e) => {
if matches!(
e.kind(),
std::io::ErrorKind::Interrupted
| std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
) {
tracing::debug!(err = %e, "transient accept error; continuing");
continue;
}
tracing::error!(err = %e, "accept failed (fatal)");
stats.stopped_by_accept_error.store(true, Ordering::Release);
break;
}
}
}
Some(joined) = forwards.join_next() => {
if let Err(e) = joined {
tracing::debug!(err = %e, "tunnel forward task ended with join error");
}
}
_ = tokio::time::sleep(Duration::from_millis(
crate::constants::TUNNEL_SIGNAL_POLL_INTERVAL_MS,
)) => {
}
}
}
drop(listener);
super::drain_forwards(&mut forwards).await;
let _ = client.disconnect().await;
Ok(())
}
async fn handle_connection(
socket: tokio::net::TcpStream,
client: Arc<dyn SshClientTrait>,
kind: &ForwardKind,
) -> Result<()> {
match kind {
ForwardKind::Tcp { host, port } => {
let channel = client
.open_tunnel_channel(
host,
*port,
crate::constants::TUNNEL_CHANNEL_ORIGIN_ADDR,
crate::constants::TUNNEL_CHANNEL_ORIGIN_PORT,
)
.await?;
super::pump(socket, channel, host, *port).await
}
ForwardKind::StreamLocal { socket_path } => {
let channel = client.open_streamlocal_channel(socket_path).await?;
super::pump(socket, channel, socket_path, 0).await
}
ForwardKind::Socks5 => handle_socks5(socket, client).await,
}
}
async fn handle_socks5(
mut socket: tokio::net::TcpStream,
client: Arc<dyn SshClientTrait>,
) -> Result<()> {
use super::socks;
let target = match socks::handshake(&mut socket).await {
Ok(Ok(target)) => target,
Ok(Err(refusal)) => {
tracing::debug!(reason = %refusal.reason, "SOCKS5 request refused");
return Ok(());
}
Err(e) => {
tracing::warn!(err = %e, "SOCKS5 handshake failed");
return Ok(());
}
};
let channel = match client
.open_tunnel_channel(
&target.host,
target.port,
crate::constants::TUNNEL_CHANNEL_ORIGIN_ADDR,
crate::constants::TUNNEL_CHANNEL_ORIGIN_PORT,
)
.await
{
Ok(channel) => channel,
Err(e) => {
tracing::warn!(
err = %e, host = %target.host, port = target.port,
"SOCKS5 CONNECT could not open an SSH channel"
);
socks::write_reply(&mut socket, socks::REP_HOST_UNREACHABLE).await?;
return Ok(());
}
};
socks::write_reply(&mut socket, socks::REP_SUCCEEDED).await?;
super::pump(socket, channel, &target.host, target.port).await
}