use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use hyper_util::rt::TokioIo;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::sync::watch;
pub(crate) async fn run(
downstream: hyper::upgrade::OnUpgrade,
upstream: hyper::upgrade::OnUpgrade,
idle_timeout: Option<Duration>,
drain: watch::Receiver<bool>,
) -> (u64, u64) {
let (down, up) = match tokio::join!(downstream, upstream) {
(Ok(d), Ok(u)) => (d, u),
(d, u) => {
tracing::debug!(
downstream_ok = d.is_ok(),
upstream_ok = u.is_ok(),
"upgrade tunnel never established"
);
return (0, 0);
}
};
splice(TokioIo::new(down), TokioIo::new(up), idle_timeout, drain).await
}
pub(crate) async fn splice<A, B>(
a: A,
b: B,
idle_timeout: Option<Duration>,
mut drain: watch::Receiver<bool>,
) -> (u64, u64)
where
A: AsyncRead + AsyncWrite + Unpin,
B: AsyncRead + AsyncWrite + Unpin,
{
let start = Instant::now();
let last_activity = Arc::new(AtomicU64::new(0));
let up_bytes = Arc::new(AtomicU64::new(0)); let down_bytes = Arc::new(AtomicU64::new(0)); let mut a = ActivityIo::new(a, start, last_activity.clone(), up_bytes.clone());
let mut b = ActivityIo::new(b, start, last_activity.clone(), down_bytes.clone());
let copy = tokio::io::copy_bidirectional(&mut a, &mut b);
tokio::pin!(copy);
tokio::select! {
_ = &mut copy => {}
_ = idle_elapsed(start, &last_activity, idle_timeout) => {
tracing::debug!("upgrade tunnel closed by idle timeout");
}
_ = crate::listener::drained(&mut drain) => {
tracing::debug!("upgrade tunnel closed by drain");
}
}
(
down_bytes.load(Ordering::Relaxed),
up_bytes.load(Ordering::Relaxed),
)
}
async fn idle_elapsed(start: Instant, last_activity: &AtomicU64, idle_timeout: Option<Duration>) {
let Some(idle) = idle_timeout else {
return std::future::pending().await;
};
loop {
let last = Duration::from_millis(last_activity.load(Ordering::Relaxed));
let since = start.elapsed().saturating_sub(last);
if since >= idle {
return;
}
tokio::time::sleep(idle - since).await;
}
}
struct ActivityIo<T> {
inner: T,
start: Instant,
last_activity: Arc<AtomicU64>,
read_bytes: Arc<AtomicU64>,
}
impl<T> ActivityIo<T> {
fn new(
inner: T,
start: Instant,
last_activity: Arc<AtomicU64>,
read_bytes: Arc<AtomicU64>,
) -> Self {
Self {
inner,
start,
last_activity,
read_bytes,
}
}
fn mark(&self) {
let elapsed = self.start.elapsed().as_millis() as u64;
self.last_activity.store(elapsed, Ordering::Relaxed);
}
}
impl<T: AsyncRead + Unpin> AsyncRead for ActivityIo<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let before = buf.filled().len();
let poll = Pin::new(&mut self.inner).poll_read(cx, buf);
let read = buf.filled().len() - before;
if matches!(poll, Poll::Ready(Ok(()))) && read > 0 {
self.mark();
self.read_bytes.fetch_add(read as u64, Ordering::Relaxed);
}
poll
}
}
impl<T: AsyncWrite + Unpin> AsyncWrite for ActivityIo<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let poll = Pin::new(&mut self.inner).poll_write(cx, buf);
if matches!(poll, Poll::Ready(Ok(n)) if n > 0) {
self.mark();
}
poll
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn splice_relays_bytes_both_ways() {
let (client, client_far) = tokio::io::duplex(64);
let (server, server_far) = tokio::io::duplex(64);
let (_tx, drain) = watch::channel(false);
let tunnel = tokio::spawn(splice(client_far, server_far, None, drain));
let (mut client, mut server) = (client, server);
client.write_all(b"c2s").await.unwrap();
let mut buf = [0u8; 3];
server.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"c2s");
server.write_all(b"s2c").await.unwrap();
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"s2c");
drop(client);
drop(server);
let (down, up) = tokio::time::timeout(Duration::from_secs(1), tunnel)
.await
.expect("the relay ends when both sides close")
.unwrap();
assert_eq!(
(down, up),
(3, 3),
"the relay reports per-direction byte totals (ADR 000059)"
);
}
#[tokio::test]
async fn splice_closes_an_idle_tunnel() {
let (_client, client_far) = tokio::io::duplex(64);
let (_server, server_far) = tokio::io::duplex(64);
let (_tx, drain) = watch::channel(false);
tokio::time::timeout(
Duration::from_secs(2),
splice(
client_far,
server_far,
Some(Duration::from_millis(50)),
drain,
),
)
.await
.expect("an idle tunnel must be closed by the idle timer");
}
#[tokio::test]
async fn splice_closes_on_drain() {
let (_client, client_far) = tokio::io::duplex(64);
let (_server, server_far) = tokio::io::duplex(64);
let (tx, drain) = watch::channel(false);
let tunnel = tokio::spawn(splice(client_far, server_far, None, drain));
tx.send(true).unwrap();
tokio::time::timeout(Duration::from_secs(1), tunnel)
.await
.expect("a drain flip must close the tunnel")
.unwrap();
}
#[tokio::test]
async fn byte_totals_survive_a_drain_cancellation() {
let (mut client, client_far) = tokio::io::duplex(64);
let (mut server, server_far) = tokio::io::duplex(64);
let (tx, drain) = watch::channel(false);
let tunnel = tokio::spawn(splice(client_far, server_far, None, drain));
client.write_all(b"c2s").await.unwrap();
let mut buf = [0u8; 3];
server.read_exact(&mut buf).await.unwrap();
server.write_all(b"s2c-x").await.unwrap();
let mut buf5 = [0u8; 5];
client.read_exact(&mut buf5).await.unwrap();
tx.send(true).unwrap();
let (down, up) = tokio::time::timeout(Duration::from_secs(1), tunnel)
.await
.expect("a drain flip must close the tunnel")
.unwrap();
assert_eq!((down, up), (5, 3), "totals survive the cancelled copy");
}
}