summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-11-13 17:14:32 +0100
committerDavid Lönnhager <david.l@mullvad.net>2023-11-17 11:09:25 +0100
commitf6ab6637456de4f30cd59b33de0e3e455ea3fb5a (patch)
treec08822833c8f22a1deeebd48d1f959f1ae3b10eb
parent0a82036e2b49dbd42819d36860b00289b3219a6b (diff)
downloadmullvadvpn-f6ab6637456de4f30cd59b33de0e3e455ea3fb5a.tar.xz
mullvadvpn-f6ab6637456de4f30cd59b33de0e3e455ea3fb5a.zip
Add settings patcher to mullvad-daemon
-rw-r--r--mullvad-daemon/src/settings/mod.rs (renamed from mullvad-daemon/src/settings.rs)2
-rw-r--r--mullvad-daemon/src/settings/patch.rs463
2 files changed, 465 insertions, 0 deletions
diff --git a/mullvad-daemon/src/settings.rs b/mullvad-daemon/src/settings/mod.rs
index f5c5e31e94..671ed87346 100644
--- a/mullvad-daemon/src/settings.rs
+++ b/mullvad-daemon/src/settings/mod.rs
@@ -16,6 +16,8 @@ use tokio::{
io::{self, AsyncWriteExt},
};
+pub mod patch;
+
const SETTINGS_FILE: &str = "settings.json";
#[derive(err_derive::Error, Debug)]
diff --git a/mullvad-daemon/src/settings/patch.rs b/mullvad-daemon/src/settings/patch.rs
new file mode 100644
index 0000000000..63152b740d
--- /dev/null
+++ b/mullvad-daemon/src/settings/patch.rs
@@ -0,0 +1,463 @@
+//! 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.
+
+use super::SettingsPersister;
+use mullvad_types::settings::Settings;
+
+#[derive(err_derive::Error, Debug)]
+#[error(no_from)]
+pub enum Error {
+ /// Missing expected JSON object
+ #[error(display = "Incorrect or missing value: {}", _0)]
+ InvalidOrMissingValue(&'static str),
+ /// Unknown or prohibited key
+ #[error(display = "Invalid or prohibited key: {}", _0)]
+ UnknownOrProhibitedKey(String),
+ /// Failed to parse patch json
+ #[error(display = "Failed to parse settings patch")]
+ ParsePatch(#[error(source)] serde_json::Error),
+ /// Failed to deserialize patched settings
+ #[error(display = "Failed to deserialize patched settings")]
+ DeserializePatched(#[error(source)] serde_json::Error),
+ /// Failed to serialize settings
+ #[error(display = "Failed to serialize current settings")]
+ SerializeSettings(#[error(source)] serde_json::Error),
+ /// Recursion limit reached
+ #[error(display = "Maximum JSON object depth reached")]
+ RecursionLimit,
+ /// Settings error
+ #[error(display = "Settings error")]
+ Settings(#[error(source)] super::Error),
+}
+
+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<serde_json::Value, Error>),
+}
+
+// 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;
+
+/// 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 mut settings_value: serde_json::Value =
+ serde_json::to_value(settings.to_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)?;
+
+ settings
+ .update(move |settings| *settings = new_settings)
+ .await
+ .map_err(Error::Settings)?;
+
+ Ok(())
+}
+
+/// 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<serde_json::Value, Error> {
+ 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) {
+ (
+ 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_permitteds),
+ serde_json::Value::Object(ref mut current),
+ serde_json::Value::Object(ref patch),
+ ) => {
+ for (k, sub_patch) in patch {
+ let Some((_, sub_permitted)) = sub_permitteds
+ .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]
+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);
+}