use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::task::{AbortHandle, JoinSet};
use crate::BoxStream;
const RELAY_HALF_CLOSE_DRAIN: Duration = Duration::from_secs(1);
const RELAY_ABORT_GRACE: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminationReason {
ClientClosed,
ServerClosed,
BothClosed,
Error,
}
#[derive(Debug)]
pub struct RelayResult {
pub bytes_upstream: u64,
pub bytes_downstream: u64,
pub termination_reason: TerminationReason,
}
#[derive(Debug, Clone, Copy)]
enum Direction {
Upstream,
Downstream,
}
async fn copy_direction<R, W>(reader: &mut R, writer: &mut W, counter: &AtomicU64) -> io::Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
let mut buf = [0u8; 65536];
loop {
let n = reader.read(&mut buf).await?;
if n == 0 {
if let Err(error) = writer.shutdown().await {
if !matches!(
error.kind(),
io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset
) {
return Err(error);
}
}
return Ok(());
}
writer.write_all(&buf[..n]).await?;
counter.fetch_add(n as u64, Ordering::Relaxed);
}
}
fn termination_reason(
first_closed: Option<TerminationReason>,
had_error: bool,
drain_timed_out: bool,
) -> TerminationReason {
if had_error {
TerminationReason::Error
} else if drain_timed_out {
first_closed.unwrap_or(TerminationReason::Error)
} else {
first_closed.unwrap_or(TerminationReason::BothClosed)
}
}
pub async fn relay(client: BoxStream, server: BoxStream) -> RelayResult {
let (mut client_read, mut client_write) = io::split(client);
let (mut server_read, mut server_write) = io::split(server);
let bytes_upstream = Arc::new(AtomicU64::new(0));
let bytes_downstream = Arc::new(AtomicU64::new(0));
let mut tasks = JoinSet::new();
let upstream_counter = Arc::clone(&bytes_upstream);
let upstream_abort: AbortHandle = tasks.spawn(async move {
let result = copy_direction(&mut client_read, &mut server_write, &upstream_counter).await;
(Direction::Upstream, result)
});
let downstream_counter = Arc::clone(&bytes_downstream);
let downstream_abort: AbortHandle = tasks.spawn(async move {
let result = copy_direction(&mut server_read, &mut client_write, &downstream_counter).await;
(Direction::Downstream, result)
});
let mut had_error = false;
let mut first_closed: Option<TerminationReason> = None;
let mut pending_abort: Option<AbortHandle> = None;
let mut drain_timed_out = false;
match tasks.join_next().await {
Some(Ok((direction, Ok(())))) => {
let reason = match direction {
Direction::Upstream => {
pending_abort = Some(downstream_abort.clone());
TerminationReason::ClientClosed
}
Direction::Downstream => {
pending_abort = Some(upstream_abort.clone());
TerminationReason::ServerClosed
}
};
first_closed = Some(reason);
}
Some(Ok((direction, Err(error)))) => {
tracing::debug!(%error, ?direction, "relay direction failed");
had_error = true;
upstream_abort.abort();
downstream_abort.abort();
}
Some(Err(error)) => {
tracing::debug!(%error, "relay direction task failed");
had_error = true;
upstream_abort.abort();
downstream_abort.abort();
}
None => {}
}
if let Some(abort) = pending_abort.as_ref() {
match tokio::time::timeout(RELAY_HALF_CLOSE_DRAIN, tasks.join_next()).await {
Ok(Some(Ok((_, Ok(()))))) => {}
Ok(Some(Ok((direction, Err(error))))) => {
tracing::debug!(%error, ?direction, "relay direction failed during drain");
had_error = true;
}
Ok(Some(Err(error))) => {
tracing::debug!(%error, "relay direction task failed during drain");
had_error = true;
}
Ok(None) => {}
Err(_) => {
drain_timed_out = true;
abort.abort();
let _ = tokio::time::timeout(RELAY_ABORT_GRACE, tasks.join_next()).await;
}
}
}
if !drain_timed_out {
while let Some(outcome) = tasks.join_next().await {
match outcome {
Ok((direction, Err(error))) => {
tracing::debug!(%error, ?direction, "relay direction failed");
had_error = true;
}
Err(error) => {
tracing::debug!(%error, "relay direction task failed");
had_error = true;
}
Ok((_, Ok(()))) => {}
}
}
}
let termination_reason = termination_reason(first_closed, had_error, drain_timed_out);
RelayResult {
bytes_upstream: bytes_upstream.load(Ordering::Relaxed),
bytes_downstream: bytes_downstream.load(Ordering::Relaxed),
termination_reason,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn relay_error_during_drain_takes_precedence_over_close_reason() {
assert_eq!(
termination_reason(Some(TerminationReason::ClientClosed), true, true),
TerminationReason::Error
);
}
#[tokio::test]
async fn test_relay_echo() {
let echo = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let echo_addr = echo.local_addr().unwrap();
let jh = tokio::spawn(async move {
let (stream, _) = echo.accept().await.unwrap();
let (mut reader, mut writer) = stream.into_split();
tokio::spawn(async move {
let mut buf = [0u8; 1024];
loop {
let n = reader.read(&mut buf).await.unwrap();
if n == 0 {
break;
}
writer.write_all(&buf[..n]).await.unwrap();
}
});
});
let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy_addr = proxy_listener.local_addr().unwrap();
let proxy_jh = tokio::spawn(async move {
let (client_stream, _) = proxy_listener.accept().await.unwrap();
let server_stream = tokio::net::TcpStream::connect(echo_addr).await.unwrap();
relay(Box::new(client_stream), Box::new(server_stream)).await
});
let mut client = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(b"hello relay").await.unwrap();
client.shutdown().await.unwrap();
let mut buf = String::new();
client.read_to_string(&mut buf).await.unwrap();
assert_eq!(buf, "hello relay");
let result = proxy_jh.await.unwrap();
assert_eq!(result.bytes_upstream, 11);
assert_eq!(result.bytes_downstream, 11);
jh.await.unwrap();
}
#[tokio::test]
async fn test_relay_half_close() {
let echo = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let echo_addr = echo.local_addr().unwrap();
let jh = tokio::spawn(async move {
let (mut stream, _) = echo.accept().await.unwrap();
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).await.unwrap();
stream.write_all(&buf[..n]).await.unwrap();
stream.shutdown().await.unwrap();
});
let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy_addr = proxy_listener.local_addr().unwrap();
let proxy_jh = tokio::spawn(async move {
let (client_stream, _) = proxy_listener.accept().await.unwrap();
let server_stream = tokio::net::TcpStream::connect(echo_addr).await.unwrap();
relay(Box::new(client_stream), Box::new(server_stream)).await
});
let mut client = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(b"data").await.unwrap();
client.shutdown().await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"data");
let result = proxy_jh.await.unwrap();
assert_eq!(result.bytes_upstream, 4);
assert_eq!(result.bytes_downstream, 4);
assert_eq!(result.termination_reason, TerminationReason::ClientClosed);
jh.await.unwrap();
}
#[tokio::test]
async fn test_relay_half_close_server_hangs() {
let upstream = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let upstream_addr = upstream.local_addr().unwrap();
let upstream_jh = tokio::spawn(async move {
let (mut stream, _) = upstream.accept().await.unwrap();
let mut buf = [0u8; 64];
let _ = stream.read(&mut buf).await.unwrap();
std::future::pending::<()>().await;
});
let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy_addr = proxy_listener.local_addr().unwrap();
let proxy_jh = tokio::spawn(async move {
let (client_stream, _) = proxy_listener.accept().await.unwrap();
let server_stream = tokio::net::TcpStream::connect(upstream_addr).await.unwrap();
relay(Box::new(client_stream), Box::new(server_stream)).await
});
let mut client = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(b"data").await.unwrap();
client.shutdown().await.unwrap();
let result = tokio::time::timeout(std::time::Duration::from_secs(5), proxy_jh)
.await
.expect("relay should not block forever on a hanging upstream")
.unwrap();
assert_eq!(result.bytes_upstream, 4);
assert_eq!(result.bytes_downstream, 0);
assert_eq!(result.termination_reason, TerminationReason::ClientClosed);
upstream_jh.abort();
}
#[tokio::test]
async fn test_relay_cancellation() {
let echo = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let echo_addr = echo.local_addr().unwrap();
let jh = tokio::spawn(async move {
let (stream, _) = echo.accept().await.unwrap();
let (mut reader, mut writer) = stream.into_split();
tokio::spawn(async move {
let mut buf = [0u8; 1024];
loop {
let n = reader.read(&mut buf).await.unwrap();
if n == 0 {
break;
}
writer.write_all(&buf[..n]).await.unwrap();
}
});
});
let proxy_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy_addr = proxy_listener.local_addr().unwrap();
let proxy_jh = tokio::spawn(async move {
let (client_stream, _) = proxy_listener.accept().await.unwrap();
let server_stream = tokio::net::TcpStream::connect(echo_addr).await.unwrap();
relay(Box::new(client_stream), Box::new(server_stream)).await
});
let mut client = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(b"data").await.unwrap();
drop(client);
let result = proxy_jh.await.unwrap();
assert!(result.bytes_upstream > 0 || result.bytes_downstream > 0);
jh.await.unwrap();
}
}