mirror of
https://github.com/telemt/telemt.git
synced 2026-07-05 17:21:10 +03:00
Delete synlimit_control.rs
This commit is contained in:
@@ -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<IpAddr>, u16, u32, u32, u32);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct SynLimitTargets {
|
|
||||||
iptables_v4: Vec<SynLimitTarget>,
|
|
||||||
iptables_v6: Vec<SynLimitTarget>,
|
|
||||||
nft_v4: Vec<SynLimitTarget>,
|
|
||||||
nft_v6: Vec<SynLimitTarget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Arc<ProxyConfig>>) {
|
|
||||||
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<Arc<ProxyConfig>>,
|
|
||||||
) {
|
|
||||||
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<IpAddr>,
|
|
||||||
port: u16,
|
|
||||||
seconds: u32,
|
|
||||||
hitcount: u32,
|
|
||||||
burst: u32,
|
|
||||||
hashlimit_name: &str,
|
|
||||||
) -> Vec<String> {
|
|
||||||
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<IpAddr>, port: u16) -> Vec<String> {
|
|
||||||
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<NftApplyPlan<'a>> {
|
|
||||||
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<String>) -> 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<String, String> {
|
|
||||||
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<PathBuf> {
|
|
||||||
let mut dirs = std::env::var_os("PATH")
|
|
||||||
.map(|path| std::env::split_paths(&path).collect::<Vec<_>>())
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user