use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tracing::{debug, error, info, instrument, warn};
use crate::documents::SessionType;
use crate::errors::{Error, Result};
use crate::mux::{SmuxConfig, SmuxSession};
use crate::session::Session;
use crate::shutdown::ShutdownSignal;
#[derive(Debug, Clone)]
pub struct PortForwardConfig {
pub local_addr: SocketAddr,
pub max_connections: usize,
pub keepalive: bool,
}
impl Default for PortForwardConfig {
fn default() -> Self {
Self {
local_addr: SocketAddr::from(([127, 0, 0, 1], 0)),
max_connections: 100,
keepalive: false,
}
}
}
#[must_use = "a PortForwarder does nothing until forward() is called"]
#[derive(Debug)]
pub struct PortForwarder {
config: PortForwardConfig,
listener: TcpListener,
local_addr: SocketAddr,
}
impl PortForwarder {
#[instrument(skip(config), fields(local_addr = %config.local_addr))]
pub async fn bind(config: PortForwardConfig) -> Result<Self> {
let listener = TcpListener::bind(config.local_addr).await?;
let local_addr = listener.local_addr()?;
info!(%local_addr, "port forwarder listening");
Ok(Self {
config,
listener,
local_addr,
})
}
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
#[instrument(skip(self, session, shutdown), fields(local_addr = %self.local_addr))]
pub async fn forward(self, session: Arc<Session>, shutdown: ShutdownSignal) -> Result<()> {
if session.config().session_type() != SessionType::Port {
return Err(Error::Config(
"PortForwarder needs a port-forwarding session. Start it with \
SessionBuilder::port_forward(port) or \
.document(PortForwardingToRemoteHost::new(host, port))."
.into(),
));
}
let Self {
config, listener, ..
} = self;
tokio::select! {
biased;
() = shutdown.cancelled() => {
debug!("shutdown requested before the session was ready");
return Ok(());
}
result = session.wait_ready() => result?,
}
let mux = Arc::new(SmuxSession::new(
Arc::clone(&session),
SmuxConfig {
keepalive: config.keepalive,
},
));
let permits = Arc::new(Semaphore::new(config.max_connections));
info!(
max_connections = config.max_connections,
"forwarding connections"
);
loop {
tokio::select! {
biased;
() = shutdown.cancelled() => {
info!("shutdown requested; releasing the local port");
return Ok(());
}
() = session.closed() => {
let reason = session.close_reason();
info!(?reason, "session ended; stopping the port forwarder");
shutdown.shutdown();
return Ok(());
}
accepted = listener.accept() => {
let (stream, peer) = match accepted {
Ok(pair) => pair,
Err(e) => {
error!(error = %e, "accept failed");
return Err(Error::Io(e));
}
};
let Ok(permit) = Arc::clone(&permits).try_acquire_owned() else {
warn!(
%peer,
max_connections = config.max_connections,
"connection limit reached; refusing"
);
drop(stream);
continue;
};
if let Err(e) = stream.set_nodelay(true) {
debug!(error = %e, "could not disable Nagle on the accepted socket");
}
debug!(%peer, "accepted");
let mux = Arc::clone(&mux);
let shutdown = shutdown.clone();
tokio::spawn(async move {
let _permit = permit;
if let Err(e) = forward_connection(stream, mux, shutdown).await {
warn!(%peer, error = %e, "forwarded connection failed");
}
});
}
}
}
}
}
async fn forward_connection(
stream: TcpStream,
mux: Arc<SmuxSession>,
shutdown: ShutdownSignal,
) -> Result<()> {
let smux_stream = mux.open_stream()?;
let stream_id = smux_stream.id();
let (tcp_read, tcp_write) = stream.into_split();
let (mux_read, mux_write) = tokio::io::split(smux_stream);
let outbound = pump(tcp_read, mux_write);
let inbound = pump(mux_read, tcp_write);
tokio::select! {
biased;
() = shutdown.cancelled() => {
debug!(stream_id, "connection cancelled by shutdown");
}
(sent, received) = async { tokio::join!(outbound, inbound) } => {
debug!(
stream_id,
sent = ?sent.as_ref().ok(),
received = ?received.as_ref().ok(),
"connection finished",
);
sent?;
received?;
}
}
Ok(())
}
async fn pump<R, W>(mut reader: R, mut writer: W) -> std::io::Result<u64>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
let copied = tokio::io::copy(&mut reader, &mut writer).await?;
writer.shutdown().await?;
Ok(copied)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_bind_loopback_on_an_ephemeral_port() {
let config = PortForwardConfig::default();
assert!(
config.local_addr.ip().is_loopback(),
"must not be public by default"
);
assert_eq!(config.local_addr.port(), 0, "0 asks the OS for a free port");
assert_eq!(config.max_connections, 100);
assert!(!config.keepalive, "match the official plugin's default");
}
#[tokio::test]
async fn bind_reports_the_os_assigned_port() {
let forwarder = PortForwarder::bind(PortForwardConfig::default())
.await
.expect("binding loopback must succeed");
assert_ne!(forwarder.local_addr().port(), 0);
assert!(forwarder.local_addr().ip().is_loopback());
}
#[tokio::test]
async fn bind_honours_an_explicit_port() {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let wanted = probe.local_addr().unwrap();
drop(probe);
let forwarder = PortForwarder::bind(PortForwardConfig {
local_addr: wanted,
..Default::default()
})
.await
.expect("re-binding a just-released port must succeed");
assert_eq!(forwarder.local_addr(), wanted);
}
#[tokio::test]
async fn binding_a_taken_port_fails() {
let holder = TcpListener::bind("127.0.0.1:0").await.unwrap();
let taken = holder.local_addr().unwrap();
let result = PortForwarder::bind(PortForwardConfig {
local_addr: taken,
..Default::default()
})
.await;
assert!(result.is_err(), "a port in use must not bind twice");
}
#[tokio::test]
async fn pump_copies_then_half_closes() {
use tokio::io::AsyncReadExt;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut received = Vec::new();
socket.read_to_end(&mut received).await.unwrap();
received
});
let client = TcpStream::connect(addr).await.unwrap();
let source = std::io::Cursor::new(b"forwarded payload".to_vec());
let copied = pump(source, client).await.unwrap();
assert_eq!(copied, 17);
assert_eq!(server.await.unwrap(), b"forwarded payload");
}
}