mirror of
https://github.com/telemt/telemt.git
synced 2026-07-11 22:40:24 +03:00
CidrRateLimitKey with IpNetwork parsing and serialization added
This commit is contained in:
+153
-4
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user