summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-01-10 13:06:03 +0100
committerDavid Lönnhager <david.l@mullvad.net>2024-01-10 13:06:03 +0100
commit75eb89c820f12d488a76934f59ba29fe999cf59c (patch)
treedee82ab3f307f8d1a72a0cf01fceb8fa51612ac1
parentedbd1f52c44ba6ee9a290146f714453fc87e689d (diff)
parent01bcf9ed16a903aa80ff8bc48cc6f2aaf3d6e80d (diff)
downloadmullvadvpn-75eb89c820f12d488a76934f59ba29fe999cf59c.tar.xz
mullvadvpn-75eb89c820f12d488a76934f59ba29fe999cf59c.zip
Merge branch 'add-settings-json-export'
-rw-r--r--CHANGELOG.md2
-rw-r--r--mullvad-cli/src/cmds/import_settings.rs101
-rw-r--r--mullvad-cli/src/cmds/mod.rs2
-rw-r--r--mullvad-cli/src/cmds/patch.rs60
-rw-r--r--mullvad-cli/src/main.rs13
-rw-r--r--mullvad-daemon/src/lib.rs10
-rw-r--r--mullvad-daemon/src/management_interface.rs8
-rw-r--r--mullvad-daemon/src/settings/patch.rs63
-rw-r--r--mullvad-management-interface/proto/management_interface.proto2
-rw-r--r--mullvad-management-interface/src/client.rs5
10 files changed, 160 insertions, 106 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1e5499df3..efd3fa6c3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ Line wrap the file at 100 chars. Th
### Added
- Add account UUID to verbose 'mullvad account get -v' output.
- Respect OS prefer-reduced-motion setting
+- Add CLI command for exporting settings patches: `mullvad export-settings`. Currently, it generates
+ a patch containing all patchable settings, which only includes relay IP overrides.
#### Android
- Add support for all screen orientations.
diff --git a/mullvad-cli/src/cmds/import_settings.rs b/mullvad-cli/src/cmds/import_settings.rs
deleted file mode 100644
index a89c6814c3..0000000000
--- a/mullvad-cli/src/cmds/import_settings.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use anyhow::{anyhow, Context, Result};
-use mullvad_management_interface::MullvadProxyClient;
-use std::{
- fs::File,
- io::{stdin, BufRead, BufReader},
- path::Path,
-};
-
-/// Maximum size of a settings patch. Bigger files/streams cause the read to fail.
-const MAX_PATCH_BYTES: usize = 10 * 1024;
-
-/// If source is specified, read from the provided file and send it as a settings patch to the
-/// daemon. Otherwise, read the patch from standard input.
-pub async fn handle(source: String) -> Result<()> {
- let json_blob = tokio::task::spawn_blocking(|| get_blob(source))
- .await
- .unwrap()?;
-
- let mut rpc = MullvadProxyClient::new().await?;
- rpc.apply_json_settings(json_blob)
- .await
- .context("Error applying patch")?;
-
- println!("Settings applied");
-
- Ok(())
-}
-
-fn get_blob(source: String) -> Result<String> {
- match source.as_str() {
- "-" => read_settings_from_stdin().context("Failed to read from stdin"),
- _ => read_settings_from_file(source).context("Failed to read from path: {source}"),
- }
-}
-
-/// Read settings from standard input
-fn read_settings_from_stdin() -> Result<String> {
- read_settings_from_reader(BufReader::new(stdin()))
-}
-
-/// Read settings from a path
-fn read_settings_from_file(path: impl AsRef<Path>) -> Result<String> {
- read_settings_from_reader(BufReader::new(File::open(path)?))
-}
-
-/// Read until EOF or until newline when the last pair of braces has been closed
-fn read_settings_from_reader(mut reader: impl BufRead) -> Result<String> {
- let mut buf = [0u8; MAX_PATCH_BYTES];
-
- let mut was_open = false;
- let mut close_after_newline = false;
- let mut brace_count: usize = 0;
- let mut cursor_pos = 0;
-
- loop {
- let Some(cursor) = buf.get_mut(cursor_pos..) else {
- return Err(anyhow!(
- "Patch too long: maximum length is {MAX_PATCH_BYTES} bytes"
- ));
- };
-
- let prev_cursor_pos = cursor_pos;
- let read_n = reader.read(cursor)?;
- if read_n == 0 {
- // EOF
- break;
- }
- cursor_pos += read_n;
-
- let additional_bytes = &buf[prev_cursor_pos..cursor_pos];
-
- if !close_after_newline {
- for next in additional_bytes {
- match next {
- b'{' => brace_count += 1,
- b'}' => {
- brace_count = brace_count.checked_sub(1).with_context(|| {
- // exit: too many closing braces
- "syntax error: unexpected '}'"
- })?
- }
- _ => (),
- }
- was_open |= brace_count > 0;
- }
- if brace_count == 0 && was_open {
- // complete settings
- close_after_newline = true;
- }
- }
-
- if close_after_newline && additional_bytes.contains(&b'\n') {
- // done
- break;
- }
- }
-
- Ok(std::str::from_utf8(&buf[0..cursor_pos])
- .context("settings must be utf8 encoded")?
- .to_owned())
-}
diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs
index 29e0508d80..5a9cede691 100644
--- a/mullvad-cli/src/cmds/mod.rs
+++ b/mullvad-cli/src/cmds/mod.rs
@@ -9,10 +9,10 @@ pub mod bridge;
pub mod custom_list;
pub mod debug;
pub mod dns;
-pub mod import_settings;
pub mod lan;
pub mod lockdown;
pub mod obfuscation;
+pub mod patch;
pub mod proxies;
pub mod relay;
pub mod relay_constraints;
diff --git a/mullvad-cli/src/cmds/patch.rs b/mullvad-cli/src/cmds/patch.rs
new file mode 100644
index 0000000000..bec686e56d
--- /dev/null
+++ b/mullvad-cli/src/cmds/patch.rs
@@ -0,0 +1,60 @@
+use anyhow::{Context, Result};
+use mullvad_management_interface::MullvadProxyClient;
+use std::{
+ fs::File,
+ io::{stdin, BufReader, Read},
+};
+
+/// If source is specified, read from the provided file and send it as a settings patch to the
+/// daemon. Otherwise, read the patch from standard input.
+pub async fn import(source: String) -> Result<()> {
+ let json_blob = tokio::task::spawn_blocking(|| get_blob(source))
+ .await
+ .unwrap()?;
+
+ let mut rpc = MullvadProxyClient::new().await?;
+ rpc.apply_json_settings(json_blob)
+ .await
+ .context("Error applying patch")?;
+
+ println!("Settings applied");
+
+ Ok(())
+}
+
+/// If source is specified, write a patch to the file. Otherwise, write the patch to standard
+/// output.
+pub async fn export(dest: String) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let blob = rpc
+ .export_json_settings()
+ .await
+ .context("Error exporting patch")?;
+
+ match dest.as_str() {
+ "-" => {
+ println!("{blob}");
+ Ok(())
+ }
+ _ => tokio::fs::write(&dest, blob)
+ .await
+ .context(format!("Failed to write to path {dest}")),
+ }
+}
+
+fn get_blob(source: String) -> Result<String> {
+ match source.as_str() {
+ "-" => {
+ read_settings_from_reader(BufReader::new(stdin())).context("Failed to read from stdin")
+ }
+ _ => read_settings_from_reader(File::open(&source)?)
+ .context(format!("Failed to read from path: {source}")),
+ }
+}
+
+/// Read until EOF or until newline when the last pair of braces has been closed
+fn read_settings_from_reader(mut reader: impl Read) -> Result<String> {
+ let mut s = String::new();
+ reader.read_to_string(&mut s)?;
+ Ok(s)
+}
diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs
index 270d3c293e..c0a8a8b992 100644
--- a/mullvad-cli/src/main.rs
+++ b/mullvad-cli/src/main.rs
@@ -138,11 +138,19 @@ enum Cli {
#[clap(subcommand)]
CustomList(custom_list::CustomList),
- /// Apply a JSON patch
+ /// Apply a JSON patch generated by 'export-settings'
+ #[clap(arg_required_else_help = true)]
ImportSettings {
/// File to read from. If this is "-", read from standard input
file: String,
},
+
+ /// Export a JSON patch based on the current settings
+ #[clap(arg_required_else_help = true)]
+ ExportSettings {
+ /// File to write to. If this is "-", write to standard output
+ file: String,
+ },
}
#[tokio::main]
@@ -169,7 +177,8 @@ async fn main() -> Result<()> {
Cli::SplitTunnel(cmd) => cmd.handle().await,
Cli::Status { cmd, args } => status::handle(cmd, args).await,
Cli::CustomList(cmd) => cmd.handle().await,
- Cli::ImportSettings { file } => import_settings::handle(file).await,
+ Cli::ImportSettings { file } => patch::import(file).await,
+ Cli::ExportSettings { file } => patch::export(file).await,
#[cfg(all(unix, not(target_os = "android")))]
Cli::ShellCompletions { shell, dir } => {
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index 0d02a068c4..06e289d22d 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -353,8 +353,10 @@ pub enum DaemonCommand {
/// Verify that a google play payment was successful through the API.
#[cfg(target_os = "android")]
VerifyPlayPurchase(ResponseTx<(), Error>, PlayPurchase),
- /// Patch the settings using a blob of JSON settings
+ /// Patch the settings using a JSON patch
ApplyJsonSettings(ResponseTx<(), settings::patch::Error>, String),
+ /// Return a JSON blob containing all overridable settings, if there are any
+ ExportJsonSettings(ResponseTx<String, settings::patch::Error>),
}
/// All events that can happen in the daemon. Sent from various threads and exposed interfaces.
@@ -1275,6 +1277,7 @@ where
self.on_verify_play_purchase(tx, play_purchase)
}
ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await,
+ ExportJsonSettings(tx) => self.on_export_json_settings(tx),
}
}
@@ -2626,6 +2629,11 @@ where
Self::oneshot_send(tx, result, "apply_json_settings response");
}
+ fn on_export_json_settings(&mut self, tx: ResponseTx<String, settings::patch::Error>) {
+ let result = settings::patch::export_settings(&self.settings);
+ Self::oneshot_send(tx, result, "export_json_settings response");
+ }
+
/// Set the target state of the client. If it changed trigger the operations needed to
/// progress towards that state.
/// Returns a bool representing whether or not a state change was initiated.
diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs
index 8b8d840cfb..261aa52853 100644
--- a/mullvad-daemon/src/management_interface.rs
+++ b/mullvad-daemon/src/management_interface.rs
@@ -892,6 +892,14 @@ impl ManagementService for ManagementServiceImpl {
self.wait_for_result(rx).await??;
Ok(Response::new(()))
}
+
+ async fn export_json_settings(&self, _: Request<()>) -> ServiceResult<String> {
+ log::debug!("export_json_settings");
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::ExportJsonSettings(tx))?;
+ let blob = self.wait_for_result(rx).await??;
+ Ok(Response::new(blob))
+ }
}
impl ManagementServiceImpl {
diff --git a/mullvad-daemon/src/settings/patch.rs b/mullvad-daemon/src/settings/patch.rs
index 50c0da6304..ad4826812b 100644
--- a/mullvad-daemon/src/settings/patch.rs
+++ b/mullvad-daemon/src/settings/patch.rs
@@ -11,6 +11,9 @@
//! 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;
@@ -33,6 +36,9 @@ pub enum Error {
/// Failed to serialize settings
#[error(display = "Failed to serialize current settings")]
SerializeSettings(#[error(source)] serde_json::Error),
+ /// Failed to serialize field
+ #[error(display = "Failed to serialize value")]
+ SerializeValue(#[error(source)] serde_json::Error),
/// Recursion limit reached
#[error(display = "Maximum JSON object depth reached")]
RecursionLimit,
@@ -54,7 +60,9 @@ impl From<Error> for mullvad_management_interface::Status {
| Error::DeserializePatched(_)
| Error::RecursionLimit => Status::invalid_argument(error.to_string()),
Error::Settings(error) => Status::from(error),
- Error::SerializeSettings(error) => Status::internal(error.to_string()),
+ Error::SerializeSettings(error) | Error::SerializeValue(error) => {
+ Status::internal(error.to_string())
+ }
}
}
}
@@ -125,6 +133,40 @@ const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[(
/// 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<String, Error> {
+ let patch = export_settings_inner(settings)?;
+ serde_json::to_string_pretty(&patch).map_err(Error::SerializeValue)
+}
+
+fn export_settings_inner(settings: &Settings) -> Result<serde_json::Value, Error> {
+ 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(
@@ -418,6 +460,25 @@ fn test_valid_patch_files() {
}
#[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",
diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto
index e4ef20cbcc..5bf0c69f00 100644
--- a/mullvad-management-interface/proto/management_interface.proto
+++ b/mullvad-management-interface/proto/management_interface.proto
@@ -102,6 +102,8 @@ service ManagementService {
// Apply a JSON blob to the settings
// See ../../docs/settings-patch-format.md for a description of the format
rpc ApplyJsonSettings(google.protobuf.StringValue) returns (google.protobuf.Empty) {}
+ // Return a JSON blob containing all overridable settings, if there are any
+ rpc ExportJsonSettings(google.protobuf.Empty) returns (google.protobuf.StringValue) {}
}
message UUID { string value = 1; }
diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs
index 2c0ca2fecf..7ff26e0371 100644
--- a/mullvad-management-interface/src/client.rs
+++ b/mullvad-management-interface/src/client.rs
@@ -678,6 +678,11 @@ impl MullvadProxyClient {
self.0.apply_json_settings(blob).await.map_err(Error::Rpc)?;
Ok(())
}
+
+ pub async fn export_json_settings(&mut self) -> Result<String> {
+ let blob = self.0.export_json_settings(()).await.map_err(Error::Rpc)?;
+ Ok(blob.into_inner())
+ }
}
fn map_device_error(status: Status) -> Error {