summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2024-04-12 15:39:30 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2024-04-12 15:39:30 +0200
commit6b7c1fcf2b28e624d90480bab480bfd7404fc499 (patch)
tree3e088931812cf2c158ffbc9f4795924d54cc9f2a
parentf36bd2e90024501f46a9dabfb032bbc9ab2b6627 (diff)
parent9531b3085b3831108fee3e4d7a8563ac79dc4225 (diff)
downloadmullvadvpn-6b7c1fcf2b28e624d90480bab480bfd7404fc499.tar.xz
mullvadvpn-6b7c1fcf2b28e624d90480bab480bfd7404fc499.zip
Merge branch 'fix-flaky-test_custom_access_methods-des-778'
-rw-r--r--test/Cargo.lock5
-rw-r--r--test/socks-server/src/lib.rs8
-rw-r--r--test/test-manager/Cargo.toml2
-rw-r--r--test/test-manager/src/mullvad_daemon.rs15
-rw-r--r--test/test-manager/src/run_tests.rs18
-rw-r--r--test/test-manager/src/tests/access_methods.rs94
-rw-r--r--test/test-manager/src/tests/dns.rs39
-rw-r--r--test/test-manager/src/tests/mod.rs38
-rw-r--r--test/test-manager/test_macro/src/lib.rs20
-rw-r--r--test/test-rpc/src/client.rs14
-rw-r--r--test/test-rpc/src/lib.rs12
-rw-r--r--test/test-runner/src/main.rs7
-rw-r--r--test/test-runner/src/sys.rs223
13 files changed, 363 insertions, 132 deletions
diff --git a/test/Cargo.lock b/test/Cargo.lock
index eb38b5f37a..6d55901090 100644
--- a/test/Cargo.lock
+++ b/test/Cargo.lock
@@ -2123,15 +2123,16 @@ dependencies = [
[[package]]
name = "pcap"
-version = "0.10.1"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da544c8115cc65b474554569c7654fc94a4f6a167f79192536e148fd654e17a"
+checksum = "99e935fc73d54a89fff576526c2ccd42bbf8247aae05b358693475b14fd4ff79"
dependencies = [
"bitflags 1.3.2",
"errno 0.2.8",
"futures",
"libc",
"libloading",
+ "pkg-config",
"regex",
"tokio",
"windows-sys 0.36.1",
diff --git a/test/socks-server/src/lib.rs b/test/socks-server/src/lib.rs
index 1b3e712643..cc8847db1b 100644
--- a/test/socks-server/src/lib.rs
+++ b/test/socks-server/src/lib.rs
@@ -1,3 +1,4 @@
+use fast_socks5::server::{AcceptAuthentication, Socks5Server};
use futures::StreamExt;
use std::{io, net::SocketAddr};
@@ -13,10 +14,9 @@ pub struct Handle {
/// Spawn a SOCKS server bound to `bind_addr`
pub async fn spawn(bind_addr: SocketAddr) -> Result<Handle, Error> {
- let socks_server: fast_socks5::server::Socks5Server =
- fast_socks5::server::Socks5Server::bind(bind_addr)
- .await
- .map_err(Error::StartSocksServer)?;
+ let socks_server: Socks5Server<AcceptAuthentication> = Socks5Server::bind(bind_addr)
+ .await
+ .map_err(Error::StartSocksServer)?;
let handle = tokio::spawn(async move {
let mut incoming = socks_server.incoming();
diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml
index 37cfbc5a85..fa86db329e 100644
--- a/test/test-manager/Cargo.toml
+++ b/test/test-manager/Cargo.toml
@@ -39,7 +39,7 @@ serde_json = { workspace = true }
tokio-serde = { workspace = true }
log = { workspace = true }
-pcap = { version = "0.10.1", features = ["capture-stream"] }
+pcap = { version = "1.3", features = ["capture-stream"] }
pnet_packet = "0.31.0"
test-rpc = { path = "../test-rpc" }
diff --git a/test/test-manager/src/mullvad_daemon.rs b/test/test-manager/src/mullvad_daemon.rs
index e1a7680cb4..115a09e049 100644
--- a/test/test-manager/src/mullvad_daemon.rs
+++ b/test/test-manager/src/mullvad_daemon.rs
@@ -51,14 +51,17 @@ pub struct RpcClientProvider {
service: DummyService,
}
+pub enum MullvadClientArgument {
+ WithClient(MullvadProxyClient),
+ None,
+}
+
impl RpcClientProvider {
- pub async fn as_type(
- &self,
- client_type: MullvadClientVersion,
- ) -> Box<dyn std::any::Any + Send> {
+ /// Whether a [test case](test_macro::test_function) needs a [`MullvadProxyClient`].
+ pub async fn mullvad_client(&self, client_type: MullvadClientVersion) -> MullvadClientArgument {
match client_type {
- MullvadClientVersion::New => Box::new(self.new_client().await),
- MullvadClientVersion::None => Box::new(()),
+ MullvadClientVersion::New => MullvadClientArgument::WithClient(self.new_client().await),
+ MullvadClientVersion::None => MullvadClientArgument::None,
}
}
diff --git a/test/test-manager/src/run_tests.rs b/test/test-manager/src/run_tests.rs
index 0c7868e529..73c72ebc2e 100644
--- a/test/test-manager/src/run_tests.rs
+++ b/test/test-manager/src/run_tests.rs
@@ -1,6 +1,6 @@
use crate::{
logging::{panic_as_string, TestOutput},
- mullvad_daemon,
+ mullvad_daemon::{self, MullvadClientArgument},
summary::{self, maybe_log_test_result},
tests::{self, config::TEST_CONFIG, get_tests, TestContext},
vm,
@@ -80,9 +80,9 @@ pub async fn run(
let logger = super::logging::Logger::get_or_init();
for test in tests {
- let mclient = test_context
+ let mullvad_client = test_context
.rpc_provider
- .as_type(test.mullvad_client_version)
+ .mullvad_client(test.mullvad_client_version)
.await;
log::info!("Running {}", test.name);
@@ -94,7 +94,7 @@ pub async fn run(
let test_result = run_test(
client.clone(),
- mclient,
+ mullvad_client,
&test.func,
test.name,
test_context.clone(),
@@ -105,8 +105,8 @@ pub async fn run(
// Try to reset the daemon state if the test failed OR if the test doesn't explicitly
// disabled cleanup.
if test.cleanup || matches!(test_result.result, Err(_) | Ok(Err(_))) {
- let mut client = test_context.rpc_provider.new_client().await;
- crate::tests::cleanup_after_test(&mut client).await?;
+ crate::tests::cleanup_after_test(client.clone(), &test_context.rpc_provider)
+ .await?;
}
}
@@ -175,15 +175,15 @@ pub async fn run(
final_result
}
-pub async fn run_test<F, R, MullvadClient>(
+pub async fn run_test<F, R>(
runner_rpc: ServiceClient,
- mullvad_rpc: MullvadClient,
+ mullvad_rpc: MullvadClientArgument,
test: &F,
test_name: &'static str,
test_context: super::tests::TestContext,
) -> TestOutput
where
- F: Fn(super::tests::TestContext, ServiceClient, MullvadClient) -> R,
+ F: Fn(super::tests::TestContext, ServiceClient, MullvadClientArgument) -> R,
R: Future<Output = anyhow::Result<()>>,
{
let _flushed = runner_rpc.try_poll_output().await;
diff --git a/test/test-manager/src/tests/access_methods.rs b/test/test-manager/src/tests/access_methods.rs
index 1d5660f4b0..747ebd23e5 100644
--- a/test/test-manager/src/tests/access_methods.rs
+++ b/test/test-manager/src/tests/access_methods.rs
@@ -1,50 +1,30 @@
//! Integration tests for API access methods.
-use super::{Error, TestContext};
+//!
+//! The tested access methods are:
+//! * Shadowsocks
+//! * SOCKS5 in remote mode
+//!
+//! These tests rely on working proxies to exist *somewhere* for all tested protocols.
+//! If the proxies themselves are bad/not running, this test will fail due to issues
+//! that are out of the test manager's control.
+use anyhow::{anyhow, ensure, Context};
+
use mullvad_management_interface::MullvadProxyClient;
+use talpid_types::net::proxy::CustomProxy;
use test_macro::test_function;
use test_rpc::ServiceClient;
-/// Assert that custom access methods may be used to access the Mullvad API.
-///
-/// The tested access methods are:
-/// * Shadowsocks
-/// * Socks5 in remote mode
-///
-/// # Note
-///
-/// This tests assume that there exists working proxies *somewhere* for all
-/// tested protocols. If the proxies themselves are bad/not running, this test
-/// will fail due to issues that are out of the test manager's control.
+use super::TestContext;
+
+/// Assert that API traffic can be proxied via a custom Shadowsocks proxy.
#[test_function]
-pub async fn test_custom_access_methods(
+async fn test_access_method_shadowsocks(
_: TestContext,
_rpc: ServiceClient,
- mullvad_client: MullvadProxyClient,
-) -> Result<(), Error> {
- log::info!("Testing Shadowsocks access method");
- test_shadowsocks(mullvad_client.clone()).await?;
- log::info!("Testing SOCKS5 (Remote) access method");
- test_socks_remote(mullvad_client.clone()).await?;
- Ok(())
-}
-
-macro_rules! assert_access_method_works {
- ($mullvad_client:expr, $access_method:expr) => {
- let successful = $mullvad_client
- .test_custom_api_access_method($access_method.clone().into())
- .await
- .expect("Failed to test custom API access method");
-
- assert!(
- successful,
- "Failed while testing access method - {:?}",
- $access_method
- );
- };
-}
-
-async fn test_shadowsocks(mut mullvad_client: MullvadProxyClient) -> Result<(), Error> {
+ mut mullvad_client: MullvadProxyClient,
+) -> anyhow::Result<()> {
use mullvad_relay_selector::{RelaySelector, SelectorConfig};
+ log::info!("Testing Shadowsocks access method");
// Set up all the parameters needed to create a custom Shadowsocks access method.
//
// Since Mullvad's bridge servers host Shadowsocks relays, we can simply
@@ -53,22 +33,48 @@ async fn test_shadowsocks(mut mullvad_client: MullvadProxyClient) -> Result<(),
let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list);
let access_method = relay_selector
.get_bridge_forced()
- .expect("`test_shadowsocks` needs at least one shadowsocks relay to execute. Found none in relay list.");
- assert_access_method_works!(mullvad_client, access_method);
+ .context("`test_shadowsocks` needs at least one shadowsocks relay to execute. Found none in relay list.")?;
+ log::info!("Selected shadowsocks bridge: {access_method:?}");
+ assert_access_method_works(mullvad_client, access_method.clone())
+ .await
+ .with_context(|| anyhow!("Access method {access_method:?} did not work!"))?;
Ok(())
}
-async fn test_socks_remote(mut mullvad_client: MullvadProxyClient) -> Result<(), Error> {
+/// Assert that API traffic can be proxied via a custom SOCKS5 proxy.
+#[test_function]
+async fn test_access_method_socks5_remote(
+ _: TestContext,
+ _rpc: ServiceClient,
+ mullvad_client: MullvadProxyClient,
+) -> anyhow::Result<()> {
use crate::vm::network::{NON_TUN_GATEWAY, SOCKS5_PORT};
use std::net::SocketAddr;
- use talpid_types::net::proxy::{CustomProxy, Socks5Remote};
+ use talpid_types::net::proxy::Socks5Remote;
+ log::info!("Testing SOCKS5 (Remote) access method");
// Set up all the parameters needed to create a custom SOCKS5 access method.
//
// The remote SOCKS5 proxy is assumed to be running on the test manager. On
// which port it listens to is defined as a constant in the `test-manager`
// crate.
let endpoint = SocketAddr::from((NON_TUN_GATEWAY, SOCKS5_PORT));
- let access_method = CustomProxy::from(Socks5Remote::new(endpoint));
- assert_access_method_works!(mullvad_client, access_method);
+ let access_method = Socks5Remote::new(endpoint);
+ log::info!("Testing SOCKS5-proxy: {access_method:?}");
+ assert_access_method_works(mullvad_client, access_method.clone())
+ .await
+ .with_context(|| anyhow!("Access method {access_method:?} did not work!"))?;
+ Ok(())
+}
+
+async fn assert_access_method_works(
+ mut mullvad_client: MullvadProxyClient,
+ access_method: impl Into<CustomProxy> + std::fmt::Debug,
+) -> anyhow::Result<()> {
+ let successful = mullvad_client
+ .test_custom_api_access_method(access_method.into())
+ .await
+ .context("Failed to test custom API access method")?;
+
+ ensure!(successful, "Failed while testing access method");
Ok(())
}
diff --git a/test/test-manager/src/tests/dns.rs b/test/test-manager/src/tests/dns.rs
index bff7059aeb..a6f564139f 100644
--- a/test/test-manager/src/tests/dns.rs
+++ b/test/test-manager/src/tests/dns.rs
@@ -1,3 +1,4 @@
+use anyhow::{anyhow, Context};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::atomic::{AtomicUsize, Ordering},
@@ -334,7 +335,7 @@ pub async fn test_dns_config_default(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
run_dns_config_tunnel_test(
&rpc,
&mut mullvad_client,
@@ -353,7 +354,7 @@ pub async fn test_dns_config_custom_private(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
log::debug!("Setting custom DNS resolver to {NON_TUN_GATEWAY}");
mullvad_client
@@ -365,7 +366,7 @@ pub async fn test_dns_config_custom_private(
state: settings::DnsState::Custom,
})
.await
- .expect("failed to configure DNS server");
+ .context("failed to configure DNS server")?;
run_dns_config_non_tunnel_test(&rpc, &mut mullvad_client, IpAddr::V4(NON_TUN_GATEWAY)).await
}
@@ -380,7 +381,7 @@ pub async fn test_dns_config_custom_public(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
let custom_ip = IpAddr::V4(Ipv4Addr::new(1, 3, 3, 7));
log::debug!("Setting custom DNS resolver to {custom_ip}");
@@ -394,7 +395,7 @@ pub async fn test_dns_config_custom_public(
state: settings::DnsState::Custom,
})
.await
- .expect("failed to configure DNS server");
+ .context("failed to configure DNS server")?;
run_dns_config_tunnel_test(&rpc, &mut mullvad_client, custom_ip).await
}
@@ -406,7 +407,7 @@ pub async fn test_content_blockers(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
const DNS_BLOCKING_IP_BASE: Ipv4Addr = Ipv4Addr::new(100, 64, 0, 0);
let content_blockers = [
(
@@ -498,7 +499,7 @@ pub async fn test_content_blockers(
state: settings::DnsState::Default,
})
.await
- .expect("failed to configure DNS server");
+ .context("failed to configure DNS server")?;
run_dns_config_tunnel_test(&rpc, &mut mullvad_client, test_ip).await?;
}
@@ -510,7 +511,7 @@ async fn run_dns_config_tunnel_test(
rpc: &ServiceClient,
mullvad_client: &mut MullvadProxyClient,
expected_dns_resolver: IpAddr,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
run_dns_config_test(
rpc,
|| {
@@ -534,7 +535,7 @@ async fn run_dns_config_non_tunnel_test(
rpc: &ServiceClient,
mullvad_client: &mut MullvadProxyClient,
expected_dns_resolver: IpAddr,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
run_dns_config_test(
rpc,
|| {
@@ -561,33 +562,33 @@ async fn run_dns_config_test<
create_monitor: impl FnOnce() -> F,
mullvad_client: &mut MullvadProxyClient,
expected_dns_resolver: IpAddr,
-) -> Result<(), Error> {
+) -> anyhow::Result<()> {
match mullvad_client.get_tunnel_state().await {
// prevent reconnect
Ok(mullvad_types::states::TunnelState::Connected { .. }) => (),
_ => {
connect_local_wg_relay(mullvad_client)
.await
- .expect("failed to connect to custom wg relay");
+ .context("failed to connect to custom wg relay")?;
}
}
let nontun_iface = rpc
.get_default_interface()
.await
- .expect("failed to find non-tun interface");
+ .context("failed to find non-tun interface")?;
let tunnel_iface = helpers::get_tunnel_interface(mullvad_client)
.await
- .expect("failed to find tunnel interface");
+ .context("failed to find tunnel interface")?;
let nontun_ip = rpc
.get_interface_ip(nontun_iface)
.await
- .expect("failed to obtain guest IP");
+ .context("failed to obtain guest IP")?;
let tunnel_ip = rpc
.get_interface_ip(tunnel_iface)
.await
- .expect("failed to obtain tunnel IP");
+ .context("failed to obtain tunnel IP")?;
log::debug!("Tunnel (guest) IP: {tunnel_ip}");
log::debug!("Non-tunnel (guest) IP: {nontun_ip}");
@@ -612,7 +613,13 @@ async fn run_dns_config_test<
});
assert_eq!(
- monitor.wait().await.unwrap().packets[0].destination,
+ monitor
+ .wait()
+ .await?
+ .packets
+ .first()
+ .ok_or(anyhow!("Expected at least one packet, saw none"))?
+ .destination,
SocketAddr::new(expected_dns_resolver, 53),
"expected tunnel packet to expected destination {expected_dns_resolver}"
);
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs
index f4c4563bde..ce38308b0d 100644
--- a/test/test-manager/src/tests/mod.rs
+++ b/test/test-manager/src/tests/mod.rs
@@ -13,14 +13,16 @@ mod tunnel;
mod tunnel_state;
mod ui;
-use crate::mullvad_daemon::RpcClientProvider;
+use crate::{
+ mullvad_daemon::{MullvadClientArgument, RpcClientProvider},
+ tests::helpers::get_app_env,
+};
use anyhow::Context;
pub use test_metadata::TestMetadata;
use test_rpc::ServiceClient;
use futures::future::BoxFuture;
-use mullvad_management_interface::MullvadProxyClient;
use std::time::Duration;
const WAIT_FOR_TUNNEL_STATE_TIMEOUT: Duration = Duration::from_secs(40);
@@ -30,11 +32,8 @@ pub struct TestContext {
pub rpc_provider: RpcClientProvider,
}
-pub type TestWrapperFunction = fn(
- TestContext,
- ServiceClient,
- Box<dyn std::any::Any + Send>,
-) -> BoxFuture<'static, anyhow::Result<()>>;
+pub type TestWrapperFunction =
+ fn(TestContext, ServiceClient, MullvadClientArgument) -> BoxFuture<'static, anyhow::Result<()>>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@@ -72,10 +71,16 @@ pub fn get_tests() -> Vec<&'static TestMetadata> {
}
/// Restore settings to the defaults.
-pub async fn cleanup_after_test(mullvad_client: &mut MullvadProxyClient) -> anyhow::Result<()> {
+pub async fn cleanup_after_test(
+ rpc: ServiceClient,
+ rpc_provider: &RpcClientProvider,
+) -> anyhow::Result<()> {
log::debug!("Cleaning up daemon in test cleanup");
+ // Check if daemon should be restarted
+ restart_daemon(rpc).await?;
+ let mut mullvad_client = rpc_provider.new_client().await;
- helpers::disconnect_and_wait(mullvad_client).await?;
+ helpers::disconnect_and_wait(&mut mullvad_client).await?;
// Bring all the settings into scope so we remember to reset them.
let mullvad_types::settings::Settings {
@@ -179,3 +184,18 @@ pub async fn cleanup_after_test(mullvad_client: &mut MullvadProxyClient) -> anyh
Ok(())
}
+
+/// Conditionally restart the running daemon
+///
+/// If the daemon was started with non-standard environment variables, subsequent tests may break
+/// due to assuming a default configuration. In that case, reset the environment variables and
+/// restart.
+async fn restart_daemon(rpc: ServiceClient) -> anyhow::Result<()> {
+ let current_env = rpc.get_daemon_environment().await?;
+ let default_env = get_app_env();
+ if current_env != default_env {
+ log::debug!("Restarting daemon due changed environment variables. Values since last test {current_env:?}");
+ rpc.set_daemon_environment(default_env).await?;
+ }
+ Ok(())
+}
diff --git a/test/test-manager/test_macro/src/lib.rs b/test/test-manager/test_macro/src/lib.rs
index 161605e8ca..7e19990b97 100644
--- a/test/test-manager/test_macro/src/lib.rs
+++ b/test/test-manager/test_macro/src/lib.rs
@@ -199,20 +199,18 @@ fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
let func_name = test_function.name;
let function_mullvad_version = test_function.function_parameters.mullvad_client.version();
let wrapper_closure = match test_function.function_parameters.mullvad_client {
- MullvadClient::New {
- mullvad_client_type,
- ..
- } => {
- let mullvad_client_type = *mullvad_client_type;
+ MullvadClient::New { .. } => {
quote! {
|test_context: crate::tests::TestContext,
rpc: test_rpc::ServiceClient,
- mullvad_client: Box<dyn std::any::Any + Send>,|
+ mullvad_client: crate::mullvad_daemon::MullvadClientArgument|
{
- use std::any::Any;
- let mullvad_client = mullvad_client.downcast::<#mullvad_client_type>().expect("invalid mullvad client");
+ let mullvad_client = match mullvad_client {
+ crate::mullvad_daemon::MullvadClientArgument::WithClient(client) => client,
+ crate::mullvad_daemon::MullvadClientArgument::None => unreachable!("invalid mullvad client")
+ };
Box::pin(async move {
- #func_name(test_context, rpc, *mullvad_client).await.map_err(Into::into)
+ #func_name(test_context, rpc, mullvad_client).await.map_err(Into::into)
})
}
}
@@ -221,7 +219,7 @@ fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
quote! {
|test_context: crate::tests::TestContext,
rpc: test_rpc::ServiceClient,
- _mullvad_client: Box<dyn std::any::Any + Send>| {
+ _mullvad_client: crate::mullvad_daemon::MullvadClientArgument| {
Box::pin(async move {
#func_name(test_context, rpc).await.map_err(Into::into)
})
@@ -264,7 +262,6 @@ enum MullvadClient {
mullvad_client_version: proc_macro2::TokenStream,
},
New {
- mullvad_client_type: Box<syn::Type>,
mullvad_client_version: proc_macro2::TokenStream,
},
}
@@ -314,7 +311,6 @@ fn get_test_function_parameters(
let mullvad_client_version =
quote! { test_rpc::mullvad_daemon::MullvadClientVersion::New };
MullvadClient::New {
- mullvad_client_type: pat_type.ty,
mullvad_client_version,
}
}
diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs
index 79c6a038ca..84d923a826 100644
--- a/test/test-rpc/src/client.rs
+++ b/test/test-rpc/src/client.rs
@@ -314,6 +314,20 @@ impl ServiceClient {
Ok(())
}
+ /// Get the current daemon's environment variables.
+ ///
+ /// # Returns
+ /// - `Result::Ok(env)` if the current environment variables could be read.
+ /// - `Result::Err(Error)` if communication with the daemon failed or the environment values
+ /// could not be parsed.
+ pub async fn get_daemon_environment(&self) -> Result<HashMap<String, String>, Error> {
+ let env = self
+ .client
+ .get_daemon_environment(tarpc::context::current())
+ .await??;
+ Ok(env)
+ }
+
pub async fn copy_file(&self, src: String, dest: String) -> Result<(), Error> {
log::debug!("Copying \"{src}\" to \"{dest}\"");
self.client
diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs
index fc9ea67c84..82fed91541 100644
--- a/test/test-rpc/src/lib.rs
+++ b/test/test-rpc/src/lib.rs
@@ -59,10 +59,19 @@ pub enum Error {
TcpForward,
#[error("Unknown process ID: {0}")]
UnknownPid(u32),
+ #[error("Failed to join tokio task: {0}")]
+ TokioJoinError(String),
#[error("{0}")]
Other(String),
}
+impl Error {
+ /// Convenient mapping from a Tokio error to the test_rpc Error type.
+ pub fn from_tokio_join_error(error: tokio::task::JoinError) -> Error {
+ Error::TokioJoinError(error.to_string())
+ }
+}
+
/// Response from am.i.mullvad.net
#[derive(Debug, Serialize, Deserialize)]
pub struct AmIMullvad {
@@ -213,6 +222,9 @@ mod service {
/// service.
async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), Error>;
+ /// Get the environment variables for the running daemon service.
+ async fn get_daemon_environment() -> Result<HashMap<String, String>, Error>;
+
/// Copy a file from `src` to `dest` on the test runner.
async fn copy_file(src: String, dest: String) -> Result<(), Error>;
diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs
index 29f0f936d3..79e11e0506 100644
--- a/test/test-runner/src/main.rs
+++ b/test/test-runner/src/main.rs
@@ -309,6 +309,13 @@ impl Service for TestServer {
sys::set_daemon_environment(env).await
}
+ async fn get_daemon_environment(
+ self,
+ _: context::Context,
+ ) -> Result<HashMap<String, String>, test_rpc::Error> {
+ sys::get_daemon_environment().await
+ }
+
async fn copy_file(
self,
_: context::Context,
diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs
index 5015f26ca1..0a176b1c3d 100644
--- a/test/test-runner/src/sys.rs
+++ b/test/test-runner/src/sys.rs
@@ -4,13 +4,22 @@ use std::io;
use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity};
#[cfg(target_os = "windows")]
-use std::ffi::OsString;
+use std::{ffi::OsString, path::Path};
#[cfg(target_os = "windows")]
use windows_service::{
service::{ServiceAccess, ServiceInfo},
service_manager::{ServiceManager, ServiceManagerAccess},
};
+#[cfg(target_os = "linux")]
+const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/override.conf";
+
+#[cfg(target_os = "macos")]
+const PLIST_OVERRIDE_FILE: &str = "/Library/LaunchDaemons/net.mullvad.daemon.plist";
+
+#[cfg(target_os = "windows")]
+const MULLVAD_WIN_REGISTRY: &str = r"SYSTEM\CurrentControlSet\Services\Mullvad VPN";
+
#[cfg(target_os = "windows")]
pub fn reboot() -> Result<(), test_rpc::Error> {
use windows_sys::Win32::{
@@ -154,9 +163,6 @@ pub fn reboot() -> Result<(), test_rpc::Error> {
#[cfg(target_os = "linux")]
pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> {
use tokio::io::AsyncWriteExt;
- const SYSTEMD_OVERRIDE_FILE: &str =
- "/etc/systemd/system/mullvad-daemon.service.d/override.conf";
-
log::debug!("Setting log level");
let verbosity = match verbosity_level {
@@ -389,17 +395,57 @@ pub async fn set_daemon_log_level(_verbosity_level: Verbosity) -> Result<(), tes
}
#[cfg(target_os = "linux")]
+#[derive(Debug)]
+struct EnvVar {
+ var: String,
+ value: String,
+}
+
+#[cfg(target_os = "linux")]
+impl EnvVar {
+ fn from_systemd_string(s: &str) -> Result<Self, &'static str> {
+ // Here, we are only concerned with parsing a line that starts with "Environment".
+ let error = "Failed to parse systemd env-config";
+ let mut input = s.trim().split('=');
+ let pre = input.next().ok_or(error)?;
+ match pre {
+ "Environment" => {
+ // Proccess the input just a bit more - remove the leading and trailing quote (").
+ let var = input
+ .next()
+ .ok_or(error)?
+ .trim_start_matches('"')
+ .to_string();
+ let value = input.next().ok_or(error)?.trim_end_matches('"').to_string();
+ Ok(EnvVar { var, value })
+ }
+ _ => Err(error),
+ }
+ }
+
+ fn to_systemd_string(&self) -> String {
+ format!(
+ "Environment=\"{key}={value}\"",
+ key = self.var,
+ value = self.value
+ )
+ }
+}
+
+#[cfg(target_os = "linux")]
pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
use std::fmt::Write;
- use tokio::io::AsyncWriteExt;
-
- const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/env.conf";
let mut override_content = String::new();
override_content.push_str("[Service]\n");
- for (k, v) in env {
- writeln!(&mut override_content, "Environment=\"{k}={v}\"").unwrap();
+ for env_var in env
+ .into_iter()
+ .map(|(var, value)| EnvVar { var, value })
+ .map(|env_var| env_var.to_systemd_string())
+ {
+ writeln!(&mut override_content, "{env_var}")
+ .map_err(|err| test_rpc::Error::Service(err.to_string()))?;
}
let override_path = std::path::Path::new(SYSTEMD_OVERRIDE_FILE);
@@ -409,15 +455,7 @@ pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(),
.map_err(|e| test_rpc::Error::Service(e.to_string()))?;
}
- let mut file = tokio::fs::OpenOptions::new()
- .create(true)
- .truncate(true)
- .write(true)
- .open(override_path)
- .await
- .map_err(|e| test_rpc::Error::Service(e.to_string()))?;
-
- file.write_all(override_content.as_bytes())
+ tokio::fs::write(override_path, override_content)
.await
.map_err(|e| test_rpc::Error::Service(e.to_string()))?;
@@ -440,7 +478,7 @@ pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(),
#[cfg(target_os = "windows")]
pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
// Set environment globally (not for service) to prevent it from being lost on upgrade
- for (k, v) in env {
+ for (k, v) in env.clone() {
tokio::process::Command::new("setx")
.arg("/m")
.args([k, v])
@@ -448,6 +486,18 @@ pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(),
.await
.map_err(|e| test_rpc::Error::Registry(e.to_string()))?;
}
+ // Persist the changed environment variables, such that we can retrieve them at will.
+ use winreg::{enums::*, RegKey};
+ let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
+ let path = Path::new(MULLVAD_WIN_REGISTRY).join("Environment");
+ let (registry, _) = hklm.create_subkey(&path).map_err(|error| {
+ test_rpc::Error::Registry(format!("Failed to open Mullvad VPN subkey: {}", error))
+ })?;
+ for (k, v) in env {
+ registry.set_value(k, &v).map_err(|error| {
+ test_rpc::Error::Registry(format!("Failed to set Environment var: {}", error))
+ })?;
+ }
// Restart service
stop_app().await?;
@@ -464,7 +514,7 @@ pub fn get_system_path_var() -> Result<String, test_rpc::Error> {
let key = hklm
.open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
.map_err(|error| {
- test_rpc::Error::Registry(format!("Failed to open environment subkey: {}", error))
+ test_rpc::Error::Registry(format!("Failed to open Environment subkey: {}", error))
})?;
let path: String = key
@@ -476,14 +526,11 @@ pub fn get_system_path_var() -> Result<String, test_rpc::Error> {
#[cfg(target_os = "macos")]
pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
- const PLIST_PATH: &str = "/Library/LaunchDaemons/net.mullvad.daemon.plist";
-
tokio::task::spawn_blocking(|| {
- let mut parsed_plist: plist::Value = plist::from_file(PLIST_PATH)
+ let mut parsed_plist = plist::Value::from_file(PLIST_OVERRIDE_FILE)
.map_err(|error| test_rpc::Error::Service(format!("failed to parse plist: {error}")))?;
let mut vars = plist::Dictionary::new();
-
for (k, v) in env {
// Set environment globally (not for service) to prevent it from being lost on upgrade
std::process::Command::new("launchctl")
@@ -503,10 +550,7 @@ pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(),
plist::Value::Dictionary(vars),
);
- let daemon_plist = std::fs::OpenOptions::new()
- .write(true)
- .truncate(true)
- .open(PLIST_PATH)
+ let daemon_plist = std::fs::File::create(PLIST_OVERRIDE_FILE)
.map_err(|e| test_rpc::Error::Service(format!("failed to open plist: {e}")))?;
parsed_plist
@@ -532,7 +576,7 @@ async fn set_launch_daemon_state(on: bool) -> Result<(), test_rpc::Error> {
.args([
if on { "load" } else { "unload" },
"-w",
- "/Library/LaunchDaemons/net.mullvad.daemon.plist",
+ PLIST_OVERRIDE_FILE,
])
.status()
.await
@@ -541,6 +585,99 @@ async fn set_launch_daemon_state(on: bool) -> Result<(), test_rpc::Error> {
}
#[cfg(target_os = "linux")]
+pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
+ let text = tokio::fs::read_to_string(SYSTEMD_OVERRIDE_FILE)
+ .await
+ .map_err(|err| test_rpc::Error::FileSystem(err.to_string()))?;
+
+ let env: HashMap<String, String> = parse_systemd_env_file(&text)
+ .map(|EnvVar { var, value }| (var, value))
+ .collect();
+ Ok(env)
+}
+
+/// Parse a systemd env-file. `input` is assumed to be the entire text content of a systemd-env file.
+///
+/// Example systemd-env file:
+/// ```
+/// [Service]
+/// Environment="VAR1=pGNqduRFkB4K9C2vijOmUDa2kPtUhArN"
+/// Environment="VAR2=JP8YLOc2bsNlrGuD6LVTq7L36obpjzxd"
+/// ```
+#[cfg(target_os = "linux")]
+fn parse_systemd_env_file(input: &str) -> impl Iterator<Item = EnvVar> + '_ {
+ input
+ .lines()
+ .map(EnvVar::from_systemd_string)
+ .filter_map(|env_var| env_var.ok())
+ .inspect(|env_var| log::trace!("Parsed {env_var:?}"))
+}
+
+#[cfg(target_os = "windows")]
+pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
+ use winreg::{enums::*, RegKey};
+
+ let env =
+ tokio::task::spawn_blocking(|| -> Result<HashMap<String, String>, test_rpc::Error> {
+ let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
+ let key = hklm.open_subkey(MULLVAD_WIN_REGISTRY).map_err(|error| {
+ test_rpc::Error::Registry(format!("Failed to open Mullvad VPN subkey: {}", error))
+ })?;
+
+ // The Strings will be quoted (surrounded by ") when read from the registry - we should trim that!
+ let trim = |string: String| string.trim_matches('"').to_owned();
+ let env = key
+ .open_subkey("Environment")
+ .map_err(|error| {
+ test_rpc::Error::Registry(format!(
+ "Failed to open Environment subkey: {}",
+ error
+ ))
+ })?
+ .enum_values()
+ .filter_map(|x| x.inspect_err(|err| log::trace!("{err}")).ok())
+ .map(|(k, v)| (trim(k), trim(v.to_string())))
+ .collect();
+ Ok(env)
+ })
+ .await
+ .map_err(test_rpc::Error::from_tokio_join_error)??;
+
+ Ok(env)
+}
+
+#[cfg(target_os = "macos")]
+pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
+ let plist = tokio::task::spawn_blocking(|| {
+ let parsed_plist = plist::Value::from_file(PLIST_OVERRIDE_FILE)
+ .map_err(|error| test_rpc::Error::Service(format!("failed to parse plist: {error}")))?;
+
+ Ok::<plist::Value, test_rpc::Error>(parsed_plist)
+ })
+ .await
+ .map_err(test_rpc::Error::from_tokio_join_error)??;
+
+ let plist_tree = plist
+ .as_dictionary()
+ .ok_or_else(|| test_rpc::Error::Service("plist missing dict".to_owned()))?;
+ let Some(env_vars) = plist_tree.get("EnvironmentVariables") else {
+ // `EnvironmentVariables` does not exist in plist file, so there are no env variables to parse.
+ return Ok(HashMap::new());
+ };
+ let env_vars = env_vars.as_dictionary().ok_or_else(|| {
+ test_rpc::Error::Service("`EnvironmentVariables` is not a dict".to_owned())
+ })?;
+
+ let env = env_vars
+ .clone()
+ .into_iter()
+ .filter_map(|(key, value)| Some((key, value.into_string()?)))
+ .collect();
+
+ Ok(env)
+}
+
+#[cfg(target_os = "linux")]
enum ServiceState {
Running,
Inactive,
@@ -618,3 +755,31 @@ pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
Ok(OsVersion::Linux)
}
+
+#[cfg(test)]
+mod test {
+
+ #[cfg(target_os = "linux")]
+ #[test]
+ fn parse_systemd_environment_variables() {
+ use super::parse_systemd_env_file;
+ // Define an example systemd environment file
+ let systemd_file = "
+ [Service]
+ Environment=\"var1=value1\"
+ Environment=\"var2=value2\"
+ ";
+
+ // Parse the "file"
+ let env_vars: Vec<_> = parse_systemd_env_file(systemd_file).collect();
+
+ // Assert that the environment variables it defines are parsed as expected.
+ assert_eq!(env_vars.len(), 2);
+ let first = env_vars.first().unwrap();
+ assert_eq!(first.var, "var1");
+ assert_eq!(first.value, "value1");
+ let second = env_vars.get(1).unwrap();
+ assert_eq!(second.var, "var2");
+ assert_eq!(second.value, "value2");
+ }
+}