Skip to content

Commit 0932f2c

Browse files
Make sure connecting works while API is unavailable
1 parent 93c31c8 commit 0932f2c

File tree

4 files changed

+110
-45
lines changed

4 files changed

+110
-45
lines changed

test/test-manager/src/tests/account.rs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::tests::helpers::{login_with_retries, THROTTLE_RETRY_DELAY};
2+
13
use super::config::TEST_CONFIG;
24
use super::{helpers, ui, Error, TestContext};
35
use mullvad_api::DevicesProxy;
@@ -10,8 +12,6 @@ use talpid_types::net::wireguard;
1012
use test_macro::test_function;
1113
use test_rpc::ServiceClient;
1214

13-
const THROTTLE_RETRY_DELAY: Duration = Duration::from_secs(120);
14-
1515
/// Log in and create a new device for the account.
1616
#[test_function(always_run = true, must_succeed = true, priority = -100)]
1717
pub async fn test_login(
@@ -241,32 +241,6 @@ pub fn new_device_client() -> DevicesProxy {
241241
DevicesProxy::new(rest_handle)
242242
}
243243

244-
/// Log in and retry if it fails due to throttling
245-
pub async fn login_with_retries(
246-
mullvad_client: &mut MullvadProxyClient,
247-
) -> Result<(), mullvad_management_interface::Error> {
248-
loop {
249-
match mullvad_client
250-
.login_account(TEST_CONFIG.account_number.clone())
251-
.await
252-
{
253-
Err(mullvad_management_interface::Error::Rpc(status))
254-
if status.message().to_uppercase().contains("THROTTLED") =>
255-
{
256-
// Work around throttling errors by sleeping
257-
log::debug!(
258-
"Login failed due to throttling. Sleeping for {} seconds",
259-
THROTTLE_RETRY_DELAY.as_secs()
260-
);
261-
262-
tokio::time::sleep(THROTTLE_RETRY_DELAY).await;
263-
}
264-
Err(err) => break Err(err),
265-
Ok(_) => break Ok(()),
266-
}
267-
}
268-
}
269-
270244
pub async fn list_devices_with_retries(
271245
device_client: &DevicesProxy,
272246
) -> Result<Vec<Device>, mullvad_api::rest::Error> {

test/test-manager/src/tests/helpers.rs

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{config::TEST_CONFIG, Error, WAIT_FOR_TUNNEL_STATE_TIMEOUT};
1+
use super::{config::TEST_CONFIG, Error, TestContext, WAIT_FOR_TUNNEL_STATE_TIMEOUT};
22
use crate::network_monitor::{
33
self, start_packet_monitor, MonitorOptions, MonitorUnexpectedlyStopped, PacketMonitor,
44
};
@@ -22,6 +22,9 @@ use std::{
2222
use talpid_types::net::wireguard::{PeerConfig, PrivateKey, TunnelConfig};
2323
use test_rpc::{package::Package, AmIMullvad, ServiceClient};
2424

25+
/// TODO: Document
26+
pub const THROTTLE_RETRY_DELAY: Duration = Duration::from_secs(120);
27+
2528
#[macro_export]
2629
macro_rules! assert_tunnel_state {
2730
($mullvad_client:expr, $pattern:pat) => {{
@@ -199,19 +202,44 @@ pub async fn ping_sized_with_timeout(
199202
.map_err(Error::Rpc)
200203
}
201204

205+
/// Log in and retry if it fails due to throttling
206+
pub async fn login_with_retries(
207+
mullvad_client: &mut MullvadProxyClient,
208+
) -> Result<(), mullvad_management_interface::Error> {
209+
loop {
210+
match mullvad_client
211+
.login_account(TEST_CONFIG.account_number.clone())
212+
.await
213+
{
214+
Err(mullvad_management_interface::Error::Rpc(status))
215+
if status.message().to_uppercase().contains("THROTTLED") =>
216+
{
217+
// Work around throttling errors by sleeping
218+
log::debug!(
219+
"Login failed due to throttling. Sleeping for {} seconds",
220+
THROTTLE_RETRY_DELAY.as_secs()
221+
);
222+
223+
tokio::time::sleep(THROTTLE_RETRY_DELAY).await;
224+
}
225+
Err(err) => break Err(err),
226+
Ok(_) => break Ok(()),
227+
}
228+
}
229+
}
230+
202231
/// Try to connect to a Mullvad Tunnel.
203232
///
204-
/// If that fails to begin to connect, [`Error::DaemonError`] is returned. If it fails to connect
205-
/// after that, the daemon ends up in the [`TunnelState::Error`] state, and
206-
/// [`Error::UnexpectedErrorState`] is returned.
233+
/// # Returns
234+
/// - `Result::Ok` if the daemon successfully connected to a tunnel
235+
/// - `Result::Err` if:
236+
/// - The daemon failed to even begin connecting. Then [`Error::Rpc`] is returned.
237+
/// - The daemon started to connect but ended up in the [`TunnelState::Error`] state.
238+
/// Then [`Error::UnexpectedErrorState`] is returned
207239
pub async fn connect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result<(), Error> {
208240
log::info!("Connecting");
209241

210-
mullvad_client
211-
.connect_tunnel()
212-
.await
213-
.map_err(|error| Error::Daemon(format!("failed to begin connecting: {}", error)))?;
214-
242+
mullvad_client.connect_tunnel().await?;
215243
let new_state = wait_for_tunnel_state(mullvad_client.clone(), |state| {
216244
matches!(
217245
state,
@@ -231,11 +259,8 @@ pub async fn connect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result
231259

232260
pub async fn disconnect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result<(), Error> {
233261
log::info!("Disconnecting");
262+
mullvad_client.disconnect_tunnel().await?;
234263

235-
mullvad_client
236-
.disconnect_tunnel()
237-
.await
238-
.map_err(|error| Error::Daemon(format!("failed to begin disconnecting: {}", error)))?;
239264
wait_for_tunnel_state(mullvad_client.clone(), |state| {
240265
matches!(state, TunnelState::Disconnected { .. })
241266
})
@@ -308,6 +333,30 @@ where
308333
}
309334
}
310335

336+
/// Set environment variables specified by `env` and restart the Mullvad daemon.
337+
/// Returns a new [rpc client][`MullvadProxyClient`], since the old client *probably*
338+
/// can't communicate with the new daemon.
339+
///
340+
/// # Note
341+
/// This is just a thin wrapper around [`ServiceClient::set_daemon_environment`] which also
342+
/// invalides the old [`MullvadProxyClient`].
343+
pub async fn restart_daemon_with<K, V, Env>(
344+
rpc: &ServiceClient,
345+
test_context: &TestContext,
346+
_: MullvadProxyClient, // Just consume the old proxy client
347+
env: Env,
348+
) -> Result<MullvadProxyClient, Error>
349+
where
350+
Env: IntoIterator<Item = (K, V)>,
351+
K: Into<String>,
352+
V: Into<String>,
353+
{
354+
rpc.set_daemon_environment(env).await?;
355+
// Need to create a new `mullvad_client` here after the restart
356+
// otherwise we *probably* can't communicate with the daemon.
357+
Ok(test_context.rpc_provider.new_client().await)
358+
}
359+
311360
pub async fn geoip_lookup_with_retries(rpc: &ServiceClient) -> Result<AmIMullvad, Error> {
312361
const MAX_ATTEMPTS: usize = 5;
313362
const BEFORE_RETRY_DELAY: Duration = Duration::from_secs(2);
@@ -409,6 +458,11 @@ pub fn unreachable_wireguard_tunnel() -> talpid_types::net::wireguard::Connectio
409458
}
410459
}
411460

461+
/// Return the current `MULLVAD_API_HOST` et al.
462+
///
463+
/// # Note
464+
/// This is independent of the running daemon's environment.
465+
/// It is solely dependant on the current value of [`TEST_CONFIG`].
412466
pub fn get_app_env() -> HashMap<String, String> {
413467
use mullvad_api::env;
414468
use std::net::ToSocketAddrs;
@@ -420,10 +474,10 @@ pub fn get_app_env() -> HashMap<String, String> {
420474
.next()
421475
.unwrap();
422476

423-
let api_host_env = (env::API_HOST_VAR.to_string(), api_host);
424-
let api_addr_env = (env::API_ADDR_VAR.to_string(), api_addr.to_string());
425-
426-
[api_host_env, api_addr_env].into_iter().collect()
477+
HashMap::from_iter(vec![
478+
(env::API_HOST_VAR.to_string(), api_host),
479+
(env::API_ADDR_VAR.to_string(), api_addr.to_string()),
480+
])
427481
}
428482

429483
/// Return a filtered version of the daemon's relay list.

test/test-manager/src/tests/tunnel.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use super::helpers::{
33
};
44
use super::{config::TEST_CONFIG, Error, TestContext};
55
use crate::network_monitor::{start_packet_monitor, MonitorOptions};
6+
use crate::tests::helpers::login_with_retries;
67

78
use mullvad_management_interface::MullvadProxyClient;
89
use mullvad_types::relay_constraints::{
@@ -770,3 +771,34 @@ pub async fn test_local_socks_bridge(
770771

771772
Ok(())
772773
}
774+
775+
/// Verify that the app can connect to a VPN server and get working internet when the API is down.
776+
/// As long as the user has managed to log in to the app, establishing a tunnel should work even if the API is down (This includes actually being down, not just censored).
777+
///
778+
/// The test procedure is as follows:
779+
/// 1. The app is logged in
780+
/// 2. The app is killed
781+
/// 3. The API is "removed" (override API IP/host to something bogus)
782+
/// 4. The app is restarted
783+
/// 5. Verify that it starts as intended and a tunnel can be established
784+
#[test_function]
785+
pub async fn test_establish_tunnel_without_api(
786+
ctx: TestContext,
787+
rpc: ServiceClient,
788+
mut mullvad_client: MullvadProxyClient,
789+
) -> anyhow::Result<()> {
790+
// 1
791+
login_with_retries(&mut mullvad_client).await?;
792+
// 2
793+
rpc.stop_mullvad_daemon().await?;
794+
// 3
795+
let borked_env = [("MULLVAD_API_ADDR", "1.3.3.7:421")];
796+
// 4
797+
log::debug!("Restarting the daemon with the following overrides: {borked_env:?}");
798+
let mut mullvad_client =
799+
helpers::restart_daemon_with(&rpc, &ctx, mullvad_client, borked_env).await?;
800+
// 5
801+
connect_and_wait(&mut mullvad_client).await?;
802+
// Profit
803+
Ok(())
804+
}

test/test-rpc/src/client.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ impl ServiceClient {
292292
Ok(())
293293
}
294294

295+
/// Set environment variables specified by `env` and restart the Mullvad daemon.
296+
///
297+
/// # Returns
298+
/// - `Result::Ok` if the daemon was successfully restarted.
299+
/// - `Result::Err(Error)` if the daemon could not be restarted and is thus no longer running.
295300
pub async fn set_daemon_environment<Env, K, V>(&self, env: Env) -> Result<(), Error>
296301
where
297302
Env: IntoIterator<Item = (K, V)>,

0 commit comments

Comments
 (0)