summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--test/test-manager/src/tests/mod.rs58
-rw-r--r--test/test-manager/src/tests/windows.rs141
-rw-r--r--test/test-rpc/src/client.rs37
-rw-r--r--test/test-rpc/src/lib.rs9
-rw-r--r--test/test-runner/src/main.rs25
-rw-r--r--test/test-runner/src/sys.rs45
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.