From 73cbfa0d211ce3772185c709cad1305a0a5d6467 Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sat, 13 Jan 2024 17:56:07 -0500 Subject: [PATCH 1/8] Add port forwarding with PIA using --port-forwarding Removes --protonvpn-port-forwarding and makes a more generic framework for API driven port forwarding that also works for PIA. This commit has several limitations noted by FIXMEs --- src/args.rs | 6 +- src/exec.rs | 45 ++-- vopono_core/Cargo.toml | 1 + vopono_core/src/config/providers/mod.rs | 25 ++- .../src/config/providers/pia/openvpn.rs | 1 + .../src/config/providers/pia/wireguard.rs | 11 +- .../src/network/application_wrapper.rs | 8 +- vopono_core/src/network/mod.rs | 7 + vopono_core/src/network/natpmpc.rs | 9 + vopono_core/src/network/piapf.rs | 193 ++++++++++++++++++ 10 files changed, 278 insertions(+), 28 deletions(-) create mode 100644 vopono_core/src/network/piapf.rs diff --git a/src/args.rs b/src/args.rs index d607b46..3ffd4c1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -213,9 +213,9 @@ pub struct ExecCommand { #[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, + /// Enable port forwarding for if supported + #[clap(long = "port-forwarding")] + pub port_forwarding: bool, /// Only create network namespace (does not run application) #[clap(long = "create-netns-only")] diff --git a/src/exec.rs b/src/exec.rs index a037e1e..679e2e2 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -15,6 +15,8 @@ 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::piapf::Piapf; +use vopono_core::network::Forwarder; use vopono_core::network::netns::NetworkNamespace; use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface}; use vopono_core::network::shadowsocks::uses_shadowsocks; @@ -139,15 +141,15 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> command.working_directory }; - // Port forwarding for ProtonVPN - let protonvpn_port_forwarding = if !command.protonvpn_port_forwarding { + // Port forwarding + let port_forwarding = if !command.port_forwarding { vopono_config_settings - .get("protonvpn-port-forwarding") + .get("port-forwarding") .map_err(|_e| anyhow!("Failed to read config file")) .ok() .unwrap_or(false) } else { - command.protonvpn_port_forwarding + command.port_forwarding }; // Create netns only @@ -547,19 +549,30 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> 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)?) + let forwarder: Option> = if port_forwarding { + match provider { + VpnProvider::PrivateInternetAccess => { + Some(Box::new(Piapf::new(&ns, &protocol)?)) + }, + VpnProvider::ProtonVPN => { + vopono_core::util::open_hosts( + &ns, + vec![vopono_core::network::natpmpc::PROTONVPN_GATEWAY], + firewall, + )?; + Some(Box::new(Natpmpc::new(&ns)?)) + }, + _ => { + anyhow::bail!("Port forwarding not supported for the selected provider"); + } + } } else { None }; - if let Some(pmpc) = natpmpc.as_ref() { - vopono_core::util::open_ports(&ns, &[pmpc.local_port], firewall)?; + // TODO: The forwarder should probably be able to do this (pass firewall?) + if let Some(fwd) = forwarder.as_ref() { + vopono_core::util::open_ports(&ns, &[fwd.forwarded_port()], firewall)?; } // Launch TCP proxy server on other threads if forwarding ports @@ -589,7 +602,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> user, group, working_directory.map(PathBuf::from), - natpmpc, + forwarder, )?; let pid = application.handle.id(); @@ -598,8 +611,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> &command.application, &ns.name, pid ); - if let Some(pmpc) = application.protonvpn_port_forwarding.as_ref() { - info!("ProtonVPN Port Forwarding on port {}", pmpc.local_port) + if let Some(fwd) = application.port_forwarding.as_ref() { + info!("Port Forwarding on port {}", fwd.forwarded_port()) } let output = application.wait_with_output()?; io::stdout().write_all(output.stdout.as_slice())?; diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index f98e707..5fd0896 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -42,3 +42,4 @@ serde_json = "1" signal-hook = "0.3" sha2 = "0.10.6" tiny_http = "0.12" +json = "0.12.4" diff --git a/vopono_core/src/config/providers/mod.rs b/vopono_core/src/config/providers/mod.rs index b3cd19f..9a229ab 100644 --- a/vopono_core/src/config/providers/mod.rs +++ b/vopono_core/src/config/providers/mod.rs @@ -5,7 +5,7 @@ mod ivpn; mod mozilla; mod mullvad; mod nordvpn; -mod pia; +pub mod pia; mod protonvpn; mod ui; mod warp; @@ -14,8 +14,12 @@ use crate::config::vpn::Protocol; use crate::util::vopono_dir; use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::{net::IpAddr, path::Path}; +use std::{ + net::IpAddr, + path::{Path, PathBuf}, + fs::File, + io::{BufReader, BufRead}, +}; use strum_macros::{Display, EnumIter}; // TODO: Consider removing this re-export pub use ui::*; @@ -137,6 +141,21 @@ pub trait OpenVpnProvider: Provider { fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)>; fn auth_file_path(&self) -> anyhow::Result>; + + fn load_openvpn_auth(&self) -> anyhow::Result<(String, String)> { + let auth_file = self.auth_file_path()?; + if let Some(auth_file) = auth_file { + let mut reader = BufReader::new(File::open(auth_file)?); + let mut user = String::new(); + reader.read_line(&mut user)?; + let mut pass = String::new(); + reader.read_line(&mut pass)?; + Ok((user.trim().to_string(), pass.trim().to_string())) + } else { + Err(anyhow!("Auth file required to load credentials!")) + } + } + fn openvpn_dir(&self) -> anyhow::Result { Ok(self.provider_dir()?.join("openvpn")) } diff --git a/vopono_core/src/config/providers/pia/openvpn.rs b/vopono_core/src/config/providers/pia/openvpn.rs index 5d0116a..81439ce 100644 --- a/vopono_core/src/config/providers/pia/openvpn.rs +++ b/vopono_core/src/config/providers/pia/openvpn.rs @@ -90,6 +90,7 @@ impl OpenVpnProvider for PrivateInternetAccess { } Ok(()) } + } #[derive(EnumIter, PartialEq)] diff --git a/vopono_core/src/config/providers/pia/wireguard.rs b/vopono_core/src/config/providers/pia/wireguard.rs index ce37c72..de5a8cb 100644 --- a/vopono_core/src/config/providers/pia/wireguard.rs +++ b/vopono_core/src/config/providers/pia/wireguard.rs @@ -93,7 +93,7 @@ impl PrivateInternetAccess { const PORT: u16 = 1337; const CERT: &'static [u8] = include_bytes!("ca.rsa.4096.crt"); - fn get_pia_token(user: &str, pass: &str) -> anyhow::Result { + pub fn get_pia_token(user: &str, pass: &str) -> anyhow::Result { let token: PiaToken = Client::new() .get("https://www.privateinternetaccess.com/gtoken/generateToken") .basic_auth(user, Some(pass)) @@ -137,6 +137,13 @@ impl PrivateInternetAccess { fn config_file_path(&self) -> anyhow::Result { Ok(self.wireguard_dir()?.join("config.txt")) } + + pub fn load_wireguard_auth(&self) -> anyhow::Result<(String, String)> { + let config_file = File::open(self.config_file_path()?)?; + let config: Config = serde_json::from_reader(config_file)?; + Ok((config.user, config.pass)) + } + } impl WireguardProvider for PrivateInternetAccess { @@ -224,7 +231,7 @@ impl WireguardProvider for PrivateInternetAccess { Ok(()) } - + fn wireguard_preup(&self, wg_config_file: &Path) -> anyhow::Result<()> { let pia_config_file = File::open(self.config_file_path()?)?; let pia_config: Config = serde_json::from_reader(pia_config_file)?; diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index 5ba017a..f9d7e3a 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; -use super::{natpmpc::Natpmpc, netns::NetworkNamespace}; +use super::{Forwarder, 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, + pub port_forwarding: Option> } impl ApplicationWrapper { @@ -16,7 +16,7 @@ impl ApplicationWrapper { user: Option, group: Option, working_directory: Option, - protonvpn_port_forwarding: Option, + port_forwarding: Option>, ) -> anyhow::Result { let running_processes = get_all_running_process_names(); let app_vec = application.split_whitespace().collect::>(); @@ -51,7 +51,7 @@ impl ApplicationWrapper { )?; Ok(Self { handle, - protonvpn_port_forwarding, + port_forwarding, }) } diff --git a/vopono_core/src/network/mod.rs b/vopono_core/src/network/mod.rs index e7fbb7e..0def6b0 100644 --- a/vopono_core/src/network/mod.rs +++ b/vopono_core/src/network/mod.rs @@ -3,6 +3,7 @@ pub mod dns_config; pub mod firewall; pub mod host_masquerade; pub mod natpmpc; +pub mod piapf; pub mod netns; pub mod network_interface; pub mod openconnect; @@ -13,3 +14,9 @@ pub mod sysctl; pub mod veth_pair; pub mod warp; pub mod wireguard; + +pub trait Forwarder { + + fn forwarded_port(&self) -> u16; + +} diff --git a/vopono_core/src/network/natpmpc.rs b/vopono_core/src/network/natpmpc.rs index 93a1894..ff9cbe0 100644 --- a/vopono_core/src/network/natpmpc.rs +++ b/vopono_core/src/network/natpmpc.rs @@ -8,6 +8,7 @@ use std::{ }; use super::netns::NetworkNamespace; +use super::Forwarder; // TODO: Move this to ProtonVPN provider pub const PROTONVPN_GATEWAY: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 2, 0, 1)); @@ -128,3 +129,11 @@ impl Drop for Natpmpc { } } } + +impl Forwarder for Natpmpc { + + fn forwarded_port(&self) -> u16 { + self.local_port + } + +} diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs new file mode 100644 index 0000000..2a15606 --- /dev/null +++ b/vopono_core/src/network/piapf.rs @@ -0,0 +1,193 @@ +extern crate json; + +use std::sync::mpsc::{self, Receiver}; +use std::{ + sync::mpsc::Sender, + thread::JoinHandle +}; +use base64::prelude::*; +use regex::Regex; + +use super::netns::NetworkNamespace; +use super::Forwarder; + +use crate::config::vpn::Protocol; +use crate::config::providers::OpenVpnProvider; // Added load_openvpn_auth to this trait +use crate::config::providers::pia::PrivateInternetAccess; // Added load_wireguard_auth to this struct + +/// Used to provide port forwarding for ProtonVPN +pub struct Piapf { + pub port: u16, + loop_thread_handle: Option>, + send_channel: Sender, +} + +struct ThreadParams { + pub port: u16, + pub netns_name: String, + pub signature: String, + pub payload: String, + pub hostname: String, + pub gateway: String, +} + +impl Piapf { + pub fn new(ns: &NetworkNamespace, protocol: &Protocol) -> anyhow::Result { + let pia = PrivateInternetAccess {}; //This is a bit weird, no? There's no state, so effectively all the methods are static... + + let traceroute_response = NetworkNamespace::exec_with_output( + &ns.name, + &["traceroute", "-n", "-m", "1", "privateinternetaccess.com" ], )?; + if !traceroute_response.status.success() { + log::error!("Could not locate gateway with traceroute"); + anyhow::bail!("Could not locate gateway with traceroute") + } + let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*").unwrap(); + let result = String::from_utf8_lossy(&traceroute_response.stdout); + let second_line = result.lines().skip(1).next().unwrap(); + let vpn_gateway = re.captures(&second_line).unwrap().get(1).unwrap().as_str().to_string(); + + log::info!("PIA gateway: {}", vpn_gateway); + + let vpn_hostname = match protocol { + Protocol::OpenVpn => "nl-amsterdam.privacy.network".to_string(), // FIXME: Parse this from the OpenVPN conf? + Protocol::Wireguard => "nl-amsterdam.privacy.network".to_string(), // FIXME: [Insert clever idea to get wireguard endpoint hostname here] + _ => { + log::error!("PIA port forwarding only supported for OpenVPN and Wireguard"); + anyhow::bail!("PIA port forwarding only supported for OpenVPN and Wireguard") + } + }; + + log::info!("PIA hostname: {}", vpn_hostname); + + let (pia_user, pia_pass) = match protocol { + Protocol::OpenVpn => pia.load_openvpn_auth()?, + Protocol::Wireguard => pia.load_wireguard_auth()?, + _ => { + log::error!("PIA port forwarding only supported for OpenVPN and Wireguard"); + anyhow::bail!("PIA port forwarding only supported for OpenVPN and Wireguard") + } + }; + + //log::info!("PIA u/p: {} / {}", pia_user, pia_pass); + + let pia_token = PrivateInternetAccess::get_pia_token(&pia_user, &pia_pass)?; + + log::info!("PIA pia_token: {}", pia_token); + + let get_response = NetworkNamespace::exec_with_output(&ns.name, &["curl", + "-s", "-m", "5", + "--connect-to", &format!("{}::{}:", vpn_hostname, vpn_gateway).to_string(), + "--cacert", "/home/benland100/vopono/vopono_core/src/config/providers/pia/ca.rsa.4096.crt", //FIXME: how to get this path? + "-G", "--data-urlencode", &format!("token={}",pia_token).to_string(), + &format!("https://{}:19999/getSignature",vpn_hostname).to_string() ] )?; + if !get_response.status.success() { + log::error!("Could not obtain signature for port forward from PIA API"); + anyhow::bail!("Could not obtain signature for port forward from PIA API") + } + + let parsed = json::parse(String::from_utf8_lossy(&get_response.stdout).as_ref())?; + if parsed["status"] != "OK" { + log::error!("Signature for port forward from PIA API not OK"); + anyhow::bail!("Signature for port forward from PIA API not OK"); + } + + let signature = parsed["signature"].as_str().unwrap().to_string(); + let payload = parsed["payload"].as_str().unwrap().to_string(); + let decoded = BASE64_STANDARD.decode(&payload)?; + let parsed = json::parse(String::from_utf8_lossy(&decoded).as_ref())?; + let port = parsed["port"].as_u16().unwrap(); + + let params = ThreadParams { + netns_name: ns.name.clone(), + hostname: vpn_hostname, + gateway: vpn_gateway, + signature: signature, + payload: payload, + port: port, + }; + Self::refresh_port(¶ms)?; + let (send, recv) = mpsc::channel::(); + let handle = std::thread::spawn(move || Self::thread_loop(params, recv)); + + log::info!("PIA forwarded local port: {port}"); + Ok(Self { + port: port, + loop_thread_handle: Some(handle), + send_channel: send, + }) + } + + fn refresh_port(params: &ThreadParams) -> anyhow::Result { + + let bind_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &["curl", + "-Gs", "-m", "5", + "--connect-to", &format!("{}::{}:", params.hostname, params.gateway).to_string(), + "--cacert", "/home/benland100/vopono/vopono_core/src/config/providers/pia/ca.rsa.4096.crt", //FIXME: how to get this path? + "--data-urlencode", &format!("payload={}", params.payload).to_string(), + "--data-urlencode", &format!("signature={}", params.signature).to_string(), + &format!("https://{}:19999/bindPort", params.hostname).to_string() ], )?; + if !bind_response.status.success() { + log::error!("Could not bind port forward from PIA API"); + anyhow::bail!("Could not bind port forward from PIA API") + } + + let parsed = json::parse(String::from_utf8_lossy(&bind_response.stdout).as_ref())?; + + if parsed["status"] != "OK" { + log::error!("Bind for port forward from PIA API not OK"); + anyhow::bail!("Bind for port forward from PIA API not OK"); + } + + //FIXME: its very useful to have a configurable callback script to receive the port number + let refresh_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &["/home/benland100/vopono/test_callback.sh", ¶ms.port.to_string()], )?; + if !refresh_response.status.success() { + log::info!("Callback script was unsuccessful!"); + } + + log::info!("Successfully updated claim to port {}", params.port); + + Ok(params.port) + } + + // Spawn thread to repeat above every 15 minutes + fn thread_loop(params: ThreadParams, recv: Receiver) { + loop { + let resp = recv.recv_timeout(std::time::Duration::from_secs(60*15)); + if resp.is_ok() { + log::debug!("Thread exiting..."); + return; + } else { + let port = Self::refresh_port(¶ms); + 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 Piapf { + 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(); + } + } +} + + +impl Forwarder for Piapf { + + fn forwarded_port(&self) -> u16 { + self.port + } + +} From 40c4594f1d314bdcdaf45c5bfcd3f5ecbc5aa1c8 Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sat, 13 Jan 2024 18:46:40 -0500 Subject: [PATCH 2/8] Fix style. --- vopono_core/src/network/piapf.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index 2a15606..b9aefb2 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -44,8 +44,8 @@ impl Piapf { } let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*").unwrap(); let result = String::from_utf8_lossy(&traceroute_response.stdout); - let second_line = result.lines().skip(1).next().unwrap(); - let vpn_gateway = re.captures(&second_line).unwrap().get(1).unwrap().as_str().to_string(); + let second_line = result.lines().nth(1).unwrap(); + let vpn_gateway = re.captures(second_line).unwrap().get(1).unwrap().as_str().to_string(); log::info!("PIA gateway: {}", vpn_gateway); From 352e58660f4787c8922c751033eeb1037872da8e Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sat, 13 Jan 2024 23:43:02 -0500 Subject: [PATCH 3/8] Store PIA certificate at sync time for port forwarding. --- .../src/config/providers/pia/openvpn.rs | 4 +++ .../src/config/providers/pia/wireguard.rs | 16 +++++++++-- vopono_core/src/network/piapf.rs | 28 +++++++++++-------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/vopono_core/src/config/providers/pia/openvpn.rs b/vopono_core/src/config/providers/pia/openvpn.rs index 81439ce..f1d37e6 100644 --- a/vopono_core/src/config/providers/pia/openvpn.rs +++ b/vopono_core/src/config/providers/pia/openvpn.rs @@ -88,6 +88,10 @@ impl OpenVpnProvider for PrivateInternetAccess { let mut outfile = File::create(auth_file)?; write!(outfile, "{user}\n{pass}")?; } + + // Write PIA certificate + self.write_pia_cert()?; + Ok(()) } diff --git a/vopono_core/src/config/providers/pia/wireguard.rs b/vopono_core/src/config/providers/pia/wireguard.rs index de5a8cb..5dc7c4d 100644 --- a/vopono_core/src/config/providers/pia/wireguard.rs +++ b/vopono_core/src/config/providers/pia/wireguard.rs @@ -1,5 +1,4 @@ -use super::PrivateInternetAccess; -use super::WireguardProvider; +use super::{PrivateInternetAccess,Provider,WireguardProvider}; use crate::config::providers::{BoolChoice, UiClient}; use crate::network::wireguard::{WireguardConfig, WireguardInterface, WireguardPeer}; use crate::util::delete_all_files_in_dir; @@ -105,6 +104,16 @@ impl PrivateInternetAccess { PiaToken::Err { message } => Err(anyhow!("{}", message)), } } + + pub fn pia_cert_path(&self) -> anyhow::Result { + Ok(self.provider_dir()?.join("ca.rsa.4096.crt")) + } + + pub fn write_pia_cert(&self) -> anyhow::Result<()> { + let mut cert_file = File::create(self.pia_cert_path()?)?; + cert_file.write_all(Self::CERT)?; + Ok(()) + } fn add_key( ip: &IpAddr, @@ -228,6 +237,9 @@ impl WireguardProvider for PrivateInternetAccess { // Write PrivateInternetAccess config file let pia_config_file = File::create(self.config_file_path()?)?; serde_json::to_writer(pia_config_file, &config)?; + + // Write PIA certificate + self.write_pia_cert()?; Ok(()) } diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index b9aefb2..f3052d7 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -29,6 +29,7 @@ struct ThreadParams { pub payload: String, pub hostname: String, pub gateway: String, + pub pia_cert_path: String, } impl Piapf { @@ -42,10 +43,10 @@ impl Piapf { log::error!("Could not locate gateway with traceroute"); anyhow::bail!("Could not locate gateway with traceroute") } - let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*").unwrap(); + let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*").expect("Unable to compile regex"); let result = String::from_utf8_lossy(&traceroute_response.stdout); - let second_line = result.lines().nth(1).unwrap(); - let vpn_gateway = re.captures(second_line).unwrap().get(1).unwrap().as_str().to_string(); + let second_line = result.lines().nth(1).expect("Missing second line (first hop) in traceroute"); + let vpn_gateway = re.captures(second_line).expect("No captures from traceroute output").get(1).expect("No matching IP group in traceroute").as_str().to_string(); log::info!("PIA gateway: {}", vpn_gateway); @@ -72,13 +73,15 @@ impl Piapf { //log::info!("PIA u/p: {} / {}", pia_user, pia_pass); let pia_token = PrivateInternetAccess::get_pia_token(&pia_user, &pia_pass)?; + let pia_cert_path = pia.pia_cert_path()?.display().to_string(); log::info!("PIA pia_token: {}", pia_token); + log::info!("PIA pia_cert_path: {}", pia_cert_path); let get_response = NetworkNamespace::exec_with_output(&ns.name, &["curl", "-s", "-m", "5", "--connect-to", &format!("{}::{}:", vpn_hostname, vpn_gateway).to_string(), - "--cacert", "/home/benland100/vopono/vopono_core/src/config/providers/pia/ca.rsa.4096.crt", //FIXME: how to get this path? + "--cacert", &pia_cert_path, "-G", "--data-urlencode", &format!("token={}",pia_token).to_string(), &format!("https://{}:19999/getSignature",vpn_hostname).to_string() ] )?; if !get_response.status.success() { @@ -92,19 +95,20 @@ impl Piapf { anyhow::bail!("Signature for port forward from PIA API not OK"); } - let signature = parsed["signature"].as_str().unwrap().to_string(); - let payload = parsed["payload"].as_str().unwrap().to_string(); + let signature = parsed["signature"].as_str().expect("getSignature response missing signature").to_string(); + let payload = parsed["payload"].as_str().expect("getSignature response missing payload").to_string(); let decoded = BASE64_STANDARD.decode(&payload)?; let parsed = json::parse(String::from_utf8_lossy(&decoded).as_ref())?; - let port = parsed["port"].as_u16().unwrap(); + let port = parsed["port"].as_u16().expect("getSignature response missing port"); let params = ThreadParams { netns_name: ns.name.clone(), hostname: vpn_hostname, gateway: vpn_gateway, - signature: signature, - payload: payload, - port: port, + pia_cert_path, + signature, + payload, + port, }; Self::refresh_port(¶ms)?; let (send, recv) = mpsc::channel::(); @@ -112,7 +116,7 @@ impl Piapf { log::info!("PIA forwarded local port: {port}"); Ok(Self { - port: port, + port, loop_thread_handle: Some(handle), send_channel: send, }) @@ -123,7 +127,7 @@ impl Piapf { let bind_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &["curl", "-Gs", "-m", "5", "--connect-to", &format!("{}::{}:", params.hostname, params.gateway).to_string(), - "--cacert", "/home/benland100/vopono/vopono_core/src/config/providers/pia/ca.rsa.4096.crt", //FIXME: how to get this path? + "--cacert", ¶ms.pia_cert_path, "--data-urlencode", &format!("payload={}", params.payload).to_string(), "--data-urlencode", &format!("signature={}", params.signature).to_string(), &format!("https://{}:19999/bindPort", params.hostname).to_string() ], )?; From 8a2d38a46eab0f36701ae476dc037214407584fa Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sat, 13 Jan 2024 23:51:35 -0500 Subject: [PATCH 4/8] Add check for traceroute utility --- vopono_core/src/network/piapf.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index f3052d7..e35e414 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -7,6 +7,7 @@ use std::{ }; use base64::prelude::*; use regex::Regex; +use which::which; use super::netns::NetworkNamespace; use super::Forwarder; @@ -36,9 +37,14 @@ impl Piapf { pub fn new(ns: &NetworkNamespace, protocol: &Protocol) -> anyhow::Result { let pia = PrivateInternetAccess {}; //This is a bit weird, no? There's no state, so effectively all the methods are static... + if ! which("traceroute").is_ok() { + log::error!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute."); + anyhow::bail!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute.") + } + let traceroute_response = NetworkNamespace::exec_with_output( - &ns.name, - &["traceroute", "-n", "-m", "1", "privateinternetaccess.com" ], )?; + &ns.name, &["traceroute", "-n", + "-m", "1", "privateinternetaccess.com" ] )?; if !traceroute_response.status.success() { log::error!("Could not locate gateway with traceroute"); anyhow::bail!("Could not locate gateway with traceroute") From 2f05f3edad0171827ea1c3c58496d6caa6ab9926 Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sun, 14 Jan 2024 10:47:50 -0500 Subject: [PATCH 5/8] Add infrastructure to cache hostnames for PIA config files. This is used for API calls after connection to enable port forwarding. --- src/args.rs | 5 ++ src/exec.rs | 16 ++++-- .../src/config/providers/pia/openvpn.rs | 55 +++++++++++++++++++ .../src/config/providers/pia/wireguard.rs | 28 ++++++++-- vopono_core/src/network/piapf.rs | 6 +- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/args.rs b/src/args.rs index 3ffd4c1..18dda72 100644 --- a/src/args.rs +++ b/src/args.rs @@ -216,6 +216,11 @@ pub struct ExecCommand { /// Enable port forwarding for if supported #[clap(long = "port-forwarding")] pub port_forwarding: bool, + + /// Path or alias to executable script or binary to be called with the port as an argumnet + /// when the port forwarding is refreshed (PIA only) + #[clap(long = "port-forwarding-callback")] + pub port_forwarding_callback: Option, /// Only create network namespace (does not run application) #[clap(long = "create-netns-only")] diff --git a/src/exec.rs b/src/exec.rs index 679e2e2..b307d27 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -431,7 +431,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> } ns.run_openvpn( - config_file.expect("No config file provided"), + config_file.clone().expect("No config file provided"), auth_file, &dns, !command.no_killswitch, @@ -466,7 +466,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> } Protocol::Wireguard => { ns.run_wireguard( - config_file.expect("No config file provided"), + config_file.clone().expect("No config file provided"), !command.no_killswitch, command.open_ports.as_ref(), command.forward_ports.as_ref(), @@ -481,7 +481,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // TODO: DNS suffixes? ns.dns_config(&dns, &[], command.hosts_entries.as_ref())?; ns.run_openconnect( - config_file.expect("No OpenConnect config file provided"), + config_file.clone().expect("No OpenConnect config file provided"), command.open_ports.as_ref(), command.forward_ports.as_ref(), firewall, @@ -492,7 +492,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> Protocol::OpenFortiVpn => { // TODO: DNS handled by OpenFortiVpn directly? ns.run_openfortivpn( - config_file.expect("No OpenFortiVPN config file provided"), + config_file.clone().expect("No OpenFortiVPN config file provided"), command.open_ports.as_ref(), command.forward_ports.as_ref(), command.hosts_entries.as_ref(), @@ -552,7 +552,13 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let forwarder: Option> = if port_forwarding { match provider { VpnProvider::PrivateInternetAccess => { - Some(Box::new(Piapf::new(&ns, &protocol)?)) + let conf_path = config_file + .expect("No PIA config file provided"); + let conf_name = conf_path + .file_name().unwrap() + .to_str().expect("No filename for PIA config file") + .to_string(); + Some(Box::new(Piapf::new(&ns, &conf_name, &protocol)?)) }, VpnProvider::ProtonVPN => { vopono_core::util::open_hosts( diff --git a/vopono_core/src/config/providers/pia/openvpn.rs b/vopono_core/src/config/providers/pia/openvpn.rs index f1d37e6..73efa09 100644 --- a/vopono_core/src/config/providers/pia/openvpn.rs +++ b/vopono_core/src/config/providers/pia/openvpn.rs @@ -13,6 +13,38 @@ use std::path::PathBuf; use strum::IntoEnumIterator; use strum_macros::EnumIter; use zip::ZipArchive; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use log::info; +use regex::Regex; +use anyhow::Context; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Config { + pub hostname_lookup: HashMap, +} + +impl PrivateInternetAccess { + + fn openvpn_config_file_path(&self) -> anyhow::Result { + Ok(self.openvpn_dir()?.join("config.txt")) + } + + //This only works if openvpn was sync'd + pub fn hostname_for_openvpn_conf(&self, config_file: &String) -> anyhow::Result { + let pia_config_file = File::open(self.openvpn_config_file_path()?)?; + let pia_config: Config = serde_json::from_reader(pia_config_file)?; + + let hostname = pia_config + .hostname_lookup + .get(config_file) + .with_context(|| format!("Could not find matching hostname for openvpn conf {config_file}"))?; + + Ok(hostname.to_string()) + } + +} impl OpenVpnProvider for PrivateInternetAccess { fn provider_dns(&self) -> Option> { @@ -40,6 +72,11 @@ impl OpenVpnProvider for PrivateInternetAccess { let country_map = crate::util::country_map::country_to_code_map(); create_dir_all(&openvpn_dir)?; delete_all_files_in_dir(&openvpn_dir)?; + + let mut config = Config { + hostname_lookup: HashMap::new(), + }; + for i in 0..zip.len() { // For each file, detect if ovpn, crl or crt // Modify auth line for config @@ -74,6 +111,20 @@ impl OpenVpnProvider for PrivateInternetAccess { } else { file.name().to_string() }; + + let re = Regex::new(r"\n *remote +([^ ]+) +\d+ *\n").expect("Failed to compile hostname regex"); + if let Some(capture) = re.captures(&String::from_utf8_lossy(&file_contents)) { + let hostname = capture + .get(1) + .expect("No matching hostname group in openvpn config") + .as_str() + .to_string(); + + info!("Associating {filename} with hostname {hostname}"); + config.hostname_lookup.insert(filename.clone(), hostname); + } else { + info!("Configuration {filename} did not have a parseable hostname - port forwarding will not work!"); + } debug!("Reading file: {}", file.name()); let mut outfile = @@ -89,6 +140,10 @@ impl OpenVpnProvider for PrivateInternetAccess { write!(outfile, "{user}\n{pass}")?; } + // Write PrivateInternetAccess openvpn config file + let pia_config_file = File::create(self.openvpn_config_file_path()?)?; + serde_json::to_writer(pia_config_file, &config)?; + // Write PIA certificate self.write_pia_cert()?; diff --git a/vopono_core/src/config/providers/pia/wireguard.rs b/vopono_core/src/config/providers/pia/wireguard.rs index 5dc7c4d..a646bde 100644 --- a/vopono_core/src/config/providers/pia/wireguard.rs +++ b/vopono_core/src/config/providers/pia/wireguard.rs @@ -86,6 +86,7 @@ pub struct Config { pub pass: String, pub pubkey: String, pub cn_lookup: HashMap, + pub hostname_lookup: HashMap, } impl PrivateInternetAccess { @@ -143,15 +144,28 @@ impl PrivateInternetAccess { } } - fn config_file_path(&self) -> anyhow::Result { + fn wireguard_config_file_path(&self) -> anyhow::Result { Ok(self.wireguard_dir()?.join("config.txt")) } pub fn load_wireguard_auth(&self) -> anyhow::Result<(String, String)> { - let config_file = File::open(self.config_file_path()?)?; + let config_file = File::open(self.wireguard_config_file_path()?)?; let config: Config = serde_json::from_reader(config_file)?; Ok((config.user, config.pass)) } + + //This only works if wireguard was sync'd + pub fn hostname_for_wireguard_conf(&self, config_file: &String) -> anyhow::Result { + let pia_config_file = File::open(self.wireguard_config_file_path()?)?; + let pia_config: Config = serde_json::from_reader(pia_config_file)?; + + let hostname = pia_config + .hostname_lookup + .get(config_file) + .with_context(|| format!("Could not find matching hostname for wireguard conf {config_file}"))?; + + Ok(hostname.to_string()) + } } @@ -197,6 +211,7 @@ impl WireguardProvider for PrivateInternetAccess { pass, pubkey: keypair.public, cn_lookup: HashMap::new(), + hostname_lookup: HashMap::new(), }; for region in vpn_info.regions { @@ -204,6 +219,9 @@ impl WireguardProvider for PrivateInternetAccess { if only_port_forwarding && !region.port_forward { continue; } + + info!("Associating {id} with hostname {}", region.dns); + config.hostname_lookup.insert(format!("{id}.conf"), region.dns); // The servers are randomized on each request so we can just use the first one if let Some(wg_server) = region.servers.wg.as_ref().and_then(|s| s.first()) { @@ -234,8 +252,8 @@ impl WireguardProvider for PrivateInternetAccess { wireguard_dir.display() ); - // Write PrivateInternetAccess config file - let pia_config_file = File::create(self.config_file_path()?)?; + // Write PrivateInternetAccess wireguard config file + let pia_config_file = File::create(self.wireguard_config_file_path()?)?; serde_json::to_writer(pia_config_file, &config)?; // Write PIA certificate @@ -245,7 +263,7 @@ impl WireguardProvider for PrivateInternetAccess { } fn wireguard_preup(&self, wg_config_file: &Path) -> anyhow::Result<()> { - let pia_config_file = File::open(self.config_file_path()?)?; + let pia_config_file = File::open(self.wireguard_config_file_path()?)?; let pia_config: Config = serde_json::from_reader(pia_config_file)?; let token = PrivateInternetAccess::get_pia_token(&pia_config.user, &pia_config.pass)?; diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index e35e414..0d8f1f5 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -34,7 +34,7 @@ struct ThreadParams { } impl Piapf { - pub fn new(ns: &NetworkNamespace, protocol: &Protocol) -> anyhow::Result { + pub fn new(ns: &NetworkNamespace, config_file: &String, protocol: &Protocol) -> anyhow::Result { let pia = PrivateInternetAccess {}; //This is a bit weird, no? There's no state, so effectively all the methods are static... if ! which("traceroute").is_ok() { @@ -57,8 +57,8 @@ impl Piapf { log::info!("PIA gateway: {}", vpn_gateway); let vpn_hostname = match protocol { - Protocol::OpenVpn => "nl-amsterdam.privacy.network".to_string(), // FIXME: Parse this from the OpenVPN conf? - Protocol::Wireguard => "nl-amsterdam.privacy.network".to_string(), // FIXME: [Insert clever idea to get wireguard endpoint hostname here] + Protocol::OpenVpn => pia.hostname_for_openvpn_conf(config_file)?, + Protocol::Wireguard => pia.hostname_for_wireguard_conf(config_file)?, _ => { log::error!("PIA port forwarding only supported for OpenVPN and Wireguard"); anyhow::bail!("PIA port forwarding only supported for OpenVPN and Wireguard") From cf818987692d6b1efc165555ea287f089cef077d Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sun, 14 Jan 2024 17:44:42 -0500 Subject: [PATCH 6/8] Implement --port-forwarding-callback for PIA forwarding This accepts an argument which is a program to execute in the network namespace which will receive the port being forwarded each time that port is refreshed. Use this to update services to utilize the forwarded port. --- src/exec.rs | 11 ++++++++++- vopono_core/src/network/piapf.rs | 15 +++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/exec.rs b/src/exec.rs index b307d27..2c6318c 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -550,6 +550,15 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let ns = ns.write_lockfile(&command.application)?; let forwarder: Option> = if port_forwarding { + + let callback = command + .port_forwarding_callback + .or_else(|| { + vopono_config_settings + .get("port_forwarding_callback") + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + }); match provider { VpnProvider::PrivateInternetAccess => { let conf_path = config_file @@ -558,7 +567,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .file_name().unwrap() .to_str().expect("No filename for PIA config file") .to_string(); - Some(Box::new(Piapf::new(&ns, &conf_name, &protocol)?)) + Some(Box::new(Piapf::new(&ns, &conf_name, &protocol, callback.as_ref())?)) }, VpnProvider::ProtonVPN => { vopono_core::util::open_hosts( diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index 0d8f1f5..0d19ea1 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -31,13 +31,14 @@ struct ThreadParams { pub hostname: String, pub gateway: String, pub pia_cert_path: String, + pub callback: Option, } impl Piapf { - pub fn new(ns: &NetworkNamespace, config_file: &String, protocol: &Protocol) -> anyhow::Result { + pub fn new(ns: &NetworkNamespace, config_file: &String, protocol: &Protocol, callback: Option<&String>) -> anyhow::Result { let pia = PrivateInternetAccess {}; //This is a bit weird, no? There's no state, so effectively all the methods are static... - if ! which("traceroute").is_ok() { + if which("traceroute").is_err() { log::error!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute."); anyhow::bail!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute.") } @@ -115,6 +116,7 @@ impl Piapf { signature, payload, port, + callback: callback.cloned(), }; Self::refresh_port(¶ms)?; let (send, recv) = mpsc::channel::(); @@ -149,10 +151,11 @@ impl Piapf { anyhow::bail!("Bind for port forward from PIA API not OK"); } - //FIXME: its very useful to have a configurable callback script to receive the port number - let refresh_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &["/home/benland100/vopono/test_callback.sh", ¶ms.port.to_string()], )?; - if !refresh_response.status.success() { - log::info!("Callback script was unsuccessful!"); + if let Some(cb) = ¶ms.callback { + let refresh_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &[&cb, ¶ms.port.to_string()], )?; + if !refresh_response.status.success() { + log::info!("Callback script was unsuccessful!"); + } } log::info!("Successfully updated claim to port {}", params.port); From 70a2aff314eac6b17d4cb5b03ff47458797c7d51 Mon Sep 17 00:00:00 2001 From: Benjamin Land Date: Sun, 14 Jan 2024 17:49:19 -0500 Subject: [PATCH 7/8] Clean up comments --- vopono_core/src/network/piapf.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index 0d19ea1..3ecea6f 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -13,10 +13,10 @@ use super::netns::NetworkNamespace; use super::Forwarder; use crate::config::vpn::Protocol; -use crate::config::providers::OpenVpnProvider; // Added load_openvpn_auth to this trait -use crate::config::providers::pia::PrivateInternetAccess; // Added load_wireguard_auth to this struct +use crate::config::providers::OpenVpnProvider; +use crate::config::providers::pia::PrivateInternetAccess; -/// Used to provide port forwarding for ProtonVPN +/// Used to provide port forwarding for PrivateInternetAccess pub struct Piapf { pub port: u16, loop_thread_handle: Option>, @@ -36,7 +36,7 @@ struct ThreadParams { impl Piapf { pub fn new(ns: &NetworkNamespace, config_file: &String, protocol: &Protocol, callback: Option<&String>) -> anyhow::Result { - let pia = PrivateInternetAccess {}; //This is a bit weird, no? There's no state, so effectively all the methods are static... + let pia = PrivateInternetAccess {}; if which("traceroute").is_err() { log::error!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute."); From 8d103bce23ee7883c92d4a90ec73981d5eb9f703 Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sat, 20 Jan 2024 14:48:35 +0100 Subject: [PATCH 8/8] rustfmt and update docs --- README.md | 4 +- USERGUIDE.md | 20 ++- src/args.rs | 2 +- src/exec.rs | 47 +++--- vopono_core/src/config/providers/mod.rs | 7 +- .../src/config/providers/pia/openvpn.rs | 50 +++--- .../src/config/providers/pia/wireguard.rs | 31 ++-- .../src/config/providers/protonvpn/openvpn.rs | 2 +- .../src/network/application_wrapper.rs | 4 +- vopono_core/src/network/mod.rs | 4 +- vopono_core/src/network/natpmpc.rs | 2 - vopono_core/src/network/piapf.rs | 155 ++++++++++++------ 12 files changed, 193 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 71049c5..7f0d16c 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ lynx all running through different VPN connections: | AirVPN | ✅ | ❌ | | Cloudflare Warp\*\*\*\* | ❌ | ❌ | -\* Port forwarding is not currently supported for PrivateInternetAccess. PRs welcome. +\* Port forwarding supported with the `--port-forwarding` option and `--port-forwarding-callback` to run a command when the port is refreshed. \*\* 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. [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` ). `natpmpc` must be installed. 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. +for details. [Port Forwarding](https://protonvpn.com/support/port-forwarding-manual-setup/) is supported with the `--port-forwarding` argument for both OpenVPN and Wireguard (with `--provider custom --custom xxx.conf --protocol wireguard` ). `natpmpc` must be installed. 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. \*\*\*\* Cloudflare Warp uses its own protocol. Set both the provider and diff --git a/USERGUIDE.md b/USERGUIDE.md index 4b62b73..f180bd1 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -488,12 +488,12 @@ 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 +$ vopono -v exec --provider custom --custom testwg-UK-17.conf --protocol wireguard --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. +Port forwarding can be enabled with the `--port-forwarding` argument, but requires using a server that supports port forwarding. `natpmpc` must be installed e.g. via the `libnatpmp` package on Arch Linux. @@ -508,6 +508,10 @@ The port you are allocated will then be printed to the console like: And that is the port you would then set up in applications that require it. +### PrivateInternetAccess + +Port forwaring supported with the `--port-forwarding` option, use the `--port-forwarding-callback` option to specify a command to run when the port is refreshed. + ### Cloudflare Warp Cloudflare Warp users must first register with Warp via the CLI client: @@ -525,9 +529,9 @@ You can then kill `warp-svc` and run it via vopono: $ vopono -v exec --no-killswitch --provider warp --protocol warp firefox-developer-edition ``` -### VPN Provider limitations +## VPN Provider limitations -#### PrivateInternetAccess +### PrivateInternetAccess Wireguard support for PrivateInternetAccess (PIA) requires the use of a user token to get the latest servers at time of use. See [issue 9](https://github.com/jamesmcm/vopono/issues/9) for details, @@ -535,21 +539,21 @@ and PIA's [official script for Wireguard access](https://github.com/pia-foss/man So if you encounter connection issues, first try re-running `vopono sync`. -#### MozillaVPN +### MozillaVPN There is no easy way to delete MozillaVPN devices (Wireguard keypairs), unlike Mullvad this _cannot_ be done on the webpage. I recommend using [MozWire](https://github.com/NilsIrl/MozWire) to manage this. -#### iVPN +### iVPN iVPN Wireguard keypairs must be uploaded manually, as the Client Area is behind a captcha login. -#### NordVPN +### NordVPN Starting 27 June 2023, the required user credentials are no longer your NordVPN login details but need to be generated in the user control panel, under Services → NordVPN. Scroll down and locate the Manual Setup tab, then click on Set up NordVPN manually and follow instructions. Copy your service credentials and re-sync NordVPN configuration inside Vopono. -### Tunnel Port Forwarding +## Tunnel Port Forwarding Some providers allow port forwarding inside the tunnel, so you can open some ports inside the network namespace which can be accessed via the diff --git a/src/args.rs b/src/args.rs index 18dda72..c539be7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -216,7 +216,7 @@ pub struct ExecCommand { /// Enable port forwarding for if supported #[clap(long = "port-forwarding")] pub port_forwarding: bool, - + /// Path or alias to executable script or binary to be called with the port as an argumnet /// when the port forwarding is refreshed (PIA only) #[clap(long = "port-forwarding-callback")] diff --git a/src/exec.rs b/src/exec.rs index 5cc1660..7af6071 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -15,12 +15,12 @@ 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::piapf::Piapf; -use vopono_core::network::Forwarder; use vopono_core::network::netns::NetworkNamespace; use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface}; +use vopono_core::network::piapf::Piapf; use vopono_core::network::shadowsocks::uses_shadowsocks; use vopono_core::network::sysctl::SysCtl; +use vopono_core::network::Forwarder; use vopono_core::util::vopono_dir; use vopono_core::util::{get_config_file_protocol, get_config_from_alias}; use vopono_core::util::{get_existing_namespaces, get_target_subnet}; @@ -484,7 +484,9 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> // TODO: DNS suffixes? ns.dns_config(&dns, &[], command.hosts_entries.as_ref())?; ns.run_openconnect( - config_file.clone().expect("No OpenConnect config file provided"), + config_file + .clone() + .expect("No OpenConnect config file provided"), command.open_ports.as_ref(), command.forward_ports.as_ref(), firewall, @@ -495,7 +497,9 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> Protocol::OpenFortiVpn => { // TODO: DNS handled by OpenFortiVpn directly? ns.run_openfortivpn( - config_file.clone().expect("No OpenFortiVPN config file provided"), + config_file + .clone() + .expect("No OpenFortiVPN config file provided"), command.open_ports.as_ref(), command.forward_ports.as_ref(), command.hosts_entries.as_ref(), @@ -553,25 +557,28 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let ns = ns.write_lockfile(&command.application)?; let forwarder: Option> = if port_forwarding { - - let callback = command - .port_forwarding_callback - .or_else(|| { - vopono_config_settings - .get("port_forwarding_callback") - .map_err(|_e| anyhow!("Failed to read config file")) - .ok() - }); + let callback = command.port_forwarding_callback.or_else(|| { + vopono_config_settings + .get("port_forwarding_callback") + .map_err(|_e| anyhow!("Failed to read config file")) + .ok() + }); match provider { VpnProvider::PrivateInternetAccess => { - let conf_path = config_file - .expect("No PIA config file provided"); + let conf_path = config_file.expect("No PIA config file provided"); let conf_name = conf_path - .file_name().unwrap() - .to_str().expect("No filename for PIA config file") + .file_name() + .unwrap() + .to_str() + .expect("No filename for PIA config file") .to_string(); - Some(Box::new(Piapf::new(&ns, &conf_name, &protocol, callback.as_ref())?)) - }, + Some(Box::new(Piapf::new( + &ns, + &conf_name, + &protocol, + callback.as_ref(), + )?)) + } VpnProvider::ProtonVPN => { vopono_core::util::open_hosts( &ns, @@ -579,7 +586,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> firewall, )?; Some(Box::new(Natpmpc::new(&ns)?)) - }, + } _ => { anyhow::bail!("Port forwarding not supported for the selected provider"); } diff --git a/vopono_core/src/config/providers/mod.rs b/vopono_core/src/config/providers/mod.rs index 9a229ab..ae088ad 100644 --- a/vopono_core/src/config/providers/mod.rs +++ b/vopono_core/src/config/providers/mod.rs @@ -15,10 +15,10 @@ use crate::util::vopono_dir; use anyhow::anyhow; use serde::{Deserialize, Serialize}; use std::{ - net::IpAddr, - path::{Path, PathBuf}, fs::File, - io::{BufReader, BufRead}, + io::{BufRead, BufReader}, + net::IpAddr, + path::{Path, PathBuf}, }; use strum_macros::{Display, EnumIter}; // TODO: Consider removing this re-export @@ -141,7 +141,6 @@ pub trait OpenVpnProvider: Provider { fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)>; fn auth_file_path(&self) -> anyhow::Result>; - fn load_openvpn_auth(&self) -> anyhow::Result<(String, String)> { let auth_file = self.auth_file_path()?; if let Some(auth_file) = auth_file { diff --git a/vopono_core/src/config/providers/pia/openvpn.rs b/vopono_core/src/config/providers/pia/openvpn.rs index 73efa09..74356ea 100644 --- a/vopono_core/src/config/providers/pia/openvpn.rs +++ b/vopono_core/src/config/providers/pia/openvpn.rs @@ -2,8 +2,14 @@ use super::PrivateInternetAccess; use super::{ConfigurationChoice, OpenVpnProvider}; use crate::config::providers::UiClient; use crate::util::delete_all_files_in_dir; -use log::debug; +use anyhow::Context; +use log::info; +use log::{debug, warn}; +use regex::Regex; use reqwest::Url; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; use std::fmt::Display; use std::fs::create_dir_all; use std::fs::File; @@ -13,12 +19,6 @@ use std::path::PathBuf; use strum::IntoEnumIterator; use strum_macros::EnumIter; use zip::ZipArchive; -use serde::Deserialize; -use serde::Serialize; -use std::collections::HashMap; -use log::info; -use regex::Regex; -use anyhow::Context; #[derive(Debug, Deserialize, Serialize)] pub struct Config { @@ -26,24 +26,24 @@ pub struct Config { } impl PrivateInternetAccess { - fn openvpn_config_file_path(&self) -> anyhow::Result { Ok(self.openvpn_dir()?.join("config.txt")) } - + //This only works if openvpn was sync'd - pub fn hostname_for_openvpn_conf(&self, config_file: &String) -> anyhow::Result { + pub fn hostname_for_openvpn_conf(&self, config_file: &String) -> anyhow::Result { let pia_config_file = File::open(self.openvpn_config_file_path()?)?; let pia_config: Config = serde_json::from_reader(pia_config_file)?; - + let hostname = pia_config .hostname_lookup .get(config_file) - .with_context(|| format!("Could not find matching hostname for openvpn conf {config_file}"))?; - + .with_context(|| { + format!("Could not find matching hostname for openvpn conf {config_file}") + })?; + Ok(hostname.to_string()) } - } impl OpenVpnProvider for PrivateInternetAccess { @@ -72,11 +72,11 @@ impl OpenVpnProvider for PrivateInternetAccess { let country_map = crate::util::country_map::country_to_code_map(); create_dir_all(&openvpn_dir)?; delete_all_files_in_dir(&openvpn_dir)?; - + let mut config = Config { hostname_lookup: HashMap::new(), }; - + for i in 0..zip.len() { // For each file, detect if ovpn, crl or crt // Modify auth line for config @@ -111,19 +111,20 @@ impl OpenVpnProvider for PrivateInternetAccess { } else { file.name().to_string() }; - - let re = Regex::new(r"\n *remote +([^ ]+) +\d+ *\n").expect("Failed to compile hostname regex"); + + let re = Regex::new(r"\n *remote +([^ ]+) +\d+ *\n") + .expect("Failed to compile hostname regex"); if let Some(capture) = re.captures(&String::from_utf8_lossy(&file_contents)) { let hostname = capture .get(1) .expect("No matching hostname group in openvpn config") .as_str() .to_string(); - + info!("Associating {filename} with hostname {hostname}"); config.hostname_lookup.insert(filename.clone(), hostname); } else { - info!("Configuration {filename} did not have a parseable hostname - port forwarding will not work!"); + warn!("Configuration {filename} did not have a parseable hostname - port forwarding will not work!"); } debug!("Reading file: {}", file.name()); @@ -139,17 +140,16 @@ impl OpenVpnProvider for PrivateInternetAccess { let mut outfile = File::create(auth_file)?; write!(outfile, "{user}\n{pass}")?; } - + // Write PrivateInternetAccess openvpn config file let pia_config_file = File::create(self.openvpn_config_file_path()?)?; serde_json::to_writer(pia_config_file, &config)?; - - // Write PIA certificate + + // Write PIA certificate self.write_pia_cert()?; - + Ok(()) } - } #[derive(EnumIter, PartialEq)] diff --git a/vopono_core/src/config/providers/pia/wireguard.rs b/vopono_core/src/config/providers/pia/wireguard.rs index a646bde..eb8d7fc 100644 --- a/vopono_core/src/config/providers/pia/wireguard.rs +++ b/vopono_core/src/config/providers/pia/wireguard.rs @@ -1,4 +1,4 @@ -use super::{PrivateInternetAccess,Provider,WireguardProvider}; +use super::{PrivateInternetAccess, Provider, WireguardProvider}; use crate::config::providers::{BoolChoice, UiClient}; use crate::network::wireguard::{WireguardConfig, WireguardInterface, WireguardPeer}; use crate::util::delete_all_files_in_dir; @@ -105,11 +105,11 @@ impl PrivateInternetAccess { PiaToken::Err { message } => Err(anyhow!("{}", message)), } } - + pub fn pia_cert_path(&self) -> anyhow::Result { Ok(self.provider_dir()?.join("ca.rsa.4096.crt")) } - + pub fn write_pia_cert(&self) -> anyhow::Result<()> { let mut cert_file = File::create(self.pia_cert_path()?)?; cert_file.write_all(Self::CERT)?; @@ -147,26 +147,27 @@ impl PrivateInternetAccess { fn wireguard_config_file_path(&self) -> anyhow::Result { Ok(self.wireguard_dir()?.join("config.txt")) } - + pub fn load_wireguard_auth(&self) -> anyhow::Result<(String, String)> { let config_file = File::open(self.wireguard_config_file_path()?)?; let config: Config = serde_json::from_reader(config_file)?; Ok((config.user, config.pass)) } - + //This only works if wireguard was sync'd - pub fn hostname_for_wireguard_conf(&self, config_file: &String) -> anyhow::Result { + pub fn hostname_for_wireguard_conf(&self, config_file: &String) -> anyhow::Result { let pia_config_file = File::open(self.wireguard_config_file_path()?)?; let pia_config: Config = serde_json::from_reader(pia_config_file)?; - + let hostname = pia_config .hostname_lookup .get(config_file) - .with_context(|| format!("Could not find matching hostname for wireguard conf {config_file}"))?; - + .with_context(|| { + format!("Could not find matching hostname for wireguard conf {config_file}") + })?; + Ok(hostname.to_string()) } - } impl WireguardProvider for PrivateInternetAccess { @@ -219,9 +220,11 @@ impl WireguardProvider for PrivateInternetAccess { if only_port_forwarding && !region.port_forward { continue; } - + info!("Associating {id} with hostname {}", region.dns); - config.hostname_lookup.insert(format!("{id}.conf"), region.dns); + config + .hostname_lookup + .insert(format!("{id}.conf"), region.dns); // The servers are randomized on each request so we can just use the first one if let Some(wg_server) = region.servers.wg.as_ref().and_then(|s| s.first()) { @@ -255,13 +258,13 @@ impl WireguardProvider for PrivateInternetAccess { // Write PrivateInternetAccess wireguard config file let pia_config_file = File::create(self.wireguard_config_file_path()?)?; serde_json::to_writer(pia_config_file, &config)?; - + // Write PIA certificate self.write_pia_cert()?; Ok(()) } - + fn wireguard_preup(&self, wg_config_file: &Path) -> anyhow::Result<()> { let pia_config_file = File::open(self.wireguard_config_file_path()?)?; let pia_config: Config = serde_json::from_reader(pia_config_file)?; diff --git a/vopono_core/src/config/providers/protonvpn/openvpn.rs b/vopono_core/src/config/providers/protonvpn/openvpn.rs index f2692bf..2c44bf4 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 ) - add +pmp suffix if using --protonvpn-port-forwarding - note not all servers support this feature" + "ProtonVPN OpenVPN username (see: https://account.protonvpn.com/account#openvpn ) - add +pmp suffix if using --port-forwarding - note not all servers support this feature" .to_string(), validator: None, })?; diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index f9d7e3a..53236e2 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; -use super::{Forwarder, netns::NetworkNamespace}; +use super::{netns::NetworkNamespace, Forwarder}; use crate::util::get_all_running_process_names; use log::warn; pub struct ApplicationWrapper { pub handle: std::process::Child, - pub port_forwarding: Option> + pub port_forwarding: Option>, } impl ApplicationWrapper { diff --git a/vopono_core/src/network/mod.rs b/vopono_core/src/network/mod.rs index 0def6b0..dea3d04 100644 --- a/vopono_core/src/network/mod.rs +++ b/vopono_core/src/network/mod.rs @@ -3,12 +3,12 @@ pub mod dns_config; pub mod firewall; pub mod host_masquerade; pub mod natpmpc; -pub mod piapf; pub mod netns; pub mod network_interface; pub mod openconnect; pub mod openfortivpn; pub mod openvpn; +pub mod piapf; pub mod shadowsocks; pub mod sysctl; pub mod veth_pair; @@ -16,7 +16,5 @@ pub mod warp; pub mod wireguard; pub trait Forwarder { - fn forwarded_port(&self) -> u16; - } diff --git a/vopono_core/src/network/natpmpc.rs b/vopono_core/src/network/natpmpc.rs index ff9cbe0..bd5a13a 100644 --- a/vopono_core/src/network/natpmpc.rs +++ b/vopono_core/src/network/natpmpc.rs @@ -131,9 +131,7 @@ impl Drop for Natpmpc { } impl Forwarder for Natpmpc { - fn forwarded_port(&self) -> u16 { self.local_port } - } diff --git a/vopono_core/src/network/piapf.rs b/vopono_core/src/network/piapf.rs index 3ecea6f..68f9daa 100644 --- a/vopono_core/src/network/piapf.rs +++ b/vopono_core/src/network/piapf.rs @@ -1,20 +1,15 @@ -extern crate json; - -use std::sync::mpsc::{self, Receiver}; -use std::{ - sync::mpsc::Sender, - thread::JoinHandle -}; use base64::prelude::*; use regex::Regex; +use std::sync::mpsc::{self, Receiver}; +use std::{sync::mpsc::Sender, thread::JoinHandle}; use which::which; use super::netns::NetworkNamespace; use super::Forwarder; -use crate::config::vpn::Protocol; -use crate::config::providers::OpenVpnProvider; use crate::config::providers::pia::PrivateInternetAccess; +use crate::config::providers::OpenVpnProvider; +use crate::config::vpn::Protocol; /// Used to provide port forwarding for PrivateInternetAccess pub struct Piapf { @@ -26,7 +21,7 @@ pub struct Piapf { struct ThreadParams { pub port: u16, pub netns_name: String, - pub signature: String, + pub signature: String, pub payload: String, pub hostname: String, pub gateway: String, @@ -35,28 +30,44 @@ struct ThreadParams { } impl Piapf { - pub fn new(ns: &NetworkNamespace, config_file: &String, protocol: &Protocol, callback: Option<&String>) -> anyhow::Result { + pub fn new( + ns: &NetworkNamespace, + config_file: &String, + protocol: &Protocol, + callback: Option<&String>, + ) -> anyhow::Result { let pia = PrivateInternetAccess {}; - + if which("traceroute").is_err() { log::error!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute."); anyhow::bail!("The traceroute utility is necessary for PIA port forwarding. Please install traceroute.") } - + let traceroute_response = NetworkNamespace::exec_with_output( - &ns.name, &["traceroute", "-n", - "-m", "1", "privateinternetaccess.com" ] )?; + &ns.name, + &["traceroute", "-n", "-m", "1", "privateinternetaccess.com"], + )?; if !traceroute_response.status.success() { log::error!("Could not locate gateway with traceroute"); anyhow::bail!("Could not locate gateway with traceroute") } - let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*").expect("Unable to compile regex"); + let re = Regex::new(r" *1 *(?P\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*") + .expect("Unable to compile regex"); let result = String::from_utf8_lossy(&traceroute_response.stdout); - let second_line = result.lines().nth(1).expect("Missing second line (first hop) in traceroute"); - let vpn_gateway = re.captures(second_line).expect("No captures from traceroute output").get(1).expect("No matching IP group in traceroute").as_str().to_string(); - + let second_line = result + .lines() + .nth(1) + .expect("Missing second line (first hop) in traceroute"); + let vpn_gateway = re + .captures(second_line) + .expect("No captures from traceroute output") + .get(1) + .expect("No matching IP group in traceroute") + .as_str() + .to_string(); + log::info!("PIA gateway: {}", vpn_gateway); - + let vpn_hostname = match protocol { Protocol::OpenVpn => pia.hostname_for_openvpn_conf(config_file)?, Protocol::Wireguard => pia.hostname_for_wireguard_conf(config_file)?, @@ -65,9 +76,9 @@ impl Piapf { anyhow::bail!("PIA port forwarding only supported for OpenVPN and Wireguard") } }; - + log::info!("PIA hostname: {}", vpn_hostname); - + let (pia_user, pia_pass) = match protocol { Protocol::OpenVpn => pia.load_openvpn_auth()?, Protocol::Wireguard => pia.load_wireguard_auth()?, @@ -76,37 +87,65 @@ impl Piapf { anyhow::bail!("PIA port forwarding only supported for OpenVPN and Wireguard") } }; - + //log::info!("PIA u/p: {} / {}", pia_user, pia_pass); - + let pia_token = PrivateInternetAccess::get_pia_token(&pia_user, &pia_pass)?; let pia_cert_path = pia.pia_cert_path()?.display().to_string(); - + log::info!("PIA pia_token: {}", pia_token); log::info!("PIA pia_cert_path: {}", pia_cert_path); - let get_response = NetworkNamespace::exec_with_output(&ns.name, &["curl", - "-s", "-m", "5", - "--connect-to", &format!("{}::{}:", vpn_hostname, vpn_gateway).to_string(), - "--cacert", &pia_cert_path, - "-G", "--data-urlencode", &format!("token={}",pia_token).to_string(), - &format!("https://{}:19999/getSignature",vpn_hostname).to_string() ] )?; + if which("curl").is_err() { + log::error!( + "The curl utility is necessary for PIA port forwarding. Please install curl." + ); + anyhow::bail!( + "The curl utility is necessary for PIA port forwarding. Please install curl." + ) + } + + let get_response = NetworkNamespace::exec_with_output( + &ns.name, + &[ + "curl", + "-s", + "-m", + "5", + "--connect-to", + &format!("{}::{}:", vpn_hostname, vpn_gateway).to_string(), + "--cacert", + &pia_cert_path, + "-G", + "--data-urlencode", + &format!("token={}", pia_token).to_string(), + &format!("https://{}:19999/getSignature", vpn_hostname).to_string(), + ], + )?; if !get_response.status.success() { log::error!("Could not obtain signature for port forward from PIA API"); anyhow::bail!("Could not obtain signature for port forward from PIA API") } - + let parsed = json::parse(String::from_utf8_lossy(&get_response.stdout).as_ref())?; if parsed["status"] != "OK" { log::error!("Signature for port forward from PIA API not OK"); anyhow::bail!("Signature for port forward from PIA API not OK"); } - - let signature = parsed["signature"].as_str().expect("getSignature response missing signature").to_string(); - let payload = parsed["payload"].as_str().expect("getSignature response missing payload").to_string(); + + let signature = parsed["signature"] + .as_str() + .expect("getSignature response missing signature") + .to_string(); + let payload = parsed["payload"] + .as_str() + .expect("getSignature response missing payload") + .to_string(); let decoded = BASE64_STANDARD.decode(&payload)?; let parsed = json::parse(String::from_utf8_lossy(&decoded).as_ref())?; - let port = parsed["port"].as_u16().expect("getSignature response missing port"); + let port = parsed["port"] + .as_u16() + .expect("getSignature response missing port"); let params = ThreadParams { netns_name: ns.name.clone(), @@ -131,33 +170,46 @@ impl Piapf { } fn refresh_port(params: &ThreadParams) -> anyhow::Result { - - let bind_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &["curl", - "-Gs", "-m", "5", - "--connect-to", &format!("{}::{}:", params.hostname, params.gateway).to_string(), - "--cacert", ¶ms.pia_cert_path, - "--data-urlencode", &format!("payload={}", params.payload).to_string(), - "--data-urlencode", &format!("signature={}", params.signature).to_string(), - &format!("https://{}:19999/bindPort", params.hostname).to_string() ], )?; + let bind_response = NetworkNamespace::exec_with_output( + ¶ms.netns_name, + &[ + "curl", + "-Gs", + "-m", + "5", + "--connect-to", + &format!("{}::{}:", params.hostname, params.gateway).to_string(), + "--cacert", + ¶ms.pia_cert_path, + "--data-urlencode", + &format!("payload={}", params.payload).to_string(), + "--data-urlencode", + &format!("signature={}", params.signature).to_string(), + &format!("https://{}:19999/bindPort", params.hostname).to_string(), + ], + )?; if !bind_response.status.success() { log::error!("Could not bind port forward from PIA API"); anyhow::bail!("Could not bind port forward from PIA API") } - + let parsed = json::parse(String::from_utf8_lossy(&bind_response.stdout).as_ref())?; - + if parsed["status"] != "OK" { log::error!("Bind for port forward from PIA API not OK"); anyhow::bail!("Bind for port forward from PIA API not OK"); } - + if let Some(cb) = ¶ms.callback { - let refresh_response = NetworkNamespace::exec_with_output(¶ms.netns_name, &[&cb, ¶ms.port.to_string()], )?; + let refresh_response = NetworkNamespace::exec_with_output( + ¶ms.netns_name, + &[&cb, ¶ms.port.to_string()], + )?; if !refresh_response.status.success() { log::info!("Callback script was unsuccessful!"); } } - + log::info!("Successfully updated claim to port {}", params.port); Ok(params.port) @@ -166,7 +218,7 @@ impl Piapf { // Spawn thread to repeat above every 15 minutes fn thread_loop(params: ThreadParams, recv: Receiver) { loop { - let resp = recv.recv_timeout(std::time::Duration::from_secs(60*15)); + let resp = recv.recv_timeout(std::time::Duration::from_secs(60 * 15)); if resp.is_ok() { log::debug!("Thread exiting..."); return; @@ -196,11 +248,8 @@ impl Drop for Piapf { } } - impl Forwarder for Piapf { - fn forwarded_port(&self) -> u16 { self.port } - }