diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 14:25:56 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-29 15:53:56 +0200 |
| commit | 8e637b9d8b73e729db214a0f6af49ec6ad606129 (patch) | |
| tree | 2180a8aab4817f72fff20ecc7ee2cd218589674e | |
| parent | 6d14475cd2b8c50ed0486349169c4440a1ff4d26 (diff) | |
| download | mullvadvpn-8e637b9d8b73e729db214a0f6af49ec6ad606129.tar.xz mullvadvpn-8e637b9d8b73e729db214a0f6af49ec6ad606129.zip | |
Add v11 settings migration
- Renames block_when_disconnected to lockdown_mode
- Renames API access methods with non-unique names
Co-authored-by: Joakim Hulthe <joakim.hulthe@mullvad.net>
4 files changed, 468 insertions, 1 deletions
diff --git a/mullvad-daemon/src/migrations/mod.rs b/mullvad-daemon/src/migrations/mod.rs index 251546ef57..284928073e 100644 --- a/mullvad-daemon/src/migrations/mod.rs +++ b/mullvad-daemon/src/migrations/mod.rs @@ -47,6 +47,7 @@ mod account_history; mod device; mod v1; mod v10; +mod v11; mod v2; mod v3; mod v4; @@ -210,6 +211,8 @@ async fn migrate_settings( v10::migrate(settings)?; + v11::migrate(settings)?; + Ok(migration_data) } diff --git a/mullvad-daemon/src/migrations/snapshots/mullvad_daemon__migrations__v11__test__v11_to_v12_migration_access_method_name_duplicates.snap b/mullvad-daemon/src/migrations/snapshots/mullvad_daemon__migrations__v11__test__v11_to_v12_migration_access_method_name_duplicates.snap new file mode 100644 index 0000000000..0bd3fe26ff --- /dev/null +++ b/mullvad-daemon/src/migrations/snapshots/mullvad_daemon__migrations__v11__test__v11_to_v12_migration_access_method_name_duplicates.snap @@ -0,0 +1,136 @@ +--- +source: mullvad-daemon/src/migrations/v11.rs +expression: "serde_json::to_string_pretty(&old_settings).unwrap()" +--- +{ + "api_access_methods": { + "custom": [ + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-128-cfb", + "endpoint": "127.0.0.1:80", + "password": "" + } + } + }, + "enabled": true, + "id": "90d35296-3823-4805-8926-720fff53c752", + "name": "test_3" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-256-cfb", + "endpoint": "127.0.0.1:443", + "password": "secret" + } + } + }, + "enabled": true, + "id": "d879f6e9-c052-4452-8e53-088183d01c0a", + "name": "test_2" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-256-gcm", + "endpoint": "127.0.0.1:443", + "password": "" + } + } + }, + "enabled": true, + "id": "7b49cef8-5a9f-4bbc-9bee-00841edc98e9", + "name": "test" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-128-gcm", + "endpoint": "127.0.0.1:80", + "password": "" + } + } + }, + "enabled": true, + "id": "0bafc4ed-cd4f-4368-b067-74527f42451b", + "name": "test_1" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-128-cfb", + "endpoint": "127.0.0.1:443", + "password": "" + } + } + }, + "enabled": false, + "id": "09d032bc-7e3a-4d85-a63f-528b6c4b890e", + "name": "test_2_1" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-256-gcm", + "endpoint": "127.0.0.1:80", + "password": "secret" + } + } + }, + "enabled": false, + "id": "4471b3f5-a87e-4355-aea8-72c4c4936479", + "name": "test_1_1" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-128-cfb", + "endpoint": "127.0.0.1:443", + "password": "" + } + } + }, + "enabled": false, + "id": "d284a9d5-307b-4959-94a6-89fef8187807", + "name": "test_4" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-256-cfb", + "endpoint": "127.0.0.1:9090", + "password": "" + } + } + }, + "enabled": false, + "id": "6f8db7c3-2258-46c0-8b7d-2016dd9e5739", + "name": "other_name" + }, + { + "access_method": { + "custom": { + "shadowsocks": { + "cipher": "aes-128-gcm", + "endpoint": "127.0.0.1:8080", + "password": "" + } + } + }, + "enabled": true, + "id": "ffdf9900-e843-4298-9478-a9dfbaa63b17", + "name": "test_5" + } + ] + } +} diff --git a/mullvad-daemon/src/migrations/v11.rs b/mullvad-daemon/src/migrations/v11.rs new file mode 100644 index 0000000000..905361bad6 --- /dev/null +++ b/mullvad-daemon/src/migrations/v11.rs @@ -0,0 +1,326 @@ +use super::{Error, Result}; +use mullvad_types::settings::SettingsVersion; + +/// The migration handles: +/// - Renaming of block_when_disconnected option to lockdown_mode. +/// - API access method names must now be unique and duplicates will be renamed. +pub fn migrate(settings: &mut serde_json::Value) -> Result<()> { + if !(version(settings) == Some(SettingsVersion::V11)) { + return Ok(()); + } + + log::info!("Migrating settings format to v12"); + + migrate_block_when_disconnected(settings)?; + migrate_duplicated_api_access_method_names(settings)?; + + settings["settings_version"] = serde_json::json!(SettingsVersion::V12); + + Ok(()) +} + +fn version(settings: &serde_json::Value) -> Option<SettingsVersion> { + settings + .get("settings_version") + .and_then(|version| serde_json::from_value(version.clone()).ok()) +} + +fn migrate_block_when_disconnected(settings: &mut serde_json::Value) -> Result<()> { + let key_name_before = "block_when_disconnected"; + let key_name_after = "lockdown_mode"; + + let settings_map = settings + .as_object_mut() + .ok_or(Error::InvalidSettingsContent)?; + + // Get the old key's value and insert the new key with that value + let value = settings_map + .get(key_name_before) + .ok_or(Error::InvalidSettingsContent)?; + settings_map.insert(key_name_after.to_string(), value.clone()); + + // Remove the old key + settings_map.remove(key_name_before); + + Ok(()) +} + +fn generate_access_method_name_initial_suffix( + access_method_names: &[impl AsRef<str>], + access_method_name: &String, +) -> usize { + let access_method_name_count = access_method_names + .iter() + .filter(|name| name.as_ref() == access_method_name) + .count(); + + let mut suffix = 1; + if access_method_name_count > 1 { + suffix = access_method_name_count - 1 + } + + suffix +} + +/// Only consider renaming access methods with a duplicate name if it has a higher index +/// thab other access methods. This is to ensure that older entries' names are preserved, +/// in favor of renaming newer access methods. +fn get_should_rename_api_access_method( + access_method_names: &[impl AsRef<str>], + access_method_name: &String, + access_method_name_index: usize, +) -> bool { + access_method_names.iter().enumerate().any(|(index, name)| { + access_method_name_index > index && name.as_ref() == *access_method_name + }) +} + +fn generate_access_method_name( + access_method_names: &[impl AsRef<str>], + access_method_name: &String, + access_method_name_index: usize, + access_method_name_suffix: usize, +) -> String { + // Generate a new name for the access method + let generated_access_method_name = format!("{access_method_name}_{access_method_name_suffix}"); + + // Verify if the generated name is unique or if a new name should be generated + let should_rename_api_access_method = get_should_rename_api_access_method( + access_method_names, + &generated_access_method_name, + access_method_name_index, + ); + if should_rename_api_access_method { + // Increment the suffix for the next attempt to generate a new access method name + generate_access_method_name( + access_method_names, + access_method_name, + access_method_name_index, + access_method_name_suffix + 1, + ) + } else { + generated_access_method_name + } +} + +fn migrate_duplicated_api_access_method_names(settings: &mut serde_json::Value) -> Result<()> { + let settings_map = settings + .as_object_mut() + .ok_or(Error::InvalidSettingsContent)?; + + let mut custom_api_access_methods: Vec<&mut String> = settings_map + .get_mut("api_access_methods") + .and_then(serde_json::Value::as_object_mut) + .and_then(|api_access_method| api_access_method.get_mut("custom")?.as_array_mut()) + .into_iter() + .flat_map(|array| array.iter_mut()) + // Take a &mut to each custom api access method name as a String + .filter_map(|custom_api_access_method| custom_api_access_method.as_object_mut()?.get_mut("name")) + .filter_map(|custom_api_access_method| match custom_api_access_method { + serde_json::Value::String(custom_api_access_method_name) => { + Some(custom_api_access_method_name) + } + _ => None, + }) + .collect(); + + for index in 0..custom_api_access_methods.len() { + let access_method_name = &*custom_api_access_methods[index]; + + let should_rename_api_access_method = get_should_rename_api_access_method( + &custom_api_access_methods, + access_method_name, + index, + ); + if should_rename_api_access_method { + let access_method_name_suffix = generate_access_method_name_initial_suffix( + &custom_api_access_methods, + access_method_name, + ); + + let generated_access_method_name = generate_access_method_name( + &custom_api_access_methods, + access_method_name, + index, + access_method_name_suffix, + ); + + // Update the access method's name to the new unique name that was generated + *custom_api_access_methods[index] = generated_access_method_name; + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::migrations::v11::migrate_block_when_disconnected; + use crate::migrations::v11::migrate_duplicated_api_access_method_names; + + /// "block_when_disconnected" is renamed to "lockdown_mode" + #[test] + fn test_v11_to_v12_migration_block_when_disconnected_disabled() { + let mut old_settings = json!({ + "block_when_disconnected": false, + }); + migrate_block_when_disconnected(&mut old_settings).unwrap(); + let new_settings: serde_json::Value = json!({ + "lockdown_mode": false, + }); + assert_eq!(&old_settings, &new_settings); + } + + #[test] + fn test_v11_to_v12_migration_block_when_disconnected_enabled() { + let mut old_settings = json!({ + "block_when_disconnected": true, + }); + migrate_block_when_disconnected(&mut old_settings).unwrap(); + let new_settings: serde_json::Value = json!({ + "lockdown_mode": true, + }); + assert_eq!(&old_settings, &new_settings); + } + + // custom access method's names are renamed if they are not unique + #[test] + fn test_v11_to_v12_migration_access_method_name_duplicates() { + let mut old_settings = json!({ + "api_access_methods": { + "custom": [ + { + "id": "90d35296-3823-4805-8926-720fff53c752", + "name": "test_3", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:80", + "password": "", + "cipher": "aes-128-cfb" + } + } + } + }, + { + "id": "d879f6e9-c052-4452-8e53-088183d01c0a", + "name": "test_2", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:443", + "password": "secret", + "cipher": "aes-256-cfb" + } + } + } + }, + { + "id": "7b49cef8-5a9f-4bbc-9bee-00841edc98e9", + "name": "test", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:443", + "password": "", + "cipher": "aes-256-gcm" + } + } + } + }, + { + "id": "0bafc4ed-cd4f-4368-b067-74527f42451b", + "name": "test_1", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:80", + "password": "", + "cipher": "aes-128-gcm" + } + } + } + }, + { + "id": "09d032bc-7e3a-4d85-a63f-528b6c4b890e", + "name": "test_2", + "enabled": false, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:443", + "password": "", + "cipher": "aes-128-cfb" + } + } + } + }, + { + "id": "4471b3f5-a87e-4355-aea8-72c4c4936479", + "name": "test_1", + "enabled": false, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:80", + "password": "secret", + "cipher": "aes-256-gcm" + } + } + } + }, + { + "id": "d284a9d5-307b-4959-94a6-89fef8187807", + "name": "test", + "enabled": false, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:443", + "password": "", + "cipher": "aes-128-cfb" + } + } + } + }, + { + "id": "6f8db7c3-2258-46c0-8b7d-2016dd9e5739", + "name": "other_name", + "enabled": false, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:9090", + "password": "", + "cipher": "aes-256-cfb" + } + } + } + }, + { + "id": "ffdf9900-e843-4298-9478-a9dfbaa63b17", + "name": "test", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "127.0.0.1:8080", + "password": "", + "cipher": "aes-128-gcm" + } + } + } + } + ] + } + }); + migrate_duplicated_api_access_method_names(&mut old_settings).unwrap(); + insta::assert_snapshot!(serde_json::to_string_pretty(&old_settings).unwrap()); + } +} diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 9c83621bf4..1263089d8f 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -20,7 +20,7 @@ mod dns; /// latest version that exists in `SettingsVersion`. /// This should be bumped when a new version is introduced along with a migration /// being added to `mullvad-daemon`. -pub const CURRENT_SETTINGS_VERSION: SettingsVersion = SettingsVersion::V11; +pub const CURRENT_SETTINGS_VERSION: SettingsVersion = SettingsVersion::V12; #[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] #[repr(u32)] @@ -35,6 +35,7 @@ pub enum SettingsVersion { V9 = 9, V10 = 10, V11 = 11, + V12 = 12, } impl<'de> Deserialize<'de> for SettingsVersion { @@ -53,6 +54,7 @@ impl<'de> Deserialize<'de> for SettingsVersion { v if v == SettingsVersion::V9 as u32 => Ok(SettingsVersion::V9), v if v == SettingsVersion::V10 as u32 => Ok(SettingsVersion::V10), v if v == SettingsVersion::V11 as u32 => Ok(SettingsVersion::V11), + v if v == SettingsVersion::V12 as u32 => Ok(SettingsVersion::V12), v => Err(serde::de::Error::custom(format!( "{v} is not a valid SettingsVersion" ))), |
