From 4fa55c0f8cfe72efc3ced608147abba7068f6f12 Mon Sep 17 00:00:00 2001 From: Markus Pettersson Date: Mon, 4 Nov 2024 10:08:47 +0100 Subject: [PATCH] Check that the daemon version is correct post-upgrade --- mullvad-version/src/lib.rs | 61 ++++++++++++++++++++++++++ mullvad-version/src/main.rs | 38 +++------------- test/Cargo.lock | 8 ++++ test/test-manager/Cargo.toml | 1 + test/test-manager/src/tests/install.rs | 14 ++++++ test/test-rpc/src/client.rs | 10 +++++ test/test-rpc/src/lib.rs | 3 ++ test/test-runner/src/app.rs | 19 ++++++++ test/test-runner/src/main.rs | 5 +++ 9 files changed, 126 insertions(+), 33 deletions(-) diff --git a/mullvad-version/src/lib.rs b/mullvad-version/src/lib.rs index 9586b9b77660..71cb27e55da8 100644 --- a/mullvad-version/src/lib.rs +++ b/mullvad-version/src/lib.rs @@ -1,2 +1,63 @@ +use std::fmt::Display; +use std::str::FromStr; +use std::sync::LazyLock; + +use regex::Regex; + /// The Mullvad VPN app product version pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-version.txt")); + +#[derive(Debug, Clone, PartialEq)] +pub struct Version { + pub year: String, + pub incremental: String, + pub beta: Option, +} + +impl Version { + pub fn parse(version: &str) -> Version { + Version::from_str(version).unwrap() + } +} + +impl Display for Version { + /// Format Version as a string: year.incremental{-beta} + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Version { + year, + incremental, + beta, + } = &self; + match beta { + Some(beta) => write!(f, "{year}.{incremental}-{beta}"), + None => write!(f, "{year}.{incremental}"), + } + } +} + +impl FromStr for Version { + type Err = String; + + fn from_str(version: &str) -> Result { + const VERSION_REGEX: &str = + r"^20([0-9]{2})\.([1-9][0-9]?)(-beta([1-9][0-9]?))?(-dev-[0-9a-f]+)?$"; + static RE: LazyLock = LazyLock::new(|| Regex::new(VERSION_REGEX).unwrap()); + + let captures = RE + .captures(version) + .ok_or_else(|| format!("Version does not match expected format: {version}"))?; + let year = captures.get(1).expect("Missing year").as_str().to_owned(); + let incremental = captures + .get(2) + .ok_or("Missing incremental")? + .as_str() + .to_owned(); + let beta = captures.get(4).map(|m| m.as_str().to_owned()); + + Ok(Version { + year, + incremental, + beta, + }) + } +} diff --git a/mullvad-version/src/main.rs b/mullvad-version/src/main.rs index a4892633c9a6..ef72286eb7be 100644 --- a/mullvad-version/src/main.rs +++ b/mullvad-version/src/main.rs @@ -1,13 +1,9 @@ -use regex::Regex; +use mullvad_version::Version; use std::{env, process::exit}; const ANDROID_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/android-product-version.txt")); -const VERSION_REGEX: &str = r"^20([0-9]{2})\.([1-9][0-9]?)(-beta([1-9][0-9]?))?(-dev-[0-9a-f]+)?$"; - -const ANDROID_STABLE_VERSION_CODE_SUFFIX: &str = "99"; - fn main() { let command = env::args().nth(1); match command.as_deref() { @@ -53,7 +49,9 @@ fn to_semver(version: &str) -> String { /// Version: 2021.34 /// versionCode: 21340099 fn to_android_version_code(version: &str) -> String { - let version = parse_version(version); + const ANDROID_STABLE_VERSION_CODE_SUFFIX: &str = "99"; + + let version = Version::parse(version); format!( "{}{:0>2}00{:0>2}", version.year, @@ -67,7 +65,7 @@ fn to_android_version_code(version: &str) -> String { fn to_windows_h_format(version: &str) -> String { let Version { year, incremental, .. - } = parse_version(version); + } = Version::parse(version); format!( "#define MAJOR_VERSION 20{year} @@ -76,29 +74,3 @@ fn to_windows_h_format(version: &str) -> String { #define PRODUCT_VERSION \"{version}\"" ) } - -struct Version { - year: String, - incremental: String, - beta: Option, -} - -fn parse_version(version: &str) -> Version { - let re = Regex::new(VERSION_REGEX).unwrap(); - let captures = re - .captures(version) - .expect("Version does not match expected format"); - let year = captures.get(1).expect("Missing year").as_str().to_owned(); - let incremental = captures - .get(2) - .expect("Missing incremental") - .as_str() - .to_owned(); - let beta = captures.get(4).map(|m| m.as_str().to_owned()); - - Version { - year, - incremental, - beta, - } -} diff --git a/test/Cargo.lock b/test/Cargo.lock index dcbe9aef95a0..1c7324b2c590 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -1997,6 +1997,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "mullvad-version" +version = "0.0.0" +dependencies = [ + "regex", +] + [[package]] name = "multimap" version = "0.10.0" @@ -3444,6 +3451,7 @@ dependencies = [ "mullvad-management-interface", "mullvad-relay-selector", "mullvad-types", + "mullvad-version", "nix 0.29.0", "once_cell", "pcap", diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml index 80c8170635f4..8be5cda40637 100644 --- a/test/test-manager/Cargo.toml +++ b/test/test-manager/Cargo.toml @@ -57,6 +57,7 @@ mullvad-api = { path = "../../mullvad-api", features = ["api-override"] } mullvad-management-interface = { path = "../../mullvad-management-interface" } mullvad-relay-selector = { path = "../../mullvad-relay-selector" } mullvad-types = { path = "../../mullvad-types" } +mullvad-version = { path = "../../mullvad-version" } talpid-types = { path = "../../talpid-types" } ssh2 = "0.9.4" diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index 2f787f629950..ee4062ae8981 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -107,6 +107,20 @@ pub async fn test_upgrade_app( if rpc.mullvad_daemon_get_status().await? != ServiceStatus::Running { bail!(Error::DaemonNotRunning); } + + // Verify that the correct version was installed + let running_daemon_version = rpc.mullvad_daemon_version().await?; + let running_daemon_version = + mullvad_version::Version::parse(&running_daemon_version).to_string(); + ensure!( + &TEST_CONFIG + .app_package_filename + .contains(&running_daemon_version), + "Incorrect deamon version installed. Expected {expected} but {actual} is installed", + expected = TEST_CONFIG.app_package_filename.clone(), + actual = running_daemon_version + ); + // Check if any traffic was observed // let guest_ip = pinger.guest_ip; diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index da64dc465840..e1a8bc5ef965 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -141,6 +141,16 @@ impl ServiceClient { .map_err(Error::Tarpc) } + /// Return the version string as reported by `mullvad --version`. + /// + /// TODO: Replace with nicer version type. + pub async fn mullvad_daemon_version(&self) -> Result { + self.client + .mullvad_version(tarpc::context::current()) + .await + .map_err(Error::Tarpc)? + } + /// Returns all Mullvad app files, directories, and other data found on the system. pub async fn find_mullvad_app_traces(&self) -> Result, Error> { self.client diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 1ec8fb73216a..7c10f0df5317 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -156,6 +156,9 @@ mod service { /// Return status of the system service. async fn mullvad_daemon_get_status() -> mullvad_daemon::ServiceStatus; + /// Return version number of installed daemon. + async fn mullvad_version() -> Result; + /// Returns all Mullvad app files, directories, and other data found on the system. async fn find_mullvad_app_traces() -> Result, Error>; diff --git a/test/test-runner/src/app.rs b/test/test-runner/src/app.rs index f4e1fc3c5303..6c6ed0b369be 100644 --- a/test/test-runner/src/app.rs +++ b/test/test-runner/src/app.rs @@ -3,6 +3,25 @@ use std::path::{Path, PathBuf}; use test_rpc::{AppTrace, Error}; +/// Get the installed app version string +pub async fn version() -> Result { + let version = tokio::process::Command::new("mullvad") + .arg("--version") + .output() + .await + .map_err(|e| Error::Service(e.to_string()))?; + let version = String::from_utf8(version.stdout).map_err(|err| Error::Other(err.to_string()))?; + // HACK: The output from `mullvad --version` includes the `mullvad-cli` binary name followed by + // the version string. Simply remove the leading noise and get at the version string. + let Some(version) = version.split_whitespace().nth(1) else { + return Err(Error::Other( + "Could not parse version number from `mullvad-cli --version`".to_string(), + )); + }; + let version = version.to_string(); + Ok(version) +} + #[cfg(target_os = "windows")] pub fn find_traces() -> Result, Error> { // TODO: Check GUI data diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index 735e61360ec1..a107f29f3c33 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -136,6 +136,11 @@ impl Service for TestServer { get_pipe_status() } + /// Get the installed app version + async fn mullvad_version(self, _: context::Context) -> Result { + app::version().await + } + async fn find_mullvad_app_traces( self, _: context::Context,