From b18c01d7eba03a0a333e8a30663a41d35cb8f001 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Tue, 6 Apr 2021 18:20:14 +0200 Subject: [PATCH] Add ability to provide PostUp and PreDown scripts --- Cargo.toml | 2 +- USERGUIDE.md | 12 ++++++++++ src/args.rs | 10 +++++++++ src/exec.rs | 59 +++++++++++++++++++++++++++++++++++++++++-------- src/netns.rs | 18 +++++++++++++++ src/util/mod.rs | 10 ++++++++- 6 files changed, 100 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf59f4a..351c083 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vopono" description = "Launch applications via VPN tunnels using temporary network namespaces" -version = "0.6.10" +version = "0.7.0" authors = ["James McMurray "] edition = "2018" license = "GPL-3.0-or-later" diff --git a/USERGUIDE.md b/USERGUIDE.md index 448d922..7016529 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -31,6 +31,9 @@ firewall = "NfTables" provider = "Mullvad" protocol = "Wireguard" server = "usa-us22" +postup = "/home/archie/postup.sh" +predown = "/home/archie/predown.sh" +user = "archie" # custom_config = "/home/user/vpn/mycustomconfig.ovpn" ``` @@ -38,6 +41,15 @@ Note that the values are case-sensitive. If you use a custom config file then you should not set the provider or server (setting the protocol is also optional). +### Host scripts + +Host scripts to run just after a network namespace is created and just before it is destroyed, +can be provided with the `postup` and `predown` arguments (or in the `config.toml`). + +Note these scripts run on the host (outside the network namespace), using the current working directory, +and with the same user as the final application itself (which can be set +with the `user` argument or config file entry). + ### Wireguard Install vopono and use `vopono sync` to diff --git a/src/args.rs b/src/args.rs index 2d9ebc7..aed58f9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -116,6 +116,16 @@ pub struct ExecCommand { /// Block all IPv6 traffic #[structopt(long = "disable-ipv6")] pub disable_ipv6: bool, + + /// Path or alias to executable PostUp script or binary for commands to run on the host after + /// bringing up the namespace + #[structopt(long = "postup")] + pub postup: Option, + + /// Path or alias to executable PreDown script or binary for commands to run on the host after + /// before shutting down the namespace + #[structopt(long = "predown")] + pub predown: Option, } #[derive(StructOpt)] diff --git a/src/exec.rs b/src/exec.rs index 26a0690..820d25c 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -26,6 +26,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { let server_name: String; let protocol: Protocol; + // TODO: Refactor this part - DRY // Create empty config file if does not exist let config_path = vopono_dir()?.join("config.toml"); { @@ -61,6 +62,42 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { .ok() }); + // Assign postup script from args or vopono config file + let postup = command.postup.clone().or_else(|| { + vopono_config_settings + .get("postup") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + }); + + // Assign predown script from args or vopono config file + let predown = command.predown.clone().or_else(|| { + vopono_config_settings + .get("predown") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + }); + + // User for application command, if None will use root + let user = if command.user.is_none() { + vopono_config_settings + .get("user") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + .or_else(|| std::env::var("SUDO_USER").ok()) + } else { + command.user + }; + // Assign protocol and server from args or vopono config file or custom config if used if let Some(path) = &custom_config { protocol = command @@ -81,7 +118,6 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { ); } else { // Get server and provider - // TODO: Handle default case and remove expect() provider = command .vpn_provider .or_else(|| { @@ -128,7 +164,6 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { }) .unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); } - // TODO: PostUp and PreDown scripts if provider != VpnProvider::Custom { // Check config files exist for provider @@ -196,6 +231,8 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { provider.clone(), protocol.clone(), firewall, + predown, + user.clone(), )?; let target_subnet = get_target_subnet()?; ns.add_loopback()?; @@ -309,17 +346,21 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> { )?; } } + + // Run PostUp script (if any) + if let Some(pucmd) = postup { + if user.is_some() { + std::process::Command::new("sudo") + .args(&["-Eu", user.as_ref().unwrap(), &pucmd]) + .spawn()?; + } else { + std::process::Command::new(&pucmd).spawn()?; + } + } } let ns = ns.write_lockfile(&command.application)?; - // User for application command, if None will use root - let user = if command.user.is_none() { - std::env::var("SUDO_USER").ok() - } else { - command.user - }; - let application = ApplicationWrapper::new(&ns, &command.application, user)?; // Launch TCP proxy server on other threads if forwarding ports diff --git a/src/netns.rs b/src/netns.rs index 8d34624..30e8a4f 100644 --- a/src/netns.rs +++ b/src/netns.rs @@ -36,6 +36,8 @@ pub struct NetworkNamespace { pub provider: VpnProvider, pub protocol: Protocol, pub firewall: Firewall, + pub predown: Option, + pub predown_user: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -67,6 +69,8 @@ impl NetworkNamespace { provider: VpnProvider, protocol: Protocol, firewall: Firewall, + predown: Option, + predown_user: Option, ) -> anyhow::Result { sudo_command(&["ip", "netns", "add", name.as_str()]) .with_context(|| format!("Failed to create network namespace: {}", &name))?; @@ -86,6 +90,8 @@ impl NetworkNamespace { provider, protocol, firewall, + predown, + predown_user, }) } @@ -388,6 +394,18 @@ impl Drop for NetworkNamespace { } } info!("Shutting down vopono namespace - as there are no processes left running inside"); + // Run PreDown script (if any) + if let Some(pdcmd) = self.predown.as_ref() { + if self.predown_user.is_some() { + std::process::Command::new("sudo") + .args(&["-Eu", self.predown_user.as_ref().unwrap(), &pdcmd]) + .spawn() + .ok(); + } else { + std::process::Command::new(&pdcmd).spawn().ok(); + } + } + self.openvpn = None; self.veth_pair = None; self.dns_config = None; diff --git a/src/util/mod.rs b/src/util/mod.rs index 7c8f3b8..2cd47d0 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -328,7 +328,15 @@ pub fn get_configs_from_alias(list_path: &Path, alias: &str) -> Vec { .to_string(), ) }) - .filter(|x| x.2.starts_with(alias) || (x.1.starts_with(alias))) + .filter(|x| { + x.2.starts_with(alias) + || (x.1.starts_with(alias)) + || x.0 + .file_name() + .to_str() + .expect("No filename") + .starts_with(alias) + }) .map(|x| PathBuf::from(x.0.path())) .collect::>() }