//! This module provides functionality for updating settings using a JSON string, i.e. applying a //! patch. It is intended to be relatively safe, preventing editing of "dangerous" settings such as //! custom DNS. //! //! Patching the settings is a three-step procedure: //! 1. Validating the input. Only a subset of settings is allowed to be edited using this method. //! Attempting to edit prohibited or invalid settings results in an error. //! 2. Merging the changes. When the patch has been accepted, it can be applied to the existing //! settings. How they're merged depends on the actual setting. See [MergeStrategy]. //! 3. Deserialize the resulting JSON back to a [Settings] instance, and, if valid, replace the //! existing settings. //! //! Permitted settings and merge strategies are defined in the [PERMITTED_SUBKEYS] constant. //! //! This implementation must be kept in sync with the //! [spec](../../../docs/settings-patch-format.md). use super::SettingsPersister; use mullvad_types::settings::Settings; #[derive(thiserror::Error, Debug)] pub enum Error { /// Missing expected JSON object #[error("Incorrect or missing value: {0}")] InvalidOrMissingValue(&'static str), /// Unknown or prohibited key #[error("Invalid or prohibited key: {0}")] UnknownOrProhibitedKey(String), /// Failed to parse patch json #[error("Failed to parse settings patch")] ParsePatch(#[source] serde_json::Error), /// Failed to deserialize patched settings #[error("Failed to deserialize patched settings")] DeserializePatched(#[source] serde_json::Error), /// Failed to serialize settings #[error("Failed to serialize current settings")] SerializeSettings(#[source] serde_json::Error), /// Failed to serialize field #[error("Failed to serialize value")] SerializeValue(#[source] serde_json::Error), /// Recursion limit reached #[error("Maximum JSON object depth reached")] RecursionLimit, /// Settings error #[error("Settings error")] Settings(#[source] super::Error), } /// Converts an [Error] to a management interface status impl From for mullvad_management_interface::Status { fn from(error: Error) -> mullvad_management_interface::Status { use mullvad_management_interface::Status; match error { Error::InvalidOrMissingValue(_) | Error::UnknownOrProhibitedKey(_) | Error::ParsePatch(_) | Error::DeserializePatched(_) | Error::RecursionLimit => Status::invalid_argument(error.to_string()), Error::Settings(error) => Status::from(error), Error::SerializeSettings(error) | Error::SerializeValue(error) => { Status::internal(error.to_string()) } } } } enum MergeStrategy { /// Replace or append keys to objects, and replace everything else Replace, /// Call a function to combine an existing setting (which may be null) with the patch. /// The returned value replaces the existing node. Custom(fn(&serde_json::Value, &serde_json::Value) -> Result), } // TODO: Use Default trait when `const_trait_impl`` is available. const DEFAULT_MERGE_STRATEGY: MergeStrategy = MergeStrategy::Replace; struct PermittedKey { key_type: PermittedKeyValue, merge_strategy: MergeStrategy, } impl PermittedKey { const fn object(keys: &'static [(&'static str, PermittedKey)]) -> Self { Self { key_type: PermittedKeyValue::Object(keys), merge_strategy: DEFAULT_MERGE_STRATEGY, } } const fn array(key: &'static PermittedKey) -> Self { Self { key_type: PermittedKeyValue::Array(key), merge_strategy: DEFAULT_MERGE_STRATEGY, } } const fn any() -> Self { Self { key_type: PermittedKeyValue::Any, merge_strategy: DEFAULT_MERGE_STRATEGY, } } const fn merge_strategy(mut self, merge_strategy: MergeStrategy) -> Self { self.merge_strategy = merge_strategy; self } } enum PermittedKeyValue { /// Select subkeys that can be modified at this level Object(&'static [(&'static str, PermittedKey)]), /// Array that can be modified at this level Array(&'static PermittedKey), /// Accept any object at this level Any, } const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[( "relay_overrides", PermittedKey::array(&PermittedKey::object(&[ ("hostname", PermittedKey::any()), ("ipv4_addr_in", PermittedKey::any()), ("ipv6_addr_in", PermittedKey::any()), ])) .merge_strategy(MergeStrategy::Custom(merge_relay_overrides)), )]); /// Prohibit stack overflow via excessive recursion. It might be possible to forgo this when /// tail-call optimization can be enforced? const RECURSE_LIMIT: usize = 15; /// Export a patch containing all currently supported settings. pub fn export_settings(settings: &Settings) -> Result { let patch = export_settings_inner(settings)?; serde_json::to_string_pretty(&patch).map_err(Error::SerializeValue) } fn export_settings_inner(settings: &Settings) -> Result { let mut out = serde_json::Map::new(); let mut overrides = vec![]; for relay_override in &settings.relay_overrides { let mut relay_override = serde_json::to_value(relay_override).map_err(Error::SerializeValue)?; if let Some(relay_overrides) = relay_override.as_object_mut() { // prune empty override entries relay_overrides.retain(|_k, v| !v.is_null()); let has_overrides = relay_overrides.iter().any(|(key, _)| key != "hostname"); if !has_overrides { continue; } } overrides.push(relay_override); } if !overrides.is_empty() { out.insert( "relay_overrides".to_owned(), serde_json::Value::Array(overrides), ); } Ok(serde_json::Value::Object(out)) } /// Update the settings with the supplied patch. Only settings specified in `PERMITTED_SUBKEYS` can /// be updated. All other changes are rejected pub async fn merge_validate_patch( settings: &mut SettingsPersister, json_patch: &str, ) -> Result<(), Error> { let new_settings = merge_validate_patch_inner(settings, json_patch)?; settings .update(move |settings| *settings = new_settings) .await .map_err(Error::Settings)?; Ok(()) } fn merge_validate_patch_inner(settings: &Settings, json_patch: &str) -> Result { let mut settings_value: serde_json::Value = serde_json::to_value(settings).map_err(Error::SerializeSettings)?; let patch_value: serde_json::Value = serde_json::from_str(json_patch).map_err(Error::ParsePatch)?; validate_patch_value(PERMITTED_SUBKEYS, &patch_value, 0)?; merge_patch_to_value(PERMITTED_SUBKEYS, &mut settings_value, &patch_value, 0)?; let new_settings: Settings = serde_json::from_value(settings_value).map_err(Error::DeserializePatched)?; Ok(new_settings) } /// Replace overrides for existing values in the array if there's a matching hostname. For hostnames /// that do not exist, just append the overrides. fn merge_relay_overrides( current_settings: &serde_json::Value, patch: &serde_json::Value, ) -> Result { if current_settings.is_null() { return Ok(patch.to_owned()); } let patch_array = patch.as_array().ok_or(Error::InvalidOrMissingValue( "relay overrides must be array", ))?; let current_array = current_settings .as_array() .ok_or(Error::InvalidOrMissingValue( "existing overrides should be an array", ))?; let mut new_array = current_array.clone(); for patch_override in patch_array.iter().cloned() { let patch_obj = patch_override .as_object() .ok_or(Error::InvalidOrMissingValue("override entry"))?; let patch_hostname = patch_obj .get("hostname") .and_then(|hostname| hostname.as_str()) .ok_or(Error::InvalidOrMissingValue("hostname"))?; let existing_obj = new_array.iter_mut().find(|value| { value .as_object() .and_then(|obj| obj.get("hostname")) .map(|hostname| hostname.as_str() == Some(patch_hostname)) .unwrap_or(false) }); match existing_obj { Some(existing_val) => { // Replace or append to existing values match (existing_val, patch_override) { ( &mut serde_json::Value::Object(ref mut current), serde_json::Value::Object(ref patch), ) => { for (k, v) in patch { current.insert(k.to_owned(), v.to_owned()); } } _ => { return Err(Error::InvalidOrMissingValue( "all override entries must be objects", )); } } } None => new_array.push(patch_override), } } Ok(serde_json::Value::Array(new_array)) } fn merge_patch_to_value( permitted_key: &'static PermittedKey, current_value: &mut serde_json::Value, patch_value: &serde_json::Value, recurse_level: usize, ) -> Result<(), Error> { if recurse_level >= RECURSE_LIMIT { return Err(Error::RecursionLimit); } match permitted_key.merge_strategy { MergeStrategy::Replace => { match (&permitted_key.key_type, current_value, patch_value) { // Append or replace keys to objects ( &PermittedKeyValue::Object(sub_permitted), &mut serde_json::Value::Object(ref mut current), serde_json::Value::Object(patch), ) => { for (k, sub_patch) in patch { let Some((_, sub_permitted)) = sub_permitted .iter() .find(|(permitted_key, _)| k == permitted_key) else { return Err(Error::UnknownOrProhibitedKey(k.to_owned())); }; let sub_current = current.entry(k).or_insert(serde_json::Value::Null); merge_patch_to_value( sub_permitted, sub_current, sub_patch, recurse_level + 1, )?; } } // Totally replace anything else (_, current, patch) => { *current = patch.clone(); } } } MergeStrategy::Custom(merge_function) => { *current_value = merge_function(current_value, patch_value)?; } } Ok(()) } fn validate_patch_value( permitted_key: &'static PermittedKey, json_value: &serde_json::Value, recurse_level: usize, ) -> Result<(), Error> { if recurse_level >= RECURSE_LIMIT { return Err(Error::RecursionLimit); } match permitted_key.key_type { PermittedKeyValue::Object(subkeys) => { let map = json_value.as_object().ok_or(Error::InvalidOrMissingValue( "expected JSON object in patch", ))?; for (k, v) in map.into_iter() { // NOTE: We're relying on the parser to shed duplicate keys here. // As of this writing, `Map` is implemented using BTreeMap. let Some((_, subkey)) = subkeys.iter().find(|(permitted_key, _)| k == permitted_key) else { return Err(Error::UnknownOrProhibitedKey(k.to_owned())); }; validate_patch_value(subkey, v, recurse_level + 1)?; } Ok(()) } PermittedKeyValue::Array(subkey) => { let values = json_value .as_array() .ok_or(Error::InvalidOrMissingValue("expected JSON array in patch"))?; for v in values { validate_patch_value(subkey, v, recurse_level + 1)?; } Ok(()) } PermittedKeyValue::Any => Ok(()), } } #[test] fn test_permitted_value() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[( "key", PermittedKey::array(&PermittedKey::object(&[("a", PermittedKey::any())])), )]); let patch = r#"{"key": [ {"a": "test" } ] }"#; let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap(); } #[test] fn test_prohibited_value() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[( "key", PermittedKey::array(&PermittedKey::object(&[("a", PermittedKey::any())])), )]); let patch = r#"{"keyx": [] }"#; let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap_err(); let patch = r#"{"key": { "b": 1 } }"#; let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap_err(); } #[test] fn test_merge_append_to_object() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[ ("test0", PermittedKey::any()), ("test1", PermittedKey::any()), ]); let current = r#"{ "test0": 1 }"#; let patch = r#"{ "test1": [] }"#; let expected = r#"{ "test0": 1, "test1": [] }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); } #[test] fn test_merge_replace_in_object() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[ ("test0", PermittedKey::any()), ( "test1", PermittedKey::object(&[("a", PermittedKey::any()), ("test0", PermittedKey::any())]), ), ]); let current = r#"{ "test0": 1, "test1": { "a": 1, "test0": [] } }"#; let patch = r#"{ "test1": { "test0": [1, 2, 3] } }"#; let expected = r#"{ "test0": 1, "test1": { "a": 1, "test0": [1, 2, 3] } }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); } #[test] fn test_overflow() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::array(&PermittedKey::array( &PermittedKey::array(&PermittedKey::any()), ))), ))), ))), ))), ))), )); let patch = r#"[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]"#; let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); assert!(matches!( validate_patch_value(PERMITTED_SUBKEYS, &patch, 0), Err(Error::RecursionLimit) )); } /// Test whether valid patches from documentation are accepted by the implementation #[test] fn test_valid_patch_files() { const OVERRIDE_PATCH: &str = include_str!("../../../docs/patch-examples/override-relay-ips.json"); let prev_settings = Settings::default(); let _ = merge_validate_patch_inner(&prev_settings, OVERRIDE_PATCH) .expect("failed to apply relay overrides"); } #[test] fn test_patch_export() { use mullvad_types::relay_constraints::RelayOverride; let mut settings = Settings::default(); let mut relay_override = RelayOverride::empty("test".to_owned()); relay_override.ipv4_addr_in = Some("1.2.3.4".parse().unwrap()); relay_override.ipv6_addr_in = Some("::1".parse().unwrap()); settings.relay_overrides.push(relay_override); let exported = export_settings_inner(&settings).expect("patch export failed"); let expected = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.2.3.4", "ipv6_addr_in": "::1" } ] }"#; let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); assert_eq!(exported, expected); } #[test] fn test_patch_relay_override() { const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[( "relay_overrides", PermittedKey::array(&PermittedKey::object(&[ ("hostname", PermittedKey::any()), ("ipv4_addr_in", PermittedKey::any()), ("ipv6_addr_in", PermittedKey::any()), ])) .merge_strategy(MergeStrategy::Custom(merge_relay_overrides)), )]); // If override has no hostname, fail // let patch = r#"{ "relay_overrides": [ { "invalid": 0 } ] }"#; let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap_err(); // If there are no overrides, append new override // let current = r#"{ "other": 1 }"#; let patch = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" } ] }"#; let expected = r#"{ "other": 1, "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" } ] }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); // If there are overrides, append new override to existing list // let current = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" } ] }"#; let patch = r#"{ "relay_overrides": [ { "hostname": "new", "ipv4_addr_in": "1.2.3.4" } ] }"#; let expected = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" }, { "hostname": "new", "ipv4_addr_in": "1.2.3.4" } ] }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); // If there are overrides, replace existing overrides but keep rest // let current = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" }, { "hostname": "test2", "ipv4_addr_in": "1.2.3.4" } ] }"#; let patch = r#"{ "relay_overrides": [ { "hostname": "test2", "ipv4_addr_in": "0.0.0.0" }, { "hostname": "test3", "ipv4_addr_in": "192.168.1.1" } ] }"#; let expected = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" }, { "hostname": "test2", "ipv4_addr_in": "0.0.0.0" }, { "hostname": "test3", "ipv4_addr_in": "192.168.1.1" } ] }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); // For same hostname, only update specified overrides // let current = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7" } ] }"#; let patch = r#"{ "relay_overrides": [ { "hostname": "test", "ipv6_addr_in": "::1" } ] }"#; let expected = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.3.3.7", "ipv6_addr_in": "::1" } ] }"#; let mut current: serde_json::Value = serde_json::from_str(current).unwrap(); let patch: serde_json::Value = serde_json::from_str(patch).unwrap(); let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); validate_patch_value(PERMITTED_SUBKEYS, &patch, 0).unwrap(); merge_patch_to_value(PERMITTED_SUBKEYS, &mut current, &patch, 0).unwrap(); assert_eq!(current, expected); }