Skip to content

Commit

Permalink
Find router IP using system configuration framework
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Oct 5, 2023
1 parent c0b066e commit f7b0b02
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 159 deletions.
232 changes: 84 additions & 148 deletions talpid-routing/src/unix/macos/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -49,35 +57,21 @@ 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);
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);
}
}

Expand All @@ -97,63 +91,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();
}
}
}

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();
}
struct NetworkServiceDetails {
name: String,
router_ip: IpAddr,
}

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<_>>()
}
Expand Down Expand Up @@ -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 = "<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()
)
}
24 changes: 13 additions & 11 deletions talpid-routing/src/unix/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,31 +357,33 @@ 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<bool> {
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:?}");
if best_route == *current_route {
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();
Expand Down Expand Up @@ -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;
};

Expand Down

0 comments on commit f7b0b02

Please sign in to comment.