summaryrefslogtreecommitdiffhomepage
path: root/test/test-manager/src/tests/daita.rs
blob: d5320148abccfe95fbf45f1319722680de653b63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
use anyhow::{Context, anyhow, bail, ensure};
use futures::StreamExt;
use mullvad_management_interface::{MullvadProxyClient, client::DaemonEvent};
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
use mullvad_types::{
    constraints::Constraint, relay_constraints::GeographicLocationConstraint,
    relay_list::RelayEndpointData, states::TunnelState,
};
use talpid_types::tunnel::ActionAfterDisconnect;
use talpid_types::{net::TunnelEndpoint, tunnel::ErrorStateCause};
use test_macro::test_function;
use test_rpc::ServiceClient;

use super::{Error, TestContext, helpers};

/// Test that daita and daita_direct_only 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 by disabling direct_only
/// - 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 relays = helpers::get_all_pickable_relays(&mut mullvad_client).await?;
    let wg_relays = relays.iter().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);

    log::info!("Setting wireguard and DAITA");
    let wireguard_query = RelayQueryBuilder::wireguard().build();
    helpers::apply_settings_from_relay_query(&mut mullvad_client, wireguard_query.clone()).await?;
    mullvad_client.set_enable_daita(true).await?;

    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 should automatically use multihop");
    {
        helpers::update_relay_constraints(&mut mullvad_client, |constraint| {
            constraint.location = Constraint::Only(non_daita_relay_location.clone().into());
        })
        .await?;
        mullvad_client.set_daita_direct_only(false).await?;

        mullvad_client.connect_tunnel().await?;
        let state = wait_for_daemon_reconnect(&mut events)
            .await
            .context("Failed to connect with 'direct only' disabled")?;

        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 'direct only' disabled");
    }

    log::info!("Connecting to non-daita relay with 'direct_only' shoud fail");
    {
        helpers::update_relay_constraints(&mut mullvad_client, |constraint| {
            constraint.location = Constraint::Only(non_daita_relay_location.clone().into());
        })
        .await?;
        mullvad_client.set_daita_direct_only(true).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 'direct_only' should not use multihop");
    {
        helpers::update_relay_constraints(&mut mullvad_client, |constraint| {
            constraint.location = Constraint::Only(daita_relay_location.clone().into());
        })
        .await?;
        mullvad_client.set_daita_direct_only(true).await?;

        let state = wait_for_daemon_reconnect(&mut events)
            .await
            .context("Failed to connect to daita location with 'direct_only' disabled")?;

        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 'direct_only' disabled");
    }

    log::info!("Connecting to a daita relay as entry for multihop and `direct_only` should work");
    {
        helpers::update_relay_constraints(&mut mullvad_client, |constraint| {
            constraint.location = Constraint::Only(non_daita_relay_location.clone().into());
            constraint.wireguard_constraints.entry_location =
                Constraint::Only(daita_relay_location.clone().into());
            constraint.wireguard_constraints.use_multihop = true;
        })
        .await?;
        mullvad_client.set_daita_direct_only(true).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 a non daita relay as entry for multihop and `direct_only` should fail"
    );
    {
        helpers::update_relay_constraints(&mut mullvad_client, |constraint| {
            constraint.location = Constraint::Only(daita_relay_location.clone().into());
            constraint.wireguard_constraints.entry_location =
                Constraint::Only(non_daita_relay_location.into());
            constraint.wireguard_constraints.use_multihop = true;
        })
        .await?;
        mullvad_client.set_daita_direct_only(true).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 { .. }
            | TunnelState::Disconnecting(ActionAfterDisconnect::Reconnect) => Ok(state),
            TunnelState::Error(state) => Err(Error::UnexpectedErrorState(state)),
            TunnelState::Disconnected { .. } => Err(Error::UnexpectedTunnelState(Box::new(state))),
            _ => return None,
        }),
        _ => 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::Connected { .. } => Some(Ok(state)),
            TunnelState::Connecting { .. } | TunnelState::Disconnecting(_) => None,
            TunnelState::Error(state) => Some(Err(Error::UnexpectedErrorState(state))),
            TunnelState::Disconnected { .. } => {
                Some(Err(Error::UnexpectedTunnelState(Box::new(state))))
            }
        },
        _ => None,
    })
    .await?
}