From c0b066e85707732e9a543197dad7012a22353aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Tue, 3 Oct 2023 17:18:43 +0200 Subject: [PATCH] Parse ipconfig instead of the routing table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Lönnhager --- talpid-routing/Cargo.toml | 2 +- talpid-routing/src/unix/macos/data.rs | 5 + talpid-routing/src/unix/macos/interface.rs | 215 +++++++++++++++------ talpid-routing/src/unix/macos/mod.rs | 145 ++++++-------- 4 files changed, 216 insertions(+), 151 deletions(-) diff --git a/talpid-routing/Cargo.toml b/talpid-routing/Cargo.toml index 223182947539..50bd4c1477a2 100644 --- a/talpid-routing/Cargo.toml +++ b/talpid-routing/Cargo.toml @@ -14,7 +14,7 @@ err-derive = { workspace = true } futures = "0.3.15" ipnetwork = "0.16" log = { workspace = true } -tokio = { workspace = true, features = ["process", "rt-multi-thread", "net"] } +tokio = { workspace = true, features = ["process", "rt-multi-thread", "net", "io-util", "time"] } [target.'cfg(not(target_os="android"))'.dependencies] talpid-types = { path = "../talpid-types" } diff --git a/talpid-routing/src/unix/macos/data.rs b/talpid-routing/src/unix/macos/data.rs index 16981ee5f64e..f0be4186bc2d 100644 --- a/talpid-routing/src/unix/macos/data.rs +++ b/talpid-routing/src/unix/macos/data.rs @@ -321,6 +321,11 @@ impl RouteMessage { self.interface_index } + pub fn set_interface_index(mut self, index: u16) -> Self { + self.interface_index = index; + self + } + pub fn interface_address(&self) -> Option { self.get_address(&AddressFlag::RTA_IFA) } diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs index 761c0f870bc7..04123ca36ef5 100644 --- a/talpid-routing/src/unix/macos/interface.rs +++ b/talpid-routing/src/unix/macos/interface.rs @@ -1,21 +1,22 @@ use ipnetwork::IpNetwork; -use libc::{if_indextoname, IFNAMSIZ}; -use nix::net::if_::{if_nametoindex, InterfaceFlags}; +use nix::{ + net::if_::{if_nametoindex, InterfaceFlags}, + sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}, +}; use std::{ - ffi::{CStr, CString}, + collections::BTreeMap, + ffi::CString, io, - net::{Ipv4Addr, Ipv6Addr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; + use system_configuration::{ core_foundation::string::CFString, network_configuration::{SCNetworkService, SCNetworkSet}, preferences::SCPreferences, }; -use super::{ - data::{Destination, RouteMessage}, - watch::RoutingTable, -}; +use super::data::{Destination, RouteMessage}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum Family { @@ -41,48 +42,6 @@ impl From for IpNetwork { } } -/// Retrieve the current unscoped default route. That is the only default route that does not have -/// the IF_SCOPE flag set, if such a route exists. -/// -/// # Note -/// -/// For some reason, the socket sometimes returns a route with the IF_SCOPE flag set, if there also -/// exists a scoped route for the same interface. This does not occur if there is no unscoped route, -/// so we can still rely on it. -pub async fn get_unscoped_default_route( - routing_table: &mut RoutingTable, - family: Family, -) -> Option { - let mut msg = RouteMessage::new_route(Destination::Network(IpNetwork::from(family))); - msg = msg.set_gateway_route(true); - - let route = routing_table - .get_route(&msg) - .await - .unwrap_or_else(|error| { - log::error!("Failed to retrieve unscoped default route: {error}"); - None - })?; - - let idx = u32::from(route.interface_index()); - if idx != 0 { - let mut ifname = [0u8; IFNAMSIZ]; - - // SAFETY: The buffer is large to contain any interface name. - if !unsafe { if_indextoname(idx, ifname.as_mut_ptr() as _) }.is_null() { - let ifname = CStr::from_bytes_until_nul(&ifname).unwrap(); - let name = ifname.to_str().expect("expected ascii"); - - // Ignore the unscoped route if its interface is not "active" - if !is_active_interface(name, family).unwrap_or(true) { - return None; - } - } - } - - Some(route) -} - /// Retrieve the best current default route. That is the first scoped default route, ordered by /// network service order, and with interfaces filtered out if they do not have valid IP addresses /// assigned. @@ -90,14 +49,15 @@ pub async fn get_unscoped_default_route( /// # Note /// /// The tunnel interface is not even listed in the service order, so it will be skipped. -pub async fn get_best_default_route( - routing_table: &mut RoutingTable, - family: Family, -) -> Option { +pub async fn get_best_default_route(family: Family) -> Option { let mut msg = RouteMessage::new_route(Destination::Network(IpNetwork::from(family))); msg = msg.set_gateway_route(true); for iface in network_service_order() { + let Ok(Some(router_addr)) = get_router_address(family, &iface).await else { + continue; + }; + let iface_bytes = match CString::new(iface.as_bytes()) { Ok(name) => name, Err(error) => { @@ -107,25 +67,79 @@ pub async fn get_best_default_route( }; // Get interface ID - let index = match if_nametoindex(iface_bytes.as_c_str()) { - Ok(index) => index, - Err(_error) => { - continue; - } + let Ok(index) = if_nametoindex(iface_bytes.as_c_str()) else { + continue; }; // Request ifscoped default route for this interface - let route_msg = msg.clone().set_ifscope(u16::try_from(index).unwrap()); - if let Ok(Some(route)) = routing_table.get_route(&route_msg).await { - if is_active_interface(&iface, family).unwrap_or(true) { - return Some(route); - } + let route_msg = msg + .clone() + .set_gateway_addr(router_addr) + .set_interface_index(u16::try_from(index).unwrap()); + if is_active_interface(&iface, family).unwrap_or(true) { + return Some(route_msg); } } None } +/// Return a map from interface name to link addresses (AF_LINK) +pub fn get_interface_link_addresses() -> io::Result> { + let mut gateway_link_addrs = BTreeMap::new(); + let addrs = nix::ifaddrs::getifaddrs()?; + for addr in addrs.into_iter() { + if addr.address.and_then(|addr| addr.family()) != Some(AddressFamily::Link) { + continue; + } + gateway_link_addrs.insert(addr.interface_name, addr.address.unwrap()); + } + Ok(gateway_link_addrs) +} + +async fn get_router_address(family: Family, interface_name: &str) -> io::Result> { + let output = tokio::process::Command::new("ipconfig") + .arg("getsummary") + .arg(interface_name) + .output() + .await? + .stdout; + + let Ok(output_str) = std::str::from_utf8(&output) else { + return Ok(None); + }; + + match family { + Family::V4 => Ok(parse_v4_ipconfig_output(output_str)), + Family::V6 => Ok(parse_v6_ipconfig_output(output_str)), + } +} + +fn parse_v4_ipconfig_output(output: &str) -> Option { + let mut iter = output.split_whitespace(); + loop { + let next_chunk = iter.next()?; + if next_chunk == "Router" && iter.next()? == ":" { + return iter.next()?.parse().ok(); + } + } +} + +fn parse_v6_ipconfig_output(output: &str) -> Option { + let mut iter = output.split_whitespace(); + let pattern = ["RouterAdvertisement", ":", "from"]; + 'outer: loop { + let mut next_chunk = iter.next()?; + for expected_chunk in pattern { + if expected_chunk != next_chunk { + continue 'outer; + } + next_chunk = iter.next()?; + } + return next_chunk.trim_end_matches(",").parse().ok(); + } +} + fn network_service_order() -> Vec { let prefs = SCPreferences::default(&CFString::new("talpid-routing")); let services = SCNetworkService::get_services(&prefs); @@ -176,3 +190,76 @@ fn is_routable_v6(addr: &Ipv6Addr) -> bool { // !(link local) && (addr.segments()[0] & 0xffc0) != 0xfe80 } + +#[cfg(test)] +const TEST_IPCONFIG_OUTPUT: &str = " { + Hashed-BSSID : 86:a2:7a:bb:7c:5c + IPv4 : { + 0 : { + Addresses : { + 0 : 192.168.1.3 + } + ChildServiceID : LINKLOCAL-en0 + ConfigMethod : Manual + IsPublished : TRUE + ManualAddress : 192.168.1.3 + ManualSubnetMask : 255.255.255.0 + Router : 192.168.1.1 + RouterARPVerified : TRUE + ServiceID : 400B48FB-2585-41DF-8459-30C5C6D5621C + SubnetMasks : { + 0 : 255.255.255.0 + } + } + 1 : { + ConfigMethod : LinkLocal + IsPublished : TRUE + ParentServiceID : 400B48FB-2585-41DF-8459-30C5C6D5621C + ServiceID : LINKLOCAL-en0 + } + } + IPv6 : { + 0 : { + ConfigMethod : Automatic + DHCPv6 : { + ElapsedTime : 2200 + Mode : Stateful + State : Solicit + } + IsPublished : TRUE + RTADV : { + RouterAdvertisement : from fe80::5aef:68ff:fe0d:18db, length 88, hop limit 0, lifetime 1800s, reacha +ble 0ms, retransmit 0ms, flags 0xc4=[ managed other proxy ], pref=medium + source link-address option (1), length 8 (1): 58:ef:68:0d:18:db + prefix info option (3), length 32 (4): ::/64, Flags [ onlink ], valid time 2592000s, pref. time 604 +800s + prefix info option (3), length 32 (4): 2a03:1b20:5:7::/64, Flags [ onlink auto ], valid time 259200 +0s, pref. time 604800s + + State : Acquired + } + ServiceID : 400B48FB-2585-41DF-8459-30C5C6D5621C + } + } + InterfaceType : WiFi + LinkStatusActive : TRUE + NetworkID : 350BCC68-6D65-4D4A-9187-264D7B543738 + SSID : app-team-lab + Security : WPA2_PSK +}"; + +#[test] +fn test_parsing_v4_ipconfig_output() { + assert_eq!( + parse_v4_ipconfig_output(&TEST_IPCONFIG_OUTPUT).unwrap(), + "192.168.1.1".parse::().unwrap() + ) +} + +#[test] +fn test_parsing_v6_ipconfig_output() { + assert_eq!( + parse_v6_ipconfig_output(&TEST_IPCONFIG_OUTPUT).unwrap(), + "fe80::5aef:68ff:fe0d:18db".parse::().unwrap() + ) +} diff --git a/talpid-routing/src/unix/macos/mod.rs b/talpid-routing/src/unix/macos/mod.rs index c249306b228c..c8ef2cbf3a07 100644 --- a/talpid-routing/src/unix/macos/mod.rs +++ b/talpid-routing/src/unix/macos/mod.rs @@ -6,12 +6,12 @@ use futures::{ stream::{FusedStream, StreamExt}, }; use ipnetwork::IpNetwork; -use nix::sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}; +use std::sync::Weak; use std::{ collections::{BTreeMap, HashSet}, + pin::Pin, time::Duration, }; -use std::{pin::Pin, sync::Weak}; use talpid_types::ErrorExt; use watch::RoutingTable; @@ -43,15 +43,11 @@ pub enum Error { /// Failed to fetch link addresses #[error(display = "Failed to fetch link addresses")] - FetchLinkAddresses(nix::Error), + FetchLinkAddresses(#[error(source)] std::io::Error), /// Received message isn't valid #[error(display = "Invalid data")] InvalidData(data::Error), - - /// Restoring unscoped default routes - #[error(display = "Restoring unscoped default routes")] - RestoringUnscopedRoutes, } /// Convenience macro to get the current default route. Macro because I don't want to borrow `self` @@ -129,6 +125,7 @@ impl RouteManagerImpl { "{}", error.display_chain_with_msg("Failed to get initial default v4 route") ); + false }); self.update_best_default_route(interface::Family::V6) .await @@ -137,6 +134,7 @@ impl RouteManagerImpl { "{}", error.display_chain_with_msg("Failed to get initial default v6 route") ); + false }); let mut completion_tx = None; @@ -198,23 +196,16 @@ impl RouteManagerImpl { Some(RouteManagerCommand::AddRoutes(routes, tx)) => { if !self.check_default_routes_restored.is_terminated() { - // Give it some time to recover, but not too much - if !self.try_restore_default_routes().await { - let _ = tokio::time::timeout( - Duration::from_millis(500), - self.check_default_routes_restored.next(), - ).await; - - if !self.try_restore_default_routes().await { - log::warn!("Unscoped routes were not restored"); - let _ = tx.send(Err(Error::RestoringUnscopedRoutes)); - continue; - } - } + log::debug!("Cancelling restoration of default routes"); self.check_default_routes_restored = Box::pin(futures::stream::pending()); - log::debug!("Unscoped routes were already restored"); } + // Reset known best route + let _ = self.update_best_default_route(interface::Family::V4) + .await; + let _ = self.update_best_default_route(interface::Family::V6) + .await; + log::debug!("Adding routes: {routes:?}"); let _ = tx.send(self.add_required_routes(routes).await); } @@ -261,7 +252,8 @@ impl RouteManagerImpl { } // Map all interfaces to their link addresses - let interface_link_addrs = get_interface_link_addresses()?; + let interface_link_addrs = + interface::get_interface_link_addresses().map_err(Error::FetchLinkAddresses)?; // Add routes not using the default interface for route in routes_to_apply { @@ -365,7 +357,9 @@ impl RouteManagerImpl { self.apply_tunnel_default_route().await?; // Update routes using default interface - self.apply_non_tunnel_routes().await + self.apply_non_tunnel_routes().await?; + + Ok(()) } /// Figure out what the best default routes to use are, and send updates to default route change @@ -378,40 +372,21 @@ impl RouteManagerImpl { /// /// If there is no tunnel device, the "best route" is the unscoped default route, whatever it /// is. - async fn update_best_default_route(&mut self, family: interface::Family) -> Result<()> { - let use_scoped_route = (family == interface::Family::V4 - && self.v4_tunnel_default_route.is_some()) - || (family == interface::Family::V6 && self.v6_tunnel_default_route.is_some()); - - let best_route = if use_scoped_route { - interface::get_best_default_route(&mut self.routing_table, family).await - } else { - interface::get_unscoped_default_route(&mut self.routing_table, family).await - }; - log::trace!("Best route ({family:?}): {best_route:?}"); - - let default_route = get_current_best_default_route!(self, family); + async fn update_best_default_route(&mut self, family: interface::Family) -> Result { + let best_route = interface::get_best_default_route(family).await; + let current_route = get_current_best_default_route!(self, family); - if default_route == &best_route { - log::trace!("Default route ({family:?}) is unchanged"); - return Ok(()); + log::trace!("Best route ({family:?}): {best_route:?}"); + if best_route == *current_route { + return Ok(false); } - let old_route = std::mem::replace(default_route, best_route); - - log::debug!( - "Default route change ({family:?}): interface {} -> {}", - old_route.map(|r| r.interface_index()).unwrap_or(0), - default_route - .as_ref() - .map(|r| r.interface_index()) - .unwrap_or(0), - ); + log::debug!("Best default route changed from {current_route:?} to {best_route:?}"); + let _ = std::mem::replace(current_route, best_route); - let changed = default_route.is_some(); + let changed = current_route.is_some(); self.notify_default_route_listeners(family, changed); - - Ok(()) + Ok(true) } fn notify_default_route_listeners(&mut self, family: interface::Family, changed: bool) { @@ -540,10 +515,11 @@ impl RouteManagerImpl { return Ok(()); }; - log::trace!("Setting non-ifscope: {default_route:?}"); - let interface_index = default_route.interface_index(); let new_route = default_route.clone().set_ifscope(interface_index); + + log::trace!("Setting ifscope: {new_route:?}"); + self.add_route_with_record(new_route).await } @@ -560,8 +536,7 @@ impl RouteManagerImpl { } async fn cleanup_routes(&mut self) -> Result<()> { - self.remove_applied_routes(|r| !r.is_ifscope() || !r.is_default().unwrap_or(false)) - .await; + self.remove_applied_routes(|_| true).await; // We have already removed the applied default routes self.v4_tunnel_default_route = None; @@ -630,46 +605,44 @@ impl RouteManagerImpl { /// Add back unscoped default route for the given `family`, if it is still missing. This /// function returns true when no route had to be added. async fn restore_default_route(&mut self, family: interface::Family) -> bool { - let current_route = get_current_best_default_route!(self, family); - let message = RouteMessage::new_route(IpNetwork::from(family).into()); - if matches!(self.routing_table.get_route(&message).await, Ok(Some(_))) { - self.remove_applied_routes(|r| r.is_ifscope() && r.is_default().unwrap_or(false)) - .await; + let Some(desired_default_route) = interface::get_best_default_route(family).await else { return true; - } - - let new_route = interface::get_best_default_route(&mut self.routing_table, family).await; - let old_route = std::mem::replace(current_route, new_route); - let notify = &old_route != current_route; + }; - let done = if let Some(route) = current_route { - *route = route.clone().set_ifscope(0); - if let Err(error) = self.routing_table.add_route(route).await { - log::trace!("Failed to add unscoped default {family} route: {error}"); + let current_default_route = RouteMessage::new_route(IpNetwork::from(family).into()); + if let Ok(Some(current_default)) = + self.routing_table.get_route(¤t_default_route).await + { + // We're done if the route we're looking for is already here + if route_matches_interface(Some(¤t_default), Some(&desired_default_route)) { + return true; } - false - } else { - true + let _ = self + .routing_table + .delete_route(¤t_default_route) + .await; }; - if notify { - let changed = current_route.is_some(); - self.notify_default_route_listeners(family, changed); + if let Err(error) = self.routing_table.add_route(&desired_default_route).await { + log::trace!("Failed to add unscoped default {family} route: {error}"); } - done + self.update_trigger.trigger(); + + false } } -/// Return a map from interface name to link addresses (AF_LINK) -fn get_interface_link_addresses() -> Result> { - let mut gateway_link_addrs = BTreeMap::new(); - let addrs = nix::ifaddrs::getifaddrs().map_err(Error::FetchLinkAddresses)?; - for addr in addrs.into_iter() { - if addr.address.and_then(|addr| addr.family()) != Some(AddressFamily::Link) { - continue; +fn route_matches_interface( + default_route: Option<&RouteMessage>, + interface_route: Option<&RouteMessage>, +) -> bool { + match (default_route, interface_route) { + (Some(default_route), Some(interface_route)) => { + default_route.gateway_ip() == interface_route.gateway_ip() + && default_route.interface_index() == interface_route.interface_index() } - gateway_link_addrs.insert(addr.interface_name, addr.address.unwrap()); + (None, None) => true, + _ => false, } - Ok(gateway_link_addrs) }