use super::{Error, Result}; use mullvad_types::{constraints::Constraint, settings::SettingsVersion}; use serde::{Deserialize, Serialize}; // ====================================================== // Section for vendoring types and values that // this settings version depend on. See `mod.rs`. pub type AccountNumber = String; /// Representation of a transport protocol, either UDP or TCP. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TransportProtocol { /// Represents the UDP transport protocol. Udp, /// Represents the TCP transport protocol. Tcp, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct TransportPort { pub protocol: TransportProtocol, pub port: Constraint, } /// Contains obfuscation settings #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct ObfuscationSettings { pub selected_obfuscation: SelectedObfuscation, pub udp2tcp: Udp2TcpObfuscationSettings, } #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct Udp2TcpObfuscationSettings { pub port: Constraint, } #[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum SelectedObfuscation { Auto, Off, Udp2Tcp, } // ====================================================== pub struct MigrationData { pub token: AccountNumber, pub wg_data: Option, } /// # Changes to the format /// /// The ability to disable WireGuard multihop while preserving the entry location was added. /// So a new field, `use_multihop` is introduced. We want this to default to `true` iff: /// * `use_multihop` was not present in the settings /// * A multihop entry location had been previously specified. /// /// It is also no longer valid to have `entry_location` set to null. So remove the field if it /// is null in order to make it default back to the default location. /// /// This also removes the account token and WireGuard key from the settings. /// /// Additionally, the WireGuard protocol constraint, if set to be using TCP, is migrated into /// having an active Udp2Tcp obfuscator. The protocol constraint is then removed from WireGuard /// settings since all WireGuard traffic is UDP. pub fn migrate(settings: &mut serde_json::Value) -> Result> { if !version_matches(settings) { return Ok(None); } log::info!("Migrating settings format to V6"); if let Some(wireguard_constraints) = get_wireguard_constraints(settings) { if let Some(location) = wireguard_constraints.get("entry_location") && wireguard_constraints.get("use_multihop").is_none() { if location.is_null() { // "Null" is no longer valid. It is not an option. wireguard_constraints .as_object_mut() .ok_or(Error::InvalidSettingsContent)? .remove("entry_location"); } else { wireguard_constraints["use_multihop"] = serde_json::json!(true); } } // The field `pub port: Constraint` is now `pub port: Constraint`. // Data is migrated as follows: // If the existing field has `protocol == Tcp` configured, then we need to create a // corresponding setting to enable the Udp2Tcp obfuscator. In this case, the port // constraint is moved as well. Otherwise the existing port constraint is moved into // the new field. // if let Some(port) = wireguard_constraints.get("port") { let port_constraint: Constraint = serde_json::from_value(port.clone()).map_err(|_| Error::InvalidSettingsContent)?; if let Some(transport_port) = port_constraint.option() { let (port, obfuscation_settings) = match transport_port.protocol { TransportProtocol::Udp => (serde_json::json!(transport_port.port), None), TransportProtocol::Tcp => ( serde_json::json!(Constraint::::Any), Some(serde_json::json!(create_migrated_obfuscation_settings( transport_port.port ))), ), }; wireguard_constraints["port"] = port; if let Some(obfuscation_settings) = obfuscation_settings { settings["obfuscation_settings"] = obfuscation_settings; } } } } let migration_data = if let Some(token) = settings.get("account_token").filter(|t| !t.is_null()) { let token: AccountNumber = serde_json::from_value(token.clone()).map_err(|_| Error::InvalidSettingsContent)?; let migration_data = if let Some(wg_data) = settings.get("wireguard").filter(|wg| !wg.is_null()) { Some(MigrationData { token, wg_data: Some(wg_data.clone()), }) } else { Some(MigrationData { token, wg_data: None, }) }; let settings_map = settings .as_object_mut() .ok_or(Error::InvalidSettingsContent)?; settings_map.remove("account_token"); settings_map.remove("wireguard"); migration_data } else { None }; settings["settings_version"] = serde_json::json!(SettingsVersion::V6); Ok(migration_data) } fn get_wireguard_constraints(settings: &mut serde_json::Value) -> Option<&mut serde_json::Value> { if let Some(relay_settings) = settings.get_mut("relay_settings") && let Some(normal) = relay_settings.get_mut("normal") { return normal.get_mut("wireguard_constraints"); } None } // Create an ObfuscationSettings struct that replaces the `protocol == TCP` setting // that was previously used on the wireguard constraints. // If a port is specified, this is the remote port to be used for Udp2Tcp. // fn create_migrated_obfuscation_settings(port: Constraint) -> ObfuscationSettings { ObfuscationSettings { selected_obfuscation: SelectedObfuscation::Udp2Tcp, udp2tcp: Udp2TcpObfuscationSettings { port }, } } fn version_matches(settings: &serde_json::Value) -> bool { settings .get("settings_version") .map(|version| version == SettingsVersion::V5 as u64) .unwrap_or(false) } #[cfg(test)] mod test { use super::{migrate, version_matches}; pub const V5_SETTINGS: &str = r#" { "account_token": "1234", "relay_settings": { "normal": { "location": { "only": { "country": "se" } }, "tunnel_protocol": "any", "wireguard_constraints": { "port": { "only": { "protocol": "tcp", "port": "any" } }, "ip_version": "any", "entry_location": "any" }, "openvpn_constraints": { "port": { "only": { "protocol": "udp", "port": { "only": 1195 } } } } } }, "bridge_settings": { "normal": { "location": "any" } }, "bridge_state": "auto", "allow_lan": true, "block_when_disconnected": false, "auto_connect": false, "tunnel_options": { "openvpn": { "mssfix": null }, "wireguard": { "mtu": null, "rotation_interval": { "secs": 86400, "nanos": 0 } }, "generic": { "enable_ipv6": false }, "dns_options": { "state": "default", "default_options": { "block_ads": false, "block_trackers": false }, "custom_options": { "addresses": [ "1.1.1.1", "1.2.3.4" ] } } }, "settings_version": 5 } "#; pub const V6_SETTINGS: &str = r#" { "relay_settings": { "normal": { "location": { "only": { "country": "se" } }, "tunnel_protocol": "any", "wireguard_constraints": { "port": "any", "ip_version": "any", "use_multihop": true, "entry_location": "any" }, "openvpn_constraints": { "port": { "only": { "protocol": "udp", "port": { "only": 1195 } } } } } }, "bridge_settings": { "normal": { "location": "any" } }, "obfuscation_settings": { "selected_obfuscation": "udp2_tcp", "udp2tcp": { "port": "any" } }, "bridge_state": "auto", "allow_lan": true, "block_when_disconnected": false, "auto_connect": false, "tunnel_options": { "openvpn": { "mssfix": null }, "wireguard": { "mtu": null, "rotation_interval": { "secs": 86400, "nanos": 0 } }, "generic": { "enable_ipv6": false }, "dns_options": { "state": "default", "default_options": { "block_ads": false, "block_trackers": false }, "custom_options": { "addresses": [ "1.1.1.1", "1.2.3.4" ] } } }, "settings_version": 6 } "#; #[tokio::test] async fn test_v5_to_v6_migration() { let mut old_settings = serde_json::from_str(V5_SETTINGS).unwrap(); assert!(version_matches(&old_settings)); migrate(&mut old_settings).unwrap(); let new_settings: serde_json::Value = serde_json::from_str(V6_SETTINGS).unwrap(); assert_eq!(&old_settings, &new_settings); } }