diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | talpid-core/src/resolver.rs | 48 | ||||
| -rw-r--r-- | talpid-routing/src/lib.rs | 2 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/data.rs | 691 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/mod.rs | 22 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/routing_socket.rs | 6 | ||||
| -rw-r--r-- | talpid-routing/src/unix/macos/watch.rs | 10 | ||||
| -rw-r--r-- | test/test-manager/src/tests/macos.rs | 89 |
8 files changed, 470 insertions, 399 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a75f3b085..45086043cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Line wrap the file at 100 chars. Th #### macOS - Fix apps attempting to use IPv6 with in-tunnel IPv6 disabled. +- Re-add missing loopback alias if removed. This fixes some issues with DNS resolution. ## [2025.9] - 2025-09-08 diff --git a/talpid-core/src/resolver.rs b/talpid-core/src/resolver.rs index 1510d805af..d4cb9bf20a 100644 --- a/talpid-core/src/resolver.rs +++ b/talpid-core/src/resolver.rs @@ -43,6 +43,7 @@ use hickory_server::{ use rand::random_range; use socket2::{Domain, Protocol, Socket, Type}; use std::sync::LazyLock; +use talpid_routing::data::RouteSocketMessage; use talpid_types::drop_guard::{OnDrop, on_drop}; use tokio::{ net::{self, UdpSocket}, @@ -456,9 +457,14 @@ impl LocalResolver { log::debug!("Created loopback address {addr}"); + let detect_removed_alias_task = + tokio::spawn(detect_loopback_address_removal(IpAddr::from(addr))); + // Clean up ip address when stopping the resolver let cleanup_ifconfig = on_drop(move || { tokio::task::spawn(async move { + detect_removed_alias_task.abort(); + log::debug!("Cleaning up loopback address {addr}"); if let Err(e) = talpid_macos::net::remove_alias(LOOPBACK, IpAddr::from(addr)).await @@ -616,6 +622,48 @@ fn kill_mdnsresponder() -> io::Result<()> { Ok(()) } +/// Detect when the loopback address is removed on the loopback interface, and add it back whenever +/// that occurs. +async fn detect_loopback_address_removal(addr: IpAddr) -> Result<(), talpid_routing::RouteError> { + let mut routing_table = talpid_routing::RoutingTable::new().map_err(|e| { + log::warn!("Failed to create routing table interface: {e}"); + e + })?; + + // Listen for the loopback address being removed, and add it back if that happens + loop { + let Ok(msg) = routing_table.next_message().await else { + log::trace!("Failed to read next message from routing table"); + continue; + }; + + let RouteSocketMessage::DeleteAddress(msg) = msg else { + continue; + }; + + // The deleted address either matches the one we care about, or we do not know + let matches_addr = msg + .address() + .map(|deleted_addr| deleted_addr == addr) + .unwrap_or(true); + if !matches_addr { + continue; + } + + // Sleep for a bit so we do not spin if something weird is going on + tokio::time::sleep(Duration::from_secs(1)).await; + + log::debug!("Detected possible removal of loopback address {addr}. Adding it back"); + + talpid_macos::net::add_alias(LOOPBACK, addr) + .await + .inspect_err(|e| { + log::warn!("Failed to add loopback {LOOPBACK} alias {addr}: {e}"); + }) + .ok(); + } +} + type LookupResponse<'a> = MessageResponse< 'a, 'a, diff --git a/talpid-routing/src/lib.rs b/talpid-routing/src/lib.rs index 5a089b1c08..d3f828428a 100644 --- a/talpid-routing/src/lib.rs +++ b/talpid-routing/src/lib.rs @@ -26,7 +26,7 @@ use netlink_packet_route::rtnl::constants::RT_TABLE_MAIN; #[cfg(target_os = "macos")] pub use imp::{ PlatformError, - imp::{DefaultRouteEvent, RouteError}, + imp::{DefaultRouteEvent, RouteError, RoutingTable, data}, }; pub use imp::{Error, RouteManagerHandle}; diff --git a/talpid-routing/src/unix/macos/data.rs b/talpid-routing/src/unix/macos/data.rs index 1365be2e44..5e3f433445 100644 --- a/talpid-routing/src/unix/macos/data.rs +++ b/talpid-routing/src/unix/macos/data.rs @@ -5,12 +5,58 @@ use nix::{ }; use std::{ collections::BTreeMap, - ffi::{c_int, c_uchar, c_ushort}, + ffi::c_int, fmt::{self, Debug}, + mem, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, }; +/// Errors associated with route socket and route messages +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Payload buffer didn't match the reported message size in header + #[error("Buffer didn't match reported message size")] + InvalidBuffer(Vec<u8>, AddressFlag), + /// Buffer too small for specific message type + #[error( + "The buffer is too small for msg \"{message_type}\": expected size >= {expect_min_size}, actual {actual_size}" + )] + BufferTooSmall { + /// Type of message + message_type: &'static str, + /// Expected minimum size of the buffer + expect_min_size: usize, + /// Actual size of the buffer + actual_size: usize, + }, + /// Unknown route flag + #[error("Unknown route flag: {0}")] + UnknownRouteFlag(c_int), + /// Unrecognized address flag + #[error("Unrecognized address flag: {0}")] + UnknownAddressFlag(c_int), + /// Mismatched socket address type + #[error("Unrecognized socket address: expected IPv4 or IPv6")] + MismatchedSocketAddress(AddressFlag, Box<SockaddrStorage>), + /// Invalid netmask + #[error("Invalid netmask")] + InvalidNetmask, + /// Route contains no netmask socket address + #[error("Found no route destination")] + NoDestination, + /// Found no netmask + #[error("Found no netmask")] + NoNetmask, + /// Address message does not contain an interface address + #[error("Found no interface address")] + NoInterfaceAddress, +} + +type Result<T> = std::result::Result<T, Error>; + /// Message that describes a route - either an added, removed, changed or plainly retrieved route. +/// +/// This corresponds to RTM_ADD, RTM_DELETE, RTM_CHANGE, or RTM_GET. #[derive(Debug, Clone, PartialEq)] pub struct RouteMessage { // INVARIANT: The `AddressFlag` must match the variant of `RouteSocketAddress`. @@ -22,6 +68,7 @@ pub struct RouteMessage { } impl RouteMessage { + /// Route message for `destination`. pub fn new_route(destination: Destination) -> Self { let mut route_flags = RouteFlag::RTF_STATIC | RouteFlag::RTF_DONE | RouteFlag::RTF_UP; let mut sockaddrs = BTreeMap::new(); @@ -51,6 +98,7 @@ impl RouteMessage { } } + /// Return all addresses in the route message pub fn route_addrs(&self) -> impl Iterator<Item = &RouteSocketAddress> { self.sockaddrs.values() } @@ -61,6 +109,7 @@ impl RouteMessage { .or_else(|| saddr_to_ipv6(sockaddr).map(Into::into)) } + /// Find netmask in the route message pub fn netmask(&self) -> Option<IpAddr> { self.route_addrs() .find_map(|saddr| match saddr { @@ -71,62 +120,29 @@ impl RouteMessage { .and_then(Self::socketaddress_to_ip) } + /// Return whether there is any default route in this message pub fn is_default(&self) -> Result<bool> { Ok(self.is_default_v4()? || self.is_default_v6()?) } + /// Return whether there is a default IPv4 route in this message pub fn is_default_v4(&self) -> Result<bool> { - let destination_is_default = self - .destination_v4()? - .map(|addr| addr == Ipv4Addr::UNSPECIFIED) - .unwrap_or(false); - let netmask = self.route_addrs().find_map(|saddr| match saddr { - RouteSocketAddress::Netmask(addr) => Some(addr), - _ => None, - }); - - // TODO: This might be superfluous - let netmask_is_default = match netmask { - // empty socket address implies that it is a 'default' netmask - Some(None) => true, - Some(Some(addr)) => { - if let Some(netmask_addr) = saddr_to_ipv4(addr) { - netmask_addr.is_unspecified() - } else if let Some(netmask_addr) = saddr_to_ipv6(addr) { - netmask_addr.is_unspecified() - } else { - // if the route socket address describing the netmask isn't a sockaddr_in or a - // sockaddr_in6, it can't possibly be a default route for IP - false - } - } - // absence of a netmask socket address implies that it is a host route - None => false, + let Some(v4_default) = self.destination_ip()? else { + return Ok(false); }; - - Ok(destination_is_default && netmask_is_default) + // TODO: Checking mask might be superfluous + Ok(v4_default.mask().is_unspecified()) } + /// Return whether there is a default IPv6 route in this message pub fn is_default_v6(&self) -> Result<bool> { - Ok(self - .destination_v6()? - .map(|addr| addr == Ipv6Addr::UNSPECIFIED) - .unwrap_or(false)) + self.destination_ip()? + .map(|addr| Ok(addr.ip() == Ipv6Addr::UNSPECIFIED)) + .unwrap_or(Ok(false)) } fn from_byte_buffer(buffer: &[u8]) -> Result<Self> { - let header: rt_msghdr = rt_msghdr::from_bytes(buffer)?; - - let msg_len = usize::from(header.rtm_msglen); - if msg_len > buffer.len() { - return Err(Error::BufferTooSmall { - message_type: "route message (rt_msghdr.msg_len)", - expect_min_size: msg_len, - actual_size: buffer.len(), - }); - } - - let payload = &buffer[ROUTE_MESSAGE_HEADER_SIZE..std::cmp::min(msg_len, buffer.len())]; + let (header, payload) = split_rtmsg_hdr(buffer)?; let route_flags = RouteFlag::from_bits(header.rtm_flags) .ok_or(Error::UnknownRouteFlag(header.rtm_flags))?; @@ -159,6 +175,7 @@ impl RouteMessage { self.sockaddrs.insert(saddr.address_flag(), saddr); } + /// Set the destination address of the route message pub fn set_destination(mut self, destination: Destination) -> Self { match destination { Destination::Network(net) => { @@ -178,23 +195,26 @@ impl RouteMessage { self } + /// Set the MTU of the route message pub fn set_mtu(mut self, mtu: u32) -> Self { self.mtu = mtu; self } - pub fn set_interface_addr(mut self, link: &InterfaceAddress) -> Self { - self.insert_sockaddr(RouteSocketAddress::Gateway(link.address)); - self.route_flags |= RouteFlag::RTF_GATEWAY; + /// Append route flags to the route message + pub fn append_route_flag(mut self, route_flag: RouteFlag) -> Self { + self.route_flags |= route_flag; self } - pub fn set_gateway_sockaddr(mut self, sockaddr: SockaddrStorage) -> Self { - self.insert_sockaddr(RouteSocketAddress::Gateway(Some(sockaddr))); + /// Set the interface address of the route message + pub fn set_interface_addr(mut self, link: &InterfaceAddress) -> Self { + self.insert_sockaddr(RouteSocketAddress::Gateway(link.address)); self.route_flags |= RouteFlag::RTF_GATEWAY; self } + /// Set the gateway address of the route message pub fn set_gateway_addr(mut self, gateway: impl Into<SockaddrStorage>) -> Self { self.insert_sockaddr(RouteSocketAddress::Gateway(Some(gateway.into()))); self.route_flags |= RouteFlag::RTF_GATEWAY; @@ -202,20 +222,7 @@ impl RouteMessage { self } - pub fn set_gateway_route(mut self, is_gateway_route: bool) -> Self { - if is_gateway_route { - self.route_flags.insert(RouteFlag::RTF_GATEWAY); - } else { - self.route_flags.remove(RouteFlag::RTF_GATEWAY); - } - self - } - - pub fn route_flag(mut self, route_flags: RouteFlag) -> Self { - self.route_flags = route_flags; - self - } - + /// Find gateway address of the route message pub fn gateway(&self) -> Option<&SockaddrStorage> { self.route_addrs() .find_map(|saddr| match saddr { @@ -225,47 +232,49 @@ impl RouteMessage { .as_ref() } + /// Gateway address of the route message, iff it is an IP address + /// (rather than, for example, a link-layer address). pub fn gateway_ip(&self) -> Option<IpAddr> { self.gateway_v4() .map(IpAddr::V4) .or(self.gateway_v6().map(IpAddr::V6)) } - pub fn gateway_v4(&self) -> Option<Ipv4Addr> { + fn gateway_v4(&self) -> Option<Ipv4Addr> { saddr_to_ipv4(self.gateway()?) } - pub fn gateway_v6(&self) -> Option<Ipv6Addr> { + fn gateway_v6(&self) -> Option<Ipv6Addr> { saddr_to_ipv6(self.gateway()?) } - pub fn destination_ip(&self) -> Result<IpNetwork> { - if let Some(saddr) = self.destination()? { - if let Some(v4) = saddr.as_sockaddr_in() { - let ip_addr = *SocketAddrV4::from(*v4).ip(); - let netmask = self.netmask().unwrap_or(Ipv4Addr::UNSPECIFIED.into()); - let destination = IpNetwork::with_netmask(ip_addr.into(), netmask) - .map_err(|_| Error::InvalidNetmask)?; - return Ok(destination); - } + /// Destination address of the route message + pub fn destination_ip(&self) -> Result<Option<IpNetwork>> { + let Some(saddr) = self.destination()? else { + return Ok(None); + }; - if let Some(v6) = saddr.as_sockaddr_in6() { - let ip_addr = *SocketAddrV6::from(*v6).ip(); - let netmask = self.netmask().unwrap_or(Ipv6Addr::UNSPECIFIED.into()); - let destination = IpNetwork::with_netmask(ip_addr.into(), netmask) - .map_err(|_| Error::InvalidNetmask)?; - return Ok(destination); - } + if let Some(ip_addr) = saddr_to_ipv4(saddr) { + let netmask = self.netmask().unwrap_or(Ipv4Addr::UNSPECIFIED.into()); + let destination = IpNetwork::with_netmask(ip_addr.into(), netmask) + .map_err(|_| Error::InvalidNetmask)?; + return Ok(Some(destination)); + } - return Err(Error::MismatchedSocketAddress( - AddressFlag::RTA_DST, - Box::new(*saddr), - )); + if let Some(ip_addr) = saddr_to_ipv6(saddr) { + let netmask = self.netmask().unwrap_or(Ipv6Addr::UNSPECIFIED.into()); + let destination = IpNetwork::with_netmask(ip_addr.into(), netmask) + .map_err(|_| Error::InvalidNetmask)?; + return Ok(Some(destination)); } - Err(Error::NoDestination) + + Err(Error::MismatchedSocketAddress( + AddressFlag::RTA_DST, + Box::new(*saddr), + )) } - pub fn destination(&self) -> Result<Option<&SockaddrStorage>> { + fn destination(&self) -> Result<Option<&SockaddrStorage>> { Ok(self .route_addrs() .find_map(|saddr| match saddr { @@ -276,24 +285,13 @@ impl RouteMessage { .as_ref()) } - pub fn destination_v4(&self) -> Result<Option<Ipv4Addr>> { - Ok(self.destination()?.and_then(saddr_to_ipv4)) - } - - pub fn destination_v6(&self) -> Result<Option<Ipv6Addr>> { - Ok(self.destination()?.and_then(saddr_to_ipv6)) - } - - pub fn flags(&self) -> &RouteFlag { - &self.route_flags - } - + /// Serialize into structs/buffers compatible with `PF_ROUTE` sockets pub fn payload( &self, message_type: MessageType, sequence: i32, pid: i32, - ) -> (rt_msghdr, Vec<Vec<u8>>) { + ) -> (libc::rt_msghdr, Vec<Vec<u8>>) { let address_flags = self.route_addrs().fold(AddressFlag::empty(), |flag, addr| { flag | addr.address_flag() }); @@ -310,11 +308,11 @@ impl RouteMessage { let payload_len: usize = payload_bytes.iter().map(Vec::len).sum(); - let rtm_msglen = (payload_len + ROUTE_MESSAGE_HEADER_SIZE) + let rtm_msglen = (payload_len + mem::size_of::<libc::rt_msghdr>()) .try_into() .expect("route message buffer size cannot fit in 32 bits"); - let mut header = super::data::rt_msghdr { + let mut header = libc::rt_msghdr { rtm_msglen, rtm_version: libc::RTM_VERSION.try_into().unwrap(), rtm_type: message_type.bits().try_into().unwrap(), @@ -326,7 +324,20 @@ impl RouteMessage { rtm_errno: 0, rtm_use: 0, rtm_inits: 0, - rtm_rmx: Default::default(), + rtm_rmx: libc::rt_metrics { + rmx_locks: 0, + rmx_mtu: 0, + rmx_hopcount: 0, + rmx_expire: 0, + rmx_recvpipe: 0, + rmx_sendpipe: 0, + rmx_ssthresh: 0, + rmx_rtt: 0, + rmx_rttvar: 0, + rmx_pksent: 0, + rmx_state: 0, + rmx_filler: [0; 3], + }, }; if self.mtu != 0 { @@ -337,89 +348,38 @@ impl RouteMessage { (header, payload_bytes) } + /// Interface index for the route pub fn interface_index(&self) -> u16 { self.interface_index } + /// Set route interface index pub fn set_interface_index(mut self, index: u16) -> Self { self.interface_index = index; self } - pub fn interface_address(&self) -> Option<IpAddr> { - self.get_address(&AddressFlag::RTA_IFA) - } - - fn get_address(&self, address_flag: &AddressFlag) -> Option<IpAddr> { - let addr = self.sockaddrs.get(address_flag)?; - saddr_to_ipv4(addr.inner()?) - .map(IpAddr::from) - .or_else(|| saddr_to_ipv6(addr.inner()?).map(IpAddr::from)) - } - - pub fn interface_sockaddr_index(&self) -> Option<u16> { - self.sockaddrs - .values() - .find_map(|addr| addr.interface_index()) - } - + /// Error associated with this route message pub fn errno(&self) -> i32 { self.errno } - pub fn is_ipv4(&self) -> bool { - self.destination_v4() - .map(|addr| addr.is_some()) - .unwrap_or(false) - } - - pub fn is_ipv6(&self) -> bool { - self.destination_v6() - .map(|addr| addr.is_some()) - .unwrap_or(false) - } - - pub fn is_ifscope(&self) -> bool { + /// Whether this route is an ifscope route. + /// If set, the route is bound to `interface_index`. + pub fn ifscope(&self) -> bool { self.route_flags.contains(RouteFlag::RTF_IFSCOPE) } - pub fn ifscope(&self) -> Option<u16> { - if self.is_ifscope() { - Some(self.interface_index) - } else { - None - } - } - - pub fn unset_ifscope(mut self) -> Self { - self.route_flags.remove(RouteFlag::RTF_IFSCOPE); + /// Turn this route into a scoped (ifscope) route for the interface index. + pub fn set_ifscope(mut self) -> Self { + self.route_flags |= RouteFlag::RTF_IFSCOPE; self } - - pub fn set_ifscope(mut self, iface_index: u16) -> Self { - if iface_index > 0 { - self.interface_index = iface_index; - self.route_flags.insert(RouteFlag::RTF_IFSCOPE); - } else { - self.route_flags.remove(RouteFlag::RTF_IFSCOPE); - } - - self - } -} - -#[derive(Debug)] -#[repr(C)] -struct ifa_msghdr { - ifam_msglen: c_ushort, - ifam_version: c_uchar, - ifam_type: c_uchar, - ifam_addrs: c_int, - ifam_flags: c_int, - ifam_index: c_ushort, - ifam_metric: c_int, } +/// Address message - used for adding or removing interface addresses. +/// +/// This corresponds to RTM_NEWADDR or RTM_DELADDR. #[derive(Debug)] pub struct AddressMessage { sockaddrs: BTreeMap<AddressFlag, RouteSocketAddress>, @@ -427,10 +387,12 @@ pub struct AddressMessage { } impl AddressMessage { - pub fn index(&self) -> u16 { + /// Interface index for the address message + pub fn interface_index(&self) -> u16 { self.interface_index } + /// IP address of the interface pub fn address(&self) -> Result<IpAddr> { self.get_address(&AddressFlag::RTA_IFP) .or_else(|| self.get_address(&AddressFlag::RTA_IFA)) @@ -439,18 +401,19 @@ impl AddressMessage { fn get_address(&self, address_flag: &AddressFlag) -> Option<IpAddr> { let addr = self.sockaddrs.get(address_flag)?; - saddr_to_ipv4(addr.inner()?) + saddr_to_ipv4(addr.address()?) .map(IpAddr::from) - .or_else(|| saddr_to_ipv6(addr.inner()?).map(IpAddr::from)) + .or_else(|| saddr_to_ipv6(addr.address()?).map(IpAddr::from)) } + /// Netmask of the interface pub fn netmask(&self) -> Result<IpAddr> { self.get_address(&AddressFlag::RTA_NETMASK) .ok_or(Error::NoNetmask) } - pub fn from_byte_buffer(buffer: &[u8]) -> Result<Self> { - const HEADER_SIZE: usize = std::mem::size_of::<ifa_msghdr>(); + fn from_byte_buffer(buffer: &[u8]) -> Result<Self> { + const HEADER_SIZE: usize = std::mem::size_of::<libc::ifa_msghdr>(); if HEADER_SIZE > buffer.len() { return Err(Error::BufferTooSmall { message_type: "ifa_msghdr", @@ -460,7 +423,8 @@ impl AddressMessage { } // SAFETY: buffer is pointing to enough memory to contain a valid value for ifa_msghdr - let header: ifa_msghdr = unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const _) }; + let header: libc::ifa_msghdr = + unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const _) }; let msg_len = usize::from(header.ifam_msglen); if msg_len > buffer.len() { @@ -487,45 +451,55 @@ impl AddressMessage { } } +/// Message types for route socket messages (associated with a PF_ROUTE socket). #[derive(Debug)] pub enum RouteSocketMessage { + /// A route add message. + /// + /// This corresponds to RTM_ADD. AddRoute(RouteMessage), + /// A route delete message. + /// + /// This corresponds to RTM_DELETE. DeleteRoute(RouteMessage), + /// A route change message. + /// + /// This corresponds to RTM_CHANGE. ChangeRoute(RouteMessage), + /// A route get message. + /// + /// This corresponds to RTM_GET. GetRoute(RouteMessage), + /// An interface message. + /// + /// This corresponds to RTM_IFINFO. Interface(Interface), + /// An address message. + /// + /// This corresponds to RTM_NEWADDR. AddAddress(AddressMessage), + /// An address message. + /// + /// This corresponds to RTM_DELADDR. DeleteAddress(AddressMessage), + /// Unhandled message type. Other { - header: rt_msghdr_short, - payload: Vec<u8>, - }, - Error { - header: rt_msghdr_short, + /// Message header + header: ffi::rt_msghdr_short, + /// Raw payload of the message payload: Vec<u8>, }, } +/// Destination of a route #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Destination { + /// Single host (i.e., netmask of 255.255.255.255) Host(IpAddr), + /// Network Network(IpNetwork), } -impl Destination { - pub fn is_network(&self) -> bool { - matches!(self, Self::Network(_)) - } - - pub fn default_v4() -> Self { - Destination::Network(IpNetwork::new(Ipv4Addr::UNSPECIFIED.into(), 0).unwrap()) - } - - pub fn default_v6() -> Self { - Destination::Network(IpNetwork::new(Ipv6Addr::UNSPECIFIED.into(), 0).unwrap()) - } -} - impl From<IpAddr> for Destination { fn from(addr: IpAddr) -> Self { Self::Host(addr) @@ -542,53 +516,15 @@ impl From<IpNetwork> for Destination { } } -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Payload buffer didn't match the reported message size in header - #[error("Buffer didn't match reported message size")] - InvalidBuffer(Vec<u8>, AddressFlag), - /// Buffer too small for specific message type - #[error( - "The buffer is too small for msg \"{message_type}\": expected size >= {expect_min_size}, actual {actual_size}" - )] - BufferTooSmall { - message_type: &'static str, - expect_min_size: usize, - actual_size: usize, - }, - /// Unknown route flag - #[error("Unknown route flag: {0}")] - UnknownRouteFlag(c_int), - /// Unrecognized address flag - #[error("Unrecognized address flag: {0}")] - UnknownAddressFlag(c_int), - /// Mismatched socket address type - #[error("Unrecognized socket address: expected IPv4 or IPv6")] - MismatchedSocketAddress(AddressFlag, Box<SockaddrStorage>), - /// Invalid netmask - #[error("Invalid netmask")] - InvalidNetmask, - /// Route contains no netmask socket address - #[error("Found no route destination")] - NoDestination, - /// Found no netmask - #[error("Found no netmask")] - NoNetmask, - /// Address message does not contain an interface address - #[error("Found no interface address")] - NoInterfaceAddress, -} - -type Result<T> = std::result::Result<T, Error>; - impl RouteSocketMessage { + /// Parse a raw buffer from a PF_ROUTE socket into a `RouteSocketMessage` pub fn parse_message(buffer: &[u8]) -> Result<Self> { let route_message = |route_constructor: fn(RouteMessage) -> RouteSocketMessage, buffer| { let route = RouteMessage::from_byte_buffer(buffer)?; Ok(route_constructor(route)) }; - match rt_msghdr_short::from_bytes(buffer) { + match ffi::rt_msghdr_short::from_bytes(buffer) { Some(header) if header.is_type(libc::RTM_ADD) => route_message(Self::AddRoute, buffer), Some(header) if header.is_type(libc::RTM_CHANGE) => { @@ -617,27 +553,39 @@ impl RouteSocketMessage { }), None => Err(Error::BufferTooSmall { message_type: "rt_msghdr_short", - expect_min_size: ROUTE_MESSAGE_HEADER_SHORT_SIZE, + expect_min_size: ffi::rt_msghdr_short::SIZE, actual_size: buffer.len(), }), } } } +/// An interface message. +/// +/// This corresponds to RTM_IFINFO. #[derive(Debug)] pub struct Interface { header: libc::if_msghdr, } impl Interface { + /// Whether the interface is up + /// + /// Corresponds to the IFF_UP flag. pub fn is_up(&self) -> bool { self.header.ifm_flags & nix::libc::IFF_UP != 0 } + /// Interface index + /// + /// Corresponds to ifm_index. pub fn index(&self) -> u16 { self.header.ifm_index } + /// Interface MTU + /// + /// Corresponds to ifi_mtu. pub fn mtu(&self) -> u32 { self.header.ifm_data.ifi_mtu } @@ -793,23 +741,38 @@ bitflags::bitflags! { } } +/// A route socket address #[derive(Clone, PartialEq)] pub enum RouteSocketAddress { + /// Destination address. + /// /// Corresponds to RTA_DST Destination(Option<SockaddrStorage>), - /// RTA_GATEWAY + /// Gateway address. + /// + /// Corresponds to RTA_GATEWAY Gateway(Option<SockaddrStorage>), - /// RTA_NETMASK + /// A netmask. + /// + /// Corresponds to RTA_NETMASK Netmask(Option<SockaddrStorage>), - /// RTA_GENMASK + /// Corresponds to RTA_GENMASK CloningMask(Option<SockaddrStorage>), - /// RTA_IFP + /// Interface name address. + /// + /// Corresponds to RTA_IFP IfName(Option<SockaddrStorage>), - /// RTA_IFA + /// Interface address. + /// + /// Corresponds to RTA_IFA IfSockaddr(Option<SockaddrStorage>), - /// RTA_AUTHOR + /// Author of redirect. + /// + /// Corresponds to RTA_AUTHOR RedirectAuthor(Option<SockaddrStorage>), - /// RTA_BRD + /// Broadcast address. + /// + /// Corresponds to RTA_BRD Broadcast(Option<SockaddrStorage>), } @@ -847,7 +810,7 @@ impl Debug for RouteSocketAddress { } impl RouteSocketAddress { - // Returns a new route socket address and number of bytes read from the buffer + /// Return a new route socket address and number of bytes read from the buffer pub fn new(flag: AddressFlag, buf: &[u8]) -> Result<(Self, u8)> { // If buffer is empty, then the socket address is empty too, the backing buffer shouldn't // be advanced. @@ -856,15 +819,15 @@ impl RouteSocketAddress { } // to get the length and type of - if buf.len() < std::mem::size_of::<sockaddr_hdr>() { + if buf.len() < std::mem::size_of::<ffi::sockaddr_hdr>() { return Err(Error::BufferTooSmall { message_type: "sockaddr_hdr", - expect_min_size: std::mem::size_of::<sockaddr_hdr>(), + expect_min_size: std::mem::size_of::<ffi::sockaddr_hdr>(), actual_size: buf.len(), }); } - let addr_header_ptr = buf.as_ptr() as *const sockaddr_hdr; + let addr_header_ptr = buf.as_ptr() as *const ffi::sockaddr_hdr; // SAFETY: Since `buf` is at least as long as a `sockaddr_hdr`, it's perfectly valid to // read from. let addr_header = unsafe { std::ptr::read(addr_header_ptr) }; @@ -889,8 +852,8 @@ impl RouteSocketAddress { Ok((Self::with_sockaddr(flag, saddr)?, saddr_len)) } - pub fn to_bytes(&self) -> Vec<u8> { - match self.inner() { + fn to_bytes(&self) -> Vec<u8> { + match self.address() { None => vec![0u8; 4], Some(addr) => { let len = usize::try_from(addr.len()).unwrap(); @@ -914,7 +877,7 @@ impl RouteSocketAddress { } } - pub fn address_flag(&self) -> AddressFlag { + fn address_flag(&self) -> AddressFlag { match &self { Self::Destination(_) => AddressFlag::RTA_DST, Self::Gateway(_) => AddressFlag::RTA_GATEWAY, @@ -927,7 +890,7 @@ impl RouteSocketAddress { } } - pub fn inner(&self) -> Option<&SockaddrStorage> { + fn address(&self) -> Option<&SockaddrStorage> { match &self { Self::Gateway(addr) | Self::Destination(addr) @@ -955,19 +918,6 @@ impl RouteSocketAddress { Ok(constructor(sockaddr)) } - - pub fn interface_index(&self) -> Option<u16> { - match self { - Self::IfName(Some(iface)) => { - let index = iface.as_link_addr()?.ifindex(); - Some( - u16::try_from(index) - .expect("interface indexes actually are u16s, nix is just *interesting*"), - ) - } - _ => None, - } - } } /// Route socket addresses should be ordered by their corresponding address flag when a route @@ -978,14 +928,6 @@ impl std::cmp::PartialOrd for RouteSocketAddress { } } -#[repr(C)] -#[derive(Copy, Clone, Debug)] -struct sockaddr_hdr { - sa_len: u8, - sa_family: libc::sa_family_t, - padding: u16, -} - /// An iterator to consume a byte buffer containing socket address structures originating from a /// routing socket message. pub struct RouteSockAddrIterator<'a> { @@ -1054,38 +996,6 @@ impl Iterator for RouteSockAddrIterator<'_> { } } -// struct rt_msghdr { -// u_short rtm_msglen; /* to skip over non-understood messages */ -// u_char rtm_version; /* future binary compatibility */ -// u_char rtm_type; /* message type */ -// u_short rtm_index; /* index for associated ifp */ -// int rtm_flags; /* flags, incl. kern & message, e.g. DONE */ -// int rtm_addrs; /* bitmask identifying sockaddrs in msg */ -// pid_t rtm_pid; /* identify sender */ -// int rtm_seq; /* for sender to identify action */ -// int rtm_errno; /* why failed */ -// int rtm_use; /* from rtentry */ -// u_int32_t rtm_inits; /* which metrics we are initializing */ -// struct rt_metrics rtm_rmx; /* metrics themselves */ -// }; -#[derive(Debug, Clone)] -#[repr(C)] -pub struct rt_msghdr { - pub rtm_msglen: c_ushort, - pub rtm_version: c_uchar, - pub rtm_type: c_uchar, - pub rtm_index: c_ushort, - pub rtm_flags: c_int, - pub rtm_addrs: c_int, - pub rtm_pid: libc::pid_t, - pub rtm_seq: c_int, - pub rtm_errno: c_int, - pub rtm_use: c_int, - pub rtm_inits: u32, - pub rtm_rmx: rt_metrics, -} -const ROUTE_MESSAGE_HEADER_SIZE: usize = std::mem::size_of::<rt_msghdr>(); - fn saddr_to_ipv4(saddr: &SockaddrStorage) -> Option<Ipv4Addr> { let addr = saddr.as_sockaddr_in()?; Some(*SocketAddrV4::from(*addr).ip()) @@ -1096,110 +1006,129 @@ fn saddr_to_ipv6(saddr: &SockaddrStorage) -> Option<Ipv6Addr> { Some(*SocketAddrV6::from(*addr).ip()) } -impl rt_msghdr { - pub fn from_bytes(buf: &[u8]) -> Result<Self> { - if buf.len() >= ROUTE_MESSAGE_HEADER_SIZE { - let ptr = buf.as_ptr(); - // SAFETY: `ptr` is backed by enough valid bytes to contain a rt_msghdr value and it's - // readable. rt_msghdr doesn't contain any pointers so any values are valid. - Ok(unsafe { std::ptr::read(ptr as *const _) }) - } else { - Err(Error::BufferTooSmall { - message_type: "rt_msghdr", - expect_min_size: ROUTE_MESSAGE_HEADER_SIZE, - actual_size: buf.len(), - }) - } +/// A route destination for [RouteMessage]. +#[derive(PartialEq, PartialOrd, Ord, Eq, Clone)] +pub struct RouteDestination { + /// The destination network + pub network: IpNetwork, + /// Interface index, if the route is scoped (RTF_IFSCOPE) + pub ifscope_interface: Option<u16>, + /// Gateway IP address + pub gateway: Option<IpAddr>, +} + +impl TryFrom<&RouteMessage> for RouteDestination { + type Error = Error; + + fn try_from(msg: &RouteMessage) -> std::result::Result<Self, Self::Error> { + let network = msg.destination_ip()?.ok_or(Error::NoDestination)?; + let interface = msg.ifscope().then(|| msg.interface_index()); + let gateway = msg.gateway_ip(); + Ok(Self { + network, + ifscope_interface: interface, + gateway, + }) } } -/// Shorter rt_msghdr version that matches all routing messages -#[derive(Debug)] -#[repr(C)] -pub struct rt_msghdr_short { - pub rtm_msglen: c_ushort, - pub rtm_version: c_uchar, - pub rtm_type: c_uchar, - pub rtm_index: c_ushort, - pub rtm_flags: c_int, - pub rtm_addrs: c_int, - pub rtm_pid: libc::pid_t, - pub rtm_seq: c_int, - pub rtm_errno: c_int, +/// Types from C headers that may not be available in libc crate +#[allow(non_camel_case_types)] +pub mod ffi { + use std::ffi::{c_int, c_uchar, c_ushort}; + + /// Socket address header + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct sockaddr_hdr { + /// Socket address length + pub sa_len: u8, + /// Socket address family + pub sa_family: libc::sa_family_t, + /// Padding + pub padding: u16, + } + + /// Partial rt_msghdr that matches all route messages + /// + /// Docs: https://github.com/apple-open-source/macos/blob/4e997debe4327c8a40fe8b87b15640f3befdb53c/xnu/bsd/net/route.h#L159 + #[derive(Debug)] + #[repr(C)] + pub struct rt_msghdr_short { + /// to skip over non-understood messages + pub rtm_msglen: c_ushort, + /// future binary compatibility + pub rtm_version: c_uchar, + /// message type + pub rtm_type: c_uchar, + /// index for associated ifp + pub rtm_index: c_ushort, + /// flags, incl. kern & message, e.g. DONE + pub rtm_flags: c_int, + /// bitmask identifying sockaddrs in msg + pub rtm_addrs: c_int, + /// identify sender + pub rtm_pid: libc::pid_t, + /// for sender to identify action + pub rtm_seq: c_int, + /// why it failed + pub rtm_errno: c_int, + } } -const ROUTE_MESSAGE_HEADER_SHORT_SIZE: usize = std::mem::size_of::<rt_msghdr_short>(); -impl rt_msghdr_short { - fn is_type(&self, expected_type: i32) -> bool { +impl ffi::rt_msghdr_short { + const SIZE: usize = std::mem::size_of::<ffi::rt_msghdr_short>(); + + /// Check if the message is of the expected type (i.e., check rtm_type) + pub fn is_type(&self, expected_type: i32) -> bool { u8::try_from(expected_type) .map(|expected| self.rtm_type == expected) .unwrap_or(false) } + /// Parse a raw buffer into a `rt_msghdr_short` pub fn from_bytes(buf: &[u8]) -> Option<Self> { - if buf.len() >= ROUTE_MESSAGE_HEADER_SHORT_SIZE { + if buf.len() >= Self::SIZE { let ptr = buf.as_ptr(); // SAFETY: `ptr` is backed by enough valid bytes to contain a rt_msghdr_short value and // is readable. `rt_msghdr_short` doesn't contain any pointers so any values are valid. - Some(unsafe { std::ptr::read(ptr as *const rt_msghdr_short) }) + Some(unsafe { std::ptr::read(ptr as *const ffi::rt_msghdr_short) }) } else { None } } } -#[derive(PartialEq, PartialOrd, Ord, Eq, Clone)] -pub struct RouteDestination { - pub network: IpNetwork, - pub interface: Option<u16>, - pub gateway: Option<IpAddr>, -} +/// Parse a raw buffer into a `rt_msghdr` and the remaining payload/body. +pub fn split_rtmsg_hdr(buf: &[u8]) -> Result<(libc::rt_msghdr, &[u8])> { + const SIZE: usize = std::mem::size_of::<libc::rt_msghdr>(); -impl TryFrom<&RouteMessage> for RouteDestination { - type Error = Error; + let header: libc::rt_msghdr = if buf.len() >= SIZE { + let ptr = buf.as_ptr(); + // SAFETY: `ptr` is backed by enough valid bytes to contain a rt_msghdr value and it's + // readable. rt_msghdr doesn't contain any pointers so any values are valid. + unsafe { std::ptr::read(ptr as *const _) } + } else { + return Err(Error::BufferTooSmall { + message_type: "rt_msghdr", + expect_min_size: SIZE, + actual_size: buf.len(), + }); + }; - fn try_from(msg: &RouteMessage) -> std::result::Result<Self, Self::Error> { - let network = msg.destination_ip()?; - let interface = msg.ifscope(); - let gateway = msg.gateway_ip(); - Ok(Self { - network, - interface, - gateway, - }) + let msg_len = usize::from(header.rtm_msglen); + if msg_len > buf.len() { + return Err(Error::BufferTooSmall { + message_type: "route message (rt_msghdr.msg_len)", + expect_min_size: msg_len, + actual_size: buf.len(), + }); } -} -// Struct containing metrics of various metrics for a specific route -// struct rt_metrics { -// u_int32_t rmx_locks; /* Kernel leaves these values alone */ -// u_int32_t rmx_mtu; /* MTU for this path */ -// u_int32_t rmx_hopcount; /* max hops expected */ -// int32_t rmx_expire; /* lifetime for route, e.g. redirect */ -// u_int32_t rmx_recvpipe; /* inbound delay-bandwidth product */ -// u_int32_t rmx_sendpipe; /* outbound delay-bandwidth product */ -// u_int32_t rmx_ssthresh; /* outbound gateway buffer limit */ -// u_int32_t rmx_rtt; /* estimated round trip time */ -// u_int32_t rmx_rttvar; /* estimated rtt variance */ -// u_int32_t rmx_pksent; /* packets sent using this route */ -// u_int32_t rmx_state; /* route state */ -// u_int32_t rmx_filler[3]; /* will be used for TCP's peer-MSS cache */ -// }; -#[derive(Debug, Default, Clone)] -#[repr(C)] -pub struct rt_metrics { - pub rmx_locks: u32, - pub rmx_mtu: u32, - pub rmx_hopcount: u32, - pub rmx_expire: i32, - pub rmx_recvpipe: u32, - pub rmx_sendpipe: u32, - pub rmx_ssthresh: u32, - pub rmx_rtt: u32, - pub rmx_rttvar: u32, - pub rmx_pksent: u32, - pub rmx_state: u32, - pub rmx_filler: [u32; 3], + // NOTE: rtm_msglen includes the header size + let payload = &buf[mem::size_of::<libc::rt_msghdr>()..msg_len]; + + Ok((header, payload)) } #[test] diff --git a/talpid-routing/src/unix/macos/mod.rs b/talpid-routing/src/unix/macos/mod.rs index e874ef26d4..c734573eda 100644 --- a/talpid-routing/src/unix/macos/mod.rs +++ b/talpid-routing/src/unix/macos/mod.rs @@ -17,7 +17,6 @@ use std::{ time::Duration, }; use talpid_types::ErrorExt; -use watch::RoutingTable; use super::RouteManagerCommand; use data::{Destination, RouteDestination, RouteMessage, RouteSocketMessage}; @@ -25,13 +24,16 @@ use data::{Destination, RouteDestination, RouteMessage, RouteSocketMessage}; pub use super::DefaultRouteEvent; pub use interface::DefaultRoute; -mod data; +/// PF_ROUTE/[RoutingTable] types +pub mod data; mod default_routes; mod interface; mod ip_map; mod routing_socket; mod watch; +pub use watch::RoutingTable; + pub use watch::Error as RouteError; pub type Result<T> = std::result::Result<T, Error>; @@ -329,7 +331,7 @@ impl RouteManagerImpl { }; RouteMessage::new_route(Destination::from(route.prefix)) - .set_gateway_sockaddr(*link_addr) + .set_gateway_addr(*link_addr) .set_interface_index(interface_index as u16) } else { log::error!("Specifying gateway by IP rather than device is unimplemented"); @@ -454,10 +456,8 @@ impl RouteManagerImpl { 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; + self.remove_applied_routes(|route| route.ifscope() && route.is_default().unwrap_or(false)) + .await; // Substitute route with a tunnel route self.apply_tunnel_default_routes().await?; @@ -525,7 +525,7 @@ impl RouteManagerImpl { .routing_table .delete_route(&default_route_msg(family)) .await; - } else if !actual_default_route.is_ifscope() { + } else if !actual_default_route.ifscope() { continue; // Skipping route } } @@ -587,7 +587,9 @@ impl RouteManagerImpl { let interface_index = default_route.interface_index; let default_route = RouteMessage::from(default_route.clone()); - let new_route = default_route.set_ifscope(interface_index); + let new_route = default_route + .set_interface_index(interface_index) + .set_ifscope(); log::trace!("Setting ifscope: {new_route:?}"); @@ -754,7 +756,7 @@ impl RouteManagerImpl { /// 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 = msg.append_route_flag(data::RouteFlag::RTF_GATEWAY); msg } diff --git a/talpid-routing/src/unix/macos/routing_socket.rs b/talpid-routing/src/unix/macos/routing_socket.rs index 0557fa081a..0028ad8e3b 100644 --- a/talpid-routing/src/unix/macos/routing_socket.rs +++ b/talpid-routing/src/unix/macos/routing_socket.rs @@ -16,7 +16,7 @@ use std::{ os::fd::{AsRawFd, RawFd}, }; -use super::data::{MessageType, RouteMessage, rt_msghdr_short}; +use super::data::{MessageType, RouteMessage, ffi::rt_msghdr_short}; use tokio::io::{AsyncWrite, AsyncWriteExt, unix::AsyncFd}; @@ -136,10 +136,10 @@ impl RoutingSocket { std::ptr::copy_nonoverlapping( &header as *const _ as *const u8, msg_buffer.as_mut_ptr(), - size_of::<super::data::rt_msghdr>(), + size_of::<libc::rt_msghdr>(), ); } - let mut sockaddr_buf = &mut msg_buffer[std::mem::size_of::<super::data::rt_msghdr>()..]; + let mut sockaddr_buf = &mut msg_buffer[std::mem::size_of::<libc::rt_msghdr>()..]; for socket_addr in payload { sockaddr_buf .write_all(socket_addr.as_slice()) diff --git a/talpid-routing/src/unix/macos/watch.rs b/talpid-routing/src/unix/macos/watch.rs index 3cee2881aa..1b5531b1b8 100644 --- a/talpid-routing/src/unix/macos/watch.rs +++ b/talpid-routing/src/unix/macos/watch.rs @@ -32,7 +32,7 @@ pub enum Error { Deletion(RouteMessage), } -/// Provides an interface for manipulating the routing table on macOS using a PF_ROUTE socket. +/// Provides an interface for PF_ROUTE sockets pub struct RoutingTable { socket: routing_socket::RoutingSocket, } @@ -47,12 +47,13 @@ pub enum AddResult { } impl RoutingTable { + /// New routing table interface pub fn new() -> Result<Self> { let socket = routing_socket::RoutingSocket::new().map_err(Error::RoutingSocket)?; - Ok(Self { socket }) } + /// Receive the next message from the routing socket pub async fn next_message(&mut self) -> Result<RouteSocketMessage> { let mut buf = [0u8; 2048]; @@ -72,8 +73,9 @@ impl RoutingTable { data::RouteSocketMessage::parse_message(msg_buf).map_err(Error::InvalidMessage) } + /// Add route to the routing table pub async fn add_route(&mut self, message: &RouteMessage) -> Result<AddResult> { - if let Ok(destination) = message.destination_ip() { + if let Ok(Some(destination)) = message.destination_ip() { if Some(destination.ip()) == message.gateway_ip() { // Workaround that allows us to reach a wg peer on our router. // If we don't do this, adding the route fails due to errno 49 @@ -132,6 +134,7 @@ impl RoutingTable { } } + /// Delete route from the routing table pub async fn delete_route(&mut self, message: &RouteMessage) -> Result<()> { log::trace!("Delete route: {message:?}"); @@ -151,6 +154,7 @@ impl RoutingTable { } } + /// Get route from the routing table pub async fn get_route( &mut self, message: &RouteMessage, diff --git a/test/test-manager/src/tests/macos.rs b/test/test-manager/src/tests/macos.rs index 140f77bb10..f05d8fb6df 100644 --- a/test/test-manager/src/tests/macos.rs +++ b/test/test-manager/src/tests/macos.rs @@ -2,12 +2,56 @@ use anyhow::{Context, bail, ensure}; use mullvad_management_interface::MullvadProxyClient; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use test_macro::test_function; use test_rpc::ServiceClient; +use crate::tests::helpers::connect_and_wait; + use super::TestContext; +/// Test that the local resolver alias is readded if removed. +#[test_function(target_os = "macos")] +async fn test_app_ifconfig_alias( + _: TestContext, + rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + // Connect to enable the local resolver + connect_and_wait(&mut mullvad_client).await?; + + let current_resolver = get_first_dns_resolver(&rpc).await?; + log::debug!("Current DNS resolver: {current_resolver}"); + + let current_resolver = match current_resolver { + IpAddr::V4(ip) => ip, + IpAddr::V6(ip) => bail!("Expected IPv4 resolver, got {ip}"), + }; + + ensure!( + current_resolver.is_loopback() && current_resolver != Ipv4Addr::LOCALHOST, + "Current resolver should be a loopback address (and not 127.0.0.1), got {current_resolver}" + ); + + // Remove all alias and assert that one is readded. + rpc.ifconfig_alias_remove("lo0", current_resolver).await?; + + ensure!( + !alias_exists(&rpc, "lo0", current_resolver).await?, + "Aliases should have been removed" + ); + + for _attempt in 0..5 { + if alias_exists(&rpc, "lo0", current_resolver).await? { + log::debug!("Alias was readded!"); + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + + bail!("lo0 alias was not readded after removal"); +} + /// Test that we can add and remove IP "aliases" to network interfaces. /// /// This is effectively testing that macOS behaves as expected, and that future versions of it @@ -93,3 +137,46 @@ async fn alias_exists( Ok(stdout.contains(&alias)) } + +/// Get first DNS resolver from `scutil --dns` +async fn get_first_dns_resolver(rpc: &ServiceClient) -> anyhow::Result<IpAddr> { + let result = rpc.exec("scutil", ["--dns"]).await?; + + let stdout = String::from_utf8(result.stdout)?; + let stderr = String::from_utf8(result.stderr)?; + + if result.code != Some(0) { + log::error!("scutil stdout:\n{stdout}"); + log::error!("scutil stderr:\n{stderr}"); + bail!("`scutil` exited with code {:?}", result.code); + } + + parse_scutil_dns_first_resolver(&stdout).context("No resolver found") +} + +fn parse_scutil_dns_first_resolver(output: &str) -> Option<IpAddr> { + output + .lines() + .map(str::trim) + // nameserver[0] : 127.230.79.91 + .flat_map(|line| line.strip_prefix("nameserver[0]")) + .flat_map(|server| server.split_whitespace().last()) + .find_map(|addr| addr.parse().ok()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_scutil_dns_first_resolver() { + let out = r#"resolver #1 + nameserver[0] : 127.230.79.91 + if_index : 11 (en0) + flags : Scoped, Request A records + reach : 0x00000000 (Not Reachable)"#; + + let aliases = parse_scutil_dns_first_resolver(out); + assert_eq!(aliases, Some("127.230.79.91".parse::<IpAddr>().unwrap()),); + } +} |
