diff --git a/Cargo.toml b/Cargo.toml index e7a6bf2..0d56d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ directories-next = "2" log = "0.4" pretty_env_logger = "0.5" clap = { version = "4", features = ["derive"] } -which = "4" -dialoguer = "0.10" +which = "5" +dialoguer = "0.11" compound_duration = "1" signal-hook = "0.3" walkdir = "2" diff --git a/README.md b/README.md index ebf0795..2e7b0df 100644 --- a/README.md +++ b/README.md @@ -24,30 +24,31 @@ lynx all running through different VPN connections: ## Supported Providers -| Provider | OpenVPN support | Wireguard support | -| --------------------- | --------------- | ----------------- | -| Mullvad | ✅ | ✅ | -| AzireVPN | ✅ | ✅ | -| iVPN | ✅ | ✅ | -| PrivateInternetAccess | ✅ | ✅\*\* | -| ProtonVPN | ✅ | ❓\* | -| MozillaVPN | ❌ | ✅ | -| NordVPN | ✅ | ❌ | -| HMA (HideMyAss) | ✅ | ❌ | -| AirVPN | ✅ | ❌ | -| Cloudflare Warp\*\*\* | ❌ | ❌ | - -\* For ProtonVPN you can generate and download specific Wireguard config +| Provider | OpenVPN support | Wireguard support | +| ----------------------- | --------------- | ----------------- | +| Mullvad | ✅ | ✅ | +| AzireVPN | ✅ | ✅ | +| iVPN | ✅ | ✅ | +| PrivateInternetAccess | ✅ | ✅\* | +| ProtonVPN | ✅\*\* | ✅\*\*\* | +| MozillaVPN | ❌ | ✅ | +| NordVPN | ✅ | ❌ | +| HMA (HideMyAss) | ✅ | ❌ | +| AirVPN | ✅ | ❌ | +| Cloudflare Warp\*\*\*\* | ❌ | ❌ | + +\* Port forwarding is not currently supported for PrivateInternetAccess. PRs welcome. + +\*\* See the [User Guide](USERGUIDE.md) for authentication instructions for generating the OpenVPN config files via `vopono sync`. You must copy the authentication header of the form `AUTH-xxx=yyy` where `yyy` is the value of the `x-pm-uid` header in the same request when logged in, in your web browser. + +\*\*\* For ProtonVPN you can generate and download specific Wireguard config files, and use them as a custom provider config. See the [User Guide](USERGUIDE.md) -for details - note that port forwarding is currently not supported for ProtonVPN. +for details. [Port Forwarding](https://protonvpn.com/support/port-forwarding-manual-setup/) is supported with the `--protonvpn-port-forwarding` argument for both OpenVPN and Wireguard (with `--provider custom --custom xxx.conf --protocol wireguard` ), note for OpenVPN you must generate the OpenVPN config files appending `+pmp` to your OpenVPN username, and you must choose servers which support this feature (e.g. at the time of writing, the Romania servers do). The assigned port is then printed to the terminal where vopono was launched - this should then be set in any applications that require it. -\*\* Port forwarding is not currently supported for PrivateInternetAccess. -\*\*\* Cloudflare Warp uses its own protocol. Set both the provider and -protocol to `warp`. Note you must first register with `sudo warp-cli -register` and then run it once with `sudo warp-svc` and `sudo warp-cli -connect` outside of vopono. Please verify this works first before trying -it with vopono. +\*\*\*\* Cloudflare Warp uses its own protocol. Set both the provider and +protocol to `warp`. Note you must first register with `sudo warp-cli register` and then run it once with `sudo warp-svc` and `sudo warp-cli connect` outside of vopono. Please verify this works first before trying it with vopono. + ## Usage diff --git a/USERGUIDE.md b/USERGUIDE.md index 4c696ac..3580530 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -438,21 +438,60 @@ Mullvad wireguard usa-us52.conf ## VPN Provider specific details +### Mullvad + Mullvad users can use [mullvad.net/en/check](https://mullvad.net/en/check/) to check the security of their browser's connection. This was used with the Mullvad configuration to verify that there is no DNS leaking or BitTorrent leaking for both the OpenVPN and Wireguard configurations. + +### AzireVPN + AzireVPN users can use [their security check page](https://www.azirevpn.com/check) for the same (note the instructions on disabling WebRTC). I noticed that when using IPv6 with OpenVPN it incorrectly states you are not connected via AzireVPN though (Wireguard works correctly). +### ProtonVPN + +#### OpenVPN Sync and authentication + ProtonVPN users must log-in to the dashboard via a web browser during the `vopono sync` process in order to copy the `AUTH-*` cookie to access the OpenVPN configuration files, and the OpenVPN specific credentials to use them. +Note that there may be multiple `AUTH-xxx=yyy` cookies - the specific one we need is where `xxx` is equal to the value of the `x-pm-uid` header in the same request. + +![AUTH cookie example](protonvpn_header.png) + +#### Wireguard servers + +Due to the way Wireguard configuration generation is handled, this should be +generated online and then used as a custom configuration, e.g.: + +```bash +$ vopono -v exec --provider custom --custom testwg-UK-17.conf --protocol wireguard --protonvpn-port-forwarding firefox-developer-edition +``` + +#### Port Forwarding + +Port forwarding can be enabled with the `--protonvpn-port-forwarding` argument, but requires using a server that supports port forwarding. + +Note for OpenVPN you must generate the OpenVPN config files appending `+pmp` to your OpenVPN username (i.e. what will be written to `~/.config/vopono/proton/openvpn/auth.txt`) + +Note the usual `-o` / `--open-ports` argument has no effect here as we only know the port number assigned after connecting to ProtonVPN. + +The port you are allocated will then be printed to the console like: +``` + 2023-11-04T14:47:31.416Z INFO vopono::exec > ProtonVPN Port Forwarding on port 62508 +``` + +And that is the port you would then set up in applications that require it. + +### Cloudflare Warp + Cloudflare Warp users must first register with Warp via the CLI client: ``` $ sudo warp-cli register @@ -470,17 +509,6 @@ $ vopono -v exec --no-killswitch --provider warp --protocol warp firefox-develop ### VPN Provider limitations -#### ProtonVPN - -Due to the way Wireguard configuration is handled, this should be -generated online and then used as a custom configuration, e.g.: - -```bash -$ vopono -v exec --provider custom --custom testwg-UK-17.conf --protocol wireguard firefox-developer-edition -``` - -Note that port forwarding is currently not supported for ProtonVPN. - #### PrivateInternetAccess Wireguard support for PrivateInternetAccess (PIA) requires the use of a diff --git a/protonvpn_header.png b/protonvpn_header.png new file mode 100644 index 0000000..108fb45 Binary files /dev/null and b/protonvpn_header.png differ diff --git a/src/args.rs b/src/args.rs index 1081319..23668ac 100644 --- a/src/args.rs +++ b/src/args.rs @@ -212,6 +212,10 @@ pub struct ExecCommand { /// Useful for accessing services on the host locally #[clap(long = "allow-host-access")] pub allow_host_access: bool, + + /// Enable port forwarding for ProtonVPN connections + #[clap(long = "protonvpn-port-forwarding")] + pub protonvpn_port_forwarding: bool, } #[derive(Parser)] diff --git a/src/cli_client.rs b/src/cli_client.rs index 7128d85..f9fd61d 100644 --- a/src/cli_client.rs +++ b/src/cli_client.rs @@ -38,26 +38,23 @@ impl UiClient for CliClient { } fn get_input(&self, inp: Input) -> anyhow::Result { - let mut d = dialoguer::Input::::new(); - - d.with_prompt(&inp.prompt); + let mut d = dialoguer::Input::::new().with_prompt(&inp.prompt); if inp.validator.is_some() { - d.validate_with(inp.validator.unwrap()); + d = d.validate_with(inp.validator.unwrap()); }; Ok(d.interact()?) } fn get_input_numeric_u16(&self, inp: InputNumericu16) -> anyhow::Result { - let mut d = dialoguer::Input::::new(); - d.with_prompt(&inp.prompt); + let mut d = dialoguer::Input::::new().with_prompt(&inp.prompt); if inp.default.is_some() { - d.default(inp.default.unwrap()); + d = d.default(inp.default.unwrap()); } if inp.validator.is_some() { - d.validate_with(inp.validator.unwrap()); + d = d.validate_with(inp.validator.unwrap()); } Ok(d.interact()?) @@ -66,9 +63,9 @@ impl UiClient for CliClient { fn get_password(&self, pw: Password) -> anyhow::Result { let mut req = dialoguer::Password::new(); if pw.confirm { - req.with_confirmation("Confirm password", "Passwords did not match"); + req = req.with_confirmation("Confirm password", "Passwords did not match"); }; - req.with_prompt(pw.prompt); + req = req.with_prompt(pw.prompt); Ok(req.interact()?) } } diff --git a/src/exec.rs b/src/exec.rs index f245720..f7d0871 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -14,6 +14,7 @@ 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::natpmpc::Natpmpc; use vopono_core::network::netns::NetworkNamespace; use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface}; use vopono_core::network::shadowsocks::uses_shadowsocks; @@ -56,10 +57,9 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .map(|x| x.to_variant()) .ok_or_else(|| anyhow!("")) .or_else(|_| { - vopono_config_settings.get("firewall").map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + vopono_config_settings + .get("firewall") + .map_err(|_e| anyhow!("Failed to read config file")) }) .or_else(|_x| vopono_core::util::get_firewall())?; @@ -67,10 +67,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let custom_config = command.custom_config.clone().or_else(|| { vopono_config_settings .get("custom_config") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }); @@ -78,10 +75,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let custom_netns_name = command.custom_netns_name.clone().or_else(|| { vopono_config_settings .get("custom_netns_name") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }); @@ -89,29 +83,20 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let mut open_hosts = command.open_hosts.clone().or_else(|| { vopono_config_settings .get("open_hosts") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .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| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .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| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }); @@ -119,10 +104,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> 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") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }); @@ -130,10 +112,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> 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") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() .or_else(|| std::env::var("SUDO_USER").ok()) } else { @@ -144,10 +123,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> 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") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() } else { command.group @@ -157,23 +133,28 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> 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") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() } else { command.working_directory }; + // Port forwarding for ProtonVPN + let protonvpn_port_forwarding = if !command.protonvpn_port_forwarding { + vopono_config_settings + .get("protonvpn-port-forwarding") + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + .unwrap_or(false) + } else { + command.protonvpn_port_forwarding + }; + // 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| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }); @@ -199,10 +180,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .or_else(|| { vopono_config_settings .get("server") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }) .or_else(|| Some(String::new())) @@ -216,10 +194,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .or_else(|| { vopono_config_settings .get("provider") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }) .expect( @@ -235,8 +210,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .or_else(|| { vopono_config_settings .get("server") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); + .map_err(|_e| { anyhow!("Failed to read config file") }) .ok() @@ -252,10 +226,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .or_else(|| { vopono_config_settings .get("protocol") - .map_err(|e| { - debug!("vopono config.toml: {:?}", e); - anyhow!("Failed to read config file") - }) + .map_err(|_e| anyhow!("Failed to read config file")) .ok() }) .unwrap_or_else(|| provider.get_dyn_provider().default_protocol()); @@ -525,7 +496,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // for the PostUp script and the application: std::env::set_var( "VOPONO_NS_IP", - &ns.veth_pair_ips.as_ref().unwrap().namespace_ip.to_string(), + ns.veth_pair_ips.as_ref().unwrap().namespace_ip.to_string(), ); // Run PostUp script (if any) @@ -560,17 +531,33 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // 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(), + ns.veth_pair_ips.as_ref().unwrap().host_ip.to_string(), ); let ns = ns.write_lockfile(&command.application)?; + let natpmpc = if protonvpn_port_forwarding { + vopono_core::util::open_hosts( + &ns, + vec![vopono_core::network::natpmpc::PROTONVPN_GATEWAY], + firewall, + )?; + Some(Natpmpc::new(&ns)?) + } else { + None + }; + + if let Some(pmpc) = natpmpc.as_ref() { + vopono_core::util::open_ports(&ns, &[pmpc.local_port], firewall)?; + } + let application = ApplicationWrapper::new( &ns, &command.application, user, group, working_directory.map(PathBuf::from), + natpmpc, )?; // Launch TCP proxy server on other threads if forwarding ports @@ -598,6 +585,10 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> "Application {} launched in network namespace {} with pid {}", &command.application, &ns.name, pid ); + + if let Some(pmpc) = application.protonvpn_port_forwarding.as_ref() { + info!("ProtonVPN Port Forwarding on port {}", pmpc.local_port) + } let output = application.wait_with_output()?; io::stdout().write_all(output.stdout.as_slice())?; diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index 4468747..fb6a1d4 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["vopono", "vpn", "wireguard", "openvpn", "netns"] anyhow = "1" directories-next = "2" log = "0.4" -which = "4" +which = "5" users = "0.11" nix = { version = "0.27", features = ["user", "signal", "fs", "process"] } serde = { version = "1", features = ["derive", "std"] } @@ -23,7 +23,7 @@ regex = "1" ron = "0.8" walkdir = "2" rand = "0.8" -toml = "0.7" +toml = "0.8" ipnet = { version = "2", features = ["serde"] } reqwest = { default-features = false, version = "0.11", features = [ "blocking", diff --git a/vopono_core/src/config/providers/mozilla/wireguard.rs b/vopono_core/src/config/providers/mozilla/wireguard.rs index e9c00db..335d818 100644 --- a/vopono_core/src/config/providers/mozilla/wireguard.rs +++ b/vopono_core/src/config/providers/mozilla/wireguard.rs @@ -151,7 +151,7 @@ impl WireguardProvider for MozillaVPN { // Get user info again in case we uploaded new key let user_info: User = client - .get(&format!("{}/vpn/account", self.base_url())) + .get(format!("{}/vpn/account", self.base_url())) .bearer_auth(login.token) .send()? .json()?; diff --git a/vopono_core/src/config/providers/mullvad/wireguard.rs b/vopono_core/src/config/providers/mullvad/wireguard.rs index 1b3b535..e34fbba 100644 --- a/vopono_core/src/config/providers/mullvad/wireguard.rs +++ b/vopono_core/src/config/providers/mullvad/wireguard.rs @@ -49,7 +49,7 @@ impl WireguardProvider for Mullvad { let username = self.request_mullvad_username(uiclient)?; let auth: AuthToken = client - .get(&format!("https://api.mullvad.net/www/accounts/{username}/")) + .get(format!("https://api.mullvad.net/www/accounts/{username}/")) .send()? .json()?; diff --git a/vopono_core/src/config/providers/protonvpn/openvpn.rs b/vopono_core/src/config/providers/protonvpn/openvpn.rs index 5410006..f2692bf 100644 --- a/vopono_core/src/config/providers/protonvpn/openvpn.rs +++ b/vopono_core/src/config/providers/protonvpn/openvpn.rs @@ -61,7 +61,7 @@ impl OpenVpnProvider for ProtonVPN { fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)> { let username = uiclient.get_input(Input { prompt: - "ProtonVPN OpenVPN username (see: https://account.protonvpn.com/account#openvpn )" + "ProtonVPN OpenVPN username (see: https://account.protonvpn.com/account#openvpn ) - add +pmp suffix if using --protonvpn-port-forwarding - note not all servers support this feature" .to_string(), validator: None, })?; @@ -94,16 +94,21 @@ impl OpenVpnProvider for ProtonVPN { ); let auth_cookie: &'static str = Box::leak(uiclient.get_input(Input { - prompt: "Please log-in at https://account.protonvpn.com/dashboard and then visit https://account.protonvpn.com/api/vpn/v2/users and copy the value of the cookie starting with \"AUTH-\" in the request from your browser's network request inspector".to_owned(), + prompt: "Please log-in at https://account.protonvpn.com/dashboard and then visit https://account.protonvpn.com/account and copy the value of the cookie of the form \"AUTH-xxx=yyy\" where xxx is equal to the value of the \"x-pm-uid\" request header, in the request from your browser's network request inspector (check the request it makes to https://account.protonvpn.com/api/vpn for example). Note there may be multiple AUTH-xxx=yyy request headers, copy the one where xxx is equal to the value of the x-pm-uid header.".to_owned(), validator: Some(Box::new(|s: &String| if s.starts_with("AUTH-") {Ok(())} else {Err("AUTH cookie must start with AUTH-".to_owned())})) })?.replace(';', "").trim().to_owned().into_boxed_str()); debug!("Using AUTH cookie: {}", &auth_cookie); - let re = Regex::new("AUTH-([^=]+)=").unwrap(); - let uid = re + let uid_re = Regex::new("AUTH-([^=]+)=").unwrap(); + let uid = uid_re .captures(auth_cookie) .and_then(|c| c.get(1)) - .ok_or(anyhow!("Failed to parse auth cookie"))?; + .ok_or(anyhow!("Failed to parse uid from auth cookie"))?; + info!( + "x-pm-uid should be {} according to AUTH cookie: {}", + uid.as_str(), + auth_cookie + ); let url = self.build_url(&config_choice, &tier, &protocol)?; let mut headers = HeaderMap::new(); diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index af436af..5ba017a 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -1,11 +1,12 @@ use std::path::PathBuf; -use super::netns::NetworkNamespace; +use super::{natpmpc::Natpmpc, netns::NetworkNamespace}; use crate::util::get_all_running_process_names; use log::warn; pub struct ApplicationWrapper { pub handle: std::process::Child, + pub protonvpn_port_forwarding: Option, } impl ApplicationWrapper { @@ -15,6 +16,7 @@ impl ApplicationWrapper { user: Option, group: Option, working_directory: Option, + protonvpn_port_forwarding: Option, ) -> anyhow::Result { let running_processes = get_all_running_process_names(); let app_vec = application.split_whitespace().collect::>(); @@ -37,7 +39,8 @@ impl ApplicationWrapper { } } - let handle = netns.exec_no_block( + let handle = NetworkNamespace::exec_no_block( + &netns.name, app_vec.as_slice(), user, group, @@ -46,7 +49,10 @@ impl ApplicationWrapper { false, working_directory, )?; - Ok(Self { handle }) + Ok(Self { + handle, + protonvpn_port_forwarding, + }) } pub fn wait_with_output(self) -> anyhow::Result { diff --git a/vopono_core/src/network/firewall.rs b/vopono_core/src/network/firewall.rs index 065774f..d97ee79 100644 --- a/vopono_core/src/network/firewall.rs +++ b/vopono_core/src/network/firewall.rs @@ -11,42 +11,51 @@ pub enum Firewall { pub fn disable_ipv6(netns: &NetworkNamespace, firewall: Firewall) -> anyhow::Result<()> { match firewall { Firewall::IpTables => { - netns.exec(&["ip6tables", "-P", "INPUT", "DROP"])?; - netns.exec(&["ip6tables", "-I", "INPUT", "-j", "DROP"])?; - netns.exec(&["ip6tables", "-P", "FORWARD", "DROP"])?; - netns.exec(&["ip6tables", "-I", "FORWARD", "-j", "DROP"])?; - netns.exec(&["ip6tables", "-P", "OUTPUT", "DROP"])?; - netns.exec(&["ip6tables", "-I", "OUTPUT", "-j", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-P", "INPUT", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-I", "INPUT", "-j", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-P", "FORWARD", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-I", "FORWARD", "-j", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-P", "OUTPUT", "DROP"])?; + NetworkNamespace::exec(&netns.name, &["ip6tables", "-I", "OUTPUT", "-j", "DROP"])?; } Firewall::NfTables => { - netns.exec(&["nft", "add", "table", "ip6", &netns.name])?; - netns.exec(&[ - "nft", - "add", - "chain", - "ip6", + NetworkNamespace::exec(&netns.name, &["nft", "add", "table", "ip6", &netns.name])?; + NetworkNamespace::exec( &netns.name, - "drop_ipv6_input", - "{ type filter hook input priority -1 ; policy drop; }", - ])?; - netns.exec(&[ - "nft", - "add", - "chain", - "ip6", + &[ + "nft", + "add", + "chain", + "ip6", + &netns.name, + "drop_ipv6_input", + "{ type filter hook input priority -1 ; policy drop; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "drop_ipv6_output", - "{ type filter hook output priority -1 ; policy drop; }", - ])?; - netns.exec(&[ - "nft", - "add", - "chain", - "ip6", + &[ + "nft", + "add", + "chain", + "ip6", + &netns.name, + "drop_ipv6_output", + "{ type filter hook output priority -1 ; policy drop; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "drop_ipv6_forward", - "{ type filter hook forward priority -1 ; policy drop; }", - ])?; + &[ + "nft", + "add", + "chain", + "ip6", + &netns.name, + "drop_ipv6_forward", + "{ type filter hook forward priority -1 ; policy drop; }", + ], + )?; } } Ok(()) diff --git a/vopono_core/src/network/host_masquerade.rs b/vopono_core/src/network/host_masquerade.rs index 4139ee4..b63bf78 100644 --- a/vopono_core/src/network/host_masquerade.rs +++ b/vopono_core/src/network/host_masquerade.rs @@ -5,7 +5,7 @@ use anyhow::Context; use log::debug; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct HostMasquerade { ip_mask: String, interface: NetworkInterface, @@ -122,7 +122,7 @@ impl Drop for HostMasquerade { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct FirewallException { host_interface: NetworkInterface, ns_interface: NetworkInterface, diff --git a/vopono_core/src/network/mod.rs b/vopono_core/src/network/mod.rs index f286795..e7fbb7e 100644 --- a/vopono_core/src/network/mod.rs +++ b/vopono_core/src/network/mod.rs @@ -2,6 +2,7 @@ pub mod application_wrapper; pub mod dns_config; pub mod firewall; pub mod host_masquerade; +pub mod natpmpc; pub mod netns; pub mod network_interface; pub mod openconnect; diff --git a/vopono_core/src/network/natpmpc.rs b/vopono_core/src/network/natpmpc.rs new file mode 100644 index 0000000..4910b3e --- /dev/null +++ b/vopono_core/src/network/natpmpc.rs @@ -0,0 +1,120 @@ +use anyhow::Context; +use regex::Regex; +use std::sync::mpsc::{self, Receiver}; +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::mpsc::Sender, + thread::JoinHandle, +}; + +use super::netns::NetworkNamespace; + +// TODO: Move this to ProtonVPN provider +pub const PROTONVPN_GATEWAY: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 2, 0, 1)); + +/// Used to provide port forwarding for ProtonVPN +pub struct Natpmpc { + pub local_port: u16, + loop_thread_handle: Option>, + send_channel: Sender, +} + +impl Natpmpc { + pub fn new(ns: &NetworkNamespace) -> anyhow::Result { + let gateway_str = PROTONVPN_GATEWAY.to_string(); + + // Check output for readnatpmpresponseorretry returned 0 (OK) + // If receive readnatpmpresponseorretry returned -7 + // Then prompt user to choose different gateway + let output = + NetworkNamespace::exec_with_output(&ns.name, &["natpmpc", "-g", &gateway_str])?; + if !output.status.success() { + log::error!("natpmpc failed - likely that this server does not support port forwarding, please choose another server"); + anyhow::bail!("natpmpc failed - likely that this server does not support port forwarding, please choose another server") + } + + let port = Self::refresh_port(&ns.name)?; + + let (send, recv) = mpsc::channel::(); + + let ns_name = ns.name.clone(); + let handle = std::thread::spawn(move || Self::thread_loop(ns_name, recv)); + + log::info!("ProtonVPN forwarded local port: {port}"); + Ok(Self { + local_port: port, + loop_thread_handle: Some(handle), + send_channel: send, + }) + } + + fn refresh_port(ns_name: &str) -> anyhow::Result { + let gateway_str = PROTONVPN_GATEWAY.to_string(); + // TODO: Cache regex + let re = Regex::new(r"Mapped public port (?P\d{1,5}) protocol").unwrap(); + // Read Mapped public port 61057 protocol UDP + let udp_output = NetworkNamespace::exec_with_output( + ns_name, + &["natpmpc", "-a", "1", "0", "udp", "60", "-g", &gateway_str], + )?; + let udp_port: u16 = re + .captures(String::from_utf8_lossy(&udp_output.stdout).as_ref()) + .context("Failed to read port from natpmpc output - no captures")? + .get(1) + .context("Failed to read port from natpmpc output - no port")? + .as_str() + .parse()?; + // Mapped public port 61057 protocol TCP + let tcp_output = NetworkNamespace::exec_with_output( + ns_name, + &["natpmpc", "-a", "1", "0", "tcp", "60", "-g", &gateway_str], + )?; + let tcp_port: u16 = re + .captures(String::from_utf8_lossy(&tcp_output.stdout).as_ref()) + .context("Failed to read port from natpmpc output - no captures")? + .get(1) + .context("Failed to read port from natpmpc output - no port")? + .as_str() + .parse()?; + if udp_port != tcp_port { + log::error!("natpmpc assigned UDP port: {udp_port} did not equal TCP port: {tcp_port}"); + anyhow::bail!( + "natpmpc assigned UDP port: {udp_port} did not equal TCP port: {tcp_port}" + ) + } + + Ok(udp_port) + } + + // Spawn thread to repeat above every 45 seconds + fn thread_loop(netns_name: String, recv: Receiver) { + loop { + let resp = recv.recv_timeout(std::time::Duration::from_secs(45)); + if resp.is_ok() { + log::debug!("Thread exiting..."); + return; + } else { + let port = Self::refresh_port(&netns_name); + match port { + Err(e) => { + log::error!("Thread failed to refresh port: {e:?}"); + return; + } + Ok(p) => log::debug!("Thread refreshed port: {p}"), + } + + // TODO: Communicate port change via channel? + } + } + } +} + +impl Drop for Natpmpc { + fn drop(&mut self) { + let handle = self.loop_thread_handle.take(); + if let Some(h) = handle { + self.send_channel.send(true).ok(); + h.join().ok(); + } + } +} diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index 0f79fd7..db62b02 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -13,7 +13,7 @@ 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 anyhow::Context; +use anyhow::{anyhow, Context}; use log::{debug, info, warn}; use nix::unistd; use serde::{Deserialize, Serialize}; @@ -21,7 +21,7 @@ use std::fs::File; use std::io::Write; use std::net::IpAddr; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Command, Output, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Serialize, Deserialize, Debug)] @@ -46,7 +46,7 @@ pub struct NetworkNamespace { pub predown_group: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VethPairIPs { pub host_ip: IpAddr, pub namespace_ip: IpAddr, @@ -107,7 +107,7 @@ impl NetworkNamespace { #[allow(clippy::too_many_arguments)] pub fn exec_no_block( - &self, + netns_name: &str, command: &[&str], user: Option, group: Option, @@ -117,7 +117,7 @@ impl NetworkNamespace { set_dir: Option, ) -> anyhow::Result { let mut handle = Command::new("ip"); - handle.args(["netns", "exec", &self.name]); + handle.args(["netns", "exec", netns_name]); if let Some(cdir) = set_dir { handle.current_dir(cdir); } @@ -155,7 +155,7 @@ impl NetworkNamespace { debug!( "ip netns exec {}{} {}", - &self.name, + netns_name, sudo_string.unwrap_or_else(|| String::from("")), command.join(" ") ); @@ -163,16 +163,24 @@ impl NetworkNamespace { Ok(handle) } - pub fn exec(&self, command: &[&str]) -> anyhow::Result<()> { - self.exec_no_block(command, None, None, false, false, false, None)? - .wait()?; + pub fn exec(netns_name: &str, command: &[&str]) -> anyhow::Result<()> { + Self::exec_no_block(netns_name, command, None, None, false, false, false, None)?.wait()?; Ok(()) } + pub fn exec_with_output(netns_name: &str, command: &[&str]) -> anyhow::Result { + Self::exec_no_block(netns_name, command, None, None, false, true, false, None)? + .wait_with_output() + .map_err(|e| anyhow!("Process Output error: {e:?}")) + } + pub fn add_loopback(&self) -> anyhow::Result<()> { - self.exec(&["ip", "addr", "add", "127.0.0.1/8", "dev", "lo"]) - .with_context(|| format!("Failed to add loopback adapter in netns: {}", &self.name))?; - self.exec(&["ip", "link", "set", "lo", "up"]) + Self::exec( + &self.name, + &["ip", "addr", "add", "127.0.0.1/8", "dev", "lo"], + ) + .with_context(|| format!("Failed to add loopback adapter in netns: {}", &self.name))?; + Self::exec(&self.name, &["ip", "link", "set", "lo", "up"]) .with_context(|| format!("Failed to start networking in netns: {}", &self.name))?; Ok(()) } @@ -213,32 +221,41 @@ impl NetworkNamespace { format!("Failed to assign static IP to veth destination: {veth_dest}") })?; - self.exec(&["ip", "addr", "add", &veth_source_ip, "dev", veth_source]) - .with_context(|| format!("Failed to assign static IP to veth source: {veth_source}"))?; - self.exec(&[ - "ip", - "route", - "add", - "default", - "via", - &ip_nosub, - "dev", - veth_source, - ]) + Self::exec( + &self.name, + &["ip", "addr", "add", &veth_source_ip, "dev", veth_source], + ) + .with_context(|| format!("Failed to assign static IP to veth source: {veth_source}"))?; + Self::exec( + &self.name, + &[ + "ip", + "route", + "add", + "default", + "via", + &ip_nosub, + "dev", + veth_source, + ], + ) .with_context(|| format!("Failed to assign static IP to veth source: {veth_source}"))?; if let Some(my_hosts) = hosts { for host in my_hosts { - self.exec(&[ - "ip", - "route", - "add", - &host.to_string(), - "via", - &ip_nosub, - "dev", - veth_source, - ]) + Self::exec( + &self.name, + &[ + "ip", + "route", + "add", + &host.to_string(), + "via", + &ip_nosub, + "dev", + veth_source, + ], + ) .with_context(|| { format!("Failed to assign hosts route {host} to veth source: {veth_source}") })?; @@ -246,7 +263,7 @@ impl NetworkNamespace { } if allow_host_access { - self.exec(&[ + Self::exec(&self.name, &[ "ip", "route", "add", diff --git a/vopono_core/src/network/openconnect.rs b/vopono_core/src/network/openconnect.rs index 7fe8d51..6c2118b 100644 --- a/vopono_core/src/network/openconnect.rs +++ b/vopono_core/src/network/openconnect.rs @@ -49,9 +49,17 @@ impl OpenConnect { command_vec.push(server.as_ref()); } - let handle = netns - .exec_no_block(&command_vec, None, None, false, false, true, None) - .context("Failed to launch OpenConnect - is openconnect installed?")?; + let handle = NetworkNamespace::exec_no_block( + &netns.name, + &command_vec, + None, + None, + false, + false, + true, + None, + ) + .context("Failed to launch OpenConnect - is openconnect installed?")?; handle .stdin diff --git a/vopono_core/src/network/openfortivpn.rs b/vopono_core/src/network/openfortivpn.rs index e3c4fee..495ead4 100644 --- a/vopono_core/src/network/openfortivpn.rs +++ b/vopono_core/src/network/openfortivpn.rs @@ -47,9 +47,17 @@ impl OpenFortiVpn { std::fs::remove_file(&pppd_log).ok(); // TODO - better handle forwarding output when blocking on password entry (no newline!) - let mut handle = netns - .exec_no_block(&command_vec, None, None, false, true, false, None) - .context("Failed to launch OpenFortiVPN - is openfortivpn installed?")?; + let mut handle = NetworkNamespace::exec_no_block( + &netns.name, + &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(); @@ -81,15 +89,18 @@ impl OpenFortiVpn { let remote_peer = get_remote_peer(&pppd_log)?; debug!("Found OpenFortiVPN route: {:?}", remote_peer); - netns.exec(&["ip", "route", "del", "default"])?; - netns.exec(&[ - "ip", - "route", - "add", - "default", - "via", - &remote_peer.to_string(), - ])?; + NetworkNamespace::exec(&netns.name, &["ip", "route", "del", "default"])?; + NetworkNamespace::exec( + &netns.name, + &[ + "ip", + "route", + "add", + "default", + "via", + &remote_peer.to_string(), + ], + )?; let dns = get_dns(&buffer)?; let dns_ip: Vec = (dns.0).into_iter().map(IpAddr::from).collect(); diff --git a/vopono_core/src/network/openvpn.rs b/vopono_core/src/network/openvpn.rs index fa4939d..3c5b549 100644 --- a/vopono_core/src/network/openvpn.rs +++ b/vopono_core/src/network/openvpn.rs @@ -63,9 +63,9 @@ impl OpenVpn { ]) .to_vec(); - if auth_file.is_some() { + if let Some(af_ref) = auth_file.as_ref() { command_vec.push("--auth-user-pass"); - command_vec.push(auth_file.as_ref().unwrap().as_os_str().to_str().unwrap()); + command_vec.push(af_ref.as_os_str().to_str().unwrap()); } let ipv6_disabled = std::fs::read_to_string("/sys/module/ipv6/parameters/disable") @@ -100,17 +100,17 @@ impl OpenVpn { debug!("Found remotes: {:?}", &remotes); let working_dir = PathBuf::from(config_file_path.parent().unwrap()); - let handle = netns - .exec_no_block( - &command_vec, - None, - None, - true, - false, - false, - Some(working_dir), - ) - .context("Failed to launch OpenVPN - is openvpn installed?")?; + let handle = NetworkNamespace::exec_no_block( + &netns.name, + &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); @@ -239,23 +239,35 @@ pub fn killswitch( }; for ipcmd in ipcmds { - netns.exec(&[ipcmd, "-P", "INPUT", "DROP"])?; - netns.exec(&[ipcmd, "-P", "FORWARD", "DROP"])?; - netns.exec(&[ipcmd, "-P", "OUTPUT", "DROP"])?; - netns.exec(&[ - ipcmd, - "-A", - "INPUT", - "-m", - "conntrack", - "--ctstate", - "RELATED,ESTABLISHED", - "-j", - "ACCEPT", - ])?; - netns.exec(&[ipcmd, "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"])?; - netns.exec(&[ipcmd, "-A", "INPUT", "-i", "tun+", "-j", "ACCEPT"])?; - netns.exec(&[ipcmd, "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"])?; + NetworkNamespace::exec(&netns.name, &[ipcmd, "-P", "INPUT", "DROP"])?; + NetworkNamespace::exec(&netns.name, &[ipcmd, "-P", "FORWARD", "DROP"])?; + NetworkNamespace::exec(&netns.name, &[ipcmd, "-P", "OUTPUT", "DROP"])?; + NetworkNamespace::exec( + &netns.name, + &[ + ipcmd, + "-A", + "INPUT", + "-m", + "conntrack", + "--ctstate", + "RELATED,ESTABLISHED", + "-j", + "ACCEPT", + ], + )?; + NetworkNamespace::exec( + &netns.name, + &[ipcmd, "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"], + )?; + NetworkNamespace::exec( + &netns.name, + &[ipcmd, "-A", "INPUT", "-i", "tun+", "-j", "ACCEPT"], + )?; + NetworkNamespace::exec( + &netns.name, + &[ipcmd, "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"], + )?; // TODO: Tidy this up - remote can be IPv4 or IPv6 address or hostname for remote in remotes { @@ -265,72 +277,87 @@ pub fn killswitch( // resolution working Host::IPv4(ip) => { if ipcmd == "iptables" { - netns.exec(&[ - ipcmd, - "-A", - "OUTPUT", - "-p", - &remote.protocol.to_string(), - "-m", - &remote.protocol.to_string(), - "-d", - &ip.to_string(), - "--dport", - port_str.as_str(), - "-j", - "ACCEPT", - ])?; + NetworkNamespace::exec( + &netns.name, + &[ + ipcmd, + "-A", + "OUTPUT", + "-p", + &remote.protocol.to_string(), + "-m", + &remote.protocol.to_string(), + "-d", + &ip.to_string(), + "--dport", + port_str.as_str(), + "-j", + "ACCEPT", + ], + )?; } } Host::IPv6(ip) => { if ipcmd == "ip6tables" { - netns.exec(&[ + NetworkNamespace::exec( + &netns.name, + &[ + ipcmd, + "-A", + "OUTPUT", + "-p", + &remote.protocol.to_string(), + "-m", + &remote.protocol.to_string(), + "-d", + &ip.to_string(), + "--dport", + port_str.as_str(), + "-j", + "ACCEPT", + ], + )?; + } + } + Host::Hostname(_name) => { + NetworkNamespace::exec( + &netns.name, + &[ ipcmd, "-A", "OUTPUT", "-p", &remote.protocol.to_string(), + // "-d", + // &name.to_string(), "-m", &remote.protocol.to_string(), - "-d", - &ip.to_string(), "--dport", port_str.as_str(), "-j", "ACCEPT", - ])?; - } - } - Host::Hostname(_name) => { - netns.exec(&[ - ipcmd, - "-A", - "OUTPUT", - "-p", - &remote.protocol.to_string(), - // "-d", - // &name.to_string(), - "-m", - &remote.protocol.to_string(), - "--dport", - port_str.as_str(), - "-j", - "ACCEPT", - ])?; + ], + )?; } } } - netns.exec(&[ipcmd, "-A", "OUTPUT", "-o", "tun+", "-j", "ACCEPT"])?; - netns.exec(&[ - ipcmd, - "-A", - "OUTPUT", - "-j", - "REJECT", - "--reject-with", - "icmp-net-unreachable", - ])?; + NetworkNamespace::exec( + &netns.name, + &[ipcmd, "-A", "OUTPUT", "-o", "tun+", "-j", "ACCEPT"], + )?; + NetworkNamespace::exec( + &netns.name, + &[ + ipcmd, + "-A", + "OUTPUT", + "-j", + "REJECT", + "--reject-with", + "icmp-net-unreachable", + ], + )?; } } Firewall::NfTables => { @@ -338,83 +365,104 @@ pub fn killswitch( crate::network::firewall::disable_ipv6(netns, firewall)?; } // TODO: - netns.exec(&["nft", "add", "table", "inet", &netns.name])?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + NetworkNamespace::exec(&netns.name, &["nft", "add", "table", "inet", &netns.name])?; + NetworkNamespace::exec( &netns.name, - "input", - "{ type filter hook input priority 100 ; policy drop; }", - ])?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "input", + "{ type filter hook input priority 100 ; policy drop; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "forward", - "{ type filter hook forward priority 100 ; policy drop; }", - ])?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "forward", + "{ type filter hook forward priority 100 ; policy drop; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "output", - "{ type filter hook output priority 100 ; policy drop; }", - ])?; - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "output", + "{ type filter hook output priority 100 ; policy drop; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "input", - "ct", - "state", - "related,established", - "counter", - "accept", - ])?; - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "input", + "ct", + "state", + "related,established", + "counter", + "accept", + ], + )?; + NetworkNamespace::exec( &netns.name, - "input", - "iifname", - "\"lo\"", - "counter", - "accept", - ])?; - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "input", + "iifname", + "\"lo\"", + "counter", + "accept", + ], + )?; + NetworkNamespace::exec( &netns.name, - "input", - "iifname", - "\"tun*\"", - "counter", - "accept", - ])?; - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "input", + "iifname", + "\"tun*\"", + "counter", + "accept", + ], + )?; + NetworkNamespace::exec( &netns.name, - "output", - "oifname", - "\"lo\"", - "counter", - "accept", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "oifname", + "\"lo\"", + "counter", + "accept", + ], + )?; for remote in remotes { let port_str = format!("{}", remote.port); @@ -422,90 +470,105 @@ pub fn killswitch( // TODO: Fix this to specify destination address - but need hostname // resolution working Host::IPv4(ip) => { - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - "ip", - "daddr", - &ip.to_string(), - &remote.protocol.to_string(), - "dport", - port_str.as_str(), - "counter", - "accept", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "ip", + "daddr", + &ip.to_string(), + &remote.protocol.to_string(), + "dport", + port_str.as_str(), + "counter", + "accept", + ], + )?; } Host::IPv6(ip) => { - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - "ip6", - "daddr", - &ip.to_string(), - &remote.protocol.to_string(), - "dport", - port_str.as_str(), - "counter", - "accept", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "ip6", + "daddr", + &ip.to_string(), + &remote.protocol.to_string(), + "dport", + port_str.as_str(), + "counter", + "accept", + ], + )?; } Host::Hostname(_name) => { // TODO: Does this work with nftables? - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - // "ip", - // "daddr", - // &name.to_string(), - &remote.protocol.to_string(), - "dport", - port_str.as_str(), - "counter", - "accept", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + // "ip", + // "daddr", + // &name.to_string(), + &remote.protocol.to_string(), + "dport", + port_str.as_str(), + "counter", + "accept", + ], + )?; } } } - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - "oifname", - "\"tun*\"", - "counter", - "accept", - ])?; - - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "oifname", + "\"tun*\"", + "counter", + "accept", + ], + )?; + + NetworkNamespace::exec( &netns.name, - "output", - "counter", - "reject", - "with", - "icmp", - "type", - "net-unreachable", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "counter", + "reject", + "with", + "icmp", + "type", + "net-unreachable", + ], + )?; } } Ok(()) diff --git a/vopono_core/src/network/shadowsocks.rs b/vopono_core/src/network/shadowsocks.rs index 5460f44..9b2c496 100644 --- a/vopono_core/src/network/shadowsocks.rs +++ b/vopono_core/src/network/shadowsocks.rs @@ -66,9 +66,17 @@ impl Shadowsocks { encrypt_method, ]; - let handle = netns - .exec_no_block(&command_vec, None, None, true, false, false, None) - .context("Failed to launch Shadowsocks - is shadowsocks-libev installed?")?; + let handle = NetworkNamespace::exec_no_block( + &netns.name, + &command_vec, + None, + None, + true, + false, + false, + None, + ) + .context("Failed to launch Shadowsocks - is shadowsocks-libev installed?")?; Ok(Self { pid: handle.id() }) } diff --git a/vopono_core/src/network/warp.rs b/vopono_core/src/network/warp.rs index add954d..7e167dc 100644 --- a/vopono_core/src/network/warp.rs +++ b/vopono_core/src/network/warp.rs @@ -31,9 +31,17 @@ impl Warp { info!("Launching Warp..."); - let handle = netns - .exec_no_block(&["warp-svc"], None, None, false, false, false, None) - .context("Failed to launch warp-svc - is waro-svc installed?")?; + let handle = NetworkNamespace::exec_no_block( + &netns.name, + &["warp-svc"], + None, + None, + false, + false, + false, + None, + ) + .context("Failed to launch warp-svc - is waro-svc installed?")?; let id = handle.id(); diff --git a/vopono_core/src/network/wireguard.rs b/vopono_core/src/network/wireguard.rs index 1944507..1414c3d 100644 --- a/vopono_core/src/network/wireguard.rs +++ b/vopono_core/src/network/wireguard.rs @@ -98,11 +98,16 @@ impl Wireguard { .to_string(); assert!(if_name.len() <= 15, "ifname must be <= 15 chars: {if_name}"); - namespace.exec(&["ip", "link", "add", &if_name, "type", "wireguard"])?; + NetworkNamespace::exec( + &namespace.name, + &["ip", "link", "add", &if_name, "type", "wireguard"], + )?; - namespace - .exec(&["wg", "setconf", &if_name, "/tmp/vopono_nft.conf"]) - .context("Failed to run wg setconf - is wireguard-tools installed?")?; + NetworkNamespace::exec( + &namespace.name, + &["wg", "setconf", &if_name, "/tmp/vopono_nft.conf"], + ) + .context("Failed to run wg setconf - is wireguard-tools installed?")?; std::fs::remove_file("/tmp/vopono_nft.conf") .context("Deleting file: /tmp/vopono_nft.conf") .ok(); @@ -110,32 +115,41 @@ impl Wireguard { for address in config.interface.address.iter() { match address { IpNet::V6(address) => { - namespace.exec(&[ - "ip", - "-6", - "address", - "add", - &address.to_string(), - "dev", - &if_name, - ])?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", + "-6", + "address", + "add", + &address.to_string(), + "dev", + &if_name, + ], + )?; } IpNet::V4(address) => { - namespace.exec(&[ - "ip", - "-4", - "address", - "add", - &address.to_string(), - "dev", - &if_name, - ])?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", + "-4", + "address", + "add", + &address.to_string(), + "dev", + &if_name, + ], + )?; } } } // TODO: Handle custom MTU - namespace.exec(&["ip", "link", "set", "mtu", "1420", "up", "dev", &if_name])?; + NetworkNamespace::exec( + &namespace.name, + &["ip", "link", "set", "mtu", "1420", "up", "dev", &if_name], + )?; let dns: Vec = dns .cloned() @@ -147,54 +161,72 @@ impl Wireguard { // TODO: DNS suffixes? namespace.dns_config(&dns, &[], hosts_entries)?; let fwmark = "51820"; - namespace.exec(&["wg", "set", &if_name, "fwmark", fwmark])?; + NetworkNamespace::exec(&namespace.name, &["wg", "set", &if_name, "fwmark", fwmark])?; // IPv4 routes - namespace.exec(&[ - "ip", - "-4", - "route", - "add", - "0.0.0.0/0", - "dev", - &if_name, - "table", - fwmark, - ])?; - namespace.exec(&[ - "ip", "-4", "rule", "add", "not", "fwmark", fwmark, "table", fwmark, - ])?; - namespace.exec(&[ - "ip", - "-4", - "rule", - "add", - "table", - "main", - "suppress_prefixlength", - "0", - ])?; - sudo_command(&["sysctl", "-q", "net.ipv4.conf.all.src_valid_mark=1"])?; - // IPv6 - if disable_ipv6 { - crate::network::firewall::disable_ipv6(namespace, firewall)?; - } else { - namespace.exec(&[ - "ip", "-6", "route", "add", "::/0", "dev", &if_name, "table", fwmark, - ])?; - namespace.exec(&[ - "ip", "-6", "rule", "add", "not", "fwmark", fwmark, "table", fwmark, - ])?; - namespace.exec(&[ + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", + "-4", + "route", + "add", + "0.0.0.0/0", + "dev", + &if_name, + "table", + fwmark, + ], + )?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", "-4", "rule", "add", "not", "fwmark", fwmark, "table", fwmark, + ], + )?; + NetworkNamespace::exec( + &namespace.name, + &[ "ip", - "-6", + "-4", "rule", "add", "table", "main", "suppress_prefixlength", "0", - ])?; + ], + )?; + sudo_command(&["sysctl", "-q", "net.ipv4.conf.all.src_valid_mark=1"])?; + // IPv6 + if disable_ipv6 { + crate::network::firewall::disable_ipv6(namespace, firewall)?; + } else { + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", "-6", "route", "add", "::/0", "dev", &if_name, "table", fwmark, + ], + )?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", "-6", "rule", "add", "not", "fwmark", fwmark, "table", fwmark, + ], + )?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip", + "-6", + "rule", + "add", + "table", + "main", + "suppress_prefixlength", + "0", + ], + )?; } match firewall { @@ -251,7 +283,7 @@ impl Wireguard { write!(f, "{nftcmd}")?; } - namespace.exec(&["nft", "-f", "/tmp/vopono_nft.sh"])?; + NetworkNamespace::exec(&namespace.name, &["nft", "-f", "/tmp/vopono_nft.sh"])?; std::fs::remove_file("/tmp/vopono_nft.sh") .context("Deleting file: /tmp/vopono_nft.sh") .ok(); @@ -260,47 +292,53 @@ impl Wireguard { for address in config.interface.address.iter() { match address { IpNet::V6(address) => { - namespace.exec(&[ - "ip6tables", - "-t", - "raw", - "-A", - "PREROUTING", - "!", - "-i", - &if_name, - "-d", - &address.to_string(), - "-m", - "addrtype", - "!", - "--src-type", - "LOCAL", - "-j", - "DROP", - ])?; + NetworkNamespace::exec( + &namespace.name, + &[ + "ip6tables", + "-t", + "raw", + "-A", + "PREROUTING", + "!", + "-i", + &if_name, + "-d", + &address.to_string(), + "-m", + "addrtype", + "!", + "--src-type", + "LOCAL", + "-j", + "DROP", + ], + )?; } IpNet::V4(address) => { - namespace.exec(&[ - "iptables", - "-t", - "raw", - "-A", - "PREROUTING", - "!", - "-i", - &if_name, - "-d", - &address.to_string(), - "-m", - "addrtype", - "!", - "--src-type", - "LOCAL", - "-j", - "DROP", - ])?; + NetworkNamespace::exec( + &namespace.name, + &[ + "iptables", + "-t", + "raw", + "-A", + "PREROUTING", + "!", + "-i", + &if_name, + "-d", + &address.to_string(), + "-m", + "addrtype", + "!", + "--src-type", + "LOCAL", + "-j", + "DROP", + ], + )?; } } } @@ -312,31 +350,37 @@ impl Wireguard { }; for ipcmd in ipcmds { - namespace.exec(&[ - ipcmd, - "-t", - "mangle", - "-A", - "POSTROUTING", - "-p", - "udp", - "-j", - "MARK", - "--set-mark", - fwmark, - ])?; - namespace.exec(&[ - ipcmd, - "-t", - "mangle", - "-A", - "PREROUTING", - "-p", - "udp", - "-j", - "CONNMARK", - "--save-mark", - ])?; + NetworkNamespace::exec( + &namespace.name, + &[ + ipcmd, + "-t", + "mangle", + "-A", + "POSTROUTING", + "-p", + "udp", + "-j", + "MARK", + "--set-mark", + fwmark, + ], + )?; + NetworkNamespace::exec( + &namespace.name, + &[ + ipcmd, + "-t", + "mangle", + "-A", + "PREROUTING", + "-p", + "udp", + "-j", + "CONNMARK", + "--save-mark", + ], + )?; } } }; @@ -372,8 +416,9 @@ pub fn killswitch( debug!("Setting Wireguard killswitch...."); match firewall { Firewall::IpTables => { - netns - .exec(&[ + NetworkNamespace::exec( + &netns.name, + &[ "iptables", "-A", "OUTPUT", @@ -392,64 +437,73 @@ pub fn killswitch( "LOCAL", "-j", "REJECT", - ]) - .context("Executing ip6tables")?; - - netns.exec(&[ - "ip6tables", - "-A", - "OUTPUT", - "!", - "-o", - ifname, - "-m", - "mark", - "!", - "--mark", - fwmark, - "-m", - "addrtype", - "!", - "--dst-type", - "LOCAL", - "-j", - "REJECT", - ])?; + ], + ) + .context("Executing ip6tables")?; + + NetworkNamespace::exec( + &netns.name, + &[ + "ip6tables", + "-A", + "OUTPUT", + "!", + "-o", + ifname, + "-m", + "mark", + "!", + "--mark", + fwmark, + "-m", + "addrtype", + "!", + "--dst-type", + "LOCAL", + "-j", + "REJECT", + ], + )?; } Firewall::NfTables => { - netns - .exec(&["nft", "add", "table", "inet", &netns.name]) + NetworkNamespace::exec(&netns.name, &["nft", "add", "table", "inet", &netns.name]) .context("Executing nft")?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - "{ type filter hook output priority -500 ; policy accept; }", - ])?; - netns.exec(&[ - "nft", - "add", - "rule", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "output", + "{ type filter hook output priority -500 ; policy accept; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "output", - "oifname", - "!=", - ifname, - "mark", - "!=", - fwmark, - "fib", - "daddr", - "type", - "!=", - "local", - "counter", - "reject", - ])?; + &[ + "nft", + "add", + "rule", + "inet", + &netns.name, + "output", + "oifname", + "!=", + ifname, + "mark", + "!=", + fwmark, + "fib", + "daddr", + "type", + "!=", + "local", + "counter", + "reject", + ], + )?; } } Ok(()) diff --git a/vopono_core/src/util/open_hosts.rs b/vopono_core/src/util/open_hosts.rs index 76f33a8..73cffd8 100644 --- a/vopono_core/src/util/open_hosts.rs +++ b/vopono_core/src/util/open_hosts.rs @@ -10,31 +10,37 @@ pub fn open_hosts( for host in hosts { match firewall { Firewall::IpTables => { - netns.exec(&[ - "iptables", - "-I", - "OUTPUT", - "1", - "-d", - &host.to_string(), - "-j", - "ACCEPT", - ])?; + NetworkNamespace::exec( + &netns.name, + &[ + "iptables", + "-I", + "OUTPUT", + "1", + "-d", + &host.to_string(), + "-j", + "ACCEPT", + ], + )?; } Firewall::NfTables => { - netns.exec(&[ - "nft", - "insert", - "rule", - "inet", + NetworkNamespace::exec( &netns.name, - "output", - "ip", - "daddr", - &host.to_string(), - "counter", - "accept", - ])?; + &[ + "nft", + "insert", + "rule", + "inet", + &netns.name, + "output", + "ip", + "daddr", + &host.to_string(), + "counter", + "accept", + ], + )?; } } } diff --git a/vopono_core/src/util/open_ports.rs b/vopono_core/src/util/open_ports.rs index 79d8b45..4df63e3 100644 --- a/vopono_core/src/util/open_ports.rs +++ b/vopono_core/src/util/open_ports.rs @@ -11,75 +11,93 @@ pub fn open_ports( for port in ports { match firewall { Firewall::IpTables => { - netns.exec(&[ - "iptables", - "-I", - "INPUT", - "-p", - "tcp", - "--dport", - &port.to_string(), - "-j", - "ACCEPT", - ])?; - netns.exec(&[ - "iptables", - "-I", - "OUTPUT", - "-p", - "tcp", - "--sport", - &port.to_string(), - "-j", - "ACCEPT", - ])?; + NetworkNamespace::exec( + &netns.name, + &[ + "iptables", + "-I", + "INPUT", + "-p", + "tcp", + "--dport", + &port.to_string(), + "-j", + "ACCEPT", + ], + )?; + NetworkNamespace::exec( + &netns.name, + &[ + "iptables", + "-I", + "OUTPUT", + "-p", + "tcp", + "--sport", + &port.to_string(), + "-j", + "ACCEPT", + ], + )?; } Firewall::NfTables => { - netns.exec(&["nft", "add", "table", "inet", &netns.name])?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + NetworkNamespace::exec(&netns.name, &["nft", "add", "table", "inet", &netns.name])?; + NetworkNamespace::exec( &netns.name, - "input", - "{ type filter hook input priority 100 ; }", - ])?; - netns.exec(&[ - "nft", - "insert", - "rule", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "input", + "{ type filter hook input priority 100 ; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "input", - "tcp", - "dport", - &port.to_string(), - "counter", - "accept", - ])?; - netns.exec(&[ - "nft", - "add", - "chain", - "inet", + &[ + "nft", + "insert", + "rule", + "inet", + &netns.name, + "input", + "tcp", + "dport", + &port.to_string(), + "counter", + "accept", + ], + )?; + NetworkNamespace::exec( &netns.name, - "output", - "{ type filter hook output priority 100 ; }", - ])?; - netns.exec(&[ - "nft", - "insert", - "rule", - "inet", + &[ + "nft", + "add", + "chain", + "inet", + &netns.name, + "output", + "{ type filter hook output priority 100 ; }", + ], + )?; + NetworkNamespace::exec( &netns.name, - "output", - "tcp", - "sport", - &port.to_string(), - "counter", - "accept", - ])?; + &[ + "nft", + "insert", + "rule", + "inet", + &netns.name, + "output", + "tcp", + "sport", + &port.to_string(), + "counter", + "accept", + ], + )?; } } }