diff --git a/src/config/types.rs b/src/config/types.rs index c249746..b89dbe2 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use ipnetwork::IpNetwork; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; use std::net::IpAddr; use std::path::PathBuf; @@ -2098,11 +2099,13 @@ pub struct AccessConfig { /// Per-CIDR aggregate transport rate limits in bits-per-second. /// - /// Matching uses longest-prefix-wins semantics. A value of `0` in one - /// direction means "unlimited" for that direction. Limits are amortized - /// with the same bounded-burst contract as per-user rate limits. + /// Explicit CIDR keys use longest-prefix-wins semantics. Auto-template + /// keys (`*4/N`, `*6/N`, `*/N`) lazily create per-source-subnet buckets + /// after explicit CIDR matching misses. A value of `0` in one direction + /// means "unlimited" for that direction. Limits are amortized with the + /// same bounded-burst contract as per-user rate limits. #[serde(default)] - pub cidr_rate_limits: HashMap, + pub cidr_rate_limits: HashMap, /// Per-username client source IP/CIDR deny list. Checked after successful /// authentication; matching IPs get the same rejection path as invalid auth @@ -2171,10 +2174,156 @@ impl AccessConfig { } } +/// Key used by `access.cidr_rate_limits`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CidrRateLimitKey { + /// Explicit source CIDR rule. + Network(IpNetwork), + /// IPv4 auto-template that creates one bucket for each matching `/N`. + AutoV4(u8), + /// IPv6 auto-template that creates one bucket for each matching `/N`. + AutoV6(u8), + /// Dual-stack auto-template; IPv4 uses `/N`, IPv6 uses `/(N * 4)`. + AutoDual(u8), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum CidrAutoTemplateFamily { + V4, + V6, +} + +impl CidrAutoTemplateFamily { + pub(crate) fn marker(self) -> &'static str { + match self { + Self::V4 => "*4", + Self::V6 => "*6", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct CidrAutoTemplate { + pub(crate) family: CidrAutoTemplateFamily, + pub(crate) prefix_len: u8, +} + +impl fmt::Display for CidrAutoTemplate { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}/{}", self.family.marker(), self.prefix_len) + } +} + +impl CidrRateLimitKey { + pub(crate) fn auto_templates(&self) -> [Option; 2] { + match *self { + Self::Network(_) => [None, None], + Self::AutoV4(prefix_len) => [ + Some(CidrAutoTemplate { + family: CidrAutoTemplateFamily::V4, + prefix_len, + }), + None, + ], + Self::AutoV6(prefix_len) => [ + Some(CidrAutoTemplate { + family: CidrAutoTemplateFamily::V6, + prefix_len, + }), + None, + ], + Self::AutoDual(prefix_len) => [ + Some(CidrAutoTemplate { + family: CidrAutoTemplateFamily::V4, + prefix_len, + }), + Some(CidrAutoTemplate { + family: CidrAutoTemplateFamily::V6, + prefix_len: prefix_len.saturating_mul(4), + }), + ], + } + } +} + +impl fmt::Display for CidrRateLimitKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Network(cidr) => write!(formatter, "{cidr}"), + Self::AutoV4(prefix_len) => write!(formatter, "*4/{prefix_len}"), + Self::AutoV6(prefix_len) => write!(formatter, "*6/{prefix_len}"), + Self::AutoDual(prefix_len) => write!(formatter, "*/{prefix_len}"), + } + } +} + +impl Serialize for CidrRateLimitKey { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for CidrRateLimitKey { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + parse_cidr_rate_limit_key(&value).map_err(serde::de::Error::custom) + } +} + +fn parse_cidr_rate_limit_key(value: &str) -> std::result::Result { + if let Some(prefix) = value.strip_prefix("*4/") { + return parse_cidr_auto_prefix(value, prefix, 32).map(CidrRateLimitKey::AutoV4); + } + if let Some(prefix) = value.strip_prefix("*6/") { + return parse_cidr_auto_prefix(value, prefix, 128).map(CidrRateLimitKey::AutoV6); + } + if let Some(prefix) = value.strip_prefix("*/") { + return parse_cidr_auto_prefix(value, prefix, 32).map(CidrRateLimitKey::AutoDual); + } + if value.starts_with('*') { + return Err(format!( + "invalid CIDR rate limit key {value:?}; expected CIDR, *4/N, *6/N, or */N" + )); + } + value + .parse::() + .map(CidrRateLimitKey::Network) + .map_err(|error| { + format!( + "invalid CIDR rate limit key {value:?}: {error}; expected CIDR, *4/N, *6/N, or */N" + ) + }) +} + +fn parse_cidr_auto_prefix( + key: &str, + prefix: &str, + max_prefix: u8, +) -> std::result::Result { + let prefix = prefix.parse::().map_err(|_| { + format!("invalid CIDR auto-template key {key:?}; prefix must be within 0..={max_prefix}") + })?; + if prefix > max_prefix { + return Err(format!( + "invalid CIDR auto-template key {key:?}; prefix must be within 0..={max_prefix}" + )); + } + Ok(prefix) +} + +/// Transport rate limit in bits-per-second. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RateLimitBps { + /// Upload direction limit in bits-per-second; `0` means unlimited. #[serde(default)] pub up_bps: u64, + /// Download direction limit in bits-per-second; `0` means unlimited. #[serde(default)] pub down_bps: u64, }