diff --git a/CHANGELOG.md b/CHANGELOG.md index 503b2ad8ede8..1234d9fc12d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,13 @@ Line wrap the file at 100 chars. Th - Don't hijack DNS when localhost is configured. This is more in line with other platforms. Unexpected DNS traffic is still blocked when leaving the host. - Enable IPv6 by default. This fixes DNS and routing being broken on some platforms. +- Proxy DNS queries through a local resolver. + +### Fixed +#### macOS +- Fix Apple leak toggle not working. The issue was that DNS queries to the tunnel resolver were + being sent on the physical interface. + ## [2024.6-beta1] - 2024-09-26 ### Added diff --git a/Cargo.lock b/Cargo.lock index d07de1332cdb..c34a60d2fb57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4107,6 +4107,7 @@ dependencies = [ "subslice", "system-configuration", "talpid-dbus", + "talpid-macos", "talpid-net", "talpid-openvpn", "talpid-platform-metadata", @@ -4149,6 +4150,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "talpid-macos" +version = "0.0.0" +dependencies = [ + "libc", + "log", +] + [[package]] name = "talpid-net" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 3157d70d1bea..32757afc7881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "talpid-core", "talpid-dbus", "talpid-future", + "talpid-macos", "talpid-net", "talpid-openvpn", "talpid-openvpn-plugin", diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 5fab3022aba3..a0ddb45825da 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -56,9 +56,10 @@ talpid-platform-metadata = { path = "../talpid-platform-metadata" } pcap = { version = "2.1", features = ["capture-stream"] } pnet_packet = "0.34" tun = { version = "0.5.5", features = ["async"] } -nix = { version = "0.28", features = ["socket"] } +nix = { version = "0.28", features = ["socket", "signal"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +talpid-macos = { path = "../talpid-macos" } talpid-net = { path = "../talpid-net" } [target.'cfg(windows)'.dependencies] diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index ef26ebb97f62..2dddc2381e33 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -98,6 +98,17 @@ impl Firewall { let remote_address = state.remote_address()?; let proto = state.proto()?; + if local_address.ip().is_loopback() || remote_address.ip().is_loopback() { + // Ignore connections to localhost + return Ok(false); + } + + if [5353, 53].contains(&remote_address.port()) { + // Ignore DNS states. The local resolver takes care of everything, + // and PQ seems to timeout if these states are flushed + return Ok(false); + } + let Some(peer) = policy.peer_endpoint().map(|endpoint| endpoint.endpoint) else { // If there's no peer, there's also no tunnel. We have no states to preserve return Ok(true); @@ -177,6 +188,12 @@ impl Firewall { let redirect_rules = match policy { FirewallPolicy::Blocked { dns_redirect_port, .. + } + | FirewallPolicy::Connecting { + dns_redirect_port, .. + } + | FirewallPolicy::Connected { + dns_redirect_port, .. } => { vec![pfctl::RedirectRuleBuilder::default() .action(pfctl::RedirectRuleAction::Redirect) @@ -186,7 +203,6 @@ impl Firewall { .redirect_to(pfctl::Port::from(*dns_redirect_port)) .build()?] } - _ => vec![], }; Ok(redirect_rules) } @@ -204,6 +220,7 @@ impl Firewall { allowed_tunnel_traffic, redirect_interface, apple_services_bypass, + dns_redirect_port: _, } => { let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; rules.push(self.get_allowed_endpoint_rule(allowed_endpoint)?); @@ -253,6 +270,7 @@ impl Firewall { dns_config, redirect_interface, apple_services_bypass, + dns_redirect_port: _, } => { let mut rules = vec![]; diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 2596f330e3ae..af3db8dcb353 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -98,6 +98,10 @@ pub enum FirewallPolicy { /// Flag setting if we should leak traffic to apple services. #[cfg(target_os = "macos")] apple_services_bypass: bool, + /// Destination port for DNS traffic redirection. Traffic destined to `127.0.0.1:53` will + /// be redirected to `127.0.0.1:$dns_redirect_port`. + #[cfg(target_os = "macos")] + dns_redirect_port: u16, }, /// Allow traffic only to server and over tunnel interface @@ -118,6 +122,10 @@ pub enum FirewallPolicy { /// Flag setting if we should leak traffic to apple services. #[cfg(target_os = "macos")] apple_services_bypass: bool, + /// Destination port for DNS traffic redirection. Traffic destined to `127.0.0.1:53` will + /// be redirected to `127.0.0.1:$dns_redirect_port`. + #[cfg(target_os = "macos")] + dns_redirect_port: u16, }, /// Block all network traffic in and out from the computer. diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 84bb4ad90ce6..bb81d7a48fa3 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -41,4 +41,4 @@ mod linux; /// A resolver that's controlled by the tunnel state machine #[cfg(target_os = "macos")] -pub mod resolver; +pub(crate) mod resolver; diff --git a/talpid-core/src/resolver.rs b/talpid-core/src/resolver.rs index 97f1f4b79c00..555edfb3e068 100644 --- a/talpid-core/src/resolver.rs +++ b/talpid-core/src/resolver.rs @@ -1,14 +1,22 @@ +//! This module implements a forwarding DNS resolver with two states: +//! * In the `Blocked` state, most queries receive an empty response, but certain captive portal +//! domains receive a spoofed answer. This fools the OS into thinking that it has connectivity. +//! * In the `Forwarding` state, queries are forwarded to a set of configured DNS servers. This +//! lets us use the routing table to determine where to send them, instead of them being forced +//! out on the primary interface (in some cases). +//! +//! See [start_resolver]. use std::{ io, - net::{Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, sync::{Arc, Weak}, + time::{Duration, Instant}, }; -use std::time::{Duration, Instant}; - use futures::{ channel::{mpsc, oneshot}, + future::Either, SinkExt, StreamExt, }; @@ -24,7 +32,12 @@ use hickory_server::{ op::{header::MessageType, op_code::OpCode, Header}, rr::{domain::Name, rdata, record_data::RData, Record}, }, - resolver::lookup::Lookup, + resolver::{ + config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, + error::{ResolveError, ResolveErrorKind}, + lookup::Lookup, + TokioAsyncResolver, + }, server::{Request, RequestHandler, ResponseHandler, ResponseInfo}, ServerFuture, }; @@ -47,8 +60,8 @@ const RESOLVED_ADDR: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 1); /// Starts a resolver. Returns a cloneable handle, which can activate, deactivate and shut down the /// resolver. When all instances of a handle are dropped, the server will stop. -pub(crate) async fn start_resolver() -> Result { - let (resolver, resolver_handle) = FilteringResolver::new().await?; +pub async fn start_resolver() -> Result { + let (resolver, resolver_handle) = LocalResolver::new().await?; tokio::spawn(resolver.run()); Ok(resolver_handle) } @@ -65,43 +78,193 @@ pub enum Error { GetSocketAddrError(#[source] io::Error), } -/// A filtering resolver. Listens on a specified port for DNS queries and responds queries for -/// `catpive.apple.com`. Can be toggled to unbind, be bound but not respond or bound and responding -/// to some queries. -struct FilteringResolver { - rx: mpsc::Receiver, +/// A DNS resolver that forwards queries to some other DNS server +/// +/// Is controlled by commands sent through [ResolverHandle]s. +struct LocalResolver { + rx: mpsc::UnboundedReceiver, dns_server: Option<(tokio::task::JoinHandle<()>, oneshot::Receiver<()>)>, + inner_resolver: Resolver, +} + +/// A message to [LocalResolver] +enum ResolverMessage { + /// Set resolver config + SetConfig { + /// New DNS config to use + new_config: Config, + /// Response channel when resolvers have been updated + response_tx: oneshot::Sender<()>, + }, + + /// Send a DNS query to the resolver + Query { + dns_query: LowerQuery, + + /// Channel for the query response + response_tx: oneshot::Sender, ResolveError>>, + }, +} + +/// Configuration for [Resolver] +#[derive(Debug, Default, Clone)] +enum Config { + /// Drop DNS queries. For captive portal domains, return faux records. + #[default] + Blocking, + + /// Forward DNS queries to a configured server + Forwarding { + /// Remote DNS server to use + dns_servers: Vec, + }, +} + +enum Resolver { + /// Drop DNS queries. For captive portal domains, return faux records + Blocking, + + /// Forward DNS queries to a configured server + Forwarding(TokioAsyncResolver), +} + +impl From for Resolver { + fn from(mut config: Config) -> Self { + match &mut config { + Config::Blocking => Resolver::Blocking, + Config::Forwarding { dns_servers } => { + // make sure not to accidentally forward queries to ourselves + dns_servers.retain(|addr| !addr.is_loopback()); + + let forward_server_config = + NameServerConfigGroup::from_ips_clear(dns_servers, 53, true); + + let forward_config = + ResolverConfig::from_parts(None, vec![], forward_server_config); + let resolver_opts = ResolverOpts::default(); + + let resolver = TokioAsyncResolver::tokio(forward_config, resolver_opts); + + Resolver::Forwarding(resolver) + } + } + } } -/// The `FilteringResolver` is an actor responding to DNS queries. -type ResolverMessage = (LowerQuery, oneshot::Sender>); +impl Resolver { + pub fn resolve( + &self, + query: LowerQuery, + tx: oneshot::Sender, ResolveError>>, + ) { + let lookup = match self { + Resolver::Blocking => Either::Left(async move { Self::resolve_blocked(query) }), + Resolver::Forwarding(resolver) => { + Either::Right(Self::resolve_forward(resolver.clone(), query)) + } + }; + + tokio::spawn(async move { + let _ = tx.send(lookup.await); + }); + } + + /// Resolution in blocked state will return spoofed records for captive portal domains. + fn resolve_blocked( + query: LowerQuery, + ) -> std::result::Result, ResolveError> { + if !Self::is_captive_portal_domain(&query) { + return Ok(Box::new(EmptyLookup)); + } + + let return_query = query.original().clone(); + let mut return_record = Record::with( + return_query.name().clone(), + return_query.query_type(), + TTL_SECONDS, + ); + return_record.set_data(Some(RData::A(rdata::A(RESOLVED_ADDR)))); + + log::debug!( + "Spoofing query for captive portal domain: {}", + return_query.name() + ); + + let lookup = Lookup::new_with_deadline( + return_query, + Arc::new([return_record]), + Instant::now() + Duration::from_secs(3), + ); + Ok(Box::new(ForwardLookup(lookup)) as Box<_>) + } -/// A handle to control a filtering resolver. When all resolver handles are dropped, custom -/// resolver will stop. + /// Determines whether a DNS query is allowable. Currently, this implies that the query is + /// either a `A` or a `CNAME` query for `captive.apple.com`. + fn is_captive_portal_domain(query: &LowerQuery) -> bool { + ALLOWED_RECORD_TYPES.contains(&query.query_type()) && ALLOWED_DOMAINS.contains(query.name()) + } + + /// Forward DNS queries to the specified DNS resolver. + async fn resolve_forward( + resolver: TokioAsyncResolver, + query: LowerQuery, + ) -> std::result::Result, ResolveError> { + let return_query = query.original().clone(); + + let lookup = resolver + .lookup(return_query.name().clone(), return_query.query_type()) + .await; + + lookup.map(|lookup| Box::new(ForwardLookup(lookup)) as Box<_>) + } +} + +/// A handle to control a DNS resolver. +/// +/// When all resolver handles are dropped, the resolver will stop. #[derive(Clone)] -pub(crate) struct ResolverHandle { - _tx: Arc>, +pub struct ResolverHandle { + tx: Arc>, listening_port: u16, } impl ResolverHandle { - fn new(tx: Arc>, listening_port: u16) -> Self { - Self { - _tx: tx, - listening_port, - } + fn new(tx: Arc>, listening_port: u16) -> Self { + Self { tx, listening_port } } /// Get listening port for resolver handle pub fn listening_port(&self) -> u16 { self.listening_port } + + /// Set the DNS server to forward queries to + pub async fn enable_forward(&self, dns_servers: Vec) { + let (response_tx, response_rx) = oneshot::channel(); + let _ = self.tx.unbounded_send(ResolverMessage::SetConfig { + new_config: Config::Forwarding { dns_servers }, + response_tx, + }); + + let _ = response_rx.await; + } + + // Disable forwarding + pub async fn disable_forward(&self) { + let (response_tx, response_rx) = oneshot::channel(); + let _ = self.tx.unbounded_send(ResolverMessage::SetConfig { + new_config: Config::Blocking, + response_tx, + }); + + let _ = response_rx.await; + } } -impl FilteringResolver { +impl LocalResolver { /// Constructs a new filtering resolver and it's handle. async fn new() -> Result<(Self, ResolverHandle), Error> { - let (tx, rx) = mpsc::channel(0); + let (tx, rx) = mpsc::unbounded(); let command_tx = Arc::new(tx); let weak_tx = Arc::downgrade(&command_tx); @@ -131,9 +294,11 @@ impl FilteringResolver { let _ = server_done_tx.send(()); }); + let resolver = Self { rx, dns_server: Some((server_handle, server_done_rx)), + inner_resolver: Resolver::from(Config::Blocking), }; Ok((resolver, ResolverHandle::new(command_tx, port))) @@ -141,7 +306,7 @@ impl FilteringResolver { async fn new_server( port: u16, - command_tx: Weak>, + command_tx: Weak>, ) -> Result<(ServerFuture, u16), Error> { let mut server = ServerFuture::new(ResolverImpl { tx: command_tx }); @@ -162,8 +327,25 @@ impl FilteringResolver { /// related [ResolverHandle] instances are dropped, this function will return, closing the DNS /// server. async fn run(mut self) { - while let Some((query, tx)) = self.rx.next().await { - self.resolve(query, tx); + while let Some(request) = self.rx.next().await { + match request { + ResolverMessage::SetConfig { + new_config, + response_tx, + } => { + log::debug!("Updating config: {new_config:?}"); + + self.inner_resolver = Resolver::from(new_config); + flush_system_cache(); + let _ = response_tx.send(()); + } + ResolverMessage::Query { + dns_query, + response_tx, + } => { + self.inner_resolver.resolve(dns_query, response_tx); + } + } } if let Some((server_handle, done_rx)) = self.dns_server.take() { @@ -171,35 +353,26 @@ impl FilteringResolver { let _ = done_rx.await; } } +} - /// Resolvers a query to nothing or a documentation address - fn resolve(&mut self, query: LowerQuery, tx: oneshot::Sender>) { - if !self.allow_query(&query) { - let _ = tx.send(Box::new(EmptyLookup) as Box); - return; - } - - let return_query = query.original().clone(); - let mut return_record = Record::with( - return_query.name().clone(), - return_query.query_type(), - TTL_SECONDS, - ); - return_record.set_data(Some(RData::A(rdata::A(RESOLVED_ADDR)))); - - let lookup = Lookup::new_with_deadline( - return_query, - Arc::new([return_record]), - Instant::now() + Duration::from_secs(3), - ); - let _ = tx.send(Box::new(ForwardLookup(lookup))); +/// Flush the DNS cache. +fn flush_system_cache() { + if let Err(error) = kill_mdnsresponder() { + log::error!("Failed to kill mDNSResponder: {error}"); } +} - /// Determines whether a DNS query is allowable. Currently, this implies that the query is - /// either a `A`, `AAAA` or a `CNAME` query for `captive.apple.com`. - fn allow_query(&self, query: &LowerQuery) -> bool { - ALLOWED_RECORD_TYPES.contains(&query.query_type()) && ALLOWED_DOMAINS.contains(query.name()) +const MDNS_RESPONDER_PATH: &str = "/usr/sbin/mDNSResponder"; + +/// Find and kill mDNSResponder. The OS will restart the service. +fn kill_mdnsresponder() -> io::Result<()> { + if let Some(mdns_pid) = talpid_macos::process::pid_of_path(MDNS_RESPONDER_PATH) { + nix::sys::signal::kill( + nix::unistd::Pid::from_raw(mdns_pid), + nix::sys::signal::SIGHUP, + )?; } + Ok(()) } type LookupResponse<'a> = MessageResponse< @@ -214,7 +387,7 @@ type LookupResponse<'a> = MessageResponse< /// An implementation of [hickory_server::server::RequestHandler] that forwards queries to /// `FilteringResolver`. struct ResolverImpl { - tx: Weak>, + tx: Weak>, } impl ResolverImpl { @@ -238,19 +411,40 @@ impl ResolverImpl { ) } + /// This function is called when a DNS query is sent to the local resolver async fn lookup(&self, message: &Request, mut response_handler: R) { if let Some(tx_ref) = self.tx.upgrade() { let mut tx = (*tx_ref).clone(); let query = message.query(); - let (lookup_tx, lookup_rx) = oneshot::channel(); - let _ = tx.send((query.clone(), lookup_tx)).await; - let lookup_result: Box = lookup_rx - .await - .unwrap_or_else(|_| Box::new(EmptyLookup) as Box); - let response = Self::build_response(message, lookup_result.as_ref()); - - if let Err(err) = response_handler.send_response(response).await { - log::error!("Failed to send response: {}", err); + let (response_tx, response_rx) = oneshot::channel(); + let _ = tx + .send(ResolverMessage::Query { + dns_query: query.clone(), + response_tx, + }) + .await; + + let lookup_result = response_rx.await; + let response_result = match lookup_result { + Ok(Ok(ref lookup)) => { + let response = Self::build_response(message, lookup.as_ref()); + response_handler.send_response(response).await + } + Err(_error) => return, + Ok(Err(resolve_err)) => match resolve_err.kind() { + ResolveErrorKind::NoRecordsFound { response_code, .. } => { + let response = MessageResponseBuilder::from_message_request(message) + .error_msg(message.header(), *response_code); + response_handler.send_response(response).await + } + _other => { + let response = Self::build_response(message, &EmptyLookup); + response_handler.send_response(response).await + } + }, + }; + if let Err(err) = response_result { + log::error!("Failed to send response: {err}"); } } } diff --git a/talpid-core/src/split_tunnel/macos/mod.rs b/talpid-core/src/split_tunnel/macos/mod.rs index b3acdb01361c..38c4201ea071 100644 --- a/talpid-core/src/split_tunnel/macos/mod.rs +++ b/talpid-core/src/split_tunnel/macos/mod.rs @@ -614,7 +614,7 @@ impl State { Some(vpn_interface.clone()), route_manager.clone(), Box::new(move |packet| { - match states.get_process_status(packet.header.pth_pid as u32) { + match states.get_process_status(packet.header.pth_pid) { ExclusionStatus::Excluded => tun::RoutingDecision::DefaultInterface, ExclusionStatus::Included => tun::RoutingDecision::VpnTunnel, ExclusionStatus::Unknown => { diff --git a/talpid-core/src/split_tunnel/macos/process.rs b/talpid-core/src/split_tunnel/macos/process.rs index 52e0fb1862b0..7069e5fdd828 100644 --- a/talpid-core/src/split_tunnel/macos/process.rs +++ b/talpid-core/src/split_tunnel/macos/process.rs @@ -6,18 +6,17 @@ //! Endpoint Security framework. use futures::channel::oneshot; -use libc::{proc_listallpids, proc_pidpath}; +use libc::pid_t; use serde::Deserialize; use std::{ collections::{HashMap, HashSet}, - ffi::c_void, io, path::PathBuf, process::Stdio, - ptr, sync::{Arc, LazyLock, Mutex}, time::Duration, }; +use talpid_macos::process::{list_pids, process_path}; use talpid_platform_metadata::MacosVersion; use talpid_types::tunnel::ErrorStateCause; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -52,7 +51,7 @@ pub enum Error { InitializePids(#[source] io::Error), /// Failed to find path for a process #[error("Failed to find path for a process: {}", _0)] - FindProcessPath(#[source] io::Error, u32), + FindProcessPath(#[source] io::Error, pid_t), } impl From<&Error> for ErrorStateCause { @@ -231,7 +230,7 @@ pub enum ExclusionStatus { #[derive(Debug)] struct InnerProcessStates { - processes: HashMap, + processes: HashMap, exclude_paths: HashSet, } @@ -277,7 +276,7 @@ impl ProcessStates { inner.exclude_paths = paths; } - pub fn get_process_status(&self, pid: u32) -> ExclusionStatus { + pub fn get_process_status(&self, pid: pid_t) -> ExclusionStatus { let inner = self.inner.lock().unwrap(); match inner.processes.get(&pid) { Some(val) if val.is_excluded() => ExclusionStatus::Excluded, @@ -300,7 +299,7 @@ impl InnerProcessStates { // For new processes, inherit all exclusion state from the parent, if there is one. // Otherwise, look up excluded paths - fn handle_fork(&mut self, parent_pid: u32, exec_path: PathBuf, msg: ESForkEvent) { + fn handle_fork(&mut self, parent_pid: pid_t, exec_path: PathBuf, msg: ESForkEvent) { let pid = msg.child.audit_token.pid; if self.processes.contains_key(&pid) { @@ -327,7 +326,7 @@ impl InnerProcessStates { self.processes.insert(pid, base_info); } - fn handle_exec(&mut self, pid: u32, msg: ESExecEvent) { + fn handle_exec(&mut self, pid: pid_t, msg: ESExecEvent) { let Some(info) = self.processes.get_mut(&pid) else { log::error!("exec received for unknown pid {pid}"); return; @@ -354,54 +353,13 @@ impl InnerProcessStates { } } - fn handle_exit(&mut self, pid: u32) { + fn handle_exit(&mut self, pid: pid_t) { if self.processes.remove(&pid).is_none() { log::error!("exit syscall for unknown pid {pid}"); } } } -/// Obtain a list of all pids -fn list_pids() -> io::Result> { - // SAFETY: Passing in null and 0 returns the number of processes - let num_pids = unsafe { proc_listallpids(ptr::null_mut(), 0) }; - if num_pids <= 0 { - return Err(io::Error::last_os_error()); - } - let num_pids = usize::try_from(num_pids).unwrap(); - let mut pids = vec![0u32; num_pids]; - - let buf_sz = (num_pids * std::mem::size_of::()) as i32; - // SAFETY: 'pids' is large enough to contain 'num_pids' processes - let num_pids = unsafe { proc_listallpids(pids.as_mut_ptr() as *mut c_void, buf_sz) }; - if num_pids == -1 { - return Err(io::Error::last_os_error()); - } - - pids.resize(usize::try_from(num_pids).unwrap(), 0); - - Ok(pids) -} - -fn process_path(pid: u32) -> io::Result { - let mut buffer = [0u8; libc::MAXPATHLEN as usize]; - // SAFETY: `proc_pidpath` returns at most `buffer.len()` bytes - let buf_len = unsafe { - proc_pidpath( - pid as i32, - buffer.as_mut_ptr() as *mut c_void, - buffer.len() as u32, - ) - }; - if buf_len == -1 { - return Err(io::Error::last_os_error()); - } - Ok(PathBuf::from( - std::str::from_utf8(&buffer[0..buf_len as usize]) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid process path"))?, - )) -} - #[derive(Debug, Clone)] struct ProcessInfo { exec_path: PathBuf, @@ -480,7 +438,7 @@ struct ESExecutable { /// https://developer.apple.com/documentation/endpointsecurity/es_process_t/3228975-audit_token?language=objc #[derive(Debug, Deserialize)] struct ESAuditToken { - pid: u32, + pid: pid_t, } /// Process information for the message returned by `eslogger`. diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index abb97d282d7e..f614b5826754 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -144,6 +144,8 @@ impl ConnectedState { redirect_interface, #[cfg(target_os = "macos")] apple_services_bypass: shared_values.apple_services_bypass, + #[cfg(target_os = "macos")] + dns_redirect_port: shared_values.filtering_resolver.listening_port(), } } @@ -157,18 +159,34 @@ impl ConnectedState { fn set_dns(&self, shared_values: &mut SharedTunnelStateValues) -> Result<(), BoxedError> { let dns_config = Self::resolve_dns(&self.metadata, shared_values); + #[cfg(not(target_os = "macos"))] shared_values .dns_monitor .set(&self.metadata.interface, dns_config) .map_err(BoxedError::new)?; + // On macOS, configure only the local DNS resolver + #[cfg(target_os = "macos")] + shared_values.runtime.block_on( + shared_values + .filtering_resolver + .enable_forward(dns_config.addresses().collect()), + ); + Ok(()) } fn reset_dns(shared_values: &mut SharedTunnelStateValues) { + #[cfg(not(target_os = "macos"))] if let Err(error) = shared_values.dns_monitor.reset_before_interface_removal() { log::error!("{}", error.display_chain_with_msg("Unable to reset DNS")); } + + // On macOS, configure only the local DNS resolver + #[cfg(target_os = "macos")] + shared_values + .runtime + .block_on(shared_values.filtering_resolver.disable_forward()); } fn reset_routes( diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index e5d9dcb6cb8f..783769251acf 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -57,6 +57,17 @@ impl ConnectingState { shared_values: &mut SharedTunnelStateValues, retry_attempt: u32, ) -> (Box, TunnelStateTransition) { + #[cfg(target_os = "macos")] + if let Err(err) = shared_values.dns_monitor.set( + "lo", + crate::dns::DnsConfig::default().resolve(&[std::net::Ipv4Addr::LOCALHOST.into()]), + ) { + log::error!( + "{}", + err.display_chain_with_msg("Failed to configure system to use filtering resolver") + ); + } + if shared_values.connectivity.is_offline() { // FIXME: Temporary: Nudge route manager to update the default interface #[cfg(target_os = "macos")] @@ -174,6 +185,8 @@ impl ConnectingState { redirect_interface, #[cfg(target_os = "macos")] apple_services_bypass: shared_values.apple_services_bypass, + #[cfg(target_os = "macos")] + dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; shared_values .firewall diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index f7bc5e712bb8..f66bac4e76d2 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -133,6 +133,10 @@ impl DisconnectedState { fn setup_local_dns_config( shared_values: &mut SharedTunnelStateValues, ) -> Result<(), dns::Error> { + shared_values + .runtime + .block_on(shared_values.filtering_resolver.disable_forward()); + shared_values.dns_monitor.set( "lo", dns::DnsConfig::default().resolve(&[Ipv4Addr::LOCALHOST.into()]), diff --git a/talpid-macos/Cargo.toml b/talpid-macos/Cargo.toml new file mode 100644 index 000000000000..7868707b4ca3 --- /dev/null +++ b/talpid-macos/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "talpid-macos" +description = "Abstractions for macOS" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[target.'cfg(target_os="macos")'.dependencies] +libc = "0.2" +log = { workspace = true } diff --git a/talpid-macos/src/lib.rs b/talpid-macos/src/lib.rs new file mode 100644 index 000000000000..964dd689e0af --- /dev/null +++ b/talpid-macos/src/lib.rs @@ -0,0 +1,7 @@ +//! Interface with macOS-specific bits. + +#![deny(missing_docs)] +#![cfg(target_os = "macos")] + +/// Processes +pub mod process; diff --git a/talpid-macos/src/process.rs b/talpid-macos/src/process.rs new file mode 100644 index 000000000000..69722e054ceb --- /dev/null +++ b/talpid-macos/src/process.rs @@ -0,0 +1,67 @@ +use libc::{c_void, pid_t, proc_listallpids, proc_pidpath}; +use std::{ + io, + path::{Path, PathBuf}, +}; + +/// Return the first process identifier matching a specified path, if one exists. +pub fn pid_of_path(find_path: impl AsRef) -> Option { + match list_pids() { + Ok(pids) => { + for pid in pids { + if let Ok(path) = process_path(pid) { + if path == find_path.as_ref() { + return Some(pid); + } + } + } + None + } + Err(error) => { + log::error!("Failed to list processes: {error}"); + None + } + } +} + +/// Obtain a list of all process identifiers +pub fn list_pids() -> io::Result> { + // SAFETY: Passing in null and 0 returns the number of processes + let num_pids = unsafe { proc_listallpids(std::ptr::null_mut(), 0) }; + if num_pids <= 0 { + return Err(io::Error::last_os_error()); + } + let num_pids = usize::try_from(num_pids).unwrap(); + let mut pids = vec![0i32; num_pids]; + + let buf_sz = (num_pids * std::mem::size_of::()) as i32; + // SAFETY: 'pids' is large enough to contain 'num_pids' processes + let num_pids = unsafe { proc_listallpids(pids.as_mut_ptr() as *mut c_void, buf_sz) }; + if num_pids == -1 { + return Err(io::Error::last_os_error()); + } + + pids.resize(usize::try_from(num_pids).unwrap(), 0); + + Ok(pids) +} + +/// Return the path of the process `pid` +pub fn process_path(pid: pid_t) -> io::Result { + let mut buffer = [0u8; libc::MAXPATHLEN as usize]; + // SAFETY: `proc_pidpath` returns at most `buffer.len()` bytes + let buf_len = unsafe { + proc_pidpath( + pid, + buffer.as_mut_ptr() as *mut c_void, + u32::try_from(buffer.len()).unwrap(), + ) + }; + if buf_len == -1 { + return Err(io::Error::last_os_error()); + } + Ok(PathBuf::from( + std::str::from_utf8(&buffer[0..buf_len as usize]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid process path"))?, + )) +}