diff options
| author | Emīls <emils@mullvad.net> | 2021-03-19 11:02:54 +0000 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2021-03-19 11:02:54 +0000 |
| commit | 6ef4d1ac540c7bcd908533cc906af3e5f78faac4 (patch) | |
| tree | fe17c69a5b445340e4c2c4b0c4b7a16c7a7439bf | |
| parent | 77621effb0011fc5347ad9ea02c2e70f8651deaf (diff) | |
| parent | e7a6569442cf2a194e5f57f7a7c3669e999e2d13 (diff) | |
| download | mullvadvpn-6ef4d1ac540c7bcd908533cc906af3e5f78faac4.tar.xz mullvadvpn-6ef4d1ac540c7bcd908533cc906af3e5f78faac4.zip | |
Merge branch 'linux-improve-offline-detection'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | talpid-core/src/offline/linux.rs | 79 |
2 files changed, 59 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d16858d72b..99f5baca0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Line wrap the file at 100 chars. Th - Stop using NM for managing DNS if it's newer than 1.26. - Fix DNS issues where NM would overwrite Mullvad tunnel's DNS config in systemd-resolved. - Fix issues with hosts where the firewall is doing reverse path filtering. +- Further improve offline monitor to properly receive `ENETUNREACH`. #### Android - Fix input area sometimes disappearing when returning to the Login screen. diff --git a/talpid-core/src/offline/linux.rs b/talpid-core/src/offline/linux.rs index b8ac8f221d..fbdf2d8700 100644 --- a/talpid-core/src/offline/linux.rs +++ b/talpid-core/src/offline/linux.rs @@ -1,23 +1,30 @@ use crate::tunnel_state_machine::TunnelCommand; use futures::{ channel::{mpsc::UnboundedSender, oneshot}, - FutureExt, StreamExt, TryStreamExt, + FutureExt, StreamExt, TryStream, TryStreamExt, +}; +use netlink_packet_core::{NetlinkPayload, NLM_F_REQUEST}; +use netlink_packet_route::{ + rtnl::route::nlas::Nla as RouteNla, NetlinkMessage, RouteFlags, RouteMessage, RtnlMessage, }; -use netlink_packet_route::rtnl::route::nlas::Nla as RouteNla; use rtnetlink::{ constants::{RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR, RTMGRP_LINK, RTMGRP_NOTIFY}, sys::SocketAddr, Handle, IpVersion, }; use std::{io, net::Ipv4Addr, sync::Weak}; +use talpid_types::ErrorExt; pub type Result<T> = std::result::Result<T, Error>; #[derive(err_derive::Error, Debug)] #[error(no_from)] pub enum Error { - #[error(display = "Failed to get list of IP links")] - GetLinksError(#[error(source)] failure::Compat<rtnetlink::Error>), + #[error(display = "Failed to resolve output interface index")] + GetLinkError(#[error(source)] failure::Compat<rtnetlink::Error>), + + #[error(display = "No netlink response for output interface query")] + NoLinkError, #[error(display = "Failed to get list of IP addresses")] GetAddressesError(#[error(source)] failure::Compat<rtnetlink::Error>), @@ -25,6 +32,9 @@ pub enum Error { #[error(display = "Failed to get a route for an arbitrary IP address")] GetRouteError(#[error(source)] failure::Compat<rtnetlink::Error>), + #[error(display = "No netlink response for route query")] + NoRouteError, + #[error(display = "Failed to connect to netlink socket")] NetlinkConnectionError(#[error(source)] io::Error), @@ -101,7 +111,14 @@ pub async fn spawn_monitor(sender: Weak<UnboundedSender<TunnelCommand>>) -> Resu while let Some(_new_message) = messages.next().await { match sender.upgrade() { Some(sender) => { - let new_offline_state = public_ip_unreachable(&handle).await.unwrap_or(false); + let new_offline_state = + public_ip_unreachable(&handle).await.unwrap_or_else(|err| { + log::error!( + "{}", + err.display_chain_with_msg("Failed to infer offline state") + ); + false + }); if new_offline_state != is_offline { is_offline = new_offline_state; let _ = sender.unbounded_send(TunnelCommand::IsOffline(is_offline)); @@ -112,7 +129,6 @@ pub async fn spawn_monitor(sender: Weak<UnboundedSender<TunnelCommand>>) -> Resu } }); - Ok(monitor_handle) } @@ -127,23 +143,44 @@ async fn public_ip_unreachable(handle: &Handle) -> Result<bool> { PUBLIC_INTERNET_ADDRESS.octets().to_vec(), )); message.header.destination_prefix_length = 32; - let mut stream = request.execute(); - while let Some(message) = stream - .try_next() - .await - .map_err(failure::Fail::compat) - .map_err(Error::GetRouteError)? - { - for nla in message.nlas.iter() { - if message.header.table != libc::RT_TABLE_MAIN { - continue; - } - if let RouteNla::Gateway(_) | RouteNla::Oif(_) = nla { - return Ok(false); - } + message.header.flags = RouteFlags::RTM_F_LOOKUP_TABLE; + let mut stream = execute_route_get_request(handle.clone(), message.clone()); + match stream.try_next().await { + // Presance of any route implies connectivity, even if it's a loopback route + Ok(Some(_)) => Ok(false), + Ok(None) => Err(Error::NoRouteError), + // ENETUNREACH implies that there exists no route that'd reach our random API address, + // as such, the host is assumed to be offline + Err(rtnetlink::Error::NetlinkError(nl_err)) if nl_err.code == -libc::ENETUNREACH => { + Ok(true) } + Err(err) => Err(Error::GetRouteError(failure::Fail::compat(err))), + } +} + +pub fn execute_route_get_request( + mut handle: Handle, + message: RouteMessage, +) -> impl TryStream<Ok = RouteMessage, Error = rtnetlink::Error> { + use futures::future::{self, Either}; + use rtnetlink::Error; + + let mut req = NetlinkMessage::from(RtnlMessage::GetRoute(message)); + req.header.flags = NLM_F_REQUEST; + + match handle.request(req) { + Ok(response) => Either::Left(response.map(move |msg| { + let (header, payload) = msg.into_parts(); + match payload { + NetlinkPayload::InnerMessage(RtnlMessage::NewRoute(msg)) => Ok(msg), + NetlinkPayload::Error(err) => Err(Error::NetlinkError(err)), + _ => Err(Error::UnexpectedMessage(NetlinkMessage::new( + header, payload, + ))), + } + })), + Err(e) => Either::Right(future::err::<RouteMessage, Error>(e).into_stream()), } - Ok(true) } #[cfg(test)] |
