Skip to content

Commit

Permalink
Add DAITA smart routing e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hulthe committed Sep 16, 2024
1 parent 240ca6a commit a779803
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 38 deletions.
11 changes: 9 additions & 2 deletions mullvad-relay-selector/src/relay_selector/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -244,6 +244,13 @@ impl Default for RelayQuery {
}
}

impl From<RelayQuery> 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`].
///
Expand Down
200 changes: 200 additions & 0 deletions test/test-manager/src/tests/daita.rs
Original file line number Diff line number Diff line change
@@ -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<Item = Result<DaemonEvent, mullvad_management_interface::Error>>
+ Unpin,
) -> Result<TunnelState, Error> {
// 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?
}
6 changes: 5 additions & 1 deletion test/test-manager/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod access_methods;
mod account;
pub mod config;
mod cve_2019_14899;
mod daita;
mod dns;
mod helpers;
mod install;
Expand Down Expand Up @@ -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<mullvad_types::states::TunnelState>),

#[error("The daemon ended up in the error state: {0:?}")]
UnexpectedErrorState(talpid_types::tunnel::ErrorState),

#[error("The gRPC client ran into an error: {0}")]
Expand Down
35 changes: 0 additions & 35 deletions test/test-manager/src/tests/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down

0 comments on commit a779803

Please sign in to comment.