use super::Mullvad;
use super::{ConfigurationChoice, OpenVpnProvider};
use crate::config::providers::{BoolChoice, UiClient};
use crate::config::vpn::OpenVpnProtocol;
use crate::util::delete_all_files_in_dir;
use anyhow::Context;
use log::warn;
use rand::prelude::{IndexedRandom, SliceRandom};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt::Display;
use std::fs::File;
use std::fs::create_dir_all;
use std::io::Write;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
impl Mullvad {
fn get_default_openvpn_settings(&self) -> Vec<&'static str> {
vec![
"client",
"dev tun",
"resolv-retry infinite",
"nobind",
"persist-key",
"persist-tun",
"verb 3",
"remote-cert-tls server",
"ping 10",
"ping-restart 60",
"sndbuf 524288",
"rcvbuf 524288",
"cipher AES-256-CBC",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA",
"auth-user-pass mullvad_userpass.txt",
"ca mullvad_ca.crt",
"tun-ipv6",
"script-security 2",
]
}
}
impl OpenVpnProvider for Mullvad {
fn provider_dns(&self) -> Option<Vec<IpAddr>> {
Some(vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))])
}
fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)> {
let username = self.request_mullvad_username(uiclient)?;
Ok((username, "m".to_string()))
}
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("mullvad_userpass.txt")))
}
fn create_openvpn_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> {
let openvpn_dir = self.openvpn_dir()?;
create_dir_all(&openvpn_dir)?;
delete_all_files_in_dir(&openvpn_dir)?;
let client = Client::new();
let relays: Vec<OpenVpnRelay> = client
.get("https://api.mullvad.net/www/relays/openvpn/")
.send()?
.json()?;
let mut config_choice = ConfigType::index_to_variant(
uiclient.get_configuration_choice(&ConfigType::default())?,
);
let port = config_choice.generate_port();
let use_ips = uiclient.get_bool_choice(BoolChoice{
prompt: "Use IP addresses instead of hostnames? (may be resistant to DNS blocking, but need to be synced more frequently)".to_string(),
default: false,
})?;
let use_bridges = uiclient.get_bool_choice(BoolChoice{
prompt:
"Connect via a bridge? (route over two separate servers, requires connecting on TCP port 443)".to_string(),
default: false})?;
let mut settings = self.get_default_openvpn_settings();
if use_bridges {
if config_choice != ConfigType::Tcp443 {
warn!("Overriding chosen protocol and port to TCP 443 due to use of bridge");
config_choice = ConfigType::Tcp443;
}
settings.push("socks-proxy 127.0.0.1 1080");
}
match config_choice.get_protocol() {
OpenVpnProtocol::UDP => {
settings.push("proto udp");
settings.push("fast-io");
}
OpenVpnProtocol::TCP => {
settings.push("proto tcp");
}
}
let mut file_set: HashMap<String, Vec<String>> = HashMap::with_capacity(128);
for relay in relays.into_iter().filter(|x| x.active) {
let file_name = format!(
"{}-{}.ovpn",
relay.country_name.to_lowercase().replace(' ', "_"),
relay.country_code
);
let remote_string = if use_ips {
format!(
"remote {} {} # {}",
relay.ipv4_addr_in, port, relay.hostname
)
} else {
format!("remote {}.relays.mullvad.net {}", relay.hostname, port)
};
file_set.entry(file_name).or_default().push(remote_string);
}
let bridge_vec = if use_bridges {
let bridges: Vec<OpenVpnRelay> = client
.get("https://api.mullvad.net/www/relays/bridge/")
.send()?
.json()?;
bridges
.into_iter()
.filter(|x| x.active)
.map(|x| {
format!(
"route {} 255.255.255.255 net_gateway # {}",
x.ipv4_addr_in, x.hostname
)
})
.collect::<Vec<String>>()
} else {
Vec::new()
};
for (file_name, mut remote_vec) in file_set.into_iter() {
let mut file = File::create(openvpn_dir.join(file_name))?;
writeln!(file, "{}", settings.join("\n"))?;
remote_vec.shuffle(&mut rand::rng());
writeln!(
file,
"{}",
remote_vec[0..remote_vec.len().min(64)].join("\n")
)?;
if remote_vec.len() > 1 {
writeln!(file, "remote-random")?;
}
if !bridge_vec.is_empty() {
writeln!(file, "{}", bridge_vec.join("\n"))?;
}
}
let ca = include_str!("mullvad_ca.crt");
{
let file = File::create(openvpn_dir.join("mullvad_ca.crt"))
.context("Could not create mullvad CA file")?;
let mut write_buf = std::io::BufWriter::new(file);
write!(write_buf, "{ca}")?;
}
let (user, pass) = self.prompt_for_auth(uiclient)?;
let auth_file = self.auth_file_path()?;
if auth_file.is_some() {
let mut outfile = File::create(auth_file.unwrap())?;
write!(outfile, "{user}\n{pass}")?;
}
Ok(())
}
}
#[derive(EnumIter, PartialEq)]
enum ConfigType {
DefaultUdp,
Udp53,
Tcp80,
Tcp443,
}
impl ConfigType {
fn get_protocol(&self) -> OpenVpnProtocol {
match self {
Self::DefaultUdp => OpenVpnProtocol::UDP,
Self::Udp53 => OpenVpnProtocol::UDP,
Self::Tcp80 => OpenVpnProtocol::TCP,
Self::Tcp443 => OpenVpnProtocol::TCP,
}
}
fn index_to_variant(index: usize) -> Self {
Self::iter().nth(index).expect("Invalid index")
}
fn generate_port(&self) -> u16 {
match self {
Self::DefaultUdp => *[1300, 1301, 1302, 1194, 1195, 1196, 1197]
.choose(&mut rand::rng())
.expect("Could not choose default port"),
Self::Udp53 => 53,
Self::Tcp80 => 80,
Self::Tcp443 => 443,
}
}
}
impl Display for ConfigType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::DefaultUdp => "Default (UDP)",
Self::Udp53 => "UDP (Port 53)",
Self::Tcp80 => "TCP (Port 80)",
Self::Tcp443 => "TCP (Port 443)",
};
write!(f, "{s}")
}
}
impl Default for ConfigType {
fn default() -> Self {
Self::DefaultUdp
}
}
impl ConfigurationChoice for ConfigType {
fn prompt(&self) -> String {
"Please choose your OpenVPN connection protocol and port".to_string()
}
fn all_names(&self) -> Vec<String> {
Self::iter().map(|x| format!("{x}")).collect()
}
fn all_descriptions(&self) -> Option<Vec<String>> {
None
}
fn description(&self) -> Option<String> {
None
}
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct OpenVpnRelay {
hostname: String,
country_code: String,
country_name: String,
city_code: String,
city_name: String,
active: bool,
owned: bool,
provider: String,
ipv4_addr_in: std::net::Ipv4Addr,
}