|
| 1 | +use super::Mullvad; |
| 2 | +use super::{ConfigurationChoice, OpenVpnProvider}; |
| 3 | +use crate::util::delete_all_files_in_dir; |
| 4 | +use crate::vpn::OpenVpnProtocol; |
| 5 | +use anyhow::Context; |
| 6 | +use log::warn; |
| 7 | +use rand::seq::SliceRandom; |
| 8 | +use reqwest::blocking::Client; |
| 9 | +use serde::Deserialize; |
| 10 | +use std::collections::HashMap; |
| 11 | +use std::fmt::Display; |
| 12 | +use std::fs::create_dir_all; |
| 13 | +use std::fs::File; |
| 14 | +use std::io::Write; |
| 15 | +use std::net::{IpAddr, Ipv4Addr}; |
| 16 | +use std::path::PathBuf; |
| 17 | +use strum::IntoEnumIterator; |
| 18 | +use strum_macros::EnumIter; |
| 19 | + |
| 20 | +impl Mullvad { |
| 21 | + fn get_default_openvpn_settings(&self) -> Vec<&'static str> { |
| 22 | + vec![ |
| 23 | + "client", |
| 24 | + "dev tun", |
| 25 | + "resolv-retry infinite", |
| 26 | + "nobind", |
| 27 | + "persist-key", |
| 28 | + "persist-tun", |
| 29 | + "verb 3", |
| 30 | + "remote-cert-tls server", |
| 31 | + "ping 10", |
| 32 | + "ping-restart 60", |
| 33 | + "sndbuf 524288", |
| 34 | + "rcvbuf 524288", |
| 35 | + "cipher AES-256-CBC", |
| 36 | + "tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA", |
| 37 | + "auth-user-pass mullvad_userpass.txt", |
| 38 | + "ca mullvad_ca.crt", |
| 39 | + "tun-ipv6", |
| 40 | + "script-security 2", |
| 41 | + ] |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +impl OpenVpnProvider for Mullvad { |
| 46 | + fn provider_dns(&self) -> Option<Vec<IpAddr>> { |
| 47 | + Some(vec![IpAddr::V4(Ipv4Addr::new(193, 138, 218, 74))]) |
| 48 | + } |
| 49 | + |
| 50 | + fn prompt_for_auth(&self) -> anyhow::Result<(String, String)> { |
| 51 | + let username = self.request_mullvad_username()?; |
| 52 | + Ok((username, "m".to_string())) |
| 53 | + } |
| 54 | + |
| 55 | + fn auth_file_path(&self) -> anyhow::Result<PathBuf> { |
| 56 | + Ok(self.openvpn_dir()?.join("mullvad_userpass.txt")) |
| 57 | + } |
| 58 | + |
| 59 | + fn create_openvpn_config(&self) -> anyhow::Result<()> { |
| 60 | + let openvpn_dir = self.openvpn_dir()?; |
| 61 | + create_dir_all(&openvpn_dir)?; |
| 62 | + delete_all_files_in_dir(&openvpn_dir)?; |
| 63 | + |
| 64 | + let client = Client::new(); |
| 65 | + let relays: Vec<OpenVpnRelay> = client |
| 66 | + .get("https://api.mullvad.net/www/relays/openvpn/") |
| 67 | + .send()? |
| 68 | + .json()?; |
| 69 | + |
| 70 | + let mut config_choice = ConfigType::choose_one()?; |
| 71 | + let port = config_choice.generate_port(); |
| 72 | + |
| 73 | + let use_ips = dialoguer::Confirm::new() |
| 74 | + .with_prompt( |
| 75 | + "Use IP addresses instead of hostnames? (may be resistant to DNS blocking, but need to be synced more frequently)" |
| 76 | + ) |
| 77 | + .default(false) |
| 78 | + .interact()?; |
| 79 | + |
| 80 | + let use_bridges = dialoguer::Confirm::new() |
| 81 | + .with_prompt( |
| 82 | + "Connect via a bridge? (route over two separate servers, requires connecting on TCP port 443)" |
| 83 | + ) |
| 84 | + .default(false) |
| 85 | + .interact()?; |
| 86 | + |
| 87 | + let mut settings = self.get_default_openvpn_settings(); |
| 88 | + |
| 89 | + if use_bridges { |
| 90 | + if config_choice != ConfigType::Tcp443 { |
| 91 | + warn!("Overriding chosen protocol and port to TCP 443 due to use of bridge"); |
| 92 | + config_choice = ConfigType::Tcp443; |
| 93 | + } |
| 94 | + settings.push("socks-proxy 127.0.0.1 1080"); |
| 95 | + } |
| 96 | + |
| 97 | + match config_choice.get_protocol() { |
| 98 | + OpenVpnProtocol::UDP => { |
| 99 | + settings.push("proto udp"); |
| 100 | + settings.push("fast-io"); |
| 101 | + } |
| 102 | + OpenVpnProtocol::TCP => { |
| 103 | + settings.push("proto tcp"); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // Group relays by country |
| 108 | + // Generate config file per relay given options |
| 109 | + // Naming: country_name-hostalias.ovpn |
| 110 | + let mut file_set: HashMap<String, Vec<String>> = HashMap::with_capacity(128); |
| 111 | + for relay in relays.into_iter().filter(|x| x.active) { |
| 112 | + let file_name = format!( |
| 113 | + "{}-{}.ovpn", |
| 114 | + relay.country_name.to_lowercase().replace(' ', "_"), |
| 115 | + relay.country_code |
| 116 | + ); |
| 117 | + |
| 118 | + let remote_string = if use_ips { |
| 119 | + format!( |
| 120 | + "remote {} {} # {}", |
| 121 | + relay.ipv4_addr_in, port, relay.hostname |
| 122 | + ) |
| 123 | + } else { |
| 124 | + format!("remote {}.mullvad.net {}", relay.hostname, port) |
| 125 | + }; |
| 126 | + |
| 127 | + file_set |
| 128 | + .entry(file_name) |
| 129 | + .or_insert_with(Vec::new) |
| 130 | + .push(remote_string); |
| 131 | + } |
| 132 | + |
| 133 | + let bridge_vec = if use_bridges { |
| 134 | + let bridges: Vec<OpenVpnRelay> = client |
| 135 | + .get("https://api.mullvad.net/www/relays/bridge/") |
| 136 | + .send()? |
| 137 | + .json()?; |
| 138 | + bridges |
| 139 | + .into_iter() |
| 140 | + .filter(|x| x.active) |
| 141 | + .map(|x| { |
| 142 | + format!( |
| 143 | + "route {} 255.255.255.255 net_gateway # {}", |
| 144 | + x.ipv4_addr_in, x.hostname |
| 145 | + ) |
| 146 | + }) |
| 147 | + .collect::<Vec<String>>() |
| 148 | + } else { |
| 149 | + Vec::new() |
| 150 | + }; |
| 151 | + |
| 152 | + for (file_name, mut remote_vec) in file_set.into_iter() { |
| 153 | + let mut file = File::create(&openvpn_dir.join(file_name))?; |
| 154 | + writeln!(file, "{}", settings.join("\n"))?; |
| 155 | + |
| 156 | + remote_vec.shuffle(&mut rand::thread_rng()); |
| 157 | + writeln!( |
| 158 | + file, |
| 159 | + "{}", |
| 160 | + remote_vec[0..remote_vec.len().min(64)].join("\n") |
| 161 | + )?; |
| 162 | + if remote_vec.len() > 1 { |
| 163 | + writeln!(file, "remote-random")?; |
| 164 | + } |
| 165 | + |
| 166 | + if !bridge_vec.is_empty() { |
| 167 | + writeln!(file, "{}", bridge_vec.join("\n"))?; |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + // Write CA cert |
| 172 | + let ca = include_str!("mullvad_ca.crt"); |
| 173 | + { |
| 174 | + let file = File::create(openvpn_dir.join("mullvad_ca.crt")) |
| 175 | + .context("Could not create mullvad CA file")?; |
| 176 | + let mut write_buf = std::io::BufWriter::new(file); |
| 177 | + write!(write_buf, "{}", ca)?; |
| 178 | + } |
| 179 | + |
| 180 | + // Write OpenVPN credentials file |
| 181 | + let (user, pass) = self.prompt_for_auth()?; |
| 182 | + let mut outfile = File::create(self.auth_file_path()?)?; |
| 183 | + write!(outfile, "{}\n{}", user, pass)?; |
| 184 | + Ok(()) |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +#[derive(EnumIter, PartialEq)] |
| 189 | +enum ConfigType { |
| 190 | + DefaultUdp, |
| 191 | + Udp53, |
| 192 | + Tcp80, |
| 193 | + Tcp443, |
| 194 | +} |
| 195 | + |
| 196 | +impl ConfigType { |
| 197 | + fn get_protocol(&self) -> OpenVpnProtocol { |
| 198 | + match self { |
| 199 | + Self::DefaultUdp => OpenVpnProtocol::UDP, |
| 200 | + Self::Udp53 => OpenVpnProtocol::UDP, |
| 201 | + Self::Tcp80 => OpenVpnProtocol::TCP, |
| 202 | + Self::Tcp443 => OpenVpnProtocol::TCP, |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + fn generate_port(&self) -> u16 { |
| 207 | + match self { |
| 208 | + Self::DefaultUdp => *[1300, 1301, 1302, 1194, 1195, 1196, 1197] |
| 209 | + .choose(&mut rand::thread_rng()) |
| 210 | + .expect("Could not choose default port"), |
| 211 | + Self::Udp53 => 53, |
| 212 | + Self::Tcp80 => 80, |
| 213 | + Self::Tcp443 => 443, |
| 214 | + } |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +impl Display for ConfigType { |
| 219 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 220 | + let s = match self { |
| 221 | + Self::DefaultUdp => "Default (UDP)", |
| 222 | + Self::Udp53 => "UDP (Port 53)", |
| 223 | + Self::Tcp80 => "TCP (Port 80)", |
| 224 | + Self::Tcp443 => "TCP (Port 443)", |
| 225 | + }; |
| 226 | + write!(f, "{}", s) |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +impl Default for ConfigType { |
| 231 | + fn default() -> Self { |
| 232 | + Self::DefaultUdp |
| 233 | + } |
| 234 | +} |
| 235 | + |
| 236 | +impl ConfigurationChoice for ConfigType { |
| 237 | + fn prompt() -> String { |
| 238 | + "Please choose your OpenVPN connection protocol and port".to_string() |
| 239 | + } |
| 240 | + |
| 241 | + fn variants() -> Vec<Self> { |
| 242 | + ConfigType::iter().collect() |
| 243 | + } |
| 244 | + |
| 245 | + fn description(&self) -> Option<String> { |
| 246 | + None |
| 247 | + } |
| 248 | +} |
| 249 | + |
| 250 | +// Note we ignore ipv6 addr here |
| 251 | +#[derive(Deserialize, Debug)] |
| 252 | +struct OpenVpnRelay { |
| 253 | + hostname: String, |
| 254 | + country_code: String, |
| 255 | + country_name: String, |
| 256 | + city_code: String, |
| 257 | + city_name: String, |
| 258 | + active: bool, |
| 259 | + owned: bool, |
| 260 | + provider: String, |
| 261 | + ipv4_addr_in: std::net::Ipv4Addr, |
| 262 | +} |
0 commit comments