summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-04-01 09:50:29 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-04-01 09:50:29 +0200
commit07d65d20750e85ed24c9705c0afc4a7da4b47ad2 (patch)
treeb4ec8a5d5f78581df84e2e38551470098cea5836
parentfc75028a644f2184a4f04d7f41702782964b1af4 (diff)
parentc9cefd9bedaf3dce6790a61dd4c29ee211836432 (diff)
downloadmullvadvpn-07d65d20750e85ed24c9705c0afc4a7da4b47ad2.tar.xz
mullvadvpn-07d65d20750e85ed24c9705c0afc4a7da4b47ad2.zip
Merge branch 'macos-fix-sc-global-decoding'
-rw-r--r--CHANGELOG.md3
-rw-r--r--talpid-routing/src/unix/macos/interface.rs127
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())