Skip to content

Commit 1bb03d8

Browse files
Make sure connecting works while API is unavailable
1 parent b2a8166 commit 1bb03d8

File tree

4 files changed

+109
-45
lines changed

4 files changed

+109
-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: 70 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,8 @@ use std::{
2222
use talpid_types::net::wireguard::{PeerConfig, PrivateKey, TunnelConfig};
2323
use test_rpc::{package::Package, AmIMullvad, ServiceClient};
2424

25+
pub const THROTTLE_RETRY_DELAY: Duration = Duration::from_secs(120);
26+
2527
#[macro_export]
2628
macro_rules! assert_tunnel_state {
2729
($mullvad_client:expr, $pattern:pat) => {{
@@ -199,19 +201,44 @@ pub async fn ping_sized_with_timeout(
199201
.map_err(Error::Rpc)
200202
}
201203

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

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

232259
pub async fn disconnect_and_wait(mullvad_client: &mut MullvadProxyClient) -> Result<(), Error> {
233260
log::info!("Disconnecting");
261+
mullvad_client.disconnect_tunnel().await?;
234262

235-
mullvad_client
236-
.disconnect_tunnel()
237-
.await
238-
.map_err(|error| Error::Daemon(format!("failed to begin disconnecting: {}", error)))?;
239263
wait_for_tunnel_state(mullvad_client.clone(), |state| {
240264
matches!(state, TunnelState::Disconnected { .. })
241265
})
@@ -308,6 +332,30 @@ where
308332
}
309333
}
310334

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

460+
/// Return the current `MULLVAD_API_HOST` et al.
461+
///
462+
/// # Note
463+
/// This is independent of the running daemon's environment.
464+
/// It is solely dependant on the current value of [`TEST_CONFIG`].
412465
pub fn get_app_env() -> HashMap<String, String> {
413466
use mullvad_api::env;
414467
use std::net::ToSocketAddrs;
@@ -420,10 +473,10 @@ pub fn get_app_env() -> HashMap<String, String> {
420473
.next()
421474
.unwrap();
422475

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()
476+
HashMap::from_iter(vec![
477+
(env::API_HOST_VAR.to_string(), api_host),
478+
(env::API_ADDR_VAR.to_string(), api_addr.to_string()),
479+
])
427480
}
428481

429482
/// 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)