summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-10-03 15:54:24 +0200
committerDavid Lönnhager <david.l@mullvad.net>2024-10-03 15:54:24 +0200
commitd6fc454386103141bba33d28013bd97f2aba07fa (patch)
treed7dbc68d725fa72c3635a455a7897c59aa444784
parent421b5dc09a57b60cc2cef1c00cd64b0c5049b73f (diff)
parent872ab2dc06b79acb684b58d6764d32b9b04dd56b (diff)
downloadmullvadvpn-d6fc454386103141bba33d28013bd97f2aba07fa.tar.xz
mullvadvpn-d6fc454386103141bba33d28013bd97f2aba07fa.zip
Merge branch 'macos-dns-local-resolver'
-rw-r--r--CHANGELOG.md7
-rw-r--r--Cargo.lock9
-rw-r--r--Cargo.toml1
-rw-r--r--talpid-core/Cargo.toml3
-rw-r--r--talpid-core/src/firewall/macos.rs20
-rw-r--r--talpid-core/src/firewall/mod.rs8
-rw-r--r--talpid-core/src/lib.rs2
-rw-r--r--talpid-core/src/resolver.rs316
-rw-r--r--talpid-core/src/split_tunnel/macos/mod.rs2
-rw-r--r--talpid-core/src/split_tunnel/macos/process.rs60
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs18
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs13
-rw-r--r--talpid-core/src/tunnel_state_machine/disconnected_state.rs4
-rw-r--r--talpid-macos/Cargo.toml15
-rw-r--r--talpid-macos/src/lib.rs7
-rw-r--r--talpid-macos/src/process.rs67
16 files changed, 436 insertions, 116 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 503b2ad8ed..1234d9fc12 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 d07de1332c..c34a60d2fb 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",
@@ -4150,6 +4151,14 @@ dependencies = [
]
[[package]]
+name = "talpid-macos"
+version = "0.0.0"
+dependencies = [
+ "libc",
+ "log",
+]
+
+[[package]]
name = "talpid-net"
version = "0.0.0"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index 3157d70d1b..32757afc78 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 5fab3022ab..a0ddb45825 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 ef26ebb97f..2dddc2381e 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 2596f330e3..af3db8dcb3 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 84bb4ad90c..bb81d7a48f 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 97f1f4b79c..555edfb3e0 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<ResolverHandle, Error> {
- let (resolver, resolver_handle) = FilteringResolver::new().await?;
+pub async fn start_resolver() -> Result<ResolverHandle, Error> {
+ 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<ResolverMessage>,
+/// A DNS resolver that forwards queries to some other DNS server
+///
+/// Is controlled by commands sent through [ResolverHandle]s.
+struct LocalResolver {
+ rx: mpsc::UnboundedReceiver<ResolverMessage>,
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<std::result::Result<Box<dyn LookupObject>, 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<IpAddr>,
+ },
+}
+
+enum Resolver {
+ /// Drop DNS queries. For captive portal domains, return faux records
+ Blocking,
+
+ /// Forward DNS queries to a configured server
+ Forwarding(TokioAsyncResolver),
+}
+
+impl From<Config> 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<Box<dyn LookupObject>>);
+impl Resolver {
+ pub fn resolve(
+ &self,
+ query: LowerQuery,
+ tx: oneshot::Sender<std::result::Result<Box<dyn LookupObject>, 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<Box<dyn LookupObject>, 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<_>)
+ }
+
+ /// 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<Box<dyn LookupObject>, ResolveError> {
+ let return_query = query.original().clone();
-/// A handle to control a filtering resolver. When all resolver handles are dropped, custom
-/// resolver will stop.
+ 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<mpsc::Sender<ResolverMessage>>,
+pub struct ResolverHandle {
+ tx: Arc<mpsc::UnboundedSender<ResolverMessage>>,
listening_port: u16,
}
impl ResolverHandle {
- fn new(tx: Arc<mpsc::Sender<ResolverMessage>>, listening_port: u16) -> Self {
- Self {
- _tx: tx,
- listening_port,
- }
+ fn new(tx: Arc<mpsc::UnboundedSender<ResolverMessage>>, 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<IpAddr>) {
+ 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<mpsc::Sender<ResolverMessage>>,
+ command_tx: Weak<mpsc::UnboundedSender<ResolverMessage>>,
) -> Result<(ServerFuture<ResolverImpl>, 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<Box<dyn LookupObject>>) {
- if !self.allow_query(&query) {
- let _ = tx.send(Box::new(EmptyLookup) as Box<dyn LookupObject>);
- 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<mpsc::Sender<ResolverMessage>>,
+ tx: Weak<mpsc::UnboundedSender<ResolverMessage>>,
}
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<R: ResponseHandler>(&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<dyn LookupObject> = lookup_rx
- .await
- .unwrap_or_else(|_| Box::new(EmptyLookup) as Box<dyn LookupObject>);
- let response = Self::build_response(message, lookup_result.as_ref());
+ let (response_tx, response_rx) = oneshot::channel();
+ let _ = tx
+ .send(ResolverMessage::Query {
+ dns_query: query.clone(),
+ response_tx,
+ })
+ .await;
- if let Err(err) = response_handler.send_response(response).await {
- log::error!("Failed to send response: {}", err);
+ 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 b3acdb0136..38c4201ea0 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 52e0fb1862..7069e5fdd8 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<u32, ProcessInfo>,
+ processes: HashMap<pid_t, ProcessInfo>,
exclude_paths: HashSet<PathBuf>,
}
@@ -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<Vec<u32>> {
- // 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::<u32>()) 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<PathBuf> {
- 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 abb97d282d..f614b58267 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 e5d9dcb6cb..783769251a 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<dyn TunnelState>, 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 f7bc5e712b..f66bac4e76 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 0000000000..7868707b4c
--- /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 0000000000..964dd689e0
--- /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 0000000000..69722e054c
--- /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<Path>) -> Option<pid_t> {
+ 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<Vec<pid_t>> {
+ // 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::<pid_t>()) 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<PathBuf> {
+ 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"))?,
+ ))
+}