From 0ed46f62e0f7894e52dc0982c1fb5dc890f551c9 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sat, 4 Nov 2023 15:03:37 +0100 Subject: [PATCH 1/2] Add port forwarding for ProtonVPN connections Works with OpenVPN and Wireguard (downloaded ProtonVPN custom config) Note for OpenVPN port forwarding you must generate the OpenVPN config files appending "+pmp" to the OpenVPN username. Note we do not currently handle if the port changes when renewed (i.e. we do not rest firewall rules in this case). --- Cargo.toml | 4 +- src/args.rs | 4 + src/cli_client.rs | 17 +- src/exec.rs | 111 ++-- vopono_core/Cargo.toml | 4 +- .../src/config/providers/mozilla/wireguard.rs | 2 +- .../src/config/providers/mullvad/wireguard.rs | 2 +- .../src/config/providers/protonvpn/openvpn.rs | 15 +- .../src/network/application_wrapper.rs | 12 +- vopono_core/src/network/firewall.rs | 71 +-- vopono_core/src/network/host_masquerade.rs | 4 +- vopono_core/src/network/mod.rs | 1 + vopono_core/src/network/natpmpc.rs | 120 +++++ vopono_core/src/network/netns.rs | 87 +-- vopono_core/src/network/openconnect.rs | 14 +- vopono_core/src/network/openfortivpn.rs | 35 +- vopono_core/src/network/openvpn.rs | 497 ++++++++++-------- vopono_core/src/network/shadowsocks.rs | 14 +- vopono_core/src/network/warp.rs | 14 +- vopono_core/src/network/wireguard.rs | 416 ++++++++------- vopono_core/src/util/open_hosts.rs | 50 +- vopono_core/src/util/open_ports.rs | 144 ++--- 22 files changed, 982 insertions(+), 656 deletions(-) create mode 100644 vopono_core/src/network/natpmpc.rs 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/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", + ], + )?; } } } From c9fc6c860532ca4dfea8625a2cb7becceaa993d0 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sat, 4 Nov 2023 16:54:00 +0100 Subject: [PATCH 2/2] Update docs --- README.md | 43 +++++++++++++++++++------------------ USERGUIDE.md | 50 +++++++++++++++++++++++++++++++++---------- protonvpn_header.png | Bin 0 -> 65607 bytes 3 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 protonvpn_header.png 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 0000000000000000000000000000000000000000..108fb455bb3c2723116953a0b15887c33cfa25d5 GIT binary patch literal 65607 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%0AtWZU++;kkKT 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 literal 0 HcmV?d00001