use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::{watch, Semaphore};
use crate::producer::Socks5Producer;
#[derive(Debug)]
enum BackendExit {
ClientGone,
Session(crate::SocksError),
Io(std::io::Error),
}
impl std::fmt::Display for BackendExit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackendExit::ClientGone => f.write_str("client gone"),
BackendExit::Session(err) => write!(f, "session: {err:#}"),
BackendExit::Io(err) => write!(f, "io: {err}"),
}
}
}
pub struct ProducerDoor {
bind_addr: Option<SocketAddr>,
producer: Socks5Producer,
idle_first_byte: Duration,
max_sessions: usize,
}
impl ProducerDoor {
pub fn builder() -> ProducerDoorBuilder {
ProducerDoorBuilder::default()
}
}
#[derive(Default)]
pub struct ProducerDoorBuilder {
producer: Option<Socks5Producer>,
bind_addr: Option<SocketAddr>,
idle_first_byte: Option<Duration>,
max_sessions: Option<usize>,
}
impl ProducerDoorBuilder {
pub fn producer(mut self, producer: Socks5Producer) -> Self {
self.producer = Some(producer);
self
}
pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
self.bind_addr = Some(addr.into());
self
}
pub fn idle_first_byte(mut self, window: Duration) -> Self {
self.idle_first_byte = Some(window);
self
}
pub fn max_sessions(mut self, cap: usize) -> Self {
self.max_sessions = Some(cap);
self
}
pub fn build(self) -> Result<ProducerDoor, ProducerDoorBuildError> {
let producer = self
.producer
.ok_or(ProducerDoorBuildError::MissingProducer)?;
Ok(ProducerDoor {
bind_addr: self.bind_addr,
producer,
idle_first_byte: self.idle_first_byte.unwrap_or(Duration::from_secs(30)),
max_sessions: self.max_sessions.unwrap_or(256),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProducerDoorBuildError {
#[error("the backend requires a producer (.producer); there is nothing to serve")]
MissingProducer,
}
#[derive(Debug, thiserror::Error)]
pub enum ProducerDoorError {
#[error("the backend requires a bind address (.bind) before serve; the bind decision is never implied")]
MissingBindAddr,
#[error("tcp bind: {0}")]
Bind(#[source] std::io::Error),
}
pub struct ProducerDoorGuard {
done_rx: Option<watch::Receiver<()>>,
task: Option<tokio::task::JoinHandle<Option<std::io::Error>>>,
}
impl ProducerDoorGuard {
pub async fn close(mut self) -> Option<std::io::Error> {
self.done_rx = None;
let task = self.task.take();
drop(self);
match task {
Some(task) => task.await.ok().flatten(),
None => None,
}
}
}
impl Drop for ProducerDoorGuard {
fn drop(&mut self) {
if let Some(task) = self.task.take() {
task.abort();
}
}
}
impl ProducerDoor {
pub async fn serve(self) -> Result<ProducerDoorGuard, ProducerDoorError> {
let ProducerDoor {
bind_addr,
producer,
idle_first_byte,
max_sessions,
} = self;
let bind_addr = bind_addr.ok_or(ProducerDoorError::MissingBindAddr)?;
let listener = TcpListener::bind(bind_addr)
.await
.map_err(ProducerDoorError::Bind)?;
Ok(serve_on_parts(
listener,
idle_first_byte,
max_sessions,
producer,
))
}
pub async fn serve_on(self, listener: TcpListener) -> ProducerDoorGuard {
let ProducerDoor {
producer,
idle_first_byte,
max_sessions,
..
} = self;
serve_on_parts(listener, idle_first_byte, max_sessions, producer)
}
}
fn serve_on_parts(
listener: TcpListener,
idle_first_byte: Duration,
max_sessions: usize,
producer: Socks5Producer,
) -> ProducerDoorGuard {
let (done_tx, done_rx) = watch::channel(());
let task = tokio::spawn(async move {
let live = Arc::new(Semaphore::new(max_sessions));
loop {
tokio::select! {
_ = done_tx.closed() => return None,
accepted = listener.accept() => {
let (stream, peer) = match accepted {
Ok(pair) => pair,
Err(err) => {
tracing::warn!(error = %err, "producer door: accept failed");
continue;
}
};
let Ok(permit) = live.clone().try_acquire_owned() else {
tracing::warn!(peer = %peer, "producer door: session cap reached, refusing");
drop(stream);
continue;
};
let producer = producer.clone();
tokio::spawn(async move {
let _permit = permit;
let exit = serve_accepted(stream, &producer, idle_first_byte).await;
tracing::debug!(peer = %peer, "producer door: connection ended: {exit}");
});
}
}
}
});
ProducerDoorGuard {
done_rx: Some(done_rx),
task: Some(task),
}
}
async fn serve_accepted(
stream: tokio::net::TcpStream,
producer: &Socks5Producer,
idle: Duration,
) -> BackendExit {
let mut first = [0u8; 1];
match tokio::time::timeout(idle, stream.peek(&mut first)).await {
Ok(Ok(_)) => {}
Ok(Err(err)) => return BackendExit::Io(err),
Err(_) => return BackendExit::ClientGone,
}
match producer.serve_socks5_over(stream).await {
Ok(()) => BackendExit::ClientGone,
Err(err) => BackendExit::Session(err),
}
}