diff --git a/Cargo.toml b/Cargo.toml index acf075d..8a11f7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vopono" description = "Launch applications via VPN tunnels using temporary network namespaces" -version = "0.10.10" +version = "0.10.11" authors = ["James McMurray "] edition = "2021" license = "GPL-3.0-or-later" @@ -19,15 +19,15 @@ directories-next = "2" log = "0.4" pretty_env_logger = "0.5" clap = { version = "4", features = ["derive"] } -which = "6" +which = "7" dialoguer = "0.11" compound_duration = "1" signal-hook = "0.3" walkdir = "2" chrono = "0.4" bs58 = "0.5" -nix = { version = "0.28", features = ["signal", "process"] } -config = "0.14" +nix = { version = "0.29", features = ["signal", "process"] } +config = "0.15" basic_tcp_proxy = "0.3.2" strum = "0.26" strum_macros = "0.26" diff --git a/USERGUIDE.md b/USERGUIDE.md index ce27bfd..39905a5 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -135,7 +135,7 @@ necessary): ```bash $ paru -S vopono-bin -$ vopono sync +$ vopono sync --protocol wireguard ``` Run vopono: @@ -168,7 +168,7 @@ create the OpenVPN configuration files and server lists. ```bash $ paru -S vopono-bin -$ vopono sync +$ vopono sync --protocol openvpn ``` Run vopono: @@ -548,9 +548,11 @@ BitTorrent leaking for both the OpenVPN and Wireguard configurations. ### AzireVPN AzireVPN users can use [their security check page](https://www.azirevpn.com/check) -for the same (note the instructions on disabling WebRTC). I noticed that -when using IPv6 with OpenVPN it incorrectly states you are not connected -via AzireVPN though (Wireguard works correctly). +for the same (note the instructions on disabling WebRTC). + +#### OpenVPN Sync + +Since AzireVPN now puts the OpenVPN configurations behind authentication, it is necessary to copy the value of the `az` cookie in order to authenticate `vopono sync` when generating the OpenVPN configuration files. ### ProtonVPN @@ -653,9 +655,22 @@ I recommend using [MozWire](https://github.com/NilsIrl/MozWire) to manage this. iVPN Wireguard keypairs must be uploaded manually, as the Client Area is behind a captcha login. +Note [iVPN no longer supports port forwarding](https://www.ivpn.net/blog/gradual-removal-of-port-forwarding). At the time of writing, ProtonVPN is the best provider with this service. + ### 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. +### AzireVPN + +For AzireVPN port forwarding is only possible for Wireguard and can be enabled by using `--port-forwarding`. This will create a port forwarding mapping for the current Wireguard device for 30 days. + +After 30 days you will need to restart vopono to re-create the port forwarding mapping. + +Note vopono attempts to delete the created mapping when vopono is closed, but this may not always succeed. However, it will use an existing mapping for the chosen device and server pair, if one still exists on AzireVPN's side. + +Note AzireVPN sometimes has some issues with rate limiting when generating the OpenVPN config files. + ## Tunnel Port Forwarding Some providers allow port forwarding inside the tunnel, so you can open @@ -663,13 +678,8 @@ some ports inside the network namespace which can be accessed via the Wireguard/OpenVPN tunnel (this can be important for BitTorrent connectivity, etc.). -For iVPN port forwarding also works the same way, however it is **only -supported for OpenVPN** on iVPN's side. So remember to pass -`--protocol openvpn -o PORTNUMBER` when trying it! Enable port -forwarding in the [Port Forwarding page in the iVPN client area](https://www.ivpn.net/clientarea/vpn/273887). - For AirVPN you must enable the port in [the client area webpage](https://airvpn.org/ports/), -and then use `--protocol openvpn -o PORTNUMBER` as for iVPN. +and then use `--protocol openvpn -o PORTNUMBER`. ## Dependencies diff --git a/src/args.rs b/src/args.rs index f9f7d22..46713d4 100644 --- a/src/args.rs +++ b/src/args.rs @@ -25,7 +25,7 @@ impl ValueEnum for WrappedArg { let use_input = input.trim().to_string(); let found = if ignore_case { - T::iter().find(|x| x.to_string().to_ascii_lowercase() == use_input.to_ascii_lowercase()) + T::iter().find(|x| x.to_string().eq_ignore_ascii_case(&use_input)) } else { T::iter().find(|x| x.to_string() == use_input) }; @@ -67,6 +67,10 @@ pub struct App { #[clap(short = 'v', long = "verbose")] pub verbose: bool, + /// Suppress all output including application output. Note RUST_LOG=off can be used to suppress only vopono log/error output. + #[clap(long = "silent")] + pub silent: bool, + /// read sudo password from program specified in SUDO_ASKPASS environment variable #[clap(short = 'A', long = "askpass")] pub askpass: bool, diff --git a/src/args_config.rs b/src/args_config.rs index 1ccb9d4..5ba9591 100644 --- a/src/args_config.rs +++ b/src/args_config.rs @@ -129,10 +129,8 @@ impl ArgsConfig { let hosts = command_else_config_option!(hosts, command, config); let open_ports = command_else_config_option!(open_ports, command, config); let forward = command_else_config_option!(forward, command, config); - dbg!(&command.postup); // TODO let postup = command_else_config_option!(postup, command, config) .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); - dbg!(&postup); let predown = command_else_config_option!(predown, command, config) .and_then(|p| shellexpand::full(&p).ok().map(|s| s.into_owned())); let group = command_else_config_option!(group, command, config); diff --git a/src/exec.rs b/src/exec.rs index 7bdad94..cadbf6e 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -17,6 +17,7 @@ use vopono_core::config::vpn::{verify_auth, Protocol}; use vopono_core::network::application_wrapper::ApplicationWrapper; use vopono_core::network::netns::NetworkNamespace; use vopono_core::network::network_interface::NetworkInterface; +use vopono_core::network::port_forwarding::azirevpn::AzireVpnPortForwarding; use vopono_core::network::port_forwarding::natpmpc::Natpmpc; use vopono_core::network::port_forwarding::piapf::Piapf; use vopono_core::network::port_forwarding::Forwarder; @@ -25,7 +26,12 @@ use vopono_core::network::sysctl::SysCtl; use vopono_core::util::{get_config_from_alias, get_existing_namespaces, get_target_subnet}; use vopono_core::util::{parse_command_str, vopono_dir}; -pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> anyhow::Result<()> { +pub fn exec( + command: ExecCommand, + uiclient: &dyn UiClient, + verbose: bool, + silent: bool, +) -> anyhow::Result<()> { // this captures all sigint signals // ignore for now, they are automatically passed on to the child let signals = Signals::new([SIGINT])?; @@ -63,7 +69,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any ); synch( parsed_command.provider.clone(), - Some(parsed_command.protocol.clone()), + &Some(parsed_command.protocol.clone()), uiclient, )?; } @@ -233,7 +239,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient, verbose: bool) -> any } if !parsed_command.create_netns_only { - run_application(&parsed_command, forwarder, &ns, signals)?; + run_application(&parsed_command, forwarder, &ns, signals, silent)?; } else { info!( "Created netns {} - will leave network namespace alive until ctrl+C received", @@ -543,6 +549,26 @@ fn provider_port_forwarding( parsed_command.port_forwarding_callback.as_ref(), )?)) } + Some(VpnProvider::AzireVPN) => { + let azirevpn = vopono_core::config::providers::azirevpn::AzireVPN {}; + let access_token = azirevpn.read_access_token()?; + + if parsed_command.port_forwarding_callback.is_some() { + warn!("Port forwarding callback not supported for AzireVPN - ignoring --port-forwarding-callback"); + } + if ns.wireguard.is_none() { + log::error!( + "AzireVPN Port Forwarding in vopono is only supported for Wireguard" + ) + } + let endpoint_ip = ns.wireguard.as_ref().map(|wg| wg.interface_addresses[0]); + // TODO: Is OpenVPN possible? Could not get it to work manually + + endpoint_ip + .map(|ip| AzireVpnPortForwarding::new(ns, &access_token, ip)) + .transpose()? + .map(|fwd| Box::new(fwd) as Box) + } Some(p) => { error!("Port forwarding not supported for the selected provider: {} - ignoring --port-forwarding", p); None @@ -568,6 +594,7 @@ fn run_application( forwarder: Option>, ns: &NetworkNamespace, signals: SignalsInfo, + silent: bool, ) -> anyhow::Result<()> { let application = ApplicationWrapper::new( ns, @@ -576,6 +603,7 @@ fn run_application( parsed_command.group.clone(), parsed_command.working_directory.clone().map(PathBuf::from), forwarder, + silent, )?; let pid = application.handle.id(); diff --git a/src/main.rs b/src/main.rs index 839381d..83446cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,12 +26,16 @@ fn main() -> anyhow::Result<()> { let app = args::App::parse(); // Set up logging let mut builder = pretty_env_logger::formatted_timed_builder(); - let log_level = if app.verbose { - LevelFilter::Debug - } else { - LevelFilter::Info - }; - builder.filter_level(log_level); + builder.parse_default_env(); + if app.verbose { + builder.filter_level(LevelFilter::Debug); + } + if app.silent { + if app.verbose { + warn!("Verbose and silent flags are mutually exclusive, ignoring verbose flag"); + } + builder.filter_level(LevelFilter::Off); + } builder.init(); let uiclient = CliClient {}; @@ -51,9 +55,10 @@ fn main() -> anyhow::Result<()> { } else { debug!("pactl not found, will not set PULSE_SERVER"); } + let verbose = app.verbose && !app.silent; elevate_privileges(app.askpass)?; clean_dead_namespaces()?; - exec::exec(cmd, &uiclient, app.verbose)? + exec::exec(cmd, &uiclient, verbose, app.silent)? } args::Command::List(listcmd) => { clean_dead_locks()?; @@ -62,11 +67,11 @@ fn main() -> anyhow::Result<()> { args::Command::Synch(synchcmd) => { // If provider given then sync that, else prompt with menu if synchcmd.vpn_provider.is_none() { - sync_menu(&uiclient)?; + sync_menu(&uiclient, synchcmd.protocol.map(|x| x.to_variant()))?; } else { synch( synchcmd.vpn_provider.unwrap().to_variant(), - synchcmd.protocol.map(|x| x.to_variant()), + &synchcmd.protocol.map(|x| x.to_variant()), &uiclient, )?; } diff --git a/src/sync.rs b/src/sync.rs index 6e4138e..020c8ea 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -8,7 +8,7 @@ use vopono_core::util::set_config_permissions; use crate::args::WrappedArg; -pub fn sync_menu(uiclient: &dyn UiClient) -> anyhow::Result<()> { +pub fn sync_menu(uiclient: &dyn UiClient, protocol: Option) -> anyhow::Result<()> { let variants = WrappedArg::::value_variants() .iter() .filter(|x| { @@ -30,7 +30,7 @@ pub fn sync_menu(uiclient: &dyn UiClient) -> anyhow::Result<()> { .into_iter() .flat_map(|x| WrappedArg::::from_str(&variants[x], true)) { - synch(provider.to_variant(), None, uiclient)?; + synch(provider.to_variant(), &protocol, uiclient)?; } Ok(()) @@ -38,7 +38,7 @@ pub fn sync_menu(uiclient: &dyn UiClient) -> anyhow::Result<()> { pub fn synch( provider: VpnProvider, - protocol: Option, + protocol: &Option, uiclient: &dyn UiClient, ) -> anyhow::Result<()> { // TODO: Separate availability from functionality, so we can filter disabled protocols from the UI @@ -67,6 +67,7 @@ pub fn synch( error!("vopono sync not supported for None protocol"); } // TODO: Fix this asking for same credentials twice + // Move auth and auth caching to base part of provider then share it for both None => { if let Ok(p) = provider.get_dyn_wireguard_provider() { info!("Starting Wireguard configuration..."); diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index 9e5c393..f82fcd2 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vopono_core" description = "Library code for running VPN connections in network namespaces" -version = "0.1.10" +version = "0.1.11" edition = "2021" authors = ["James McMurray "] license = "GPL-3.0-or-later" @@ -14,9 +14,9 @@ keywords = ["vopono", "vpn", "wireguard", "openvpn", "netns"] anyhow = "1" directories-next = "2" log = "0.4" -which = "6" +which = "7" users = "0.11" -nix = { version = "0.28", features = ["user", "signal", "fs", "process"] } +nix = { version = "0.29", features = ["user", "signal", "fs", "process"] } serde = { version = "1", features = ["derive", "std"] } csv = "1" regex = "1" @@ -30,14 +30,14 @@ reqwest = { default-features = false, version = "0.12", features = [ "json", "rustls-tls", ] } # TODO: Can we remove Tokio dependency? -sysinfo = "0.30" +sysinfo = "0.33" base64 = "0.22" x25519-dalek = { version = "2", features = ["static_secrets"] } strum = "0.26" strum_macros = "0.26" -zip = "0.6" +zip = "2" maplit = "1" -webbrowser = "0.8" +webbrowser = "1" serde_json = "1" signal-hook = "0.3" sha2 = "0.10" @@ -45,3 +45,4 @@ tiny_http = "0.12" chrono = "0.4" json = "0.12" shell-words = "1" +dns-lookup = "2" diff --git a/vopono_core/src/config/providers/azirevpn/mod.rs b/vopono_core/src/config/providers/azirevpn/mod.rs index 89eb7cb..040dbec 100644 --- a/vopono_core/src/config/providers/azirevpn/mod.rs +++ b/vopono_core/src/config/providers/azirevpn/mod.rs @@ -1,23 +1,22 @@ mod openvpn; mod wireguard; -use super::{Input, OpenVpnProvider, Password, Provider, UiClient, WireguardProvider}; +use super::{ + ConfigurationChoice, Input, OpenVpnProvider, Password, Provider, UiClient, WireguardProvider, +}; use crate::config::vpn::Protocol; -use crate::network::wireguard::{de_socketaddr, de_vec_ipaddr, de_vec_ipnet}; -use ipnet::IpNet; +use anyhow::Context; use serde::Deserialize; -use std::net::IpAddr; +use std::io::Write; +use std::{net::IpAddr, path::PathBuf}; // AzireVPN details: https://www.azirevpn.com/docs/servers - +// servers: https://www.azirevpn.com/service/servers#openvpn pub struct AzireVPN {} impl AzireVPN { - fn server_aliases(&self) -> &[&str] { - &[ - "ca1", "dk1", "fr1", "de1", "it1", "es1", "nl1", "no1", "ro1", "se1", "se2", "ch1", - "th1", "us1", "us2", "uk1", - ] + fn locations_url(&self) -> &str { + "https://api.azirevpn.com/v2/locations" } } impl Provider for AzireVPN { @@ -35,21 +34,169 @@ impl Provider for AzireVPN { #[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] -struct ConnectResponse { +struct ReplaceKeyResponse { + status: String, + data: Vec, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct KeyResponse { + key: String, // public key + created_at: i64, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct ExistingDeviceResponse { + status: String, + data: ExistingDeviceResponseData, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct ExistingDevicesResponse { + status: String, + data: Vec, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct ExistingDeviceResponseData { + id: String, + ipv4_address: String, + ipv4_netmask: u8, + ipv6_address: String, + ipv6_netmask: u8, + dns: Vec, + device_name: String, + keys: Vec, +} + +impl ConfigurationChoice for ExistingDevicesResponse { + fn prompt(&self) -> String { + "The following Wireguard devices exist on your account, which would you like to use (you will need to enter the private key or replace the existing keys)".to_string() + } + + fn all_names(&self) -> Vec { + let mut v: Vec = self.data.iter().map(|x| x.id.clone()).collect(); + v.push("Create a new device".to_string()); + v + } + + fn all_descriptions(&self) -> Option> { + let mut v: Vec = self + .data + .iter() + .map(|x| format!("{}, {}", x.device_name, x.ipv4_address)) + .collect(); + v.push("generate a new keypair".to_string()); + Some(v) + } + fn description(&self) -> Option { + None + } +} + +impl ConfigurationChoice for ExistingDeviceResponseData { + fn prompt(&self) -> String { + "The selected device has the following public keys assigned - select which one you wish to use (and enter the private key for)".to_string() + } + + fn all_names(&self) -> Vec { + self.keys.iter().map(|x| x.key.clone()).collect() + } + + fn all_descriptions(&self) -> Option> { + None + } + fn description(&self) -> Option { + None + } +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct UserProfileResponse { status: String, - data: WgResponse, + data: UserProfileResponseData, } +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct UserProfileResponseData { + username: String, + email: String, + currency: String, + is_email_verified: bool, + is_active: bool, + is_oldschool: bool, + is_subscribed: bool, + ips: UserIpsData, + created_at: i64, + expires_at: i64, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct UserIpsData { + allocated: u32, + available: u32, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct AccessTokenResponse { + status: String, + user: UserResponse, + token: String, + device_name: String, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct UserResponse { + username: String, + email: String, + email_verified: bool, + active: bool, + expires_at: i64, + subscription: bool, + is_oldschool: bool, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct DeviceResponse { + status: String, + ipv4: IpResponse, + ipv6: IpResponse, + dns: Vec, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct IpResponse { + address: String, + netmask: u8, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Clone)] +struct LocationsResponse { + status: String, + locations: Vec, +} + +#[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] -struct WgResponse { - #[serde(alias = "DNS", deserialize_with = "de_vec_ipaddr")] - dns: Option>, - #[serde(alias = "Address", deserialize_with = "de_vec_ipnet")] - address: Vec, - #[serde(alias = "PublicKey")] - public_key: String, - #[serde(alias = "Endpoint", deserialize_with = "de_socketaddr")] - endpoint: std::net::SocketAddr, +struct LocationResponse { + name: String, + city: String, + country: String, + iso: String, + pool: String, + pubkey: String, } impl AzireVPN { @@ -66,4 +213,60 @@ impl AzireVPN { let password = password.trim(); Ok((username.to_string(), password.to_string())) } + + fn token_file_path(&self) -> PathBuf { + self.provider_dir().unwrap().join("token.txt") + } + + pub fn get_access_token(&self, uiclient: &dyn UiClient) -> anyhow::Result { + let token_file_path = self.token_file_path(); + if token_file_path.exists() { + let token = std::fs::read_to_string(&token_file_path)?; + log::debug!( + "AzireVPN Auth Token read from {}", + self.token_file_path().display() + ); + return Ok(token); + } + let (username, password) = self.request_userpass(uiclient)?; + let client = reqwest::blocking::Client::new(); + let auth_response: AccessTokenResponse = client + .post("https://api.azirevpn.com/v2/auth/client") + .form(&[ + ("username", &username), + ("password", &password), + ("comment", &"web generator".to_string()), + ]) + .send()? + .json() + .with_context(|| { + "Authentication error: Ensure your AzireVPN credentials are correct" + })?; + + // log::debug!("auth_response: {:?}", &auth_response); + let mut outfile = std::fs::File::create(self.token_file_path())?; + write!(outfile, "{}", auth_response.token)?; + log::debug!( + "AzireVPN Auth Token written to {}", + self.token_file_path().display() + ); + + Ok(auth_response.token) + } + + pub fn read_access_token(&self) -> anyhow::Result { + let token_file_path = self.token_file_path(); + if token_file_path.exists() { + let token = std::fs::read_to_string(&token_file_path)?; + log::debug!( + "AzireVPN Auth Token read from {}", + self.token_file_path().display() + ); + return Ok(token); + } + Err(anyhow::anyhow!( + "AzireVPN Auth Token not found at {}", + token_file_path.display() + )) + } } diff --git a/vopono_core/src/config/providers/azirevpn/openvpn.rs b/vopono_core/src/config/providers/azirevpn/openvpn.rs index 9b3822c..6dc868a 100644 --- a/vopono_core/src/config/providers/azirevpn/openvpn.rs +++ b/vopono_core/src/config/providers/azirevpn/openvpn.rs @@ -1,17 +1,24 @@ use super::AzireVPN; +use super::LocationResponse; +use super::LocationsResponse; use super::OpenVpnProvider; +use crate::config::providers::Input; use crate::config::providers::UiClient; use crate::config::vpn::OpenVpnProtocol; use crate::util::delete_all_files_in_dir; use log::{debug, info}; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use reqwest::header::COOKIE; use std::fs::create_dir_all; use std::fs::File; use std::io::Write; use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; +use std::time::Duration; impl OpenVpnProvider for AzireVPN { - // AzireVPN details: https://www.azirevpn.com/docs/servers + // AzireVPN details: https://www.azirevpn.com/service/servers#dns // TODO: Add IPv6 DNS fn provider_dns(&self) -> Option> { Some(vec![ @@ -29,31 +36,74 @@ impl OpenVpnProvider for AzireVPN { } fn create_openvpn_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> { - let protocol = uiclient.get_configuration_choice(&OpenVpnProtocol::default())?; - // TODO: Allow port selection, TLS version selection + // OpenVPN: + // https://manager.azirevpn.com/account/openvpn/generate?country=ca-tor&os=linux-cli&port=random&protocol=udp + let protocol = OpenVpnProtocol::index_to_variant( + uiclient.get_configuration_choice(&OpenVpnProtocol::default())?, + ); + + let mut auth_cookie: &'static str = Box::leak(uiclient.get_input(Input { + prompt: "Please log-in at https://manager.azirevpn.com/account/openvpn and copy the value of the 'az' cookie in the request data from your browser's network request inspector.".to_owned(), + validator: None + })?.replace(';', "").trim().to_owned().into_boxed_str()); + + debug!("Using az cookie: {}", &auth_cookie); + if !auth_cookie.starts_with("az=") { + auth_cookie = Box::leak(format!("az={}", auth_cookie).into_boxed_str()); + } + let openvpn_dir = self.openvpn_dir()?; let country_map = crate::util::country_map::code_to_country_map(); + let client = reqwest::blocking::Client::new(); + let mut headers = HeaderMap::new(); + headers.insert(COOKIE, HeaderValue::from_static(auth_cookie)); create_dir_all(&openvpn_dir)?; delete_all_files_in_dir(&openvpn_dir)?; - for alias in self.server_aliases() { - let url = format!("https://www.azirevpn.com/cfg/openvpn/generate?country={alias}&os=linux-cli&nat=1&port=random&protocol={protocol}&tls=gcm&keys=0"); - let file = reqwest::blocking::get(&url)?.bytes()?; - - let file_contents = std::str::from_utf8(&file)?; - let file_contents = file_contents - .split('\n') - .filter(|&x| !(x.starts_with("up ") || x.starts_with("down "))) - .collect::>() - .join("\n"); + let locations_resp: LocationsResponse = client.get(self.locations_url()).send()?.json()?; + let locations = locations_resp.locations; + for location in locations { + let location_name = &location.name; let country = country_map - .get(&alias[0..2]) + .get(&location_name[0..2]) .expect("Could not map country to name"); - let filename = format!("{country}-{alias}.ovpn"); + let filename = format!("{country}-{location_name}.ovpn"); + + let mut file_contents = get_openvpn_file( + &location, + &client, + &headers, + auth_cookie, + &protocol.to_string(), + ); + if file_contents.is_err() { + log::info!("Sleeping 90 seconds to avoid rate limiting..."); + std::thread::sleep(Duration::from_secs(90)); + file_contents = get_openvpn_file( + &location, + &client, + &headers, + auth_cookie, + &protocol.to_string(), + ); + } + if file_contents.is_err() { + log::error!( + "Failed to get valid OpenVPN config for location: {} - even after retry.", + location_name + ); + log::info!("Sleeping 60 seconds to avoid rate limiting..."); + std::thread::sleep(Duration::from_secs(60)); + continue; + } + + let file_contents = file_contents.unwrap(); + debug!("Writing file: {}", filename); let mut outfile = File::create(openvpn_dir.join(filename.to_lowercase().replace(' ', "_")))?; write!(outfile, "{file_contents}")?; + std::thread::sleep(Duration::from_millis(200)); } // Write OpenVPN credentials file @@ -70,3 +120,38 @@ impl OpenVpnProvider for AzireVPN { Ok(()) } } + +fn get_openvpn_file( + location: &LocationResponse, + client: &reqwest::blocking::Client, + headers: &HeaderMap, + mut auth_cookie: &str, + protocol: &str, +) -> anyhow::Result { + let location_name = &location.name; + let url = format!("https://manager.azirevpn.com/account/openvpn/generate?country={location_name}&os=linux-cli&port=random&protocol={protocol}"); + + let response = client.get(url).headers(headers.clone()).send()?; + let new_cookie = response.headers().get_all(COOKIE); + new_cookie.iter().for_each(|x| { + if x.to_str().unwrap().starts_with("az=") && auth_cookie != x.to_str().unwrap() { + log::debug!("New az cookie: {}", x.to_str().unwrap()); + auth_cookie = Box::leak(x.to_str().unwrap().to_owned().into_boxed_str()); + } + }); + let file = response.bytes()?; + + let file_contents = std::str::from_utf8(&file)?; + if !file_contents.contains("BEGIN CERTIFICATE") { + log::debug!("File contents: {}", &file_contents); + log::error!("Failed to get valid OpenVPN config for location: {} - could be rate limiting or invalid az cookie.", location_name); + return Err(anyhow::anyhow!("Failed to get valid OpenVPN config for location: {} - check the az cookie is given correctly", location_name)); + } + let file_contents = file_contents + .split('\n') + .filter(|&x| !(x.starts_with("up ") || x.starts_with("down "))) + .collect::>() + .join("\n"); + + Ok(file_contents) +} diff --git a/vopono_core/src/config/providers/azirevpn/wireguard.rs b/vopono_core/src/config/providers/azirevpn/wireguard.rs index 092c0c7..0476b5b 100644 --- a/vopono_core/src/config/providers/azirevpn/wireguard.rs +++ b/vopono_core/src/config/providers/azirevpn/wireguard.rs @@ -1,18 +1,99 @@ -use super::AzireVPN; -use super::{ConnectResponse, WgResponse, WireguardProvider}; -use crate::config::providers::UiClient; +use super::{AzireVPN, ExistingDeviceResponseData}; +use super::{DeviceResponse, LocationsResponse, WireguardProvider}; +use crate::config::providers::azirevpn::{ + ExistingDevicesResponse, LocationResponse, ReplaceKeyResponse, UserProfileResponse, +}; +use crate::config::providers::{BoolChoice, UiClient}; use crate::network::wireguard::{WireguardConfig, WireguardInterface, WireguardPeer}; use crate::util::country_map::code_to_country_map; use crate::util::delete_all_files_in_dir; -use crate::util::wireguard::{generate_keypair, WgKey}; +use crate::util::wireguard::{generate_keypair, generate_public_key, WgKey}; +use anyhow::Context; use ipnet::IpNet; use log::{debug, info}; use regex::Regex; use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; use std::fs::create_dir_all; use std::io::Write; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; +impl AzireVPN { + fn upload_wg_key( + &self, + keypair: &WgKey, + token: &str, + client: &Client, + ) -> anyhow::Result { + let device_response: DeviceResponse = client + .post("https://api.azirevpn.com/v2/ip/add") + .form(&[("key", keypair.public.as_str()), ("token", token)]) + .send()? + .json()?; + + debug!("device_response: {:?}", &device_response); + + let v4_net = IpNet::new( + IpAddr::V4(Ipv4Addr::from_str(&device_response.ipv4.address)?), + device_response.ipv4.netmask, + )?; + let interface = WireguardInterface { + private_key: keypair.private.clone(), + address: vec![v4_net], + dns: Some(device_response.dns), + }; + + Ok(interface) + } + + // Replaces all keys for the given device + fn replace_wg_key( + &self, + device: &ExistingDeviceResponseData, + keypair: &WgKey, + token: &str, + client: &Client, + ) -> anyhow::Result { + let replace_key_response: ReplaceKeyResponse = client + .put(format!( + "https://api.azirevpn.com/v3/ips/{}/keys", + device.id + )) + .bearer_auth(token) + .json(&serde_json::json!({ "key": keypair.public })) + .send()? + .json() + .with_context(|| "Deserialisation of ReplaceKeyResponse failed")?; + + debug!("replace_key_response: {:?}", &replace_key_response); + + // Look up new device details + // TODO: This fails with a 500 internal error - for now just use all devices list + // debug!("Getting details for device ID: {}", id); + // let existing_device_response: ExistingDeviceResponse = client + // .get(format!("https://api.azirevpn.com/v3/ips/{}", id)) + // .bearer_auth(token) + // .send()? + // .json() + // .with_context(|| "Deserialisation of ExistingDeviceResponse failed")?; + + // debug!("existing_device_response: {:?}", &existing_device_response); + + let v4_net = IpNet::new( + IpAddr::V4(Ipv4Addr::from_str(&device.ipv4_address)?), + device.ipv4_netmask, + )?; + let interface = WireguardInterface { + private_key: keypair.private.clone(), + address: vec![v4_net], + dns: Some(device.dns.clone()), + }; + + Ok(interface) + } +} + impl WireguardProvider for AzireVPN { fn create_wireguard_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> { let wireguard_dir = self.wireguard_dir()?; @@ -21,49 +102,171 @@ impl WireguardProvider for AzireVPN { let client = Client::new(); - // TODO: Hardcoded list, can this be retrieved from the API? - let aliases = self.server_aliases(); let country_map = code_to_country_map(); - let (username, password) = self.request_userpass(uiclient)?; - let keypair: WgKey = generate_keypair()?; - debug!("Chosen keypair: {:?}", keypair); - - let mut peers: Vec<(String, WgResponse)> = vec![]; - for alias in aliases { - let response = client - .post(reqwest::Url::parse(&format!( - "https://api.azirevpn.com/v1/wireguard/connect/{alias}" - ))?) - .form(&[ - ("username", &username), - ("password", &password), - ("pubkey", &keypair.public), - ]) - .send()?; - debug!("Response: {:?}", response); - let response: ConnectResponse = response.json()?; - - peers.push((alias.to_string(), response.data)); + // TODO: Allow user to specify existing device and provide private key + + // This creates an API token for the user if we do not have one cached + let token = self.get_access_token(uiclient)?; + // TODO: Check account is active and credentials okay + let user_profile_response: UserProfileResponse = client + .get("https://api.azirevpn.com/v3/users/me") + .header("Authorization", format!("Bearer {}", token)) + .send()? + .json().with_context(|| "Failed to parse AzireVPN user profile response - if this persists try deleting cached data at ~/.config/vopono/azire/ and/or manually deleting access tokens at https://manager.azirevpn.com/account/token")?; + + if !user_profile_response.data.is_active { + log::error!( + "AzireVPN reports that account is inactive - please check your account status" + ); } - // TODO: Allow custom port - need to check AzireVPN's restrictions - // let port = 51820; + // Note with AzireVPN it is possible to replace keys but keep an existing device + // This could be useful for separate long-term port forwarding set ups + // So we also support replacing keys for existing devices - let allowed_ips = vec![IpNet::from_str("0.0.0.0/0")?, IpNet::from_str("::0/0")?]; + // WireguardInterface is defined by device selection + let interface: WireguardInterface = if user_profile_response.data.ips.allocated > 0 { + // Existing Wireguard devices registered - ask to select and enter private key + // Or replace existing keys with new keypair + let existing_devices: ExistingDevicesResponse = client + .get("https://api.azirevpn.com/v3/ips") + .header("Authorization", format!("Bearer {}", token)) + .send()? + .json() + .with_context(|| "Failed to parse existing devices response")?; - // TODO: avoid hacky regex for TOML -> wireguard config conversion - let re = Regex::new(r"=\s\[(?P[^\]]+)\]")?; - for (alias, wg_peer) in peers { - let interface = WireguardInterface { - private_key: keypair.private.clone(), - address: wg_peer.address, - dns: wg_peer.dns, - }; + let selection = uiclient.get_configuration_choice(&existing_devices)?; + + if selection > existing_devices.data.len() { + if user_profile_response.data.ips.allocated + >= user_profile_response.data.ips.available + { + log::error!("Maximum number of devices registered - please delete an existing device at https://manager.azirevpn.com/wireguard before creating a new one"); + return Err(anyhow::anyhow!("Maximum number of devices registered")); + } + // Create new device + let keypair: WgKey = generate_keypair()?; + debug!("Chosen keypair: {:?}", keypair); + self.upload_wg_key(&keypair, &token, &client)? + } else { + let existing_device = &existing_devices.data[selection]; + let replace_keys = uiclient.get_bool_choice(BoolChoice { + prompt: "Would you like to replace the existing keys for this device?" + .to_string(), + default: false, + })?; + + if replace_keys { + // Replace existing keys + let keypair: WgKey = generate_keypair()?; + debug!("Chosen keypair: {:?}", keypair); + self.replace_wg_key(existing_device, &keypair, &token, &client)? + } else { + // Use existing device + // TODO: Refactor common code between this and Mullvad key management + let pubkey = if existing_device.keys.len() > 1 { + let key_selection = uiclient.get_configuration_choice(existing_device)?; + existing_device.keys[key_selection].key.clone() + } else { + existing_device.keys[0].key.clone() + }; + let pubkey_clone = pubkey.clone(); + + // Check number of public keys - if more than 1 prompt for key to use + let private_key = uiclient.get_input(crate::config::providers::Input { + prompt: format!( + "Private key for {} - {}", + existing_device.device_name, pubkey + ), + validator: Some(Box::new( + move |private_key: &String| -> Result<(), String> { + let private_key = private_key.trim(); + + if private_key.len() != 44 { + return Err( + "Expected private key length of 44 characters".to_string() + ); + } + + match generate_public_key(private_key) { + Ok(public_key) => { + if public_key != pubkey_clone { + return Err( + "Private key does not match public key".to_string() + ); + } + Ok(()) + } + Err(_) => Err("Failed to generate public key".to_string()), + } + }, + )), + })?; + + let v4_net = IpNet::new( + IpAddr::V4(Ipv4Addr::from_str(&existing_device.ipv4_address)?), + existing_device.ipv4_netmask, + )?; + WireguardInterface { + private_key, + address: vec![v4_net], + dns: Some(existing_device.dns.clone()), + } + } + } + } else { + // No existing devices - create new device + // Note max devices is limited to 10 registered, 5 concurrent connections + // Start device and keypair generation + let keypair: WgKey = generate_keypair()?; + debug!("Chosen keypair: {:?}", keypair); + self.upload_wg_key(&keypair, &token, &client)? + }; + + // Save keypair + let details = WireguardDetails::from_interface(&interface); + if let Ok(det) = details { + let path = self.wireguard_dir()?.join("wireguard_device.json"); + { + let mut f = std::fs::File::create(path.clone())?; + write!( + f, + "{}", + serde_json::to_string(&det) + .expect("JSON serialisation of WireguardDetails failed") + )?; + } + info!( + "Saved Wireguard keypair details to {}", + &path.to_string_lossy() + ); + } else { + log::error!("Failed to save Wireguard keypair details: {:?}", details); + } + + // This gets locations data from token + let location_resp: LocationsResponse = client.get(self.locations_url()).send()?.json()?; + + debug!("locations_response: {:?}", &location_resp); + let locations: Vec = location_resp.locations; + + let allowed_ips = vec![IpNet::from_str("0.0.0.0/0")?, IpNet::from_str("::0/0")?]; + let re = Regex::new(r"=\s\[(?P[^\]]+)\]")?; + for location in locations { + // TODO: Can we avoid DNS lookup here? + let host_lookup = dns_lookup::lookup_host(&location.pool); + if host_lookup.is_err() { + log::error!("Could not resolve hostname: {}, skipping...", location.pool); + continue; + } + let host_ip = host_lookup.unwrap().first().cloned().unwrap(); + log::debug!("Resolved hostname: {} to IP: {}", &location.pool, &host_ip); + // TODO: avoid hacky regex for TOML -> wireguard config conversion let wireguard_peer = WireguardPeer { - public_key: wg_peer.public_key.clone(), + public_key: location.pubkey.clone(), allowed_ips: allowed_ips.clone(), - endpoint: wg_peer.endpoint, + endpoint: SocketAddr::new(host_ip, 51820), keepalive: None, }; @@ -71,12 +274,13 @@ impl WireguardProvider for AzireVPN { interface: interface.clone(), peer: wireguard_peer, }; + let location_name = location.name.as_str(); let country = country_map - .get(&alias[0..2]) + .get(&location_name[0..2]) .expect("Could not map country code"); - let path = wireguard_dir.join(format!("{country}-{alias}.conf")); + let path = wireguard_dir.join(format!("{country}-{location_name}.conf")); let mut toml = toml::to_string(&wireguard_conf)?; toml.retain(|c| c != '"'); @@ -97,3 +301,21 @@ impl WireguardProvider for AzireVPN { Ok(()) } } + +// TODO: Can we add AzireVPN device name here? +#[derive(Serialize, Deserialize, Debug, Clone)] +struct WireguardDetails { + public_key: String, + private_key: String, + addresses: Vec, +} + +impl WireguardDetails { + fn from_interface(interface: &WireguardInterface) -> anyhow::Result { + Ok(WireguardDetails { + public_key: generate_public_key(interface.private_key.as_str())?, + private_key: interface.private_key.clone(), + addresses: interface.address.clone(), + }) + } +} diff --git a/vopono_core/src/config/providers/mod.rs b/vopono_core/src/config/providers/mod.rs index f50a06e..a7d2d77 100644 --- a/vopono_core/src/config/providers/mod.rs +++ b/vopono_core/src/config/providers/mod.rs @@ -1,5 +1,5 @@ mod airvpn; -mod azirevpn; +pub mod azirevpn; mod hma; mod ivpn; mod mozilla; diff --git a/vopono_core/src/config/providers/mullvad/openvpn.rs b/vopono_core/src/config/providers/mullvad/openvpn.rs index a7298ed..09bd213 100644 --- a/vopono_core/src/config/providers/mullvad/openvpn.rs +++ b/vopono_core/src/config/providers/mullvad/openvpn.rs @@ -147,7 +147,7 @@ impl OpenVpnProvider for Mullvad { }; for (file_name, mut remote_vec) in file_set.into_iter() { - let mut file = File::create(&openvpn_dir.join(file_name))?; + let mut file = File::create(openvpn_dir.join(file_name))?; writeln!(file, "{}", settings.join("\n"))?; remote_vec.shuffle(&mut rand::thread_rng()); diff --git a/vopono_core/src/config/providers/nordvpn/openvpn.rs b/vopono_core/src/config/providers/nordvpn/openvpn.rs index 9e51b1e..7f71e25 100644 --- a/vopono_core/src/config/providers/nordvpn/openvpn.rs +++ b/vopono_core/src/config/providers/nordvpn/openvpn.rs @@ -73,10 +73,10 @@ impl OpenVpnProvider for NordVPN { .extension() .map(|x| x.to_str().expect("Could not convert OsStr")) { - let fname = file - .enclosed_name() - .and_then(|x| x.file_name()) - .and_then(|x| x.to_str()); + let enclosed_name = file.enclosed_name(); + let fname = enclosed_name + .and_then(|x| x.file_name().map(|x| x.to_string_lossy().to_string())); + if fname.is_none() { debug!("Could not parse filename: {}", file.name().to_string()); continue; diff --git a/vopono_core/src/config/providers/pia/openvpn.rs b/vopono_core/src/config/providers/pia/openvpn.rs index 74356ea..44a1542 100644 --- a/vopono_core/src/config/providers/pia/openvpn.rs +++ b/vopono_core/src/config/providers/pia/openvpn.rs @@ -77,6 +77,8 @@ impl OpenVpnProvider for PrivateInternetAccess { hostname_lookup: HashMap::new(), }; + let re = + Regex::new(r"\n *remote +([^ ]+) +\d+ *\n").expect("Failed to compile hostname regex"); for i in 0..zip.len() { // For each file, detect if ovpn, crl or crt // Modify auth line for config @@ -112,8 +114,6 @@ impl OpenVpnProvider for PrivateInternetAccess { 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) diff --git a/vopono_core/src/config/providers/pia/wireguard.rs b/vopono_core/src/config/providers/pia/wireguard.rs index eb8d7fc..7d7bbbe 100644 --- a/vopono_core/src/config/providers/pia/wireguard.rs +++ b/vopono_core/src/config/providers/pia/wireguard.rs @@ -30,12 +30,17 @@ pub struct VpnInfo { #[derive(Debug, Deserialize)] pub struct Region { pub id: String, + #[allow(unused)] pub name: String, + #[allow(unused)] pub country: String, + #[allow(unused)] pub auto_region: bool, pub dns: String, pub port_forward: bool, + #[allow(unused)] pub geo: bool, + #[allow(unused)] pub offline: bool, pub servers: Servers, } @@ -74,8 +79,10 @@ pub struct WireguardServerInfo { pub server_key: String, pub server_port: u16, pub server_ip: IpAddr, + #[allow(unused)] pub server_vip: IpAddr, pub peer_ip: IpAddr, + #[allow(unused)] pub peer_pubkey: String, pub dns_servers: Vec, } diff --git a/vopono_core/src/config/vpn.rs b/vopono_core/src/config/vpn.rs index 770ffbd..e61d23c 100644 --- a/vopono_core/src/config/vpn.rs +++ b/vopono_core/src/config/vpn.rs @@ -20,7 +20,9 @@ pub enum OpenVpnProtocol { impl OpenVpnProtocol { pub fn index_to_variant(index: usize) -> Self { - Self::iter().nth(index).expect("Invalid index") + Self::iter() + .nth(index) + .expect("Invalid index for OpenVPN Protocol enum") } } impl Default for OpenVpnProtocol { diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index 4956b15..a5e9a5a 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -17,6 +17,7 @@ impl ApplicationWrapper { group: Option, working_directory: Option, port_forwarding: Option>, + silent: bool, ) -> anyhow::Result { let running_processes = get_all_running_process_names(); let app_vec = parse_command_str(application)?; @@ -46,7 +47,7 @@ impl ApplicationWrapper { app_vec_ptrs.as_slice(), user, group, - false, + silent, false, false, working_directory, diff --git a/vopono_core/src/network/dns_config.rs b/vopono_core/src/network/dns_config.rs index 3296ee9..9e05d2e 100644 --- a/vopono_core/src/network/dns_config.rs +++ b/vopono_core/src/network/dns_config.rs @@ -102,15 +102,14 @@ impl DnsConfig { format!("Failed to set file permissions for {}", &nsswitch_path) })?; + let hosts_re = Regex::new(r"^hosts:.*$").expect("Failed to compile hosts regex"); for line in std::io::BufReader::new(nsswitch_src).lines() { writeln!( nsswitch, "{}", - Regex::new(r"^hosts:.*$") - .unwrap() - .replace(&line?, |_caps: &Captures| { - "hosts: files mymachines myhostname dns" - }) + hosts_re.replace(&line?, |_caps: &Captures| { + "hosts: files mymachines myhostname dns" + }) ) .with_context(|| { format!("Failed to overwrite nsswitch.conf: /etc/netns/{ns_name}/nsswitch.conf") diff --git a/vopono_core/src/network/openfortivpn.rs b/vopono_core/src/network/openfortivpn.rs index 829cf3b..e974b7b 100644 --- a/vopono_core/src/network/openfortivpn.rs +++ b/vopono_core/src/network/openfortivpn.rs @@ -104,7 +104,7 @@ impl OpenFortiVpn { )?; let dns = get_dns(&buffer)?; - let dns_ip: Vec = (dns.0).into_iter().map(IpAddr::from).collect(); + let dns_ip: Vec = (dns.0).into_iter().collect(); // TODO: Avoid this meaningless collect let suffixes: Vec<&str> = (dns.1).iter().map(|x| x.as_str()).collect(); netns.dns_config( diff --git a/vopono_core/src/network/openvpn.rs b/vopono_core/src/network/openvpn.rs index 2772a24..71bffc0 100644 --- a/vopono_core/src/network/openvpn.rs +++ b/vopono_core/src/network/openvpn.rs @@ -17,6 +17,7 @@ pub struct OpenVpn { pid: u32, pub openvpn_dns: Option, pub logfile: PathBuf, + // pub distinct_remotes: Vec, // Unique IP Addresses or hostnames } impl OpenVpn { @@ -161,13 +162,14 @@ impl OpenVpn { if buffer.contains("AUTH_FAILED") { if auth_file.is_some() { error!( - "OpenVPN authentication failed, deleting {}", + "OpenVPN authentication failed, modify your username and/or password in {}", auth_file.as_ref().unwrap().display() ); - std::fs::remove_file(auth_file.unwrap())?; + // std::fs::remove_file(auth_file.unwrap())?; } return Err(anyhow!( - "OpenVPN authentication failed, use -v for full log output" + "OpenVPN authentication failed, use -v for full log output. Modify your username and/or password in {}", + auth_file.as_ref().unwrap().display() )); } if buffer.contains("Options error") { diff --git a/vopono_core/src/network/port_forwarding/azirevpn.rs b/vopono_core/src/network/port_forwarding/azirevpn.rs new file mode 100644 index 0000000..0f989d9 --- /dev/null +++ b/vopono_core/src/network/port_forwarding/azirevpn.rs @@ -0,0 +1,195 @@ +// https://www.azirevpn.com/docs/api/portforwardings#create-portforwarding +// AzireVPN Port Forwarding needs to send one request from *INSIDE* the network namespace +// Then handle open port +// Attempt to destroy port forwarding on Drop + +use std::net::IpAddr; + +use crate::network::netns::NetworkNamespace; +use anyhow::Context; +use serde::Deserialize; + +use super::Forwarder; + +pub struct AzireVpnPortForwarding { + pub port: u16, + pub local_ip: IpAddr, + pub access_token: String, + pub netns_name: String, + // TODO: We could run check endpoint but it means we need to temporarily listen on this port too + // But it would confirm success and give us our remote IP + // TODO: Do we want to look up remote IP from ifconfig.co? +} + +// Unused since we use curl here for now +// #[derive(Serialize, Debug)] +// struct RequestBody { +// internal_ipv4: String, +// hidden: bool, +// expires_in: u32, +// } + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct CreateResponse { + status: String, + data: CreateResponseData, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct CreateResponseData { + internal_ipv4: String, + internal_ipv6: String, + port: u16, + hidden: bool, + expires_at: u64, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct ListResponse { + status: String, + data: ListResponseData, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct ListResponseData { + internal_ipv4: String, + internal_ipv6: String, + ports: Vec, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct PortData { + port: u16, + hidden: bool, + expires_at: u64, +} + +impl AzireVpnPortForwarding { + // This must run on forked process inside the network namespace + // Could just use curl? + pub fn new( + netns: &NetworkNamespace, + access_token: &str, + local_ip: IpAddr, + ) -> anyhow::Result { + // Check if any port forwarding exists for current connection + log::info!("Sleeping 10 seconds so connection is up before requesting port forwarding"); + std::thread::sleep(std::time::Duration::from_secs(10)); + let cmd = [ + "curl", + Box::leak( + format!( + "https://api.azirevpn.com/v3/portforwardings?internal_ipv4={}", + local_ip + ) + .into_boxed_str(), + ), + "-H", + Box::leak(format!("Authorization: Bearer {}", access_token).into_boxed_str()), + ]; + + let output = NetworkNamespace::exec_with_output(&netns.name, &cmd)?; + let output_string = String::from_utf8(output.stdout.clone())?; + log::debug!("AzireVPN Port forwarding list response: {}", output_string); + + let output_data_result: anyhow::Result = serde_json::from_str(&output_string) + .with_context(|| "Failed to parse JSON response from listing AzireVPN Port Forwarding"); + + // If so, return that port + if let Ok(output_data) = output_data_result { + if !output_data.data.ports.is_empty() { + let port = output_data.data.ports[0].port; + log::info!("Port forwarding already enabled on port {}", port); + return Ok(Self { + port, + local_ip, + access_token: access_token.to_string(), + netns_name: netns.name.clone(), + }); + } + } + + // If not, create a new port forwarding + let cmd = [ + "curl", + "https://api.azirevpn.com/v3/portforwardings", + "-H", + Box::leak(format!("Authorization: Bearer {}", access_token).into_boxed_str()), + "--json", + Box::leak( + format!( + "{{\"internal_ipv4\": \"{}\", \"hidden\": false, \"expires_in\": 30}}", + local_ip + ) + .into_boxed_str(), + ), + ]; + + let output = NetworkNamespace::exec_with_output(&netns.name, &cmd)?; + let output_string = String::from_utf8(output.stdout.clone())?; + + log::debug!( + "AzireVPN Port forwarding creation response: {}", + output_string + ); + let data: CreateResponse = + serde_json::from_str(output_string.as_str()).with_context(|| { + "Failed to parse JSON response from creating AzireVPN Port Forwarding" + })?; + + log::info!( + "AzireVPN Port forwarding enabled on port {}", + data.data.port + ); + Ok(Self { + port: data.data.port, + local_ip, + access_token: access_token.to_string(), + netns_name: netns.name.clone(), + }) + } +} + +impl Forwarder for AzireVpnPortForwarding { + fn forwarded_port(&self) -> u16 { + self.port + } +} + +impl Drop for AzireVpnPortForwarding { + fn drop(&mut self) { + let cmd = [ + "curl", + "-X", + "DELETE", + "https://api.azirevpn.com/v3/portforwardings", + "-H", + Box::leak(format!("Authorization: Bearer {}", self.access_token).into_boxed_str()), + "--json", + Box::leak( + format!( + "{{\"internal_ipv4\": \"{}\", \"port\": {}}}", + self.local_ip, self.port + ) + .into_boxed_str(), + ), + ]; + + // Note this must run BEFORE the network namespace is destroyed + let output = std::process::Command::new("ip") + .arg("netns") + .arg("exec") + .arg(&self.netns_name) + .args(cmd) + .output() + .expect("Failed to destroy AzireVPN Port Forwarding"); + + let output_string = String::from_utf8(output.stdout.clone()).unwrap(); + log::info!("AzireVPN Port forwarding destroyed: {}", output_string); + } +} diff --git a/vopono_core/src/network/port_forwarding/mod.rs b/vopono_core/src/network/port_forwarding/mod.rs index d01a0a1..7cd214d 100644 --- a/vopono_core/src/network/port_forwarding/mod.rs +++ b/vopono_core/src/network/port_forwarding/mod.rs @@ -2,6 +2,7 @@ use std::sync::mpsc::Receiver; use super::netns::NetworkNamespace; +pub mod azirevpn; pub mod natpmpc; pub mod piapf; diff --git a/vopono_core/src/network/wireguard.rs b/vopono_core/src/network/wireguard.rs index 269afd7..7a50852 100644 --- a/vopono_core/src/network/wireguard.rs +++ b/vopono_core/src/network/wireguard.rs @@ -15,10 +15,11 @@ use std::str::FromStr; #[derive(Serialize, Deserialize, Debug)] pub struct Wireguard { - ns_name: String, - config_file: PathBuf, - firewall: Firewall, - if_name: String, + pub ns_name: String, + pub config_file: PathBuf, + pub firewall: Firewall, + pub if_name: String, + pub interface_addresses: Vec, } impl Wireguard { @@ -112,10 +113,12 @@ impl Wireguard { std::fs::remove_file("/tmp/vopono_nft.conf") .context("Deleting file: /tmp/vopono_nft.conf") .ok(); + let mut interface_addresses: Vec = Vec::new(); // Extract addresses for address in config.interface.address.iter() { match address { IpNet::V6(address) => { + interface_addresses.push(IpAddr::V6(address.addr())); NetworkNamespace::exec( &namespace.name, &[ @@ -130,6 +133,7 @@ impl Wireguard { )?; } IpNet::V4(address) => { + interface_addresses.push(IpAddr::V4(address.addr())); NetworkNamespace::exec( &namespace.name, &[ @@ -404,6 +408,7 @@ impl Wireguard { ns_name: namespace.name.clone(), firewall, if_name, + interface_addresses, }) } } @@ -548,7 +553,7 @@ impl Drop for Wireguard { } } -#[derive(Deserialize, Debug, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone)] pub struct WireguardInterface { #[serde(rename = "PrivateKey")] pub private_key: String, @@ -558,6 +563,16 @@ pub struct WireguardInterface { pub dns: Option>, } +impl std::fmt::Debug for WireguardInterface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WireguardInterface") + .field("private_key", &"********".to_string()) + .field("address", &self.address) + .field("dns", &self.dns) + .finish() + } +} + #[derive(Deserialize, Debug, Serialize)] pub struct WireguardPeer { #[serde(rename = "PublicKey")] diff --git a/vopono_core/src/util/country_map.rs b/vopono_core/src/util/country_map.rs index 742a197..64ec6e6 100644 --- a/vopono_core/src/util/country_map.rs +++ b/vopono_core/src/util/country_map.rs @@ -46,6 +46,7 @@ pub fn code_to_country_map() -> HashMap<&'static str, &'static str> { "ge" => "georgia", "gd" => "grenada", "uk" => "united_kingdom", + "gb" => "united_kingdom", "ga" => "gabon", "sv" => "el_salvador", "gn" => "guinea", diff --git a/vopono_core/src/util/mod.rs b/vopono_core/src/util/mod.rs index 012f7ec..8eef6d8 100644 --- a/vopono_core/src/util/mod.rs +++ b/vopono_core/src/util/mod.rs @@ -208,23 +208,26 @@ pub fn get_pids_in_namespace(ns_name: &str) -> anyhow::Result> { } pub fn check_process_running(pid: u32) -> bool { - let s = - System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); + let s = System::new_with_specifics( + RefreshKind::everything().with_processes(ProcessRefreshKind::everything()), + ); s.process(sysinfo::Pid::from_u32(pid)).is_some() } pub fn get_all_running_pids() -> Vec { - let s = - System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); + let s = System::new_with_specifics( + RefreshKind::everything().with_processes(ProcessRefreshKind::everything()), + ); s.processes().keys().map(|x| x.as_u32()).collect() } pub fn get_all_running_process_names() -> Vec { - let s = - System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); + let s = System::new_with_specifics( + RefreshKind::everything().with_processes(ProcessRefreshKind::everything()), + ); s.processes() .values() - .map(|x| x.name().to_string()) + .map(|x| x.name().to_string_lossy().to_string()) .collect() } @@ -345,6 +348,7 @@ pub fn elevate_privileges(askpass: bool) -> anyhow::Result<()> { flag::register(SIGINT, Arc::clone(&terminated))?; let sudo_flags = if askpass { "-AE" } else { "-E" }; + // TODO: This isn't passing RUST_LOG ? debug!("Args: {:?}", &args); // status blocks until the process has ended diff --git a/vopono_core/src/util/wireguard.rs b/vopono_core/src/util/wireguard.rs index 4b9e46d..13f13be 100644 --- a/vopono_core/src/util/wireguard.rs +++ b/vopono_core/src/util/wireguard.rs @@ -10,12 +10,21 @@ use x25519_dalek::{PublicKey, StaticSecret}; const B64_ENGINE: GeneralPurpose = general_purpose::STANDARD; -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Clone)] pub struct WgKey { pub public: String, pub private: String, } +impl std::fmt::Debug for WgKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgKey") + .field("public", &self.public) + .field("private", &"********".to_string()) + .finish() + } +} + #[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] pub struct WgPeer {