Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent apt from being confused about mullvad-vpn package version #7125

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions mullvad-version/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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<Self, Self::Err> {
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<Regex> = 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,
})
}
}
38 changes: 5 additions & 33 deletions mullvad-version/src/main.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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,
Expand All @@ -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}
Expand All @@ -76,29 +74,3 @@ fn to_windows_h_format(version: &str) -> String {
#define PRODUCT_VERSION \"{version}\""
)
}

struct Version {
year: String,
incremental: String,
beta: Option<String>,
}

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,
}
}
10 changes: 8 additions & 2 deletions test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion test/test-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 8 additions & 4 deletions test/test-manager/src/tests/config.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -110,9 +112,13 @@ impl Default for BootstrapScript {
}

#[derive(Debug, Clone)]
pub struct TestConfigContainer(OnceCell<TestConfig>);
pub struct TestConfigContainer(OnceLock<TestConfig>);

impl TestConfigContainer {
const fn new() -> Self {
TestConfigContainer(OnceLock::new())
}

/// Initializes the constants.
///
/// # Panics
Expand All @@ -130,5 +136,3 @@ impl Deref for TestConfigContainer {
self.0.get().unwrap()
}
}

pub static TEST_CONFIG: TestConfigContainer = TestConfigContainer(OnceCell::new());
14 changes: 14 additions & 0 deletions test/test-manager/src/tests/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions test/test-rpc/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
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<Vec<AppTrace>, Error> {
self.client
Expand Down
3 changes: 3 additions & 0 deletions test/test-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error>;

/// Returns all Mullvad app files, directories, and other data found on the system.
async fn find_mullvad_app_traces() -> Result<Vec<AppTrace>, Error>;

Expand Down
1 change: 0 additions & 1 deletion test/test-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
19 changes: 19 additions & 0 deletions test/test-runner/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
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<Vec<AppTrace>, Error> {
// TODO: Check GUI data
Expand Down
5 changes: 5 additions & 0 deletions test/test-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ impl Service for TestServer {
get_pipe_status()
}

/// Get the installed app version
async fn mullvad_version(self, _: context::Context) -> Result<String, test_rpc::Error> {
app::version().await
}

async fn find_mullvad_app_traces(
self,
_: context::Context,
Expand Down
4 changes: 2 additions & 2 deletions test/test-runner/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ pub fn get_interface_mac(_interface: &str) -> Result<Option<[u8; 6]>, 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<WindowsVersion> = OnceCell::new();
static WINDOWS_VERSION: OnceLock<WindowsVersion> = OnceLock::new();
let version = WINDOWS_VERSION
.get_or_init(|| WindowsVersion::new().expect("failed to obtain Windows version"));

Expand Down
7 changes: 7 additions & 0 deletions test/test-runner/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Loading