summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-04-23 17:12:52 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-04-23 17:12:52 +0200
commit2260667e3a16214dc0e79dc3d5e640021cb8f765 (patch)
tree78a2c90ff1de687b8607417057fdb4f9395d1ef8
parent4e5168734f69ad696a84a8436b5dda2712e0266c (diff)
parente947e34ce6e83013f8d5bee86de3d59350efe447 (diff)
downloadmullvadvpn-2260667e3a16214dc0e79dc3d5e640021cb8f765.tar.xz
mullvadvpn-2260667e3a16214dc0e79dc3d5e640021cb8f765.zip
Merge branch 'macos-ipv6-dissapearing-route'
-rw-r--r--talpid-routing/src/unix/macos/data.rs80
-rw-r--r--talpid-routing/src/unix/macos/default_routes.rs279
-rw-r--r--talpid-routing/src/unix/macos/interface.rs334
-rw-r--r--talpid-routing/src/unix/macos/ip_map.rs67
-rw-r--r--talpid-routing/src/unix/macos/mod.rs378
-rw-r--r--talpid-routing/src/unix/macos/routing_socket.rs2
-rw-r--r--talpid-routing/src/unix/macos/watch.rs4
7 files changed, 828 insertions, 316 deletions
diff --git a/talpid-routing/src/unix/macos/data.rs b/talpid-routing/src/unix/macos/data.rs
index 66644d6ce3..5ee2b24a06 100644
--- a/talpid-routing/src/unix/macos/data.rs
+++ b/talpid-routing/src/unix/macos/data.rs
@@ -6,12 +6,14 @@ use nix::{
use std::{
collections::BTreeMap,
ffi::{c_int, c_uchar, c_ushort},
+ fmt::{self, Debug},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
};
/// 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,
@@ -789,7 +791,7 @@ bitflags::bitflags! {
}
}
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Clone, PartialEq)]
pub enum RouteSocketAddress {
/// Corresponds to RTA_DST
Destination(Option<SockaddrStorage>),
@@ -809,6 +811,39 @@ pub enum RouteSocketAddress {
Broadcast(Option<SockaddrStorage>),
}
+/// Custom Debug-impl that uses the Display-impl of [SockaddrStorage] since its Debug-impl is
+/// basically unreadable.
+impl Debug for RouteSocketAddress {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let (variant, sockaddr) = match self {
+ Self::Destination(sockaddr) => ("Destination", sockaddr),
+ Self::Gateway(sockaddr) => ("Gateway", sockaddr),
+ Self::Netmask(sockaddr) => ("Netmask", sockaddr),
+ Self::CloningMask(sockaddr) => ("CloningMask", sockaddr),
+ Self::IfName(sockaddr) => ("IfName", sockaddr),
+ Self::IfSockaddr(sockaddr) => ("IfSockaddr", sockaddr),
+ Self::RedirectAuthor(sockaddr) => ("RedirectAuthor", sockaddr),
+ Self::Broadcast(sockaddr) => ("Broadcast", sockaddr),
+ };
+
+ if let Some(sockaddr) = sockaddr {
+ if let Some(link_addr) = sockaddr.as_link_addr() {
+ // The default Display impl for LinkAddrs does not print ifindex
+ write!(f, "{variant}(")?;
+ f.debug_struct("LinkAddr")
+ .field("addr", &link_addr.addr())
+ .field("iface", &link_addr.ifindex())
+ .finish()?;
+ write!(f, ")")
+ } else {
+ write!(f, "{variant}({sockaddr})")
+ }
+ } else {
+ write!(f, "{variant}(None)")
+ }
+ }
+}
+
impl RouteSocketAddress {
// Returns a new route socket address and number of bytes read from the buffer
pub fn new(flag: AddressFlag, buf: &[u8]) -> Result<(Self, u8)> {
@@ -953,17 +988,15 @@ struct sockaddr_hdr {
/// routing socket message.
pub struct RouteSockAddrIterator<'a> {
buffer: &'a [u8],
- flags: AddressFlag,
- // Cursor used to iterate through address flags
- flag_cursor: i32,
+ /// Iterator over all the set bits in the provided [AddressFlag].
+ flags_iter: bitflags::iter::Iter<AddressFlag>,
}
impl<'a> RouteSockAddrIterator<'a> {
fn new(buffer: &'a [u8], flags: AddressFlag) -> Self {
Self {
buffer,
- flags,
- flag_cursor: AddressFlag::RTA_DST.bits(),
+ flags_iter: flags.iter(),
}
}
@@ -995,25 +1028,26 @@ impl Iterator for RouteSockAddrIterator<'_> {
type Item = Result<RouteSocketAddress>;
fn next(&mut self) -> Option<Self::Item> {
- loop {
- // If address flags don't contain the current one, try the next one.
- // Will return None if it runs out of valid flags.
- let current_flag = AddressFlag::from_bits(self.flag_cursor)?;
- self.flag_cursor <<= 1;
+ // Will return None if it runs out of set flags.
+ let current_flag = self.flags_iter.next()?;
+
+ // Any undefiend flags are all returned as a clump in the final iteration.
+ let no_undefined_flags = AddressFlag::all().contains(current_flag);
+ debug_assert!(
+ no_undefined_flags,
+ "AddressFlag contained undefined bits! {current_flag:?}. \
+ Consider adding them to the definition."
+ );
- if !self.flags.contains(current_flag) {
- continue;
+ match RouteSocketAddress::new(current_flag, self.buffer) {
+ Ok((next_addr, addr_len)) => {
+ self.advance_buffer(addr_len);
+ Some(Ok(next_addr))
+ }
+ Err(err) => {
+ self.buffer = &[];
+ Some(Err(err))
}
- return match RouteSocketAddress::new(current_flag, self.buffer) {
- Ok((next_addr, addr_len)) => {
- self.advance_buffer(addr_len);
- Some(Ok(next_addr))
- }
- Err(err) => {
- 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 df89767e38..975ebef927 100644
--- a/talpid-routing/src/unix/macos/mod.rs
+++ b/talpid-routing/src/unix/macos/mod.rs
@@ -1,11 +1,14 @@
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 nix::net::if_::if_nametoindex;
use std::{
collections::{BTreeMap, HashSet},
net::{IpAddr, SocketAddr},
@@ -23,7 +26,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 +63,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 +75,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 +128,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 +180,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 +210,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));
@@ -303,17 +313,24 @@ impl RouteManagerImpl {
// Add routes not using the default interface
for route in routes_to_apply {
- let mut message = if let Some(ref device) = route.node.device {
- // If we specify route by interface name, use the link address of the given
- // interface
- match interface_link_addrs.get(device) {
- Some(link_addr) => RouteMessage::new_route(Destination::from(route.prefix))
- .set_gateway_sockaddr(*link_addr),
- None => {
- log::error!("Route with unknown device: {route:?}, {device}");
- continue;
- }
- }
+ let mut message = if let Some(device) = route.node.get_device() {
+ // Get the link-address of the provided network interface (device).
+ // We need the link address to create a route that targets the interface.
+ let Some(link_addr) = interface_link_addrs.get(device) else {
+ log::error!("Route with unknown device: {route:?}, {device}");
+ continue;
+ };
+
+ // Get the index of the network interface. This is not needed to create the route,
+ // but we use it to later validate that the route is correct.
+ let Ok(interface_index) = if_nametoindex(device) else {
+ log::error!("Route with unknown device: {route:?}, {device}");
+ continue;
+ };
+
+ RouteMessage::new_route(Destination::from(route.prefix))
+ .set_gateway_sockaddr(*link_addr)
+ .set_interface_index(interface_index as u16)
} else {
log::error!("Specifying gateway by IP rather than device is unimplemented");
continue;
@@ -326,19 +343,19 @@ 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?;
}
- self.apply_tunnel_default_route().await?;
+ self.apply_tunnel_default_routes().await?;
// Add routes that use the default interface
if let Err(error) = self.apply_non_tunnel_routes().await {
@@ -355,6 +372,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 +393,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,23 +441,26 @@ impl RouteManagerImpl {
async fn refresh_routes(&mut self) -> Result<()> {
talpid_types::detect_flood!();
- 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_exist().await?;
return Ok(());
}
- // Remove any existing ifscope route that we've added
+ 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)
})
.await;
// Substitute route with a tunnel route
- self.apply_tunnel_default_route().await?;
+ self.apply_tunnel_default_routes().await?;
// Update routes using default interface
self.apply_non_tunnel_routes().await?;
@@ -449,117 +470,68 @@ 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
/// add a new default tunnel route.
- async fn apply_tunnel_default_route(&mut self) -> Result<()> {
+ async fn apply_tunnel_default_routes(&mut self) -> Result<()> {
// As long as the relay route has a way of reaching the internet, we'll want to add a tunnel
// 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?;
// Make sure there is really no other unscoped default route
- let mut msg = RouteMessage::new_route(family.default_network().into());
- msg = msg.set_gateway_route(true);
- let old_route = self.routing_table.get_route(&msg).await;
- if let Ok(Some(old_route)) = old_route {
- let tun_gateway_link_addr =
- tunnel_route.gateway().and_then(|addr| addr.as_link_addr());
- let current_link_addr = old_route.gateway().and_then(|addr| addr.as_link_addr());
- if current_link_addr
- .map(|addr| Some(addr) != tun_gateway_link_addr)
- .unwrap_or(true)
- {
+
+ let actual_default_route = self.get_actual_default_route(family).await.unwrap_or(None);
+
+ if let Some(actual_default_route) = actual_default_route {
+ debug_assert!(tunnel_route.interface_index() != 0);
+ if tunnel_route.interface_index() != actual_default_route.interface_index() {
log::trace!("Removing existing unscoped default route");
- let _ = self.routing_table.delete_route(&msg).await;
- } else if !old_route.is_ifscope() {
- // NOTE: Skipping route
- continue;
+
+ let _ = self
+ .routing_table
+ .delete_route(&default_route_msg(family))
+ .await;
+ } else if !actual_default_route.is_ifscope() {
+ continue; // Skipping route
}
}
- 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(())
@@ -569,12 +541,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
@@ -609,7 +581,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(());
};
@@ -641,8 +613,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;
@@ -707,24 +679,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(&current_default_route).await
{
// We're done if the route we're looking for is already here
if route_matches_interface(&current_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(&current_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}");
@@ -734,6 +712,50 @@ impl RouteManagerImpl {
false
}
+
+ 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;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Check if the `0.0.0.0/0`/`::/0`-route goes to our tunnel interface.
+ async fn default_route_is_tunnel_route(&mut self, family: interface::Family) -> Result<bool> {
+ let actual_default_route = self.get_actual_default_route(family).await?;
+
+ let Some(actual_default_route) = actual_default_route else {
+ return Ok(false);
+ };
+
+ let Some(tunnel_route) = self.tunnel_default_routes.get(family) else {
+ return Ok(false);
+ };
+
+ Ok(actual_default_route.interface_index() == tunnel_route.interface_index())
+ }
+
+ /// Get the route which goes to `0.0.0.0/0`/`::/0`, if any.
+ async fn get_actual_default_route(
+ &mut self,
+ family: interface::Family,
+ ) -> Result<Option<data::RouteMessage>> {
+ self.routing_table
+ .get_route(&default_route_msg(family))
+ .await
+ .map_err(Error::RoutingTable)
+ }
+}
+
+/// Construct a [RouteMessage] that refers to the `0.0.0.0/0` or `::/0` route with the
+/// RTF_GATEAWAY-flag set. Used to reference the default route created by macOS.
+fn default_route_msg(family: interface::Family) -> RouteMessage {
+ let mut msg = RouteMessage::new_route(family.default_network().into());
+ msg = msg.set_gateway_route(true);
+ msg
}
fn route_matches_interface(default_route: &RouteMessage, interface_route: &RouteMessage) -> bool {
diff --git a/talpid-routing/src/unix/macos/routing_socket.rs b/talpid-routing/src/unix/macos/routing_socket.rs
index 763dbb9264..cfa0f0973f 100644
--- a/talpid-routing/src/unix/macos/routing_socket.rs
+++ b/talpid-routing/src/unix/macos/routing_socket.rs
@@ -173,7 +173,7 @@ impl RoutingSocketInner {
let mut guard = self.socket.readable().await?;
match guard.try_io(|sock| sock.get_ref().read(out)) {
Ok(result) => return result,
- Err(_err) => continue,
+ Err(_would_block) => continue,
}
}
}
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?;