summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-06-03 18:00:42 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-06-09 14:52:53 +0200
commit0859b5d8c0b69f402087ce17b5fc9d178d802d32 (patch)
treec8506ed39f7b90a732c2214269590d43aa25960f
parenta6f99ee822d4ec40594bb2ce89498526bc0cf453 (diff)
downloadmullvadvpn-0859b5d8c0b69f402087ce17b5fc9d178d802d32.tar.xz
mullvadvpn-0859b5d8c0b69f402087ce17b5fc9d178d802d32.zip
Add ifconfig alias e2e test
Co-Authored-By: David Lönnhager <david.l@mullvad.net>
-rw-r--r--test/Cargo.lock15
-rw-r--r--test/test-manager/src/tests/macos.rs95
-rw-r--r--test/test-manager/src/tests/mod.rs1
-rw-r--r--test/test-manager/test_macro/src/lib.rs2
-rw-r--r--test/test-rpc/src/client.rs20
-rw-r--r--test/test-rpc/src/lib.rs7
-rw-r--r--test/test-runner/Cargo.toml4
-rw-r--r--test/test-runner/src/main.rs34
8 files changed, 174 insertions, 4 deletions
diff --git a/test/Cargo.lock b/test/Cargo.lock
index 1ccf04e2e7..dee10ed1cb 100644
--- a/test/Cargo.lock
+++ b/test/Cargo.lock
@@ -1871,9 +1871,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.171"
+version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
@@ -3632,6 +3632,16 @@ dependencies = [
]
[[package]]
+name = "talpid-macos"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "libc",
+ "log",
+ "tokio",
+]
+
+[[package]]
name = "talpid-platform-metadata"
version = "0.0.0"
dependencies = [
@@ -3816,6 +3826,7 @@ dependencies = [
"serde_json",
"socket2 0.5.8",
"surge-ping",
+ "talpid-macos",
"talpid-platform-metadata",
"talpid-windows",
"tarpc",
diff --git a/test/test-manager/src/tests/macos.rs b/test/test-manager/src/tests/macos.rs
new file mode 100644
index 0000000000..362a9b3c61
--- /dev/null
+++ b/test/test-manager/src/tests/macos.rs
@@ -0,0 +1,95 @@
+//! macOS-specific tests.
+
+use anyhow::{bail, ensure, Context};
+use mullvad_management_interface::MullvadProxyClient;
+use std::net::{Ipv4Addr, SocketAddr};
+use test_macro::test_function;
+use test_rpc::ServiceClient;
+
+use super::TestContext;
+
+/// Test that we can add and remove IP "aliases" to network interfaces.
+///
+/// This is effectively testing that macOS behaves as expected, and that future versions of it
+/// don't break this functionality.
+#[test_function(target_os = "macos")]
+async fn test_ifconfig_add_alias(
+ _: TestContext,
+ rpc: ServiceClient,
+ _: MullvadProxyClient,
+) -> anyhow::Result<()> {
+ let alias = Ipv4Addr::new(127, 123, 123, 123);
+ let interface = "lo0";
+
+ log::info!("Will try to assign alias {alias} to interface {interface}");
+
+ // Sanity-check that alias does not exist before we add it.
+ ensure!(
+ !alias_exists(&rpc, interface, alias).await?,
+ "Alias shouldn't exist before it's created. Was it left over from a previous test?"
+ );
+
+ // Add alias and assert that it exists.
+ rpc.ifconfig_alias_add(interface, alias).await?;
+ ensure!(
+ alias_exists(&rpc, interface, alias).await?,
+ "Alias should have been created!"
+ );
+
+ // Ensure that we clean up the alias after the test, even if it fails
+ let rpc2 = rpc.clone();
+ let _cleanup_guard = scopeguard::guard((), |()| {
+ log::info!("Cleaning up after test_ifconfig_add_alias");
+
+ let Ok(runtime_handle) = tokio::runtime::Handle::try_current() else {
+ log::error!("Missing tokio runtime");
+ return;
+ };
+
+ runtime_handle.spawn(async move {
+ // Ensure that the alias is removed even if the test fails.
+ if let Err(e) = rpc2.ifconfig_alias_remove(interface, alias).await {
+ log::error!("Failed to remove alias {alias} from interface {interface}: {e}");
+ }
+ });
+ });
+
+ // Assert that we can bind to the alias.
+ rpc.send_udp(
+ None,
+ SocketAddr::from((alias, 0)),
+ SocketAddr::from((Ipv4Addr::LOCALHOST, 1234)),
+ )
+ .await
+ .context("Failed to bind to alias")?;
+
+ // Remove alias and assert that it doesn't exist.
+ rpc.ifconfig_alias_remove(interface, alias).await?;
+ ensure!(
+ !alias_exists(&rpc, interface, alias).await?,
+ "Alias should have been removed!"
+ );
+
+ Ok(())
+}
+
+/// Check if an IP alias exists for `interface`.
+async fn alias_exists(
+ rpc: &ServiceClient,
+ interface: &str,
+ alias: Ipv4Addr,
+) -> anyhow::Result<bool> {
+ let alias = alias.to_string();
+ let result = rpc.exec("ifconfig", [interface]).await?;
+
+ let stdout = String::from_utf8(result.stdout)?;
+ let stderr = String::from_utf8(result.stderr)?;
+
+ if result.code != Some(0) {
+ log::error!("ifconfig stdout:\n{stdout}");
+ log::error!("ifconfig stderr:\n{stderr}");
+ bail!("`ifconfig` exited with code {:?}", result.code);
+ }
+
+ Ok(stdout.contains(&alias))
+}
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs
index 6d39c94e51..0858ffd9fa 100644
--- a/test/test-manager/src/tests/mod.rs
+++ b/test/test-manager/src/tests/mod.rs
@@ -6,6 +6,7 @@ mod daita;
mod dns;
mod helpers;
mod install;
+mod macos;
mod relay_ip_overrides;
mod settings;
mod software;
diff --git a/test/test-manager/test_macro/src/lib.rs b/test/test-manager/test_macro/src/lib.rs
index 048bb1975e..5f5af2c4da 100644
--- a/test/test-manager/test_macro/src/lib.rs
+++ b/test/test-manager/test_macro/src/lib.rs
@@ -154,7 +154,7 @@ fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
let wrapper_closure = quote! {
|test_context: crate::tests::TestContext,
rpc: test_rpc::ServiceClient,
- mullvad_client: Option<MullvadProxyClient>|
+ mullvad_client: Option<::mullvad_management_interface::MullvadProxyClient>|
{
let mullvad_client = mullvad_client.expect("Test functions defined using the macro should be given a mullvad client");
Box::pin(async move {
diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs
index e1a8bc5ef9..ad3d39dce9 100644
--- a/test/test-rpc/src/client.rs
+++ b/test/test-rpc/src/client.rs
@@ -420,4 +420,24 @@ impl ServiceClient {
.get_os_version(tarpc::context::current())
.await?
}
+
+ pub async fn ifconfig_alias_add(
+ &self,
+ interface: impl Into<String>,
+ alias: impl Into<IpAddr>,
+ ) -> Result<(), Error> {
+ self.client
+ .ifconfig_alias_add(tarpc::context::current(), interface.into(), alias.into())
+ .await?
+ }
+
+ pub async fn ifconfig_alias_remove(
+ &self,
+ interface: impl Into<String>,
+ alias: impl Into<IpAddr>,
+ ) -> Result<(), Error> {
+ self.client
+ .ifconfig_alias_remove(tarpc::context::current(), interface.into(), alias.into())
+ .await?
+ }
}
diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs
index a23eb84266..d92e6ff3af 100644
--- a/test/test-rpc/src/lib.rs
+++ b/test/test-rpc/src/lib.rs
@@ -71,6 +71,8 @@ pub enum Error {
UnknownPid(u32),
#[error("Failed to join tokio task: {0}")]
TokioJoinError(String),
+ #[error("gRPC command is not implemented for this target")]
+ TargetNotImplemented,
#[error("{0}")]
Other(String),
}
@@ -275,6 +277,11 @@ mod service {
/// Returns operating system details
async fn get_os_version() -> Result<meta::OsVersion, Error>;
+
+ /// Create an IP alias for the provided interface. (macOS only)
+ async fn ifconfig_alias_add(interface: String, alias: IpAddr) -> Result<(), Error>;
+ /// Remove an IP alias for the provided interface. (macOS only)
+ async fn ifconfig_alias_remove(interface: String, alias: IpAddr) -> Result<(), Error>;
}
}
diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml
index af84ef4dae..ad01aecbc8 100644
--- a/test/test-runner/Cargo.toml
+++ b/test/test-runner/Cargo.toml
@@ -35,7 +35,8 @@ talpid-platform-metadata = { path = "../../talpid-platform-metadata", default-fe
socket2 = { workspace = true, features = ["all"] }
-[target."cfg(target_os=\"windows\")".dependencies]
+
+[target.'cfg(windows)'.dependencies]
talpid-windows = { path = "../../talpid-windows" }
windows-service = "0.6"
@@ -64,4 +65,5 @@ nix = { workspace = true, features = ["user"] }
rs-release = "0.1.7"
[target.'cfg(target_os = "macos")'.dependencies]
+talpid-macos = { path = "../../talpid-macos" }
plist = "1"
diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs
index a107f29f3c..a7e1a26515 100644
--- a/test/test-runner/src/main.rs
+++ b/test/test-runner/src/main.rs
@@ -576,6 +576,40 @@ impl Service for TestServer {
async fn get_os_version(self, _: context::Context) -> Result<OsVersion, test_rpc::Error> {
sys::get_os_version()
}
+
+ #[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
+ async fn ifconfig_alias_add(
+ self,
+ _: context::Context,
+ interface: String,
+ alias: IpAddr,
+ ) -> Result<(), test_rpc::Error> {
+ #[cfg(not(target_os = "macos"))]
+ return Err(test_rpc::Error::TargetNotImplemented);
+
+ #[cfg(target_os = "macos")]
+ talpid_macos::net::add_alias(&interface, alias)
+ .await
+ .map_err(|e| format!("{e:#}"))
+ .map_err(test_rpc::Error::Other)
+ }
+
+ #[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
+ async fn ifconfig_alias_remove(
+ self,
+ _: context::Context,
+ interface: String,
+ alias: IpAddr,
+ ) -> Result<(), test_rpc::Error> {
+ #[cfg(not(target_os = "macos"))]
+ return Err(test_rpc::Error::TargetNotImplemented);
+
+ #[cfg(target_os = "macos")]
+ talpid_macos::net::remove_alias(&interface, alias)
+ .await
+ .map_err(|e| format!("{e:#}"))
+ .map_err(test_rpc::Error::Other)
+ }
}
fn get_pipe_status() -> ServiceStatus {