diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2021-01-06 12:53:19 +0000 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2021-01-11 12:32:09 +0000 |
| commit | b29cd20f62a3a9e12cdc4f5e46f752b723b84700 (patch) | |
| tree | e9f77af2c42b39faafd12c9b519b6bd545c677e2 | |
| parent | 866fc79a7337611854354fc9e9e54ea9c0817839 (diff) | |
| download | mullvadvpn-b29cd20f62a3a9e12cdc4f5e46f752b723b84700.tar.xz mullvadvpn-b29cd20f62a3a9e12cdc4f5e46f752b723b84700.zip | |
Query route for an IP to infer offline state
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | talpid-core/src/offline/linux.rs | 110 |
2 files changed, 57 insertions, 56 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8511aa6851..cda39a08d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ Line wrap the file at 100 chars. Th - Prefer the last used API endpoint when the service starts back up, as well as in other tools such as the problem report tool. +#### Linux +- Improve offline check to query the routing table to allow users to use a bridged adapter as their + primary interface. ## [2020.8-beta2] - 2020-12-11 This release is for desktop only. diff --git a/talpid-core/src/offline/linux.rs b/talpid-core/src/offline/linux.rs index c581b9b0d3..31a8de05db 100644 --- a/talpid-core/src/offline/linux.rs +++ b/talpid-core/src/offline/linux.rs @@ -3,17 +3,13 @@ use futures::{ channel::{mpsc::UnboundedSender, oneshot}, FutureExt, StreamExt, TryStreamExt, }; -use netlink_packet_route::{ - constants::{ARPHRD_LOOPBACK, ARPHRD_NONE, IFF_LOWER_UP, IFF_UP}, - rtnl::link::nlas::{Info as LinkInfo, InfoKind, Nla as LinkNla}, - LinkMessage, -}; +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, + Handle, IpVersion, }; -use std::{collections::BTreeSet, io, sync::Weak}; +use std::{io, net::Ipv4Addr, sync::Weak}; pub type Result<T> = std::result::Result<T, Error>; @@ -26,6 +22,9 @@ pub enum Error { #[error(display = "Failed to get list of IP addresses")] GetAddressesError(#[error(source)] failure::Compat<rtnetlink::Error>), + #[error(display = "Failed to get a route for an arbitrary IP address")] + GetRouteError(#[error(source)] failure::Compat<rtnetlink::Error>), + #[error(display = "Failed to connect to netlink socket")] NetlinkConnectionError(#[error(source)] io::Error), @@ -50,9 +49,13 @@ pub struct MonitorHandle { _stop_connection_tx: oneshot::Sender<()>, } +// Mullvad API's public IP address, correct at the time of writing, but any public IP address will +// work. +const PUBLIC_INTERNET_ADDRESS: Ipv4Addr = Ipv4Addr::new(193, 138, 218, 78); + impl MonitorHandle { pub async fn is_offline(&mut self) -> bool { - match check_offline_state(&self.handle).await { + match public_ip_unreachable(&self.handle).await { Ok(is_offline) => is_offline, Err(err) => { log::error!( @@ -86,7 +89,7 @@ pub async fn spawn_monitor(sender: Weak<UnboundedSender<TunnelCommand>>) -> Resu _ = stop_rx.fuse() => (), } }); - let mut is_offline = check_offline_state(&handle).await?; + let mut is_offline = public_ip_unreachable(&handle).await?; let monitor_handle = MonitorHandle { handle: handle.clone(), @@ -98,7 +101,7 @@ 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 = check_offline_state(&handle).await.unwrap_or(false); + let new_offline_state = public_ip_unreachable(&handle).await.unwrap_or(false); if new_offline_state != is_offline { is_offline = new_offline_state; let _ = sender.unbounded_send(TunnelCommand::IsOffline(is_offline)); @@ -113,63 +116,58 @@ pub async fn spawn_monitor(sender: Weak<UnboundedSender<TunnelCommand>>) -> Resu Ok(monitor_handle) } -async fn check_offline_state(handle: &Handle) -> Result<bool> { - let mut link_request = handle.link().get().execute(); - let mut links = BTreeSet::new(); - while let Some(link) = link_request - .try_next() - .await - .map_err(failure::Fail::compat) - .map_err(Error::GetLinksError)? - { - if link_provides_connectivity(&link) { - links.insert(link.header.index); - } - } - - if links.is_empty() { - return Ok(true); - } - - let mut address_request = handle.address().get().execute(); - while let Some(address) = address_request +async fn public_ip_unreachable(handle: &Handle) -> Result<bool> { + let mut request = handle.route().get(IpVersion::V4); + let message = request.message_mut(); + message + .nlas + .push(RouteNla::Mark(crate::linux::TUNNEL_FW_MARK)); + message.nlas.push(RouteNla::Destination( + 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::GetAddressesError)? + .map_err(Error::GetRouteError)? { - if links.contains(&address.header.index) { - return Ok(false); + for nla in message.nlas.iter() { + if let RouteNla::Gateway(_) | RouteNla::Oif(_) = nla { + return Ok(false); + } } } Ok(true) } +#[cfg(test)] +mod test { + use super::*; + use rtnetlink::{ + constants::{RTMGRP_IPV4_IFADDR, RTMGRP_IPV6_IFADDR, RTMGRP_LINK, RTMGRP_NOTIFY}, + sys::SocketAddr, + }; -// TODO: Improve by allowing bridge links to provide connectivity, will require route checking. -fn link_provides_connectivity(link: &LinkMessage) -> bool { - // Some tunnels have the link layer type set to None - link.header.link_layer_type != ARPHRD_NONE - && link.header.link_layer_type != ARPHRD_LOOPBACK - && (link.header.flags & IFF_UP > 0 || link.header.flags & IFF_LOWER_UP > 0) - && !is_virtual_interface(link) -} + #[test] + fn test_route_table_query() { + let mut runtime = tokio::runtime::Runtime::new().expect("failed to initialize runtime"); + let (mut connection, handle, _) = runtime.block_on(async { + rtnetlink::new_connection() + .map_err(Error::NetlinkConnectionError) + .expect("Failed to create a netlink connection") + }); -fn is_virtual_interface(link: &LinkMessage) -> bool { - for nla in link.nlas.iter() { - if let LinkNla::Info(info_nlas) = nla { - for info in info_nlas.iter() { - // LinkInfo::Kind seems to only be set when the link is actually virtual - if let LinkInfo::Kind(ref kind) = info { - use InfoKind::*; - return match kind { - Dummy | Bridge | Tun | Nlmon | IpTun => true, - _ => false, - }; - } - } - } + let mgroup_flags = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR | RTMGRP_LINK | RTMGRP_NOTIFY; + let addr = SocketAddr::new(0, mgroup_flags); + + connection.socket_mut().bind(&addr).unwrap(); + runtime.spawn(connection); + + runtime + .block_on(public_ip_unreachable(&handle)) + .expect("Failed to query routing table"); } - false } |
