summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-08-29 10:47:52 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-09-03 09:27:54 +0200
commit4ce272c4e3f0a8b5457d56dee5964bf61d35677b (patch)
tree47cfb6384085a3063197c8820a798be8f3188cb9
parentbcb3aed921840a20a890df5e44541ecc7482a068 (diff)
downloadmullvadvpn-4ce272c4e3f0a8b5457d56dee5964bf61d35677b.tar.xz
mullvadvpn-4ce272c4e3f0a8b5457d56dee5964bf61d35677b.zip
Add command for querying latest version to mullvad-release
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--mullvad-update/Cargo.toml2
-rw-r--r--mullvad-update/mullvad-release/Cargo.toml1
-rw-r--r--mullvad-update/mullvad-release/src/main.rs74
-rw-r--r--mullvad-update/mullvad-release/src/platform.rs91
-rw-r--r--mullvad-update/src/format/mod.rs7
-rw-r--r--mullvad-update/src/version.rs6
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(
+ &params,
+ 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.;