diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2023-11-17 15:37:13 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2023-12-06 14:37:06 +0100 |
| commit | 1b930b893fa3fcb06f5fee4affbfb62887d0e68a (patch) | |
| tree | 21b0836c5b5730f844950a2764aa652832f13849 | |
| parent | ac3e222d031b0f599561c4c30504de5cd3f871a2 (diff) | |
| download | mullvadvpn-1b930b893fa3fcb06f5fee4affbfb62887d0e68a.tar.xz mullvadvpn-1b930b893fa3fcb06f5fee4affbfb62887d0e68a.zip | |
Implement RPC for reading & writing to app cache file
- Implement RPC for writing to a file in a test runner / guest VM.
- Implement RPC for getting app cache directory
- Implement RPC for restarting the app in a test runner / guest vm
- Implement RPC for starting the app in a test runner / guest vm
- Implement RPC for stopping the app in a test runner / guest vm
- Implement `find_cache_traces` on Window & macOS
| -rw-r--r-- | test/test-manager/src/tests/tunnel_state.rs | 34 | ||||
| -rw-r--r-- | test/test-rpc/src/client.rs | 46 | ||||
| -rw-r--r-- | test/test-rpc/src/lib.rs | 14 | ||||
| -rw-r--r-- | test/test-runner/Cargo.toml | 2 | ||||
| -rw-r--r-- | test/test-runner/src/app.rs | 58 | ||||
| -rw-r--r-- | test/test-runner/src/main.rs | 44 | ||||
| -rw-r--r-- | test/test-runner/src/net.rs | 4 | ||||
| -rw-r--r-- | test/test-runner/src/sys.rs | 103 |
8 files changed, 251 insertions, 54 deletions
diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index eb78828fd0..176127d7c5 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -1,6 +1,6 @@ use super::helpers::{ - self, connect_and_wait, get_tunnel_state, send_guest_probes, set_relay_settings, - unreachable_wireguard_tunnel, wait_for_tunnel_state, + self, connect_and_wait, disconnect_and_wait, get_tunnel_state, send_guest_probes, + set_relay_settings, unreachable_wireguard_tunnel, wait_for_tunnel_state, }; use super::{ui, Error, TestContext}; use crate::assert_tunnel_state; @@ -335,3 +335,33 @@ pub async fn test_connected_state( Ok(()) } + +/// Verify that the app defaults to the connecting state if it is started with a +/// corrupt state cache. +#[test_function] +pub async fn test_connecting_state_when_corrupted_state_cache( + _: TestContext, + rpc: ServiceClient, + mut mullvad_client: ManagementServiceClient, +) -> Result<(), Error> { + // Enter the disconnected state. Normally this would be preserved when + // restarting the app, i.e. the user would still be disconnected after a + // successfull restart. However, as we will intentionally corrupt the state + // target cache the user should end up in the connecting/connected state, + // *not in the disconnected state, upon restart. + disconnect_and_wait(&mut mullvad_client).await?; + + // Stopping the app should write to the state target cache. + log::info!("Stopping the app"); + rpc.restart_app().await?; + + // Intentionally corrupt the state cache + todo!("Intentionally corrupt the state cache"); + // Start a leak monitor + todo!("Start a leak monitor"); + // Start the app & make sure that we start in the 'connecting state'. The + // side-effect of this is that no network traffic is allowed to leak. + todo!("Start the app"); + assert_tunnel_state!(&mut mullvad_client, TunnelState::Connecting { .. }); + Ok(()) +} diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index 6be77afb40..acc462dd60 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -151,6 +151,13 @@ impl ServiceClient { .await? } + /// Returns path of Mullvad app cache directorie on the test runner. + pub async fn find_mullvad_app_cache_dir(&self) -> Result<PathBuf, Error> { + self.client + .get_mullvad_app_cache_dir(tarpc::context::current()) + .await? + } + /// Send TCP packet pub async fn send_tcp( &self, @@ -213,6 +220,34 @@ impl ServiceClient { .await? } + pub async fn restart_app(&self) -> Result<(), Error> { + let _ = self.client.restart_app(tarpc::context::current()).await?; + Ok(()) + } + + /// Stop the app. + /// + /// Shuts down a running app, making it disconnect from any current tunnel + /// connection and making it write to caches. + /// + /// # Note + /// This function will return *after* the app has been stopped, thus + /// blocking execution until then. + pub async fn stop_app(&self) -> Result<(), Error> { + let _ = self.client.stop_app(tarpc::context::current()).await?; + Ok(()) + } + + /// Start the app. + /// + /// # Note + /// This function will return *after* the app has been start, thus + /// blocking execution until then. + pub async fn start_app(&self) -> Result<(), Error> { + let _ = self.client.start_app(tarpc::context::current()).await?; + Ok(()) + } + pub async fn set_daemon_log_level( &self, verbosity_level: mullvad_daemon::Verbosity, @@ -247,6 +282,17 @@ impl ServiceClient { .await? } + pub async fn write_file(&self, dest: String, bytes: Vec<u8>) -> Result<(), Error> { + log::debug!( + "Writing {bytes} bytes to \"{file}\"", + bytes = bytes.len(), + file = dest + ); + self.client + .write_file(tarpc::context::current(), dest, bytes) + .await? + } + pub async fn reboot(&mut self) -> Result<(), Error> { log::debug!("Rebooting server"); diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 6968a3c613..65ccb2fb7a 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -120,6 +120,8 @@ mod service { /// Returns all Mullvad app files, directories, and other data found on the system. async fn find_mullvad_app_traces() -> Result<Vec<AppTrace>, Error>; + async fn get_mullvad_app_cache_dir() -> Result<PathBuf, Error>; + /// Send TCP packet async fn send_tcp( interface: Option<String>, @@ -149,6 +151,15 @@ mod service { /// Perform DNS resolution. async fn resolve_hostname(hostname: String) -> Result<Vec<SocketAddr>, Error>; + /// Restart the Mullvad VPN application. + async fn restart_app() -> Result<(), Error>; + + /// Stop the Mullvad VPN application. + async fn stop_app() -> Result<(), Error>; + + /// Start the Mullvad VPN application. + async fn start_app() -> Result<(), Error>; + /// Sets the log level of the daemon service, the verbosity level represents the number of /// `-v`s passed on the command line. This will restart the daemon system service. async fn set_daemon_log_level( @@ -161,6 +172,9 @@ mod service { /// Copy a file from `src` to `dest` on the test runner. async fn copy_file(src: String, dest: String) -> Result<(), Error>; + /// Write arbitrary bytes to some file `dest` on the test runner. + async fn write_file(dest: String, bytes: Vec<u8>) -> Result<(), Error>; + async fn reboot() -> Result<(), Error>; async fn set_mullvad_daemon_service_state(on: bool) -> Result<(), Error>; diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml index 2c431ffa49..64461d76aa 100644 --- a/test/test-runner/Cargo.toml +++ b/test/test-runner/Cargo.toml @@ -18,7 +18,7 @@ serde_json = { workspace = true } tokio-serde = { workspace = true } libc = "0.2" -chrono = { workspace = true } +chrono = { workspace = true, features = ["serde"] } test-rpc = { path = "../test-rpc" } mullvad-paths = { path = "../../mullvad-paths" } diff --git a/test/test-runner/src/app.rs b/test/test-runner/src/app.rs index 43aca23abb..f4e1fc3c53 100644 --- a/test/test-runner/src/app.rs +++ b/test/test-runner/src/app.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use std::path::Path; +use std::path::{Path, PathBuf}; use test_rpc::{AppTrace, Error}; @@ -14,21 +14,18 @@ pub fn find_traces() -> Result<Vec<AppTrace>, Error> { Error::Syscall })?; - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"C:\Program Files\Mullvad VPN"), // NOTE: This only works as of `499c06decda37dc639e5f` in the Mullvad app. // Older builds have no way of silently fully uninstalling the app. Path::new(r"C:\ProgramData\Mullvad VPN"), // NOTE: Works as of `4116ebc` (Mullvad app). &settings_dir, + &caches, ]; - filter_non_existent_paths(&mut traces)?; - - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) + Ok(existing_paths(&traces)) } #[cfg(target_os = "linux")] @@ -36,10 +33,11 @@ pub fn find_traces() -> Result<Vec<AppTrace>, Error> { // TODO: Check GUI data // TODO: Check temp data - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"/etc/mullvad-vpn/"), Path::new(r"/var/log/mullvad-vpn/"), - Path::new(r"/var/cache/mullvad-vpn/"), + &caches, Path::new(r"/opt/Mullvad VPN/"), // management interface socket Path::new(r"/var/run/mullvad-vpn"), @@ -55,12 +53,11 @@ pub fn find_traces() -> Result<Vec<AppTrace>, Error> { Path::new(r"/usr/share/fish/vendor_completions.d/mullvad.fish"), ]; - filter_non_existent_paths(&mut traces)?; + Ok(existing_paths(&traces)) +} - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) +pub fn find_cache_traces() -> Result<PathBuf, Error> { + mullvad_paths::get_cache_dir().map_err(|error| Error::FileSystem(error.to_string())) } #[cfg(target_os = "macos")] @@ -68,10 +65,11 @@ pub fn find_traces() -> Result<Vec<AppTrace>, Error> { // TODO: Check GUI data // TODO: Check temp data - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"/Applications/Mullvad VPN.app/"), Path::new(r"/var/log/mullvad-vpn/"), - Path::new(r"/Library/Caches/mullvad-vpn/"), + &caches, // management interface socket Path::new(r"/var/run/mullvad-vpn"), // launch daemon @@ -84,26 +82,16 @@ pub fn find_traces() -> Result<Vec<AppTrace>, Error> { Path::new(r"/usr/local/share/fish/vendor_completions.d/mullvad.fish"), ]; - filter_non_existent_paths(&mut traces)?; - - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) + Ok(existing_paths(&traces)) } -fn filter_non_existent_paths(paths: &mut Vec<&Path>) -> Result<(), Error> { - for i in (0..paths.len()).rev() { - let path_exists = paths[i].try_exists().map_err(|error| { - log::error!("Failed to check whether path exists: {error}"); - Error::Syscall - })?; - if !path_exists { - paths.swap_remove(i); - continue; - } - } - Ok(()) +/// Find all present app traces on the test runner. +fn existing_paths(paths: &[&Path]) -> Vec<AppTrace> { + paths + .iter() + .filter(|&path| path.try_exists().is_ok_and(|exists| exists)) + .map(|path| AppTrace::Path(path.to_path_buf())) + .collect() } pub async fn make_device_json_old() -> Result<(), Error> { diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index ebf0d1e474..259ccdbab6 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -3,7 +3,7 @@ use logging::LOGGER; use std::{ collections::{BTreeMap, HashMap}, net::{IpAddr, SocketAddr}, - path::Path, + path::{Path, PathBuf}, }; use tarpc::context; @@ -114,6 +114,13 @@ impl Service for TestServer { app::find_traces() } + async fn get_mullvad_app_cache_dir( + self, + _: context::Context, + ) -> Result<PathBuf, test_rpc::Error> { + app::find_cache_traces() + } + async fn send_tcp( self, _: context::Context, @@ -140,7 +147,7 @@ impl Service for TestServer { interface: Option<String>, destination: IpAddr, ) -> Result<(), test_rpc::Error> { - net::send_ping(interface.as_ref().map(String::as_str), destination).await + net::send_ping(interface.as_deref(), destination).await } async fn geoip_lookup( @@ -219,6 +226,20 @@ impl Service for TestServer { logging::get_mullvad_app_logs().await } + async fn restart_app(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::restart_app().await + } + + /// Stop the Mullvad VPN application. + async fn stop_app(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::stop_app().await + } + + /// Start the Mullvad VPN application. + async fn start_app(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::start_app().await + } + async fn set_daemon_log_level( self, _: context::Context, @@ -248,6 +269,25 @@ impl Service for TestServer { Ok(()) } + /// Write a slice as the entire contents of a file. + /// + /// See the documention of [`tokio::fs::write`] for details of the behavior. + async fn write_file( + self, + _: context::Context, + dest: PathBuf, + bytes: Vec<u8>, + ) -> Result<(), test_rpc::Error> { + tokio::fs::write(&dest, bytes).await.map_err(|error| { + log::error!( + "Failed to write to \"{dest}\": {error}", + dest = dest.display() + ); + test_rpc::Error::Syscall + })?; + Ok(()) + } + async fn reboot(self, _: context::Context) -> Result<(), test_rpc::Error> { sys::reboot() } diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index f40aece4c9..a4a7a2db47 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -35,7 +35,7 @@ pub async fn send_tcp( }; #[cfg(target_os = "macos")] - sock.bind_device_by_index(Some(interface_index)) + sock.bind_device_by_index_v4(Some(interface_index)) .map_err(|error| { log::error!("Failed to set IP_BOUND_IF on socket: {error}"); test_rpc::Error::SendTcp @@ -102,7 +102,7 @@ pub async fn send_udp( }; #[cfg(target_os = "macos")] - sock.bind_device_by_index(Some(interface_index)) + sock.bind_device_by_index_v4(Some(interface_index)) .map_err(|error| { log::error!("Failed to set IP_BOUND_IF on socket: {error}"); test_rpc::Error::SendUdp diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs index 93d148a2b5..cc89205425 100644 --- a/test/test-runner/src/sys.rs +++ b/test/test-runner/src/sys.rs @@ -193,16 +193,102 @@ ExecStart=/usr/bin/mullvad-daemon --disable-stdout-timestamps {verbosity}"# .await .map_err(|e| test_rpc::Error::Service(e.to_string()))?; + restart_app().await?; + Ok(()) +} + +/// Restart the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "linux")] +pub async fn restart_app() -> Result<(), test_rpc::Error> { tokio::process::Command::new("systemctl") .args(["restart", "mullvad-daemon"]) .status() .await .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - wait_for_service_state(ServiceState::Running).await?; Ok(()) } +/// Stop the Mullvad VPN application. +/// +/// This function waits for the app to successfully shut down. +#[cfg(target_os = "linux")] +pub async fn stop_app() -> Result<(), test_rpc::Error> { + set_mullvad_daemon_service_state(false).await +} + +/// Start the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "linux")] +pub async fn start_app() -> Result<(), test_rpc::Error> { + set_mullvad_daemon_service_state(true).await +} + +/// Restart the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "windows")] +pub async fn restart_app() -> Result<(), test_rpc::Error> { + stop_app().await?; + start_app().await?; + Ok(()) +} + +/// Stop the Mullvad VPN application. +/// +/// This function waits for the app to successfully shut down. +#[cfg(target_os = "windows")] +pub async fn stop_app() -> Result<(), test_rpc::Error> { + let _ = tokio::process::Command::new("net") + .args(["stop", "mullvadvpn"]) + .status() + .await + .map_err(|e| test_rpc::Error::Service(e.to_string()))?; + Ok(()) +} + +/// Start the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "windows")] +pub async fn start_app() -> Result<(), test_rpc::Error> { + let _ = tokio::process::Command::new("net") + .args(["start", "mullvadvpn"]) + .status() + .await + .map_err(|e| test_rpc::Error::Service(e.to_string()))?; + Ok(()) +} + +/// Restart the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "macos")] +pub async fn restart_app() -> Result<(), test_rpc::Error> { + stop_app().await?; + start_app().await?; + Ok(()) +} + +/// Stop the Mullvad VPN application. +/// +/// This function waits for the app to successfully shut down. +#[cfg(target_os = "macos")] +pub async fn stop_app() -> Result<(), test_rpc::Error> { + set_mullvad_daemon_service_state(false).await +} + +/// Start the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +#[cfg(target_os = "macos")] +pub async fn start_app() -> Result<(), test_rpc::Error> { + set_mullvad_daemon_service_state(true).await +} + #[cfg(target_os = "windows")] pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> { log::debug!("Setting log level"); @@ -226,6 +312,7 @@ pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test .map_err(|e| test_rpc::Error::Service(e.to_string()))?; // Stop the service + // TODO: Extract to separate function. service .stop() .map_err(|e| test_rpc::Error::Service(e.to_string()))?; @@ -266,6 +353,7 @@ pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test .map_err(|e| test_rpc::Error::Service(e.to_string()))?; // Start the service + // TODO: Extract to separate function. service .start::<String>(&[]) .map_err(|e| test_rpc::Error::Service(e.to_string()))?; @@ -341,17 +429,8 @@ pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), } // Restart service - tokio::process::Command::new("net") - .args(["stop", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - - tokio::process::Command::new("net") - .args(["start", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; + stop_app().await?; + start_app().await?; Ok(()) } |
