Namespace synlimit netfilter rules per target set

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-07-01 01:47:14 +03:00
parent 81ae483201
commit 451227da60
4 changed files with 327 additions and 82 deletions
+80 -38
View File
@@ -1,10 +1,8 @@
use std::net::IpAddr; use std::net::IpAddr;
use super::command::run_command; use super::command::run_command;
use super::model::{SynLimitRule, SynLimitTargets, synlimit_rate_arg}; use super::model::{SynLimitNamespace, SynLimitRule, SynLimitTargets, synlimit_rate_arg};
const IPTABLES_CHAIN: &str = "TELEMT_SYNLIMIT";
const IPTABLES_HASHLIMIT_PREFIX: &str = "TMT-SYN";
const IPV4_IOS_PACKET_LENGTH: u16 = 64; const IPV4_IOS_PACKET_LENGTH: u16 = 64;
const IPV6_IOS_PACKET_LENGTH: u16 = 84; const IPV6_IOS_PACKET_LENGTH: u16 = 84;
const IOS_TTL_LIMIT: u8 = 65; const IOS_TTL_LIMIT: u8 = 65;
@@ -38,24 +36,41 @@ impl IpTablesFamily {
} }
} }
pub(super) async fn apply_synlimit_rules(targets: &SynLimitTargets) -> Result<(), String> { pub(super) async fn apply_synlimit_rules(
apply_rules_for_binary("iptables", &targets.iptables_v4, IpTablesFamily::V4).await?; targets: &SynLimitTargets,
apply_rules_for_binary("ip6tables", &targets.iptables_v6, IpTablesFamily::V6).await namespace: &SynLimitNamespace,
) -> Result<(), String> {
apply_rules_for_binary(
"iptables",
&targets.iptables_v4,
IpTablesFamily::V4,
namespace,
)
.await?;
apply_rules_for_binary(
"ip6tables",
&targets.iptables_v6,
IpTablesFamily::V6,
namespace,
)
.await
} }
async fn apply_rules_for_binary( async fn apply_rules_for_binary(
binary: &str, binary: &str,
targets: &[SynLimitRule], targets: &[SynLimitRule],
family: IpTablesFamily, family: IpTablesFamily,
namespace: &SynLimitNamespace,
) -> Result<(), String> { ) -> Result<(), String> {
if targets.is_empty() { if targets.is_empty() {
return Ok(()); return Ok(());
} }
let _ = run_command(binary, &["-t", "filter", "-N", IPTABLES_CHAIN], None).await; let chain = namespace.iptables_chain.as_str();
run_command(binary, &["-t", "filter", "-F", IPTABLES_CHAIN], None).await?; let _ = run_command(binary, &["-t", "filter", "-N", chain], None).await;
run_command(binary, &["-t", "filter", "-F", chain], None).await?;
if run_command( if run_command(
binary, binary,
&["-t", "filter", "-C", "INPUT", "-j", IPTABLES_CHAIN], &["-t", "filter", "-C", "INPUT", "-j", chain],
None, None,
) )
.await .await
@@ -63,24 +78,19 @@ async fn apply_rules_for_binary(
{ {
run_command( run_command(
binary, binary,
&["-t", "filter", "-I", "INPUT", "1", "-j", IPTABLES_CHAIN], &["-t", "filter", "-I", "INPUT", "1", "-j", chain],
None, None,
) )
.await?; .await?;
} }
for (idx, target) in targets.iter().enumerate() { for (idx, target) in targets.iter().enumerate() {
for rule in iptables_synfix_rule_args(target, idx, family) { for rule in iptables_synfix_rule_args(target, idx, family, namespace) {
let refs: Vec<&str> = rule.iter().map(String::as_str).collect(); let refs: Vec<&str> = rule.iter().map(String::as_str).collect();
run_command(binary, &refs, None).await?; run_command(binary, &refs, None).await?;
} }
} }
run_command( run_command(binary, &["-t", "filter", "-A", chain, "-j", "RETURN"], None).await?;
binary,
&["-t", "filter", "-A", IPTABLES_CHAIN, "-j", "RETURN"],
None,
)
.await?;
Ok(()) Ok(())
} }
@@ -89,12 +99,13 @@ fn iptables_synfix_rule_args(
target: &SynLimitRule, target: &SynLimitRule,
idx: usize, idx: usize,
family: IpTablesFamily, family: IpTablesFamily,
namespace: &SynLimitNamespace,
) -> Vec<Vec<String>> { ) -> Vec<Vec<String>> {
vec![ vec![
iptables_ios_accept_rule_args(target, idx, family), iptables_ios_accept_rule_args(target, idx, family, namespace),
iptables_ios_reject_rule_args(target, family), iptables_ios_reject_rule_args(target, family, namespace),
iptables_generic_accept_rule_args(target, idx, family), iptables_generic_accept_rule_args(target, idx, family, namespace),
iptables_generic_reject_rule_args(target), iptables_generic_reject_rule_args(target, namespace),
] ]
} }
@@ -102,12 +113,15 @@ fn iptables_ios_accept_rule_args(
target: &SynLimitRule, target: &SynLimitRule,
idx: usize, idx: usize,
family: IpTablesFamily, family: IpTablesFamily,
namespace: &SynLimitNamespace,
) -> Vec<String> { ) -> Vec<String> {
let hashlimit_name = format!( let hashlimit_name = format!(
"{IPTABLES_HASHLIMIT_PREFIX}-I{}-{idx}", "{}-I{}-{idx}",
namespace.iptables_hashlimit_prefix,
family.hashlimit_tag() family.hashlimit_tag()
); );
let mut args = iptables_base_rule_args(target.ip, target.port); let mut args =
iptables_base_rule_args(namespace.iptables_chain.as_str(), target.ip, target.port);
args.extend(iptables_ios_match_args(family)); args.extend(iptables_ios_match_args(family));
args.extend(iptables_hashlimit_args( args.extend(iptables_hashlimit_args(
&hashlimit_name, &hashlimit_name,
@@ -121,8 +135,13 @@ fn iptables_ios_accept_rule_args(
args args
} }
fn iptables_ios_reject_rule_args(target: &SynLimitRule, family: IpTablesFamily) -> Vec<String> { fn iptables_ios_reject_rule_args(
let mut args = iptables_base_rule_args(target.ip, target.port); target: &SynLimitRule,
family: IpTablesFamily,
namespace: &SynLimitNamespace,
) -> Vec<String> {
let mut args =
iptables_base_rule_args(namespace.iptables_chain.as_str(), target.ip, target.port);
args.extend(iptables_ios_match_args(family)); args.extend(iptables_ios_match_args(family));
args.extend(iptables_reject_args()); args.extend(iptables_reject_args());
args args
@@ -132,12 +151,15 @@ fn iptables_generic_accept_rule_args(
target: &SynLimitRule, target: &SynLimitRule,
idx: usize, idx: usize,
family: IpTablesFamily, family: IpTablesFamily,
namespace: &SynLimitNamespace,
) -> Vec<String> { ) -> Vec<String> {
let hashlimit_name = format!( let hashlimit_name = format!(
"{IPTABLES_HASHLIMIT_PREFIX}-G{}-{idx}", "{}-G{}-{idx}",
namespace.iptables_hashlimit_prefix,
family.hashlimit_tag() family.hashlimit_tag()
); );
let mut args = iptables_base_rule_args(target.ip, target.port); let mut args =
iptables_base_rule_args(namespace.iptables_chain.as_str(), target.ip, target.port);
args.extend(iptables_hashlimit_args( args.extend(iptables_hashlimit_args(
&hashlimit_name, &hashlimit_name,
target.generic_seconds, target.generic_seconds,
@@ -150,18 +172,22 @@ fn iptables_generic_accept_rule_args(
args args
} }
fn iptables_generic_reject_rule_args(target: &SynLimitRule) -> Vec<String> { fn iptables_generic_reject_rule_args(
let mut args = iptables_base_rule_args(target.ip, target.port); target: &SynLimitRule,
namespace: &SynLimitNamespace,
) -> Vec<String> {
let mut args =
iptables_base_rule_args(namespace.iptables_chain.as_str(), target.ip, target.port);
args.extend(iptables_reject_args()); args.extend(iptables_reject_args());
args args
} }
fn iptables_base_rule_args(ip: Option<IpAddr>, port: u16) -> Vec<String> { fn iptables_base_rule_args(chain: &str, ip: Option<IpAddr>, port: u16) -> Vec<String> {
let mut args = vec![ let mut args = vec![
"-t".to_string(), "-t".to_string(),
"filter".to_string(), "filter".to_string(),
"-A".to_string(), "-A".to_string(),
IPTABLES_CHAIN.to_string(), chain.to_string(),
"-p".to_string(), "-p".to_string(),
"tcp".to_string(), "tcp".to_string(),
"--syn".to_string(), "--syn".to_string(),
@@ -226,13 +252,17 @@ fn iptables_reject_args() -> Vec<String> {
] ]
} }
pub(super) async fn clear_rules_for_binary(binary: &str) -> Result<bool, String> { pub(super) async fn clear_rules_for_binary(
binary: &str,
namespace: &SynLimitNamespace,
) -> Result<bool, String> {
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut removed = false; let mut removed = false;
let chain = namespace.iptables_chain.as_str();
for _ in 0..8 { for _ in 0..8 {
match run_command( match run_command(
binary, binary,
&["-t", "filter", "-D", "INPUT", "-j", IPTABLES_CHAIN], &["-t", "filter", "-D", "INPUT", "-j", chain],
None, None,
) )
.await .await
@@ -247,7 +277,7 @@ pub(super) async fn clear_rules_for_binary(binary: &str) -> Result<bool, String>
} }
} }
} }
match run_command(binary, &["-t", "filter", "-F", IPTABLES_CHAIN], None).await { match run_command(binary, &["-t", "filter", "-F", chain], None).await {
Ok(()) => { Ok(()) => {
removed = true; removed = true;
} }
@@ -256,7 +286,7 @@ pub(super) async fn clear_rules_for_binary(binary: &str) -> Result<bool, String>
errors.push(format!("{binary} flush chain failed: {error}")); errors.push(format!("{binary} flush chain failed: {error}"));
} }
} }
match run_command(binary, &["-t", "filter", "-X", IPTABLES_CHAIN], None).await { match run_command(binary, &["-t", "filter", "-X", chain], None).await {
Ok(()) => { Ok(()) => {
removed = true; removed = true;
} }
@@ -296,12 +326,22 @@ mod tests {
args.iter().any(|arg| arg == key) args.iter().any(|arg| arg == key)
} }
fn test_namespace() -> SynLimitNamespace {
SynLimitNamespace {
nft_table: "telemt_synlimit_test".to_string(),
iptables_chain: "TMT_SYN_TEST".to_string(),
iptables_hashlimit_prefix: "TMTTEST".to_string(),
}
}
#[test] #[test]
fn iptables_rules_use_synfix_order_and_rejects() { fn iptables_rules_use_synfix_order_and_rejects() {
let target = test_rule(Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7))), 443); let target = test_rule(Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7))), 443);
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V4); let namespace = test_namespace();
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V4, &namespace);
assert_eq!(rules.len(), 4); assert_eq!(rules.len(), 4);
assert!(has_pair(&rules[0], "-A", "TMT_SYN_TEST"));
assert!(has_pair(&rules[0], "--length", "64")); assert!(has_pair(&rules[0], "--length", "64"));
assert!(has_pair(&rules[0], "--ttl-lt", "65")); assert!(has_pair(&rules[0], "--ttl-lt", "65"));
assert!(has_pair(&rules[0], "--hashlimit-upto", "12/second")); assert!(has_pair(&rules[0], "--hashlimit-upto", "12/second"));
@@ -318,7 +358,8 @@ mod tests {
#[test] #[test]
fn ip6tables_rules_use_ipv6_hoplimit_classifier() { fn ip6tables_rules_use_ipv6_hoplimit_classifier() {
let target = test_rule(Some(IpAddr::V6(Ipv6Addr::LOCALHOST)), 443); let target = test_rule(Some(IpAddr::V6(Ipv6Addr::LOCALHOST)), 443);
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V6); let namespace = test_namespace();
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V6, &namespace);
assert!(has_pair(&rules[0], "--length", "84")); assert!(has_pair(&rules[0], "--length", "84"));
assert!(has_pair(&rules[0], "--hl-lt", "65")); assert!(has_pair(&rules[0], "--hl-lt", "65"));
@@ -347,7 +388,8 @@ mod tests {
#[test] #[test]
fn iptables_wildcard_rule_omits_destination_match() { fn iptables_wildcard_rule_omits_destination_match() {
let target = test_rule(None, 443); let target = test_rule(None, 443);
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V4); let namespace = test_namespace();
let rules = iptables_synfix_rule_args(&target, 0, IpTablesFamily::V4, &namespace);
for rule in rules { for rule in rules {
assert!(!has_key(&rule, "-d")); assert!(!has_key(&rule, "-d"));
+72 -19
View File
@@ -1,4 +1,4 @@
use std::sync::Arc; use std::sync::{Arc, Mutex};
use tokio::sync::watch; use tokio::sync::watch;
use tracing::warn; use tracing::warn;
@@ -11,7 +11,9 @@ mod model;
mod nftables; mod nftables;
use self::command::has_cap_net_admin; use self::command::has_cap_net_admin;
use self::model::synlimit_targets; use self::model::{SynLimitNamespace, synlimit_namespace, synlimit_targets};
static ACTIVE_SYNLIMIT_NAMESPACE: Mutex<Option<SynLimitNamespace>> = Mutex::new(None);
pub(crate) fn spawn_synlimit_controller(config_rx: watch::Receiver<Arc<ProxyConfig>>) { pub(crate) fn spawn_synlimit_controller(config_rx: watch::Receiver<Arc<ProxyConfig>>) {
if !cfg!(target_os = "linux") { if !cfg!(target_os = "linux") {
@@ -39,7 +41,34 @@ async fn wait_for_config_channel_close_and_reconcile(
} }
pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) { pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) {
match clear_synlimit_rules_all_backends().await { let targets = synlimit_targets(cfg);
let namespace = synlimit_namespace(&targets);
if let Some(previous_namespace) = set_active_synlimit_namespace(namespace.clone()) {
match clear_synlimit_rules_for_namespace(&previous_namespace).await {
Ok(true) => {
warn!("Removed previous SYN limiter namespace before reconcile");
}
Ok(false) => {}
Err(error) => {
warn!(error = %error, "Failed to clear previous SYN limiter namespace before reconcile");
}
}
}
if targets.is_empty() {
return;
}
let Some(namespace) = namespace else {
return;
};
if !has_cap_net_admin() {
warn!(
"SYN limiter configured but CAP_NET_ADMIN is not available; netfilter rules not applied"
);
return;
}
match clear_synlimit_rules_for_namespace(&namespace).await {
Ok(true) => { Ok(true) => {
warn!("Removed stale SYN limiter rules left by a previous run before reconcile"); warn!("Removed stale SYN limiter rules left by a previous run before reconcile");
} }
@@ -49,37 +78,35 @@ pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) {
} }
} }
let targets = synlimit_targets(cfg);
if targets.is_empty() {
return;
}
if !has_cap_net_admin() {
warn!(
"SYN limiter configured but CAP_NET_ADMIN is not available; netfilter rules not applied"
);
return;
}
if targets.has_iptables_targets() if targets.has_iptables_targets()
&& let Err(error) = iptables::apply_synlimit_rules(&targets).await && let Err(error) = iptables::apply_synlimit_rules(&targets, &namespace).await
{ {
warn!(error = %error, "Failed to apply iptables SYN limiter rules"); warn!(error = %error, "Failed to apply iptables SYN limiter rules");
} }
if targets.has_nft_targets() if targets.has_nft_targets()
&& let Err(error) = nftables::apply_synlimit_rules(&targets).await && let Err(error) = nftables::apply_synlimit_rules(&targets, &namespace).await
{ {
warn!(error = %error, "Failed to apply nftables SYN limiter rules"); warn!(error = %error, "Failed to apply nftables SYN limiter rules");
} }
} }
pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String> { pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String> {
let Some(namespace) = take_active_synlimit_namespace() else {
return Ok(false);
};
clear_synlimit_rules_for_namespace(&namespace).await
}
async fn clear_synlimit_rules_for_namespace(
namespace: &SynLimitNamespace,
) -> Result<bool, String> {
if !has_cap_net_admin() { if !has_cap_net_admin() {
return Ok(false); return Ok(false);
} }
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut removed = false; let mut removed = false;
match nftables::clear_rules_all_families().await { match nftables::clear_rules_all_families(namespace).await {
Ok(value) => { Ok(value) => {
removed |= value; removed |= value;
} }
@@ -87,7 +114,7 @@ pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String>
errors.push(error); errors.push(error);
} }
} }
match iptables::clear_rules_for_binary("iptables").await { match iptables::clear_rules_for_binary("iptables", namespace).await {
Ok(value) => { Ok(value) => {
removed |= value; removed |= value;
} }
@@ -95,7 +122,7 @@ pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String>
errors.push(error); errors.push(error);
} }
} }
match iptables::clear_rules_for_binary("ip6tables").await { match iptables::clear_rules_for_binary("ip6tables", namespace).await {
Ok(value) => { Ok(value) => {
removed |= value; removed |= value;
} }
@@ -111,6 +138,32 @@ pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String>
} }
} }
fn set_active_synlimit_namespace(next: Option<SynLimitNamespace>) -> Option<SynLimitNamespace> {
match ACTIVE_SYNLIMIT_NAMESPACE.lock() {
Ok(mut active) => {
if *active == next {
None
} else {
std::mem::replace(&mut *active, next)
}
}
Err(error) => {
warn!(error = %error, "Failed to update active SYN limiter namespace");
None
}
}
}
fn take_active_synlimit_namespace() -> Option<SynLimitNamespace> {
match ACTIVE_SYNLIMIT_NAMESPACE.lock() {
Ok(mut active) => active.take(),
Err(error) => {
warn!(error = %error, "Failed to read active SYN limiter namespace");
None
}
}
}
fn has_synlimit_config(cfg: &ProxyConfig) -> bool { fn has_synlimit_config(cfg: &ProxyConfig) -> bool {
cfg.server cfg.server
.listeners .listeners
+128
View File
@@ -17,6 +17,13 @@ pub(super) struct SynLimitRule {
pub(super) hashlimit_size: u32, pub(super) hashlimit_size: u32,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct SynLimitNamespace {
pub(super) nft_table: String,
pub(super) iptables_chain: String,
pub(super) iptables_hashlimit_prefix: String,
}
#[derive(Default)] #[derive(Default)]
pub(super) struct SynLimitTargets { pub(super) struct SynLimitTargets {
pub(super) iptables_v4: Vec<SynLimitRule>, pub(super) iptables_v4: Vec<SynLimitRule>,
@@ -42,6 +49,44 @@ impl SynLimitTargets {
} }
} }
struct SynLimitNamespaceHasher {
value: u64,
}
impl SynLimitNamespaceHasher {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
fn new() -> Self {
Self {
value: Self::OFFSET,
}
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.value ^= u64::from(*byte);
self.value = self.value.wrapping_mul(Self::PRIME);
}
}
fn write_u8(&mut self, value: u8) {
self.write(&[value]);
}
fn write_u16(&mut self, value: u16) {
self.write(&value.to_le_bytes());
}
fn write_u32(&mut self, value: u32) {
self.write(&value.to_le_bytes());
}
fn finish(self) -> u64 {
self.value
}
}
pub(super) fn synlimit_targets(cfg: &ProxyConfig) -> SynLimitTargets { pub(super) fn synlimit_targets(cfg: &ProxyConfig) -> SynLimitTargets {
let mut iptables_v4 = BTreeSet::new(); let mut iptables_v4 = BTreeSet::new();
let mut iptables_v6 = BTreeSet::new(); let mut iptables_v6 = BTreeSet::new();
@@ -91,6 +136,64 @@ pub(super) fn synlimit_targets(cfg: &ProxyConfig) -> SynLimitTargets {
} }
} }
pub(super) fn synlimit_namespace(targets: &SynLimitTargets) -> Option<SynLimitNamespace> {
if targets.is_empty() {
return None;
}
let mut hasher = SynLimitNamespaceHasher::new();
write_namespace_rule_group(&mut hasher, b"iptables-v4", &targets.iptables_v4);
write_namespace_rule_group(&mut hasher, b"iptables-v6", &targets.iptables_v6);
write_namespace_rule_group(&mut hasher, b"nft-v4", &targets.nft_v4);
write_namespace_rule_group(&mut hasher, b"nft-v6", &targets.nft_v6);
let suffix = format!("{:016x}", hasher.finish());
let iptables_suffix = &suffix[..12];
let hashlimit_suffix = &suffix[..10];
Some(SynLimitNamespace {
nft_table: format!("telemt_synlimit_{suffix}"),
iptables_chain: format!("TMT_SYN_{iptables_suffix}"),
iptables_hashlimit_prefix: format!("TMT{hashlimit_suffix}"),
})
}
fn write_namespace_rule_group(
hasher: &mut SynLimitNamespaceHasher,
group: &[u8],
rules: &[SynLimitRule],
) {
hasher.write(group);
hasher.write_u32(rules.len() as u32);
for rule in rules {
write_namespace_rule(hasher, rule);
}
}
fn write_namespace_rule(hasher: &mut SynLimitNamespaceHasher, rule: &SynLimitRule) {
match rule.ip {
Some(IpAddr::V4(ip)) => {
hasher.write_u8(4);
hasher.write(&ip.octets());
}
Some(IpAddr::V6(ip)) => {
hasher.write_u8(6);
hasher.write(&ip.octets());
}
None => {
hasher.write_u8(0);
}
}
hasher.write_u16(rule.port);
hasher.write_u32(rule.generic_seconds);
hasher.write_u32(rule.generic_hitcount);
hasher.write_u32(rule.generic_burst);
hasher.write_u32(rule.ios_seconds);
hasher.write_u32(rule.ios_hitcount);
hasher.write_u32(rule.ios_burst);
hasher.write_u32(rule.hashlimit_expire_ms);
hasher.write_u32(rule.hashlimit_size);
}
pub(super) fn synlimit_rate_arg(seconds: u32, hitcount: u32) -> String { pub(super) fn synlimit_rate_arg(seconds: u32, hitcount: u32) -> String {
let seconds = u64::from(seconds.max(1)); let seconds = u64::from(seconds.max(1));
let hitcount = u64::from(hitcount.max(1)); let hitcount = u64::from(hitcount.max(1));
@@ -224,6 +327,31 @@ mod tests {
assert_eq!(targets.nft_v6[0].ip, None); assert_eq!(targets.nft_v6[0].ip, None);
} }
#[test]
fn synlimit_namespace_is_stable_and_changes_by_targets() {
let mut cfg = ProxyConfig::default();
cfg.server.listeners = vec![listener(
IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)),
Some(443),
SynLimitMode::Nftables,
)];
let first = synlimit_namespace(&synlimit_targets(&cfg))
.expect("configured targets must have a namespace");
let second = synlimit_namespace(&synlimit_targets(&cfg))
.expect("configured targets must have a namespace");
cfg.server.listeners[0].port = Some(444);
let changed = synlimit_namespace(&synlimit_targets(&cfg))
.expect("configured targets must have a namespace");
assert_eq!(first, second);
assert_ne!(first, changed);
assert!(first.nft_table.starts_with("telemt_synlimit_"));
assert!(first.iptables_chain.starts_with("TMT_SYN_"));
assert!(first.iptables_chain.len() <= 28);
assert!(first.iptables_hashlimit_prefix.starts_with("TMT"));
}
#[test] #[test]
fn synlimit_rate_arg_uses_native_units_without_fractional_rates() { fn synlimit_rate_arg_uses_native_units_without_fractional_rates() {
assert_eq!(synlimit_rate_arg(1, 12), "12/second"); assert_eq!(synlimit_rate_arg(1, 12), "12/second");
+47 -25
View File
@@ -1,7 +1,6 @@
use super::command::{run_command, run_command_stdout}; use super::command::{run_command, run_command_stdout};
use super::model::{SynLimitRule, SynLimitTargets, synlimit_rate_arg}; use super::model::{SynLimitNamespace, SynLimitRule, SynLimitTargets, synlimit_rate_arg};
const NFT_TABLE: &str = "telemt_synlimit";
const NFT_CHAIN: &str = "input"; const NFT_CHAIN: &str = "input";
const NFT_INPUT_PRIORITY: i16 = -5; const NFT_INPUT_PRIORITY: i16 = -5;
const IPV4_IOS_PACKET_LENGTH: u16 = 64; const IPV4_IOS_PACKET_LENGTH: u16 = 64;
@@ -38,10 +37,13 @@ impl NftFamily {
} }
} }
pub(super) async fn apply_synlimit_rules(targets: &SynLimitTargets) -> Result<(), String> { pub(super) async fn apply_synlimit_rules(
targets: &SynLimitTargets,
namespace: &SynLimitNamespace,
) -> Result<(), String> {
let families = detect_nft_table_families().await; let families = detect_nft_table_families().await;
for plan in nft_apply_plan(families, &targets.nft_v4, &targets.nft_v6) { for plan in nft_apply_plan(families, &targets.nft_v4, &targets.nft_v6) {
let script = nft_synlimit_script(plan); let script = nft_synlimit_script(plan, namespace);
run_command("nft", &["-f", "-"], Some(script)).await?; run_command("nft", &["-f", "-"], Some(script)).await?;
} }
@@ -114,9 +116,13 @@ fn nft_apply_plan<'a>(
Vec::new() Vec::new()
} }
fn nft_synlimit_script(plan: NftApplyPlan<'_>) -> String { fn nft_synlimit_script(plan: NftApplyPlan<'_>, namespace: &SynLimitNamespace) -> String {
let mut script = String::new(); let mut script = String::new();
script.push_str(&format!("table {} {NFT_TABLE} {{\n", plan.family.as_str())); script.push_str(&format!(
"table {} {} {{\n",
plan.family.as_str(),
namespace.nft_table
));
script.push_str(&format!(" chain {NFT_CHAIN} {{\n")); script.push_str(&format!(" chain {NFT_CHAIN} {{\n"));
script.push_str(&format!( script.push_str(&format!(
" type filter hook input priority {NFT_INPUT_PRIORITY}; policy accept;\n" " type filter hook input priority {NFT_INPUT_PRIORITY}; policy accept;\n"
@@ -186,16 +192,14 @@ fn push_nft_v6_rules(script: &mut String, target: &SynLimitRule, idx: usize) {
)); ));
} }
pub(super) async fn clear_rules_all_families() -> Result<bool, String> { pub(super) async fn clear_rules_all_families(
namespace: &SynLimitNamespace,
) -> Result<bool, String> {
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut removed = false; let mut removed = false;
let table = namespace.nft_table.as_str();
for family in [NftFamily::Inet, NftFamily::Ip, NftFamily::Ip6] { for family in [NftFamily::Inet, NftFamily::Ip, NftFamily::Ip6] {
match run_command( match run_command("nft", &["delete", "table", family.as_str(), table], None).await
"nft",
&["delete", "table", family.as_str(), NFT_TABLE],
None,
)
.await
{ {
Ok(()) => { Ok(()) => {
removed = true; removed = true;
@@ -203,8 +207,8 @@ pub(super) async fn clear_rules_all_families() -> Result<bool, String> {
Err(error) if is_missing_command_or_nft_table(&error) => {} Err(error) if is_missing_command_or_nft_table(&error) => {}
Err(error) => { Err(error) => {
errors.push(format!( errors.push(format!(
"nft delete table {} {NFT_TABLE} failed: {error}", "nft delete table {} {table} failed: {error}",
family.as_str() family.as_str(),
)); ));
} }
} }
@@ -228,15 +232,28 @@ mod tests {
use super::*; use super::*;
use crate::synlimit_control::model::test_rule; use crate::synlimit_control::model::test_rule;
fn test_namespace(table: &str) -> SynLimitNamespace {
SynLimitNamespace {
nft_table: table.to_string(),
iptables_chain: "TMT_SYN_TEST".to_string(),
iptables_hashlimit_prefix: "TMTTEST".to_string(),
}
}
#[test] #[test]
fn nft_script_uses_synfix_v4_rules_and_early_priority() { fn nft_script_uses_synfix_v4_rules_and_early_priority() {
let rule = test_rule(Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7))), 443); let rule = test_rule(Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7))), 443);
let script = nft_synlimit_script(NftApplyPlan { let namespace = test_namespace("telemt_synlimit_test_a");
family: NftFamily::Inet, let script = nft_synlimit_script(
v4_targets: &[rule], NftApplyPlan {
v6_targets: &[], family: NftFamily::Inet,
}); v4_targets: &[rule],
v6_targets: &[],
},
&namespace,
);
assert!(script.contains("table inet telemt_synlimit_test_a"));
assert!(script.contains("type filter hook input priority -5; policy accept;")); assert!(script.contains("type filter hook input priority -5; policy accept;"));
assert!(script.contains("ip daddr 203.0.113.7")); assert!(script.contains("ip daddr 203.0.113.7"));
assert!(script.contains("meta length 64 ip ttl < 65")); assert!(script.contains("meta length 64 ip ttl < 65"));
@@ -248,12 +265,17 @@ mod tests {
#[test] #[test]
fn nft_script_uses_ipv6_hoplimit_classifier() { fn nft_script_uses_ipv6_hoplimit_classifier() {
let rule = test_rule(Some(IpAddr::V6(Ipv6Addr::LOCALHOST)), 443); let rule = test_rule(Some(IpAddr::V6(Ipv6Addr::LOCALHOST)), 443);
let script = nft_synlimit_script(NftApplyPlan { let namespace = test_namespace("telemt_synlimit_test_b");
family: NftFamily::Inet, let script = nft_synlimit_script(
v4_targets: &[], NftApplyPlan {
v6_targets: &[rule], family: NftFamily::Inet,
}); v4_targets: &[],
v6_targets: &[rule],
},
&namespace,
);
assert!(script.contains("table inet telemt_synlimit_test_b"));
assert!(script.contains("ip6 daddr ::1")); assert!(script.contains("ip6 daddr ::1"));
assert!(script.contains("meta length 84 ip6 hoplimit < 65")); assert!(script.contains("meta length 84 ip6 hoplimit < 65"));
assert!(script.contains("ip6 saddr limit rate over 12/second burst 24 packets")); assert!(script.contains("ip6 saddr limit rate over 12/second burst 24 packets"));