use crate::rate_limiter::{RateLimiterProps, RateLimiterState, Units};
use fxhash::FxHashMap;
use log::warn;
use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub trait ProvideIpLimiter: Clone {
fn provide_ip_limiter<R>(&self, f: impl FnOnce(&mut IpLimiter) -> R) -> R;
fn stats(&self, mut visitor: impl FnMut(IpAddr, IpStats)) {
self.provide_ip_limiter(|this| {
for (ip, v) in &this.usage {
visitor(*ip, v.stats);
}
});
}
fn total_connections(&self) -> u32 {
self.provide_ip_limiter(|this| this.total_connections)
}
fn set_bandwidth_limits(&self, bytes_per_second: Units, bytes_burst: Units) {
self.provide_ip_limiter(|this| {
this.connection_rate_limit =
RateLimiterProps::new_throughput(bytes_per_second, bytes_burst);
})
}
fn set_custom_limits(&self, props: RateLimiterProps) {
self.provide_ip_limiter(|this| {
this.custom_rate_limit = props;
})
}
fn set_connections_per_active_p90(&self, connections_per_active_p90: u32) {
self.provide_ip_limiter(|this| {
this.connections_per_active_p90 = connections_per_active_p90;
this.connections_per_active_p99 = this
.connections_per_active_p99
.max(connections_per_active_p90);
})
}
fn set_connections_per_active_p99(&self, connections_per_active_p99: u32) {
self.provide_ip_limiter(|this| {
this.connections_per_active_p99 = connections_per_active_p99;
this.connections_per_active_p90 = this
.connections_per_active_p90
.min(connections_per_active_p99);
})
}
fn set_total_connections_soft_limit(&self, total_connections_soft_limit: u32) {
self.provide_ip_limiter(|this| {
this.total_connections_soft_limit = total_connections_soft_limit;
})
}
fn set_total_connections_hard_limit(&self, total_connections_hard_limit: u32) {
self.provide_ip_limiter(|this| {
this.total_connections_hard_limit = total_connections_hard_limit;
})
}
fn should_limit_bandwidth(
&self,
ip: IpAddr,
bytes: Units,
label: &'static str,
now: Instant,
) -> bool {
self.provide_ip_limiter(|this| this.should_limit_bandwidth_inner(ip, bytes, label, now))
}
fn should_limit_custom(&self, ip: IpAddr, usage: Units, now: Instant) -> bool {
self.provide_ip_limiter(|this| {
let entry = this
.usage
.entry(ip)
.or_insert_with(|| Usage::new(now, &mut this.new_ip_counter));
entry
.custom_rate_limit
.should_limit_rate_with_now_and_usage(&this.custom_rate_limit, now, usage)
})
}
fn set_ddos_memory(&self, ddos_memory: Duration) {
self.provide_ip_limiter(|this| {
this.ddos_memory = ddos_memory;
})
}
fn compute_pressure(&self) -> bool {
self.provide_ip_limiter(|this| this.compute_pressure)
}
fn set_compute_pressure(&self, compute_pressure: bool) {
self.provide_ip_limiter(|this| {
this.compute_pressure = compute_pressure;
});
}
}
#[derive(Copy, Clone, Default, Debug)]
pub struct SystemIpLimiter;
impl ProvideIpLimiter for SystemIpLimiter {
fn provide_ip_limiter<R>(&self, f: impl FnOnce(&mut IpLimiter) -> R) -> R {
static SINGLETON: Mutex<Option<IpLimiter>> = Mutex::new(None);
let mut opt = SINGLETON.lock().unwrap();
let this = opt.get_or_insert_with(Default::default);
f(this)
}
}
#[derive(Clone, Default, Debug)]
pub struct ArcIpLimiter(Arc<Mutex<IpLimiter>>);
impl ProvideIpLimiter for ArcIpLimiter {
fn provide_ip_limiter<R>(&self, f: impl FnOnce(&mut IpLimiter) -> R) -> R {
let mut this = self.0.lock().unwrap();
f(&mut this)
}
}
#[derive(Debug)]
pub struct IpLimiter {
usage: FxHashMap<IpAddr, Usage>,
connection_rate_limit: RateLimiterProps,
custom_rate_limit: RateLimiterProps,
next_prune: Instant,
warning_limiter: RateLimiterState,
pending: WarningSet,
connections_per_active_p90: u32,
connections_per_active_p99: u32,
total_connections: u32,
total_connections_soft_limit: u32,
total_connections_hard_limit: u32,
last_soft_limit: Option<Instant>,
ddos_memory: Duration,
compute_pressure: bool,
new_ip_counter: u32,
}
#[derive(Debug)]
struct WarningSet {
small: Vec<Warning>,
small_full: bool,
bandwidth: FxHashMap<IpAddr, u32>,
connections: FxHashMap<IpAddr, u32>,
granted_rare_exemptions: u32,
}
#[derive(Debug)]
struct Warning {
ip: IpAddr,
kind: WarningKind,
label: &'static str,
}
#[derive(Debug)]
enum WarningKind {
Bandwidth {
amount: u32,
},
ConnectionCount {
connections: u32,
active_sessions: u32,
limit: u32,
granted_rare_exemption: bool,
},
}
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub struct IpStats {
pub first: Instant,
pub connections: u32,
pub active_sessions: u32,
pub last_limit: Option<Instant>,
}
impl IpStats {
fn limited(&mut self, now: Instant) {
self.last_limit = Some(now);
}
}
impl Default for IpLimiter {
fn default() -> Self {
Self::new(500000, 1000000)
}
}
impl IpLimiter {
pub(crate) fn should_limit_bandwidth_inner(
&mut self,
ip: IpAddr,
bytes: Units,
label: &'static str,
now: Instant,
) -> bool {
let should_rate_limit = self.should_limit_bandwidth_inner_inner(ip, bytes, now);
if should_rate_limit {
self.warn(
Warning {
label,
ip,
kind: WarningKind::Bandwidth { amount: bytes },
},
now,
);
}
should_rate_limit
}
}
#[derive(Debug)]
struct Usage {
connection_rate_limit: RateLimiterState,
custom_rate_limit: RateLimiterState,
stats: IpStats,
granted_rare_exemption: bool,
}
impl Usage {
fn new(now: Instant, new_ip_counter: &mut u32) -> Self {
*new_ip_counter = new_ip_counter.saturating_add(1);
Self {
connection_rate_limit: RateLimiterState {
until: now,
burst_used: 0,
},
custom_rate_limit: RateLimiterState {
until: now,
burst_used: 0,
},
stats: IpStats {
first: now,
connections: 0,
active_sessions: 0,
last_limit: None,
},
granted_rare_exemption: false,
}
}
}
const WARNING_LIMIT: RateLimiterProps = RateLimiterProps::const_new(Duration::from_secs(1), 0);
#[derive(Debug)]
pub struct ConnectionPermit<P: ProvideIpLimiter = SystemIpLimiter>(IpAddr, P);
impl ConnectionPermit<SystemIpLimiter> {
pub fn new(ip: IpAddr, label: &'static str) -> Option<Self> {
Self::new_with(ip, label, SystemIpLimiter)
}
}
impl<P: ProvideIpLimiter> ConnectionPermit<P> {
pub fn new_with(ip: IpAddr, label: &'static str, provide: P) -> Option<Self> {
let now = Instant::now();
provide
.provide_ip_limiter(|limiter| {
let entry = limiter
.usage
.entry(ip)
.or_insert_with(|| Usage::new(now, &mut limiter.new_ip_counter));
if limiter.total_connections >= limiter.total_connections_hard_limit {
let warning = Warning {
ip,
label,
kind: WarningKind::ConnectionCount {
connections: entry.stats.connections,
active_sessions: entry.stats.active_sessions,
limit: entry.stats.connections,
granted_rare_exemption: false,
},
};
limiter.warn(warning, now);
return None;
}
let amount = 10000;
let should_rate_limit = entry
.connection_rate_limit
.should_limit_rate_with_now_and_usage(
&limiter.connection_rate_limit,
now,
amount,
);
if should_rate_limit {
entry.stats.limited(now);
limiter.warn(
Warning {
label,
ip,
kind: WarningKind::Bandwidth { amount },
},
now,
);
return None;
}
let old = now.duration_since(entry.stats.first) > Duration::from_secs(60);
let soft_limit_reached = limiter.compute_pressure
|| limiter.total_connections >= limiter.total_connections_soft_limit;
if soft_limit_reached {
limiter.last_soft_limit = Some(now);
}
let recent_global_soft_limit = limiter
.last_soft_limit
.filter(|&last| now.duration_since(last) < limiter.ddos_memory)
.is_some();
let recent_local_limit = entry
.stats
.last_limit
.filter(|&last| now.duration_since(last) < limiter.ddos_memory)
.is_some();
let strict_limit = (!old || recent_local_limit) && recent_global_soft_limit;
let mut just_granted_rare_exemption = false;
let granted_rare_exemption = if entry.granted_rare_exemption {
true
} else if strict_limit && !recent_local_limit && limiter.new_ip_counter >= 100 {
limiter.new_ip_counter =
(limiter.new_ip_counter - 100).min(limiter.new_ip_counter / 2);
entry.granted_rare_exemption = true;
just_granted_rare_exemption = true;
true
} else {
false
};
let limit = (entry.stats.active_sessions + 1 + (!strict_limit) as u32)
.saturating_mul(if strict_limit && !granted_rare_exemption {
limiter.connections_per_active_p90
} else {
limiter.connections_per_active_p99
});
if entry.stats.connections >= limit {
entry.stats.limited(now);
let warning = Warning {
ip,
label,
kind: WarningKind::ConnectionCount {
connections: entry.stats.connections,
active_sessions: entry.stats.active_sessions,
limit,
granted_rare_exemption: false,
},
};
limiter.warn(warning, now);
None
} else {
entry.stats.connections += 1;
limiter.total_connections += 1;
if just_granted_rare_exemption {
let warning = Warning {
ip,
label,
kind: WarningKind::ConnectionCount {
connections: entry.stats.connections,
active_sessions: entry.stats.active_sessions,
limit,
granted_rare_exemption: true,
},
};
limiter.warn(warning, now);
}
Some(ip)
}
})
.map(|ip| Self(ip, provide))
}
}
impl<P: ProvideIpLimiter> Drop for ConnectionPermit<P> {
fn drop(&mut self) {
self.1.provide_ip_limiter(|limiter| {
if let Some(usage) = limiter.usage.get_mut(&self.0) {
debug_assert!(usage.stats.connections > 0);
usage.stats.connections = usage.stats.connections.saturating_sub(1);
} else {
debug_assert!(false);
}
debug_assert!(limiter.total_connections > 0);
limiter.total_connections = limiter.total_connections.saturating_sub(1);
})
}
}
#[derive(Debug)]
pub struct ActiveSession<P: ProvideIpLimiter = SystemIpLimiter>(IpAddr, P);
impl ActiveSession<SystemIpLimiter> {
pub fn new(addr: IpAddr) -> Self {
Self::new_with(addr, SystemIpLimiter)
}
}
impl<P: ProvideIpLimiter> ActiveSession<P> {
pub fn new_with(addr: IpAddr, provide: P) -> Self {
provide.provide_ip_limiter(|limiter| {
limiter
.usage
.entry(addr)
.or_insert_with(|| Usage::new(Instant::now(), &mut limiter.new_ip_counter))
.stats
.active_sessions += 1;
});
Self(addr, provide)
}
}
impl<P: ProvideIpLimiter> Drop for ActiveSession<P> {
fn drop(&mut self) {
self.1.provide_ip_limiter(|limiter| {
if let Some(usage) = limiter.usage.get_mut(&self.0) {
debug_assert!(usage.stats.active_sessions > 0);
usage.stats.active_sessions = usage.stats.active_sessions.saturating_sub(1);
} else {
debug_assert!(false);
}
})
}
}
impl IpLimiter {
pub(crate) fn new(bytes_per_second: Units, bytes_burst: Units) -> Self {
Self {
usage: FxHashMap::default(),
connection_rate_limit: RateLimiterProps::new_throughput(bytes_per_second, bytes_burst),
custom_rate_limit: RateLimiterProps::no_limit(),
next_prune: Instant::now(),
warning_limiter: Default::default(),
pending: WarningSet {
small: Vec::with_capacity(5),
small_full: false,
bandwidth: Default::default(),
connections: Default::default(),
granted_rare_exemptions: 0,
},
connections_per_active_p90: 1,
connections_per_active_p99: 6,
total_connections: 0,
total_connections_soft_limit: 400,
total_connections_hard_limit: 1000,
last_soft_limit: None,
ddos_memory: Duration::from_secs(5 * 60),
compute_pressure: false,
new_ip_counter: 200,
}
}
fn warn(&mut self, warning: Warning, now: Instant) {
if !self.pending.small_full && self.pending.small.len() < 5 {
self.pending.small.push(warning);
} else {
self.pending.small_full = true;
for warning in self.pending.small.drain(..).chain(std::iter::once(warning)) {
match warning.kind {
WarningKind::Bandwidth { amount } => {
let entry = self.pending.bandwidth.entry(warning.ip).or_default();
*entry = entry.saturating_add(amount);
}
WarningKind::ConnectionCount {
granted_rare_exemption,
..
} => {
if granted_rare_exemption {
self.pending.granted_rare_exemptions =
self.pending.granted_rare_exemptions.saturating_add(1);
} else {
let entry = self.pending.connections.entry(warning.ip).or_default();
*entry = entry.saturating_add(1);
}
}
}
}
}
if self
.warning_limiter
.should_limit_rate_with_now(&WARNING_LIMIT, now)
{
return;
}
if !self.pending.small_full {
for Warning { ip, label, kind } in self.pending.small.drain(..) {
match kind {
WarningKind::Bandwidth { amount } => {
warn!("{ip} exceeded bw limit with {label} ({amount}B)");
}
WarningKind::ConnectionCount {
connections,
active_sessions,
limit,
granted_rare_exemption,
} => {
let event = if granted_rare_exemption {
"granted special"
} else {
"hit"
};
warn!("{ip} {event} conn limit {limit} with {label} ({active_sessions} act, {connections})");
}
}
}
return;
}
if let Some(sample) = self.pending.bandwidth.keys().next() {
let mut bytes = self
.pending
.bandwidth
.values()
.copied()
.map(|v| v as u64)
.sum::<u64>();
let mut unit = "B";
if bytes >= 1000 {
bytes /= 1000;
unit = "KB";
}
if bytes >= 1000 {
bytes /= 1000;
unit = "MB";
}
if bytes >= 1000 {
bytes /= 1000;
unit = "GB";
}
warn!(
"{} IP's, e.g. {sample}, hit bw limit with {bytes}{unit}",
self.pending.bandwidth.len()
);
self.pending.bandwidth.clear();
}
if let Some(sample) = self.pending.connections.keys().next() {
let attempts = self.pending.connections.values().copied().sum::<u32>();
warn!(
"{} IP's, e.g. {sample}, hit conn limit with {attempts} attempts ({} exempt. granted)",
self.pending.connections.len(),
self.pending.granted_rare_exemptions,
);
self.pending.connections.clear();
self.pending.granted_rare_exemptions = 0;
} else if self.pending.granted_rare_exemptions > 0 {
warn!("{} exempt. granted", self.pending.granted_rare_exemptions);
self.pending.granted_rare_exemptions = 0;
}
self.pending.small_full = false;
}
pub(crate) fn should_limit_bandwidth_inner_inner(
&mut self,
ip: IpAddr,
bytes: Units,
now: Instant,
) -> bool {
let entry = self
.usage
.entry(ip)
.or_insert_with(|| Usage::new(now, &mut self.new_ip_counter));
let should_limit_rate = entry
.connection_rate_limit
.should_limit_rate_with_now_and_usage(&self.connection_rate_limit, now, bytes);
if should_limit_rate {
entry.stats.limited(now);
}
self.maybe_prune(now);
should_limit_rate
}
fn maybe_prune(&mut self, now: Instant) {
if now < self.next_prune {
return;
}
self.next_prune = now + Duration::from_secs(5).max(self.ddos_memory / 2);
self.prune(now);
}
fn prune(&mut self, now: Instant) {
let forget = now + self.ddos_memory;
self.usage.retain(|_, usage: &mut Usage| {
usage.connection_rate_limit.until > forget
|| usage.custom_rate_limit.until > forget
|| usage.stats.active_sessions > 0
|| usage.stats.connections > 0
})
}
#[allow(unused)]
pub(crate) fn len(&self) -> usize {
self.usage.len()
}
#[allow(unused)]
pub(crate) fn is_empty(&self) -> bool {
self.usage.is_empty()
}
}
#[cfg(test)]
mod test {
use super::IpLimiter;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::{Duration, Instant};
#[test]
pub fn ip_rate_limiter() {
let ip_one = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let ip_two = IpAddr::V6(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8));
let mut limiter = IpLimiter::new(10, 3);
limiter.ddos_memory = Duration::ZERO;
assert_eq!(limiter.len(), 0);
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_one, 1, Instant::now()));
assert_eq!(limiter.len(), 1);
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_one, 1, Instant::now()));
assert_eq!(limiter.len(), 1);
limiter.prune(Instant::now());
assert_eq!(limiter.len(), 1);
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_one, 1, Instant::now()));
assert_eq!(limiter.len(), 1);
limiter.prune(Instant::now());
assert_eq!(limiter.len(), 1);
assert!(limiter.should_limit_bandwidth_inner_inner(ip_one, 1, Instant::now()));
assert_eq!(limiter.len(), 1);
std::thread::sleep(Duration::from_millis(250));
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_two, 1, Instant::now()));
assert_eq!(limiter.len(), 2);
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_two, 1, Instant::now()));
assert_eq!(limiter.len(), 2);
limiter.prune(Instant::now());
assert_eq!(limiter.len(), 2);
std::thread::sleep(Duration::from_millis(100));
limiter.prune(Instant::now());
assert_eq!(limiter.len(), 1);
std::thread::sleep(Duration::from_millis(500));
limiter.prune(Instant::now());
assert_eq!(limiter.len(), 0);
assert!(!limiter.should_limit_bandwidth_inner_inner(ip_one, 1, Instant::now()));
assert_eq!(limiter.len(), 1);
}
}