From 0ed46f62e0f7894e52dc0982c1fb5dc890f551c9 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sat, 4 Nov 2023 15:03:37 +0100 Subject: [PATCH] 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", + ], + )?; } } }