use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpListener;
use crate::ConnectError;
use crate::lifecycle::Lifecycle;
use crate::peer::Peer;
use crate::refusal;
use crate::ticket::Ticket;
pub(crate) struct ConnectState {
pub(crate) peer: Peer,
pub(crate) local_addr: SocketAddr,
pub(crate) lifecycle: Lifecycle,
}
pub(crate) async fn dial(
ticket: &Ticket,
bind: Option<SocketAddr>,
) -> Result<(Arc<ConnectState>, TcpListener), ConnectError> {
let requested = bind.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 0)));
let listener = TcpListener::bind(requested)
.await
.map_err(ConnectError::Bind)?;
let local_addr = listener.local_addr().map_err(ConnectError::Bind)?;
let peer = Peer::dial(ticket).await?;
let lifecycle = Lifecycle::new();
peer.publish_path(&lifecycle);
Ok((
Arc::new(ConnectState {
peer,
local_addr,
lifecycle,
}),
listener,
))
}
pub(crate) async fn local_loop(state: Arc<ConnectState>, listener: TcpListener) {
loop {
let accepted = tokio::select! {
biased;
() = state.lifecycle.wait_until_closed() => break,
accepted = listener.accept() => accepted,
};
let local = match accepted {
Ok((local, _)) => local,
Err(e) if transient(&e) => {
tracing::debug!(error = %e, "an accept failed, and the listener continues");
continue;
}
Err(e) => {
tracing::warn!(error = %e, "the local listener stopped accepting");
break;
}
};
let state = state.clone();
let guard = state.lifecycle.enter();
tokio::spawn(async move {
let _ = carry(&state, local, guard).await;
});
}
drop(listener);
state.lifecycle.close();
state.lifecycle.mark_torn_down();
}
fn transient(e: &std::io::Error) -> bool {
use std::io::ErrorKind::{ConnectionAborted, Interrupted, OutOfMemory, WouldBlock};
matches!(
e.kind(),
ConnectionAborted | Interrupted | WouldBlock | OutOfMemory
) || e.raw_os_error() == Some(24) || e.raw_os_error() == Some(23) }
async fn carry(
state: &ConnectState,
mut local: tokio::net::TcpStream,
guard: crate::lifecycle::InFlight,
) -> std::io::Result<()> {
let mut first = [0u8; 1];
tokio::select! {
biased;
() = state.lifecycle.wait_until_closed() => return Ok(()),
peeked = local.peek(&mut first) => {
if peeked? == 0 {
tracing::debug!("a local connection said nothing and was not an exchange");
return Ok(());
}
}
}
let _guard = guard;
let Some(connection) = state.peer.current() else {
tracing::info!("refused a request: the peer is away");
return refuse_locally(&mut local).await;
};
let opened = connection.open_bi().await;
let Ok((send, recv)) = opened else {
tracing::info!("refused a request: the tunnel is down");
return refuse_locally(&mut local).await;
};
let mut remote = tokio::io::join(recv, send);
tokio::io::copy_bidirectional(&mut local, &mut remote)
.await
.map(|_| ())
}
async fn refuse_locally(local: &mut tokio::net::TcpStream) -> std::io::Result<()> {
local.write_all(&refusal::tunnel_unavailable()).await?;
local.flush().await?;
let _ = local.shutdown().await;
let mut discard = [0u8; 4096];
let _ = tokio::time::timeout(REFUSAL_DRAIN, async {
while matches!(local.read(&mut discard).await, Ok(n) if n > 0) {}
})
.await;
Ok(())
}
const REFUSAL_DRAIN: Duration = Duration::from_secs(5);
pub(crate) async fn shutdown(state: &ConnectState) {
state.lifecycle.close();
state.lifecycle.wait_until_drained().await;
state.peer.close(b"shutdown");
state.lifecycle.wait_until_torn_down().await;
}
pub(crate) async fn shutdown_timeout(state: &ConnectState, grace: Duration) -> bool {
state.lifecycle.close();
let drained = tokio::time::timeout(grace, state.lifecycle.wait_until_drained())
.await
.is_ok();
state.peer.close(b"shutdown");
state.lifecycle.wait_until_torn_down().await;
drained
}