From 942882f9dedd719a5dcb3d3ef761bfbd8b9f3904 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:29:23 +0300 Subject: [PATCH] SYN Limiter interval and hitcount in Config Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/config/defaults.rs | 10 +++++ src/config/load.rs | 16 ++++++++ src/config/types.rs | 6 +++ src/synlimit_control.rs | 82 +++++++++++------------------------------ 4 files changed, 53 insertions(+), 61 deletions(-) diff --git a/src/config/defaults.rs b/src/config/defaults.rs index da9e472..b2079b7 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -54,6 +54,8 @@ const DEFAULT_CONNTRACK_CONTROL_ENABLED: bool = true; const DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT: u8 = 85; const DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT: u8 = 70; const DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC: u64 = 4096; +const DEFAULT_SYNLIMIT_SECONDS: u32 = 1; +const DEFAULT_SYNLIMIT_HITCOUNT: u32 = 1; const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2; const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5; const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000; @@ -243,6 +245,14 @@ pub(crate) fn default_conntrack_delete_budget_per_sec() -> u64 { DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC } +pub(crate) fn default_synlimit_seconds() -> u32 { + DEFAULT_SYNLIMIT_SECONDS +} + +pub(crate) fn default_synlimit_hitcount() -> u32 { + DEFAULT_SYNLIMIT_HITCOUNT +} + pub(crate) fn default_prefer_4() -> u8 { 4 } diff --git a/src/config/load.rs b/src/config/load.rs index 85d830a..bd72fbf 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -347,6 +347,8 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[ "port", "client_mss", "synlimit", + "synlimit_seconds", + "synlimit_hitcount", "announce", "announce_ip", "proxy_protocol", @@ -1949,6 +1951,16 @@ impl ProxyConfig { ProxyError::Config(format!("server.listeners[{idx}].client_mss {error}")) })?; } + if listener.synlimit_seconds == 0 { + return Err(ProxyError::Config(format!( + "server.listeners[{idx}].synlimit_seconds must be > 0" + ))); + } + if listener.synlimit_hitcount == 0 { + return Err(ProxyError::Config(format!( + "server.listeners[{idx}].synlimit_hitcount must be > 0" + ))); + } } if config.server.accept_permit_timeout_ms > 60_000 { @@ -2188,6 +2200,8 @@ impl ProxyConfig { port: Some(config.server.port), client_mss: None, synlimit: SynLimitMode::default(), + synlimit_seconds: default_synlimit_seconds(), + synlimit_hitcount: default_synlimit_hitcount(), announce: None, announce_ip: None, proxy_protocol: None, @@ -2202,6 +2216,8 @@ impl ProxyConfig { port: Some(config.server.port), client_mss: None, synlimit: SynLimitMode::default(), + synlimit_seconds: default_synlimit_seconds(), + synlimit_hitcount: default_synlimit_hitcount(), announce: None, announce_ip: None, proxy_protocol: None, diff --git a/src/config/types.rs b/src/config/types.rs index 27a5e71..cc443bf 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -2176,6 +2176,12 @@ pub struct ListenerConfig { /// Per-listener SYN limiter mode. #[serde(default)] pub synlimit: SynLimitMode, + /// Iptables recent-match interval for the per-listener SYN limiter. + #[serde(default = "default_synlimit_seconds")] + pub synlimit_seconds: u32, + /// Iptables recent-match hit count for the per-listener SYN limiter. + #[serde(default = "default_synlimit_hitcount")] + pub synlimit_hitcount: u32, /// IP address or hostname to announce in proxy links. /// Takes precedence over `announce_ip` if both are set. #[serde(default)] diff --git a/src/synlimit_control.rs b/src/synlimit_control.rs index 2a393c1..f6fb0e2 100644 --- a/src/synlimit_control.rs +++ b/src/synlimit_control.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use tokio::io::AsyncWriteExt; use tokio::process::Command; use tokio::sync::watch; -use tracing::{debug, warn}; +use tracing::warn; use crate::config::{ProxyConfig, SynLimitMode}; @@ -19,8 +19,8 @@ const NFT_SET_V6: &str = "telemt_synlimit_v6"; #[derive(Default)] struct SynLimitTargets { - iptables_v4: Vec<(Option, u16)>, - iptables_v6: Vec<(Option, u16)>, + iptables_v4: Vec<(Option, u16, u32, u32)>, + iptables_v6: Vec<(Option, u16, u32, u32)>, nft_v4: Vec<(Option, u16)>, nft_v6: Vec<(Option, u16)>, } @@ -45,14 +45,6 @@ struct NftApplyPlan<'a> { v6_targets: &'a [(Option, u16)], } -struct SynLimitRuleGuard; - -impl Drop for SynLimitRuleGuard { - fn drop(&mut self) { - clear_synlimit_rules_all_backends_sync(); - } -} - impl SynLimitTargets { fn is_empty(&self) -> bool { self.iptables_v4.is_empty() @@ -89,7 +81,6 @@ pub(crate) fn spawn_synlimit_controller(config_rx: watch::Receiver SynLimitTargets { } 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; match (backend, listener.ip.is_ipv4()) { (SynLimitMode::Iptables, true) => { - iptables_v4.insert((ip, port)); + iptables_v4.insert((ip, port, seconds, hitcount)); } (SynLimitMode::Iptables, false) => { - iptables_v6.insert((ip, port)); + iptables_v6.insert((ip, port, seconds, hitcount)); } (SynLimitMode::Nftables, true) => { nft_v4.insert((ip, port)); @@ -186,7 +179,7 @@ async fn apply_iptables_synlimit_rules(targets: &SynLimitTargets) -> Result<(), async fn apply_iptables_synlimit_rules_for_binary( binary: &str, - targets: &[(Option, u16)], + targets: &[(Option, u16, u32, u32)], ) -> Result<(), String> { if targets.is_empty() { return Ok(()); @@ -213,9 +206,11 @@ async fn apply_iptables_synlimit_rules_for_binary( .await?; } - for (ip, port) in targets { - let drop_args = iptables_synlimit_rule_args(ip, *port, "--rcheck", "DROP"); - let accept_args = iptables_synlimit_rule_args(ip, *port, "--set", "ACCEPT"); + for (ip, port, seconds, hitcount) in targets { + let drop_args = + iptables_synlimit_rule_args(ip, *port, *seconds, *hitcount, "--rcheck", "DROP"); + let accept_args = + iptables_synlimit_rule_args(ip, *port, *seconds, *hitcount, "--set", "ACCEPT"); 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, &drop_refs, None).await?; @@ -228,6 +223,8 @@ async fn apply_iptables_synlimit_rules_for_binary( fn iptables_synlimit_rule_args( ip: &Option, port: u16, + seconds: u32, + hitcount: u32, recent_op: &str, verdict: &str, ) -> Vec { @@ -254,7 +251,12 @@ fn iptables_synlimit_rule_args( recent_op.to_string(), ]); if recent_op == "--rcheck" { - args.extend(["--seconds".to_string(), "1".to_string()]); + args.extend([ + "--seconds".to_string(), + seconds.to_string(), + "--hitcount".to_string(), + hitcount.to_string(), + ]); } args.extend(["-j".to_string(), verdict.to_string()]); args @@ -507,45 +509,3 @@ fn has_cap_net_admin() -> bool { false } } - -fn clear_synlimit_rules_all_backends_sync() { - run_command_sync("nft", &["delete", "table", "inet", NFT_TABLE]); - run_command_sync("nft", &["delete", "table", "ip", NFT_TABLE]); - run_command_sync("nft", &["delete", "table", "ip6", NFT_TABLE]); - clear_iptables_synlimit_rules_for_binary_sync("iptables"); - clear_iptables_synlimit_rules_for_binary_sync("ip6tables"); -} - -fn clear_iptables_synlimit_rules_for_binary_sync(binary: &str) { - if !command_exists(binary) { - return; - } - for _ in 0..8 { - if !run_command_sync( - binary, - &["-t", "filter", "-D", "INPUT", "-j", IPTABLES_CHAIN], - ) { - break; - } - } - run_command_sync(binary, &["-t", "filter", "-F", IPTABLES_CHAIN]); - run_command_sync(binary, &["-t", "filter", "-X", IPTABLES_CHAIN]); -} - -fn run_command_sync(binary: &str, args: &[&str]) -> bool { - if !command_exists(binary) { - return false; - } - match std::process::Command::new(binary) - .args(args) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - { - Ok(status) => status.success(), - Err(error) => { - debug!(binary, error = %error, "SYN limiter cleanup command failed to spawn"); - false - } - } -}