use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use crate::edge::{EdgeError, TcpConn};
const PUMP_CHUNK_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ForwardSpec {
pub local: SocketAddr,
pub remote: SocketAddr,
}
impl FromStr for ForwardSpec {
type Err = EdgeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (local_raw, remote_raw) = s.split_once('=').ok_or_else(|| {
EdgeError::Forward(format!(
"forward spec {s:?} is missing the '=' separator \
(expected LOCAL_IP:PORT=OVERLAY_IP:PORT)"
))
})?;
let local = parse_side(local_raw, "local")?;
let remote = parse_side(remote_raw, "remote")?;
Ok(Self { local, remote })
}
}
fn parse_side(raw: &str, which: &str) -> Result<SocketAddr, EdgeError> {
let trimmed = raw.trim();
let addr = SocketAddr::from_str(trimmed).map_err(|e| {
EdgeError::Forward(format!(
"forward spec {which} side {trimmed:?} is not a valid IP:PORT address ({e}); \
IPv6 must be bracketed, e.g. [::1]:8080"
))
})?;
if addr.port() == 0 {
return Err(EdgeError::Forward(format!(
"forward spec {which} side {trimmed:?} has port 0, which is not a valid forward port"
)));
}
Ok(addr)
}
#[async_trait]
pub trait TunnelDialer: Send + Sync {
async fn dial(&self, dst: SocketAddr) -> Result<TcpConn, EdgeError>;
}
pub async fn serve_forward<D>(spec: ForwardSpec, dial: D) -> Result<(), EdgeError>
where
D: TunnelDialer + 'static,
{
let listener = TcpListener::bind(spec.local).await.map_err(|e| {
EdgeError::Forward(format!(
"failed to bind forward listener on {}: {e}",
spec.local
))
})?;
tracing::debug!(local = %spec.local, remote = %spec.remote, "edge forward listening");
let dial = Arc::new(dial);
loop {
let (stream, peer) = listener.accept().await.map_err(|e| {
EdgeError::Forward(format!("forward accept on {} failed: {e}", spec.local))
})?;
let dial = Arc::clone(&dial);
let remote = spec.remote;
tokio::spawn(async move {
tracing::debug!(%peer, %remote, "edge forward connection accepted");
let conn = match dial.dial(remote).await {
Ok(conn) => conn,
Err(e) => {
tracing::debug!(%peer, %remote, error = %e, "edge forward dial failed");
return;
}
};
pump(stream, conn).await;
tracing::debug!(%peer, %remote, "edge forward connection closed");
});
}
}
async fn pump(stream: TcpStream, conn: TcpConn) {
let TcpConn { tx, mut rx } = conn;
let (mut read_half, mut write_half) = stream.into_split();
let host_to_overlay = async move {
let mut buf = vec![0u8; PUMP_CHUNK_BYTES];
loop {
let n = match read_half.read(&mut buf).await {
Ok(0) | Err(_) => break, Ok(n) => n,
};
if tx.send(buf[..n].to_vec()).await.is_err() {
break; }
}
drop(tx);
};
let overlay_to_host = async move {
while let Some(chunk) = rx.recv().await {
if write_half.write_all(&chunk).await.is_err() {
break; }
}
let _ = write_half.shutdown().await;
};
tokio::join!(host_to_overlay, overlay_to_host);
}
#[cfg(test)]
mod tests {
use super::{serve_forward, ForwardSpec, TunnelDialer};
use crate::edge::{EdgeError, TcpConn};
use async_trait::async_trait;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn forward_spec_parses_ipv4() {
let spec = ForwardSpec::from_str("127.0.0.1:8080=10.42.0.5:80").unwrap();
assert_eq!(spec.local, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
assert_eq!(spec.remote, "10.42.0.5:80".parse::<SocketAddr>().unwrap());
}
#[test]
fn forward_spec_parses_bracketed_ipv6() {
let spec = ForwardSpec::from_str("[::1]:8080=[fd00::2]:443").unwrap();
assert_eq!(spec.local, "[::1]:8080".parse::<SocketAddr>().unwrap());
assert_eq!(spec.remote, "[fd00::2]:443".parse::<SocketAddr>().unwrap());
}
#[test]
fn forward_spec_rejects_missing_separator() {
let err = ForwardSpec::from_str("127.0.0.1:8080").unwrap_err();
assert!(err.to_string().contains("'='"), "{err}");
}
#[test]
fn forward_spec_rejects_missing_port() {
let err = ForwardSpec::from_str("127.0.0.1=10.0.0.1:80").unwrap_err();
assert!(matches!(err, EdgeError::Forward(_)), "{err}");
}
#[test]
fn forward_spec_rejects_bad_ip() {
let err = ForwardSpec::from_str("not-an-ip:80=10.0.0.1:80").unwrap_err();
assert!(matches!(err, EdgeError::Forward(_)), "{err}");
}
#[test]
fn forward_spec_rejects_zero_port() {
let local_zero = ForwardSpec::from_str("127.0.0.1:0=10.0.0.1:80").unwrap_err();
assert!(local_zero.to_string().contains("port 0"), "{local_zero}");
let remote_zero = ForwardSpec::from_str("127.0.0.1:80=10.0.0.1:0").unwrap_err();
assert!(remote_zero.to_string().contains("port 0"), "{remote_zero}");
}
struct EchoDialer;
#[async_trait]
impl TunnelDialer for EchoDialer {
async fn dial(&self, _dst: SocketAddr) -> Result<TcpConn, EdgeError> {
let (host_tx, mut host_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
let (back_tx, back_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
tokio::spawn(async move {
while let Some(chunk) = host_rx.recv().await {
if back_tx.send(chunk).await.is_err() {
break;
}
}
});
Ok(TcpConn {
tx: host_tx,
rx: back_rx,
})
}
}
#[tokio::test]
async fn serve_forward_echoes_through_dialer() {
let probe = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let local: SocketAddr = ([127, 0, 0, 1], port).into();
let spec = ForwardSpec {
local,
remote: ([10, 0, 0, 1], 80).into(),
};
tokio::spawn(serve_forward(spec, EchoDialer));
let mut stream = None;
for _ in 0..100 {
if let Ok(s) = tokio::net::TcpStream::connect(local).await {
stream = Some(s);
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
let mut stream = stream.expect("forward listener never came up");
stream.write_all(b"hello edge").await.unwrap();
stream.shutdown().await.unwrap();
let mut got = Vec::new();
stream.read_to_end(&mut got).await.unwrap();
assert_eq!(got, b"hello edge");
}
}