summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src/access_method.rs
blob: 7d9d3dba95d0d2cd44ac453dd8fc6b33fe739fa6 (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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
use crate::{
    api::{self, AccessModeSelectorHandle},
    settings::{self, MadeChanges},
    Daemon, EventListener,
};
use mullvad_api::rest::{self, MullvadRestHandle};
use mullvad_types::{
    access_method::{self, AccessMethod, AccessMethodSetting},
    settings::Settings,
};

#[derive(err_derive::Error, Debug)]
pub enum Error {
    /// Can not add access method
    #[error(display = "Cannot add custom access method")]
    Add,
    /// Can not remove built-in access method
    #[error(display = "Cannot remove built-in access method")]
    RemoveBuiltIn,
    /// Can not find access method
    #[error(display = "Cannot find custom access method {}", _0)]
    NoSuchMethod(access_method::Id),
    /// Access method could not be rotate
    #[error(display = "Access method could not be rotated")]
    RotationError,
    /// Some error occured in the daemon's state of handling
    /// [`AccessMethodSetting`]s & [`ApiConnectionMode`]s.
    #[error(display = "Error occured when handling connection settings & details")]
    ConnectionMode(#[error(source)] api::Error),
    #[error(display = "API endpoint rotation failed")]
    RestError(#[error(source)] rest::Error),
    /// Access methods settings error
    #[error(display = "Settings error")]
    Settings(#[error(source)] settings::Error),
}

/// A tiny datastructure used for signaling whether the daemon should force a
/// rotation of the currently used [`AccessMethodSetting`] or not, and if so:
/// how it should do it.
pub enum Command {
    /// There is no need to force a rotation of [`AccessMethodSetting`]
    Nothing,
    /// Select the next available [`AccessMethodSetting`], whichever that is
    Rotate,
    /// Select the [`AccessMethodSetting`] with a certain [`access_method::Id`]
    Set(access_method::Id),
}

impl<L> Daemon<L>
where
    L: EventListener + Clone + Send + 'static,
{
    /// Add a [`AccessMethod`] to the daemon's settings.
    ///
    /// If the daemon settings are successfully updated, the
    /// [`access_method::Id`] of the newly created [`AccessMethodSetting`]
    /// (which has been derived from the [`AccessMethod`]) is returned.
    pub async fn add_access_method(
        &mut self,
        name: String,
        enabled: bool,
        access_method: AccessMethod,
    ) -> Result<access_method::Id, Error> {
        let access_method_setting = AccessMethodSetting::new(name, enabled, access_method);
        let id = access_method_setting.get_id();
        self.settings
            .update(|settings| settings.api_access_methods.append(access_method_setting))
            .await
            .map(|did_change| self.notify_on_change(did_change))
            .map(|_| id)
            .map_err(Error::Settings)
    }

    /// Remove a [`AccessMethodSetting`] from the daemon's saved settings.
    ///
    /// If the [`AccessMethodSetting`] which is currently in use happens to be
    /// removed, the daemon should force a rotation of the active API endpoint.
    pub async fn remove_access_method(
        &mut self,
        access_method: access_method::Id,
    ) -> Result<(), Error> {
        // Make sure that we are not trying to remove a built-in API access
        // method
        let command = match self.settings.api_access_methods.find(&access_method) {
            Some(api_access_method) => {
                if api_access_method.is_builtin() {
                    Err(Error::RemoveBuiltIn)
                } else if api_access_method.get_id()
                    == self.get_current_access_method().await?.get_id()
                {
                    Ok(Command::Rotate)
                } else {
                    Ok(Command::Nothing)
                }
            }
            None => Ok(Command::Nothing),
        }?;

        self.settings
            .update(|settings| settings.api_access_methods.remove(&access_method))
            .await
            .map(|did_change| self.notify_on_change(did_change))
            .map_err(Error::Settings)?
            .process_command(command)
            .await
    }

    /// Set a [`AccessMethodSetting`] as the current API access method.
    ///
    /// If successful, the daemon will force a rotation of the active API access
    /// method, which means that subsequent API calls will use the new
    /// [`AccessMethodSetting`] to figure out the API endpoint.
    pub async fn set_api_access_method(
        &mut self,
        access_method: access_method::Id,
    ) -> Result<(), Error> {
        let access_method = self.get_api_access_method(access_method)?;
        self.connection_modes_handler
            .set_access_method(access_method)
            .await?;
        // Force a rotation of Access Methods.
        //
        // This is not a call to `process_command` due to the restrictions on
        // recursively calling async functions.
        self.force_api_endpoint_rotation().await
    }

    pub fn get_api_access_method(
        &mut self,
        access_method: access_method::Id,
    ) -> Result<AccessMethodSetting, Error> {
        self.settings
            .api_access_methods
            .find(&access_method)
            .ok_or(Error::NoSuchMethod(access_method))
            .cloned()
    }

    /// "Updates" an [`AccessMethodSetting`] by replacing the existing entry
    /// with the argument `access_method_update` if an existing entry with
    /// matching [`access_method::Id`] is found.
    ///
    /// If the currently active [`AccessMethodSetting`] is updated, the daemon
    /// will automatically use this updated [`AccessMethodSetting`] when
    /// performing subsequent API calls.
    pub async fn update_access_method(
        &mut self,
        access_method_update: AccessMethodSetting,
    ) -> Result<(), Error> {
        // We have to be a bit careful. If we are about to disable the last
        // remaining enabled access method, we would cause an inconsistent state
        // in the daemon's settings. Therefore, we have to safeguard against
        // this by explicitly checking for & disallow any update which would
        // cause the last enabled access method to become disabled.
        let current = self.get_current_access_method().await?;
        let mut command = Command::Nothing;
        let settings_update = |settings: &mut Settings| {
            if let Some(access_method) = settings
                .api_access_methods
                .find_mut(&access_method_update.get_id())
            {
                *access_method = access_method_update;
                if access_method.get_id() == current.get_id() {
                    command = Command::Set(access_method.get_id())
                }
            }
        };

        self.settings
            .update(settings_update)
            .await
            .map(|did_change| self.notify_on_change(did_change))
            .map_err(Error::Settings)?
            .process_command(command)
            .await
    }

    /// Return the [`AccessMethodSetting`] which is currently used to access the
    /// Mullvad API.
    pub async fn get_current_access_method(&self) -> Result<AccessMethodSetting, Error> {
        Ok(self.connection_modes_handler.get_access_method().await?)
    }

    /// Change which [`AccessMethodSetting`] which will be used to figure out
    /// the Mullvad API endpoint.
    async fn force_api_endpoint_rotation(&self) -> Result<(), Error> {
        self.api_handle
            .service()
            .next_api_endpoint()
            .await
            .map_err(|error| {
                log::error!("Failed to rotate API endpoint: {}", error);
                Error::RotationError
            })
    }

    /// If settings were changed due to an update, notify all listeners.
    fn notify_on_change(&mut self, settings_changed: MadeChanges) -> &mut Self {
        if settings_changed {
            self.event_listener
                .notify_settings(self.settings.to_settings());

            let handle = self.connection_modes_handler.clone();
            let new_access_methods = self.settings.api_access_methods.collect_enabled();
            tokio::spawn(async move {
                match handle.update_access_methods(new_access_methods).await {
                    Ok(_) => (),
                    Err(api::Error::NoAccessMethods) | Err(_) => {
                        // `access_methods` was empty! This implies that the user
                        // disabled all access methods. If we ever get into this
                        // state, we should default to using the direct access
                        // method.
                        let default = access_method::Settings::direct();
                        handle.update_access_methods(vec![default]).await.expect("Failed to create the data structure responsible for managing access methods");
                    }
                }
            });
        };
        self
    }

    /// The semantics of the [`Command`] datastructure.
    async fn process_command(&mut self, command: Command) -> Result<(), Error> {
        match command {
            Command::Nothing => Ok(()),
            Command::Rotate => self.force_api_endpoint_rotation().await,
            Command::Set(id) => self.set_api_access_method(id).await,
        }
    }
}

/// Try to reach the Mullvad API using a specific access method, returning
/// an [`Error`] in the case where the test fails to reach the API.
///
/// Ephemerally sets a new access method (associated with `access_method`)
/// to be used for subsequent API calls, before performing an API call and
/// switching back to the previously active access method. The previous
/// access method is *always* reset.
pub async fn test_access_method(
    new_access_method: AccessMethodSetting,
    access_mode_selector: AccessModeSelectorHandle,
    rest_handle: MullvadRestHandle,
) -> Result<bool, Error> {
    // Setup test
    let previous_access_method = access_mode_selector
        .get_access_method()
        .await
        .map_err(Error::ConnectionMode)?;

    let method_under_test = new_access_method.clone();
    access_mode_selector
        .set_access_method(new_access_method)
        .await
        .map_err(Error::ConnectionMode)?;

    // We need to perform a rotation of API endpoint after a set action
    let rotation_handle = rest_handle.clone();
    rotation_handle
        .service()
        .next_api_endpoint()
        .await
        .map_err(|err| {
            log::error!("Failed to rotate API endpoint: {err}");
            Error::RestError(err)
        })?;

    // Set up the reset
    //
    // In case the API call fails, the next API endpoint will
    // automatically be selected, which means that we need to set up
    // with the previous API endpoint beforehand.
    access_mode_selector
        .set_access_method(previous_access_method)
        .await
        .map_err(|err| {
            log::error!(
                "Could not reset to previous access
            method after API reachability test was carried out. This should only
            happen if the previous access method was removed in the meantime."
            );
            Error::ConnectionMode(err)
        })?;

    // Perform test
    //
    // Send a HEAD request to some Mullvad API endpoint. We issue a HEAD
    // request because we are *only* concerned with if we get a reply from
    // the API, and not with the actual data that the endpoint returns.
    let result = mullvad_api::ApiProxy::new(rest_handle)
        .api_addrs_available()
        .await
        .map_err(Error::RestError)?;

    // We need to perform a rotation of API endpoint after a set action
    // Note that this will be done automatically if the API call fails,
    // so it only has to be done if the call succeeded ..
    if result {
        rotation_handle
            .service()
            .next_api_endpoint()
            .await
            .map_err(|err| {
                log::error!("Failed to rotate API endpoint: {err}");
                Error::RestError(err)
            })?;
    }

    log::info!(
        "The result of testing {method:?} is {result}",
        method = method_under_test.access_method,
        result = if result {
            "success".to_string()
        } else {
            "failed".to_string()
        }
    );

    Ok(result)
}