mirror of
https://github.com/telemt/telemt.git
synced 2026-07-01 15:21:09 +03:00
81ae483201
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
235 lines
7.3 KiB
Rust
235 lines
7.3 KiB
Rust
use std::collections::BTreeSet;
|
|
use std::net::IpAddr;
|
|
|
|
use crate::config::{ProxyConfig, SynLimitMode};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub(super) struct SynLimitRule {
|
|
pub(super) ip: Option<IpAddr>,
|
|
pub(super) port: u16,
|
|
pub(super) generic_seconds: u32,
|
|
pub(super) generic_hitcount: u32,
|
|
pub(super) generic_burst: u32,
|
|
pub(super) ios_seconds: u32,
|
|
pub(super) ios_hitcount: u32,
|
|
pub(super) ios_burst: u32,
|
|
pub(super) hashlimit_expire_ms: u32,
|
|
pub(super) hashlimit_size: u32,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct SynLimitTargets {
|
|
pub(super) iptables_v4: Vec<SynLimitRule>,
|
|
pub(super) iptables_v6: Vec<SynLimitRule>,
|
|
pub(super) nft_v4: Vec<SynLimitRule>,
|
|
pub(super) nft_v6: Vec<SynLimitRule>,
|
|
}
|
|
|
|
impl SynLimitTargets {
|
|
pub(super) fn is_empty(&self) -> bool {
|
|
self.iptables_v4.is_empty()
|
|
&& self.iptables_v6.is_empty()
|
|
&& self.nft_v4.is_empty()
|
|
&& self.nft_v6.is_empty()
|
|
}
|
|
|
|
pub(super) fn has_iptables_targets(&self) -> bool {
|
|
!self.iptables_v4.is_empty() || !self.iptables_v6.is_empty()
|
|
}
|
|
|
|
pub(super) fn has_nft_targets(&self) -> bool {
|
|
!self.nft_v4.is_empty() || !self.nft_v6.is_empty()
|
|
}
|
|
}
|
|
|
|
pub(super) fn synlimit_targets(cfg: &ProxyConfig) -> SynLimitTargets {
|
|
let mut iptables_v4 = BTreeSet::new();
|
|
let mut iptables_v6 = BTreeSet::new();
|
|
let mut nft_v4 = BTreeSet::new();
|
|
let mut nft_v6 = BTreeSet::new();
|
|
|
|
for listener in &cfg.server.listeners {
|
|
let backend = listener.synlimit;
|
|
if matches!(backend, SynLimitMode::Off) {
|
|
continue;
|
|
}
|
|
let target = SynLimitRule {
|
|
ip: (!listener.ip.is_unspecified()).then_some(listener.ip),
|
|
port: listener.port.unwrap_or(cfg.server.port),
|
|
generic_seconds: listener.synlimit_seconds,
|
|
generic_hitcount: listener.synlimit_hitcount,
|
|
generic_burst: listener.synlimit_burst,
|
|
ios_seconds: listener.synlimit_ios_seconds,
|
|
ios_hitcount: listener.synlimit_ios_hitcount,
|
|
ios_burst: listener.synlimit_ios_burst,
|
|
hashlimit_expire_ms: listener.synlimit_hashlimit_expire_ms,
|
|
hashlimit_size: listener.synlimit_hashlimit_size,
|
|
};
|
|
|
|
match (backend, listener.ip.is_ipv4()) {
|
|
(SynLimitMode::Iptables, true) => {
|
|
iptables_v4.insert(target);
|
|
}
|
|
(SynLimitMode::Iptables, false) => {
|
|
iptables_v6.insert(target);
|
|
}
|
|
(SynLimitMode::Nftables, true) => {
|
|
nft_v4.insert(target);
|
|
}
|
|
(SynLimitMode::Nftables, false) => {
|
|
nft_v6.insert(target);
|
|
}
|
|
(SynLimitMode::Off, _) => {}
|
|
}
|
|
}
|
|
|
|
SynLimitTargets {
|
|
iptables_v4: iptables_v4.into_iter().collect(),
|
|
iptables_v6: iptables_v6.into_iter().collect(),
|
|
nft_v4: nft_v4.into_iter().collect(),
|
|
nft_v6: nft_v6.into_iter().collect(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn synlimit_rate_arg(seconds: u32, hitcount: u32) -> String {
|
|
let seconds = u64::from(seconds.max(1));
|
|
let hitcount = u64::from(hitcount.max(1));
|
|
for (unit_seconds, unit_name) in [
|
|
(1_u64, "second"),
|
|
(60_u64, "minute"),
|
|
(3_600_u64, "hour"),
|
|
(86_400_u64, "day"),
|
|
] {
|
|
let amount = hitcount.saturating_mul(unit_seconds);
|
|
if amount >= seconds && amount % seconds == 0 {
|
|
return format!("{}/{}", amount / seconds, unit_name);
|
|
}
|
|
}
|
|
let amount = hitcount.saturating_mul(86_400).saturating_add(seconds - 1) / seconds;
|
|
format!("{}/day", amount.max(1))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn test_rule(ip: Option<IpAddr>, port: u16) -> SynLimitRule {
|
|
SynLimitRule {
|
|
ip,
|
|
port,
|
|
generic_seconds: 60,
|
|
generic_hitcount: 48,
|
|
generic_burst: 1,
|
|
ios_seconds: 1,
|
|
ios_hitcount: 12,
|
|
ios_burst: 24,
|
|
hashlimit_expire_ms: 60_000,
|
|
hashlimit_size: 32_768,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
|
|
use super::*;
|
|
use crate::config::ListenerConfig;
|
|
|
|
fn listener(ip: IpAddr, port: Option<u16>, synlimit: SynLimitMode) -> ListenerConfig {
|
|
ListenerConfig {
|
|
ip,
|
|
port,
|
|
client_mss: None,
|
|
synlimit,
|
|
synlimit_seconds: 60,
|
|
synlimit_hitcount: 48,
|
|
synlimit_burst: 1,
|
|
synlimit_ios_seconds: 1,
|
|
synlimit_ios_hitcount: 12,
|
|
synlimit_ios_burst: 24,
|
|
synlimit_hashlimit_expire_ms: 60_000,
|
|
synlimit_hashlimit_size: 32_768,
|
|
announce: None,
|
|
announce_ip: None,
|
|
proxy_protocol: None,
|
|
reuse_allow: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn synlimit_targets_deduplicate_and_use_legacy_port_fallback() {
|
|
let mut cfg = ProxyConfig::default();
|
|
cfg.server.port = 9443;
|
|
cfg.server.listeners = vec![
|
|
listener(
|
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
|
None,
|
|
SynLimitMode::Iptables,
|
|
),
|
|
listener(
|
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
|
None,
|
|
SynLimitMode::Iptables,
|
|
),
|
|
];
|
|
|
|
let targets = synlimit_targets(&cfg);
|
|
|
|
assert_eq!(targets.iptables_v4.len(), 1);
|
|
assert_eq!(targets.iptables_v4[0].ip, None);
|
|
assert_eq!(targets.iptables_v4[0].port, 9443);
|
|
assert!(targets.iptables_v6.is_empty());
|
|
assert!(targets.nft_v4.is_empty());
|
|
assert!(targets.nft_v6.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn synlimit_targets_separate_backends_and_ip_families() {
|
|
let mut cfg = ProxyConfig::default();
|
|
cfg.server.listeners = vec![
|
|
listener(
|
|
IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)),
|
|
Some(443),
|
|
SynLimitMode::Iptables,
|
|
),
|
|
listener(
|
|
IpAddr::V6(Ipv6Addr::LOCALHOST),
|
|
Some(443),
|
|
SynLimitMode::Iptables,
|
|
),
|
|
listener(
|
|
IpAddr::V4(Ipv4Addr::new(203, 0, 113, 2)),
|
|
Some(444),
|
|
SynLimitMode::Nftables,
|
|
),
|
|
listener(
|
|
IpAddr::V6(Ipv6Addr::UNSPECIFIED),
|
|
Some(444),
|
|
SynLimitMode::Nftables,
|
|
),
|
|
];
|
|
|
|
let targets = synlimit_targets(&cfg);
|
|
|
|
assert_eq!(targets.iptables_v4.len(), 1);
|
|
assert_eq!(targets.iptables_v6.len(), 1);
|
|
assert_eq!(targets.nft_v4.len(), 1);
|
|
assert_eq!(targets.nft_v6.len(), 1);
|
|
assert_eq!(
|
|
targets.iptables_v4[0].ip,
|
|
Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)))
|
|
);
|
|
assert_eq!(targets.iptables_v6[0].ip, Some(IpAddr::V6(Ipv6Addr::LOCALHOST)));
|
|
assert_eq!(
|
|
targets.nft_v4[0].ip,
|
|
Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 2)))
|
|
);
|
|
assert_eq!(targets.nft_v6[0].ip, None);
|
|
}
|
|
|
|
#[test]
|
|
fn synlimit_rate_arg_uses_native_units_without_fractional_rates() {
|
|
assert_eq!(synlimit_rate_arg(1, 12), "12/second");
|
|
assert_eq!(synlimit_rate_arg(60, 48), "48/minute");
|
|
assert_eq!(synlimit_rate_arg(3600, 121), "121/hour");
|
|
assert_eq!(synlimit_rate_arg(86400, 241), "241/day");
|
|
}
|
|
}
|