diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-04-01 09:50:29 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-04-01 09:50:29 +0200 |
| commit | 07d65d20750e85ed24c9705c0afc4a7da4b47ad2 (patch) | |
| tree | b4ec8a5d5f78581df84e2e38551470098cea5836 | |
| parent | fc75028a644f2184a4f04d7f41702782964b1af4 (diff) | |
| parent | c9cefd9bedaf3dce6790a61dd4c29ee211836432 (diff) | |
| download | mullvadvpn-07d65d20750e85ed24c9705c0afc4a7da4b47ad2.tar.xz mullvadvpn-07d65d20750e85ed24c9705c0afc4a7da4b47ad2.zip | |
Merge branch 'macos-fix-sc-global-decoding'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/interface.rs | 127 |
2 files changed, 76 insertions, 54 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fed86ec51..e3a0b0b2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ Line wrap the file at 100 chars. Th - Fix node native module being unpacked to a temporary folder. - Fix BSOD caused by routing loop in wireguard-nt. +#### macOS +- Fix bug in parsing of network services from SCDynamicStore. + ## [2025.5] - 2025-03-26 This release is identical to 2025.5-beta1 diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs index 7de3f84cc5..f23870945f 100644 --- a/talpid-routing/src/unix/macos/interface.rs +++ b/talpid-routing/src/unix/macos/interface.rs @@ -24,16 +24,21 @@ use system_configuration::{ dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext}, network_configuration::SCNetworkSet, preferences::SCPreferences, - sys::schema_definitions::{ - kSCDynamicStorePropNetPrimaryInterface, kSCPropInterfaceName, kSCPropNetIPv4Addresses, - kSCPropNetIPv4Router, kSCPropNetIPv6Addresses, kSCPropNetIPv6Router, - }, }; const STATE_IPV4_KEY: &str = "State:/Network/Global/IPv4"; const STATE_IPV6_KEY: &str = "State:/Network/Global/IPv6"; const STATE_SERVICE_PATTERN: &str = "State:/Network/Service/.*/IP.*"; +/// Safely read a symbol in [system_configuration::sys::schema_definitions]. +macro_rules! schema_definition { + ($name:ident) => { + // SAFETY: system_configuration_sys is generated using bindgen, and all symbols in the + // schema_definitions module are to static string pointers, and should be safe to read. + unsafe { ::system_configuration::sys::schema_definitions::$name } + }; +} + #[derive(Debug, PartialEq, Clone, Copy)] pub enum Family { V4, @@ -137,7 +142,11 @@ impl PrimaryInterfaceMonitor { } let run_loop_source = listener_store.create_run_loop_source(); - CFRunLoop::get_current().add_source(&run_loop_source, unsafe { kCFRunLoopCommonModes }); + + // SAFETY: this is just a static string pointer, referencing it should be safe. + let run_loop_common_modes = unsafe { kCFRunLoopCommonModes }; + + CFRunLoop::get_current().add_source(&run_loop_source, run_loop_common_modes); CFRunLoop::run_current(); log::debug!("Interface listener exiting"); @@ -218,25 +227,19 @@ impl PrimaryInterfaceMonitor { .store .get(key) .and_then(|v| v.downcast_into::<CFDictionary>())?; - let name = get_dict_elem_as_string(&global_dict, unsafe { - kSCDynamicStorePropNetPrimaryInterface - }) + + let service_id = get_dict_elem_as_string( + &global_dict, + schema_definition!(kSCDynamicStorePropNetPrimaryService), + ) .or_else(|| { - log::debug!("Missing name for primary interface ({family})"); - None - })?; - let router_ip = get_service_router_ip(&global_dict, family).or_else(|| { - log::debug!("Missing router IP for primary interface ({name}, {family})"); + log::debug!("Missing service ID for primary interface ({family})"); None })?; - let first_ip = get_service_first_ip(&global_dict, family).or_else(|| { - log::debug!("Missing IP for primary interface ({name}, {family})"); + + self.get_network_service(&service_id, family).or_else(|| { + log::debug!("Invalid service ID for primary interface ({family})"); None - })?; - Some(NetworkServiceDetails { - name, - router_ip, - first_ip, }) } @@ -244,39 +247,45 @@ impl PrimaryInterfaceMonitor { SCNetworkSet::new(&self.prefs) .service_order() .iter() - .filter_map(|service_id| { - let service_id_s = service_id.to_string(); - let service_key = if family == Family::V4 { - format!("State:/Network/Service/{service_id_s}/IPv4") - } else { - format!("State:/Network/Service/{service_id_s}/IPv6") - }; - let service_dict = self - .store - .get(CFString::new(&service_key)) - .and_then(|v| v.downcast_into::<CFDictionary>())?; - let name = get_dict_elem_as_string(&service_dict, unsafe { kSCPropInterfaceName }) - .or_else(|| { - log::debug!("Missing name for service {service_key} ({family})"); - None - })?; - let router_ip = get_service_router_ip(&service_dict, family).or_else(|| { - log::debug!("Missing router IP for {service_key} ({name}, {family})"); - None - })?; - let first_ip = get_service_first_ip(&service_dict, family).or_else(|| { - log::debug!("Missing IP for \"{service_key}\" ({name}, {family})"); - None - })?; - Some(NetworkServiceDetails { - name, - router_ip, - first_ip, - }) - }) + .filter_map(|service_id| self.get_network_service(&service_id.to_string(), family)) .collect::<Vec<_>>() } + /// Get details about a specific network interface. + /// + /// Will return `None` and log a message on any error. + fn get_network_service( + &self, + service_id: &str, + family: Family, + ) -> Option<NetworkServiceDetails> { + let service_key = network_service_key(service_id.to_string(), family); + let service_dict = self + .store + .get(CFString::new(&service_key)) + .and_then(|v| v.downcast_into::<CFDictionary>())?; + + let name = get_dict_elem_as_string(&service_dict, schema_definition!(kSCPropInterfaceName)) + .or_else(|| { + log::debug!("Missing name for service {service_key} ({family})"); + None + })?; + let router_ip = get_service_router_ip(&service_dict, family).or_else(|| { + log::debug!("Missing router IP for {service_key} ({name}, {family})"); + None + })?; + let first_ip = get_service_first_ip(&service_dict, family).or_else(|| { + log::debug!("Missing IP for \"{service_key}\" ({name}, {family})"); + None + })?; + + Some(NetworkServiceDetails { + name, + router_ip, + first_ip, + }) + } + pub fn debug(&self) { for family in [Family::V4, Family::V6] { log::debug!( @@ -291,6 +300,16 @@ impl PrimaryInterfaceMonitor { } } +/// Construct the string key for a network service from its ID. +fn network_service_key(service_id: String, family: Family) -> String { + let family = match family { + Family::V4 => "IPv4", + Family::V6 => "IPv6", + }; + + format!("State:/Network/Service/{service_id}/{family}") +} + /// Return a map from interface name to link addresses (AF_LINK) pub fn get_interface_link_addresses() -> io::Result<BTreeMap<String, SockaddrStorage>> { let mut gateway_link_addrs = BTreeMap::new(); @@ -310,9 +329,9 @@ fn is_link_local_v6(addr: &Ipv6Addr) -> bool { fn get_service_router_ip(service_dict: &CFDictionary, family: Family) -> Option<IpAddr> { let router_key = if family == Family::V4 { - unsafe { kSCPropNetIPv4Router } + schema_definition!(kSCPropNetIPv4Router) } else { - unsafe { kSCPropNetIPv6Router } + schema_definition!(kSCPropNetIPv6Router) }; get_dict_elem_as_string(service_dict, router_key).and_then(|ip| ip.parse().ok()) } @@ -323,9 +342,9 @@ fn get_service_router_ip(service_dict: &CFDictionary, family: Family) -> Option< /// `kSCPropNetIPv6Addresses`, depending on the family. fn get_service_first_ip(service_dict: &CFDictionary, family: Family) -> Option<IpAddr> { let ip_key = if family == Family::V4 { - unsafe { kSCPropNetIPv4Addresses } + schema_definition!(kSCPropNetIPv4Addresses) } else { - unsafe { kSCPropNetIPv6Addresses } + schema_definition!(kSCPropNetIPv6Addresses) }; service_dict .find(ip_key.to_void()) |
