Skip to content

Commit

Permalink
Merge pull request #246 from jamesmcm/mullvad_wg_devices
Browse files Browse the repository at this point in the history
Re-add Mullvad Wireguard device management
  • Loading branch information
jamesmcm authored Jan 20, 2024
2 parents d89807a + 2d900be commit 6919ca8
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
7 changes: 4 additions & 3 deletions vopono_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
Expand All @@ -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"
1 change: 1 addition & 0 deletions vopono_core/src/config/providers/mozilla/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl ConfigurationChoice for Devices {
}
}

// TODO: Update API calls for new API
impl MozillaVPN {
fn upload_new_device(
&self,
Expand Down
36 changes: 29 additions & 7 deletions vopono_core/src/config/providers/mullvad/mod.rs
Original file line number Diff line number Diff line change
@@ -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<WgPeer>,
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 {}
Expand Down
227 changes: 220 additions & 7 deletions vopono_core/src/config/providers/mullvad/wireguard.rs
Original file line number Diff line number Diff line change
@@ -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<Device> {
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<Device> = 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()?;
Expand All @@ -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);

Expand Down Expand Up @@ -114,7 +328,7 @@ struct WireguardRelay {
}

struct Devices {
devices: Vec<WgPeer>,
devices: Vec<Device>,
}

impl ConfigurationChoice for Devices {
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion vopono_core/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 6919ca8

Please sign in to comment.