use std::{
io,
net::{IpAddr, Ipv6Addr, SocketAddr},
sync::Arc,
time::Duration,
};
use axum::{Extension, Router, extract::ConnectInfo};
use hyper_util::{
rt::{TokioExecutor, TokioIo, TokioTimer},
server::conn::auto::Builder,
service::TowerToHyperService,
};
use tokio::{net::TcpListener, sync::Semaphore};
use tower::Layer;
use tracing::{debug, error, warn};
use crate::security::rate_limit::{
RateLimitConfig, RateLimitError, RateLimitGuard, WebSocketRateLimiter,
};
const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(20);
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ConnectionLimits {
pub header_read_timeout: Option<Duration>,
pub max_connection_duration: Option<Duration>,
pub max_connections: usize,
pub max_concurrent_streams: Option<u32>,
pub max_connections_per_ip: Option<usize>,
}
impl Default for ConnectionLimits {
fn default() -> Self {
Self {
header_read_timeout: Some(Duration::from_secs(10)),
max_connection_duration: Some(Duration::from_secs(300)),
max_connections: 1024,
max_concurrent_streams: Some(128),
max_connections_per_ip: Some(64),
}
}
}
pub async fn serve_with_limits(
listener: TcpListener,
router: Router,
limits: ConnectionLimits,
) -> std::io::Result<()> {
let mut builder = Builder::new(TokioExecutor::new());
builder.http1().timer(TokioTimer::new());
builder.http2().timer(TokioTimer::new());
if let Some(max_concurrent_streams) = limits.max_concurrent_streams {
builder
.http2()
.max_concurrent_streams(max_concurrent_streams);
}
builder
.http2()
.keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
.keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT);
if let Some(header_read_timeout) = limits.header_read_timeout {
builder.http1().header_read_timeout(header_read_timeout);
}
let builder = Arc::new(builder);
let semaphore = Arc::new(Semaphore::new(limits.max_connections));
let max_connection_duration = limits.max_connection_duration;
let header_read_timeout = limits.header_read_timeout;
let per_ip_limiter = limits.max_connections_per_ip.map(|max_connections_per_ip| {
let limiter = Arc::new(WebSocketRateLimiter::new(RateLimitConfig {
max_connections_per_ip,
..Default::default()
}));
limiter.spawn_cleanup_task(Duration::from_secs(60));
limiter
});
loop {
let Ok(permit) = Arc::clone(&semaphore).acquire_owned().await else {
return Ok(());
};
let (stream, peer_addr) = accept_with_retry(&listener).await;
let guard = match &per_ip_limiter {
Some(limiter) => {
let ip_key = accept_rate_limit_key(peer_addr.ip());
match RateLimitGuard::new(Arc::clone(limiter), ip_key) {
Ok(guard) => Some(guard),
Err(RateLimitError::ConnectionLimitExceeded { .. }) => {
warn!(%peer_addr, "per-IP connection limit exceeded, dropping connection");
continue;
}
Err(error) => {
debug!(
%peer_addr, %error,
"per-IP rate limiter rejected connection for a reason other \
than the per-IP cap; admitting"
);
None
}
}
}
None => None,
};
let peer_service = Extension(ConnectInfo(peer_addr)).layer(router.clone());
let service = TowerToHyperService::new(peer_service);
let builder = Arc::clone(&builder);
tokio::spawn(async move {
let _permit = permit;
let _guard = guard;
if let Some(timeout) = header_read_timeout
&& tokio::time::timeout(timeout, stream.readable())
.await
.is_err()
{
debug!(%peer_addr, "no bytes received within header_read_timeout, dropping");
return;
}
let io = TokioIo::new(stream);
let conn = builder.serve_connection_with_upgrades(io, service);
let result = match max_connection_duration {
Some(deadline) => match tokio::time::timeout(deadline, conn).await {
Ok(result) => result,
Err(_elapsed) => {
debug!("connection exceeded max_connection_duration, dropping");
return;
}
},
None => conn.await,
};
if let Err(error) = result {
debug!(%error, "connection closed with error");
}
});
}
}
fn accept_rate_limit_key(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V4(_) => ip,
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
return IpAddr::V4(v4);
}
if v6.is_loopback() {
return ip;
}
let mut octets = v6.octets();
octets[8..].fill(0);
IpAddr::V6(Ipv6Addr::from(octets))
}
}
}
async fn accept_with_retry(listener: &TcpListener) -> (tokio::net::TcpStream, SocketAddr) {
loop {
match listener.accept().await {
Ok(accepted) => return accepted,
Err(error) if is_connection_error(&error) => continue,
Err(error) => {
error!(%error, "accept error, retrying after backoff");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}
fn is_connection_error(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::ConnectionRefused
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::ConnectionReset
)
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
#[test]
fn ipv4_addresses_pass_through_unmasked() {
let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7));
assert_eq!(accept_rate_limit_key(ip), ip);
}
#[test]
fn ipv6_addresses_sharing_a_64_prefix_mask_to_the_same_key() {
let a: IpAddr = "2001:db8:1234:5678:aaaa:bbbb:cccc:dddd".parse().unwrap();
let b: IpAddr = "2001:db8:1234:5678:1111:2222:3333:4444".parse().unwrap();
assert_eq!(accept_rate_limit_key(a), accept_rate_limit_key(b));
}
#[test]
fn ipv6_addresses_with_different_64_prefixes_mask_to_different_keys() {
let a: IpAddr = "2001:db8:1234:5678::1".parse().unwrap();
let b: IpAddr = "2001:db8:1234:5679::1".parse().unwrap();
assert_ne!(accept_rate_limit_key(a), accept_rate_limit_key(b));
}
#[test]
fn masked_ipv6_key_zeroes_exactly_the_low_64_bits() {
let ip: IpAddr = "2001:db8:1234:5678:ffff:ffff:ffff:ffff".parse().unwrap();
let expected: IpAddr = "2001:db8:1234:5678::".parse().unwrap();
assert_eq!(accept_rate_limit_key(ip), expected);
}
#[test]
fn ipv4_mapped_ipv6_addresses_are_not_collapsed_into_one_key() {
let a: IpAddr = "::ffff:203.0.113.7".parse().unwrap();
let b: IpAddr = "::ffff:198.51.100.99".parse().unwrap();
assert_ne!(
accept_rate_limit_key(a),
accept_rate_limit_key(b),
"distinct IPv4-mapped addresses must not share a per-IP rate-limit key"
);
}
#[test]
fn ipv4_mapped_ipv6_address_keys_like_its_ipv4_form() {
let mapped: IpAddr = "::ffff:203.0.113.7".parse().unwrap();
let plain = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7));
assert_ne!(
accept_rate_limit_key(mapped),
accept_rate_limit_key(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
"an IPv4-mapped address must not key as the unspecified `::/64` bucket"
);
assert_eq!(
accept_rate_limit_key(mapped),
accept_rate_limit_key(plain),
"an IPv4-mapped address should key identically to its plain IPv4 form"
);
}
}