diff --git a/Cargo.toml b/Cargo.toml index 7c76d0e..cc03847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ directories-next = "2" log = "0.4" pretty_env_logger = "0.5" clap = { version = "4", features = ["derive"] } -which = "5" +which = "6" dialoguer = "0.11" compound_duration = "1" signal-hook = "0.3" diff --git a/src/exec.rs b/src/exec.rs index a037e1e..ff2f06b 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -169,6 +169,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .ok() }); + // TODO: Modify this to allow creating base netns only // Assign protocol and server from args or vopono config file or custom config if used if let Some(path) = &custom_config { protocol = command @@ -382,6 +383,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> firewall, )?; _sysctl = SysCtl::enable_ipv4_forwarding(); + + // TODO: Skip this if netns config only match protocol { Protocol::Warp => ns.run_warp( command.open_ports.as_ref(), diff --git a/vopono_core/Cargo.toml b/vopono_core/Cargo.toml index f98e707..c77709a 100644 --- a/vopono_core/Cargo.toml +++ b/vopono_core/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["vopono", "vpn", "wireguard", "openvpn", "netns"] anyhow = "1" directories-next = "2" log = "0.4" -which = "5" +which = "6" users = "0.11" nix = { version = "0.27", features = ["user", "signal", "fs", "process"] } serde = { version = "1", features = ["derive", "std"] } @@ -30,7 +30,7 @@ reqwest = { default-features = false, version = "0.11", features = [ "json", "rustls-tls", ] } # TODO: Can we remove Tokio dependency? -sysinfo = "0.29" +sysinfo = "0.30" base64 = "0.21" x25519-dalek = { version = "2", features = ["static_secrets"] } strum = "0.25" @@ -40,5 +40,6 @@ maplit = "1" webbrowser = "0.8" serde_json = "1" signal-hook = "0.3" -sha2 = "0.10.6" +sha2 = "0.10" tiny_http = "0.12" +chrono = "0.4" diff --git a/vopono_core/src/config/providers/mozilla/wireguard.rs b/vopono_core/src/config/providers/mozilla/wireguard.rs index 5af7922..47cac2b 100644 --- a/vopono_core/src/config/providers/mozilla/wireguard.rs +++ b/vopono_core/src/config/providers/mozilla/wireguard.rs @@ -45,6 +45,7 @@ impl ConfigurationChoice for Devices { } } +// TODO: Update API calls for new API impl MozillaVPN { fn upload_new_device( &self, diff --git a/vopono_core/src/config/providers/mullvad/mod.rs b/vopono_core/src/config/providers/mullvad/mod.rs index b6a3e24..6bbec15 100644 --- a/vopono_core/src/config/providers/mullvad/mod.rs +++ b/vopono_core/src/config/providers/mullvad/mod.rs @@ -1,23 +1,45 @@ mod openvpn; mod wireguard; +use std::fmt::Display; + use super::{ ConfigurationChoice, Input, OpenVpnProvider, Provider, ShadowsocksProvider, UiClient, WireguardProvider, }; use crate::config::vpn::Protocol; -use crate::util::wireguard::WgPeer; use anyhow::anyhow; use serde::Deserialize; -#[allow(dead_code)] +#[derive(Deserialize, Debug)] +struct AccessToken { + access_token: String, +} + #[derive(Deserialize, Debug, Clone)] struct UserInfo { - max_ports: u8, - active: bool, - max_wg_peers: u8, - can_add_wg_peers: bool, - wg_peers: Vec, + expiry: String, + max_devices: u8, + can_add_devices: bool, +} + +#[derive(Deserialize, Debug, Clone)] +struct Device { + name: String, + pubkey: String, + created: String, + ipv4_address: String, + ipv6_address: String, +} + +impl Display for Device { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}: {} (created: {})", + self.name, self.pubkey, self.created + ) + } } pub struct Mullvad {} diff --git a/vopono_core/src/config/providers/mullvad/wireguard.rs b/vopono_core/src/config/providers/mullvad/wireguard.rs index 905836c..34f2aa0 100644 --- a/vopono_core/src/config/providers/mullvad/wireguard.rs +++ b/vopono_core/src/config/providers/mullvad/wireguard.rs @@ -1,20 +1,234 @@ use super::Mullvad; use super::WireguardProvider; +use crate::config::providers::mullvad::AccessToken; +use crate::config::providers::mullvad::Device; +use crate::config::providers::mullvad::UserInfo; +use crate::config::providers::BoolChoice; use crate::config::providers::{ConfigurationChoice, Input, InputNumericu16, UiClient}; use crate::network::wireguard::{WireguardConfig, WireguardInterface, WireguardPeer}; use crate::util::delete_all_files_in_dir; -use crate::util::wireguard::{generate_public_key, WgKey, WgPeer}; -use anyhow::Context; +use crate::util::wireguard::generate_keypair; +use crate::util::wireguard::{generate_public_key, WgKey}; +use anyhow::{anyhow, Context}; +use chrono::DateTime; +use chrono::Utc; use ipnet::IpNet; +use log::warn; use log::{debug, info}; use regex::Regex; use reqwest::blocking::Client; +use reqwest::header::AUTHORIZATION; use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; use std::fs::create_dir_all; use std::io::Write; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; +#[derive(Serialize, Deserialize, Debug, Clone)] +struct PrivateDevice { + public_key: String, + private_key: String, + ipv4_address: String, + ipv6_address: String, +} + +impl PrivateDevice { + fn from_device(device: &Device, private_key: &str) -> Self { + PrivateDevice { + public_key: device.pubkey.clone(), + private_key: private_key.to_owned(), + ipv4_address: device.ipv4_address.clone(), + ipv6_address: device.ipv6_address.clone(), + } + } +} + +impl Mullvad { + fn upload_wg_key( + client: &Client, + access_token: &str, + keypair: &WgKey, + ) -> anyhow::Result { + let mut map = HashMap::new(); + map.insert("pubkey", keypair.public.clone()); + let device: Device = client + .post("https://api.mullvad.net/accounts/v1/devices") + .header(AUTHORIZATION, format!("Bearer {access_token}")) + .json(&map) + .send() + .context("Failed to upload keypair to Mullvad")? + .error_for_status()? + .json()?; + info!( + "Public key {} submitted to Mullvad. Private key will be saved in generated config files.", &keypair.public + ); + Ok(device) + } + + fn prompt_for_wg_key(&self, uiclient: &dyn UiClient) -> anyhow::Result<(WgKey, IpNet, IpNet)> { + // - Get or upload keypair from/to Mullvad + // - List existing keys + // - Create new keypair and upload (save keypair locally too) + // - Choose key and enter private key (validate that is valid for this public key) + // - Enter previously uploaded keypair manually + + let use_automatic = uiclient.get_bool_choice(BoolChoice { + prompt: "Handle Mullvad key upload automatically?".to_string(), + default: true, + })?; + + if use_automatic { + let client = Client::new(); + let username = self.request_mullvad_username(uiclient)?; + + let mut map = HashMap::new(); + map.insert("account_number", username.clone()); + + let auth: AccessToken = client + .post("https://api.mullvad.net/auth/v1/token".to_owned()) + .json(&map) + .send()? + .json()?; + + let user_info: UserInfo = client + .get("https://api.mullvad.net/accounts/v1/accounts/me") + .header(AUTHORIZATION, format!("Bearer {}", &auth.access_token)) + .send()? + .json()?; + + // Warn if account expired + match DateTime::parse_from_rfc3339(&user_info.expiry) { + Ok(datetime) => { + let datetime_utc = datetime.with_timezone(&Utc); + if datetime_utc <= Utc::now() { + warn!("Mullvad account expired on {}", &user_info.expiry); + } + } + Err(e) => warn!("Could not parse Mullvad account expiry date: {}", e), + } + + debug!("Received user info: {:?}", user_info); + + let existing_devices: Vec = client + .get("https://api.mullvad.net/accounts/v1/devices") + .header(AUTHORIZATION, format!("Bearer {}", &auth.access_token)) + .send()? + .json()?; + + if !existing_devices.is_empty() { + let existing = Devices { devices: existing_devices.clone()}; + + let selection = uiclient.get_configuration_choice(&existing)?; + + if selection >= existing_devices.len() { + if existing_devices.len() >= user_info.max_devices as usize + || !user_info.can_add_devices + { + return Err(anyhow!("Cannot add more Wireguard keypairs to this account. Try to delete existing keypairs.")); + } + let keypair = generate_keypair()?; + let dev = Mullvad::upload_wg_key(&client, &auth.access_token, &keypair)?; + + // Save keypair + let path = self.wireguard_dir()?.join("wireguard_device.json"); + { + let mut f = std::fs::File::create(path.clone())?; + write!(f, "{}", serde_json::to_string(&PrivateDevice::from_device(&dev, &keypair.private))?)?; + } + info!("Saved Wireguard keypair details to {}", &path.to_string_lossy()); + + Ok((keypair, IpNet::from_str(&dev.ipv4_address).expect("Invalid IPv4 address"), IpNet::from_str(&dev.ipv6_address).expect("Invalid IPv6 address"))) + } else { + let dev = existing_devices[selection].clone(); + let pubkey_clone = dev.pubkey.clone(); + + let private_key = uiclient.get_input(Input{ + prompt: format!("Private key for {}", + &existing.devices[selection].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()) + }}))})?; + + // Save keypair + let path = self.wireguard_dir()?.join("wireguard_device.json"); + { + let mut f = std::fs::File::create(path.clone())?; + write!(f, "{}", serde_json::to_string(&PrivateDevice::from_device(&dev, &private_key))?)?; + } + info!("Saved Wireguard keypair details to {}", &path.to_string_lossy()); + + + Ok((WgKey { + public: dev.pubkey.clone(), + private: private_key, + }, + IpNet::from_str(&dev.ipv4_address).expect("Invalid IPv4 address"), IpNet::from_str(&dev.ipv6_address).expect("Invalid IPv6 address")) + ) + } + } else if uiclient.get_bool_choice(BoolChoice{ + prompt: + "No Wireguard keys currently exist on your Mullvad account, would you like to generate a new keypair?".to_string(), + default: true, + })? + { + let keypair = generate_keypair()?; + let dev = Mullvad::upload_wg_key(&client, &auth.access_token, &keypair)?; + + // Save keypair + let path = self.wireguard_dir()?.join("wireguard_device.json"); + { + let mut f = std::fs::File::create(path.clone())?; + write!(f, "{}", serde_json::to_string(&PrivateDevice::from_device(&dev, &keypair.private))?)?; + } + info!("Saved Wireguard keypair details to {}", &path.to_string_lossy()); + + Ok((keypair, IpNet::from_str(&dev.ipv4_address).expect("Invalid IPv4 address"), IpNet::from_str(&dev.ipv6_address).expect("Invalid IPv6 address"))) + } else { + Err(anyhow!("Wireguard requires a keypair, either upload one to Mullvad or let vopono generate one")) + } + } else { + let manual_dev = get_manually_entered_keypair(uiclient)?; + // Save keypair + let path = self.wireguard_dir()?.join("wireguard_device.json"); + { + let mut f = std::fs::File::create(path.clone())?; + write!( + f, + "{}", + serde_json::to_string(&PrivateDevice { + public_key: manual_dev.0.public.clone(), + private_key: manual_dev.0.private.clone(), + ipv4_address: manual_dev.1.to_string(), + ipv6_address: manual_dev.2.to_string() + })? + )?; + } + info!( + "Saved Wireguard keypair details to {}", + &path.to_string_lossy() + ); + Ok(manual_dev) + } + } +} + impl WireguardProvider for Mullvad { fn create_wireguard_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> { let wireguard_dir = self.wireguard_dir()?; @@ -27,7 +241,7 @@ impl WireguardProvider for Mullvad { .send()? .json().with_context(|| "Failed to parse Mullvad relays response - try again after a few minutes or report an issue if it is persistent")?; - let (keypair, ipv4_net, ipv6_net) = prompt_for_wg_key(uiclient)?; + let (keypair, ipv4_net, ipv6_net) = self.prompt_for_wg_key(uiclient)?; debug!("Chosen keypair: {:?}", keypair); @@ -114,7 +328,7 @@ struct WireguardRelay { } struct Devices { - devices: Vec, + devices: Vec, } impl ConfigurationChoice for Devices { @@ -135,9 +349,8 @@ impl ConfigurationChoice for Devices { None } } - -fn prompt_for_wg_key(uiclient: &dyn UiClient) -> anyhow::Result<(WgKey, IpNet, IpNet)> { - // TODO: We could also generate new private key first - generate_keypair() +fn get_manually_entered_keypair(uiclient: &dyn UiClient) -> anyhow::Result<(WgKey, IpNet, IpNet)> { + // Manual keypair entry let private_key = uiclient.get_input(Input { prompt: "Enter your Wireguard Private key and upload the Public Key as a Mullvad device" .to_owned(), diff --git a/vopono_core/src/util/mod.rs b/vopono_core/src/util/mod.rs index 2bc989c..da474c3 100644 --- a/vopono_core/src/util/mod.rs +++ b/vopono_core/src/util/mod.rs @@ -23,7 +23,7 @@ use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; -use sysinfo::{PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt}; +use sysinfo::{ProcessRefreshKind, RefreshKind, System}; use users::{get_current_uid, get_user_by_uid}; use walkdir::WalkDir; use which::which;