diff --git a/talpid-routing/Cargo.toml b/talpid-routing/Cargo.toml index 223182947539..3ebf8682a922 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"] } [target.'cfg(not(target_os="android"))'.dependencies] talpid-types = { path = "../talpid-types" } diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs index 761c0f870bc7..abb674baf3ea 100644 --- a/talpid-routing/src/unix/macos/interface.rs +++ b/talpid-routing/src/unix/macos/interface.rs @@ -4,8 +4,9 @@ use nix::net::if_::{if_nametoindex, InterfaceFlags}; use std::{ ffi::{CStr, CString}, io, - net::{Ipv4Addr, Ipv6Addr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; + use system_configuration::{ core_foundation::string::CFString, network_configuration::{SCNetworkService, SCNetworkSet}, @@ -82,7 +83,6 @@ pub async fn get_unscoped_default_route( 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. @@ -98,34 +98,63 @@ pub async fn get_best_default_route( msg = msg.set_gateway_route(true); for iface in network_service_order() { - 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 index = match if_nametoindex(iface_bytes.as_c_str()) { - Ok(index) => index, - Err(_error) => { - continue; - } + let Ok(Some(router_addr)) = get_router_address(family, &iface).await 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); + if is_active_interface(&iface, family).unwrap_or(true) { + return Some(route_msg); } } None } +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"]; + loop { + let mut next_chunk = iter.next()?; + for expected_chunk in pattern { + if expected_chunk != next_chunk { + continue; + } + next_chunk = iter.next()?; + } + return iter.next()?.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 +205,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 f1b07b0a1825..5754711940c1 100644 --- a/talpid-routing/src/unix/macos/mod.rs +++ b/talpid-routing/src/unix/macos/mod.rs @@ -7,9 +7,9 @@ use futures::{ }; use ipnetwork::IpNetwork; use nix::sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}; -use std::pin::Pin; use std::{ collections::{BTreeMap, HashSet}, + pin::Pin, time::Duration, }; use talpid_types::ErrorExt;