diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-06-09 14:54:44 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-06-09 14:54:44 +0200 |
| commit | 87e716c551f563b6bf181bcef87a58bee0fb2599 (patch) | |
| tree | c8506ed39f7b90a732c2214269590d43aa25960f | |
| parent | bcca08c804560b4d2413fdfd3e3df8413330fae9 (diff) | |
| parent | 0859b5d8c0b69f402087ce17b5fc9d178d802d32 (diff) | |
| download | mullvadvpn-87e716c551f563b6bf181bcef87a58bee0fb2599.tar.xz mullvadvpn-87e716c551f563b6bf181bcef87a58bee0fb2599.zip | |
Merge branch 'macos-set-reuseport'
| -rw-r--r-- | Cargo.lock | 4 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 7 | ||||
| -rw-r--r-- | talpid-core/src/resolver.rs | 222 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 4 | ||||
| -rw-r--r-- | talpid-macos/Cargo.toml | 4 | ||||
| -rw-r--r-- | talpid-macos/src/lib.rs | 3 | ||||
| -rw-r--r-- | talpid-macos/src/net.rs | 45 | ||||
| -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 |
15 files changed, 427 insertions, 40 deletions
diff --git a/Cargo.lock b/Cargo.lock index 649fa608ca..4af995fc7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5339,6 +5339,7 @@ dependencies = [ "resolv-conf", "serde", "serde_json", + "socket2 0.5.8", "system-configuration", "talpid-dbus", "talpid-macos", @@ -5357,6 +5358,7 @@ dependencies = [ "tonic-build", "triggered", "tun 0.5.5", + "typed-builder 0.20.1", "which", "widestring", "windows 0.58.0", @@ -5392,8 +5394,10 @@ dependencies = [ name = "talpid-macos" version = "0.0.0" dependencies = [ + "anyhow", "libc", "log", + "tokio", ] [[package]] diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 35518f7ac0..4a11e9125d 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -57,9 +57,10 @@ talpid-platform-metadata = { path = "../talpid-platform-metadata" } pcap = { version = "2.1", features = ["capture-stream"] } pnet_packet = { workspace = true } tun = { workspace = true, features = ["async"] } -nix = { version = "0.28", features = ["socket", "signal"] } +nix = { version = "0.28", features = ["socket", "signal", "user"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +socket2 = { workspace = true } talpid-macos = { path = "../talpid-macos" } talpid-net = { path = "../talpid-net" } @@ -102,10 +103,12 @@ features = [ "Win32_System_SystemInformation", ] +[target.'cfg(target_os = "macos")'.dev-dependencies] +typed-builder = "0.20.0" + [build-dependencies] tonic-build = { workspace = true, default-features = false, features = ["transport", "prost"] } - [dev-dependencies] test-log = "0.2.17" tokio = { workspace = true, features = ["io-util", "test-util", "time"] } diff --git a/talpid-core/src/resolver.rs b/talpid-core/src/resolver.rs index 0f51e5023e..2a69998b2e 100644 --- a/talpid-core/src/resolver.rs +++ b/talpid-core/src/resolver.rs @@ -41,11 +41,11 @@ use hickory_server::{ ServerFuture, }; use rand::random; +use socket2::{Domain, Protocol, Socket, Type}; use std::sync::LazyLock; use talpid_types::drop_guard::{on_drop, OnDrop}; use tokio::{ net::{self, UdpSocket}, - process::Command, task::JoinHandle, }; @@ -91,10 +91,24 @@ const TTL_SECONDS: u32 = 3; /// belongs to the documentation range so should never be reachable. const RESOLVED_ADDR: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 1); +#[derive(Clone, Debug, PartialEq)] +pub struct LocalResolverConfig { + /// Try to bind to a random address in the `127/8` subnet. + pub use_random_loopback: bool, +} + +impl Default for LocalResolverConfig { + fn default() -> Self { + Self { + use_random_loopback: true, + } + } +} + /// Starts a resolver. Returns a cloneable handle, which can activate, deactivate and shut down the /// resolver. When all instances of a handle are dropped, the server will stop. -pub async fn start_resolver() -> Result<ResolverHandle, Error> { - let (resolver, resolver_handle) = LocalResolver::new().await?; +pub async fn start_resolver(config: LocalResolverConfig) -> Result<ResolverHandle, Error> { + let (resolver, resolver_handle) = LocalResolver::new(config).await?; tokio::spawn(resolver.run()); Ok(resolver_handle) } @@ -140,6 +154,12 @@ enum ResolverMessage { /// Channel for the query response response_tx: oneshot::Sender<std::result::Result<Box<dyn LookupObject>, ResolveError>>, }, + + /// Gracefully stop resolver + Stop { + /// Channel for the query response + response_tx: oneshot::Sender<()>, + }, } /// Configuration for [Resolver] @@ -237,7 +257,7 @@ impl Resolver { /// A handle to control a DNS resolver. /// /// When all resolver handles are dropped, the resolver will stop. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ResolverHandle { tx: Arc<mpsc::UnboundedSender<ResolverMessage>>, listening_addr: SocketAddr, @@ -274,16 +294,25 @@ impl ResolverHandle { let _ = response_rx.await; } + + /// Gracefully shut down resolver + pub async fn stop(self) { + let (response_tx, response_rx) = oneshot::channel(); + let _ = self + .tx + .unbounded_send(ResolverMessage::Stop { response_tx }); + let _ = response_rx.await; + } } impl LocalResolver { /// Constructs a new filtering resolver and it's handle. - async fn new() -> Result<(Self, ResolverHandle), Error> { + async fn new(config: LocalResolverConfig) -> Result<(Self, ResolverHandle), Error> { let (command_tx, command_rx) = mpsc::unbounded(); let command_tx = Arc::new(command_tx); let weak_tx = Arc::downgrade(&command_tx); - let (socket, cleanup_ifconfig) = Self::new_random_socket().await?; + let (socket, cleanup_ifconfig) = Self::new_random_socket(&config).await?; let resolver_addr = socket.local_addr().map_err(Error::GetSocketAddr)?; let mut server = Self::new_server(socket, weak_tx.clone())?; @@ -357,11 +386,14 @@ impl LocalResolver { /// /// We do this to try and avoid collisions with other DNS servers running on the same system. /// + /// If [LocalResolverConfig::use_random_loopback] is `false`, we will only try to bind to + /// `127.0.0.1`. + /// /// # Returns /// - The first successfully bound [UdpSocket] /// - An [OnDrop] guard that will delete the IP aliases added, if any. /// If the guard is dropped while the socket is in use, calls to read/write will likely fail. - async fn new_random_socket() -> Result<(UdpSocket, OnDrop), Error> { + async fn new_random_socket(config: &LocalResolverConfig) -> Result<(UdpSocket, OnDrop), Error> { use std::net::Ipv4Addr; let random_loopback = || async move { @@ -370,33 +402,22 @@ impl LocalResolver { // TODO: this command requires root privileges and will thus not work in `cargo test`. // This means that the tests will fall back to 127.0.0.1, and will not assert that the // ifconfig stuff actually works. We probably do want to test this, so what do? - let output = Command::new("ifconfig") - .args([LOOPBACK, "alias", &format!("{addr}"), "up"]) - .output() + talpid_macos::net::add_alias(LOOPBACK, IpAddr::from(addr)) .await .inspect_err(|e| { - log::warn!("Failed to spawn `ifconfig {LOOPBACK} alias {addr} up`: {e}") + log::warn!("Failed to add loopback {LOOPBACK} alias {addr}: {e}"); }) .ok()?; - if !output.status.success() { - log::warn!("Non-zero exit code from ifconfig: {}", output.status); - return None; - } - log::debug!("Created loopback address {addr}"); // Clean up ip address when stopping the resolver let cleanup_ifconfig = on_drop(move || { tokio::task::spawn(async move { log::debug!("Cleaning up loopback address {addr}"); - - let result = Command::new("ifconfig") - .args([LOOPBACK, "delete", &format!("{addr}")]) - .output() - .await; - - if let Err(e) = result { + if let Err(e) = + talpid_macos::net::remove_alias(LOOPBACK, IpAddr::from(addr)).await + { log::warn!("Failed to clean up {LOOPBACK} alias {addr}: {e}"); } }); @@ -408,16 +429,42 @@ impl LocalResolver { for attempt in 0.. { let (socket_addr, on_drop) = match attempt { + ..3 if !config.use_random_loopback => continue, ..3 => match random_loopback().await { Some(random) => random, None => continue, }, + 3 => (Ipv4Addr::LOCALHOST, OnDrop::noop()), 4.. => break, }; - match net::UdpSocket::bind((socket_addr, DNS_PORT)).await { - Ok(socket) => return Ok((socket, on_drop)), + let sock = match Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) { + Ok(sock) => sock, + Err(error) => { + log::error!("Failed to open IPv4/UDP socket: {error}"); + continue; + } + }; + + // SO_NONBLOCK is required for turning this into a tokio socket. + if let Err(error) = sock.set_nonblocking(true) { + log::warn!("Failed to set socket as nonblocking: {error}"); + continue; + } + + // SO_REUSEADDR allows us to bind to `127.x.y.z` even if another socket is bound to + // `0.0.0.0`. This can happen e.g. when macOS "Internet Sharing" is turned on. + if let Err(error) = sock.set_reuse_address(true) { + log::warn!("Failed to set SO_REUSEADDR on resolver socket: {error}"); + } + + match sock.bind(&SocketAddr::from((socket_addr, DNS_PORT)).into()) { + Ok(()) => { + let socket = + net::UdpSocket::from_std(sock.into()).expect("socket is non-blocking"); + return Ok((socket, on_drop)); + } Err(err) => log::warn!("Failed to bind DNS server to {socket_addr}: {err}"), } } @@ -432,6 +479,7 @@ impl LocalResolver { async fn run(mut self) { let abort_handle = self.dns_server_task.abort_handle(); let _abort_dns_server_task = on_drop(|| abort_handle.abort()); + let mut stop_tx = None; while let Some(request) = self.rx.next().await { match request { @@ -451,8 +499,19 @@ impl LocalResolver { } => { self.inner_resolver.resolve(dns_query, response_tx); } + ResolverMessage::Stop { response_tx } => { + stop_tx = Some(response_tx); + break; + } } } + + self.dns_server_task.abort(); + let _ = self.dns_server_task.await; + + if let Some(stop_tx) = stop_tx { + let _ = stop_tx.send(()); + } } /// Update the current DNS config. @@ -632,14 +691,21 @@ mod test { config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, TokioAsyncResolver, }; - use std::{mem, net::UdpSocket, sync::Mutex, thread, time::Duration}; + use std::{net::UdpSocket, sync::Mutex}; + use typed_builder::TypedBuilder; /// Can't have multiple local resolvers running at the same time, as they will try to bind to /// the same address and port. The tests below use this lock to run sequentially. static LOCK: Mutex<()> = Mutex::new(()); async fn start_resolver() -> ResolverHandle { - super::start_resolver().await.unwrap() + // NOTE: We're disabling lo0 aliases + super::start_resolver(LocalResolverConfig { + // Bind resolver to 127.0.0.1 + use_random_loopback: false, + }) + .await + .unwrap() } fn get_test_resolver(addr: SocketAddr) -> hickory_server::resolver::TokioAsyncResolver { @@ -651,6 +717,61 @@ mod test { TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) } + /// Test whether we can successfully bind the socket even if the address is already used to + /// in different scenarios. + /// + /// # Note + /// + /// This test does not test aliases on lo0, as that requires root privileges. + #[test_log::test] + fn test_bind() { + let _mutex = LOCK.lock().unwrap(); + let rt = tokio::runtime::Runtime::new().unwrap(); + + rt.block_on(async move { + // bind() succeeds if wildcard address is bound without REUSEADDR and REUSEPORT + let _sock = bind_sock( + BindParams::builder() + .bind_addr(format!("0.0.0.0:{DNS_PORT}").parse().unwrap()) + .reuse_addr(false) + .reuse_port(false) + .build(), + ) + .unwrap(); + + let handle = start_resolver().await; + let test_resolver = get_test_resolver(handle.listening_addr()); + test_resolver + .lookup(&ALLOWED_DOMAINS[0], RecordType::A) + .await + .expect("lookup should succeed"); + drop(_sock); + handle.stop().await; + + // bind() succeeds if wildcard address is bound with REUSEADDR and REUSEPORT + let _sock = bind_sock( + BindParams::builder() + .bind_addr(format!("0.0.0.0:{DNS_PORT}").parse().unwrap()) + .reuse_addr(true) + .reuse_port(true) + .build(), + ) + .unwrap(); + + let handle = start_resolver().await; + let test_resolver = get_test_resolver(handle.listening_addr()); + test_resolver + .lookup(&ALLOWED_DOMAINS[0], RecordType::A) + .await + .expect("lookup should succeed"); + drop(_sock); + handle.stop().await; + + // bind() should succeeds if 127.0.0.1 is already bound without REUSEADDR and REUSEPORT + // NOTE: We cannot test this as creating an alias requires root privileges. + }); + } + #[test_log::test] fn test_successful_lookup() { let _mutex = LOCK.lock().unwrap(); @@ -688,15 +809,52 @@ mod test { ) } + /// Test that we close the socket when shutting down the local resolver. #[test_log::test] - fn test_shutdown() { + fn test_unbind_socket_on_stop() { let _mutex = LOCK.lock().unwrap(); - let rt = tokio::runtime::Runtime::new().unwrap(); + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); - let handle = rt.block_on(start_resolver()); + let config = LocalResolverConfig { + // Bind resolver to 127.0.0.1 so that we can easily bind to the same address here. + use_random_loopback: false, + }; + let handle = rt.block_on(super::start_resolver(config)).unwrap(); let addr = handle.listening_addr(); - mem::drop(handle); - thread::sleep(Duration::from_millis(300)); + assert_eq!(addr, SocketAddr::from((Ipv4Addr::LOCALHOST, DNS_PORT))); + rt.block_on(handle.stop()); UdpSocket::bind(addr).expect("Failed to bind to a port that should have been removed"); } + + #[derive(TypedBuilder)] + struct BindParams { + bind_addr: SocketAddr, + reuse_addr: bool, + reuse_port: bool, + #[builder(default)] + connect_addr: Option<SocketAddr>, + } + + /// Helper function for creating and binding a UDP socket + fn bind_sock(params: BindParams) -> io::Result<UdpSocket> { + let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; + + let addr = params.bind_addr; + sock.set_reuse_address(params.reuse_addr)?; + sock.set_reuse_port(params.reuse_port)?; + sock.bind(&addr.into())?; + + if let Some(addr) = params.connect_addr { + sock.connect(&addr.into())?; + } + + println!( + "Bound to {} (reuseport: {}, reuseaddr: {})", + params.bind_addr, params.reuse_port, params.reuse_addr + ); + Ok(sock.into()) + } } diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index fc393e466e..207291360c 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -277,7 +277,7 @@ impl TunnelStateMachine { let runtime = tokio::runtime::Handle::current(); #[cfg(target_os = "macos")] - let filtering_resolver = crate::resolver::start_resolver().await?; + let filtering_resolver = crate::resolver::start_resolver(Default::default()).await?; #[cfg(windows)] let split_tunnel = split_tunnel::SplitTunnel::new( @@ -436,6 +436,8 @@ impl TunnelStateMachine { #[cfg(target_os = "macos")] runtime.block_on(self.shared_values.split_tunnel.shutdown()); + #[cfg(target_os = "macos")] + runtime.block_on(self.shared_values.filtering_resolver.stop()); runtime.block_on(self.shared_values.route_manager.stop()); } } diff --git a/talpid-macos/Cargo.toml b/talpid-macos/Cargo.toml index 7b910f5e6d..0b4caa4258 100644 --- a/talpid-macos/Cargo.toml +++ b/talpid-macos/Cargo.toml @@ -11,5 +11,7 @@ rust-version.workspace = true workspace = true [target.'cfg(target_os="macos")'.dependencies] +anyhow.workspace = true +log.workspace = true libc = "0.2.172" -log = { workspace = true } +tokio = { workspace = true, features = ["process"] } diff --git a/talpid-macos/src/lib.rs b/talpid-macos/src/lib.rs index 5a282660d3..1dad718685 100644 --- a/talpid-macos/src/lib.rs +++ b/talpid-macos/src/lib.rs @@ -9,3 +9,6 @@ pub mod process; /// OS bindings generated by 'generate_bindings.rs' #[allow(non_camel_case_types)] mod bindings; + +/// Networking utilities +pub mod net; diff --git a/talpid-macos/src/net.rs b/talpid-macos/src/net.rs new file mode 100644 index 0000000000..5eff2f6878 --- /dev/null +++ b/talpid-macos/src/net.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, bail, Context}; +use std::net::IpAddr; +use tokio::process::Command; + +/// Adds an alias to a network interface. +pub async fn add_alias(interface: &str, addr: IpAddr) -> anyhow::Result<()> { + let context = || anyhow!("Failed to add interface {interface} alias {addr}"); + let output = Command::new("ifconfig") + .args([interface, "alias", &format!("{addr}"), "up"]) + .output() + .await + .context("Failed to spawn ifconfig") + .with_context(context)?; + + if !output.status.success() { + bail!( + "{}: Non-zero exit code from ifconfig: {}", + context(), + output.status + ); + } + + Ok(()) +} + +/// Removes an alias from a network interface. +pub async fn remove_alias(interface: &str, addr: IpAddr) -> anyhow::Result<()> { + let context = || anyhow!("Failed to remove interface {interface} alias {addr}"); + let output = Command::new("ifconfig") + .args([interface, "delete", &format!("{addr}")]) + .output() + .await + .context("Failed to spawn ifconfig") + .with_context(context)?; + + if !output.status.success() { + bail!( + "{}: Non-zero exit code from ifconfig: {}", + context(), + output.status + ); + } + + Ok(()) +} 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 { |
