diff --git a/src/args.rs b/src/args.rs index 525c5b8..e457c20 100644 --- a/src/args.rs +++ b/src/args.rs @@ -135,6 +135,14 @@ pub struct ExecCommand { #[clap(long = "user", short = 'u')] pub user: Option, + /// Group with which to run the application + #[clap(long = "group", short = 'g')] + pub group: Option, + + /// Working directory in which to run the application (default is current working directory) + #[clap(long = "working-directory", short = 'w')] + pub working_directory: Option, + /// Custom VPN Provider - OpenVPN or Wireguard config file (will override other settings) #[clap(parse(from_os_str), long = "custom")] pub custom_config: Option, diff --git a/src/exec.rs b/src/exec.rs index c0a157d..0205d5a 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, bail}; use log::{debug, error, info, warn}; use signal_hook::{consts::SIGINT, iterator::Signals}; use std::net::{IpAddr, Ipv4Addr}; +use std::path::PathBuf; use std::str::FromStr; use std::{ fs::create_dir_all, @@ -139,6 +140,32 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> command.user }; + // Group for application command + let group = if command.group.is_none() { + vopono_config_settings + .get("group") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + } else { + command.group + }; + + // Working directory for application command + let working_directory = if command.working_directory.is_none() { + vopono_config_settings + .get("working-directory") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + } else { + command.working_directory + }; + // Assign DNS server from args or vopono config file let base_dns = command.dns.clone().or_else(|| { vopono_config_settings @@ -328,6 +355,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> firewall, predown, user.clone(), + group.clone(), )?; let target_subnet = get_target_subnet()?; ns.add_loopback()?; @@ -482,13 +510,27 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // Temporarily set env var referring to this network namespace name if let Some(pucmd) = postup { std::env::set_var("VOPONO_NS", &ns.name); - if user.is_some() { - std::process::Command::new("sudo") - .args(&["-Eu", user.as_ref().unwrap(), &pucmd]) - .spawn()?; + + let mut sudo_args = Vec::new(); + if let Some(ref user) = user { + sudo_args.push("--user"); + sudo_args.push(user); + } + if let Some(ref group) = group { + sudo_args.push("--group"); + sudo_args.push(group); + } + + if !sudo_args.is_empty() { + let mut args = vec!["--preserve-env"]; + args.append(&mut sudo_args); + args.push(&pucmd); + + std::process::Command::new("sudo").args(args).spawn()?; } else { std::process::Command::new(&pucmd).spawn()?; - } + }; + std::env::remove_var("VOPONO_NS"); } } @@ -501,7 +543,13 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let ns = ns.write_lockfile(&command.application)?; - let application = ApplicationWrapper::new(&ns, &command.application, user)?; + let application = ApplicationWrapper::new( + &ns, + &command.application, + user, + group, + working_directory.map(PathBuf::from), + )?; // Launch TCP proxy server on other threads if forwarding ports // TODO: Fix when running as root diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index 8398dba..af436af 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use super::netns::NetworkNamespace; use crate::util::get_all_running_process_names; use log::warn; @@ -11,6 +13,8 @@ impl ApplicationWrapper { netns: &NetworkNamespace, application: &str, user: Option, + group: Option, + working_directory: Option, ) -> anyhow::Result { let running_processes = get_all_running_process_names(); let app_vec = application.split_whitespace().collect::>(); @@ -33,8 +37,15 @@ impl ApplicationWrapper { } } - // TODO: Could allow user to set custom working directory here - let handle = netns.exec_no_block(app_vec.as_slice(), user, false, false, false, None)?; + let handle = netns.exec_no_block( + app_vec.as_slice(), + user, + group, + false, + false, + false, + working_directory, + )?; Ok(Self { handle }) } diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index cd25700..5394faa 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -41,6 +41,7 @@ pub struct NetworkNamespace { pub firewall: Firewall, pub predown: Option, pub predown_user: Option, + pub predown_group: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -74,6 +75,7 @@ impl NetworkNamespace { firewall: Firewall, predown: Option, predown_user: Option, + predown_group: Option, ) -> anyhow::Result { sudo_command(&["ip", "netns", "add", name.as_str()]) .with_context(|| format!("Failed to create network namespace: {}", &name))?; @@ -96,13 +98,16 @@ impl NetworkNamespace { firewall, predown, predown_user, + predown_group, }) } + #[allow(clippy::too_many_arguments)] pub fn exec_no_block( &self, command: &[&str], user: Option, + group: Option, silent: bool, capture_output: bool, capture_input: bool, @@ -113,12 +118,26 @@ impl NetworkNamespace { if let Some(cdir) = set_dir { handle.current_dir(cdir); } - let sudo_string = if user.is_some() { - handle.args(&["sudo", "-Eu", user.as_ref().unwrap()]); - Some(format!(" sudo -Eu {}", user.as_ref().unwrap())) + + let mut sudo_args = Vec::new(); + if let Some(ref user) = user { + sudo_args.push("--user"); + sudo_args.push(user); + } + if let Some(ref group) = group { + sudo_args.push("--group"); + sudo_args.push(group); + } + + let sudo_string = if !sudo_args.is_empty() { + let mut args = vec!["sudo", "--preserve-env"]; + args.append(&mut sudo_args); + handle.args(args.clone()); + Some(format!(" {}", args.join(" "))) } else { None }; + if silent { handle.stdout(Stdio::null()); handle.stderr(Stdio::null()); @@ -142,7 +161,7 @@ impl NetworkNamespace { } pub fn exec(&self, command: &[&str]) -> anyhow::Result<()> { - self.exec_no_block(command, None, false, false, false, None)? + self.exec_no_block(command, None, None, false, false, false, None)? .wait()?; Ok(()) } @@ -505,14 +524,27 @@ impl Drop for NetworkNamespace { .namespace_ip .to_string(), ); - if self.predown_user.is_some() { - std::process::Command::new("sudo") - .args(&["-Eu", self.predown_user.as_ref().unwrap(), pdcmd]) - .spawn() - .ok(); + + let mut sudo_args = Vec::new(); + if let Some(ref predown_user) = self.predown_user { + sudo_args.push("--user"); + sudo_args.push(predown_user); + } + if let Some(ref predown_group) = self.predown_group { + sudo_args.push("--group"); + sudo_args.push(predown_group); + } + + if !sudo_args.is_empty() { + let mut args = vec!["--preserve-env"]; + args.append(&mut sudo_args); + args.push(pdcmd); + + std::process::Command::new("sudo").args(args).spawn().ok(); } else { std::process::Command::new(&pdcmd).spawn().ok(); } + std::env::remove_var("VOPONO_NS"); std::env::remove_var("VOPONO_NS_IP"); } diff --git a/vopono_core/src/network/openconnect.rs b/vopono_core/src/network/openconnect.rs index f7c9572..7fe8d51 100644 --- a/vopono_core/src/network/openconnect.rs +++ b/vopono_core/src/network/openconnect.rs @@ -50,7 +50,7 @@ impl OpenConnect { } let handle = netns - .exec_no_block(&command_vec, None, false, false, true, None) + .exec_no_block(&command_vec, None, None, false, false, true, None) .context("Failed to launch OpenConnect - is openconnect installed?")?; handle diff --git a/vopono_core/src/network/openfortivpn.rs b/vopono_core/src/network/openfortivpn.rs index 1458716..e5d0e96 100644 --- a/vopono_core/src/network/openfortivpn.rs +++ b/vopono_core/src/network/openfortivpn.rs @@ -48,7 +48,7 @@ impl OpenFortiVpn { // TODO - better handle forwarding output when blocking on password entry (no newline!) let mut handle = netns - .exec_no_block(&command_vec, None, false, true, false, None) + .exec_no_block(&command_vec, None, None, false, true, false, None) .context("Failed to launch OpenFortiVPN - is openfortivpn installed?")?; let stdout = handle.stdout.take().unwrap(); let id = handle.id(); diff --git a/vopono_core/src/network/openvpn.rs b/vopono_core/src/network/openvpn.rs index 604070a..79e1fe3 100644 --- a/vopono_core/src/network/openvpn.rs +++ b/vopono_core/src/network/openvpn.rs @@ -98,7 +98,15 @@ impl OpenVpn { let working_dir = PathBuf::from(config_file_path.parent().unwrap()); let handle = netns - .exec_no_block(&command_vec, None, true, false, false, Some(working_dir)) + .exec_no_block( + &command_vec, + None, + None, + true, + false, + false, + Some(working_dir), + ) .context("Failed to launch OpenVPN - is openvpn installed?")?; let id = handle.id(); let mut buffer = String::with_capacity(16384); diff --git a/vopono_core/src/network/shadowsocks.rs b/vopono_core/src/network/shadowsocks.rs index 420e49e..fe1cc0b 100644 --- a/vopono_core/src/network/shadowsocks.rs +++ b/vopono_core/src/network/shadowsocks.rs @@ -67,7 +67,7 @@ impl Shadowsocks { ]; let handle = netns - .exec_no_block(&command_vec, None, true, false, false, None) + .exec_no_block(&command_vec, None, None, true, false, false, None) .context("Failed to launch Shadowsocks - is shadowsocks-libev installed?")?; Ok(Self { pid: handle.id() })