diff options
| -rw-r--r-- | test/test-manager/src/tests/mod.rs | 58 | ||||
| -rw-r--r-- | test/test-manager/src/tests/windows.rs | 141 | ||||
| -rw-r--r-- | test/test-rpc/src/client.rs | 37 | ||||
| -rw-r--r-- | test/test-rpc/src/lib.rs | 9 | ||||
| -rw-r--r-- | test/test-runner/src/main.rs | 25 | ||||
| -rw-r--r-- | test/test-runner/src/sys.rs | 45 |
6 files changed, 288 insertions, 27 deletions
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index 39f4f12e90..76d3a4bb48 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -15,6 +15,7 @@ mod test_metadata; mod tunnel; mod tunnel_state; mod ui; +mod windows; use itertools::Itertools; use mullvad_types::relay_constraints::{GeographicLocationConstraint, LocationConstraint}; @@ -213,19 +214,35 @@ async fn ensure_daemon_version( ) -> anyhow::Result<MullvadProxyClient> { let app_package_filename = &TEST_CONFIG.app_package_filename; - let mullvad_client = if correct_daemon_version_is_running(rpc_provider.new_client().await).await - { - ensure_daemon_environment(rpc) - .await - .context("Failed to reset daemon environment")?; - rpc_provider.new_client().await - } else { + let must_reinstall_app = + match correct_daemon_version_is_running(rpc_provider.new_client().await).await { + Ok(correct_version) => !correct_version, + // Failing to reach the daemon is a sign that it is not installed + Err(mullvad_management_interface::Error::Rpc(..)) => { + log::debug!("Daemon is not running, attempting to start it"); + + let failed_starting_daemon = rpc.enable_mullvad_daemon().await.is_err() + || rpc.start_mullvad_daemon().await.is_err(); + if failed_starting_daemon { + log::warn!("Failed to start the daemon service"); + } + failed_starting_daemon + } + Err(e) => panic!("Failed to get app version: {e}"), + }; + + if must_reinstall_app { // NOTE: Reinstalling the app resets the daemon environment install_app(rpc, app_package_filename, rpc_provider) .await - .with_context(|| format!("Failed to install app '{app_package_filename}'"))? - }; - Ok(mullvad_client) + .with_context(|| format!("Failed to install app '{app_package_filename}'")) + } else { + ensure_daemon_environment(rpc) + .await + .context("Failed to reset daemon environment")?; + + Ok(rpc_provider.new_client().await) + } } /// Conditionally restart the running daemon @@ -253,23 +270,12 @@ pub async fn ensure_daemon_environment(rpc: &ServiceClient) -> Result<(), anyhow } /// Checks if daemon is installed with the version specified by `TEST_CONFIG.app_package_filename` -async fn correct_daemon_version_is_running(mut mullvad_client: MullvadProxyClient) -> bool { +async fn correct_daemon_version_is_running( + mut mullvad_client: MullvadProxyClient, +) -> Result<bool, mullvad_management_interface::Error> { let app_package_filename = &TEST_CONFIG.app_package_filename; let expected_version = get_version_from_path(std::path::Path::new(app_package_filename)) .unwrap_or_else(|_| panic!("Invalid app version: {app_package_filename}")); - - use mullvad_management_interface::Error::*; - match mullvad_client.get_current_version().await { - // Failing to reach the daemon is a sign that it is not installed - Err(Rpc(..)) => { - log::debug!("Could not reach active daemon before test, it is not running"); - false - } - Err(e) => panic!("Failed to get app version: {e}"), - Ok(version) if version == expected_version => true, - _ => { - log::debug!("Daemon version mismatch"); - false - } - } + let version = mullvad_client.get_current_version().await?; + Ok(version == expected_version) } diff --git a/test/test-manager/src/tests/windows.rs b/test/test-manager/src/tests/windows.rs new file mode 100644 index 0000000000..e857b6901f --- /dev/null +++ b/test/test-manager/src/tests/windows.rs @@ -0,0 +1,141 @@ +//! Windows-specific tests. + +use anyhow::{Context, ensure}; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::states::TunnelState; +use test_macro::test_function; +use test_rpc::ServiceClient; + +use crate::tests::helpers::{geoip_lookup_with_retries, wait_for_tunnel_state}; + +use super::TestContext; + +/// Test that, on a failed upgrade, blocking firewall rules are cleared on a reboot. +#[test_function(target_os = "windows")] +async fn test_clearing_blocked_state_on_failed_upgrade( + _: TestContext, + mut rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + // Assert that the below settings are disabled. If they are not, + // then blocking firewall rules *will* persist after a reboot. + { + let settings = mullvad_client.get_settings().await?; + ensure!( + !settings.block_when_disconnected, + "Block when disconnected should be disabled" + ); + ensure!(!settings.auto_connect, "Auto connect should be disabled"); + } + + log::info!("Connecting to tunnel to enter secured state"); + // This is necessary to ensure that the firewall rules are applied + // Note that we do not need to wait for the tunnel to be fully connected + mullvad_client + .connect_tunnel() + .await + .context("failed to begin connecting")?; + log::info!("Waiting for tunnel state to be Connected or Error"); + wait_for_tunnel_state(mullvad_client.clone(), |state| { + matches!( + state, + TunnelState::Connected { .. } | TunnelState::Error(..) + ) + }) + .await?; + log::info!("Preparing daemon for restart (simulate failed upgrade)"); + mullvad_client + .prepare_restart_v2(false) + .await + .context("Failed to prepare restart")?; + + // Simulate that the daemon has been uninstalled, by disabling the system service. + // We cannot actually uninstall the daemon here, because it would remove the blocking firewall rules, + // regardless of having called `prepare_restart_v2`. + log::info!("Disabling Mullvad daemon system service"); + rpc.disable_mullvad_daemon().await?; + rpc.stop_mullvad_daemon().await?; + + // Make sure that blocking firewall rules are active - there should be no leaks (yet) 💦❌ + log::info!("Checking that blocking firewall rules are active..."); + let geoip = geoip_lookup_with_retries(&rpc).await; + ensure!( + geoip.is_err(), + "Device is leaking with geo IP '{:?}'- blocking rules have not applied properly", + geoip.unwrap() + ); + // Reboot - we expect desperate users to take this measure + log::info!("Rebooting device..."); + rpc.reboot().await?; + // The conn check should now fail - the firewall filters should have been removed at this point 💦💦💦 + log::info!("Checking connectivity after reboot (should be online, but not secured)"); + let mullvad_exit_ip = geoip_lookup_with_retries(&rpc) + .await + .context("Device is offline after reboot")? + .mullvad_exit_ip; + ensure!(!mullvad_exit_ip, "Should *not* be a Mullvad Exit IP"); + + Ok(()) +} + +/// Test that, on a failed upgrade when `Auto-connect` is enabled, blocking firewall rules are *not* cleared on a reboot. +#[test_function(target_os = "windows")] +async fn test_not_clearing_blocked_state_on_failed_upgrade_with_lockdown_mode( + _: TestContext, + mut rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + // Make sure that lockdown mode is enabled. + // If it is not, then blocking firewall rules *will not* persist after a reboot. + { + mullvad_client.set_block_when_disconnected(true).await?; + let settings = mullvad_client.get_settings().await?; + ensure!( + settings.block_when_disconnected, + "Block when disconnected should be enabled" + ); + ensure!(!settings.auto_connect, "Auto connect should be disabled"); + } + + log::info!("Waiting for tunnel state to be Disconnected with lockdown enabled"); + wait_for_tunnel_state(mullvad_client.clone(), |state| { + matches!( + state, + TunnelState::Disconnected { locked_down, .. } if *locked_down + ) + }) + .await?; + log::info!("Preparing daemon for restart (simulate failed upgrade)"); + mullvad_client + .prepare_restart_v2(false) + .await + .context("Failed to prepare restart")?; + + // Simulate that the daemon has been uninstalled, by disabling the system service. + // We cannot actually uninstall the daemon here, because it would remove the blocking firewall rules, + // regardless of having called `prepare_restart_v2`. + log::info!("Disabling Mullvad daemon system service"); + rpc.disable_mullvad_daemon().await?; + rpc.stop_mullvad_daemon().await?; + + // Make sure that blocking firewall rules are active - there should be no leaks 💦❌ + log::info!("Checking that blocking firewall rules are active..."); + let blocked = geoip_lookup_with_retries(&rpc).await.is_err(); + ensure!( + blocked, + "Device is leaking - blocking rules have not applied properly" + ); + // Reboot - we expect desperate users to take this measure + log::info!("Rebooting device..."); + rpc.reboot().await?; + + // The conn check should now fail - the firewall filters should *not* have been removed at this point 💦❌ + log::info!("Checking connectivity after reboot (should be blocked)"); + let blocked = geoip_lookup_with_retries(&rpc).await.is_err(); + ensure!( + blocked, + "Device is leaking - blocking rules have not applied properly" + ); + + Ok(()) +} diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index ad3d39dce9..8f4833f2b9 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -298,6 +298,41 @@ impl ServiceClient { Ok(()) } + /// Enable the daemon system service. + /// + /// Does *not* start a stopped app. See [start_mullvad_daemon]. + pub async fn enable_mullvad_daemon(&self) -> Result<(), Error> { + let mut ctx = tarpc::context::current(); + ctx.deadline = SystemTime::now() + .checked_add(DAEMON_RESTART_TIMEOUT) + .unwrap(); + self.client + .enable_mullvad_daemon(ctx) + .await + .map_err(Error::Tarpc)??; + Ok(()) + } + + /// Disable the daemon system service. *Current only works on Windows*. + /// + /// This will not stop the daemon system service, but it will prevent it from starting + /// automatically on system boot. + /// + /// Note that if the daemon is also stopped, using [stop_mullvad_daemon], it will + /// not be possible to start it again until it is enabled again using + /// [enable_mullvad_daemon]. + pub async fn disable_mullvad_daemon(&self) -> Result<(), Error> { + let mut ctx = tarpc::context::current(); + ctx.deadline = SystemTime::now() + .checked_add(DAEMON_RESTART_TIMEOUT) + .unwrap(); + self.client + .disable_mullvad_daemon(ctx) + .await + .map_err(Error::Tarpc)??; + Ok(()) + } + pub async fn set_daemon_log_level( &self, verbosity_level: mullvad_daemon::Verbosity, @@ -372,6 +407,8 @@ impl ServiceClient { .await? } + /// Reboot the testing VM. The VM should be completely rebooted and responsive when this + /// future completes. 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 d92e6ff3af..853f626da9 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -89,7 +89,8 @@ impl Error { pub struct AmIMullvad { pub ip: IpAddr, pub mullvad_exit_ip: bool, - pub mullvad_exit_ip_hostname: String, + /// Will be `None` when not connected via mullvad relay + pub mullvad_exit_ip_hostname: Option<String>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -230,6 +231,12 @@ mod service { /// Start the Mullvad VPN application. async fn start_mullvad_daemon() -> Result<(), Error>; + /// Disable the Mullvad VPN system service. + async fn disable_mullvad_daemon() -> Result<(), Error>; + + /// Enable the Mullvad VPN system service. + async fn enable_mullvad_daemon() -> 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( diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index 95392dc93f..51a357543e 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -318,6 +318,31 @@ impl Service for TestServer { sys::start_app().await } + /// Disable the Mullvad VPN system service. + async fn disable_mullvad_daemon(self, _: context::Context) -> Result<(), test_rpc::Error> { + #[cfg(not(target_os = "windows"))] + { + log::warn!("disable_mullvad_daemon is only implemented on Windows"); + return Err(test_rpc::Error::Syscall); + } + #[cfg(target_os = "windows")] + { + sys::disable_system_service_startup().await + } + } + + async fn enable_mullvad_daemon(self, _: context::Context) -> Result<(), test_rpc::Error> { + #[cfg(not(target_os = "windows"))] + { + log::warn!("enable_mullvad_daemon is only implemented on Windows"); + return Err(test_rpc::Error::Syscall); + } + #[cfg(target_os = "windows")] + { + sys::enable_system_service_startup().await + } + } + async fn set_daemon_log_level( self, _: context::Context, diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs index a20a84cbaa..3490353b55 100644 --- a/test/test-runner/src/sys.rs +++ b/test/test-runner/src/sys.rs @@ -284,6 +284,51 @@ pub async fn start_app() -> Result<(), test_rpc::Error> { Ok(()) } +/// Disable the Mullvad VPN system service startup. This will not trigger the service to stop +/// immediately, but it will prevent it from starting on the next system boot. +#[cfg(target_os = "windows")] +pub async fn disable_system_service_startup() -> Result<(), test_rpc::Error> { + let status = tokio::process::Command::new("powershell") + .args([ + "-NoProfile", + "-Command", + "Set-Service -Name MullvadVPN -StartupType Disabled", + ]) + .status() + .await + .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?; + + if !status.success() { + return Err(test_rpc::Error::ServiceChange( + "Failed to disable MullvadVPN service".to_string(), + )); + } + + Ok(()) +} + +/// Enable the Mullvad VPN system service startup. This will configure the service to start automatically on system boot. +#[cfg(target_os = "windows")] +pub async fn enable_system_service_startup() -> Result<(), test_rpc::Error> { + let status = tokio::process::Command::new("powershell") + .args([ + "-NoProfile", + "-Command", + "Set-Service -Name MullvadVPN -StartupType Automatic", + ]) + .status() + .await + .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?; + + if !status.success() { + return Err(test_rpc::Error::ServiceChange( + "Failed to enable MullvadVPN service".to_string(), + )); + } + + Ok(()) +} + /// Restart the Mullvad VPN application. /// /// This function waits for the app to successfully start again. |
