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
+72 -19
View File
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use tokio::sync::watch;
use tracing::warn;
@@ -11,7 +11,9 @@ mod model;
mod nftables;
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>>) {
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) {
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) => {
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()
&& 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");
}
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");
}
}
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() {
return Ok(false);
}
let mut errors = Vec::new();
let mut removed = false;
match nftables::clear_rules_all_families().await {
match nftables::clear_rules_all_families(namespace).await {
Ok(value) => {
removed |= value;
}
@@ -87,7 +114,7 @@ pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String>
errors.push(error);
}
}
match iptables::clear_rules_for_binary("iptables").await {
match iptables::clear_rules_for_binary("iptables", namespace).await {
Ok(value) => {
removed |= value;
}
@@ -95,7 +122,7 @@ pub(crate) async fn clear_synlimit_rules_all_backends() -> Result<bool, String>
errors.push(error);
}
}
match iptables::clear_rules_for_binary("ip6tables").await {
match iptables::clear_rules_for_binary("ip6tables", namespace).await {
Ok(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 {
cfg.server
.listeners