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..24852c3356e4 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,8 +3451,8 @@ dependencies = [ "mullvad-management-interface", "mullvad-relay-selector", "mullvad-types", + "mullvad-version", "nix 0.29.0", - "once_cell", "pcap", "pnet_base", "pnet_packet", @@ -3507,7 +3514,6 @@ dependencies = [ "log", "mullvad-paths", "nix 0.29.0", - "once_cell", "parity-tokio-ipc", "plist", "rand 0.8.5", diff --git a/test/Cargo.toml b/test/Cargo.toml index 8c691719ecaf..621591699260 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -75,7 +75,6 @@ shadowsocks-service = "1.20.3" windows-sys = "0.52.0" chrono = { version = "0.4.26", default-features = false } clap = { version = "4.2.7", features = ["cargo", "derive"] } -once_cell = "1.16.0" bytes = "1.3.0" async-trait = "0.1.58" surge-ping = "0.8" diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml index 80c8170635f4..2671ea454a4d 100644 --- a/test/test-manager/Cargo.toml +++ b/test/test-manager/Cargo.toml @@ -22,7 +22,6 @@ thiserror = { workspace = true } bytes = { workspace = true } test_macro = { path = "./test_macro" } ipnetwork = "0.20" -once_cell = { workspace = true } inventory = "0.3" data-encoding-macro = "0.1.12" itertools = "0.10.5" @@ -57,6 +56,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/config.rs b/test/test-manager/src/tests/config.rs index ae2f434698c2..04b2e01e71b1 100644 --- a/test/test-manager/src/tests/config.rs +++ b/test/test-manager/src/tests/config.rs @@ -1,7 +1,9 @@ -use once_cell::sync::OnceCell; +use std::sync::OnceLock; use std::{ops::Deref, path::Path}; use test_rpc::meta::Os; +pub static TEST_CONFIG: TestConfigContainer = TestConfigContainer::new(); + /// Default `mullvad_host`. This should match the production env. pub const DEFAULT_MULLVAD_HOST: &str = "mullvad.net"; /// Bundled OpenVPN CA certificate use with the installed Mullvad app. @@ -110,9 +112,13 @@ impl Default for BootstrapScript { } #[derive(Debug, Clone)] -pub struct TestConfigContainer(OnceCell); +pub struct TestConfigContainer(OnceLock); impl TestConfigContainer { + const fn new() -> Self { + TestConfigContainer(OnceLock::new()) + } + /// Initializes the constants. /// /// # Panics @@ -130,5 +136,3 @@ impl Deref for TestConfigContainer { self.0.get().unwrap() } } - -pub static TEST_CONFIG: TestConfigContainer = TestConfigContainer(OnceCell::new()); 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/Cargo.toml b/test/test-runner/Cargo.toml index 8df61e7164f8..fd53f4b7cb79 100644 --- a/test/test-runner/Cargo.toml +++ b/test/test-runner/Cargo.toml @@ -18,7 +18,6 @@ tokio = { workspace = true } tokio-serial = { workspace = true } thiserror = { workspace = true } log = { workspace = true } -once_cell = { workspace = true } parity-tokio-ipc = "0.9" bytes = { workspace = true } serde = { workspace = true } 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, diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index a12fa2776cb6..7d32f0481208 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -251,10 +251,10 @@ pub fn get_interface_mac(_interface: &str) -> Result, test_rpc:: #[cfg(target_os = "windows")] pub fn get_default_interface() -> &'static str { - use once_cell::sync::OnceCell; + use std::sync::OnceLock; use talpid_platform_metadata::WindowsVersion; - static WINDOWS_VERSION: OnceCell = OnceCell::new(); + static WINDOWS_VERSION: OnceLock = OnceLock::new(); let version = WINDOWS_VERSION .get_or_init(|| WindowsVersion::new().expect("failed to obtain Windows version")); diff --git a/test/test-runner/src/package.rs b/test/test-runner/src/package.rs index 8de1f32c3fc3..5cc0b92dabe2 100644 --- a/test/test-runner/src/package.rs +++ b/test/test-runner/src/package.rs @@ -173,6 +173,13 @@ fn apt_command() -> Command { // instead. cmd.args(["-o", "DPkg::Lock::Timeout=60"]); cmd.arg("-qy"); + // `apt` may consider installing a development build to be a downgrade from the baseline if the + // major version is identical, in which case the ordering is incorrectly based on the git hash + // suffix. + // + // Note that this is only sound if we take precaution to check the installed version after + // running this command. + cmd.arg("--allow-downgrades"); cmd.env("DEBIAN_FRONTEND", "noninteractive");