From 555d0be6d9b1493c788df9862ff9b57f1a817dfe Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Mon, 28 Sep 2026 05:48:43 +0300 Subject: [PATCH] Trusted Helper Argv0 for Multi-call Firewall Binaries Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/cli/init.rs | 14 ++++------ src/conntrack_control/firewall/command.rs | 6 ++--- src/synlimit_control/command.rs | 10 +++---- src/util/trusted_command.rs | 33 +++++++++++++++++++++++ 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 43282c3..41e0953 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,9 +1,8 @@ use std::path::{Path, PathBuf}; -use std::process::Command; use rand::RngExt; -use crate::util::trusted_command::resolve_trusted_helper; +use crate::util::trusted_command::trusted_helper_command; /// Options for the fire-and-forget init command. #[derive(Debug, Clone)] @@ -167,11 +166,8 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box> { eprintln!("[+] Service started"); std::thread::sleep(std::time::Duration::from_secs(1)); - let status = resolve_trusted_helper("systemctl").and_then(|command_path| { - Command::new(command_path) - .args(["is-active", "telemt.service"]) - .output() - .ok() + let status = trusted_helper_command("systemctl").and_then(|mut command| { + command.args(["is-active", "telemt.service"]).output().ok() }); match status { Some(out) if out.status.success() => { @@ -334,11 +330,11 @@ weight = 10 } fn run_cmd(cmd: &str, args: &[&str]) { - let Some(command_path) = resolve_trusted_helper(cmd) else { + let Some(mut command) = trusted_helper_command(cmd) else { eprintln!("[!] Refusing unavailable or untrusted command: {}", cmd); return; }; - match Command::new(command_path).args(args).output() { + match command.args(args).output() { Ok(output) => { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/src/conntrack_control/firewall/command.rs b/src/conntrack_control/firewall/command.rs index c9fb7d5..4dc55f9 100644 --- a/src/conntrack_control/firewall/command.rs +++ b/src/conntrack_control/firewall/command.rs @@ -6,7 +6,7 @@ use tokio::io::AsyncWriteExt; use tokio::process::Command; #[cfg(unix)] -use crate::util::trusted_command::resolve_trusted_helper; +use crate::util::trusted_command::{resolve_trusted_helper, trusted_helper_command}; const COMMAND_TIMEOUT: Duration = Duration::from_secs(30); @@ -133,13 +133,13 @@ impl FirewallCommandRunner for SystemCommandRunner { } #[cfg(unix)] { - let Some(command_path) = resolve_trusted_helper(spec.binary) else { + let Some(command) = trusted_helper_command(spec.binary) else { return Err(CommandError { kind: CommandErrorKind::Missing, message: format!("{} is not available", spec.binary), }); }; - let mut command = Command::new(command_path); + let mut command = Command::from(command); command.args(&spec.args); command.env("LC_ALL", "C"); if spec.stdin.is_some() { diff --git a/src/synlimit_control/command.rs b/src/synlimit_control/command.rs index d631d92..86c695a 100644 --- a/src/synlimit_control/command.rs +++ b/src/synlimit_control/command.rs @@ -1,7 +1,7 @@ use tokio::io::AsyncWriteExt; use tokio::process::Command; -use crate::util::trusted_command::resolve_trusted_helper; +use crate::util::trusted_command::trusted_helper_command; const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); @@ -10,10 +10,10 @@ pub(super) async fn run_command( args: &[&str], stdin: Option, ) -> Result<(), String> { - let Some(command_path) = resolve_trusted_helper(binary) else { + let Some(command) = trusted_helper_command(binary) else { return Err(format!("{binary} is not available")); }; - let mut command = Command::new(command_path); + let mut command = Command::from(command); command.args(args); if stdin.is_some() { command.stdin(std::process::Stdio::piped()); @@ -52,10 +52,10 @@ pub(super) async fn run_command( } pub(super) async fn run_command_stdout(binary: &str, args: &[&str]) -> Result { - let Some(command_path) = resolve_trusted_helper(binary) else { + let Some(command) = trusted_helper_command(binary) else { return Err(format!("{binary} is not available")); }; - let mut command = Command::new(command_path); + let mut command = Command::from(command); command.args(args).kill_on_drop(true); let output = tokio::time::timeout(COMMAND_TIMEOUT, command.output()) .await diff --git a/src/util/trusted_command.rs b/src/util/trusted_command.rs index cb40cdd..c52363c 100644 --- a/src/util/trusted_command.rs +++ b/src/util/trusted_command.rs @@ -1,5 +1,7 @@ use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; +use std::process::Command; const TRUSTED_HELPER_DIRS: [&str; 4] = ["/usr/sbin", "/usr/bin", "/sbin", "/bin"]; const TRUSTED_HELPERS: [&str; 12] = [ @@ -29,6 +31,19 @@ pub(crate) fn resolve_trusted_helper(binary: &str) -> Option { .find_map(|candidate| trusted_executable(&candidate)) } +/// Builds a trusted helper command with its allowlisted invocation name. +pub(crate) fn trusted_helper_command(binary: &str) -> Option { + let command_path = resolve_trusted_helper(binary)?; + Some(command_for_resolved_helper(binary, command_path)) +} + +fn command_for_resolved_helper(binary: &str, command_path: PathBuf) -> Command { + let mut command = Command::new(command_path); + // Multi-call helpers dispatch from argv[0], which canonical path resolution discards. + command.arg0(binary); + command +} + fn trusted_executable(candidate: &Path) -> Option { let canonical = std::fs::canonicalize(candidate).ok()?; let metadata = std::fs::metadata(&canonical).ok()?; @@ -78,6 +93,24 @@ mod tests { assert!(!TRUSTED_HELPERS.contains(&"iptables-restore-wrapper")); } + #[test] + fn trusted_multicall_command_preserves_logical_argv0() { + let command_path = std::fs::canonicalize("/bin/sh").unwrap(); + for binary in [ + "iptables", + "ip6tables", + "iptables-restore", + "ip6tables-restore", + ] { + let mut command = command_for_resolved_helper(binary, command_path.clone()); + assert_eq!(command.get_program(), command_path.as_os_str()); + + let output = command.args(["-c", "printf '%s' \"$0\""]).output().unwrap(); + assert!(output.status.success()); + assert_eq!(String::from_utf8(output.stdout).unwrap(), binary); + } + } + #[test] fn writable_executable_is_not_trusted() { let directory = tempfile::tempdir().unwrap();