diff --git a/Cargo.toml b/Cargo.toml index 4cfc540..acf075d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ basic_tcp_proxy = "0.3.2" strum = "0.26" strum_macros = "0.26" shellexpand = { version = "3", features = ["full"] } +shell-words = "1" [package.metadata.rpm] package = "vopono" diff --git a/README.md b/README.md index cb245fd..26d7cef 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ See the [vopono User Guide](USERGUIDE.md) for much more detailed usage instructi Install the `vopono-git` package with your favourite AUR helper. ```bash -$ yay -S vopono-git +$ paru -S vopono-git $ vopono sync ``` diff --git a/USERGUIDE.md b/USERGUIDE.md index 0afe946..ce27bfd 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -61,29 +61,55 @@ 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). -The current network namespace name is provided to the PostUp and PreDown -scripts in the environment variable `$VOPONO_NS`. It is temporarily set -when running these scripts only. +### PostUp and PreDown scripts - run on Host (outside network namespace) -Similarly, the network namespace IP address is provided via `$VOPONO_NS_IP`, -and is available to the PostUp and PreDown scripts, and the application to -run itself. `$VOPONO_NS_IP` is useful if you'd like to configure a server +vopono can run scripts on the host machine (outside the network namespace) with the `--postup` and `--predown` arguments (or set in the `~/.config/vopono/config.toml` file). The postup script will run right after the network namespace is set up (also after provider port forwarding if used), the postdown script will run when tearing down the network namespace after the target application has terminated. + +These scripts run outside of the network namespace so that they can be used to run proxies and DNS services which the network namespace can use. To run extra commands within the network namespace, you can just wrap the target command with your own script (see the Plex example below). + +These scripts run using the current working directory with the same user as the target application itself (which can be set +with the `user` argument or config file entry). + +Arguments can be passed to the scripts from the command line (note the escaped spaces if the script path contains spaces), e.g.: + +```bash +--postup "~/test\ postup.sh argy1 argy2" +``` + +e.g. with a script like: + +```bash +#!/bin/bash +echo "arg1: $1 arg2: $2" +``` + +Just like for the target application execution, the following environment variables are available (but recall that these scripts run *outside* the network namespace): + +- `$VOPONO_HOST_IP` the local IP address of the host machine as seen from inside the network namespace. +- `$VOPONO_NS_IP` the local IP address of the network namespace as seen from the host. +- `$VOPONO_NS` the name of the network namespace. +- `$VOPONO_FORWARDED_PORT` the forwarded port for provider port forwarding (ProtonVPN or PIA) - only when using `--port-forwarding` or `--custom-port-forwarding` + +`$VOPONO_NS_IP` is useful if you'd like to configure a server running within the network namespace to listen on its local IP address only -(see below, for more information on that). +(see below for more information on that). -The application to run within the namespace also has access to -`$VOPONO_HOST_IP`, to get the IP address of the host. +### Host access from within the network namespace -Note: These environment variables are currently only available from within -the application/script to run, not on the command line. So the following -doesn't work: +The host IP address (as seen from inside the network namespace) is provided as the +`$VOPONO_HOST_IP` environment variable to the target application/script itself (and also the host scripts described above). -`vopono exec {other Vopono options} 'echo "HOST IP: $VOPONO_HOST_IP"'` +If `--allow-host-access` is set then the host machine IP (as seen from the network namespace) is also added to the hosts file as `vopono.host` so you can access services on the host machine from within the network namespace e.g. at `http://vopono.host:8080` etc. -Output: `HOST IP: $VOPONO_HOST_IP` (the environ variable wasn't expanded). +If provider port forwarding is enabled (e.g. `--port-forwarding` or `--custom-port-forwarding` with ProtonVPN or PIA) then the forwarded port is provided as `$VOPONO_FORWARDED_PORT`. -A work around is to create a executable script, that executes the -application you'd like to run: +### Running commands before and after execution within the network namespace + +To run extra commands inside the network namespace you can wrap your target application with a bash script and provide that script as the target to vopono. + +Note that the target user *must* have read and executable access to this script! + +For example with a simple script using the environment variables (also see the Plex section below): ```bash #!/bin/bash @@ -101,24 +127,6 @@ Output: => HOST IP: 10.200.1.1 ``` -### 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). - -Script arguments (e.g. `script.sh arg1 arg1`), are currently not possible, resulting in an error: - -``` -$ vopono exec {other Vopono options} --postup 'echo POSTUP' ls -[...] -sudo: echo POSTUP: command not found -[...] -``` - ### Wireguard Install vopono and use `vopono sync` to @@ -126,7 +134,7 @@ create the Wireguard configuration files (and generate a keypair if necessary): ```bash -$ yay -S vopono-git +$ paru -S vopono-bin $ vopono sync ``` @@ -159,7 +167,7 @@ Install vopono and use `vopono sync` to create the OpenVPN configuration files and server lists. ```bash -$ yay -S vopono-git +$ paru -S vopono-bin $ vopono sync ``` @@ -386,6 +394,52 @@ $ vopono -v exec -u jackett "/usr/lib/jackett/jackett --NoRestart --NoUpdates -- You can then access the web UI on the host machine at `http://127.0.0.1:9117/UI/Dashboard`, but all of Jackett's connections will go via the VPN. +#### Plex - port forwarding + +Plex can be run and port forwarded (for Internet accessibility) with the following command and bash script. + +Note this assumes a standard Arch Linux plex-media-server installation (i.e. where Plex uses its own `plex` user). + +In `/home/plex/plex_launch.sh`: +```bash +#!/bin/bash +# Plex config env vars +export LD_LIBRARY_PATH=/usr/lib/plexmediaserver/lib +export PLEX_MEDIA_SERVER_HOME=/usr/lib/plexmediaserver +export PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/var/lib/plex +export PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS=6 +export PLEX_MEDIA_SERVER_TMPDIR=/tmp +export TMPDIR=/tmp + +PLEX_LOCAL_PORT=32400 # Seems this is hardcoded in Plex +# local address will only be accesible if running vopono with -o 32400 +echo "local Plex address: http://$VOPONO_NS_IP:$PLEX_LOCAL_PORT" +# This assume using --port-forwarding or --custom-port-forwarding for ProtonVPN or PIA +echo "remote Plex address: http://$(curl -4s ifconfig.co):$VOPONO_FORWARDED_PORT" +# Here we redirect incoming connections on the forwarded to the port to the local Plex one (since we cannot change it! unlike transmission-gtk etc.) +# Run this in background as it would block +socat tcp-l:"$VOPONO_FORWARDED_PORT",fork,reuseaddr tcp:"$VOPONO_NS_IP":"$PLEX_LOCAL_PORT" & + +/usr/lib/plexmediaserver/Plex\ Media\ Server +# Kill socat background process on termination +kill %1 +``` + +The `plex` user must have access to the above script, e.g.: +```bash +sudo chown -R plex /home/plex/ +sudo chmod 777 /home/plex/plex_launch.sh +``` + +Launch vopono with the following command (e.g. here with a ProtonVPN custom Wireguard config): + +```bash +$ vopono exec --user plex --custom ~/Downloads/protonvpn-RO-9.conf --protocol wireguard --provider custom --custom-port-forwarding protonvpn -o 32400 /home/plex/plex_launch.sh +``` + +Note the `-o 32400` is necessary to have local access to the Plex server from the host, which can be very useful for debugging. + + #### Proxy to host By default, vopono runs a small TCP proxy to proxy the ports on your diff --git a/src/args_config.rs b/src/args_config.rs index 1f440ce..1ccb9d4 100644 --- a/src/args_config.rs +++ b/src/args_config.rs @@ -113,14 +113,26 @@ impl ArgsConfig { .ok() .and_then(|s| PathBuf::from_str(s.as_ref()).ok()) }); + + // Note application cannot be defined in config file + let application: String = shellexpand::full(&command.application) + .map_err(|e| { + anyhow!( + "Shell expansion error for application: {:?}, error: {:?}", + &command.application, + e + ) + }) + .map(|c| c.to_string())?; let custom_netns_name = command_else_config_option!(custom_netns_name, command, config); let open_hosts = command_else_config_option!(open_hosts, command, config); let hosts = command_else_config_option!(hosts, command, config); let open_ports = command_else_config_option!(open_ports, command, config); let forward = command_else_config_option!(forward, command, config); + dbg!(&command.postup); // TODO let postup = command_else_config_option!(postup, command, config) .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); - + dbg!(&postup); let predown = command_else_config_option!(predown, command, config) .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); let group = command_else_config_option!(group, command, config); @@ -256,8 +268,7 @@ impl ArgsConfig { protocol, interface, server, - // TODO: Allow application to be saved in config file? - breaking change to CLI interface - application: command.application, + application, user, group, working_directory, diff --git a/src/exec.rs b/src/exec.rs index fa24ae7..7bdad94 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -22,8 +22,8 @@ use vopono_core::network::port_forwarding::piapf::Piapf; use vopono_core::network::port_forwarding::Forwarder; use vopono_core::network::shadowsocks::uses_shadowsocks; use vopono_core::network::sysctl::SysCtl; -use vopono_core::util::vopono_dir; use vopono_core::util::{get_config_from_alias, get_existing_namespaces, get_target_subnet}; +use vopono_core::util::{parse_command_str, vopono_dir}; pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> anyhow::Result<()> { // this captures all sigint signals @@ -89,8 +89,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any let mut ns; let _sysctl; - // Better to check for lockfile exists? - let using_existing_netns; + let _using_existing_netns; + let forwarder; if get_existing_namespaces()?.contains(&ns_name) { // If namespace exists, read its lock config info!( @@ -98,10 +98,16 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any &ns_name ); ns = NetworkNamespace::from_existing(ns_name)?; - using_existing_netns = true; + _using_existing_netns = true; + + if parsed_command.port_forwarding || parsed_command.custom_port_forwarding.is_some() { + warn!("Re-using existing network namespace {} - will not run port forwarder, should be run when netns first created", &ns.name); + } + + forwarder = None; } else { // Create new network namespace - using_existing_netns = false; + _using_existing_netns = false; ns = NetworkNamespace::new( ns_name.clone(), parsed_command.provider.clone(), @@ -159,12 +165,21 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any "VOPONO_NS_IP", ns.veth_pair_ips.as_ref().unwrap().namespace_ip.to_string(), ); + // Set env var referring to the host IP for the application: + std::env::set_var( + "VOPONO_HOST_IP", + ns.veth_pair_ips.as_ref().unwrap().host_ip.to_string(), + ); + std::env::set_var("VOPONO_NS", &ns.name); + + forwarder = provider_port_forwarding(&parsed_command, &ns)?; + if let Some(f) = forwarder.as_ref() { + std::env::set_var("VOPONO_FORWARDED_PORT", f.forwarded_port().to_string()); + } // Run PostUp script (if any) // Temporarily set env var referring to this network namespace name if let Some(pucmd) = parsed_command.postup.clone() { - std::env::set_var("VOPONO_NS", &ns.name); - let mut sudo_args = Vec::new(); if let Some(ref user) = parsed_command.user { sudo_args.push("--user"); @@ -175,31 +190,27 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any sudo_args.push(group); } + let parsed_pucmd = parse_command_str(&pucmd)?; + let parsed_pucmd_ptrs: Vec<&str> = parsed_pucmd.iter().map(|s| s.as_str()).collect(); + if !sudo_args.is_empty() { let mut args = vec!["--preserve-env"]; args.append(&mut sudo_args); - args.push(&pucmd); + args.extend(parsed_pucmd_ptrs); std::process::Command::new("sudo").args(args).spawn()?; } else { - std::process::Command::new(&pucmd).spawn()?; + std::process::Command::new(parsed_pucmd_ptrs[0]) + .args(parsed_pucmd_ptrs[1..].iter()) + .spawn()?; }; - - std::env::remove_var("VOPONO_NS"); } } - // Set env var referring to the host IP for the application: - std::env::set_var( - "VOPONO_HOST_IP", - ns.veth_pair_ips.as_ref().unwrap().host_ip.to_string(), - ); - let ns = ns.write_lockfile(&parsed_command.application)?; // Port forwarding for ProtonVPN and PIA which require loop to keep it active // Forwarder is returned so it isn't dropped - let forwarder = provider_port_forwarding(&parsed_command, using_existing_netns, &ns)?; // Launch TCP proxy server on other threads if forwarding ports // TODO: Fix when running as root @@ -231,6 +242,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any stay_alive(None, signals); } + std::env::remove_var("VOPONO_FORWARDED_PORT"); + std::env::remove_var("VOPONO_NS"); std::env::remove_var("VOPONO_NS_IP"); std::env::remove_var("VOPONO_HOST_IP"); @@ -286,7 +299,12 @@ fn run_protocol_in_netns( ); if let Some(dns) = &parsed_command.dns { // TODO: Separate hosts entries from DNS config? - ns.dns_config(dns, &[], parsed_command.hosts.as_ref())?; + ns.dns_config( + dns, + &[], + parsed_command.hosts.as_ref(), + parsed_command.allow_host_access, + )?; } return Ok(None); } @@ -350,7 +368,12 @@ fn run_protocol_in_netns( .unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]); // TODO: DNS suffixes? - ns.dns_config(&dns, &[], parsed_command.hosts.as_ref())?; + ns.dns_config( + &dns, + &[], + parsed_command.hosts.as_ref(), + parsed_command.allow_host_access, + )?; // Check if using Shadowsocks if let Some((ss_host, ss_lport)) = uses_shadowsocks( config_file @@ -408,7 +431,12 @@ fn run_protocol_in_netns( let old_dns = ns.dns_config.take(); std::mem::forget(old_dns); // TODO: DNS suffixes? - ns.dns_config(&[newdns], &[], parsed_command.hosts.as_ref())?; + ns.dns_config( + &[newdns], + &[], + parsed_command.hosts.as_ref(), + parsed_command.allow_host_access, + )?; } } } @@ -424,6 +452,7 @@ fn run_protocol_in_netns( parsed_command.disable_ipv6, parsed_command.dns.as_ref(), parsed_command.hosts.as_ref(), + parsed_command.allow_host_access, )?; } Protocol::OpenConnect => { @@ -432,7 +461,12 @@ fn run_protocol_in_netns( .clone() .unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]); // TODO: DNS suffixes? - ns.dns_config(&dns, &[], parsed_command.hosts.as_ref())?; + ns.dns_config( + &dns, + &[], + parsed_command.hosts.as_ref(), + parsed_command.allow_host_access, + )?; ns.run_openconnect( config_file .clone() @@ -454,6 +488,7 @@ fn run_protocol_in_netns( parsed_command.forward.as_ref(), parsed_command.hosts.as_ref(), parsed_command.firewall, + parsed_command.allow_host_access, )?; } } @@ -462,18 +497,11 @@ fn run_protocol_in_netns( fn provider_port_forwarding( parsed_command: &ArgsConfig, - using_existing_netns: bool, ns: &NetworkNamespace, ) -> anyhow::Result>> { // Does not re-run if re-using existing namespace - if using_existing_netns - && (parsed_command.port_forwarding || parsed_command.custom_port_forwarding.is_some()) - { - warn!("Re-using existing network namespace {} - will not run port forwarder, should be run when netns first created", &ns.name); - } - let forwarder: Option> = if (parsed_command.port_forwarding - || parsed_command.custom_port_forwarding.is_some()) - && !using_existing_netns + let forwarder: Option> = if parsed_command.port_forwarding + || parsed_command.custom_port_forwarding.is_some() { let provider_or_custom = if parsed_command.custom.is_some() { parsed_command.custom_port_forwarding.clone() diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index 99403e8..9e5c393 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -44,3 +44,4 @@ sha2 = "0.10" tiny_http = "0.12" chrono = "0.4" json = "0.12" +shell-words = "1" diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index afb9257..4956b15 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use super::{netns::NetworkNamespace, port_forwarding::Forwarder}; -use crate::util::get_all_running_process_names; +use crate::util::{get_all_running_process_names, parse_command_str}; use log::warn; pub struct ApplicationWrapper { @@ -19,7 +19,7 @@ impl ApplicationWrapper { port_forwarding: Option>, ) -> anyhow::Result { let running_processes = get_all_running_process_names(); - let app_vec = application.split_whitespace().collect::>(); + let app_vec = parse_command_str(application)?; for shared_process_app in [ "google-chrome-stable", @@ -32,16 +32,18 @@ impl ApplicationWrapper { .iter() { // TODO: Avoid String allocation here - if app_vec.contains(shared_process_app) + if app_vec.contains(&shared_process_app.to_string()) && running_processes.contains(&shared_process_app.to_string()) { warn!("{} is already running. You must force it to use a separate profile in order to launch a new process!", shared_process_app); } } + let app_vec_ptrs: Vec<&str> = app_vec.iter().map(|s| s.as_str()).collect(); + let handle = NetworkNamespace::exec_no_block( &netns.name, - app_vec.as_slice(), + app_vec_ptrs.as_slice(), user, group, false, diff --git a/vopono_core/src/network/dns_config.rs b/vopono_core/src/network/dns_config.rs index ad37b3a..3296ee9 100644 --- a/vopono_core/src/network/dns_config.rs +++ b/vopono_core/src/network/dns_config.rs @@ -18,6 +18,8 @@ impl DnsConfig { servers: &[IpAddr], suffixes: &[&str], hosts_entries: Option<&Vec>, + host_ip: IpAddr, // IP address of host as seen inside from network namespace, + allow_host_access: bool, ) -> anyhow::Result { let dir_path = format!("/etc/netns/{ns_name}"); std::fs::create_dir_all(&dir_path) @@ -54,15 +56,35 @@ impl DnsConfig { })?; } + let mut effective_hosts_entries = if allow_host_access { + debug!( + "--allow-host-access is true so adding host IP {} to hosts file as vopono.host", + &host_ip.to_string() + ); + vec![format!("{} vopono.host", host_ip.to_string())] + } else { + Vec::new() + }; + if let Some(my_hosts_entries) = hosts_entries { + effective_hosts_entries.extend(my_hosts_entries.iter().cloned()) + }; + + let effective_hosts_entries = if effective_hosts_entries.is_empty() { + None + } else { + Some(effective_hosts_entries) + }; + + if let Some(my_hosts_entries) = effective_hosts_entries { let hosts_path = format!("/etc/netns/{ns_name}/hosts"); let mut hosts = std::fs::File::create(&hosts_path) .with_context(|| format!("Failed to open hosts: {}", &hosts_path))?; std::fs::set_permissions(&hosts_path, PermissionsExt::from_mode(0o644)) .with_context(|| format!("Failed to set file permissions for {}", &hosts_path))?; - for hosts_enty in my_hosts_entries { - writeln!(hosts, "{hosts_enty}").with_context(|| { + for hosts_entry in my_hosts_entries { + writeln!(hosts, "{hosts_entry}").with_context(|| { format!("Failed to overwrite hosts: /etc/netns/{ns_name}/hosts") })?; } diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index bdad951..b42d6e5 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -12,7 +12,7 @@ use super::wireguard::Wireguard; use crate::config::providers::{UiClient, VpnProvider}; use crate::config::vpn::Protocol; use crate::network::host_masquerade::FirewallException; -use crate::util::{config_dir, set_config_permissions, sudo_command}; +use crate::util::{config_dir, parse_command_str, set_config_permissions, sudo_command}; use anyhow::{anyhow, Context}; use log::{debug, info, warn}; use nix::unistd; @@ -303,12 +303,18 @@ impl NetworkNamespace { server: &[IpAddr], suffixes: &[&str], hosts_entries: Option<&Vec>, + allow_host_access: bool, ) -> anyhow::Result<()> { self.dns_config = Some(DnsConfig::new( self.name.clone(), server, suffixes, hosts_entries, + self.veth_pair_ips + .as_ref() + .expect("Failed to get veth pair IPs") + .host_ip, + allow_host_access, )?); Ok(()) } @@ -369,6 +375,7 @@ impl NetworkNamespace { forward_ports: Option<&Vec>, hosts_entries: Option<&Vec>, firewall: Firewall, + allow_host_access: bool, ) -> anyhow::Result<()> { self.openfortivpn = Some(OpenFortiVpn::run( self, @@ -377,6 +384,7 @@ impl NetworkNamespace { forward_ports, hosts_entries, firewall, + allow_host_access, )?); Ok(()) } @@ -421,6 +429,7 @@ impl NetworkNamespace { disable_ipv6: bool, dns: Option<&Vec>, hosts_entries: Option<&Vec>, + allow_host_access: bool, ) -> anyhow::Result<()> { if let Ok(wgprov) = self.provider.get_dyn_wireguard_provider() { wgprov.wireguard_preup(config_file.as_path())?; @@ -436,6 +445,7 @@ impl NetworkNamespace { disable_ipv6, dns, hosts_entries, + allow_host_access, )?); Ok(()) } @@ -541,38 +551,60 @@ 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() { - std::env::set_var("VOPONO_NS", &self.name); - std::env::set_var( - "VOPONO_NS_IP", - self.veth_pair_ips - .as_ref() - .unwrap() - .namespace_ip - .to_string(), - ); - - 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); + // Extra check so we don't panic on bad predown command + match parse_command_str(pdcmd) { + Ok(parsed_pdcmd) => { + let parsed_pdcmd_ptrs: Vec<&str> = + parsed_pdcmd.iter().map(|s| s.as_str()).collect(); + + std::env::set_var("VOPONO_NS", &self.name); + std::env::set_var( + "VOPONO_NS_IP", + self.veth_pair_ips + .as_ref() + .unwrap() + .namespace_ip + .to_string(), + ); + std::env::set_var( + "VOPONO_HOST_IP", + self.veth_pair_ips.as_ref().unwrap().host_ip.to_string(), + ); - std::process::Command::new("sudo").args(args).spawn().ok(); - } else { - std::process::Command::new(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.extend(parsed_pdcmd_ptrs); + + std::process::Command::new("sudo").args(args).spawn().ok(); + } else { + std::process::Command::new(parsed_pdcmd_ptrs[0]) + .args(parsed_pdcmd_ptrs[1..].iter()) + .spawn() + .ok(); + } + + std::env::remove_var("VOPONO_NS"); + std::env::remove_var("VOPONO_NS_IP"); + std::env::remove_var("VOPONO_HOST_IP"); + } + Err(e) => { + log::error!( + "Failed to parse postdown command: {} in shutdown state - skipped postdown execution, error: {:?}", + &pdcmd, e + ) + } } - - std::env::remove_var("VOPONO_NS"); - std::env::remove_var("VOPONO_NS_IP"); } self.openvpn = None; diff --git a/vopono_core/src/network/openfortivpn.rs b/vopono_core/src/network/openfortivpn.rs index 495ead4..829cf3b 100644 --- a/vopono_core/src/network/openfortivpn.rs +++ b/vopono_core/src/network/openfortivpn.rs @@ -23,6 +23,7 @@ impl OpenFortiVpn { forward_ports: Option<&Vec>, hosts_entries: Option<&Vec>, firewall: Firewall, + allow_host_access: bool, ) -> anyhow::Result { if let Err(x) = which::which("openfortivpn") { error!("OpenFortiVPN not found. Is OpenFortiVPN installed and on PATH?"); @@ -106,7 +107,12 @@ impl OpenFortiVpn { let dns_ip: Vec = (dns.0).into_iter().map(IpAddr::from).collect(); // TODO: Avoid this meaningless collect let suffixes: Vec<&str> = (dns.1).iter().map(|x| x.as_str()).collect(); - netns.dns_config(dns_ip.as_slice(), suffixes.as_slice(), hosts_entries)?; + netns.dns_config( + dns_ip.as_slice(), + suffixes.as_slice(), + hosts_entries, + allow_host_access, + )?; // Allow input to and output from open ports (for port forwarding in tunnel) if let Some(opens) = open_ports { crate::util::open_ports(netns, opens.as_slice(), firewall)?; diff --git a/vopono_core/src/network/wireguard.rs b/vopono_core/src/network/wireguard.rs index 1414c3d..269afd7 100644 --- a/vopono_core/src/network/wireguard.rs +++ b/vopono_core/src/network/wireguard.rs @@ -33,6 +33,7 @@ impl Wireguard { disable_ipv6: bool, dns: Option<&Vec>, hosts_entries: Option<&Vec>, + allow_host_access: bool, ) -> anyhow::Result { if let Err(x) = which::which("wg") { error!("wg binary not found. Is wireguard-tools installed and on PATH?"); @@ -159,7 +160,7 @@ impl Wireguard { vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))] }); // TODO: DNS suffixes? - namespace.dns_config(&dns, &[], hosts_entries)?; + namespace.dns_config(&dns, &[], hosts_entries, allow_host_access)?; let fwmark = "51820"; NetworkNamespace::exec(&namespace.name, &["wg", "set", &if_name, "fwmark", fwmark])?; diff --git a/vopono_core/src/util/mod.rs b/vopono_core/src/util/mod.rs index 2843949..012f7ec 100644 --- a/vopono_core/src/util/mod.rs +++ b/vopono_core/src/util/mod.rs @@ -4,6 +4,7 @@ pub mod open_ports; pub mod pulseaudio; pub mod wireguard; +extern crate shell_words as shellwords; use crate::config::vpn::Protocol; use crate::network::firewall::Firewall; use crate::network::netns::Lockfile; @@ -481,3 +482,13 @@ pub fn get_lock_namespaces() -> anyhow::Result>> { })?; Ok(namespaces) } + +pub fn parse_command_str(command_str: &str) -> anyhow::Result> { + shellwords::split(command_str).map_err(|e| { + anyhow::anyhow!( + "Failed to parse command string: {}, error: {:?}", + &command_str, + e + ) + }) +}