use crate::logging::emit::Log;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, OwnedSemaphorePermit, Semaphore};
pub const DEFAULT_MAX_CONNECTIONS: usize = 256;
const REFUSAL_WRITE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Clone, Debug)]
pub struct ConnectionLimiter {
slots: Arc<Semaphore>,
max: usize,
}
impl ConnectionLimiter {
pub fn new(max: usize) -> Self {
let max = max.max(1);
Self {
slots: Arc::new(Semaphore::new(max)),
max,
}
}
pub fn max(&self) -> usize {
self.max
}
pub fn in_use(&self) -> usize {
self.max - self.slots.available_permits()
}
pub fn try_acquire(&self) -> Option<ConnectionPermit> {
Arc::clone(&self.slots)
.try_acquire_owned()
.ok()
.map(|permit| ConnectionPermit { _permit: permit })
}
}
#[derive(Debug)]
pub struct ConnectionPermit {
_permit: OwnedSemaphorePermit,
}
pub async fn accept_bounded(
listener: &TcpListener,
limiter: &ConnectionLimiter,
refusal: &[u8],
protocol: &str,
status_tx: Option<&mpsc::UnboundedSender<String>>,
) -> std::io::Result<(TcpStream, SocketAddr, ConnectionPermit)> {
loop {
let (socket, peer_addr) = listener.accept().await?;
match limiter.try_acquire() {
Some(permit) => return Ok((socket, peer_addr, permit)),
None => {
Log::new(status_tx).warn(format!(
"{} connection from {} refused decision=fail_closed_connection_cap \
(limit {} concurrent)",
protocol,
peer_addr,
limiter.max()
));
refuse(socket, refusal).await;
}
}
}
}
#[cfg(unix)]
pub async fn accept_bounded_unix(
listener: &tokio::net::UnixListener,
limiter: &ConnectionLimiter,
refusal: &[u8],
protocol: &str,
status_tx: Option<&mpsc::UnboundedSender<String>>,
) -> std::io::Result<(tokio::net::UnixStream, ConnectionPermit)> {
loop {
let (socket, _) = listener.accept().await?;
match limiter.try_acquire() {
Some(permit) => return Ok((socket, permit)),
None => {
let path = listener
.local_addr()
.ok()
.and_then(|a| a.as_pathname().map(|p| p.display().to_string()))
.unwrap_or_else(|| "<unnamed>".to_string());
Log::new(status_tx).warn(format!(
"{} connection on {} refused decision=fail_closed_connection_cap \
(limit {} concurrent)",
protocol,
path,
limiter.max()
));
refuse(socket, refusal).await;
}
}
}
}
async fn refuse<S: tokio::io::AsyncWrite + Unpin>(mut socket: S, refusal: &[u8]) {
let _ = tokio::time::timeout(REFUSAL_WRITE_TIMEOUT, async {
if !refusal.is_empty() {
let _ = socket.write_all(refusal).await;
let _ = socket.flush().await;
}
let _ = socket.shutdown().await;
})
.await;
}
const RESPONSE_DRAIN_BYTES: usize = 2 * 1024 * 1024;
const RESPONSE_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
pub(crate) async fn drain_after_response(reader: &mut (impl AsyncRead + Unpin)) {
use tokio::io::AsyncReadExt;
let deadline = tokio::time::Instant::now() + RESPONSE_DRAIN_TIMEOUT;
let mut remaining = RESPONSE_DRAIN_BYTES;
let mut scratch = [0u8; 8192];
while remaining > 0 {
let read_size = remaining.min(scratch.len());
match tokio::time::timeout_at(deadline, reader.read(&mut scratch[..read_size])).await {
Ok(Ok(n)) if n > 0 => remaining -= n,
_ => break,
}
}
}
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::{AsyncRead, ReadBuf};
pub struct IdleTimeoutReader<R> {
inner: R,
first: Duration,
idle: Duration,
seen_bytes: bool,
deadline: Option<Pin<Box<tokio::time::Sleep>>>,
}
impl<R> IdleTimeoutReader<R> {
pub fn new(inner: R, idle: Duration) -> Self {
Self::with_first(inner, idle, idle)
}
pub fn with_first(inner: R, first: Duration, idle: Duration) -> Self {
Self {
inner,
first,
idle,
seen_bytes: false,
deadline: None,
}
}
}
impl<R: AsyncRead + Unpin> AsyncRead for IdleTimeoutReader<R> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
let before = buf.filled().len();
match Pin::new(&mut this.inner).poll_read(cx, buf) {
Poll::Ready(result) => {
this.deadline = None;
if result.is_ok() && buf.filled().len() > before {
this.seen_bytes = true;
}
Poll::Ready(result)
}
Poll::Pending => {
let bound = if this.seen_bytes {
this.idle
} else {
this.first
};
let deadline = this
.deadline
.get_or_insert_with(|| Box::pin(tokio::time::sleep(bound)));
match deadline.as_mut().poll(cx) {
Poll::Ready(()) => {
this.deadline = None;
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"peer sent nothing before the idle deadline",
)))
}
Poll::Pending => Poll::Pending,
}
}
}
}
}
#[derive(Debug)]
pub struct ConnectionActivity {
last: AtomicU64,
in_flight: AtomicUsize,
}
impl Default for ConnectionActivity {
fn default() -> Self {
Self::new()
}
}
impl ConnectionActivity {
pub fn new() -> Self {
Self {
last: AtomicU64::new(now_millis()),
in_flight: AtomicUsize::new(0),
}
}
pub fn touch(&self) {
self.last.store(now_millis(), Ordering::Relaxed);
}
pub fn begin_work(&self) {
self.touch();
self.in_flight.fetch_add(1, Ordering::SeqCst);
}
pub fn end_work(&self) {
self.in_flight.fetch_sub(1, Ordering::SeqCst);
self.touch();
}
pub fn busy(self: &Arc<Self>) -> BusyGuard {
self.begin_work();
BusyGuard(Arc::clone(self))
}
pub fn idle_for(&self) -> Option<Duration> {
if self.in_flight.load(Ordering::SeqCst) > 0 {
return None;
}
let last = self.last.load(Ordering::Relaxed);
Some(Duration::from_millis(now_millis().saturating_sub(last)))
}
}
pub struct BusyGuard(Arc<ConnectionActivity>);
impl Drop for BusyGuard {
fn drop(&mut self) {
self.0.end_work();
}
}
pub async fn watch_idle(activity: Arc<ConnectionActivity>, idle: Duration) {
let tick = (idle / 20).clamp(Duration::from_millis(250), Duration::from_secs(15));
loop {
tokio::time::sleep(tick).await;
if let Some(elapsed) = activity.idle_for() {
if elapsed >= idle {
return;
}
}
}
}
pub async fn watch_idle_with_probe<F: FnMut()>(
activity: Arc<ConnectionActivity>,
idle: Duration,
mut probe: F,
) {
let tick = (idle / 20).clamp(Duration::from_millis(100), Duration::from_secs(15));
let mut probed = false;
loop {
tokio::time::sleep(tick).await;
match activity.idle_for() {
Some(elapsed) if elapsed >= idle => return,
Some(elapsed) if elapsed >= idle / 2 => {
if !probed {
probe();
probed = true;
}
}
_ => probed = false,
}
}
}
pub const KEEPALIVE_PING_PAYLOAD: &[u8] = b"netget-keepalive";
fn now_millis() -> u64 {
crate::utils::clock::Instant::now()
.duration_since(*PROCESS_START)
.as_millis() as u64
}
static PROCESS_START: std::sync::LazyLock<crate::utils::clock::Instant> =
std::sync::LazyLock::new(crate::utils::clock::Instant::now);
#[cfg(test)]
mod response_drain_tests {
use super::*;
#[tokio::test]
async fn response_drain_stops_at_the_byte_budget() {
let bytes = vec![0; RESPONSE_DRAIN_BYTES + 1];
let mut reader = bytes.as_slice();
drain_after_response(&mut reader).await;
assert_eq!(
reader.len(),
1,
"an endless uploader cannot keep the task alive"
);
}
#[tokio::test]
async fn response_drain_stops_even_when_the_peer_keeps_its_write_half_open() {
let (mut reader, _silent_peer) = tokio::io::duplex(1);
let started = tokio::time::Instant::now();
tokio::time::timeout(
RESPONSE_DRAIN_TIMEOUT + std::time::Duration::from_secs(5),
drain_after_response(&mut reader),
)
.await
.expect("a silent peer cannot keep the task alive");
assert!(started.elapsed() >= RESPONSE_DRAIN_TIMEOUT);
}
}