diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-06-03 18:00:42 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-06-09 14:52:53 +0200 |
| commit | 0859b5d8c0b69f402087ce17b5fc9d178d802d32 (patch) | |
| tree | c8506ed39f7b90a732c2214269590d43aa25960f | |
| parent | a6f99ee822d4ec40594bb2ce89498526bc0cf453 (diff) | |
| download | mullvadvpn-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.lock | 15 | ||||
| -rw-r--r-- | test/test-manager/src/tests/macos.rs | 95 | ||||
| -rw-r--r-- | test/test-manager/src/tests/mod.rs | 1 | ||||
| -rw-r--r-- | test/test-manager/test_macro/src/lib.rs | 2 | ||||
| -rw-r--r-- | test/test-rpc/src/client.rs | 20 | ||||
| -rw-r--r-- | test/test-rpc/src/lib.rs | 7 | ||||
| -rw-r--r-- | test/test-runner/Cargo.toml | 4 | ||||
| -rw-r--r-- | test/test-runner/src/main.rs | 34 |
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 { |
