From a779803a50123b43e9aa523ed9889e120036b090 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Wed, 28 Aug 2024 14:48:44 +0200 Subject: [PATCH] Add DAITA smart routing e2e tests --- .../src/relay_selector/query.rs | 11 +- test/test-manager/src/tests/daita.rs | 200 ++++++++++++++++++ test/test-manager/src/tests/mod.rs | 6 +- test/test-manager/src/tests/tunnel.rs | 35 --- 4 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 test/test-manager/src/tests/daita.rs diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index 4a9cd7c12b2c..2dc81b083386 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -34,8 +34,8 @@ use mullvad_types::{ relay_constraints::{ BridgeConstraints, BridgeSettings, BridgeState, BridgeType, LocationConstraint, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - SelectedObfuscation, ShadowsocksSettings, TransportPort, Udp2TcpObfuscationSettings, - WireguardConstraints, + RelaySettings, SelectedObfuscation, ShadowsocksSettings, TransportPort, + Udp2TcpObfuscationSettings, WireguardConstraints, }, wireguard::QuantumResistantState, Intersection, @@ -244,6 +244,13 @@ impl Default for RelayQuery { } } +impl From for RelaySettings { + fn from(query: RelayQuery) -> Self { + let (relay_constraints, ..) = query.into_settings(); + RelaySettings::from(relay_constraints) + } +} + /// A query for a relay with Wireguard-specific properties, such as `multihop` and [wireguard /// obfuscation][`SelectedObfuscation`]. /// diff --git a/test/test-manager/src/tests/daita.rs b/test/test-manager/src/tests/daita.rs new file mode 100644 index 000000000000..912c811eccc2 --- /dev/null +++ b/test/test-manager/src/tests/daita.rs @@ -0,0 +1,200 @@ +use anyhow::{anyhow, bail, ensure, Context}; +use futures::StreamExt; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_relay_selector::query::builder::RelayQueryBuilder; +use mullvad_types::{ + relay_constraints::GeographicLocationConstraint, relay_list::RelayEndpointData, + states::TunnelState, +}; +use talpid_types::{net::TunnelEndpoint, tunnel::ErrorStateCause}; +use test_macro::test_function; +use test_rpc::ServiceClient; + +use super::{helpers, Error, TestContext}; + +/// Test that daita and daita_smart_routing works by connecting +/// - to a non-DAITA relay with singlehop (should block) +/// - to a DAITA relay with singlehop +/// - to a DAITA relay with auto-multihop using smart_routing +/// - to a DAITA relay with explicit multihop +/// - to a non-DAITA relay with multihop (should block) +/// +/// # Limitations +/// +/// The test does not analyze any traffic, nor verify that DAITA is in use in any way except +/// by looking at [TunnelEndpoint::daita]. +#[test_function] +pub async fn test_daita( + _ctx: TestContext, + _rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + let relay_list = mullvad_client.get_relay_locations().await?; + let wg_relays = relay_list + .relays() + .flat_map(|relay| match &relay.endpoint_data { + RelayEndpointData::Wireguard(wireguard) => Some((relay, wireguard)), + _ => None, + }); + + // Select two relays to use for the test, one with DAITA and one without. + let daita_relay = wg_relays + .clone() + .find(|(_relay, wireguard_data)| wireguard_data.daita) + .map(|(relay, _)| relay) + .context("Failed to find a daita wireguard relay")?; + log::info!("Selected daita relay: {}", daita_relay.hostname); + let daita_relay_location = GeographicLocationConstraint::hostname( + &daita_relay.location.country_code, + &daita_relay.location.city_code, + &daita_relay.hostname, + ); + + let non_daita_relay = wg_relays + .clone() + .find(|(_relay, wireguard_data)| !wireguard_data.daita) + .map(|(relay, _)| relay) + .context("Failed to find a non-daita wireguard relay")?; + let non_daita_relay_location = GeographicLocationConstraint::hostname( + &non_daita_relay.location.country_code, + &non_daita_relay.location.city_code, + &non_daita_relay.hostname, + ); + log::info!("Selected non-daita relay: {}", non_daita_relay.hostname); + + let non_daita_location_query = RelayQueryBuilder::new() + .wireguard() + .location(non_daita_relay_location.clone()) + .build(); + + let daita_location_query = RelayQueryBuilder::new() + .wireguard() + .location(daita_relay_location.clone()) + .build(); + + let daita_to_non_daita_multihop_query = RelayQueryBuilder::new() + .wireguard() + .multihop() + .entry(daita_relay_location.clone()) + .location(non_daita_relay_location.clone()) + .build(); + + let non_daita_multihop_query = RelayQueryBuilder::new() + .wireguard() + .multihop() + .entry(non_daita_relay_location.clone()) + .build(); + + let mut events = mullvad_client + .events_listen() + .await? + .inspect(|event| log::debug!("New daemon event: {event:?}")); + + log::info!("Connecting to non-daita relay with DAITA smart routing"); + { + helpers::set_relay_settings(&mut mullvad_client, non_daita_location_query.clone()).await?; + mullvad_client.set_enable_daita(true).await?; + mullvad_client.connect_tunnel().await?; + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect with smart_routing enabled")?; + + let endpoint: &TunnelEndpoint = state.endpoint().ok_or(anyhow!("No endpoint"))?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!(endpoint.entry_endpoint.is_some(), "multihop must be used"); + + log::info!("Successfully multihopped with use smart_routing"); + } + + log::info!("Connecting to non-daita relay with DAITA but no smart routing"); + { + mullvad_client.set_daita_smart_routing(false).await?; + + let result = wait_for_daemon_reconnect(&mut events).await; + let Err(Error::UnexpectedErrorState(state)) = result else { + bail!("Connection failed unsuccessfully, reason: {:?}", result); + }; + let ErrorStateCause::TunnelParameterError(_) = state.cause() else { + bail!("Connection failed unsuccessfully, cause: {}", state.cause()); + }; + + log::info!("Failed to connect, this is expected!"); + } + + log::info!("Connecting to daita relay with smart_routing"); + { + helpers::set_relay_settings(&mut mullvad_client, daita_location_query).await?; + + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect to daita location with smart_routing enabled")?; + + let endpoint = state.endpoint().context("No endpoint")?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!( + endpoint.entry_endpoint.is_none(), + "multihop must not be used" + ); + + log::info!("Successfully singlehopped with smart_routing"); + } + + log::info!("Connecting to daita relay with multihop"); + { + helpers::set_relay_settings(&mut mullvad_client, daita_to_non_daita_multihop_query).await?; + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect via daita location with multihop enabled")?; + + let endpoint = state.endpoint().context("No endpoint")?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!(endpoint.entry_endpoint.is_some(), "multihop must be used"); + + log::info!("Successfully connected with multihop"); + } + + log::info!("Connecting to non_daita relay with multihop"); + { + helpers::set_relay_settings(&mut mullvad_client, non_daita_multihop_query).await?; + let result = wait_for_daemon_reconnect(&mut events).await; + let Err(Error::UnexpectedErrorState(state)) = result else { + bail!("Connection failed unsuccessfully, reason: {:?}", result); + }; + let ErrorStateCause::TunnelParameterError(_) = state.cause() else { + bail!("Connection failed unsuccessfully, cause: {}", state.cause()); + }; + + log::info!("Failed to connect, this is expected!"); + } + + Ok(()) +} + +async fn wait_for_daemon_reconnect( + mut event_stream: impl futures::Stream> + + Unpin, +) -> Result { + // wait until the daemon informs us that it's trying to connect + helpers::find_daemon_event(&mut event_stream, |event| match event { + DaemonEvent::TunnelState(state) => Some(match state { + TunnelState::Connecting { .. } => Ok(state), + TunnelState::Connected { .. } => return None, + TunnelState::Disconnecting { .. } => return None, + TunnelState::Disconnected { .. } => Err(Error::UnexpectedTunnelState(Box::new(state))), + TunnelState::Error(state) => Err(Error::UnexpectedErrorState(state)), + }), + _ => None, + }) + .await??; + + // then wait until the daemon informs us that it connected (or failed) + helpers::find_daemon_event(&mut event_stream, |event| match event { + DaemonEvent::TunnelState(state) => match state { + TunnelState::Connecting { .. } => None, + TunnelState::Connected { .. } => Some(Ok(state)), + _ => Some(Err(Error::UnexpectedTunnelState(Box::new(state)))), + }, + _ => None, + }) + .await? +} diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index 7e4cbc9eb6a5..bc17a7f3f64a 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -2,6 +2,7 @@ mod access_methods; mod account; pub mod config; mod cve_2019_14899; +mod daita; mod dns; mod helpers; mod install; @@ -57,7 +58,10 @@ pub enum Error { #[error("The daemon returned an error: {0}")] Daemon(String), - #[error("The daemon ended up in the error state")] + #[error("The daemon ended up in the the wrong tunnel-state: {0:?}")] + UnexpectedTunnelState(Box), + + #[error("The daemon ended up in the error state: {0:?}")] UnexpectedErrorState(talpid_types::tunnel::ErrorState), #[error("The gRPC client ran into an error: {0}")] diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index f5d8f6c95960..6fb57adc1498 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -390,41 +390,6 @@ pub async fn test_wireguard_autoconnect( Ok(()) } -/// Test connecting to a WireGuard relay using DAITA. -/// -/// # Limitations -/// -/// The test does not analyze any traffic, nor verify that DAITA is in use. -#[test_function] -pub async fn test_daita( - _: TestContext, - rpc: ServiceClient, - mut mullvad_client: MullvadProxyClient, -) -> anyhow::Result<()> { - log::info!("Connecting to relay with DAITA"); - - apply_settings_from_relay_query( - &mut mullvad_client, - RelayQueryBuilder::new().wireguard().build(), - ) - .await?; - - mullvad_client - .set_daita_settings(wireguard::DaitaSettings { - enabled: true, - use_anywhere: false, - }) - .await - .context("Failed to enable daita")?; - - connect_and_wait(&mut mullvad_client).await?; - - log::info!("Check that the connection works"); - let _ = helpers::geoip_lookup_with_retries(&rpc).await?; - - Ok(()) -} - /// Test whether the daemon automatically connects on reboot when using /// OpenVPN. ///