diff options
| author | Joakim Hulthe <joakim@hulthe.net> | 2025-03-27 12:44:14 +0100 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-04-23 15:35:51 +0200 |
| commit | 779cfe3790a3d43db67c33e937e2c3d2d4895b27 (patch) | |
| tree | 41c10c0eddd76a1bd68aec51b386878478baf8f1 | |
| parent | aabe5cedec3385ad160779dda49594cb7bdbced8 (diff) | |
| download | mullvadvpn-779cfe3790a3d43db67c33e937e2c3d2d4895b27.tar.xz mullvadvpn-779cfe3790a3d43db67c33e937e2c3d2d4895b27.zip | |
Handle changes to best default route better
| -rw-r--r-- | talpid-routing/src/unix/macos/data.rs | 5 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/default_routes.rs | 279 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/interface.rs | 334 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/ip_map.rs | 67 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/mod.rs | 285 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/watch.rs | 4 |
6 files changed, 698 insertions, 276 deletions
diff --git a/talpid-routing/src/unix/macos/data.rs b/talpid-routing/src/unix/macos/data.rs index 9221ec16a9..5ee2b24a06 100644 --- a/talpid-routing/src/unix/macos/data.rs +++ b/talpid-routing/src/unix/macos/data.rs @@ -13,6 +13,7 @@ use std::{ /// Message that describes a route - either an added, removed, changed or plainly retrieved route. #[derive(Debug, Clone, PartialEq)] pub struct RouteMessage { + // INVARIANT: The `AddressFlag` must match the variant of `RouteSocketAddress`. sockaddrs: BTreeMap<AddressFlag, RouteSocketAddress>, mtu: u32, route_flags: RouteFlag, @@ -1038,7 +1039,7 @@ impl Iterator for RouteSockAddrIterator<'_> { Consider adding them to the definition." ); - return match RouteSocketAddress::new(current_flag, self.buffer) { + match RouteSocketAddress::new(current_flag, self.buffer) { Ok((next_addr, addr_len)) => { self.advance_buffer(addr_len); Some(Ok(next_addr)) @@ -1047,7 +1048,7 @@ impl Iterator for RouteSockAddrIterator<'_> { self.buffer = &[]; Some(Err(err)) } - }; + } } } diff --git a/talpid-routing/src/unix/macos/default_routes.rs b/talpid-routing/src/unix/macos/default_routes.rs new file mode 100644 index 0000000000..e6a68a508d --- /dev/null +++ b/talpid-routing/src/unix/macos/default_routes.rs @@ -0,0 +1,279 @@ +use std::{collections::HashMap, convert::Infallible, future::pending, mem, time::Duration}; + +use futures::{ + channel::mpsc::{self, UnboundedReceiver, UnboundedSender}, + select_biased, FutureExt, StreamExt, +}; +use tokio::{ + runtime, + time::{sleep_until, Instant}, +}; + +use crate::imp::imp::interface::NetworkServiceDetails; + +use super::{ + interface::{Family, InterfaceEvent, PrimaryInterfaceDetails, PrimaryInterfaceMonitor}, + ip_map::IpMap, + DefaultRoute, +}; + +/// Grace time during which we don't act if the best default route dissapears. +// +// 5 seconds seemed to be a reasonable value when testing. +// Increasing this value will increase the time it takes the daemon to realize when there's no +// network connectivity. Decreasing it will increase the risk of unnecessary reconnects when the +// best default route simply goes away for a few seconds. +const NO_ROUTE_GRACE_TIME: Duration = Duration::from_secs(5); + +/// Monitors changes to the primary interface and reports [BestRoute]. +pub struct DefaultRouteMonitor { + monitor: PrimaryInterfaceMonitor, + event_rx: UnboundedReceiver<Vec<InterfaceEvent>>, + + route_tx: IpMap<UnboundedSender<Option<DefaultRoute>>>, + + /// The current best routes. + current_route: IpMap<DefaultRoute>, + + /// The current primary interfaces. + primary_interfaces: IpMap<PrimaryInterfaceDetails>, +} + +impl DefaultRouteMonitor { + /// Start monitoring interfaces for changes to the best route. + /// + /// Returns an IPv4 and an IPv6 channel of [BestRoute] updates. + pub fn start( + monitor: PrimaryInterfaceMonitor, + event_rx: UnboundedReceiver<Vec<InterfaceEvent>>, + ) -> ( + UnboundedReceiver<Option<DefaultRoute>>, + UnboundedReceiver<Option<DefaultRoute>>, + ) { + let (route_v4_tx, route_v4_rx) = mpsc::unbounded(); + let (route_v6_tx, route_v6_rx) = mpsc::unbounded(); + + let mut route_tx = IpMap::new(); + route_tx.insert(Family::V4, route_v4_tx); + route_tx.insert(Family::V6, route_v6_tx); + + let monitor = DefaultRouteMonitor { + monitor, + event_rx, + route_tx, + current_route: IpMap::new(), + primary_interfaces: IpMap::new(), + }; + + tokio::task::spawn_blocking(move || { + runtime::Handle::current().block_on(monitor.run()); + }); + + let route_v4_rx = filter_duplicates(delay_nones(NO_ROUTE_GRACE_TIME, route_v4_rx)); + let route_v6_rx = filter_duplicates(delay_nones(NO_ROUTE_GRACE_TIME, route_v6_rx)); + + (route_v4_rx, route_v6_rx) + } + + async fn run(mut self) { + for family in [Family::V4, Family::V6] { + let route = self.monitor.get_route(family); + + self.current_route.set(family, route.clone()); + if let Some(tx) = self.route_tx.get(family) { + let _ = tx.unbounded_send(route); + } + } + + while let Some(events) = self.event_rx.next().await { + if self.route_tx.is_empty() { + break; + } + + self.handle_events(events); + } + } + + fn handle_events(&mut self, events: Vec<InterfaceEvent>) { + // Split events by address family and handle them seperately. + let mut ipv4_events = vec![]; + let mut ipv6_events = vec![]; + for event in events { + match event.family() { + Family::V4 => ipv4_events.push(event), + Family::V6 => ipv6_events.push(event), + } + } + + self.handle_events_for_family(Family::V4, ipv4_events); + self.handle_events_for_family(Family::V6, ipv6_events); + } + + fn handle_events_for_family(&mut self, family: Family, events: Vec<InterfaceEvent>) { + enum Change<T> { + New(T), + Removed, + } + + // Go through the events and figure out if the primary interface changed. + let mut primary_interface_change: Option<Change<PrimaryInterfaceDetails>> = None; + for event in &events { + let InterfaceEvent::PrimaryInterfaceUpdate { new_value, .. } = event else { + continue; + }; + + primary_interface_change = Some(match new_value { + Some(new_value) => Change::New(new_value.clone()), + None => Change::Removed, + }); + } + + // Collect all NetworkServiceUpdates into a HashMap. + let changed_services: HashMap<String, Change<NetworkServiceDetails>> = events + .into_iter() + .filter_map(|service| { + let InterfaceEvent::NetworkServiceUpdate { + service_id, + new_value, + .. + } = service + else { + return None; + }; + + let change = match new_value { + Some(service) => Change::New(service), + None => Change::Removed, + }; + + Some((service_id, change)) + }) + .collect(); + + // Figure out if anything interesting happened. + // Things we care about: + // - The primary interface changed. + // - The service of the primary interface changed. + // - If we're NOT using the primary interface, we care about whether ANY service changed. + let an_important_service_changed = + if let Some(primary_interface) = self.primary_interfaces.get(family) { + changed_services.contains_key(&primary_interface.service_id) + } else { + !changed_services.is_empty() + }; + + // If nothing interesting has happened, just return. + if primary_interface_change.is_none() && !an_important_service_changed { + return; + } + + // Figure out what the new default route should be. + // Match on the new primary interface, and the previous primary interface + let new_route = match ( + primary_interface_change.as_ref(), + self.primary_interfaces.get(family), + ) { + // This match covers two cases: + // - The primary interface changed. + // - The primary interface didn't change, and we have one from before. + (Some(Change::New(interface)), _) | (None, Some(interface)) => changed_services + .get(&interface.service_id) + .and_then(|change| match change { + Change::New(service) => Some(service), + Change::Removed => None, + }) + .and_then(|service| self.monitor.route_from_service(service)) + .or_else(|| self.monitor.get_route_by_service_order(family)), + + // This match covers the case where the primary interface was removed, or it never + // existed. In this case we iterate over all network services and pick the first good + // one. + _ => self.monitor.get_route_by_service_order(family), + }; + + self.current_route.set(family, new_route.clone()); + if let Some(tx) = self.route_tx.get(family) { + if tx.unbounded_send(new_route).is_err() { + self.route_tx.remove(family); + } + } + } +} + +/// Filter out duplicate messages from a channel. +/// +/// This will always keep a clone of the last value that was sent on the channel. +fn filter_duplicates<T: PartialEq + Clone + Send + 'static>( + unfiltered_rx: UnboundedReceiver<T>, +) -> UnboundedReceiver<T> { + async fn do_filtering<T: PartialEq + Clone + Send + 'static>( + mut unfiltered_rx: UnboundedReceiver<T>, + filtered_tx: UnboundedSender<T>, + ) -> Option<Infallible> { + let mut last_value = unfiltered_rx.next().await?; + filtered_tx.unbounded_send(last_value.clone()).ok()?; + + loop { + let prev_value = mem::replace(&mut last_value, unfiltered_rx.next().await?); + + if last_value != prev_value { + filtered_tx.unbounded_send(last_value.clone()).ok()?; + } + } + } + + let (filtered_tx, filtered_rx) = mpsc::unbounded(); + tokio::task::spawn(do_filtering(unfiltered_rx, filtered_tx)); + filtered_rx +} + +/// Delay `None`-events by `grace_time`. +/// +/// When receiving a `None` on the channel, a timer will start. If no `Some`s are received within +/// the deadline, a `None` will be sent. +/// +/// Some `None`s may be dropped, but `Some`-values are passed along immediately. +fn delay_nones<T: Send + 'static>( + grace_time: Duration, + mut fast_rx: UnboundedReceiver<Option<T>>, +) -> UnboundedReceiver<Option<T>> { + let (slow_tx, slow_rx) = mpsc::unbounded(); + + tokio::task::spawn(async move { + let mut no_route_grace_timeout = None; + + loop { + let no_route_grace_timer = async { + match no_route_grace_timeout { + None => pending().await, + Some(time) => sleep_until(time).await, + }; + }; + + select_biased! { + route = fast_rx.next() => { + let Some(route) = route else { return }; + + if route.is_some() { + no_route_grace_timeout = None; + if slow_tx.unbounded_send(route).is_err() { + return; + }; + + } else if no_route_grace_timeout.is_none() { + no_route_grace_timeout = Some(Instant::now() + grace_time); + } + } + + _ = no_route_grace_timer.fuse() => { + no_route_grace_timeout = None; + if slow_tx.unbounded_send(None).is_err() { + return; + }; + } + } + } + }); + + slow_rx +} diff --git a/talpid-routing/src/unix/macos/interface.rs b/talpid-routing/src/unix/macos/interface.rs index f23870945f..33ddaed9e4 100644 --- a/talpid-routing/src/unix/macos/interface.rs +++ b/talpid-routing/src/unix/macos/interface.rs @@ -10,6 +10,7 @@ use std::{ collections::BTreeMap, io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + ops::Deref, }; use super::data::{Destination, RouteMessage}; @@ -63,13 +64,6 @@ impl Family { } } -#[derive(Debug)] -struct NetworkServiceDetails { - name: String, - router_ip: IpAddr, - first_ip: IpAddr, -} - pub struct PrimaryInterfaceMonitor { store: SCDynamicStore, prefs: SCPreferences, @@ -78,21 +72,68 @@ pub struct PrimaryInterfaceMonitor { // FIXME: Implement Send on SCDynamicStore, if it's safe unsafe impl Send for PrimaryInterfaceMonitor {} +/// Contents of a `/Network/Service/<service_id>/IPvX` key in the [SCDynamicStore]. +#[derive(Clone, Debug)] +pub struct NetworkServiceDetails { + pub interface_name: String, + pub router_ip: IpAddr, + pub first_ip: IpAddr, +} + +/// Contents of the `/Network/Global/IPvX` key in the [SCDynamicStore]. +#[derive(Clone, Debug)] +pub struct PrimaryInterfaceDetails { + #[allow(dead_code)] // this field is useful for debugging + pub name: String, + pub service_id: String, +} + pub enum InterfaceEvent { - Update, + /// The `/Network/Global/IPvX` key in the [SCDynamicStore] was updated. + PrimaryInterfaceUpdate { + /// The IP address family. + family: Family, + + /// The updated [PrimaryInterfaceDetails]. + new_value: Option<PrimaryInterfaceDetails>, + }, + + /// A network service in the [SCDynamicStore] was updated. + NetworkServiceUpdate { + /// The IP address family of the network service. + family: Family, + + /// The ID of the network service. + service_id: String, + + /// The updated [NetworkServiceDetails]. + new_value: Option<NetworkServiceDetails>, + }, +} +impl InterfaceEvent { + pub fn family(&self) -> Family { + match *self { + InterfaceEvent::PrimaryInterfaceUpdate { family, .. } => family, + InterfaceEvent::NetworkServiceUpdate { family, .. } => family, + } + } } -/// Default interface/route +/// The best network route. Either suggested by macOS, or inferred by looking at the available +/// network interfaces. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DefaultRoute { - /// Default interface name + /// Interface name. pub interface: String, - /// Default interface index + + /// Interface index. pub interface_index: u16, - /// Router IP - pub router_ip: IpAddr, - /// Default interface IP address + + /// IP address of the interface. pub ip: IpAddr, + + /// Router IP. + pub router_ip: IpAddr, } impl From<DefaultRoute> for RouteMessage { @@ -111,7 +152,7 @@ impl From<DefaultRoute> for RouteMessage { } impl PrimaryInterfaceMonitor { - pub fn new() -> (Self, UnboundedReceiver<InterfaceEvent>) { + pub fn new() -> (Self, UnboundedReceiver<Vec<InterfaceEvent>>) { let store = SCDynamicStoreBuilder::new("talpid-routing").build(); let prefs = SCPreferences::default(&CFString::new("talpid-routing")); @@ -121,7 +162,7 @@ impl PrimaryInterfaceMonitor { (Self { store, prefs }, rx) } - fn start_listener(tx: UnboundedSender<InterfaceEvent>) { + fn start_listener(tx: UnboundedSender<Vec<InterfaceEvent>>) { std::thread::spawn(|| { let listener_store = SCDynamicStoreBuilder::new("talpid-routing-listener") .callback_context(SCDynamicStoreCallBackContext { @@ -154,46 +195,79 @@ impl PrimaryInterfaceMonitor { } fn store_change_handler( - _store: SCDynamicStore, + store: SCDynamicStore, changed_keys: CFArray<CFString>, - tx: &mut UnboundedSender<InterfaceEvent>, + tx: &mut UnboundedSender<Vec<InterfaceEvent>>, ) { - for k in changed_keys.iter() { - log::debug!("Interface change, key {}", k.to_string()); - } - let _ = tx.unbounded_send(InterfaceEvent::Update); + let events = changed_keys + .iter() + .filter_map(|key| { + let key = key.deref().to_string(); + + let family = match key.as_str() { + STATE_IPV4_KEY => Family::V4, + STATE_IPV6_KEY => Family::V6, + + key => { + let Some((service_id, family)) = service_id_from_service_key(key) else { + log::debug!("Unknown SCDynStore key: {key:?}"); + return None; // skip invalid keys + }; + + let new_value = get_network_service(&store, service_id, family); + return Some(InterfaceEvent::NetworkServiceUpdate { + family, + service_id: service_id.to_string(), + new_value, + }); + } + }; + + let new_value = get_primary_interface(&store, family); + Some(InterfaceEvent::PrimaryInterfaceUpdate { family, new_value }) + }) + .collect(); + + let _ = tx.unbounded_send(events); } /// Retrieve the best current default route. This is based on the primary interface, or else /// the first active interface in the network service order. pub fn get_route(&self, family: Family) -> Option<DefaultRoute> { - let ifaces = self - .get_primary_interface(family) - .map(|iface| { + self.get_primary_interface_service(family) + .map(|service| { log::debug!("Found primary interface for {family}"); - vec![iface] + vec![service] }) - .unwrap_or_else(|| self.network_services(family)); + .unwrap_or_else(|| self.network_services(family)) + .into_iter() + .filter_map(|service| self.route_from_service(&service)) + .next() + } - let (iface, index) = ifaces + /// Iterate through active interfaces in network service order and return a suggested route for + /// the first one with a valid IP and gateway. + pub fn get_route_by_service_order(&self, family: Family) -> Option<DefaultRoute> { + self.network_services(family) .into_iter() - .filter_map(|iface| { - let index = if_nametoindex(iface.name.as_str()) - .inspect_err(|error| { - log::error!( - "Failed to retrieve interface index for \"{}\": {error}", - iface.name - ); - }) - .ok()?; - Some((iface, index)) + .filter_map(|service| self.route_from_service(&service)) + .next() + } + + pub fn route_from_service(&self, service: &NetworkServiceDetails) -> Option<DefaultRoute> { + let index = if_nametoindex(service.interface_name.as_str()) + .inspect_err(|error| { + log::error!( + "Failed to retrieve interface index for \"{}\": {error}", + service.interface_name + ); }) - .next()?; + .ok()?; let index = u16::try_from(index).unwrap(); - let mut router_ip = iface.router_ip; - if let IpAddr::V6(ref mut addr) = router_ip { + let mut router_ip = service.router_ip; + if let IpAddr::V6(addr) = &mut router_ip { if is_link_local_v6(addr) { // The second pair of octets should be set to the scope id // See getaddr() in route.c: @@ -210,37 +284,23 @@ impl PrimaryInterfaceMonitor { } Some(DefaultRoute { - interface: iface.name, + interface: service.interface_name.clone(), interface_index: index, router_ip, - ip: iface.first_ip, + ip: service.first_ip, }) } - fn get_primary_interface(&self, family: Family) -> Option<NetworkServiceDetails> { - let key = if family == Family::V4 { - STATE_IPV4_KEY - } else { - STATE_IPV6_KEY - }; - let global_dict = self - .store - .get(key) - .and_then(|v| v.downcast_into::<CFDictionary>())?; - - let service_id = get_dict_elem_as_string( - &global_dict, - schema_definition!(kSCDynamicStorePropNetPrimaryService), - ) - .or_else(|| { - log::debug!("Missing service ID for primary interface ({family})"); - None - })?; + fn get_primary_interface_service(&self, family: Family) -> Option<NetworkServiceDetails> { + get_primary_interface_service(&self.store, family) + } - self.get_network_service(&service_id, family).or_else(|| { - log::debug!("Invalid service ID for primary interface ({family})"); - None - }) + pub fn get_network_service( + &self, + service_id: &str, + family: Family, + ) -> Option<NetworkServiceDetails> { + get_network_service(&self.store, service_id, family) } fn network_services(&self, family: Family) -> Vec<NetworkServiceDetails> { @@ -250,54 +310,6 @@ impl PrimaryInterfaceMonitor { .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!( - "Primary interface ({family}): {:?}", - self.get_primary_interface(family) - ); - log::debug!( - "Network services ({family}): {:?}", - self.network_services(family) - ); - } - } } /// Construct the string key for a network service from its ID. @@ -310,6 +322,18 @@ fn network_service_key(service_id: String, family: Family) -> String { format!("State:/Network/Service/{service_id}/{family}") } +fn service_id_from_service_key(key: &str) -> Option<(&str, Family)> { + let id_and_family = key.strip_prefix("State:/Network/Service/")?; + let (id, family) = id_and_family.split_once('/')?; + let family = match family { + "IPv4" => Family::V4, + "IPv6" => Family::V6, + _ => return None, + }; + + Some((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(); @@ -327,6 +351,88 @@ fn is_link_local_v6(addr: &Ipv6Addr) -> bool { (addr.segments()[0] & 0xffc0) == 0xfe80 } +fn get_primary_interface( + store: &SCDynamicStore, + family: Family, +) -> Option<PrimaryInterfaceDetails> { + let key = if family == Family::V4 { + STATE_IPV4_KEY + } else { + STATE_IPV6_KEY + }; + let global_dict = store + .get(key) + .or_else(|| { + log::debug!("{key} is missing!"); + None + }) + .and_then(|v| v.downcast_into::<CFDictionary>())?; + + let service_id = get_dict_elem_as_string( + &global_dict, + schema_definition!(kSCDynamicStorePropNetPrimaryService), + ) + .or_else(|| { + log::debug!("Missing service ID for primary interface ({family})"); + None + })?; + + let name = get_dict_elem_as_string( + &global_dict, + schema_definition!(kSCDynamicStorePropNetPrimaryInterface), + ) + .or_else(|| { + log::debug!("Missing name for primary interface ({family})"); + None + })?; + + Some(PrimaryInterfaceDetails { name, service_id }) +} + +fn get_primary_interface_service( + store: &SCDynamicStore, + family: Family, +) -> Option<NetworkServiceDetails> { + let primary_interface = get_primary_interface(store, family)?; + get_network_service(store, &primary_interface.service_id, family) +} + +/// Get details about a specific network interface. +/// +/// Will return `None` and log a message on any error. +fn get_network_service( + store: &SCDynamicStore, + service_id: &str, + family: Family, +) -> Option<NetworkServiceDetails> { + let service_key = network_service_key(service_id.to_string(), family); + let service_dict = store + .get(CFString::new(&service_key)) + .and_then(|v| v.downcast_into::<CFDictionary>())?; + + let interface_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} ({interface_name}, {family})"); + None + })?; + let first_ip = get_service_first_ip(&service_dict, family).or_else(|| { + log::debug!("Missing IP for \"{service_key}\" ({interface_name}, {family})"); + None + })?; + + Some(NetworkServiceDetails { + interface_name, + router_ip, + first_ip, + }) +} + fn get_service_router_ip(service_dict: &CFDictionary, family: Family) -> Option<IpAddr> { let router_key = if family == Family::V4 { schema_definition!(kSCPropNetIPv4Router) diff --git a/talpid-routing/src/unix/macos/ip_map.rs b/talpid-routing/src/unix/macos/ip_map.rs new file mode 100644 index 0000000000..5d0ad4c61d --- /dev/null +++ b/talpid-routing/src/unix/macos/ip_map.rs @@ -0,0 +1,67 @@ +use super::interface::Family; + +/// A map where the key is [Family]. +#[derive(Clone, Debug)] +pub struct IpMap<T> { + v4: Option<T>, + v6: Option<T>, +} + +impl<T> IpMap<T> { + pub const fn new() -> Self { + Self { v4: None, v6: None } + } + + pub fn get(&self, family: Family) -> Option<&T> { + match family { + Family::V4 => self.v4.as_ref(), + Family::V6 => self.v6.as_ref(), + } + } + + /// Insert an option value and return the old value. + pub fn set(&mut self, family: Family, value: Option<T>) -> Option<T> { + let old_value = self.remove(family); + match family { + Family::V4 => self.v4 = value, + Family::V6 => self.v6 = value, + }; + old_value + } + + /// Insert a value and return the old value. + pub fn insert(&mut self, family: Family, value: T) -> Option<T> { + match family { + Family::V4 => self.v4.replace(value), + Family::V6 => self.v6.replace(value), + } + } + + /// Remove a value and return it. + pub fn remove(&mut self, family: Family) -> Option<T> { + match family { + Family::V4 => self.v4.take(), + Family::V6 => self.v6.take(), + } + } + + pub fn len(&self) -> usize { + self.iter().count() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> impl Iterator<Item = (Family, &T)> { + [(Family::V4, &self.v4), (Family::V6, &self.v6)] + .into_iter() + .flat_map(|(family, elem)| Some((family, elem.as_ref()?))) + } + + pub fn drain(&mut self) -> impl Iterator<Item = (Family, T)> { + [(Family::V4, self.v4.take()), (Family::V6, self.v6.take())] + .into_iter() + .flat_map(|(family, elem)| Some((family, elem?))) + } +} diff --git a/talpid-routing/src/unix/macos/mod.rs b/talpid-routing/src/unix/macos/mod.rs index 7e819b3f0b..b88ad0c5f1 100644 --- a/talpid-routing/src/unix/macos/mod.rs +++ b/talpid-routing/src/unix/macos/mod.rs @@ -1,10 +1,12 @@ use crate::{debounce::BurstGuard, Gateway, MacAddress, NetNode, RequiredRoute, Route}; +use default_routes::DefaultRouteMonitor; use futures::{ channel::mpsc::{self, UnboundedReceiver}, future::FutureExt, stream::{FusedStream, StreamExt}, }; +use ip_map::IpMap; use ipnetwork::IpNetwork; use std::{ collections::{BTreeMap, HashSet}, @@ -23,7 +25,9 @@ pub use super::DefaultRouteEvent; pub use interface::DefaultRoute; mod data; +mod default_routes; mod interface; +mod ip_map; mod routing_socket; mod watch; @@ -58,17 +62,6 @@ pub enum Error { InvalidData(#[source] data::Error), } -/// Convenience macro to get the current default route. Macro because I don't want to borrow `self` -/// mutably. -macro_rules! get_current_best_default_route { - ($self:expr, $family:expr) => {{ - match $family { - interface::Family::V4 => &mut $self.v4_default_route, - interface::Family::V6 => &mut $self.v6_default_route, - } - }}; -} - /// Route manager can be in 1 of 4 states - /// - waiting for a route to be added or removed from the route table /// - obtaining default routes @@ -81,21 +74,49 @@ macro_rules! get_current_best_default_route { /// new changes, obtain new default routes and reapply routes that should be routed through the /// default nodes. Once the routes are reapplied, the route table changes are monitored again. pub struct RouteManagerImpl { + /// Interface to the macOS routing table. routing_table: RoutingTable, - // Routes that use the default non-tunnel interface + + /// Routes that use the default non-tunnel interface. non_tunnel_routes: HashSet<IpNetwork>, - v4_tunnel_default_route: Option<data::RouteMessage>, - v6_tunnel_default_route: Option<data::RouteMessage>, + + /// The IPv4 and IPv6 routes that we will add that points at the tun interface. + /// + /// This is populated by [Self::add_required_routes] + /// and used by [Self::apply_tunnel_default_routes]. + tunnel_default_routes: IpMap<data::RouteMessage>, + + /// The list of routes we have added to macOSs routing table in [Self::add_route_with_record]. applied_routes: BTreeMap<RouteDestination, RouteMessage>, - v4_default_route: Option<interface::DefaultRoute>, - v6_default_route: Option<interface::DefaultRoute>, + + /// Callback that fires when we receive an event from our route-monitoring socket, + /// or from [Self::interface_change_rx]. It indicates that *something* has changed with the + /// routing table or with the network interfaces. update_trigger: BurstGuard, + + /// Indicates that `best_default_route` has changed, or that a `/0`-route in the routing table + /// was changed. + unhandled_default_route_changes: bool, + + check_default_routes_restored: Pin<Box<dyn FusedStream<Item = ()> + Send>>, + + /// Channel that receives updates to the best [DefaultRoute] for IPv4. + best_default_route_rx_v4: UnboundedReceiver<Option<DefaultRoute>>, + + /// Channel that receives updates to the best [DefaultRoute] for IPv6. + best_default_route_rx_v6: UnboundedReceiver<Option<DefaultRoute>>, + + /// The best IPv4 and v6 network routes suggested by macOS. + /// + /// Example: Interface "en0" (1) with IP 192.168.1.222, and with router_ip 192.168.1.1. + best_default_route: IpMap<interface::DefaultRoute>, + + /// Message to notify `default_route_listeners` when `best_default_route` changes. + best_default_route_update: IpMap<DefaultRouteEvent>, + default_route_listeners: Vec<mpsc::UnboundedSender<DefaultRouteEvent>>, + interface_change_listeners: Vec<mpsc::UnboundedSender<super::InterfaceEvent>>, - check_default_routes_restored: Pin<Box<dyn FusedStream<Item = ()> + Send>>, - unhandled_default_route_changes: bool, - primary_interface_monitor: interface::PrimaryInterfaceMonitor, - interface_change_rx: UnboundedReceiver<interface::InterfaceEvent>, } impl RouteManagerImpl { @@ -106,61 +127,42 @@ impl RouteManagerImpl { ) -> Result<Self> { let (primary_interface_monitor, interface_change_rx) = interface::PrimaryInterfaceMonitor::new(); + + let (best_route_rx_v4, best_route_rx_v6) = + DefaultRouteMonitor::start(primary_interface_monitor, interface_change_rx); + let routing_table = RoutingTable::new().map_err(Error::RoutingTable)?; let update_trigger = BurstGuard::new( BURST_BUFFER_PERIOD, BURST_LONGEST_BUFFER_PERIOD, move || { - let Some(manage_tx) = manage_tx.upgrade() else { - return; - }; - let _ = manage_tx.unbounded_send(RouteManagerCommand::RefreshRoutes); + if let Some(manage_tx) = manage_tx.upgrade() { + let _ = manage_tx.unbounded_send(RouteManagerCommand::RefreshRoutes); + } }, ); Ok(Self { routing_table, non_tunnel_routes: HashSet::new(), - v4_tunnel_default_route: None, - v6_tunnel_default_route: None, + tunnel_default_routes: IpMap::new(), applied_routes: BTreeMap::new(), - v4_default_route: None, - v6_default_route: None, - update_trigger, + best_default_route: IpMap::new(), + best_default_route_update: IpMap::new(), default_route_listeners: vec![], - interface_change_listeners: vec![], + best_default_route_rx_v4: best_route_rx_v4, + best_default_route_rx_v6: best_route_rx_v6, + update_trigger, check_default_routes_restored: Box::pin(futures::stream::pending()), unhandled_default_route_changes: false, - primary_interface_monitor, - interface_change_rx, + interface_change_listeners: vec![], }) } pub(crate) async fn run(mut self, manage_rx: mpsc::UnboundedReceiver<RouteManagerCommand>) { let mut manage_rx = manage_rx.fuse(); - // 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) - .unwrap_or_else(|error| { - log::error!( - "{}", - error.display_chain_with_msg("Failed to get initial default v4 route") - ); - false - }); - self.update_best_default_route(interface::Family::V6) - .unwrap_or_else(|error| { - log::error!( - "{}", - error.display_chain_with_msg("Failed to get initial default v6 route") - ); - false - }); - - self.debug_offline(); - let mut completion_tx = None; loop { @@ -177,14 +179,21 @@ impl RouteManagerImpl { if self.check_default_routes_restored.is_terminated() { continue; } + if self.try_restore_default_routes().await { log::debug!("Unscoped routes were already restored"); self.check_default_routes_restored = Box::pin(futures::stream::pending()); } } - _event = self.interface_change_rx.next() => { - self.update_trigger.trigger(); + new_best_route = self.best_default_route_rx_v4.next() => { + let Some(new_best_route)= new_best_route else { continue }; + self.handle_new_best_default_route(interface::Family::V4, new_best_route); + } + + new_best_route = self.best_default_route_rx_v6.next() => { + let Some(new_best_route)= new_best_route else { continue }; + self.handle_new_best_default_route(interface::Family::V6, new_best_route); } command = manage_rx.next() => { @@ -200,18 +209,18 @@ impl RouteManagerImpl { let _ = tx.send(events_rx); } Some(RouteManagerCommand::GetDefaultRoutes(tx)) => { - let v4_route = self.v4_default_route.clone(); - let v6_route = self.v6_default_route.clone(); + let v4_route = self.best_default_route.get(interface::Family::V4).cloned(); + let v6_route = self.best_default_route.get(interface::Family::V6).cloned(); let _ = tx.send((v4_route, v6_route)); } Some(RouteManagerCommand::GetDefaultGateway(tx)) => { let mut v4_gateway = None; let mut v6_gateway = None; - if let Some(v4_route) = &self.v4_default_route { + if let Some(v4_route) = self.best_default_route.get(interface::Family::V4) { v4_gateway = self.get_gateway_link_address(v4_route.router_ip).await; } - if let Some(v6_route) = &self.v6_default_route { + if let Some(v6_route) = self.best_default_route.get(interface::Family::V6) { v6_gateway = self.get_gateway_link_address(v6_route.router_ip).await; } let _ = tx.send((v4_gateway, v6_gateway)); @@ -326,15 +335,15 @@ impl RouteManagerImpl { // Default routes are a special case: We must apply it after replacing the current // default route with an ifscope route. if route.prefix.prefix() == 0 { - if route.prefix.is_ipv4() { - self.v4_tunnel_default_route = Some(message); + let family = if route.prefix.is_ipv4() { + interface::Family::V4 } else { - self.v6_tunnel_default_route = Some(message); - } + interface::Family::V6 + }; + self.tunnel_default_routes.insert(family, message); continue; } - // Add route self.add_route_with_record(message).await?; } @@ -355,6 +364,8 @@ impl RouteManagerImpl { ) { talpid_types::detect_flood!(); + log::trace!("got RouteSocketMessage::{:?}", message.as_ref().unwrap()); + match message { Ok(RouteSocketMessage::DeleteRoute(route)) => { // Forget about applied route, if relevant @@ -374,8 +385,7 @@ impl RouteManagerImpl { } self.update_trigger.trigger(); } - Ok(RouteSocketMessage::AddRoute(route)) - | Ok(RouteSocketMessage::ChangeRoute(route)) => { + Ok(RouteSocketMessage::AddRoute(route) | RouteSocketMessage::ChangeRoute(route)) => { if route.errno() != 0 { return; } @@ -423,17 +433,18 @@ impl RouteManagerImpl { async fn refresh_routes(&mut self) -> Result<()> { talpid_types::detect_flood!(); - // These may set `self.unhandled_default_route_changes` - self.update_best_default_route(interface::Family::V4)?; - self.update_best_default_route(interface::Family::V6)?; - - self.debug_offline(); + for (_, event) in self.best_default_route_update.drain() { + self.default_route_listeners + .retain(|tx| tx.unbounded_send(event).is_ok()); + } if !self.unhandled_default_route_changes { - self.ensure_default_tunnel_routes_exists().await?; + self.ensure_default_tunnel_routes_exist().await?; return Ok(()); } + log::trace!("Refreshing routes"); + // Remove any existing ifscoped default route that we've added self.remove_applied_routes(|route| { route.is_ifscope() && route.is_default().unwrap_or(false) @@ -451,56 +462,24 @@ impl RouteManagerImpl { Ok(()) } - fn debug_offline(&self) { - if self.v4_default_route.is_none() && self.v6_default_route.is_none() { - self.primary_interface_monitor.debug(); - } - } - - /// 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. - /// - /// The "best route" is determined by the first interface in the network service order that has - /// a valid IP address and gateway. - /// - /// 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 = self.primary_interface_monitor.get_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); - } - - self.unhandled_default_route_changes = true; - - let old_pair = current_route - .as_ref() - .map(|r| (r.interface_index, r.router_ip)); - let new_pair = best_route - .as_ref() - .map(|r| (r.interface_index, r.router_ip)); - log::debug!("Best default route ({family}) changed from {old_pair:?} to {new_pair:?}"); - let _ = std::mem::replace(current_route, best_route); - - let changed = current_route.is_some(); - self.notify_default_route_listeners(family, changed); - Ok(true) - } + /// Handle a new [DefaultRoute] received from [DefaultRouteMonitor]. + fn handle_new_best_default_route( + &mut self, + family: interface::Family, + new_best_route: Option<DefaultRoute>, + ) { + log::trace!("Best route ({family:?}): {new_best_route:?}"); - fn notify_default_route_listeners(&mut self, family: interface::Family, changed: bool) { - // Notify default route listeners - let event = match (family, changed) { + let event = match (family, new_best_route.is_some()) { (interface::Family::V4, true) => DefaultRouteEvent::AddedOrChangedV4, - (interface::Family::V6, true) => DefaultRouteEvent::AddedOrChangedV6, (interface::Family::V4, false) => DefaultRouteEvent::RemovedV4, + (interface::Family::V6, true) => DefaultRouteEvent::AddedOrChangedV6, (interface::Family::V6, false) => DefaultRouteEvent::RemovedV6, }; - self.default_route_listeners - .retain(|tx| tx.unbounded_send(event).is_ok()); + self.best_default_route_update.insert(family, event); + self.best_default_route.set(family, new_best_route); + self.unhandled_default_route_changes = true; + self.update_trigger.trigger(); } /// Replace the default routes with an ifscope route, and @@ -510,33 +489,18 @@ impl RouteManagerImpl { // route for both IPv4 and IPv6. // NOTE: This is incorrect. We're assuming that any "default destination" is used for // tunneling. - let (v4_conn, v6_conn) = self - .non_tunnel_routes - .iter() - .fold((false, false), |(v4, v6), route| { - (v4 || route.is_ipv4(), v6 || route.is_ipv6()) - }); - let relay_route_is_valid = (v4_conn && self.v4_default_route.is_some()) - || (v6_conn && self.v6_default_route.is_some()); + let v4_conn = self.non_tunnel_routes.iter().any(|r| r.is_ipv4()); + let v6_conn = self.non_tunnel_routes.iter().any(|r| r.is_ipv6()); + + let relay_route_is_valid = (v4_conn + && self.best_default_route.get(interface::Family::V4).is_some()) + || (v6_conn && self.best_default_route.get(interface::Family::V6).is_some()); if !relay_route_is_valid { return Ok(()); } - for tunnel_route in [ - self.v4_tunnel_default_route.clone(), - self.v6_tunnel_default_route.clone(), - ] { - let tunnel_route = match tunnel_route { - Some(route) => route, - None => continue, - }; - let family = if tunnel_route.is_ipv4() { - interface::Family::V4 - } else { - interface::Family::V6 - }; - + for (family, tunnel_route) in self.tunnel_default_routes.clone().iter() { // Replace the default route with an ifscope route self.replace_with_scoped_route(family).await?; @@ -545,6 +509,7 @@ impl RouteManagerImpl { let actual_default_route = self.get_actual_default_route(family).await.unwrap_or(None); if let Some(actual_default_route) = actual_default_route { + // TODO: compare interface index instead? let tun_gateway_link_addr = tunnel_route.gateway().and_then(|addr| addr.as_link_addr()); let actual_link_addr = actual_default_route @@ -563,8 +528,8 @@ impl RouteManagerImpl { } } - log::debug!("Adding default route for tunnel"); - self.add_route_with_record(tunnel_route).await?; + log::debug!("Adding default route for tunnel ({family:?})"); + self.add_route_with_record(tunnel_route.clone()).await?; } Ok(()) @@ -574,12 +539,12 @@ impl RouteManagerImpl { /// a default route, this function replaces the non-tunnel default route with an ifscope route. async fn apply_non_tunnel_routes(&mut self) -> Result<()> { let v4_gateway = self - .v4_default_route - .as_ref() + .best_default_route + .get(interface::Family::V4) .map(|route| SocketAddr::new(route.router_ip, 0)); let v6_gateway = self - .v6_default_route - .as_ref() + .best_default_route + .get(interface::Family::V6) .map(|route| SocketAddr::new(route.router_ip, 0)); // Reapply routes that use the default (non-tunnel) node @@ -614,7 +579,7 @@ impl RouteManagerImpl { /// Replace a known default route with an ifscope route. async fn replace_with_scoped_route(&mut self, family: interface::Family) -> Result<()> { - let Some(default_route) = get_current_best_default_route!(self, family) else { + let Some(default_route) = self.best_default_route.get(family) else { return Ok(()); }; @@ -646,8 +611,8 @@ impl RouteManagerImpl { self.remove_applied_routes(|_| true).await; // We have already removed the applied default routes - self.v4_tunnel_default_route = None; - self.v6_tunnel_default_route = None; + self.tunnel_default_routes.remove(interface::Family::V4); + self.tunnel_default_routes.remove(interface::Family::V6); self.try_restore_default_routes().await; @@ -712,24 +677,30 @@ 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) = self.primary_interface_monitor.get_route(family) else { + let Some(desired_default_route) = self.best_default_route.get(family).cloned() else { return true; }; let desired_default_route = RouteMessage::from(desired_default_route); let current_default_route = RouteMessage::new_route(family.default_network().into()); + if let Ok(Some(current_default)) = self.routing_table.get_route(¤t_default_route).await { // We're done if the route we're looking for is already here if route_matches_interface(¤t_default, &desired_default_route) { + log::trace!("current default route for {family:?} matches the desired"); return true; } + + log::trace!("current default route for {family:?} does NOT match the desired"); let _ = self .routing_table .delete_route(¤t_default_route) .await; - }; + } else { + log::trace!("no current default route") + } if let Err(error) = self.routing_table.add_route(&desired_default_route).await { log::trace!("Failed to add unscoped default {family} route: {error}"); @@ -740,9 +711,8 @@ impl RouteManagerImpl { false } - async fn ensure_default_tunnel_routes_exists(&mut self) -> Result<()> { - // TODO: ignore ipv6 if disabled? - for family in [interface::Family::V4, interface::Family::V6] { + async fn ensure_default_tunnel_routes_exist(&mut self) -> Result<()> { + for (family, _) in self.tunnel_default_routes.clone().iter() { if !self.default_route_is_tunnel_route(family).await? { return self.apply_tunnel_default_routes().await; } @@ -759,16 +729,11 @@ impl RouteManagerImpl { return Ok(false); }; - let tunnel_route = match family { - interface::Family::V4 => self.v4_tunnel_default_route.as_ref(), - interface::Family::V6 => self.v6_tunnel_default_route.as_ref(), - }; - - let Some(tunnel_route) = tunnel_route else { + let Some(tunnel_route) = self.tunnel_default_routes.get(family) else { return Ok(false); }; - Ok(route_matches_interface(&actual_default_route, tunnel_route)) + Ok(actual_default_route.interface_index() == tunnel_route.interface_index()) } /// Get the route which goes to `0.0.0.0/0`/`::/0`, if any. diff --git a/talpid-routing/src/unix/macos/watch.rs b/talpid-routing/src/unix/macos/watch.rs index 61c620b2f3..e3f034a841 100644 --- a/talpid-routing/src/unix/macos/watch.rs +++ b/talpid-routing/src/unix/macos/watch.rs @@ -83,6 +83,8 @@ impl RoutingTable { } } + log::trace!("Add route: {message:?}"); + let msg = self .alter_routing_table(message, MessageType::RTM_ADD) .await; @@ -131,6 +133,8 @@ impl RoutingTable { } pub async fn delete_route(&mut self, message: &RouteMessage) -> Result<()> { + log::trace!("Delete route: {message:?}"); + let response = self .alter_routing_table(message, MessageType::RTM_DELETE) .await?; |
