From f7b0b02df06519bf4abdd388ff0a434c31209eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Thu, 5 Oct 2023 00:08:49 +0200 Subject: [PATCH] Find router IP using system configuration framework --- talpid-routing/src/unix/macos/interface.rs | 232 ++++++++------------- talpid-routing/src/unix/macos/mod.rs | 24 ++- 2 files changed, 97 insertions(+), 159 deletions(-) diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs index 04123ca36ef5..c5ae7ee46579 100644 --- a/talpid-routing/src/unix/macos/interface.rs +++ b/talpid-routing/src/unix/macos/interface.rs @@ -5,15 +5,23 @@ use nix::{ }; use std::{ collections::BTreeMap, - ffi::CString, io, net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; use system_configuration::{ - core_foundation::string::CFString, - network_configuration::{SCNetworkService, SCNetworkSet}, + core_foundation::{ + base::{CFType, TCFType, ToVoid}, + dictionary::CFDictionary, + string::CFString, + }, + dynamic_store::SCDynamicStoreBuilder, + network_configuration::SCNetworkSet, preferences::SCPreferences, + sys::schema_definitions::{ + kSCDynamicStorePropNetPrimaryInterface, kSCPropInterfaceName, kSCPropNetIPv4Router, + kSCPropNetIPv6Router, + }, }; use super::data::{Destination, RouteMessage}; @@ -49,35 +57,21 @@ impl From for IpNetwork { /// # Note /// /// The tunnel interface is not even listed in the service order, so it will be skipped. -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) => { - log::error!("Invalid interface name: {iface}, {error}"); - continue; - } - }; - - // Get interface ID - let Ok(index) = if_nametoindex(iface_bytes.as_c_str()) else { +pub fn get_best_default_route(family: Family) -> Option { + for iface in network_service_order(family) { + let Ok(index) = if_nametoindex(iface.name.as_str()) else { continue; }; // Request ifscoped default route for this interface - let route_msg = msg - .clone() - .set_gateway_addr(router_addr) + let msg = RouteMessage::new_route(Destination::Network(IpNetwork::from(family))) + .set_gateway_addr(iface.router_ip) .set_interface_index(u16::try_from(index).unwrap()); - if is_active_interface(&iface, family).unwrap_or(true) { - return Some(route_msg); + if is_active_interface(&iface.name, family).unwrap_or_else(|error| { + log::error!("is_active_interface() returned an error for interface \"{}\", assuming active. Error: {error}", iface.name); + true + }) { + return Some(msg); } } @@ -97,63 +91,78 @@ pub fn get_interface_link_addresses() -> io::Result 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(); - } +struct NetworkServiceDetails { + name: String, + router_ip: IpAddr, } -fn network_service_order() -> Vec { +fn network_service_order(family: Family) -> Vec { let prefs = SCPreferences::default(&CFString::new("talpid-routing")); - let services = SCNetworkService::get_services(&prefs); let set = SCNetworkSet::new(&prefs); let service_order = set.service_order(); + let store = SCDynamicStoreBuilder::new("talpid-routing").build(); + + let global_dict = if family == Family::V4 { + "State:/Network/Global/IPv4" + } else { + "State:/Network/Global/IPv6" + }; + let global_dict = store + .get(CFString::new(global_dict)) + .and_then(|v| v.downcast_into::()); + let primary_interface = if let Some(ref dict) = global_dict { + dict.find(unsafe { kSCDynamicStorePropNetPrimaryInterface }.to_void()) + .map(|s| unsafe { CFType::wrap_under_get_rule(*s) }) + .and_then(|s| s.downcast::()) + .map(|s| s.to_string()) + } else { + None + }; + + let router_key = if family == Family::V4 { + unsafe { kSCPropNetIPv4Router.to_void() } + } else { + unsafe { kSCPropNetIPv6Router.to_void() } + }; service_order .iter() .filter_map(|service_id| { - services - .iter() - .find(|service| service.id().as_ref() == Some(&*service_id)) - .and_then(|service| service.network_interface()?.bsd_name()) - .map(|cf_name| cf_name.to_string()) + let service_id_s = service_id.to_string(); + let key = if family == Family::V4 { + format!("State:/Network/Service/{service_id_s}/IPv4") + } else { + format!("State:/Network/Service/{service_id_s}/IPv6") + }; + + let ip_dict = store + .get(CFString::new(&key)) + .and_then(|v| v.downcast_into::())?; + let name = ip_dict + .find(unsafe { kSCPropInterfaceName }.to_void()) + .map(|s| unsafe { CFType::wrap_under_get_rule(*s) }) + .and_then(|s| s.downcast::()) + .map(|s| s.to_string())?; + let router_ip = ip_dict + .find(router_key) + .map(|s| unsafe { CFType::wrap_under_get_rule(*s) }) + .and_then(|s| s.downcast::()) + .and_then(|ip| ip.to_string().parse().ok()) + .or_else(|| { + if Some(&name) != primary_interface.as_ref() { + return None; + } + let Some(ref dict) = global_dict else { + return None; + }; + // Sometimes only the primary interface contains the router IPv6 addr + dict.find(router_key) + .map(|s| unsafe { CFType::wrap_under_get_rule(*s) }) + .and_then(|s| s.downcast::()) + .and_then(|ip| ip.to_string().parse().ok()) + })?; + + Some(NetworkServiceDetails { name, router_ip }) }) .collect::>() } @@ -190,76 +199,3 @@ 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 c8ef2cbf3a07..f886d0b693dc 100644 --- a/talpid-routing/src/unix/macos/mod.rs +++ b/talpid-routing/src/unix/macos/mod.rs @@ -357,23 +357,19 @@ impl RouteManagerImpl { self.apply_tunnel_default_route().await?; // Update routes using default interface - self.apply_non_tunnel_routes().await?; - - Ok(()) + self.apply_non_tunnel_routes().await } /// Figure out what the best default routes to use are, and send updates to default route change /// subscribers. The "best routes" are used by the tunnel device to send packets to the VPN /// relay. /// - /// If there is a tunnel device, the "best route" is the first ifscope default route found, - /// ordered after network service order (after filtering out interfaces without valid IP - /// addresses). + /// The "best route" is determined by the first interface in the network service order that has + /// a valid IP address and gateway. /// - /// If there is no tunnel device, the "best route" is the unscoped default route, whatever it - /// is. + /// On success, the function returns whether the previously known best default changed. async fn update_best_default_route(&mut self, family: interface::Family) -> Result { - let best_route = interface::get_best_default_route(family).await; + let best_route = interface::get_best_default_route(family); let current_route = get_current_best_default_route!(self, family); log::trace!("Best route ({family:?}): {best_route:?}"); @@ -381,7 +377,13 @@ impl RouteManagerImpl { return Ok(false); } - log::debug!("Best default route changed from {current_route:?} to {best_route:?}"); + let old_pair = current_route + .as_ref() + .map(|r| (r.interface_index(), r.gateway_ip())); + let new_pair = best_route + .as_ref() + .map(|r| (r.interface_index(), r.gateway_ip())); + log::debug!("Best default route changed from {old_pair:?} to {new_pair:?}"); let _ = std::mem::replace(current_route, best_route); let changed = current_route.is_some(); @@ -605,7 +607,7 @@ 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 Some(desired_default_route) = interface::get_best_default_route(family).await else { + let Some(desired_default_route) = interface::get_best_default_route(family) else { return true; };