From 0221806e876ac89136a3be4673151c3af6a05717 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sat, 2 Mar 2024 18:12:35 +0100 Subject: [PATCH] Add None provider, path expansion, refactoring - Add None provider and protocol for solely creating network-ready network namespace with no VPN service - Add shell path expansion to path arguments (e.g. you can use ~ in custom config path) - Refactor how CLI arguments are parsed using macro_rules --- Cargo.toml | 3 +- src/args.rs | 8 +- src/args_config.rs | 298 ++++++++ src/exec.rs | 927 ++++++++++-------------- src/list_configs.rs | 1 + src/main.rs | 2 +- src/sync.rs | 3 + vopono_core/Cargo.toml | 2 +- vopono_core/src/config/providers/mod.rs | 5 + vopono_core/src/config/vpn.rs | 1 + vopono_core/src/network/netns.rs | 6 + vopono_core/src/util/mod.rs | 17 +- 12 files changed, 706 insertions(+), 567 deletions(-) create mode 100644 src/args_config.rs diff --git a/Cargo.toml b/Cargo.toml index 8fc9f10..4cfef28 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.10.8" +version = "0.10.9" authors = ["James McMurray "] edition = "2021" license = "GPL-3.0-or-later" @@ -31,6 +31,7 @@ config = "0.14" basic_tcp_proxy = "0.3.2" strum = "0.26" strum_macros = "0.26" +shellexpand = { version = "3", features = ["full"] } [package.metadata.rpm] package = "vopono" diff --git a/src/args.rs b/src/args.rs index 862258a..f9f7d22 100644 --- a/src/args.rs +++ b/src/args.rs @@ -114,7 +114,7 @@ pub struct SynchCommand { pub struct ExecCommand { /// VPN Provider (must be given unless using custom config) #[clap(value_enum, long = "provider", short = 'p', ignore_case = true)] - pub vpn_provider: Option>, + pub provider: Option>, /// VPN Protocol (if not given will use default) #[clap(value_enum, long = "protocol", short = 'c', ignore_case = true)] @@ -145,7 +145,7 @@ pub struct ExecCommand { /// Custom VPN Provider - OpenVPN or Wireguard config file (will override other settings) #[clap(long = "custom")] - pub custom_config: Option, + pub custom: Option, /// DNS Server (will override provider's DNS server) #[clap(long = "dns", short = 'd')] @@ -153,7 +153,7 @@ pub struct ExecCommand { /// List of /etc/hosts entries for the network namespace (e.g. "10.0.1.10 webdav.server01.lan","10.0.1.10 vaultwarden.server01.lan"). For a local host you should also provide the open-hosts option. #[clap(long = "hosts", use_value_delimiter = true)] - pub hosts_entries: Option>, + pub hosts: Option>, /// List of host IP addresses to open on the network namespace (comma separated) #[clap(long = "open-hosts", use_value_delimiter = true)] @@ -174,7 +174,7 @@ pub struct ExecCommand { /// List of ports to forward from network namespace to host - useful for running servers and daemons #[clap(long = "forward", short = 'f')] - pub forward_ports: Option>, + pub forward: Option>, /// Disable proxying to host machine when forwarding ports #[clap(long = "no-proxy")] diff --git a/src/args_config.rs b/src/args_config.rs new file mode 100644 index 0000000..876f84a --- /dev/null +++ b/src/args_config.rs @@ -0,0 +1,298 @@ +// Handles using the args from either the CLI or config file + +use std::{net::IpAddr, path::PathBuf, str::FromStr}; + +use anyhow::anyhow; +use config::Config; +use vopono_core::{ + config::{providers::VpnProvider, vpn::Protocol}, + network::{ + firewall::Firewall, + network_interface::{get_active_interfaces, NetworkInterface}, + }, + util::{get_config_file_protocol, vopono_dir}, +}; + +use crate::args::ExecCommand; + +macro_rules! command_else_config_option { + // Get expression from command - command.expr + // If None then read from Config .get("expr") + // Returns None if absent in both + ($field_id:ident, $command:ident, $config:ident) => { + $command.$field_id.clone().or_else(|| { + $config + .get(stringify!($field_id)) + .or($config.get(&stringify!($field_id).replace('_', "-"))) + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + }) + }; +} +macro_rules! command_else_config_bool { + // Get bool ident from command - command.expr + // If None then read from Config .get("expr") + // Returns false if absent in both + ($field_id:ident, $command:ident, $config:ident) => { + $command.$field_id + || $config + .get(stringify!($field_id)) + .or($config.get(&stringify!($field_id).replace('_', "-"))) + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + .unwrap_or(false) + }; +} + +macro_rules! command_else_config_option_variant { + // Get enum variant ident from command - command.expr + // If None then read from Config .get("expr") + // Returns None if absent in both + ($field_id:ident, $command:ident, $config:ident) => { + $command.$field_id.map(|x| x.to_variant()).or_else(|| { + $config + .get(stringify!($field_id)) + .or($config.get(&stringify!($field_id).replace('_', "-"))) + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + }) + }; +} + +macro_rules! error_and_bail { + // log to error and bail + ($msg:literal) => { + log::error!("{}", $msg); + anyhow::bail!($msg); + }; +} + +// TODO: Generate this from procedural macro? +pub struct ArgsConfig { + pub provider: VpnProvider, + pub protocol: Protocol, + pub interface: NetworkInterface, + pub server: String, + pub application: String, + pub user: Option, + pub group: Option, + pub working_directory: Option, + pub custom: Option, + pub dns: Option>, + pub hosts: Option>, + pub open_hosts: Option>, + pub no_killswitch: bool, + pub keep_alive: bool, + pub open_ports: Option>, + pub forward: Option>, + pub no_proxy: bool, + pub firewall: Firewall, + pub disable_ipv6: bool, + pub postup: Option, + pub predown: Option, + pub custom_netns_name: Option, + pub allow_host_access: bool, + pub port_forwarding: bool, + pub custom_port_forwarding: Option, + pub port_forwarding_callback: Option, + pub create_netns_only: bool, +} + +impl ArgsConfig { + /// Return new ExecCommand with args from config file if missing in CLI but present there + /// Also handle CLI args consistency errors + pub fn get_cli_or_config_args(command: ExecCommand, config: Config) -> anyhow::Result { + // TODO: Automate field mapping with procedural macro over ExecCommand struct? + let custom: Option = command_else_config_option!(custom, command, config) + .and_then(|p| { + shellexpand::full(&p.to_string_lossy()) + .ok() + .and_then(|s| PathBuf::from_str(s.as_ref()).ok()) + }); + 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); + let postup = command_else_config_option!(postup, command, config) + .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); + + 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); + let working_directory = command_else_config_option!(working_directory, command, config) + .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); + let dns = command_else_config_option!(dns, command, config); + let user = command_else_config_option!(user, command, config) + .or_else(|| std::env::var("SUDO_USER").ok()); + let port_forwarding_callback = + command_else_config_option!(port_forwarding_callback, command, config) + .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); + + let no_proxy = command_else_config_bool!(no_proxy, command, config); + let keep_alive = command_else_config_bool!(keep_alive, command, config); + let port_forwarding = command_else_config_bool!(port_forwarding, command, config); + let allow_host_access = command_else_config_bool!(allow_host_access, command, config); + let create_netns_only = command_else_config_bool!(create_netns_only, command, config); + let disable_ipv6 = command_else_config_bool!(disable_ipv6, command, config); + let no_killswitch = command_else_config_bool!(no_killswitch, command, config); + + let firewall = command_else_config_option_variant!(firewall, command, config) + .ok_or_else(|| anyhow!("Failed to get Firewall variant from args")) + .or_else(|_| vopono_core::util::get_firewall())?; + let custom_port_forwarding = + command_else_config_option_variant!(custom_port_forwarding, command, config); + + if custom_port_forwarding.is_some() && custom.is_none() { + log::error!("Custom port forwarding implementation is set, but not using custom provider config file. custom-port-forwarding setting will be ignored"); + } + + // Assign network interface from args or vopono config file + // TODO: Does this work with string from config file? + let interface = command_else_config_option!(interface, command, config); + + let interface: NetworkInterface = match interface { + Some(x) => anyhow::Result::::Ok(x), + None => { + let active_interfaces = get_active_interfaces()?; + if active_interfaces.len() > 1 { + log::warn!("Multiple network interfaces are active: {:#?}, consider specifying the interface with the -i argument. Using {}", &active_interfaces, &active_interfaces[0]); + } + Ok( + NetworkInterface::new( + active_interfaces + .into_iter() + .next() + .ok_or_else(|| anyhow!("No active network interface - consider overriding network interface selection with -i argument"))?, + )?) + } + }?; + log::debug!("Interface: {}", &interface.name); + + let provider: VpnProvider; + let server: String; + let protocol: Protocol; + + // Assign protocol and server from args or vopono config file or custom config if used + if let Some(path) = &custom { + protocol = command + .protocol + .map(|x| x.to_variant()) + .ok_or_else(|| anyhow!(".")) + .or_else(|_| get_config_file_protocol(path))?; + + provider = VpnProvider::Custom; + + if protocol != Protocol::OpenConnect { + // Encode filename with base58 so we can fit it within 16 chars for the veth pair name + let sname = bs58::encode(&path.to_str().unwrap()).into_string(); + + server = sname[0..std::cmp::min(11, sname.len())].to_string(); + } else { + // For OpenConnect the server-name can be provided via the usual config or + // command-line-options. Since it also can be provided via the custom-config we will + // set an empty-string if it isn't provided. + server = command_else_config_option!(server, command, config).unwrap_or_default(); + } + } else { + // Get server and provider + provider = command_else_config_option_variant!(provider, command, config).ok_or_else( + || { + let msg = + "Enter a VPN provider as a command-line argument or in the vopono config.toml file"; + log::error!("{}", msg); + anyhow!(msg) + }, + )?; + + if provider == VpnProvider::Custom { + error_and_bail!("Must provide config file if using custom VPN Provider"); + } + + server = command_else_config_option!(server, command, config) + // Work-around for providers which do not need a server - TODO: Clean this + .or_else(|| if provider == VpnProvider::Warp {Some("warp".to_owned())} else {None}) + .or_else(|| if provider == VpnProvider::None {Some("none".to_owned())} else {None}) + .ok_or_else(|| { + let msg = "VPN server prefix must be provided as a command-line argument or in the vopono config.toml file"; + log::error!("{}", msg); anyhow!(msg)})?; + + // Check protocol is valid for provider + protocol = command_else_config_option_variant!(protocol, command, config) + .unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); + } + + if (provider == VpnProvider::Warp && protocol != Protocol::Warp) + || (provider != VpnProvider::Warp && protocol == Protocol::Warp) + { + error_and_bail!("Cloudflare Warp protocol must use Warp provider"); + } + + if provider == VpnProvider::None && custom.is_some() { + error_and_bail!("Custom config cannot be set when using None provider"); + } + + if (provider == VpnProvider::None && protocol != Protocol::None) + || (provider != VpnProvider::None && protocol == Protocol::None) + { + error_and_bail!("None protocol must use None provider - will run not run any VPN service inside netns"); + } + + Ok(Self { + provider, + protocol, + interface, + server, + // TODO: Allow application to be saved in config file? - breaking change to CLI interface + application: command.application, + user, + group, + working_directory, + custom, + dns, + hosts, + open_hosts, + no_killswitch, + keep_alive, + open_ports, + forward, + no_proxy, + firewall, + disable_ipv6, + postup, + predown, + custom_netns_name, + allow_host_access, + port_forwarding, + custom_port_forwarding, + port_forwarding_callback, + create_netns_only, + }) + } + + /// Read vopono config file to Config struct + pub fn get_config_file(command: &ExecCommand) -> anyhow::Result { + let config_path = command + .vopono_config + .clone() + .ok_or_else(|| anyhow!("No config file passed")) + .or_else::(|_| Ok(vopono_dir()?.join("config.toml")))?; + { + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .read(true) + .open(&config_path)?; + } + let vopono_config_settings_builder = + config::Config::builder().add_source(config::File::from(config_path.clone())); + vopono_config_settings_builder.build().map_err(|e| { + anyhow!( + "Failed to parse config from: {} , err: {}", + config_path.to_string_lossy(), + e + ) + }) + } +} diff --git a/src/exec.rs b/src/exec.rs index 391629c..f818859 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -1,11 +1,13 @@ +use crate::args_config::ArgsConfig; + use super::args::ExecCommand; use super::sync::synch; use anyhow::{anyhow, bail}; use log::{debug, error, info, warn}; +use signal_hook::iterator::SignalsInfo; 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, io::{self, Write}, @@ -13,291 +15,73 @@ use std::{ use vopono_core::config::providers::{UiClient, VpnProvider}; use vopono_core::config::vpn::{verify_auth, Protocol}; use vopono_core::network::application_wrapper::ApplicationWrapper; -use vopono_core::network::firewall::Firewall; use vopono_core::network::netns::NetworkNamespace; -use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface}; +use vopono_core::network::network_interface::NetworkInterface; use vopono_core::network::port_forwarding::natpmpc::Natpmpc; 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_file_protocol, get_config_from_alias}; -use vopono_core::util::{get_existing_namespaces, get_target_subnet}; +use vopono_core::util::{get_config_from_alias, get_existing_namespaces, get_target_subnet}; pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> { // this captures all sigint signals // ignore for now, they are automatically passed on to the child let signals = Signals::new([SIGINT])?; - let provider: VpnProvider; - let server_name: String; - let protocol: Protocol; - - // TODO: Refactor this part - DRY - macro_rules ? // Check if we have config file path passed on command line // Create empty config file if does not exist create_dir_all(vopono_dir()?)?; - let config_path = command - .vopono_config - .ok_or_else(|| anyhow!("No config file passed")) - .or_else::(|_| Ok(vopono_dir()?.join("config.toml")))?; - { - std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .read(true) - .open(&config_path)?; - } - let vopono_config_settings_builder = - config::Config::builder().add_source(config::File::from(config_path)); - let vopono_config_settings = vopono_config_settings_builder.build()?; - - // Assign firewall from args or vopono config file - let firewall: Firewall = command - .firewall - .map(|x| x.to_variant()) - .ok_or_else(|| anyhow!("")) - .or_else(|_| { - vopono_config_settings - .get("firewall") - .map_err(|_e| anyhow!("Failed to read config file")) - }) - .or_else(|_x| vopono_core::util::get_firewall())?; - - // Assign custom_config from args or vopono config file - let custom_config = command.custom_config.clone().or_else(|| { - vopono_config_settings - .get("custom_config") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); + let vopono_config_settings = ArgsConfig::get_config_file(&command)?; - // Assign custom_config from args or vopono config file - let custom_netns_name = command.custom_netns_name.clone().or_else(|| { - vopono_config_settings - .get("custom_netns_name") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); - - // Assign open_hosts from args or vopono config file - let mut open_hosts = command.open_hosts.clone().or_else(|| { - vopono_config_settings - .get("open_hosts") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); - let allow_host_access = command.allow_host_access - || vopono_config_settings - .get("allow_host_access") - .map_err(|_e| anyhow!("Failed to read config file")) - .unwrap_or(false); - - // Assign postup script from args or vopono config file - let postup = command.postup.clone().or_else(|| { - vopono_config_settings - .get("postup") - .map_err(|_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| 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| anyhow!("Failed to read config file")) - .ok() - .or_else(|| std::env::var("SUDO_USER").ok()) - } else { - command.user - }; - - // Group for application command - let group = if command.group.is_none() { - vopono_config_settings - .get("group") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - } else { - command.group - }; + let mut parsed_command = ArgsConfig::get_cli_or_config_args(command, vopono_config_settings)?; - // Working directory for application command - let working_directory = if command.working_directory.is_none() { - vopono_config_settings - .get("working-directory") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - } else { - command.working_directory - }; - - // Port forwarding - let port_forwarding = if !command.port_forwarding { - vopono_config_settings - .get("port-forwarding") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - .unwrap_or(false) - } else { - command.port_forwarding - }; - - // Custom port forwarding (implementation to use for --custom-config) - let custom_port_forwarding: Option = command - .custom_port_forwarding - .map(|x| x.to_variant()) - .or_else(|| { - vopono_config_settings - .get("custom_port_forwarding") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); - if custom_port_forwarding.is_some() && custom_config.is_none() { - error!("Custom port forwarding implementation is set, but not using custom provider config file. custom-port-forwarding setting will be ignored"); - } - - // Create netns only - let create_netns_only = if !command.create_netns_only { - vopono_config_settings - .get("create-netns-only") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - .unwrap_or(false) - } else { - command.create_netns_only - }; - - // Assign DNS server from args or vopono config file - let base_dns = command.dns.clone().or_else(|| { - vopono_config_settings - .get("dns") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); - - // TODO: Modify this to allow creating base netns only - // Assign protocol and server from args or vopono config file or custom config if used - if let Some(path) = &custom_config { - protocol = command - .protocol - .map(|x| x.to_variant()) - .unwrap_or_else(|| get_config_file_protocol(path)); - provider = VpnProvider::Custom; - - if protocol != Protocol::OpenConnect { - // Encode filename with base58 so we can fit it within 16 chars for the veth pair name - let sname = bs58::encode(&path.to_str().unwrap()).into_string(); - - server_name = sname[0..std::cmp::min(11, sname.len())].to_string(); - } else { - // For OpenConnect the server-name can be provided via the usual config or - // command-line-options. Since it also can be provided via the custom-config we will - // set an empty-string if it isn't provided. - server_name = command - .server - .or_else(|| { - vopono_config_settings - .get("server") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }) - .or_else(|| Some(String::new())) - .unwrap(); - } - } else { - // Get server and provider - provider = command - .vpn_provider - .map(|x| x.to_variant()) - .or_else(|| { - vopono_config_settings - .get("provider") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }).ok_or_else(|| { - let msg = "Enter a VPN provider as a command-line argument or in the vopono config.toml file"; - error!("{}", msg); anyhow!(msg)})?; - - if provider == VpnProvider::Custom { - let msg = "Must provide config file if using custom VPN Provider"; - error!("{}", msg); - bail!(msg); - } - - server_name = command - .server - .or_else(|| if provider == VpnProvider::Warp {Some("warp".to_owned())} else {None}) - .or_else(|| { - vopono_config_settings - .get("server") - .map_err(|_e| { - anyhow!("Failed to read config file") - }) - .ok() - }).ok_or_else(|| { - let msg = "VPN server prefix must be provided as a command-line argument or in the vopono config.toml file"; - error!("{}", msg); anyhow!(msg)})?; - - // Check protocol is valid for provider - protocol = command - .protocol - .map(|x| x.to_variant()) - .or_else(|| { - vopono_config_settings - .get("protocol") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }) - .unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); - } - - if (provider == VpnProvider::Warp && protocol != Protocol::Warp) - || (provider != VpnProvider::Warp && protocol == Protocol::Warp) + if parsed_command.provider != VpnProvider::Custom + && parsed_command.provider != VpnProvider::None + && parsed_command.protocol != Protocol::Warp { - bail!("Cloudflare Warp protocol must use Warp provider"); - } - - if provider != VpnProvider::Custom && protocol != Protocol::Warp { // Check config files exist for provider - let cdir = match protocol { - Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(), - Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(), + let cdir = match parsed_command.protocol { + Protocol::OpenVpn => parsed_command + .provider + .get_dyn_openvpn_provider()? + .openvpn_dir(), + Protocol::Wireguard => parsed_command + .provider + .get_dyn_wireguard_provider()? + .wireguard_dir(), Protocol::Warp => unreachable!("Unreachable, Warp must use Warp provider"), Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"), Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"), + Protocol::None => bail!("None protocol must use None provider"), }?; if !cdir.exists() || cdir.read_dir()?.next().is_none() { info!( "Config files for {} {} do not exist, running vopono sync", - provider, protocol + parsed_command.provider, parsed_command.protocol ); - synch(provider.clone(), Some(protocol.clone()), uiclient)?; + synch( + parsed_command.provider.clone(), + Some(parsed_command.protocol.clone()), + uiclient, + )?; } } - let alias = match provider { + let alias = match parsed_command.provider { VpnProvider::Custom => "c".to_string(), - _ => provider.get_dyn_provider().alias_2char(), + VpnProvider::None => "none".to_string(), + _ => parsed_command.provider.get_dyn_provider().alias_2char(), }; - let ns_name = if let Some(c_ns_name) = custom_netns_name { + let ns_name = if let Some(c_ns_name) = parsed_command.custom_netns_name.clone() { c_ns_name } else { - let short_name = if server_name.len() > 7 { - bs58::encode(&server_name).into_string()[0..7].to_string() + let short_name = if parsed_command.server.len() > 7 { + bs58::encode(&parsed_command.server).into_string()[0..7].to_string() } else { - server_name.replace('-', "") + parsed_command.server.replace('-', "") }; format!("vo_{alias}_{short_name}") }; @@ -305,58 +89,6 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let mut ns; let _sysctl; - // Assign network interface from args or vopono config file - let interface = command.interface.clone().or_else(|| { - vopono_config_settings - .get_string("interface") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) - .map(|x| { - NetworkInterface::from_str(&x) - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to parse network interface in config file") - }) - .ok() - }) - .ok() - .flatten() - }); - let interface: NetworkInterface = match interface { - Some(x) => anyhow::Result::::Ok(x), - None => { - let active_interfaces = get_active_interfaces()?; - if active_interfaces.len() > 1 { - warn!("Multiple network interfaces are active: {:#?}, consider specifying the interface with the -i argument. Using {}", &active_interfaces, &active_interfaces[0]); - } - Ok( - NetworkInterface::new( - active_interfaces - .into_iter() - .next() - .ok_or_else(|| anyhow!("No active network interface - consider overriding network interface selection with -i argument"))?, - )?) - } - }?; - debug!("Interface: {}", &interface.name); - - let config_file = if protocol == Protocol::Warp { - None - } else if provider != VpnProvider::Custom { - let cdir = match protocol { - Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(), - Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(), - Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"), - Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"), - Protocol::Warp => unreachable!(), - }?; - Some(get_config_from_alias(&cdir, &server_name)?) - } else { - Some(custom_config.clone().expect("No custom config provided")) - }; - // Better to check for lockfile exists? let using_existing_netns; if get_existing_namespaces()?.contains(&ns_name) { @@ -368,175 +100,58 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> ns = NetworkNamespace::from_existing(ns_name)?; using_existing_netns = true; } else { + // Create new network namespace using_existing_netns = false; ns = NetworkNamespace::new( ns_name.clone(), - provider.clone(), - protocol.clone(), - firewall, - predown, - user.clone(), - group.clone(), + parsed_command.provider.clone(), + parsed_command.protocol.clone(), + parsed_command.firewall, + parsed_command.predown.clone(), + parsed_command.user.clone(), + parsed_command.group.clone(), )?; let target_subnet = get_target_subnet()?; ns.add_loopback()?; ns.add_veth_pair()?; - ns.add_routing(target_subnet, open_hosts.as_ref(), allow_host_access)?; + ns.add_routing( + target_subnet, + parsed_command.open_hosts.as_ref(), + parsed_command.allow_host_access, + )?; // Add local host to open hosts if allow_host_access enabled - if allow_host_access { + if parsed_command.allow_host_access { let host_ip = ns.veth_pair_ips.as_ref().unwrap().host_ip; warn!( "Allowing host access from network namespace, host IP address is: {}", host_ip ); - if let Some(oh) = open_hosts.iter_mut().next() { + if let Some(oh) = parsed_command.open_hosts.iter_mut().next() { oh.push(host_ip); } else { - open_hosts = Some(vec![host_ip]); + parsed_command.open_hosts = Some(vec![host_ip]); } } - ns.add_host_masquerade(target_subnet, interface.clone(), firewall)?; + ns.add_host_masquerade( + target_subnet, + parsed_command.interface.clone(), + parsed_command.firewall, + )?; ns.add_firewall_exception( - interface, + parsed_command.interface.clone(), NetworkInterface::new(ns.veth_pair.as_ref().unwrap().dest.clone())?, - firewall, + parsed_command.firewall, )?; _sysctl = SysCtl::enable_ipv4_forwarding(); // TODO: Skip this if netns config only - match protocol { - Protocol::Warp => ns.run_warp( - command.open_ports.as_ref(), - command.forward_ports.as_ref(), - firewall, - )?, - Protocol::OpenVpn => { - // Handle authentication check - let auth_file = if provider != VpnProvider::Custom { - verify_auth(provider.get_dyn_openvpn_provider()?, uiclient)? - } else { - None - }; - - let dns = base_dns - .clone() - .or_else(|| { - provider - .get_dyn_openvpn_provider() - .ok() - .and_then(|x| x.provider_dns()) - }) - .unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]); - - // TODO: DNS suffixes? - ns.dns_config(&dns, &[], command.hosts_entries.as_ref())?; - // Check if using Shadowsocks - if let Some((ss_host, ss_lport)) = uses_shadowsocks( - config_file - .as_ref() - .expect("No OpenVPN config file provided"), - )? { - if provider == VpnProvider::Custom { - warn!("Custom provider specifies socks-proxy, if this is local you must run it yourself (e.g. shadowsocks)"); - } else { - let dyn_ss_provider = provider.get_dyn_shadowsocks_provider()?; - let password = dyn_ss_provider.password(); - let encrypt_method = dyn_ss_provider.encrypt_method(); - ns.run_shadowsocks( - config_file - .as_ref() - .expect("No OpenVPN config file provided"), - ss_host, - ss_lport, - &password, - &encrypt_method, - )?; - } - } + let config_file = run_protocol_in_netns(&parsed_command, &mut ns, uiclient)?; + ns.set_config_file(config_file); - ns.run_openvpn( - config_file - .clone() - .expect("No OpenVPN config file provided"), - auth_file, - &dns, - !command.no_killswitch, - command.open_ports.as_ref(), - command.forward_ports.as_ref(), - firewall, - command.disable_ipv6, - )?; - debug!( - "Checking that OpenVPN is running in namespace: {}", - &ns_name - ); - if !ns.check_openvpn_running() { - error!( - "OpenVPN not running in network namespace {}, probable dead lock file or authentication error", - &ns_name - ); - return Err(anyhow!( - "OpenVPN not running in network namespace, probable dead lock file authentication error" - )); - } - - // Set DNS with OpenVPN server response if present - if base_dns.is_none() { - if let Some(newdns) = ns.openvpn.as_ref().unwrap().openvpn_dns { - let old_dns = ns.dns_config.take(); - std::mem::forget(old_dns); - // TODO: DNS suffixes? - ns.dns_config(&[newdns], &[], command.hosts_entries.as_ref())?; - } - } - } - Protocol::Wireguard => { - ns.run_wireguard( - config_file - .clone() - .expect("No Wireguard config file provided"), - !command.no_killswitch, - command.open_ports.as_ref(), - command.forward_ports.as_ref(), - firewall, - command.disable_ipv6, - base_dns.as_ref(), - command.hosts_entries.as_ref(), - )?; - } - Protocol::OpenConnect => { - let dns = base_dns.unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]); - // TODO: DNS suffixes? - ns.dns_config(&dns, &[], command.hosts_entries.as_ref())?; - ns.run_openconnect( - config_file - .clone() - .expect("No OpenConnect config file provided"), - command.open_ports.as_ref(), - command.forward_ports.as_ref(), - firewall, - &server_name, - uiclient, - )?; - } - Protocol::OpenFortiVpn => { - // TODO: DNS handled by OpenFortiVpn directly? - ns.run_openfortivpn( - config_file - .clone() - .expect("No OpenFortiVPN config file provided"), - command.open_ports.as_ref(), - command.forward_ports.as_ref(), - command.hosts_entries.as_ref(), - firewall, - )?; - } - } - - if let Some(ref hosts) = open_hosts { - vopono_core::util::open_hosts(&ns, hosts.to_vec(), firewall)?; + if let Some(ref hosts) = parsed_command.open_hosts { + vopono_core::util::open_hosts(&ns, hosts.to_vec(), parsed_command.firewall)?; } // Temporarily set env var referring to this network namespace IP @@ -548,15 +163,15 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // Run PostUp script (if any) // Temporarily set env var referring to this network namespace name - if let Some(pucmd) = postup { + 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) = user { + if let Some(ref user) = parsed_command.user { sudo_args.push("--user"); sudo_args.push(user); } - if let Some(ref group) = group { + if let Some(ref group) = parsed_command.group { sudo_args.push("--group"); sudo_args.push(group); } @@ -581,83 +196,17 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> ns.veth_pair_ips.as_ref().unwrap().host_ip.to_string(), ); - let ns = ns.write_lockfile(&command.application)?; - - // Does not re-run if re-using existing namespace - if using_existing_netns && (port_forwarding || 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 (port_forwarding - || custom_port_forwarding.is_some()) - && !using_existing_netns - { - let provider_or_custom = if custom_config.is_some() { - custom_port_forwarding - } else { - Some(provider) - }; - - if provider_or_custom.is_some() { - debug!( - "Will use {:?} as provider for port forwarding", - &provider_or_custom - ); - } - - let callback = command.port_forwarding_callback.or_else(|| { - vopono_config_settings - .get("port_forwarding_callback") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); - - match provider_or_custom { - Some(VpnProvider::PrivateInternetAccess) => { - let conf_path = config_file.expect("No PIA config file provided"); - let conf_name = conf_path - .file_name() - .unwrap() - .to_str() - .expect("No filename for PIA config file") - .to_string(); - Some(Box::new(Piapf::new( - &ns, - &conf_name, - &protocol, - callback.as_ref(), - )?)) - } - Some(VpnProvider::ProtonVPN) => { - vopono_core::util::open_hosts( - &ns, - vec![vopono_core::network::port_forwarding::natpmpc::PROTONVPN_GATEWAY], - firewall, - )?; - Some(Box::new(Natpmpc::new(&ns, callback.as_ref())?)) - } - Some(p) => { - error!("Port forwarding not supported for the selected provider: {} - ignoring --port-forwarding", p); - None - } - None => { - error!("--port-forwarding set but --custom-port-forwarding provider not provided for --custom-config usage. Ignoring --port-forwarding"); - None - } - } - } else { - None - }; + let ns = ns.write_lockfile(&parsed_command.application)?; - // TODO: The forwarder should probably be able to do this (pass firewall?) - if let Some(fwd) = forwarder.as_ref() { - vopono_core::util::open_ports(&ns, &[fwd.forwarded_port()], firewall)?; - } + // 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 let mut proxy = Vec::new(); - if let Some(f) = command.forward_ports { - if !(command.no_proxy || f.is_empty()) { + if let Some(f) = parsed_command.forward.clone() { + if !(parsed_command.no_proxy || f.is_empty()) { for p in f { debug!( "Forwarding port: {}, {:?}", @@ -673,41 +222,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> } } - if !create_netns_only { - let application = ApplicationWrapper::new( - &ns, - &command.application, - user, - group, - working_directory.map(PathBuf::from), - forwarder, - )?; - - let pid = application.handle.id(); - info!( - "Application {} launched in network namespace {} with pid {}", - &command.application, &ns.name, pid - ); - - if let Some(fwd) = application.port_forwarding.as_ref() { - info!("Port Forwarding on port {}", fwd.forwarded_port()) - } - let output = application.wait_with_output()?; - io::stdout().write_all(output.stdout.as_slice())?; - - // Allow daemons to leave namespace open - if vopono_core::util::check_process_running(pid) { - info!( - "Process {} still running, assumed to be daemon - will leave network namespace {} alive until ctrl+C received", - pid, &ns.name - ); - stay_alive(Some(pid), signals); - } else if command.keep_alive { - info!( - "Keep-alive flag active - will leave network namespace {} alive until ctrl+C received", &ns.name - ); - stay_alive(None, signals); - } + if !parsed_command.create_netns_only { + run_application(&parsed_command, forwarder, &ns, signals)?; } else { info!( "Created netns {} - will leave network namespace alive until ctrl+C received", @@ -758,3 +274,306 @@ fn stay_alive(pid: Option, mut signals: Signals) { handle.close(); thread.join().unwrap(); } + +fn run_protocol_in_netns( + parsed_command: &ArgsConfig, + ns: &mut NetworkNamespace, + uiclient: &dyn UiClient, +) -> anyhow::Result> { + if parsed_command.provider == VpnProvider::None { + log::warn!( + "Provider set to None, will not run any VPN protocol inside the network namespace" + ); + if let Some(dns) = &parsed_command.dns { + // TODO: Separate hosts entries from DNS config? + ns.dns_config(dns, &[], parsed_command.hosts.as_ref())?; + } + return Ok(None); + } + + let config_file = if parsed_command.protocol == Protocol::Warp { + None + } else if parsed_command.provider != VpnProvider::Custom { + let cdir = match parsed_command.protocol { + Protocol::OpenVpn => parsed_command + .provider + .get_dyn_openvpn_provider()? + .openvpn_dir(), + Protocol::Wireguard => parsed_command + .provider + .get_dyn_wireguard_provider()? + .wireguard_dir(), + Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"), + Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"), + Protocol::Warp => unreachable!(), + Protocol::None => unreachable!(), + }?; + Some(get_config_from_alias(&cdir, &parsed_command.server)?) + } else { + // TODO: Improve error here + Some( + parsed_command + .custom + .clone() + .expect("No custom config provided"), + ) + }; + + match parsed_command.protocol { + Protocol::None => unreachable!(), + Protocol::Warp => ns.run_warp( + parsed_command.open_ports.as_ref(), + parsed_command.forward.as_ref(), + parsed_command.firewall, + )?, + Protocol::OpenVpn => { + // Handle authentication check + let auth_file = if parsed_command.provider != VpnProvider::Custom { + verify_auth( + parsed_command.provider.get_dyn_openvpn_provider()?, + uiclient, + )? + } else { + None + }; + + let dns = parsed_command + .dns + .clone() + .or_else(|| { + parsed_command + .provider + .get_dyn_openvpn_provider() + .ok() + .and_then(|x| x.provider_dns()) + }) + .unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]); + + // TODO: DNS suffixes? + ns.dns_config(&dns, &[], parsed_command.hosts.as_ref())?; + // Check if using Shadowsocks + if let Some((ss_host, ss_lport)) = uses_shadowsocks( + config_file + .as_ref() + .expect("No OpenVPN config file provided"), + )? { + if parsed_command.provider == VpnProvider::Custom { + warn!("Custom provider specifies socks-proxy, if this is local you must run it yourself (e.g. shadowsocks)"); + } else { + let dyn_ss_provider = parsed_command.provider.get_dyn_shadowsocks_provider()?; + let password = dyn_ss_provider.password(); + let encrypt_method = dyn_ss_provider.encrypt_method(); + ns.run_shadowsocks( + config_file + .as_ref() + .expect("No OpenVPN config file provided"), + ss_host, + ss_lport, + &password, + &encrypt_method, + )?; + } + } + + ns.run_openvpn( + config_file + .clone() + .expect("No OpenVPN config file provided"), + auth_file, + &dns, + !parsed_command.no_killswitch, + parsed_command.open_ports.as_ref(), + parsed_command.forward.as_ref(), + parsed_command.firewall, + parsed_command.disable_ipv6, + )?; + debug!( + "Checking that OpenVPN is running in namespace: {}", + &ns.name + ); + if !ns.check_openvpn_running() { + error!( + "OpenVPN not running in network namespace {}, probable dead lock file or authentication error", + &ns.name + ); + return Err(anyhow!( + "OpenVPN not running in network namespace, probable dead lock file authentication error" + )); + } + + // Set DNS with OpenVPN server response if present + if parsed_command.dns.is_none() { + if let Some(newdns) = ns.openvpn.as_ref().unwrap().openvpn_dns { + let old_dns = ns.dns_config.take(); + std::mem::forget(old_dns); + // TODO: DNS suffixes? + ns.dns_config(&[newdns], &[], parsed_command.hosts.as_ref())?; + } + } + } + Protocol::Wireguard => { + ns.run_wireguard( + config_file + .clone() + .expect("No Wireguard config file provided"), + !parsed_command.no_killswitch, + parsed_command.open_ports.as_ref(), + parsed_command.forward.as_ref(), + parsed_command.firewall, + parsed_command.disable_ipv6, + parsed_command.dns.as_ref(), + parsed_command.hosts.as_ref(), + )?; + } + Protocol::OpenConnect => { + let dns = parsed_command + .dns + .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.run_openconnect( + config_file + .clone() + .expect("No OpenConnect config file provided"), + parsed_command.open_ports.as_ref(), + parsed_command.forward.as_ref(), + parsed_command.firewall, + &parsed_command.server, + uiclient, + )?; + } + Protocol::OpenFortiVpn => { + // TODO: DNS handled by OpenFortiVpn directly? + ns.run_openfortivpn( + config_file + .clone() + .expect("No OpenFortiVPN config file provided"), + parsed_command.open_ports.as_ref(), + parsed_command.forward.as_ref(), + parsed_command.hosts.as_ref(), + parsed_command.firewall, + )?; + } + } + Ok(config_file) +} + +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 provider_or_custom = if parsed_command.custom.is_some() { + parsed_command.custom_port_forwarding.clone() + } else { + Some(parsed_command.provider.clone()) + }; + + if provider_or_custom.is_some() { + debug!( + "Will use {:?} as provider for port forwarding", + &provider_or_custom + ); + } + + match provider_or_custom { + Some(VpnProvider::PrivateInternetAccess) => { + let conf_path = ns.config_file.clone().expect("No PIA config file provided"); + let conf_name = conf_path + .file_name() + .unwrap() + .to_str() + .expect("No filename for PIA config file") + .to_string(); + Some(Box::new(Piapf::new( + ns, + &conf_name, + &parsed_command.protocol, + parsed_command.port_forwarding_callback.as_ref(), + )?)) + } + Some(VpnProvider::ProtonVPN) => { + vopono_core::util::open_hosts( + ns, + vec![vopono_core::network::port_forwarding::natpmpc::PROTONVPN_GATEWAY], + parsed_command.firewall, + )?; + Some(Box::new(Natpmpc::new( + ns, + parsed_command.port_forwarding_callback.as_ref(), + )?)) + } + Some(p) => { + error!("Port forwarding not supported for the selected provider: {} - ignoring --port-forwarding", p); + None + } + None => { + error!("--port-forwarding set but --custom-port-forwarding provider not provided for --custom-config usage. Ignoring --port-forwarding"); + None + } + } + } else { + None + }; + + // TODO: The forwarder should probably be able to do this (pass firewall?) + if let Some(fwd) = forwarder.as_ref() { + vopono_core::util::open_ports(ns, &[fwd.forwarded_port()], parsed_command.firewall)?; + } + Ok(forwarder) +} + +fn run_application( + parsed_command: &ArgsConfig, + forwarder: Option>, + ns: &NetworkNamespace, + signals: SignalsInfo, +) -> anyhow::Result<()> { + let application = ApplicationWrapper::new( + ns, + &parsed_command.application, + parsed_command.user.clone(), + parsed_command.group.clone(), + parsed_command.working_directory.clone().map(PathBuf::from), + forwarder, + )?; + + let pid = application.handle.id(); + info!( + "Application {} launched in network namespace {} with pid {}", + &parsed_command.application, &ns.name, pid + ); + + if let Some(fwd) = application.port_forwarding.as_ref() { + info!("Port Forwarding on port {}", fwd.forwarded_port()) + } + let output = application.wait_with_output()?; + io::stdout().write_all(output.stdout.as_slice())?; + + // Allow daemons to leave namespace open + if vopono_core::util::check_process_running(pid) { + info!( + "Process {} still running, assumed to be daemon - will leave network namespace {} alive until ctrl+C received", + pid, &ns.name + ); + stay_alive(Some(pid), signals); + } else if parsed_command.keep_alive { + info!( + "Keep-alive flag active - will leave network namespace {} alive until ctrl+C received", + &ns.name + ); + stay_alive(None, signals); + } + Ok(()) +} diff --git a/src/list_configs.rs b/src/list_configs.rs index 4959cfc..284607b 100644 --- a/src/list_configs.rs +++ b/src/list_configs.rs @@ -24,6 +24,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> { Protocol::Warp => bail!("Config listing not implemented for Cloudflare Warp"), Protocol::OpenConnect => bail!("Config listing not implemented for OpenConnect"), Protocol::OpenFortiVpn => bail!("Config listing not implemented for OpenFortiVPN"), + Protocol::None => bail!("Config listing not implemented for None Protocol"), }?; if !cdir.exists() || cdir.read_dir()?.next().is_none() { bail!( diff --git a/src/main.rs b/src/main.rs index 557794d..e3e9d36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] mod args; +mod args_config; mod cli_client; mod exec; mod list; @@ -23,7 +24,6 @@ use which::which; fn main() -> anyhow::Result<()> { // Get struct of args using structopt let app = args::App::parse(); - // Set up logging let mut builder = pretty_env_logger::formatted_timed_builder(); let log_level = if app.verbose { diff --git a/src/sync.rs b/src/sync.rs index 7d1556d..5ff1288 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -61,6 +61,9 @@ pub fn synch( Some(Protocol::Warp) => { error!("vopono sync not supported for Cloudflare Warp protocol"); } + Some(Protocol::None) => { + error!("vopono sync not supported for None protocol"); + } // TODO: Fix this asking for same credentials twice None => { if let Ok(p) = provider.get_dyn_wireguard_provider() { diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index ecb26a9..07cbaa4 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vopono_core" description = "Library code for running VPN connections in network namespaces" -version = "0.1.8" +version = "0.1.9" edition = "2021" authors = ["James McMurray "] license = "GPL-3.0-or-later" diff --git a/vopono_core/src/config/providers/mod.rs b/vopono_core/src/config/providers/mod.rs index ae088ad..f50a06e 100644 --- a/vopono_core/src/config/providers/mod.rs +++ b/vopono_core/src/config/providers/mod.rs @@ -47,6 +47,7 @@ pub enum VpnProvider { HMA, Warp, Custom, + None, // Run no protocol inside netns } // Do this since we can't downcast from Provider to other trait objects @@ -64,6 +65,7 @@ impl VpnProvider { Self::HMA => Box::new(hma::HMA {}), Self::Warp => Box::new(warp::Warp {}), Self::Custom => unimplemented!("Custom provider uses separate logic"), + Self::None => unimplemented!("None provider runs no protocol"), } } @@ -80,6 +82,7 @@ impl VpnProvider { Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), Self::MozillaVPN => Err(anyhow!("MozillaVPN only supports Wireguard!")), Self::Custom => Err(anyhow!("Custom provider uses separate logic")), + Self::None => unimplemented!("None provider runs no protocol"), } } @@ -92,6 +95,7 @@ impl VpnProvider { Self::IVPN => Ok(Box::new(ivpn::IVPN {})), Self::Custom => Err(anyhow!("Custom provider uses separate logic")), Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), + Self::None => unimplemented!("None provider runs no protocol"), _ => Err(anyhow!("Wireguard not implemented")), } } @@ -101,6 +105,7 @@ impl VpnProvider { Self::Mullvad => Ok(Box::new(mullvad::Mullvad {})), Self::Custom => Err(anyhow!("Start Shadowsocks manually for custom provider")), Self::Warp => Err(anyhow!("Cloudflare Warp supports only the Warp protocol")), + Self::None => unimplemented!("None provider runs no protocol"), _ => Err(anyhow!("Shadowsocks not supported")), } } diff --git a/vopono_core/src/config/vpn.rs b/vopono_core/src/config/vpn.rs index aa18f67..770ffbd 100644 --- a/vopono_core/src/config/vpn.rs +++ b/vopono_core/src/config/vpn.rs @@ -75,6 +75,7 @@ pub enum Protocol { OpenConnect, OpenFortiVpn, Warp, + None, } #[derive(Serialize, Deserialize)] diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index db62b02..0e5343a 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -44,6 +44,7 @@ pub struct NetworkNamespace { pub predown: Option, pub predown_user: Option, pub predown_group: Option, + pub config_file: Option, // Used to save config file path in lockfile } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -102,9 +103,14 @@ impl NetworkNamespace { predown, predown_user, predown_group, + config_file: None, }) } + pub fn set_config_file(&mut self, config_file: Option) { + self.config_file = config_file; + } + #[allow(clippy::too_many_arguments)] pub fn exec_no_block( netns_name: &str, diff --git a/vopono_core/src/util/mod.rs b/vopono_core/src/util/mod.rs index da474c3..2843949 100644 --- a/vopono_core/src/util/mod.rs +++ b/vopono_core/src/util/mod.rs @@ -433,15 +433,20 @@ pub fn get_config_from_alias(list_path: &Path, alias: &str) -> anyhow::Result Protocol { - let content = fs::read_to_string(config_file) - .context(format!("Reading VPN config file: {config_file:?}")) - .unwrap(); +pub fn get_config_file_protocol(config_file: &Path) -> anyhow::Result { + let content = fs::read_to_string(config_file).map_err(|e| { + anyhow!( + "Failed to read VPN config file: {}, err: {}", + config_file.to_string_lossy(), + e + ) + })?; + if content.contains("[Interface]") { - Protocol::Wireguard + Ok(Protocol::Wireguard) } else { // TODO: Don't always assume OpenVPN - Protocol::OpenVpn + Ok(Protocol::OpenVpn) } }