summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 14:25:56 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-29 15:53:56 +0200
commit8e637b9d8b73e729db214a0f6af49ec6ad606129 (patch)
tree2180a8aab4817f72fff20ecc7ee2cd218589674e
parent6d14475cd2b8c50ed0486349169c4440a1ff4d26 (diff)
downloadmullvadvpn-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>
-rw-r--r--mullvad-daemon/src/migrations/mod.rs3
-rw-r--r--mullvad-daemon/src/migrations/snapshots/mullvad_daemon__migrations__v11__test__v11_to_v12_migration_access_method_name_duplicates.snap136
-rw-r--r--mullvad-daemon/src/migrations/v11.rs326
-rw-r--r--mullvad-types/src/settings/mod.rs4
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"
))),