Skip to content

Commit

Permalink
Re-add Mullvad Wireguard device management
Browse files Browse the repository at this point in the history
Adds support for new API for Wireguard device management so we can
generate and add Wireguard keys directly when syncing
  • Loading branch information
jamesmcm committed Jan 20, 2024
1 parent d89807a commit 2d900be
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 2d900be

Please sign in to comment.