summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-10-05 00:08:49 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-10-05 15:19:01 +0200
commit618e4b5e2dbeae6dc1a2a85293d2df235eb8db74 (patch)
tree030a43008930acb33a80b8627f22b14995c171a6
parent1c316fb50cfe108f52e06a4f85703f33a402e019 (diff)
downloadmullvadvpn-618e4b5e2dbeae6dc1a2a85293d2df235eb8db74.tar.xz
mullvadvpn-618e4b5e2dbeae6dc1a2a85293d2df235eb8db74.zip
Find router IP using system configuration framework
-rw-r--r--talpid-routing/src/unix/macos/interface.rs233
-rw-r--r--talpid-routing/src/unix/macos/mod.rs40
2 files changed, 103 insertions, 170 deletions
diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs
index 04123ca36e..85b76377d4 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,22 @@ impl From<Family> 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<RouteMessage> {
- 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<RouteMessage> {
+ 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);
+ let active = 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
+ });
+ if active {
+ return Some(msg);
}
}
@@ -97,63 +92,78 @@ pub fn get_interface_link_addresses() -> io::Result<BTreeMap<String, SockaddrSto
Ok(gateway_link_addrs)
}
-async fn get_router_address(family: Family, interface_name: &str) -> io::Result<Option<IpAddr>> {
- 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<IpAddr> {
- let mut iter = output.split_whitespace();
- loop {
- let next_chunk = iter.next()?;
- if next_chunk == "Router" && iter.next()? == ":" {
- return iter.next()?.parse().ok();
- }
- }
+struct NetworkServiceDetails {
+ name: String,
+ router_ip: IpAddr,
}
-fn parse_v6_ipconfig_output(output: &str) -> Option<IpAddr> {
- 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<String> {
+fn network_service_order(family: Family) -> Vec<NetworkServiceDetails> {
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::<CFDictionary>());
+ 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::<CFString>())
+ .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::<CFDictionary>())?;
+ let name = ip_dict
+ .find(unsafe { kSCPropInterfaceName }.to_void())
+ .map(|s| unsafe { CFType::wrap_under_get_rule(*s) })
+ .and_then(|s| s.downcast::<CFString>())
+ .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::<CFString>())
+ .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::<CFString>())
+ .and_then(|ip| ip.to_string().parse().ok())
+ })?;
+
+ Some(NetworkServiceDetails { name, router_ip })
})
.collect::<Vec<_>>()
}
@@ -190,76 +200,3 @@ fn is_routable_v6(addr: &Ipv6Addr) -> bool {
// !(link local)
&& (addr.segments()[0] & 0xffc0) != 0xfe80
}
-
-#[cfg(test)]
-const TEST_IPCONFIG_OUTPUT: &str = "<dictionary> {
- Hashed-BSSID : 86:a2:7a:bb:7c:5c
- IPv4 : <array> {
- 0 : <dictionary> {
- Addresses : <array> {
- 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 : <array> {
- 0 : 255.255.255.0
- }
- }
- 1 : <dictionary> {
- ConfigMethod : LinkLocal
- IsPublished : TRUE
- ParentServiceID : 400B48FB-2585-41DF-8459-30C5C6D5621C
- ServiceID : LINKLOCAL-en0
- }
- }
- IPv6 : <array> {
- 0 : <dictionary> {
- ConfigMethod : Automatic
- DHCPv6 : <dictionary> {
- ElapsedTime : 2200
- Mode : Stateful
- State : Solicit
- }
- IsPublished : TRUE
- RTADV : <dictionary> {
- 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::<IpAddr>().unwrap()
- )
-}
-
-#[test]
-fn test_parsing_v6_ipconfig_output() {
- assert_eq!(
- parse_v6_ipconfig_output(&TEST_IPCONFIG_OUTPUT).unwrap(),
- "fe80::5aef:68ff:fe0d:18db".parse::<IpAddr>().unwrap()
- )
-}
diff --git a/talpid-routing/src/unix/macos/mod.rs b/talpid-routing/src/unix/macos/mod.rs
index c8ef2cbf3a..8cca3594f9 100644
--- a/talpid-routing/src/unix/macos/mod.rs
+++ b/talpid-routing/src/unix/macos/mod.rs
@@ -119,7 +119,6 @@ impl RouteManagerImpl {
// Initialize default routes
// NOTE: This isn't race-free, as we're not listening for route changes before initializing
self.update_best_default_route(interface::Family::V4)
- .await
.unwrap_or_else(|error| {
log::error!(
"{}",
@@ -128,7 +127,6 @@ impl RouteManagerImpl {
false
});
self.update_best_default_route(interface::Family::V6)
- .await
.unwrap_or_else(|error| {
log::error!(
"{}",
@@ -201,10 +199,8 @@ impl RouteManagerImpl {
}
// Reset known best route
- let _ = self.update_best_default_route(interface::Family::V4)
- .await;
- let _ = self.update_best_default_route(interface::Family::V6)
- .await;
+ let _ = self.update_best_default_route(interface::Family::V4);
+ let _ = self.update_best_default_route(interface::Family::V6);
log::debug!("Adding routes: {routes:?}");
let _ = tx.send(self.add_required_routes(routes).await);
@@ -342,10 +338,8 @@ impl RouteManagerImpl {
/// server. The gateway of the relay route is set to the first interface in the network
/// service order that has a working ifscoped default route.
async fn refresh_routes(&mut self) -> Result<()> {
- self.update_best_default_route(interface::Family::V4)
- .await?;
- self.update_best_default_route(interface::Family::V6)
- .await?;
+ self.update_best_default_route(interface::Family::V4)?;
+ self.update_best_default_route(interface::Family::V6)?;
// Remove any existing ifscope route that we've added
self.remove_applied_routes(|route| {
@@ -357,23 +351,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.
- async fn update_best_default_route(&mut self, family: interface::Family) -> Result<bool> {
- let best_route = interface::get_best_default_route(family).await;
+ /// On success, the function returns whether the previously known best default changed.
+ fn update_best_default_route(&mut self, family: interface::Family) -> Result<bool> {
+ 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 +371,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 +601,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;
};