From fe56621a833fee21d6e3cb817e9a71a29fdea840 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:43:26 +0300 Subject: [PATCH] Delete synlimit_control.rs --- src/synlimit_control.rs | 605 ---------------------------------------- 1 file changed, 605 deletions(-) delete mode 100644 src/synlimit_control.rs diff --git a/src/synlimit_control.rs b/src/synlimit_control.rs deleted file mode 100644 index 8ca8d7c..0000000 --- a/src/synlimit_control.rs +++ /dev/null @@ -1,605 +0,0 @@ -use std::collections::BTreeSet; -use std::net::IpAddr; -use std::path::PathBuf; -use std::sync::Arc; - -use tokio::io::AsyncWriteExt; -use tokio::process::Command; -use tokio::sync::watch; -use tracing::warn; - -use crate::config::{ProxyConfig, SynLimitMode}; - -const IPTABLES_CHAIN: &str = "TELEMT_SYNLIMIT"; -const IPTABLES_HASHLIMIT_NAME: &str = "TELEMT-BUMPER"; -const NFT_TABLE: &str = "telemt_synlimit"; -const NFT_CHAIN: &str = "input"; -type SynLimitTarget = (Option, u16, u32, u32, u32); - -#[derive(Default)] -struct SynLimitTargets { - iptables_v4: Vec, - iptables_v6: Vec, - nft_v4: Vec, - nft_v6: Vec, -} - -#[derive(Clone, Copy)] -struct NftTableFamilies { - inet: bool, - ip: bool, - ip6: bool, -} - -#[derive(Clone, Copy)] -enum NftFamily { - Inet, - Ip, - Ip6, -} - -struct NftApplyPlan<'a> { - family: NftFamily, - v4_targets: &'a [SynLimitTarget], - v6_targets: &'a [SynLimitTarget], -} - -impl SynLimitTargets { - 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() - } - - fn has_iptables_targets(&self) -> bool { - !self.iptables_v4.is_empty() || !self.iptables_v6.is_empty() - } - - fn has_nft_targets(&self) -> bool { - !self.nft_v4.is_empty() || !self.nft_v6.is_empty() - } -} -impl NftFamily { - fn as_str(self) -> &'static str { - match self { - Self::Inet => "inet", - Self::Ip => "ip", - Self::Ip6 => "ip6", - } - } -} - -pub(crate) fn spawn_synlimit_controller(config_rx: watch::Receiver>) { - if !cfg!(target_os = "linux") { - if has_synlimit_config(&config_rx.borrow()) { - warn!("SYN limiter is configured but unsupported on this OS; skipping netfilter rules"); - } - return; - } - - tokio::spawn(async move { - wait_for_config_channel_close_and_reconcile(config_rx).await; - if let Err(error) = clear_synlimit_rules_all_backends().await { - warn!(error = %error, "Failed to clear SYN limiter rules after config channel close"); - } - }); -} - -async fn wait_for_config_channel_close_and_reconcile( - mut config_rx: watch::Receiver>, -) { - while config_rx.changed().await.is_ok() { - let cfg = config_rx.borrow_and_update().clone(); - reconcile_synlimit_rules(&cfg).await; - } -} - -pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) { - if let Err(error) = clear_synlimit_rules_all_backends().await { - warn!(error = %error, "Failed to clear existing SYN limiter rules before reconcile"); - } - - 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() - && let Err(error) = apply_iptables_synlimit_rules(&targets).await - { - warn!(error = %error, "Failed to apply iptables SYN limiter rules"); - } - if targets.has_nft_targets() - && let Err(error) = apply_nft_synlimit_rules(&targets).await - { - warn!(error = %error, "Failed to apply nftables SYN limiter rules"); - } -} - -pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<(), String> { - if !has_cap_net_admin() { - return Ok(()); - } - - let mut errors = Vec::new(); - if let Err(error) = clear_nft_synlimit_rules_all_families().await { - errors.push(error); - } - if let Err(error) = clear_iptables_synlimit_rules_for_binary("iptables").await { - errors.push(error); - } - if let Err(error) = clear_iptables_synlimit_rules_for_binary("ip6tables").await { - errors.push(error); - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors.join("; ")) - } -} - -fn has_synlimit_config(cfg: &ProxyConfig) -> bool { - cfg.server - .listeners - .iter() - .any(|listener| !matches!(listener.synlimit, SynLimitMode::Off)) -} - -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 port = listener.port.unwrap_or(cfg.server.port); - let ip = (!listener.ip.is_unspecified()).then_some(listener.ip); - let seconds = listener.synlimit_seconds; - let hitcount = listener.synlimit_hitcount; - let burst = listener.synlimit_burst; - - match (backend, listener.ip.is_ipv4()) { - (SynLimitMode::Iptables, true) => { - iptables_v4.insert((ip, port, seconds, hitcount, burst)); - } - (SynLimitMode::Iptables, false) => { - iptables_v6.insert((ip, port, seconds, hitcount, burst)); - } - (SynLimitMode::Nftables, true) => { - nft_v4.insert((ip, port, seconds, hitcount, burst)); - } - (SynLimitMode::Nftables, false) => { - nft_v6.insert((ip, port, seconds, hitcount, burst)); - } - (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(), - } -} - -async fn apply_iptables_synlimit_rules(targets: &SynLimitTargets) -> Result<(), String> { - apply_iptables_synlimit_rules_for_binary("iptables", &targets.iptables_v4).await?; - apply_iptables_synlimit_rules_for_binary("ip6tables", &targets.iptables_v6).await -} - -async fn apply_iptables_synlimit_rules_for_binary( - binary: &str, - targets: &[SynLimitTarget], -) -> Result<(), String> { - if targets.is_empty() { - return Ok(()); - } - let _ = run_command(binary, &["-t", "filter", "-N", IPTABLES_CHAIN], None).await; - run_command(binary, &["-t", "filter", "-F", IPTABLES_CHAIN], None).await?; - if run_command( - binary, - &["-t", "filter", "-C", "INPUT", "-j", IPTABLES_CHAIN], - None, - ) - .await - .is_err() - { - run_command( - binary, - &["-t", "filter", "-A", "INPUT", "-j", IPTABLES_CHAIN], - None, - ) - .await?; - } - - for (idx, (ip, port, seconds, hitcount, burst)) in targets.iter().enumerate() { - let hashlimit_name = format!("{IPTABLES_HASHLIMIT_NAME}-{idx}"); - let accept_args = iptables_hashlimit_accept_rule_args( - ip, - *port, - *seconds, - *hitcount, - *burst, - &hashlimit_name, - ); - let drop_args = iptables_synlimit_drop_rule_args(ip, *port); - let drop_refs: Vec<&str> = drop_args.iter().map(String::as_str).collect(); - let accept_refs: Vec<&str> = accept_args.iter().map(String::as_str).collect(); - run_command(binary, &accept_refs, None).await?; - run_command(binary, &drop_refs, None).await?; - } - run_command( - binary, - &["-t", "filter", "-A", IPTABLES_CHAIN, "-j", "RETURN"], - None, - ) - .await?; - - Ok(()) -} - -fn iptables_hashlimit_accept_rule_args( - ip: &Option, - port: u16, - seconds: u32, - hitcount: u32, - burst: u32, - hashlimit_name: &str, -) -> Vec { - let mut args = vec![ - "-t".to_string(), - "filter".to_string(), - "-A".to_string(), - IPTABLES_CHAIN.to_string(), - "-p".to_string(), - "tcp".to_string(), - "--syn".to_string(), - ]; - if let Some(ip) = ip { - args.push("-d".to_string()); - args.push(ip.to_string()); - } - let rate = synlimit_rate_arg(seconds, hitcount); - args.extend([ - "--dport".to_string(), - port.to_string(), - "-m".to_string(), - "hashlimit".to_string(), - "--hashlimit-name".to_string(), - hashlimit_name.to_string(), - "--hashlimit-mode".to_string(), - "srcip".to_string(), - "--hashlimit-upto".to_string(), - rate, - "--hashlimit-burst".to_string(), - burst.to_string(), - "--hashlimit-htable-expire".to_string(), - "15000".to_string(), - "-j".to_string(), - "ACCEPT".to_string(), - ]); - args -} - -fn iptables_synlimit_drop_rule_args(ip: &Option, port: u16) -> Vec { - let mut args = vec![ - "-t".to_string(), - "filter".to_string(), - "-A".to_string(), - IPTABLES_CHAIN.to_string(), - "-p".to_string(), - "tcp".to_string(), - "--syn".to_string(), - ]; - if let Some(ip) = ip { - args.push("-d".to_string()); - args.push(ip.to_string()); - } - args.extend([ - "--dport".to_string(), - port.to_string(), - "-j".to_string(), - "DROP".to_string(), - ]); - args -} - -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)) -} - -async fn clear_iptables_synlimit_rules_for_binary(binary: &str) -> Result<(), String> { - let mut errors = Vec::new(); - for _ in 0..8 { - match run_command( - binary, - &["-t", "filter", "-D", "INPUT", "-j", IPTABLES_CHAIN], - None, - ) - .await - { - Ok(()) => {} - Err(error) if is_missing_command_or_iptables_rule(&error) => break, - Err(error) => { - errors.push(format!("{binary} delete INPUT jump failed: {error}")); - break; - } - } - } - if let Err(error) = run_command(binary, &["-t", "filter", "-F", IPTABLES_CHAIN], None).await - && !is_missing_command_or_iptables_rule(&error) - { - errors.push(format!("{binary} flush chain failed: {error}")); - } - if let Err(error) = run_command(binary, &["-t", "filter", "-X", IPTABLES_CHAIN], None).await - && !is_missing_command_or_iptables_rule(&error) - { - errors.push(format!("{binary} delete chain failed: {error}")); - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors.join(", ")) - } -} - -async fn apply_nft_synlimit_rules(targets: &SynLimitTargets) -> Result<(), String> { - let families = detect_nft_table_families().await; - for plan in nft_apply_plan(families, &targets.nft_v4, &targets.nft_v6) { - let script = nft_synlimit_script(plan); - run_command("nft", &["-f", "-"], Some(script)).await?; - } - - Ok(()) -} - -async fn detect_nft_table_families() -> NftTableFamilies { - let Ok(output) = run_command_stdout("nft", &["list", "tables"]).await else { - return NftTableFamilies { - inet: false, - ip: false, - ip6: false, - }; - }; - - let mut families = NftTableFamilies { - inet: false, - ip: false, - ip6: false, - }; - for line in output.lines() { - let mut fields = line.split_whitespace(); - if fields.next() != Some("table") { - continue; - } - match fields.next() { - Some("inet") => families.inet = true, - Some("ip") => families.ip = true, - Some("ip6") => families.ip6 = true, - _ => {} - } - } - families -} -fn nft_apply_plan<'a>( - families: NftTableFamilies, - v4_targets: &'a [SynLimitTarget], - v6_targets: &'a [SynLimitTarget], -) -> Vec> { - if !v4_targets.is_empty() && !v6_targets.is_empty() { - return vec![NftApplyPlan { - family: NftFamily::Inet, - v4_targets, - v6_targets, - }]; - } - if !v4_targets.is_empty() { - return vec![NftApplyPlan { - family: if families.inet || !families.ip { - NftFamily::Inet - } else { - NftFamily::Ip - }, - v4_targets, - v6_targets: &[], - }]; - } - if !v6_targets.is_empty() { - return vec![NftApplyPlan { - family: if families.inet || !families.ip6 { - NftFamily::Inet - } else { - NftFamily::Ip6 - }, - v4_targets: &[], - v6_targets, - }]; - } - Vec::new() -} -fn nft_synlimit_script(plan: NftApplyPlan<'_>) -> String { - let mut script = String::new(); - script.push_str(&format!("table {} {NFT_TABLE} {{\n", plan.family.as_str())); - script.push_str(&format!(" chain {NFT_CHAIN} {{\n")); - script.push_str(" type filter hook input priority filter; policy accept;\n"); - for (idx, (ip, port, seconds, hitcount, burst)) in plan.v4_targets.iter().enumerate() { - let daddr = ip - .map(|ip| format!(" ip daddr {ip}")) - .unwrap_or_else(String::new); - let rate = synlimit_rate_arg(*seconds, *hitcount); - script.push_str(&format!( - " tcp flags & (fin|syn|rst|ack) == syn{daddr} tcp dport {port} meter telemt_synlimit_v4_{idx} {{ ip saddr limit rate over {rate} burst {burst} packets }} drop\n" - )); - script.push_str(&format!( - " tcp flags & (fin|syn|rst|ack) == syn{daddr} tcp dport {port} accept\n" - )); - } - for (idx, (ip, port, seconds, hitcount, burst)) in plan.v6_targets.iter().enumerate() { - let daddr = ip - .map(|ip| format!(" ip6 daddr {ip}")) - .unwrap_or_else(String::new); - let rate = synlimit_rate_arg(*seconds, *hitcount); - script.push_str(&format!( - " tcp flags & (fin|syn|rst|ack) == syn{daddr} tcp dport {port} meter telemt_synlimit_v6_{idx} {{ ip6 saddr limit rate over {rate} burst {burst} packets }} drop\n" - )); - script.push_str(&format!( - " tcp flags & (fin|syn|rst|ack) == syn{daddr} tcp dport {port} accept\n" - )); - } - script.push_str(" }\n"); - script.push_str("}\n"); - script -} - -async fn clear_nft_synlimit_rules_all_families() -> Result<(), String> { - let mut errors = Vec::new(); - for family in [NftFamily::Inet, NftFamily::Ip, NftFamily::Ip6] { - if let Err(error) = run_command( - "nft", - &["delete", "table", family.as_str(), NFT_TABLE], - None, - ) - .await - && !is_missing_command_or_nft_table(&error) - { - errors.push(format!( - "nft delete table {} {NFT_TABLE} failed: {error}", - family.as_str() - )); - } - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors.join(", ")) - } -} - -fn is_missing_command_or_iptables_rule(error: &str) -> bool { - error.contains("is not available") - || error.contains("No chain/target/match by that name") - || error.contains("does not exist") -} - -fn is_missing_command_or_nft_table(error: &str) -> bool { - error.contains("is not available") || error.contains("No such file or directory") -} - -async fn run_command(binary: &str, args: &[&str], stdin: Option) -> Result<(), String> { - let Some(command_path) = resolve_command(binary) else { - return Err(format!("{binary} is not available")); - }; - let mut command = Command::new(command_path); - command.args(args); - if stdin.is_some() { - command.stdin(std::process::Stdio::piped()); - } - command.stdout(std::process::Stdio::null()); - command.stderr(std::process::Stdio::piped()); - let mut child = command - .spawn() - .map_err(|e| format!("spawn {binary} failed: {e}"))?; - if let Some(blob) = stdin - && let Some(mut writer) = child.stdin.take() - { - writer - .write_all(blob.as_bytes()) - .await - .map_err(|e| format!("stdin write {binary} failed: {e}"))?; - } - let output = child - .wait_with_output() - .await - .map_err(|e| format!("wait {binary} failed: {e}"))?; - if output.status.success() { - return Ok(()); - } - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(if stderr.is_empty() { - format!("{binary} exited with status {}", output.status) - } else { - stderr - }) -} - -async fn run_command_stdout(binary: &str, args: &[&str]) -> Result { - let Some(command_path) = resolve_command(binary) else { - return Err(format!("{binary} is not available")); - }; - let output = Command::new(command_path) - .args(args) - .output() - .await - .map_err(|e| format!("wait {binary} failed: {e}"))?; - if output.status.success() { - return Ok(String::from_utf8_lossy(&output.stdout).to_string()); - } - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(if stderr.is_empty() { - format!("{binary} exited with status {}", output.status) - } else { - stderr - }) -} - -fn resolve_command(binary: &str) -> Option { - let mut dirs = std::env::var_os("PATH") - .map(|path| std::env::split_paths(&path).collect::>()) - .unwrap_or_default(); - dirs.extend(["/usr/sbin", "/sbin", "/usr/bin", "/bin"].map(PathBuf::from)); - dirs.into_iter() - .map(|dir| dir.join(binary)) - .find(|candidate| candidate.exists() && candidate.is_file()) -} - -fn has_cap_net_admin() -> bool { - #[cfg(target_os = "linux")] - { - let Ok(status) = std::fs::read_to_string("/proc/self/status") else { - return false; - }; - for line in status.lines() { - if let Some(raw) = line.strip_prefix("CapEff:") { - let caps = raw.trim(); - if let Ok(bits) = u64::from_str_radix(caps, 16) { - const CAP_NET_ADMIN_BIT: u64 = 12; - return (bits & (1u64 << CAP_NET_ADMIN_BIT)) != 0; - } - } - } - false - } - #[cfg(not(target_os = "linux"))] - { - false - } -}