diff --git a/.github/workflows/desktop-e2e.yml b/.github/workflows/desktop-e2e.yml index 511a450cc7a3..cd10128bc8e3 100644 --- a/.github/workflows/desktop-e2e.yml +++ b/.github/workflows/desktop-e2e.yml @@ -70,11 +70,10 @@ jobs: path: ~/.cache/mullvad-test/packages - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-tags: true - name: Run end-to-end tests shell: bash -ieo pipefail {0} run: | + git fetch --tags --force ./test/ci-runtests.sh ${{ matrix.os }} - uses: actions/upload-artifact@v3 if: '!cancelled()' @@ -140,11 +139,10 @@ jobs: path: ~/.cache/mullvad-test/packages - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-tags: true - name: Run end-to-end tests shell: bash -ieo pipefail {0} run: | + git fetch --tags --force ./test/ci-runtests.sh ${{ matrix.os }} - uses: actions/upload-artifact@v3 if: '!cancelled()' @@ -154,36 +152,30 @@ jobs: build-macos: if: ${{ !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main' }} - runs-on: macos-latest + runs-on: [self-hosted, desktop-test, macOS] # app-test-macos-arm steps: - name: Checkout repository uses: actions/checkout@v2 - name: Checkout submodules run: git submodule update --init --depth=1 + - name: Install Go + uses: actions/setup-go@v3 + with: + go-version: 1.18.5 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v3 with: token: ${{ secrets.GITHUB_TOKEN }} node-version: 18 - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: stable - target: aarch64-apple-darwin - default: true - - name: Install Go - uses: actions/setup-go@v3 - with: - go-version: 1.18.5 + cache: 'npm' + cache-dependency-path: gui/package-lock.json - name: Build app - run: ./build.sh --universal + run: ./build.sh - name: Build test executable - run: ./gui/scripts/build-test-executable.sh aarch64-apple-darwin - # FIXME: This fails for some reason, but the artifact is built - continue-on-error: true + run: ./gui/scripts/build-test-executable.sh - uses: actions/upload-artifact@v3 if: '!cancelled()' with: @@ -210,11 +202,10 @@ jobs: path: ~/Library/Caches/mullvad-test/packages - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-tags: true - name: Run end-to-end tests shell: bash -ieo pipefail {0} run: | + git fetch --tags --force ./test/ci-runtests.sh ${{ matrix.os }} - uses: actions/upload-artifact@v3 if: '!cancelled()' diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index b8a21dda075b..a540791838a5 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -1899,9 +1899,15 @@ where .await { Ok(settings_changed) => { - Self::oneshot_send(tx, Ok(()), "set_allow_lan response"); if settings_changed { - self.send_tunnel_command(TunnelCommand::AllowLan(allow_lan)); + self.send_tunnel_command(TunnelCommand::AllowLan( + allow_lan, + oneshot_map(tx, |tx, ()| { + Self::oneshot_send(tx, Ok(()), "set_allow_lan response"); + }), + )); + } else { + Self::oneshot_send(tx, Ok(()), "set_allow_lan response"); } } Err(e) => { @@ -1946,11 +1952,15 @@ where .await { Ok(settings_changed) => { - Self::oneshot_send(tx, Ok(()), "set_block_when_disconnected response"); if settings_changed { self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected( block_when_disconnected, + oneshot_map(tx, |tx, ()| { + Self::oneshot_send(tx, Ok(()), "set_block_when_disconnected response"); + }), )); + } else { + Self::oneshot_send(tx, Ok(()), "set_block_when_disconnected response"); } } Err(e) => { @@ -2148,12 +2158,18 @@ where .await { Ok(settings_changed) => { - Self::oneshot_send(tx, Ok(()), "set_dns_options response"); if settings_changed { let settings = self.settings.to_settings(); let resolvers = dns::addresses_from_options(&settings.tunnel_options.dns_options); - self.send_tunnel_command(TunnelCommand::Dns(resolvers)); + self.send_tunnel_command(TunnelCommand::Dns( + resolvers, + oneshot_map(tx, |tx, ()| { + Self::oneshot_send(tx, Ok(()), "set_dns_options response"); + }), + )); + } else { + Self::oneshot_send(tx, Ok(()), "set_dns_options response"); } } Err(e) => { @@ -2396,7 +2412,8 @@ where && (*self.target_state == TargetState::Secured || self.settings.auto_connect) { log::debug!("Blocking firewall during shutdown since system is going down"); - self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true)); + let (tx, _rx) = oneshot::channel(); + self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true, tx)); } self.state.shutdown(&self.tunnel_state); @@ -2408,7 +2425,8 @@ where // without causing the service to be restarted. if *self.target_state == TargetState::Secured { - self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true)); + let (tx, _rx) = oneshot::channel(); + self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true, tx)); } self.target_state.lock(); } @@ -2569,3 +2587,19 @@ fn new_selector_config(settings: &Settings) -> SelectorConfig { relay_overrides: settings.relay_overrides.clone(), } } + +/// Consume a oneshot sender of `T1` and return a sender that takes a different type `T2`. `forwarder` should map `T1` back to `T2` and +/// send the result back to the original receiver. +fn oneshot_map( + tx: oneshot::Sender, + forwarder: impl Fn(oneshot::Sender, T2) + Send + 'static, +) -> oneshot::Sender { + let (new_tx, new_rx) = oneshot::channel(); + tokio::spawn(async move { + match new_rx.await { + Ok(result) => forwarder(tx, result), + Err(oneshot::Canceled) => (), + } + }); + new_tx +} diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index c1b27fea6561..64ee088d1877 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -73,6 +73,10 @@ impl MullvadProxyClient { super::new_rpc_client().await.map(Self) } + pub fn from_rpc_client(client: crate::ManagementServiceClient) -> Self { + Self(client) + } + pub async fn connect_tunnel(&mut self) -> Result { Ok(self .0 diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 687e941aa729..21375f056ba2 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -213,8 +213,8 @@ impl ConnectedState { use self::EventConsequence::*; match command { - Some(TunnelCommand::AllowLan(allow_lan)) => { - if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { + let consequence = if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) } else { match self.set_firewall_policy(shared_values) { @@ -230,43 +230,55 @@ impl ConnectedState { AfterDisconnect::Block(ErrorStateCause::SetFirewallPolicyError(error)), ), } - } + }; + let _ = complete_tx.send(()); + consequence } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { shared_values.allowed_endpoint = endpoint; let _ = tx.send(()); SameState(self) } - Some(TunnelCommand::Dns(servers)) => match shared_values.set_dns_servers(servers) { - Ok(true) => { - if let Err(error) = self.set_firewall_policy(shared_values) { - return self.disconnect( - shared_values, - AfterDisconnect::Block(ErrorStateCause::SetFirewallPolicyError(error)), - ); - } - - match self.set_dns(shared_values) { - #[cfg(target_os = "android")] - Ok(()) => self.disconnect(shared_values, AfterDisconnect::Reconnect(0)), - #[cfg(not(target_os = "android"))] - Ok(()) => SameState(self), - Err(error) => { - log::error!("{}", error.display_chain_with_msg("Failed to set DNS")); - self.disconnect( + Some(TunnelCommand::Dns(servers, complete_tx)) => { + let consequence = match shared_values.set_dns_servers(servers) { + Ok(true) => { + if let Err(error) = self.set_firewall_policy(shared_values) { + return self.disconnect( shared_values, - AfterDisconnect::Block(ErrorStateCause::SetDnsError), - ) + AfterDisconnect::Block(ErrorStateCause::SetFirewallPolicyError( + error, + )), + ); + } + + match self.set_dns(shared_values) { + #[cfg(target_os = "android")] + Ok(()) => self.disconnect(shared_values, AfterDisconnect::Reconnect(0)), + #[cfg(not(target_os = "android"))] + Ok(()) => SameState(self), + Err(error) => { + log::error!( + "{}", + error.display_chain_with_msg("Failed to set DNS") + ); + self.disconnect( + shared_values, + AfterDisconnect::Block(ErrorStateCause::SetDnsError), + ) + } } } - } - Ok(false) => SameState(self), - Err(error_cause) => { - self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) - } - }, - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Ok(false) => SameState(self), + Err(error_cause) => { + self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) + } + }; + let _ = complete_tx.send(()); + consequence + } + Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected, complete_tx)) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); SameState(self) } Some(TunnelCommand::IsOffline(is_offline)) => { diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 2a728513ff88..d7d93da9d43e 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -392,12 +392,14 @@ impl ConnectingState { use self::EventConsequence::*; match command { - Some(TunnelCommand::AllowLan(allow_lan)) => { - if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { + let consequence = if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) } else { self.reset_firewall(shared_values) - } + }; + let _ = complete_tx.send(()); + consequence } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.allowed_endpoint != endpoint { @@ -418,14 +420,19 @@ impl ConnectingState { let _ = tx.send(()); SameState(self) } - Some(TunnelCommand::Dns(servers)) => match shared_values.set_dns_servers(servers) { - #[cfg(target_os = "android")] - Ok(true) => self.disconnect(shared_values, AfterDisconnect::Reconnect(0)), - Ok(_) => SameState(self), - Err(cause) => self.disconnect(shared_values, AfterDisconnect::Block(cause)), - }, - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::Dns(servers, complete_tx)) => { + let consequence = match shared_values.set_dns_servers(servers) { + #[cfg(target_os = "android")] + Ok(true) => self.disconnect(shared_values, AfterDisconnect::Reconnect(0)), + Ok(_) => SameState(self), + Err(cause) => self.disconnect(shared_values, AfterDisconnect::Block(cause)), + }; + let _ = complete_tx.send(()); + consequence + } + Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected, complete_tx)) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); SameState(self) } Some(TunnelCommand::IsOffline(is_offline)) => { diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index 5a2cf6fc4d2f..d46f06e782f7 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -128,7 +128,7 @@ impl TunnelState for DisconnectedState { use self::EventConsequence::*; match runtime.block_on(commands.next()) { - Some(TunnelCommand::AllowLan(allow_lan)) => { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { if shared_values.allow_lan != allow_lan { // The only platform that can fail is Android, but Android doesn't support the // "block when disconnected" option, so the following call never fails. @@ -138,6 +138,7 @@ impl TunnelState for DisconnectedState { Self::set_firewall_policy(shared_values, false); } + let _ = complete_tx.send(()); SameState(self) } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { @@ -148,15 +149,15 @@ impl TunnelState for DisconnectedState { let _ = tx.send(()); SameState(self) } - Some(TunnelCommand::Dns(servers)) => { + Some(TunnelCommand::Dns(servers, complete_tx)) => { // Same situation as allow LAN above. shared_values .set_dns_servers(servers) .expect("Failed to reconnect after changing custom DNS servers"); - + let _ = complete_tx.send(()); SameState(self) } - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected, complete_tx)) => { if shared_values.block_when_disconnected != block_when_disconnected { shared_values.block_when_disconnected = block_when_disconnected; Self::set_firewall_policy(shared_values, true); @@ -178,6 +179,7 @@ impl TunnelState for DisconnectedState { Self::reset_dns(shared_values); } } + let _ = complete_tx.send(()); SameState(self) } Some(TunnelCommand::IsOffline(is_offline)) => { diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index 08248fbac2d8..185d2f7d0afc 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -40,8 +40,9 @@ impl DisconnectingState { self.after_disconnect = match after_disconnect { AfterDisconnect::Nothing => match command { - Some(TunnelCommand::AllowLan(allow_lan)) => { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { let _ = shared_values.set_allow_lan(allow_lan); + let _ = complete_tx.send(()); AfterDisconnect::Nothing } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { @@ -49,12 +50,17 @@ impl DisconnectingState { let _ = tx.send(()); AfterDisconnect::Nothing } - Some(TunnelCommand::Dns(servers)) => { + Some(TunnelCommand::Dns(servers, complete_tx)) => { let _ = shared_values.set_dns_servers(servers); + let _ = complete_tx.send(()); AfterDisconnect::Nothing } - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::BlockWhenDisconnected( + block_when_disconnected, + complete_tx, + )) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); AfterDisconnect::Nothing } Some(TunnelCommand::IsOffline(is_offline)) => { @@ -76,8 +82,9 @@ impl DisconnectingState { } }, AfterDisconnect::Block(reason) => match command { - Some(TunnelCommand::AllowLan(allow_lan)) => { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { let _ = shared_values.set_allow_lan(allow_lan); + let _ = complete_tx.send(()); AfterDisconnect::Block(reason) } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { @@ -85,12 +92,17 @@ impl DisconnectingState { let _ = tx.send(()); AfterDisconnect::Block(reason) } - Some(TunnelCommand::Dns(servers)) => { + Some(TunnelCommand::Dns(servers, complete_tx)) => { let _ = shared_values.set_dns_servers(servers); + let _ = complete_tx.send(()); AfterDisconnect::Block(reason) } - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::BlockWhenDisconnected( + block_when_disconnected, + complete_tx, + )) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); AfterDisconnect::Block(reason) } Some(TunnelCommand::IsOffline(is_offline)) => { @@ -117,8 +129,9 @@ impl DisconnectingState { None => AfterDisconnect::Block(reason), }, AfterDisconnect::Reconnect(retry_attempt) => match command { - Some(TunnelCommand::AllowLan(allow_lan)) => { + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { let _ = shared_values.set_allow_lan(allow_lan); + let _ = complete_tx.send(()); AfterDisconnect::Reconnect(retry_attempt) } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { @@ -126,12 +139,17 @@ impl DisconnectingState { let _ = tx.send(()); AfterDisconnect::Reconnect(retry_attempt) } - Some(TunnelCommand::Dns(servers)) => { + Some(TunnelCommand::Dns(servers, complete_tx)) => { let _ = shared_values.set_dns_servers(servers); + let _ = complete_tx.send(()); AfterDisconnect::Reconnect(retry_attempt) } - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::BlockWhenDisconnected( + block_when_disconnected, + complete_tx, + )) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); AfterDisconnect::Reconnect(retry_attempt) } Some(TunnelCommand::IsOffline(is_offline)) => { diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index 11a805f7dc0f..2f82cb4cf567 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -138,13 +138,16 @@ impl TunnelState for ErrorState { use self::EventConsequence::*; match runtime.block_on(commands.next()) { - Some(TunnelCommand::AllowLan(allow_lan)) => { - if let Err(error_state_cause) = shared_values.set_allow_lan(allow_lan) { - NewState(Self::enter(shared_values, error_state_cause)) - } else { - let _ = Self::set_firewall_policy(shared_values); - SameState(self) - } + Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { + let consequence = + if let Err(error_state_cause) = shared_values.set_allow_lan(allow_lan) { + NewState(Self::enter(shared_values, error_state_cause)) + } else { + let _ = Self::set_firewall_policy(shared_values); + SameState(self) + }; + let _ = complete_tx.send(()); + consequence } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.allowed_endpoint != endpoint { @@ -163,15 +166,19 @@ impl TunnelState for ErrorState { let _ = tx.send(()); SameState(self) } - Some(TunnelCommand::Dns(servers)) => { - if let Err(error_state_cause) = shared_values.set_dns_servers(servers) { - NewState(Self::enter(shared_values, error_state_cause)) - } else { - SameState(self) - } + Some(TunnelCommand::Dns(servers, complete_tx)) => { + let consequence = + if let Err(error_state_cause) = shared_values.set_dns_servers(servers) { + NewState(Self::enter(shared_values, error_state_cause)) + } else { + SameState(self) + }; + let _ = complete_tx.send(()); + consequence } - Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { + Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected, complete_tx)) => { shared_values.block_when_disconnected = block_when_disconnected; + let _ = complete_tx.send(()); SameState(self) } Some(TunnelCommand::IsOffline(is_offline)) => { diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 12bc4cfc86fc..5957b2f73152 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -189,15 +189,15 @@ pub async fn spawn( /// Representation of external commands for the tunnel state machine. pub enum TunnelCommand { /// Enable or disable LAN access in the firewall. - AllowLan(bool), + AllowLan(bool, oneshot::Sender<()>), /// Endpoint that should never be blocked. `()` is sent to the /// channel after attempting to set the firewall policy, regardless /// of whether it succeeded. AllowEndpoint(AllowedEndpoint, oneshot::Sender<()>), /// Set DNS servers to use. - Dns(Option>), + Dns(Option>, oneshot::Sender<()>), /// Enable or disable the block_when_disconnected feature. - BlockWhenDisconnected(bool), + BlockWhenDisconnected(bool, oneshot::Sender<()>), /// Notify the state machine of the connectivity of the device. IsOffline(bool), /// Open tunnel connection. diff --git a/test/test-manager/src/tests/account.rs b/test/test-manager/src/tests/account.rs index 5b1991abb5e0..92c95346b286 100644 --- a/test/test-manager/src/tests/account.rs +++ b/test/test-manager/src/tests/account.rs @@ -107,12 +107,13 @@ pub async fn test_too_many_devices( ) .await .unwrap(); - assert!(ui_result.success()); if let Err(error) = clear_devices(&device_client).await { log::error!("Failed to clear devices: {error}"); } + assert!(ui_result.success()); + Ok(()) } diff --git a/test/test-manager/src/tests/dns.rs b/test/test-manager/src/tests/dns.rs index 310d5eb0dc0b..89755c3fd633 100644 --- a/test/test-manager/src/tests/dns.rs +++ b/test/test-manager/src/tests/dns.rs @@ -9,9 +9,12 @@ use mullvad_management_interface::{types, ManagementServiceClient}; use mullvad_types::{relay_constraints::RelaySettings, ConnectionConfig, CustomTunnelEndpoint}; use talpid_types::net::wireguard; use test_macro::test_function; -use test_rpc::{Interface, ServiceClient}; +use test_rpc::ServiceClient; -use super::{helpers::connect_and_wait, Error, TestContext}; +use super::{ + helpers::{self, connect_and_wait, set_relay_settings}, + Error, TestContext, +}; use crate::network_monitor::{ start_packet_monitor_until, start_tunnel_packet_monitor_until, Direction, IpHeaderProtocols, MonitorOptions, @@ -22,8 +25,6 @@ use crate::vm::network::{ CUSTOM_TUN_REMOTE_TUN_ADDR, NON_TUN_GATEWAY, }; -use super::helpers::set_relay_settings; - /// How long to wait for expected "DNS queries" to appear const MONITOR_TIMEOUT: Duration = Duration::from_secs(5); @@ -47,7 +48,7 @@ pub async fn test_dns_leak_default( leak_test_dns( &rpc, &mut mullvad_client, - Interface::Tunnel, + true, IpAddr::V4(CUSTOM_TUN_REMOTE_TUN_ADDR), ) .await @@ -82,7 +83,7 @@ pub async fn test_dns_leak_custom_public_ip( .await .expect("failed to configure DNS server"); - leak_test_dns(&rpc, &mut mullvad_client, Interface::Tunnel, CONFIG_IP).await + leak_test_dns(&rpc, &mut mullvad_client, true, CONFIG_IP).await } /// Test whether DNS leaks can be produced when using a custom private IP. This test succeeds if and @@ -114,7 +115,7 @@ pub async fn test_dns_leak_custom_private_ip( .await .expect("failed to configure DNS server"); - leak_test_dns(&rpc, &mut mullvad_client, Interface::NonTunnel, CONFIG_IP).await + leak_test_dns(&rpc, &mut mullvad_client, false, CONFIG_IP).await } /// See whether it is possible to send "DNS queries" to a particular whitelisted destination on @@ -124,11 +125,9 @@ pub async fn test_dns_leak_custom_private_ip( async fn leak_test_dns( rpc: &ServiceClient, mullvad_client: &mut ManagementServiceClient, - interface: Interface, + use_tun: bool, whitelisted_dest: IpAddr, ) -> Result<(), Error> { - let use_tun = interface == Interface::Tunnel; - // // Connect to local wireguard relay // @@ -137,24 +136,32 @@ async fn leak_test_dns( .await .expect("failed to connect to custom wg relay"); - let guest_ip = rpc - .get_interface_ip(Interface::NonTunnel) + let nontun_iface = rpc + .get_default_interface() + .await + .expect("failed to find non-tun interface"); + let tunnel_iface = helpers::get_tunnel_interface(mullvad_client.clone()) + .await + .expect("failed to find tunnel interface"); + + let nontun_ip = rpc + .get_interface_ip(nontun_iface.clone()) .await .expect("failed to obtain guest IP"); let tunnel_ip = rpc - .get_interface_ip(Interface::Tunnel) + .get_interface_ip(tunnel_iface.clone()) .await .expect("failed to obtain tunnel IP"); log::debug!("Tunnel (guest) IP: {tunnel_ip}"); - log::debug!("Non-tunnel (guest) IP: {guest_ip}"); + log::debug!("Non-tunnel (guest) IP: {nontun_ip}"); // // Spoof DNS packets // let tun_bind_addr = SocketAddr::new(tunnel_ip, 0); - let guest_bind_addr = SocketAddr::new(guest_ip, 0); + let nontun_bind_addr = SocketAddr::new(nontun_ip, 0); let whitelisted_dest = SocketAddr::new(whitelisted_dest, 53); let blocked_dest_local = "10.64.100.100:53".parse().unwrap(); @@ -216,40 +223,35 @@ async fn leak_test_dns( // send to allowed dest spoof_packets( &rpc, - Some(Interface::Tunnel), + Some(tunnel_iface.clone()), tun_bind_addr, whitelisted_dest, ), spoof_packets( &rpc, - Some(Interface::NonTunnel), - guest_bind_addr, + Some(nontun_iface.clone()), + nontun_bind_addr, whitelisted_dest, ), // send to blocked local dest spoof_packets( &rpc, - Some(Interface::Tunnel), + Some(tunnel_iface.clone()), tun_bind_addr, blocked_dest_local, ), spoof_packets( &rpc, - Some(Interface::NonTunnel), - guest_bind_addr, + Some(nontun_iface.clone()), + nontun_bind_addr, blocked_dest_local, ), // send to blocked public dest + spoof_packets(&rpc, Some(tunnel_iface), tun_bind_addr, blocked_dest_public,), spoof_packets( &rpc, - Some(Interface::Tunnel), - tun_bind_addr, - blocked_dest_public, - ), - spoof_packets( - &rpc, - Some(Interface::NonTunnel), - guest_bind_addr, + Some(nontun_iface), + nontun_bind_addr, blocked_dest_public, ), ) @@ -578,17 +580,25 @@ async fn run_dns_config_test< } } - let guest_ip = rpc - .get_interface_ip(Interface::NonTunnel) + let nontun_iface = rpc + .get_default_interface() + .await + .expect("failed to find non-tun interface"); + let tunnel_iface = helpers::get_tunnel_interface(mullvad_client.clone()) + .await + .expect("failed to find tunnel interface"); + + let nontun_ip = rpc + .get_interface_ip(nontun_iface) .await .expect("failed to obtain guest IP"); let tunnel_ip = rpc - .get_interface_ip(Interface::Tunnel) + .get_interface_ip(tunnel_iface) .await .expect("failed to obtain tunnel IP"); log::debug!("Tunnel (guest) IP: {tunnel_ip}"); - log::debug!("Non-tunnel (guest) IP: {guest_ip}"); + log::debug!("Non-tunnel (guest) IP: {nontun_ip}"); let monitor = create_monitor().await; @@ -661,19 +671,21 @@ async fn connect_local_wg_relay(mullvad_client: &mut ManagementServiceClient) -> async fn spoof_packets( rpc: &ServiceClient, - interface: Option, + interface: Option, bind_addr: SocketAddr, dest: SocketAddr, ) { let tcp_rpc = rpc.clone(); + let tcp_interface = interface.clone(); let tcp_send = async move { log::debug!("sending to {}/tcp from {}", dest, bind_addr); - let _ = tcp_rpc.send_tcp(interface, bind_addr, dest).await; + let _ = tcp_rpc.send_tcp(tcp_interface, bind_addr, dest).await; }; let udp_rpc = rpc.clone(); + let udp_interface = interface.clone(); let udp_send = async move { log::debug!("sending to {}/udp from {}", dest, bind_addr); - let _ = udp_rpc.send_udp(interface, bind_addr, dest).await; + let _ = udp_rpc.send_udp(udp_interface, bind_addr, dest).await; }; let _ = tokio::join!(tcp_send, udp_send); } diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index ea876bbbbe5b..5b1c0a990379 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -1,7 +1,7 @@ use super::{config::TEST_CONFIG, Error, PING_TIMEOUT, WAIT_FOR_TUNNEL_STATE_TIMEOUT}; use crate::network_monitor::{start_packet_monitor, MonitorOptions}; use futures::StreamExt; -use mullvad_management_interface::{types, ManagementServiceClient}; +use mullvad_management_interface::{types, ManagementServiceClient, MullvadProxyClient}; use mullvad_types::{ location::Location, relay_constraints::{ @@ -19,7 +19,7 @@ use std::{ time::Duration, }; use talpid_types::net::wireguard::{PeerConfig, PrivateKey, TunnelConfig}; -use test_rpc::{package::Package, AmIMullvad, Interface, ServiceClient}; +use test_rpc::{package::Package, AmIMullvad, ServiceClient}; use tokio::time::timeout; #[macro_export] @@ -82,17 +82,30 @@ pub async fn using_mullvad_exit(rpc: &ServiceClient) -> bool { .mullvad_exit_ip } +/// Get VPN tunnel interface name +pub async fn get_tunnel_interface(rpc: ManagementServiceClient) -> Option { + let mut client = MullvadProxyClient::from_rpc_client(rpc); + match client.get_tunnel_state().await.ok()? { + TunnelState::Connecting { endpoint, .. } | TunnelState::Connected { endpoint, .. } => { + endpoint.tunnel_interface + } + _ => None, + } +} + /// Sends a number of probes and returns the number of observed packets (UDP, TCP, or ICMP) pub async fn send_guest_probes( rpc: ServiceClient, - interface: Option, + interface: String, destination: SocketAddr, ) -> Result { + const MONITOR_DURATION: Duration = Duration::from_secs(8); + let pktmon = start_packet_monitor( move |packet| packet.destination.ip() == destination.ip(), MonitorOptions { direction: Some(crate::network_monitor::Direction::In), - timeout: Some(Duration::from_secs(3)), + timeout: Some(MONITOR_DURATION), ..Default::default() }, ) @@ -100,7 +113,7 @@ pub async fn send_guest_probes( let send_handle = tokio::spawn(send_guest_probes_without_monitor( rpc, - interface, + Some(interface), destination, )); @@ -132,12 +145,12 @@ pub async fn send_guest_probes( /// Send one probe per transport protocol to `destination` without running a packet monitor pub async fn send_guest_probes_without_monitor( rpc: ServiceClient, - interface: Option, + interface: Option, destination: SocketAddr, ) { - let bind_addr = if let Some(interface) = interface { + let bind_addr = if let Some(ref interface) = interface { SocketAddr::new( - rpc.get_interface_ip(interface) + rpc.get_interface_ip(interface.clone()) .await .expect("failed to obtain interface IP"), 0, @@ -147,9 +160,19 @@ pub async fn send_guest_probes_without_monitor( }; let tcp_rpc = rpc.clone(); - let tcp_send = async move { tcp_rpc.send_tcp(interface, bind_addr, destination).await }; + let tcp_interface = interface.clone(); + let tcp_send = async move { + tcp_rpc + .send_tcp(tcp_interface, bind_addr, destination) + .await + }; let udp_rpc = rpc.clone(); - let udp_send = async move { udp_rpc.send_udp(interface, bind_addr, destination).await }; + let udp_interface = interface.clone(); + let udp_send = async move { + udp_rpc + .send_udp(udp_interface, bind_addr, destination) + .await + }; let icmp = async move { ping_with_timeout(&rpc, destination.ip(), interface).await }; let _ = tokio::join!(tcp_send, udp_send, icmp); } @@ -157,7 +180,7 @@ pub async fn send_guest_probes_without_monitor( pub async fn ping_with_timeout( rpc: &ServiceClient, dest: IpAddr, - interface: Option, + interface: Option, ) -> Result<(), Error> { timeout(PING_TIMEOUT, rpc.send_ping(interface, dest)) .await @@ -449,30 +472,6 @@ pub fn unreachable_wireguard_tunnel() -> talpid_types::net::wireguard::Connectio } } -/// Randomly select an entry and exit node from the daemon's relay list. -/// The exit node is distinct from the entry node. -/// -/// * `mullvad_client` - An interface to the Mullvad daemon. -/// * `critera` - A function used to determine which relays to include in random selection. -pub async fn random_entry_and_exit( - mullvad_client: &mut ManagementServiceClient, - criteria: Filter, -) -> Result<(Relay, Relay), Error> -where - Filter: Fn(&Relay) -> bool, -{ - use itertools::Itertools; - // Pluck the first 2 relays and return them as a tuple. - // This will fail if there are less than 2 relays in the relay list. - filter_relays(mullvad_client, criteria) - .await? - .into_iter() - .next_tuple() - .ok_or(Error::Other( - "failed to randomly select two relays from daemon's relay list".to_string(), - )) -} - /// Return a filtered version of the daemon's relay list. /// /// * `mullvad_client` - An interface to the Mullvad daemon. diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index 04d401a22d96..37a89f5fc83c 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -11,7 +11,7 @@ use std::{ }; use test_macro::test_function; use test_rpc::meta::Os; -use test_rpc::{mullvad_daemon::ServiceStatus, Interface, ServiceClient}; +use test_rpc::{mullvad_daemon::ServiceStatus, ServiceClient}; /// Install the last stable version of the app and verify that it is running. #[test_function(priority = -200)] @@ -102,10 +102,14 @@ pub async fn test_upgrade_app(ctx: TestContext, rpc: ServiceClient) -> Result<() // Begin monitoring outgoing traffic and pinging // + let guest_iface = rpc + .get_default_interface() + .await + .expect("failed to obtain default interface"); let guest_ip = rpc - .get_interface_ip(Interface::NonTunnel) + .get_interface_ip(guest_iface) .await - .expect("failed to obtain tunnel IP"); + .expect("failed to obtain non-tun IP"); log::debug!("Guest IP: {guest_ip}"); log::debug!("Monitoring outgoing traffic"); diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index b35f1b9b7a4c..ada613ca3465 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -63,6 +63,7 @@ pub enum Error { #[error(display = "Failed to parse gRPC response")] InvalidGrpcResponse(#[error(source)] types::FromProtobufTypeError), + #[cfg(target_os = "macos")] #[error(display = "An error occurred: {}", _0)] Other(String), } diff --git a/test/test-manager/src/tests/settings.rs b/test/test-manager/src/tests/settings.rs index 4c5808f79042..aea81028fe38 100644 --- a/test/test-manager/src/tests/settings.rs +++ b/test/test-manager/src/tests/settings.rs @@ -1,5 +1,5 @@ use super::helpers; -use super::helpers::{connect_and_wait, disconnect_and_wait, get_tunnel_state, send_guest_probes}; +use super::helpers::{connect_and_wait, get_tunnel_state, send_guest_probes}; use super::{Error, TestContext}; use crate::assert_tunnel_state; use crate::vm::network::DUMMY_LAN_INTERFACE_IP; @@ -8,7 +8,7 @@ use mullvad_management_interface::ManagementServiceClient; use mullvad_types::states::TunnelState; use std::net::{IpAddr, SocketAddr}; use test_macro::test_function; -use test_rpc::{Interface, ServiceClient}; +use test_rpc::ServiceClient; /// Verify that traffic to private IPs is blocked when /// "local network sharing" is disabled, but not blocked @@ -46,11 +46,12 @@ pub async fn test_lan( log::info!("Test whether outgoing LAN traffic is blocked"); + let default_interface = rpc.get_default_interface().await?; let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination).await?; + send_guest_probes(rpc.clone(), default_interface.clone(), lan_destination).await?; assert!( detected_probes.none(), - "observed unexpected outgoing LAN packets" + "observed unexpected outgoing LAN packets: {detected_probes:?}" ); // @@ -71,14 +72,12 @@ pub async fn test_lan( log::info!("Test whether outgoing LAN traffic is blocked"); let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination).await?; + send_guest_probes(rpc.clone(), default_interface, lan_destination).await?; assert!( detected_probes.all(), - "did not observe all outgoing LAN packets" + "did not observe all outgoing LAN packets: {detected_probes:?}" ); - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } @@ -133,15 +132,20 @@ pub async fn test_lockdown( // Ensure all destinations are unreachable // + let default_interface = rpc.get_default_interface().await?; + let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination).await?; - assert!(detected_probes.none(), "observed outgoing packets to LAN"); + send_guest_probes(rpc.clone(), default_interface.clone(), lan_destination).await?; + assert!( + detected_probes.none(), + "observed outgoing packets to LAN: {detected_probes:?}" + ); let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination).await?; + send_guest_probes(rpc.clone(), default_interface.clone(), inet_destination).await?; assert!( detected_probes.none(), - "observed outgoing packets to internet" + "observed outgoing packets to internet: {detected_probes:?}" ); // @@ -160,17 +164,17 @@ pub async fn test_lockdown( // let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination).await?; + send_guest_probes(rpc.clone(), default_interface.clone(), lan_destination).await?; assert!( detected_probes.all(), - "did not observe some outgoing packets" + "did not observe some outgoing packets: {detected_probes:?}" ); let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination).await?; + send_guest_probes(rpc.clone(), default_interface.clone(), inet_destination).await?; assert!( detected_probes.none(), - "observed outgoing packets to internet" + "observed outgoing packets to internet: {detected_probes:?}" ); // @@ -191,10 +195,10 @@ pub async fn test_lockdown( // Send traffic outside the tunnel to sanity check that the internet is *not* reachable via non- // tunnel interfaces. let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination).await?; + send_guest_probes(rpc.clone(), default_interface, inet_destination).await?; assert!( detected_probes.none(), - "observed outgoing packets to internet" + "observed outgoing packets to internet: {detected_probes:?}" ); // @@ -205,7 +209,5 @@ pub async fn test_lockdown( .await .expect("failed to disable lockdown mode"); - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 1f1d61670e87..6f759d782601 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -10,14 +10,13 @@ use mullvad_types::relay_constraints::{ OpenVpnConstraints, RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings, WireguardConstraints, }; -use mullvad_types::relay_list::{Relay, RelayEndpointData}; use mullvad_types::wireguard; use pnet_packet::ip::IpNextHeaderProtocols; use talpid_types::net::{TransportProtocol, TunnelType}; use test_macro::test_function; use test_rpc::meta::Os; use test_rpc::mullvad_daemon::ServiceStatus; -use test_rpc::{Interface, ServiceClient}; +use test_rpc::ServiceClient; /// Set up an OpenVPN tunnel, UDP as well as TCP. /// This test fails if a working tunnel cannot be set up. @@ -123,18 +122,14 @@ pub async fn test_wireguard_tunnel( Ok(()) } -/// Use udp2tcp obfuscation. This test connects to a -/// WireGuard relay over TCP. It fails if no outgoing TCP -/// traffic to the relay is observed on the expected port. +/// Use udp2tcp obfuscation. This test connects to a WireGuard relay over TCP. It fails if no +/// outgoing TCP traffic to the relay is observed on the expected port. #[test_function] pub async fn test_udp2tcp_tunnel( _: TestContext, rpc: ServiceClient, mut mullvad_client: ManagementServiceClient, ) -> Result<(), Error> { - // TODO: check if src <-> target / tcp is observed (only) - // TODO: ping a public IP on the fake network (not possible using real relay) - mullvad_client .set_obfuscation_settings(types::ObfuscationSettings::from(ObfuscationSettings { selected_obfuscation: SelectedObfuscation::Udp2Tcp, @@ -158,18 +153,19 @@ pub async fn test_udp2tcp_tunnel( connect_and_wait(&mut mullvad_client).await?; + let endpoint = match helpers::get_tunnel_state(&mut mullvad_client).await { + mullvad_types::states::TunnelState::Connected { endpoint, .. } => endpoint.endpoint, + _ => panic!("unexpected tunnel state"), + }; + // // Set up packet monitor // - let guest_ip = rpc - .get_interface_ip(Interface::NonTunnel) - .await - .expect("failed to obtain inet interface IP"); - let monitor = start_packet_monitor( move |packet| { - packet.source.ip() != guest_ip || (packet.protocol == IpNextHeaderProtocols::Tcp) + packet.destination.ip() == endpoint.address.ip() + && packet.protocol == IpNextHeaderProtocols::Tcp }, MonitorOptions::default(), ) @@ -185,9 +181,10 @@ pub async fn test_udp2tcp_tunnel( ); let monitor_result = monitor.into_result().await.unwrap(); - assert_eq!(monitor_result.discarded_packets, 0); - - disconnect_and_wait(&mut mullvad_client).await?; + assert!( + !monitor_result.packets.is_empty(), + "detected no tcp traffic", + ); Ok(()) } @@ -235,22 +232,19 @@ pub async fn test_bridge( log::info!("Connect to OpenVPN relay via bridge"); - connect_and_wait(&mut mullvad_client) - .await - .expect("connect_and_wait"); + connect_and_wait(&mut mullvad_client).await?; - let tunnel = helpers::get_tunnel_state(&mut mullvad_client).await; - let (entry, exit) = match tunnel { + let (entry, exit) = match helpers::get_tunnel_state(&mut mullvad_client).await { mullvad_types::states::TunnelState::Connected { endpoint, .. } => { (endpoint.proxy.unwrap().endpoint, endpoint.endpoint) } - _ => return Err(Error::DaemonError("daemon entered error state".to_string())), + _ => panic!("unexpected tunnel state"), }; log::info!( - "Selected entry bridge {entry_ip} & exit relay {exit_ip}", - entry_ip = entry.address.ip().to_string(), - exit_ip = exit.address.ip().to_string() + "Selected entry bridge {entry_addr} & exit relay {exit_addr}", + entry_addr = entry.address, + exit_addr = exit.address ); // Start recording outgoing packets. Their destination will be verified @@ -284,14 +278,11 @@ pub async fn test_bridge( "detected no traffic to entry server", ); - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } /// Test whether WireGuard multihop works. This fails if: -/// * No outgoing traffic to the entry relay is -/// observed from the SUT. +/// * No outgoing traffic to the entry relay is observed from the SUT. /// * The conncheck reports an unexpected exit relay. #[test_function] pub async fn test_multihop( @@ -299,24 +290,12 @@ pub async fn test_multihop( rpc: ServiceClient, mut mullvad_client: ManagementServiceClient, ) -> Result<(), Error> { - // - // Set relays to use - // - - log::info!("Select relay"); - let relay_filter = |relay: &Relay| { - relay.active && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) - }; - let (entry, exit) = helpers::random_entry_and_exit(&mut mullvad_client, relay_filter).await?; - let exit_constraint = helpers::into_constraint(&exit); let wireguard_constraints = WireguardConstraints { use_multihop: true, - entry_location: helpers::into_constraint(&entry), ..Default::default() }; let relay_settings = RelaySettings::Normal(RelayConstraints { - location: exit_constraint, wireguard_constraints, ..Default::default() }); @@ -329,25 +308,32 @@ pub async fn test_multihop( // Connect // - let monitor = start_packet_monitor( - move |packet| { - packet.destination.ip() == entry.ipv4_addr_in - && packet.protocol == IpNextHeaderProtocols::Udp - }, - MonitorOptions::default(), - ) - .await; + log::info!("Connect using WG multihop"); connect_and_wait(&mut mullvad_client).await?; + let (entry, exit) = match helpers::get_tunnel_state(&mut mullvad_client).await { + mullvad_types::states::TunnelState::Connected { endpoint, .. } => { + (endpoint.entry_endpoint.unwrap(), endpoint.endpoint) + } + _ => panic!("unexpected tunnel state"), + }; + + log::info!( + "Selected entry {entry_addr} & exit relay {exit_addr}", + entry_addr = entry.address, + exit_addr = exit.address + ); + // - // Verify entry IP + // Record outgoing packets to the entry relay // - log::info!("Verifying entry server"); - - let monitor_result = monitor.into_result().await.unwrap(); - assert!(!monitor_result.packets.is_empty(), "no matching packets",); + let monitor = start_packet_monitor( + move |packet| packet.destination.ip() == entry.address.ip(), + MonitorOptions::default(), + ) + .await; // // Verify exit IP @@ -358,7 +344,14 @@ pub async fn test_multihop( "expected Mullvad exit IP" ); - disconnect_and_wait(&mut mullvad_client).await?; + // + // Verify entry IP + // + + log::info!("Verifying entry server"); + + let monitor_result = monitor.into_result().await.unwrap(); + assert!(!monitor_result.packets.is_empty(), "no matching packets",); Ok(()) } @@ -473,7 +466,7 @@ pub async fn test_quantum_resistant_tunnel( // connect_and_wait(&mut mullvad_client).await?; - check_tunnel_psk(&rpc, false).await; + check_tunnel_psk(&rpc, &mullvad_client, false).await; log::info!("Setting tunnel protocol to WireGuard"); @@ -497,7 +490,7 @@ pub async fn test_quantum_resistant_tunnel( // connect_and_wait(&mut mullvad_client).await?; - check_tunnel_psk(&rpc, true).await; + check_tunnel_psk(&rpc, &mullvad_client, true).await; assert!( helpers::using_mullvad_exit(&rpc).await, @@ -507,11 +500,14 @@ pub async fn test_quantum_resistant_tunnel( Ok(()) } -async fn check_tunnel_psk(rpc: &ServiceClient, should_have_psk: bool) { +async fn check_tunnel_psk( + rpc: &ServiceClient, + mullvad_client: &ManagementServiceClient, + should_have_psk: bool, +) { match rpc.get_os().await.expect("failed to get OS") { Os::Linux => { - let name = rpc - .get_interface_name(Interface::Tunnel) + let name = helpers::get_tunnel_interface(mullvad_client.clone()) .await .expect("failed to get tun name"); let output = rpc diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index 87fda3b68589..eb78828fd0d6 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -1,6 +1,6 @@ use super::helpers::{ - self, connect_and_wait, disconnect_and_wait, get_tunnel_state, send_guest_probes, - set_relay_settings, unreachable_wireguard_tunnel, wait_for_tunnel_state, + self, connect_and_wait, get_tunnel_state, send_guest_probes, set_relay_settings, + unreachable_wireguard_tunnel, wait_for_tunnel_state, }; use super::{ui, Error, TestContext}; use crate::assert_tunnel_state; @@ -17,7 +17,7 @@ use mullvad_types::{ use std::net::{IpAddr, SocketAddr}; use talpid_types::net::{Endpoint, TransportProtocol, TunnelEndpoint, TunnelType}; use test_macro::test_function; -use test_rpc::{Interface, ServiceClient}; +use test_rpc::ServiceClient; /// Verify that outgoing TCP, UDP, and ICMP packets can be observed /// in the disconnected state. The purpose is mostly to rule prevent @@ -40,8 +40,13 @@ pub async fn test_disconnected_state( log::info!("Sending packets to {inet_destination}"); + let non_tunnel_interface = rpc + .get_default_interface() + .await + .expect("failed to obtain non-tun interface"); + let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination).await?; + send_guest_probes(rpc.clone(), non_tunnel_interface, inet_destination).await?; assert!( detected_probes.all(), "did not see (all) outgoing packets to destination: {detected_probes:?}", @@ -118,26 +123,31 @@ pub async fn test_connecting_state( // Leak test // + let non_tunnel_interface = rpc + .get_default_interface() + .await + .expect("failed to obtain non-tun interface"); + assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination) + send_guest_probes(rpc.clone(), non_tunnel_interface.clone(), inet_destination) .await? .none(), "observed unexpected outgoing packets (inet)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination) + send_guest_probes(rpc.clone(), non_tunnel_interface.clone(), lan_destination) .await? .none(), "observed unexpected outgoing packets (lan)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_dns) + send_guest_probes(rpc.clone(), non_tunnel_interface.clone(), inet_dns) .await? .none(), "observed unexpected outgoing packets (DNS, inet)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_dns) + send_guest_probes(rpc.clone(), non_tunnel_interface, lan_dns) .await? .none(), "observed unexpected outgoing packets (DNS, lan)" @@ -145,14 +155,6 @@ pub async fn test_connecting_state( assert_tunnel_state!(&mut mullvad_client, TunnelState::Connecting { .. }); - // - // Disconnect - // - - log::info!("Disconnecting"); - - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } @@ -201,39 +203,36 @@ pub async fn test_error_state( // Leak test // + let default_interface = rpc + .get_default_interface() + .await + .expect("failed to obtain non-tun interface"); + assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination) + send_guest_probes(rpc.clone(), default_interface.clone(), inet_destination) .await? .none(), "observed unexpected outgoing packets (inet)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_destination) + send_guest_probes(rpc.clone(), default_interface.clone(), lan_destination) .await? .none(), "observed unexpected outgoing packets (lan)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_dns) + send_guest_probes(rpc.clone(), default_interface.clone(), inet_dns) .await? .none(), "observed unexpected outgoing packets (DNS, inet)" ); assert!( - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), lan_dns) + send_guest_probes(rpc.clone(), default_interface, lan_dns) .await? .none(), "observed unexpected outgoing packets (DNS, lan)" ); - // - // Disconnect - // - - log::info!("Disconnecting"); - - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } @@ -318,11 +317,15 @@ pub async fn test_connected_state( log::info!("Test whether outgoing non-tunnel traffic is blocked"); - let detected_probes = - send_guest_probes(rpc.clone(), Some(Interface::NonTunnel), inet_destination).await?; + let nontun_iface = rpc + .get_default_interface() + .await + .expect("failed to find non-tun interface"); + + let detected_probes = send_guest_probes(rpc.clone(), nontun_iface, inet_destination).await?; assert!( detected_probes.none(), - "observed unexpected outgoing packets" + "observed unexpected outgoing packets: {detected_probes:?}" ); assert!( @@ -330,7 +333,5 @@ pub async fn test_connected_state( "expected Mullvad exit IP" ); - disconnect_and_wait(&mut mullvad_client).await?; - Ok(()) } diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index 387d0a2435d1..6be77afb4035 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -17,8 +17,6 @@ pub struct ServiceClient { client: service::ServiceClient, } -// TODO: implement wrapper methods using macro on Service trait - impl ServiceClient { pub fn new( connection_handle: transport::ConnectionHandle, @@ -156,7 +154,7 @@ impl ServiceClient { /// Send TCP packet pub async fn send_tcp( &self, - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), Error> { @@ -168,7 +166,7 @@ impl ServiceClient { /// Send UDP packet pub async fn send_udp( &self, - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), Error> { @@ -180,7 +178,7 @@ impl ServiceClient { /// Send ICMP pub async fn send_ping( &self, - interface: Option, + interface: Option, destination: IpAddr, ) -> Result<(), Error> { self.client @@ -196,16 +194,16 @@ impl ServiceClient { } /// Returns the IP of the given interface. - pub async fn get_interface_name(&self, interface: Interface) -> Result { + pub async fn get_interface_ip(&self, interface: String) -> Result { self.client - .get_interface_name(tarpc::context::current(), interface) + .get_interface_ip(tarpc::context::current(), interface) .await? } - /// Returns the IP of the given interface. - pub async fn get_interface_ip(&self, interface: Interface) -> Result { + /// Returns the name of the default non-tunnel interface + pub async fn get_default_interface(&self) -> Result { self.client - .get_interface_ip(tarpc::context::current(), interface) + .get_default_interface(tarpc::context::current()) .await? } diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 2fd4411f4940..6968a3c613e5 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -55,12 +55,6 @@ pub enum Error { Timeout, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] -pub enum Interface { - Tunnel, - NonTunnel, -} - /// Response from am.i.mullvad.net #[derive(Debug, Serialize, Deserialize)] pub struct AmIMullvad { @@ -128,29 +122,29 @@ mod service { /// Send TCP packet async fn send_tcp( - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), Error>; /// Send UDP packet async fn send_udp( - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), Error>; /// Send ICMP - async fn send_ping(interface: Option, destination: IpAddr) -> Result<(), Error>; + async fn send_ping(interface: Option, destination: IpAddr) -> Result<(), Error>; /// Fetch the current location. async fn geoip_lookup(mullvad_host: String) -> Result; - /// Returns the name of the given interface. - async fn get_interface_name(interface: Interface) -> Result; - /// Returns the IP of the given interface. - async fn get_interface_ip(interface: Interface) -> Result; + async fn get_interface_ip(interface: String) -> Result; + + /// Returns the name of the default interface. + async fn get_default_interface() -> Result; /// Perform DNS resolution. async fn resolve_hostname(hostname: String) -> Result, Error>; diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index 8d7991d6fa29..ebf0d1e47454 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -13,7 +13,7 @@ use test_rpc::{ mullvad_daemon::{ServiceStatus, SOCKET_PATH}, package::Package, transport::GrpcForwarder, - AppTrace, Interface, Service, + AppTrace, Service, }; use tokio::sync::broadcast::error::TryRecvError; use tokio::{ @@ -117,7 +117,7 @@ impl Service for TestServer { async fn send_tcp( self, _: context::Context, - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), test_rpc::Error> { @@ -127,7 +127,7 @@ impl Service for TestServer { async fn send_udp( self, _: context::Context, - interface: Option, + interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), test_rpc::Error> { @@ -137,10 +137,10 @@ impl Service for TestServer { async fn send_ping( self, _: context::Context, - interface: Option, + interface: Option, destination: IpAddr, ) -> Result<(), test_rpc::Error> { - net::send_ping(interface, destination).await + net::send_ping(interface.as_ref().map(String::as_str), destination).await } async fn geoip_lookup( @@ -165,20 +165,16 @@ impl Service for TestServer { .collect()) } - async fn get_interface_name( - self, - _: context::Context, - interface: Interface, - ) -> Result { - Ok(net::get_interface_name(interface).to_owned()) - } - async fn get_interface_ip( self, _: context::Context, - interface: Interface, + interface: String, ) -> Result { - net::get_interface_ip(interface) + net::get_interface_ip(&interface).await + } + + async fn get_default_interface(self, _: context::Context) -> Result { + Ok(net::get_default_interface().to_owned()) } async fn poll_output( diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index 851a2f29510d..f40aece4c9bb 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -2,27 +2,14 @@ use socket2::SockAddr; #[cfg(target_os = "macos")] use std::{ffi::CString, num::NonZeroU32}; use std::{ + io::Write, net::{IpAddr, SocketAddr}, process::Output, }; -use test_rpc::Interface; -use tokio::{ - io::AsyncWriteExt, - net::{TcpStream, UdpSocket}, - process::Command, -}; - -#[cfg(target_os = "linux")] -const TUNNEL_INTERFACE: &str = "wg-mullvad"; - -#[cfg(target_os = "windows")] -const TUNNEL_INTERFACE: &str = "Mullvad"; - -#[cfg(target_os = "macos")] -const TUNNEL_INTERFACE: &str = "utun3"; +use tokio::process::Command; pub async fn send_tcp( - bind_interface: Option, + bind_interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), test_rpc::Error> { @@ -36,14 +23,7 @@ pub async fn send_tcp( test_rpc::Error::SendTcp })?; - sock.set_nonblocking(true).map_err(|error| { - log::error!("Failed to set non-blocking TCP socket: {error}"); - test_rpc::Error::SendTcp - })?; - if let Some(iface) = bind_interface { - let iface = get_interface_name(iface); - #[cfg(target_os = "macos")] let interface_index = unsafe { let name = CString::new(iface).unwrap(); @@ -71,35 +51,32 @@ pub async fn send_tcp( log::trace!("Bind interface {iface} is ignored on Windows") } - sock.bind(&SockAddr::from(bind_addr)).map_err(|error| { - log::error!("Failed to bind TCP socket to {bind_addr}: {error}"); - test_rpc::Error::SendTcp - })?; - log::debug!("Connecting from {bind_addr} to {destination}/TCP"); - sock.connect(&SockAddr::from(destination)) - .map_err(|error| { - log::error!("Failed to connect to {destination}: {error}"); + tokio::task::spawn_blocking(move || { + sock.bind(&SockAddr::from(bind_addr)).map_err(|error| { + log::error!("Failed to bind TCP socket to {bind_addr}: {error}"); test_rpc::Error::SendTcp })?; - let std_stream = std::net::TcpStream::from(sock); - let mut stream = TcpStream::from_std(std_stream).map_err(|error| { - log::error!("Failed to convert to TCP stream to tokio stream: {error}"); - test_rpc::Error::SendTcp - })?; - - stream.write_all(b"hello").await.map_err(|error| { - log::error!("Failed to send message to {destination}: {error}"); - test_rpc::Error::SendTcp - })?; + sock.connect(&SockAddr::from(destination)) + .map_err(|error| { + log::error!("Failed to connect to {destination}: {error}"); + test_rpc::Error::SendTcp + })?; - Ok(()) + let mut stream = std::net::TcpStream::from(sock); + stream.write_all(b"hello").map_err(|error| { + log::error!("Failed to send message to {destination}: {error}"); + test_rpc::Error::SendTcp + }) + }) + .await + .unwrap() } pub async fn send_udp( - bind_interface: Option, + bind_interface: Option, bind_addr: SocketAddr, destination: SocketAddr, ) -> Result<(), test_rpc::Error> { @@ -113,14 +90,7 @@ pub async fn send_udp( test_rpc::Error::SendUdp })?; - sock.set_nonblocking(true).map_err(|error| { - log::error!("Failed to set non-blocking UDP socket: {error}"); - test_rpc::Error::SendUdp - })?; - if let Some(iface) = bind_interface { - let iface = get_interface_name(iface); - #[cfg(target_os = "macos")] let interface_index = unsafe { let name = CString::new(iface).unwrap(); @@ -148,32 +118,28 @@ pub async fn send_udp( log::trace!("Bind interface {iface} is ignored on Windows") } - sock.bind(&SockAddr::from(bind_addr)).map_err(|error| { - log::error!("Failed to bind UDP socket to {bind_addr}: {error}"); - test_rpc::Error::SendUdp - })?; + let _ = tokio::task::spawn_blocking(move || { + sock.bind(&SockAddr::from(bind_addr)).map_err(|error| { + log::error!("Failed to bind UDP socket to {bind_addr}: {error}"); + test_rpc::Error::SendUdp + })?; - log::debug!("Send message from {bind_addr} to {destination}/UDP"); + log::debug!("Send message from {bind_addr} to {destination}/UDP"); - let std_socket = std::net::UdpSocket::from(sock); - let tokio_socket = UdpSocket::from_std(std_socket).map_err(|error| { - log::error!("Failed to convert to UDP socket to tokio socket: {error}"); - test_rpc::Error::SendUdp - })?; - - tokio_socket - .send_to(b"hello", destination) - .await - .map_err(|error| { + let std_socket = std::net::UdpSocket::from(sock); + std_socket.send_to(b"hello", destination).map_err(|error| { log::error!("Failed to send message to {destination}: {error}"); test_rpc::Error::SendUdp - })?; + }) + }) + .await + .unwrap()?; Ok(()) } pub async fn send_ping( - interface: Option, + interface: Option<&str>, destination: IpAddr, ) -> Result<(), test_rpc::Error> { #[cfg(target_os = "windows")] @@ -202,22 +168,8 @@ pub async fn send_ping( cmd.args(["-c", "1"]); match interface { - Some(Interface::Tunnel) => { - log::info!("Pinging {destination} in tunnel"); - - #[cfg(target_os = "windows")] - if let Some(source_ip) = source_ip { - cmd.args(["-S", &source_ip.to_string()]); - } - - #[cfg(target_os = "windows")] - cmd.args(["-I", TUNNEL_INTERFACE]); - - #[cfg(target_os = "macos")] - cmd.args(["-b", TUNNEL_INTERFACE]); - } - Some(Interface::NonTunnel) => { - log::info!("Pinging {destination} outside tunnel"); + Some(interface) => { + log::info!("Pinging {destination} on interface {interface}"); #[cfg(target_os = "windows")] if let Some(source_ip) = source_ip { @@ -225,10 +177,10 @@ pub async fn send_ping( } #[cfg(target_os = "linux")] - cmd.args(["-I", non_tunnel_interface()]); + cmd.args(["-I", interface]); #[cfg(target_os = "macos")] - cmd.args(["-b", non_tunnel_interface()]); + cmd.args(["-b", interface]); } None => log::info!("Pinging {destination}"), } @@ -250,18 +202,16 @@ pub async fn send_ping( } #[cfg(unix)] -pub fn get_interface_ip(interface: Interface) -> Result { +pub async fn get_interface_ip(interface: &str) -> Result { // TODO: IPv6 use std::net::Ipv4Addr; - let alias = get_interface_name(interface); - let addrs = nix::ifaddrs::getifaddrs().map_err(|error| { log::error!("Failed to obtain interfaces: {}", error); test_rpc::Error::Syscall })?; for addr in addrs { - if addr.interface_name == alias { + if addr.interface_name == interface { if let Some(address) = addr.address { if let Some(sockaddr) = address.as_sockaddr_in() { return Ok(IpAddr::V4(Ipv4Addr::from(sockaddr.ip()))); @@ -274,15 +224,8 @@ pub fn get_interface_ip(interface: Interface) -> Result Err(test_rpc::Error::InterfaceNotFound) } -pub fn get_interface_name(interface: Interface) -> &'static str { - match interface { - Interface::Tunnel => TUNNEL_INTERFACE, - Interface::NonTunnel => non_tunnel_interface(), - } -} - #[cfg(target_os = "windows")] -pub fn get_interface_ip(interface: Interface) -> Result { +pub async fn get_interface_ip(interface: &str) -> Result { // TODO: IPv6 get_interface_ip_for_family(interface, talpid_windows::net::AddressFamily::Ipv4) @@ -292,24 +235,19 @@ pub fn get_interface_ip(interface: Interface) -> Result #[cfg(target_os = "windows")] fn get_interface_ip_for_family( - interface: Interface, + interface: &str, family: talpid_windows::net::AddressFamily, ) -> Result, ()> { - let interface = match interface { - Interface::NonTunnel => non_tunnel_interface(), - Interface::Tunnel => TUNNEL_INTERFACE, - }; - let interface_alias = talpid_windows::net::luid_from_alias(interface).map_err(|error| { + let luid = talpid_windows::net::luid_from_alias(interface).map_err(|error| { log::error!("Failed to obtain interface LUID: {error}"); })?; - - talpid_windows::net::get_ip_address_for_interface(family, interface_alias).map_err(|error| { + talpid_windows::net::get_ip_address_for_interface(family, luid).map_err(|error| { log::error!("Failed to obtain interface IP: {error}"); }) } #[cfg(target_os = "windows")] -fn non_tunnel_interface() -> &'static str { +pub fn get_default_interface() -> &'static str { use once_cell::sync::OnceCell; use talpid_platform_metadata::WindowsVersion; @@ -326,12 +264,12 @@ fn non_tunnel_interface() -> &'static str { } #[cfg(target_os = "linux")] -fn non_tunnel_interface() -> &'static str { +pub fn get_default_interface() -> &'static str { "ens3" } #[cfg(target_os = "macos")] -fn non_tunnel_interface() -> &'static str { +pub fn get_default_interface() -> &'static str { "en0" }