Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add port forwarding with PIA using --port-forwarding #245

Merged
merged 11 commits into from
Jan 20, 2024
Prev Previous commit
Next Next commit
Re-add Mullvad Wireguard device management
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
commit 2d900be0ad379dfaa4e99147a936a2d363e6b3fa
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
Loading