diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-08-29 10:47:52 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-03 09:27:54 +0200 |
| commit | 4ce272c4e3f0a8b5457d56dee5964bf61d35677b (patch) | |
| tree | 47cfb6384085a3063197c8820a798be8f3188cb9 | |
| parent | bcb3aed921840a20a890df5e44541ecc7482a068 (diff) | |
| download | mullvadvpn-4ce272c4e3f0a8b5457d56dee5964bf61d35677b.tar.xz mullvadvpn-4ce272c4e3f0a8b5457d56dee5964bf61d35677b.zip | |
Add command for querying latest version to mullvad-release
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-update/Cargo.toml | 2 | ||||
| -rw-r--r-- | mullvad-update/mullvad-release/Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-update/mullvad-release/src/main.rs | 74 | ||||
| -rw-r--r-- | mullvad-update/mullvad-release/src/platform.rs | 91 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 7 | ||||
| -rw-r--r-- | mullvad-update/src/version.rs | 6 |
8 files changed, 167 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9bd3b2e401..b92081e112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3350,6 +3350,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "strum", "tokio", "toml 0.8.19", ] diff --git a/Cargo.toml b/Cargo.toml index c01852c345..8485729562 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ serde = "1.0.204" serde_json = "1.0.122" windows-sys = "0.52.0" nix = "0.30.1" +strum = { version = "0.27" } # Networking pnet_packet = "0.35.0" diff --git a/mullvad-update/Cargo.toml b/mullvad-update/Cargo.toml index 73318c2a3a..4a60350d27 100644 --- a/mullvad-update/Cargo.toml +++ b/mullvad-update/Cargo.toml @@ -29,7 +29,7 @@ itertools = { workspace = true } reqwest = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } -strum = { version = "0.27", features = ["derive"], optional = true } +strum = { workspace = true, features = ["derive"], optional = true } tokio = { workspace = true, features = ["rt-multi-thread", "fs", "process", "macros"], optional = true } vec1 = { workspace = true } diff --git a/mullvad-update/mullvad-release/Cargo.toml b/mullvad-update/mullvad-release/Cargo.toml index 25a1191fe0..efb5a11ba6 100644 --- a/mullvad-update/mullvad-release/Cargo.toml +++ b/mullvad-update/mullvad-release/Cargo.toml @@ -20,6 +20,7 @@ reqwest = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } sha2 = { workspace = true } +strum = { workspace = true } tokio = { version = "1", features = ["full"] } toml = "0.8" diff --git a/mullvad-update/mullvad-release/src/main.rs b/mullvad-update/mullvad-release/src/main.rs index f2272d99e0..36ba666ec6 100644 --- a/mullvad-update/mullvad-release/src/main.rs +++ b/mullvad-update/mullvad-release/src/main.rs @@ -11,7 +11,10 @@ use config::Config; use io_util::create_dir_and_write; use platform::Platform; -use mullvad_update::format::{self, SignedResponse, key}; +use mullvad_update::{ + format::{self, SignedResponse, key}, + version::Rollout, +}; mod artifacts; mod config; @@ -104,6 +107,20 @@ pub enum Opt { /// Platforms to remove releases for. All if none are specified platforms: Vec<Platform>, }, + + /// Return the latest releases in `signed/` based on the given parameters. + /// The output is in JSON format. + QueryLatest { + /// Platforms to query for. All if none are specified + platforms: Vec<Platform>, + /// Rollout threshold to use (.0 = not rolled out, 1 = fully rolled out). + /// + /// By default, any non-zero rollout is accepted. + /// Setting the value to zero will also show supported versions that have + /// been released but are currently not being rolled out. + #[arg(long, default_value_t = mullvad_update::version::SUPPORTED_VERSION)] + rollout: Rollout, + }, } #[tokio::main] @@ -209,6 +226,61 @@ async fn main() -> anyhow::Result<()> { } Ok(()) } + Opt::QueryLatest { platforms, rollout } => { + #[derive(Default, serde::Serialize)] + struct SummaryQueryResult { + linux: Option<QueryResultOs>, + windows: Option<QueryResultOs>, + macos: Option<QueryResultOs>, + } + #[derive(serde::Serialize)] + struct QueryResultOs { + stable: QueryResultVersion, + beta: Option<QueryResultVersion>, + } + #[derive(serde::Serialize)] + struct QueryResultVersion { + version: mullvad_version::Version, + } + impl From<mullvad_version::Version> for QueryResultVersion { + fn from(version: mullvad_version::Version) -> Self { + QueryResultVersion { version } + } + } + + let mut summary_result = SummaryQueryResult::default(); + + for platform in all_platforms_if_empty(platforms) { + let out = platform.query_latest(rollout).await?; + + match platform { + Platform::Linux => { + summary_result.linux = Some(QueryResultOs { + stable: out.stable.into(), + beta: out.beta.map(Into::into), + }); + } + Platform::Windows => { + summary_result.windows = Some(QueryResultOs { + stable: out.stable.into(), + beta: out.beta.map(Into::into), + }); + } + Platform::Macos => { + summary_result.macos = Some(QueryResultOs { + stable: out.stable.into(), + beta: out.beta.map(Into::into), + }); + } + } + } + + let json = serde_json::to_string_pretty(&summary_result) + .context("Failed to serialize versions")?; + println!("{json}"); + + Ok(()) + } } } diff --git a/mullvad-update/mullvad-release/src/platform.rs b/mullvad-update/mullvad-release/src/platform.rs index bea8fb8876..3ebfcaaf37 100644 --- a/mullvad-update/mullvad-release/src/platform.rs +++ b/mullvad-update/mullvad-release/src/platform.rs @@ -4,6 +4,7 @@ use anyhow::{Context, anyhow, bail}; use mullvad_update::{ api::{HttpVersionInfoProvider, MetaRepositoryPlatform}, format::{self, key}, + version::{MIN_VERIFY_METADATA_VERSION, VersionArchitecture, VersionInfo, VersionParameters}, }; use std::{ cmp::Ordering, @@ -11,6 +12,7 @@ use std::{ path::{Path, PathBuf}, str::FromStr, }; +use strum::IntoEnumIterator; use tokio::{fs, io}; use crate::{ @@ -18,13 +20,23 @@ use crate::{ io_util::{create_dir_and_write, wait_for_confirm}, }; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum Platform { Windows, Linux, Macos, } +/// Output used by `Platform::query_latest` +#[derive(serde::Serialize)] +pub struct VersionQueryOutput { + /// Stable version info + pub stable: mullvad_version::Version, + /// Beta version info (if available and newer than `stable`). + /// If latest stable version is newer, this will be `None`. + pub beta: Option<mullvad_version::Version>, +} + impl fmt::Display for Platform { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -204,14 +216,7 @@ impl Platform { pub async fn verify(&self) -> anyhow::Result<()> { let signed_path = self.signed_path(); println!("Verifying signature of {}...", signed_path.display()); - let bytes = fs::read(signed_path).await.context("Failed to read file")?; - - format::SignedResponse::deserialize_and_verify( - &bytes, - mullvad_update::version::MIN_VERIFY_METADATA_VERSION, - ) - .context("Failed to verify metadata for {platform}: {error}")?; - + self.read_signed().await?; Ok(()) } @@ -382,6 +387,41 @@ impl Platform { Ok(()) } + /// Return the latest release for platforms in `signed/` + pub async fn query_latest(&self, rollout: f32) -> anyhow::Result<VersionQueryOutput> { + let response = self.read_signed().await?; + + // Grab version info for all architectures + let mut version_info = vec![]; + + for architecture in VersionArchitecture::iter() { + let params = VersionParameters { + architecture, + rollout, + // NOTE: Empty versions are allowed on Linux + allow_empty: self == &Platform::Linux, + lowest_metadata_version: MIN_VERIFY_METADATA_VERSION, + }; + version_info.push(VersionInfo::try_from_response( + ¶ms, + response.signed.clone(), + )?); + } + + // Verify that all architectures have the same version + assert_same_architecture_versions(version_info.iter())?; + + let version_info = version_info + .into_iter() + .next() + .expect("at least one version exists"); + + Ok(VersionQueryOutput { + stable: version_info.stable.version, + beta: version_info.beta.map(|v| v.version), + }) + } + /// Reads the metadata for `platform` in the work directory. /// If the file doesn't exist, this returns a new, empty response. async fn read_work(&self) -> anyhow::Result<format::SignedResponse> { @@ -400,6 +440,18 @@ impl Platform { // Note: We don't need to verify the signature here format::SignedResponse::deserialize_insecure(&bytes) } + + /// Read and verify the metadata for `platform` in the signed directory. + async fn read_signed(&self) -> anyhow::Result<format::SignedResponse> { + let signed_path = self.signed_path(); + let bytes = fs::read(signed_path).await.context("Failed to read file")?; + + format::SignedResponse::deserialize_and_verify( + &bytes, + mullvad_update::version::MIN_VERIFY_METADATA_VERSION, + ) + .context(format!("Failed to verify metadata for {self}")) + } } impl From<Platform> for MetaRepositoryPlatform { @@ -431,3 +483,24 @@ fn print_release_info(release: &format::Release) { (release.rollout * 100.) as u32 ); } + +fn assert_same_architecture_versions<'a>( + mut version_infos: impl Iterator<Item = &'a VersionInfo> + 'a, +) -> anyhow::Result<()> { + let Some(first_version_info) = version_infos.next() else { + bail!("No version was found"); + }; + + fn versions_are_equal(a: &VersionInfo, b: &VersionInfo) -> bool { + a.stable.version == b.stable.version + && a.beta.as_ref().map(|version| &version.version) + == b.beta.as_ref().map(|version| &version.version) + } + + let all_are_equal = version_infos.all(|info| versions_are_equal(first_version_info, info)); + if !all_are_equal { + bail!("Versions differ for different architectures. This is currently unsupported"); + } + + Ok(()) +} diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs index 762c1c14cc..790f38ee4b 100644 --- a/mullvad-update/src/format/mod.rs +++ b/mullvad-update/src/format/mod.rs @@ -50,9 +50,9 @@ struct PartialSignedResponse { } /// Signed JSON response, not including the signature -#[derive(Default, Debug, Deserialize, Serialize)] +#[derive(Default, Debug, Deserialize, Serialize, Clone)] #[serde(deny_unknown_fields)] -#[cfg_attr(test, derive(Clone, PartialEq))] +#[cfg_attr(test, derive(PartialEq))] pub struct Response { /// Version counter pub metadata_version: usize, @@ -63,8 +63,7 @@ pub struct Response { } /// App release -#[derive(Debug, Deserialize, Serialize)] -#[cfg_attr(test, derive(Clone))] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Release { /// Mullvad app version pub version: mullvad_version::Version, diff --git a/mullvad-update/src/version.rs b/mullvad-update/src/version.rs index 94b3f52e38..c37c2a2d7b 100644 --- a/mullvad-update/src/version.rs +++ b/mullvad-update/src/version.rs @@ -16,7 +16,7 @@ use crate::format::{self, Installer}; pub const MIN_VERIFY_METADATA_VERSION: usize = 0; /// Query type for [VersionInfo] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VersionParameters { /// Architecture to retrieve data for pub architecture: VersionArchitecture, @@ -35,6 +35,10 @@ pub type Rollout = f32; /// Accept *any* version (rollout >= 0) when querying for app info. pub const IGNORE: Rollout = 0.; +/// Accept any version (rollout > 0) when querying for app info. +/// Only versions with a non-zero rollout are supported. +pub const SUPPORTED_VERSION: Rollout = f32::EPSILON; + /// Accept only fully rolled out versions (rollout >= 1) when querying for app info. pub const FULLY_ROLLED_OUT: Rollout = 1.; |
