summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src/geoip.rs
blob: 09d52ba824a2f7b86a64086ba8980736d061f9f8 (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
use std::time::Duration;

use futures::join;
use mullvad_api::rest::{Error, RequestServiceHandle};
use mullvad_types::location::{AmIMullvad, GeoIpLocation, LocationEventData};
use std::sync::LazyLock;
use talpid_core::mpsc::Sender;
use talpid_future::retry::{ExponentialBackoff, Jittered, retry_future};
use talpid_types::ErrorExt;

use crate::{DaemonEventSender, InternalDaemonEvent};

// Define the Mullvad connection checking api endpoint.
//
// In a development build the host name for the connection checking endpoint can
// be overriden by defining the env variable `MULLVAD_CONNCHECK_HOST`.
//
// If `MULLVAD_CONNCHECK_HOST` is set when running `mullvad-daemon` in a
// production build, a warning will be logged and the env variable *won´t* have
// any effect on the api call. The default host name `am.i.mullvad.net` will
// always be used in release mode.
static MULLVAD_CONNCHECK_HOST: LazyLock<String> = LazyLock::new(|| {
    const DEFAULT_CONNCHECK_HOST: &str = "am.i.mullvad.net";
    let conncheck_host_var = std::env::var("MULLVAD_CONNCHECK_HOST").ok();
    let host = if cfg!(feature = "api-override") {
        match conncheck_host_var.as_deref() {
            Some(host) => {
                log::debug!("Overriding conncheck endpoint. Using {}", &host);
                host
            }
            None => DEFAULT_CONNCHECK_HOST,
        }
    } else {
        if conncheck_host_var.is_some() {
            log::warn!("These variables are ignored in production builds: MULLVAD_CONNCHECK_HOST");
        };
        DEFAULT_CONNCHECK_HOST
    };
    host.to_string()
});

const LOCATION_RETRY_STRATEGY: Jittered<ExponentialBackoff> =
    Jittered::jitter(ExponentialBackoff::new(Duration::from_secs(1), 4));

/// Handler for request to am.i.mullvad.net, manages in-flight request and validity of responses.
pub(crate) struct GeoIpHandler {
    /// Unique ID for each request. If the ID attached to the
    /// [`InternalDaemonEvent::LocationEvent`] used by [`crate::Daemon::handle_location_event`] to
    /// determine if the location belongs to the current tunnel state.
    pub request_id: usize,
    rest_service: RequestServiceHandle,
    location_sender: DaemonEventSender,
}

impl GeoIpHandler {
    pub fn new(rest_service: RequestServiceHandle, location_sender: DaemonEventSender) -> Self {
        Self {
            request_id: 0,
            rest_service,
            location_sender,
        }
    }

    /// Send a location request to am.i.mullvad.net. When it arrives, send an
    /// [`InternalDaemonEvent::LocationEvent`], which triggers an update of the current
    /// tunnel state with the `ipv4` and/or `ipv6` fields filled in.
    pub fn send_geo_location_request(&mut self, use_ipv6: bool) {
        // Increment request ID
        self.request_id = self.request_id.wrapping_add(1);

        self.abort_current_request();

        let request_id = self.request_id;
        let rest_service = self.rest_service.clone();
        let location_sender = self.location_sender.clone();
        tokio::spawn(async move {
            if let Ok(location) = get_geo_location_with_retry(use_ipv6, rest_service).await {
                let _ =
                    location_sender.send(InternalDaemonEvent::LocationEvent(LocationEventData {
                        request_id,
                        location,
                    }));
            }
        });
    }

    /// Abort any ongoing call to am.i.mullvad.net
    pub fn abort_current_request(&mut self) {
        self.rest_service.reset();
    }
}

/// Fetch the current `GeoIpLocation` from am.i.mullvad.net. Handles retries on network errors.
async fn get_geo_location_with_retry(
    use_ipv6: bool,
    rest_service: RequestServiceHandle,
) -> Result<GeoIpLocation, Error> {
    log::debug!("Fetching GeoIpLocation");
    retry_future(
        move || send_location_request(rest_service.clone(), use_ipv6),
        move |result| match result {
            Err(error) => error.is_network_error(),
            _ => false,
        },
        LOCATION_RETRY_STRATEGY,
    )
    .await
}

async fn send_location_request(
    request_sender: RequestServiceHandle,
    use_ipv6: bool,
) -> Result<GeoIpLocation, Error> {
    let v4_sender = request_sender.clone();
    let v4_future = async move {
        let uri_v4 = format!("https://ipv4.{}/json", *MULLVAD_CONNCHECK_HOST);
        let location = send_location_request_internal(&uri_v4, v4_sender).await?;
        Ok::<GeoIpLocation, Error>(GeoIpLocation::from(location))
    };
    let v6_sender = request_sender.clone();
    let v6_future = async move {
        if use_ipv6 {
            let uri_v6 = format!("https://ipv6.{}/json", *MULLVAD_CONNCHECK_HOST);
            let location = send_location_request_internal(&uri_v6, v6_sender).await;
            Some(location.map(GeoIpLocation::from))
        } else {
            None
        }
    };

    let (v4_result, v6_result) = join!(v4_future, v6_future);

    match (v4_result, v6_result) {
        (Ok(mut v4), Some(Ok(v6))) => {
            v4.ipv6 = v6.ipv6;
            v4.mullvad_exit_ip = v4.mullvad_exit_ip && v6.mullvad_exit_ip;
            Ok(v4)
        }
        (Ok(v4), None) => Ok(v4),
        (Ok(v4), Some(Err(e))) => {
            log_network_error(e, "IPv6");
            Ok(v4)
        }
        (Err(e), Some(Ok(v6))) => {
            log_network_error(e, "IPv4");
            Ok(v6)
        }
        (Err(e_v4), _) => Err(e_v4),
    }
}

async fn send_location_request_internal(
    uri: &str,
    service: RequestServiceHandle,
) -> Result<AmIMullvad, Error> {
    let future_service = service.clone();
    let request = mullvad_api::rest::get(uri)?;
    future_service.request(request).await?.deserialize().await
}

fn log_network_error(err: Error, version: &'static str) {
    if !err.is_offline() {
        let err_message = &format!("Unable to fetch {version} GeoIP location");
        log::debug!("{}", err.display_chain_with_msg(err_message));
    }
}