summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-03-25 10:37:35 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-25 10:37:35 +0100
commit36e9c8599f0b33d540636f773c8f58dc2d971c29 (patch)
treef1d43cd7a87692108bb0cc8b545da686e74de1f3
parent2094a39d7ec07d5c154f46cff82a3335d23af751 (diff)
parent6b8d2c7178c8d0c3f8428ee2fad3df59c591454d (diff)
downloadmullvadvpn-36e9c8599f0b33d540636f773c8f58dc2d971c29.tar.xz
mullvadvpn-36e9c8599f0b33d540636f773c8f58dc2d971c29.zip
Merge branch 'use-new-version-endpoint'
-rw-r--r--Cargo.lock3
-rw-r--r--mullvad-api/Cargo.toml5
-rw-r--r--mullvad-api/src/lib.rs46
-rw-r--r--mullvad-api/src/rest.rs26
-rw-r--r--mullvad-api/src/version.rs96
-rw-r--r--mullvad-daemon/src/version_check.rs37
-rw-r--r--mullvad-ios/Cargo.toml2
-rw-r--r--test/Cargo.lock79
8 files changed, 225 insertions, 69 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e9d7219758..37ad7063b9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2609,6 +2609,7 @@ dependencies = [
name = "mullvad-api"
version = "0.0.0"
dependencies = [
+ "anyhow",
"async-trait",
"cbindgen 0.28.0",
"chrono",
@@ -2624,6 +2625,7 @@ dependencies = [
"mullvad-encrypted-dns-proxy",
"mullvad-fs",
"mullvad-types",
+ "mullvad-update",
"rustls-pemfile 2.1.3",
"serde",
"serde_json",
@@ -2636,6 +2638,7 @@ dependencies = [
"tokio-socks",
"tower 0.5.1",
"uuid",
+ "vec1",
]
[[package]]
diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml
index 46eb8fbaff..f55ab02f9a 100644
--- a/mullvad-api/Cargo.toml
+++ b/mullvad-api/Cargo.toml
@@ -15,6 +15,7 @@ workspace = true
api-override = []
[dependencies]
+anyhow = { workspace = true }
async-trait = "0.1"
libc = "0.2"
chrono = { workspace = true }
@@ -46,6 +47,7 @@ tokio-rustls = { version = "0.26.0", features = [
tokio-socks = "0.5.1"
rustls-pemfile = "2.1.3"
uuid = { version = "1.4.1", features = ["v4"] }
+vec1 = { workspace = true }
mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" }
mullvad-fs = { path = "../mullvad-fs" }
@@ -55,6 +57,9 @@ talpid-time = { path = "../talpid-time" }
shadowsocks = { workspace = true, features = ["stream-cipher"] }
+[target.'cfg(not(target_os = "ios"))'.dependencies]
+mullvad-update = { path = "../mullvad-update", features = ["client"] }
+
[dev-dependencies]
talpid-time = { path = "../talpid-time", features = ["test"] }
tokio = { workspace = true, features = ["test-util", "time"] }
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs
index d8980a17b5..1ced489080 100644
--- a/mullvad-api/src/lib.rs
+++ b/mullvad-api/src/lib.rs
@@ -3,12 +3,9 @@ use async_trait::async_trait;
#[cfg(target_os = "android")]
use futures::channel::mpsc;
use hyper::body::Incoming;
+use mullvad_types::account::{AccountData, AccountNumber, VoucherSubmission};
#[cfg(target_os = "android")]
use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken};
-use mullvad_types::{
- account::{AccountData, AccountNumber, VoucherSubmission},
- version::AppVersion,
-};
use proxy::{ApiConnectionMode, ConnectionModeProvider};
use std::{
collections::BTreeMap,
@@ -23,6 +20,8 @@ use talpid_types::ErrorExt;
pub mod availability;
use availability::ApiAvailability;
pub mod rest;
+#[cfg(not(target_os = "ios"))]
+pub mod version;
mod abortable_stream;
pub mod access_mode;
@@ -699,45 +698,6 @@ impl ProblemReportProxy {
}
#[derive(Clone)]
-pub struct AppVersionProxy {
- handle: rest::MullvadRestHandle,
-}
-
-#[derive(serde::Deserialize, Debug)]
-pub struct AppVersionResponse {
- pub supported: bool,
- pub latest: AppVersion,
- pub latest_stable: Option<AppVersion>,
- pub latest_beta: AppVersion,
-}
-
-impl AppVersionProxy {
- pub fn new(handle: rest::MullvadRestHandle) -> Self {
- Self { handle }
- }
-
- pub fn version_check(
- &self,
- app_version: AppVersion,
- platform: &str,
- platform_version: String,
- ) -> impl Future<Output = Result<AppVersionResponse, rest::Error>> + use<> {
- let service = self.handle.service.clone();
-
- let path = format!("{APP_URL_PREFIX}/releases/{platform}/{app_version}");
- let request = self.handle.factory.get(&path);
-
- async move {
- let request = request?
- .expected_status(&[StatusCode::OK])
- .header("M-Platform-Version", &platform_version)?;
- let response = service.request(request).await?;
- response.deserialize().await
- }
- }
-}
-
-#[derive(Clone)]
pub struct ApiProxy {
handle: rest::MullvadRestHandle,
}
diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs
index bab6d8112a..94c43392ad 100644
--- a/mullvad-api/src/rest.rs
+++ b/mullvad-api/src/rest.rs
@@ -13,7 +13,7 @@ use futures::{
};
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
use hyper::{
- body::{Body, Bytes, Incoming},
+ body::{Body, Buf, Bytes, Incoming},
header::{self, HeaderValue},
Method, Uri,
};
@@ -73,6 +73,14 @@ pub enum Error {
#[error("Set account number on factory with no access token store")]
NoAccessTokenStore,
+
+ /// Failed to obtain versions
+ #[error("Failed to obtain versions")]
+ FetchVersions(#[from] Arc<anyhow::Error>),
+
+ /// Body exceeded size limit
+ #[error("Body exceeded size limit")]
+ BodyTooLarge,
}
impl From<Infallible> for Error {
@@ -493,7 +501,7 @@ pub struct Response<B> {
response: hyper::Response<B>,
}
-impl<B: Body> Response<B>
+impl<B: Body + Unpin> Response<B>
where
Error: From<<B as Body>::Error>,
{
@@ -516,6 +524,20 @@ where
pub async fn body(self) -> Result<Vec<u8>> {
Ok(BodyExt::collect(self.response).await?.to_bytes().to_vec())
}
+
+ pub async fn body_with_max_size(self, size_limit: usize) -> Result<Vec<u8>> {
+ let mut data: Vec<u8> = vec![];
+ let mut stream = self.response.into_data_stream();
+
+ while let Some(chunk) = stream.next().await {
+ data.extend(chunk?.chunk());
+ if data.len() > size_limit {
+ return Err(Error::BodyTooLarge);
+ }
+ }
+
+ Ok(data)
+ }
}
#[derive(serde::Deserialize)]
diff --git a/mullvad-api/src/version.rs b/mullvad-api/src/version.rs
new file mode 100644
index 0000000000..26cbe1d2d3
--- /dev/null
+++ b/mullvad-api/src/version.rs
@@ -0,0 +1,96 @@
+use std::future::Future;
+use std::sync::Arc;
+
+use http::StatusCode;
+use mullvad_types::version::AppVersion;
+use mullvad_update::version::{VersionInfo, VersionParameters};
+use vec1::vec1;
+
+use super::rest;
+use super::APP_URL_PREFIX;
+
+#[derive(Clone)]
+pub struct AppVersionProxy {
+ handle: super::rest::MullvadRestHandle,
+}
+
+#[derive(serde::Deserialize, Debug)]
+pub struct AppVersionResponse {
+ pub supported: bool,
+ pub latest: AppVersion,
+ pub latest_stable: Option<AppVersion>,
+ pub latest_beta: AppVersion,
+}
+
+impl AppVersionProxy {
+ /// Public key to use for `version_check_2` response
+ const VERSION_PROVIDER_PUBKEY: &str = include_str!("../../mullvad-update/stagemole-pubkey");
+
+ /// Maximum size of `version_check_2` response
+ const SIZE_LIMIT: usize = 1024 * 1024;
+
+ pub fn new(handle: rest::MullvadRestHandle) -> Self {
+ Self { handle }
+ }
+
+ pub fn version_check(
+ &self,
+ app_version: AppVersion,
+ platform: &str,
+ platform_version: String,
+ ) -> impl Future<Output = Result<AppVersionResponse, rest::Error>> + use<> {
+ let service = self.handle.service.clone();
+
+ let path = format!("{APP_URL_PREFIX}/releases/{platform}/{app_version}");
+ let request = self.handle.factory.get(&path);
+
+ async move {
+ let request = request?
+ .expected_status(&[StatusCode::OK])
+ .header("M-Platform-Version", &platform_version)?;
+ let response = service.request(request).await?;
+ response.deserialize().await
+ }
+ }
+
+ /// Get versions from `/app/releases/<platform>.json`
+ pub fn version_check_2(
+ &self,
+ platform: &str,
+ architecture: mullvad_update::format::Architecture,
+ rollout: f32,
+ lowest_metadata_version: usize,
+ ) -> impl Future<Output = Result<VersionInfo, rest::Error>> + use<> {
+ let service = self.handle.service.clone();
+ let path = format!("app/releases/{platform}.json");
+ let request = self.handle.factory.get(&path);
+
+ let verifying_key =
+ mullvad_update::format::key::VerifyingKey::from_hex(Self::VERSION_PROVIDER_PUBKEY)
+ .expect("valid key");
+ let verifying_keys = vec1![verifying_key];
+
+ async move {
+ let request = request?.expected_status(&[StatusCode::OK]);
+ let response = service.request(request).await?;
+ let bytes = response.body_with_max_size(Self::SIZE_LIMIT).await?;
+
+ let response = mullvad_update::format::SignedResponse::deserialize_and_verify(
+ &verifying_keys,
+ &bytes,
+ lowest_metadata_version,
+ )
+ .map_err(|err| rest::Error::FetchVersions(Arc::new(err)))?;
+
+ let params = VersionParameters {
+ architecture,
+ rollout,
+ lowest_metadata_version,
+ };
+
+ VersionInfo::try_from_response(&params, response.signed)
+ .map_err(Arc::new)
+ .map_err(rest::Error::FetchVersions)
+ }
+ }
+}
diff --git a/mullvad-daemon/src/version_check.rs b/mullvad-daemon/src/version_check.rs
index 0539d675f1..a66dbc6903 100644
--- a/mullvad-daemon/src/version_check.rs
+++ b/mullvad-daemon/src/version_check.rs
@@ -4,7 +4,11 @@ use futures::{
future::{BoxFuture, FusedFuture},
FutureExt, SinkExt, StreamExt, TryFutureExt,
};
-use mullvad_api::{availability::ApiAvailability, rest::MullvadRestHandle, AppVersionProxy};
+use mullvad_api::{
+ availability::ApiAvailability,
+ rest::MullvadRestHandle,
+ version::{AppVersionProxy, AppVersionResponse},
+};
use mullvad_types::version::AppVersionInfo;
use mullvad_version::Version;
use serde::{Deserialize, Serialize};
@@ -194,11 +198,8 @@ impl VersionUpdaterInner {
self.last_app_version_info.as_ref().map(|(info, _)| info)
}
- /// Convert a [mullvad_api::AppVersionResponse] to an [AppVersionInfo].
- fn response_to_version_info(
- &self,
- response: mullvad_api::AppVersionResponse,
- ) -> AppVersionInfo {
+ /// Convert a [AppVersionResponse] to an [AppVersionInfo].
+ fn response_to_version_info(&self, response: AppVersionResponse) -> AppVersionInfo {
let suggested_upgrade = suggested_upgrade(
&APP_VERSION,
&response.latest_stable,
@@ -295,12 +296,9 @@ impl VersionUpdaterInner {
mut self,
mut rx: mpsc::Receiver<VersionUpdaterCommand>,
update: impl Fn(AppVersionInfo) -> BoxFuture<'static, Result<(), Error>>,
- do_version_check: impl Fn()
- -> BoxFuture<'static, Result<mullvad_api::AppVersionResponse, Error>>,
- do_version_check_in_background: impl Fn() -> BoxFuture<
- 'static,
- Result<mullvad_api::AppVersionResponse, Error>,
- >,
+ do_version_check: impl Fn() -> BoxFuture<'static, Result<AppVersionResponse, Error>>,
+ do_version_check_in_background: impl Fn()
+ -> BoxFuture<'static, Result<AppVersionResponse, Error>>,
) {
let mut version_is_stale = self.wait_until_version_is_stale();
let mut version_check = futures::future::Fuse::terminated();
@@ -422,9 +420,7 @@ struct ApiContext {
}
/// Immediately query the API for the latest [AppVersionInfo].
-fn do_version_check(
- api: ApiContext,
-) -> BoxFuture<'static, Result<mullvad_api::AppVersionResponse, Error>> {
+fn do_version_check(api: ApiContext) -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
let download_future_factory = move || {
api.version_proxy
.version_check(
@@ -459,7 +455,7 @@ fn do_version_check(
/// On any error, this function retries repeatedly every [UPDATE_INTERVAL_ERROR] until success.
fn do_version_check_in_background(
api: ApiContext,
-) -> BoxFuture<'static, Result<mullvad_api::AppVersionResponse, Error>> {
+) -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
let download_future_factory = move || {
let when_available = api.api_handle.wait_background();
let request = api.version_proxy.version_check(
@@ -721,12 +717,11 @@ mod test {
}
}
- fn fake_version_check() -> BoxFuture<'static, Result<mullvad_api::AppVersionResponse, Error>> {
+ fn fake_version_check() -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
Box::pin(async { Ok(fake_version_response()) })
}
- fn fake_version_check_err() -> BoxFuture<'static, Result<mullvad_api::AppVersionResponse, Error>>
- {
+ fn fake_version_check_err() -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
Box::pin(retry_future(
|| async { Err(Error::Download(mullvad_api::rest::Error::TimeoutError)) },
|_| true,
@@ -734,8 +729,8 @@ mod test {
))
}
- fn fake_version_response() -> mullvad_api::AppVersionResponse {
- mullvad_api::AppVersionResponse {
+ fn fake_version_response() -> AppVersionResponse {
+ AppVersionResponse {
supported: true,
latest: "2024.1".to_owned(),
latest_stable: None,
diff --git a/mullvad-ios/Cargo.toml b/mullvad-ios/Cargo.toml
index 8f95f148d9..aa7ee661f8 100644
--- a/mullvad-ios/Cargo.toml
+++ b/mullvad-ios/Cargo.toml
@@ -28,7 +28,7 @@ talpid-future = { path = "../talpid-future" }
talpid-types = { path = "../talpid-types" }
talpid-tunnel-config-client = { path = "../talpid-tunnel-config-client" }
mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" }
-mullvad-api = { path = "../mullvad-api" }
+mullvad-api = { path = "../mullvad-api", default-features = false }
serde_json = { workspace = true }
shadowsocks-service = { workspace = true, features = [
diff --git a/test/Cargo.lock b/test/Cargo.lock
index e2e014cce3..3cde6c40bd 100644
--- a/test/Cargo.lock
+++ b/test/Cargo.lock
@@ -642,6 +642,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
+ "digest",
"fiat-crypto",
"rustc_version",
"subtle",
@@ -769,6 +770,21 @@ dependencies = [
]
[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "educe"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1800,6 +1816,17 @@ dependencies = [
]
[[package]]
+name = "json-canon"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447ae153a2bd47d61acc0d131295408e32ef87ed9785825a6f4ecef85afc0edb"
+dependencies = [
+ "ryu-js",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2046,6 +2073,7 @@ dependencies = [
name = "mullvad-api"
version = "0.0.0"
dependencies = [
+ "anyhow",
"async-trait",
"cbindgen",
"chrono",
@@ -2060,6 +2088,7 @@ dependencies = [
"mullvad-encrypted-dns-proxy",
"mullvad-fs",
"mullvad-types",
+ "mullvad-update",
"rustls-pemfile 2.1.3",
"serde",
"serde_json",
@@ -2072,6 +2101,7 @@ dependencies = [
"tokio-socks",
"tower 0.5.1",
"uuid",
+ "vec1",
]
[[package]]
@@ -2162,10 +2192,32 @@ dependencies = [
]
[[package]]
+name = "mullvad-update"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "chrono",
+ "ed25519-dalek",
+ "hex",
+ "json-canon",
+ "mullvad-version",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 2.0.3",
+ "tokio",
+ "vec1",
+ "zeroize",
+]
+
+[[package]]
name = "mullvad-version"
version = "0.0.0"
dependencies = [
"regex-lite",
+ "serde",
]
[[package]]
@@ -2906,9 +2958,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "reqwest"
-version = "0.12.7"
+version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
+checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64 0.22.0",
"bytes",
@@ -3116,6 +3168,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
+name = "ryu-js"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f"
+
+[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3252,6 +3310,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "shadowsocks"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4248,6 +4317,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
+name = "vec1"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"