CidrRateLimitKey with IpNetwork parsing and serialization added

This commit is contained in:
Alexey
2026-07-04 10:54:55 +03:00
parent 451227da60
commit 50538d234e
+153 -4
View File
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::PathBuf; use std::path::PathBuf;
@@ -2098,11 +2099,13 @@ pub struct AccessConfig {
/// Per-CIDR aggregate transport rate limits in bits-per-second. /// Per-CIDR aggregate transport rate limits in bits-per-second.
/// ///
/// Matching uses longest-prefix-wins semantics. A value of `0` in one /// Explicit CIDR keys use longest-prefix-wins semantics. Auto-template
/// direction means "unlimited" for that direction. Limits are amortized /// keys (`*4/N`, `*6/N`, `*/N`) lazily create per-source-subnet buckets
/// with the same bounded-burst contract as per-user rate limits. /// 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)] #[serde(default)]
pub cidr_rate_limits: HashMap<IpNetwork, RateLimitBps>, pub cidr_rate_limits: HashMap<CidrRateLimitKey, RateLimitBps>,
/// Per-username client source IP/CIDR deny list. Checked after successful /// Per-username client source IP/CIDR deny list. Checked after successful
/// authentication; matching IPs get the same rejection path as invalid auth /// 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<CidrAutoTemplate>; 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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for CidrRateLimitKey {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<CidrRateLimitKey, String> {
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::<IpNetwork>()
.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<u8, String> {
let prefix = prefix.parse::<u8>().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)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimitBps { pub struct RateLimitBps {
/// Upload direction limit in bits-per-second; `0` means unlimited.
#[serde(default)] #[serde(default)]
pub up_bps: u64, pub up_bps: u64,
/// Download direction limit in bits-per-second; `0` means unlimited.
#[serde(default)] #[serde(default)]
pub down_bps: u64, pub down_bps: u64,
} }