Fix SYN limiter lifecycle and default burst

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-06-12 14:40:26 +03:00
parent c4954f745f
commit 2675779915
6 changed files with 96 additions and 34 deletions
+75 -15
View File
@@ -80,7 +80,9 @@ pub(crate) fn spawn_synlimit_controller(config_rx: watch::Receiver<Arc<ProxyConf
tokio::spawn(async move {
wait_for_config_channel_close_and_reconcile(config_rx).await;
clear_synlimit_rules_all_backends().await;
if let Err(error) = clear_synlimit_rules_all_backends().await {
warn!(error = %error, "Failed to clear SYN limiter rules after config channel close");
}
});
}
@@ -94,7 +96,9 @@ async fn wait_for_config_channel_close_and_reconcile(
}
pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) {
clear_synlimit_rules_all_backends().await;
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() {
@@ -119,10 +123,23 @@ pub(crate) async fn reconcile_synlimit_rules(cfg: &ProxyConfig) {
}
}
pub(crate) async fn clear_synlimit_rules_all_backends() {
clear_nft_synlimit_rules_all_families().await;
clear_iptables_synlimit_rules_for_binary("iptables").await;
clear_iptables_synlimit_rules_for_binary("ip6tables").await;
pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<(), String> {
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 {
@@ -314,21 +331,40 @@ fn synlimit_rate_arg(seconds: u32, hitcount: u32) -> String {
format!("{}/day", amount.max(1))
}
async fn clear_iptables_synlimit_rules_for_binary(binary: &str) {
async fn clear_iptables_synlimit_rules_for_binary(binary: &str) -> Result<(), String> {
let mut errors = Vec::new();
for _ in 0..8 {
if run_command(
match run_command(
binary,
&["-t", "filter", "-D", "INPUT", "-j", IPTABLES_CHAIN],
None,
)
.await
.is_err()
{
break;
Ok(()) => {}
Err(error) if is_missing_command_or_iptables_rule(&error) => break,
Err(error) => {
errors.push(format!("{binary} delete INPUT jump failed: {error}"));
break;
}
}
}
let _ = run_command(binary, &["-t", "filter", "-F", IPTABLES_CHAIN], None).await;
let _ = run_command(binary, &["-t", "filter", "-X", IPTABLES_CHAIN], None).await;
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> {
@@ -439,15 +475,39 @@ fn nft_synlimit_script(plan: NftApplyPlan<'_>) -> String {
script
}
async fn clear_nft_synlimit_rules_all_families() {
async fn clear_nft_synlimit_rules_all_families() -> Result<(), String> {
let mut errors = Vec::new();
for family in [NftFamily::Inet, NftFamily::Ip, NftFamily::Ip6] {
let _ = run_command(
if let Err(error) = run_command(
"nft",
&["delete", "table", family.as_str(), NFT_TABLE],
None,
)
.await;
.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<String>) -> Result<(), String> {