From 9604d1ae258032da1f53267aaf1e361aeb562e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Mon, 23 Sep 2024 14:18:12 +0200 Subject: [PATCH] Add FFI for encrypted-dns-proxy for iOS --- .../include/mullvad_rust_runtime.h | 44 +++- mullvad-encrypted-dns-proxy/src/config/mod.rs | 3 + .../src/config_resolver.rs | 48 ++-- mullvad-ios/src/encrypted_dns_proxy.rs | 221 ++++++++++++++++++ mullvad-ios/src/ephemeral_peer_proxy/mod.rs | 1 + mullvad-ios/src/lib.rs | 1 + 6 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 mullvad-ios/src/encrypted_dns_proxy.rs diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index c42d2ae84074..7aa7cbb3e8fa 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -5,17 +5,55 @@ #include #include -typedef struct EphemeralPeerCancelToken { - void *context; -} EphemeralPeerCancelToken; +typedef struct EncryptedDnsProxyState EncryptedDnsProxyState; typedef struct ProxyHandle { void *context; uint16_t port; } ProxyHandle; +typedef struct EphemeralPeerCancelToken { + void *context; +} EphemeralPeerCancelToken; + extern const uint16_t CONFIG_SERVICE_PORT; +/** + * Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. + */ +struct EncryptedDnsProxyState *encrypted_dns_proxy_init(void); + +/** + * This must be called only once to deallocate `EncryptedDnsProxyState`. + * + * # Safety + * `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized + * by `encrypted_dns_proxy_init`. This function is not thread safe, and should only be called + * once. + */ +void encrypted_dns_proxy_free(struct EncryptedDnsProxyState *ptr); + +/** + * # Safety + * encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized + * by `encrypted_dns_proxy_init`. This function is not thread safe. + * `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This + * function is not thread safe, but it can be called repeatedly. Each successful invocation should + * clean up the resulting proxy via `[encrypted_dns_proxy_stop]`. + * + * `proxy_handle` will only contain valid values if the return value is zero. It is still valid to + * deallocate the memory. + */ +int32_t encrypted_dns_proxy_start(struct EncryptedDnsProxyState *encrypted_dns_proxy, + struct ProxyHandle *proxy_handle); + +/** + * # Safety + * `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by + * [`encrypted_dns_proxy_start`]. It should only ever be called once. + */ +int32_t encrypted_dns_proxy_stop(struct ProxyHandle *proxy_config); + /** * Called by the Swift side to signal that the ephemeral peer exchange should be cancelled. * After this call, the cancel token is no longer valid. diff --git a/mullvad-encrypted-dns-proxy/src/config/mod.rs b/mullvad-encrypted-dns-proxy/src/config/mod.rs index cd93d2989e43..bd75fd25eb04 100644 --- a/mullvad-encrypted-dns-proxy/src/config/mod.rs +++ b/mullvad-encrypted-dns-proxy/src/config/mod.rs @@ -39,8 +39,11 @@ impl std::error::Error for Error {} /// order. E.g. an IPv6 address such as `7f7f:2323::` would have a proxy type value of `0x2323`. #[derive(PartialEq, Debug)] enum ProxyType { + /// Plain proxy type Plain, + /// XorV1 - deprecated XorV1, + /// XorV2 XorV2, } diff --git a/mullvad-encrypted-dns-proxy/src/config_resolver.rs b/mullvad-encrypted-dns-proxy/src/config_resolver.rs index 2f3734aac7e4..5ff689b3073e 100644 --- a/mullvad-encrypted-dns-proxy/src/config_resolver.rs +++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs @@ -4,10 +4,12 @@ use crate::config; use core::fmt; use hickory_resolver::{config::*, error::ResolveError, TokioAsyncResolver}; use rustls::ClientConfig; -use std::{net::IpAddr, sync::Arc}; +use std::{net::IpAddr, sync::Arc, time::Duration}; +use tokio::time::error::Elapsed; /// The port to connect to the DoH resolvers over. const RESOLVER_PORT: u16 = 443; +const DEFAULT_TIMEOUT: Duration = std::time::Duration::from_secs(10); pub struct Nameserver { pub name: String, @@ -15,17 +17,26 @@ pub struct Nameserver { } #[derive(Debug)] -pub struct ResolutionError(ResolveError); +pub enum Error { + ResolutionError(ResolveError), + Timeout(Elapsed), +} -impl fmt::Display for ResolutionError { +impl fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + match self { + Error::ResolutionError(err) => err.fmt(f), + Error::Timeout(err) => err.fmt(f), + } } } -impl std::error::Error for ResolutionError { +impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.0.source() + match self { + Self::ResolutionError(ref err) => Some(err), + Self::Timeout(ref err) => Some(err), + } } } @@ -50,13 +61,17 @@ pub fn default_resolvers() -> Vec { ] } +pub async fn resolve_default_config() -> Result, Error> { + resolve_configs(&default_resolvers(), "frakta.eu").await +} + /// Look up the `domain` towards the given `resolvers`, and try to deserialize all the returned /// AAAA records into [`ProxyConfig`](config::ProxyConfig)s. pub async fn resolve_configs( resolvers: &[Nameserver], domain: &str, -) -> Result, ResolutionError> { - let mut resolver_config = ResolverConfig::new(); +) -> Result, Error> { + let mut nameservers = ResolverConfig::new(); for resolver in resolvers.iter() { let ns_config_group = NameServerConfigGroup::from_ips_https( &resolver.addr, @@ -66,25 +81,28 @@ pub async fn resolve_configs( ) .into_inner(); for ns_config in ns_config_group { - resolver_config.add_name_server(ns_config); + nameservers.add_name_server(ns_config); } } - resolver_config.set_tls_client_config(Arc::new(client_config_tls12())); + nameservers.set_tls_client_config(Arc::new(client_config_tls12())); + let mut resolver_config: ResolverOpts = Default::default(); - resolve_config_with_resolverconfig(resolver_config, Default::default(), domain).await + resolver_config.timeout = Duration::from_secs(5); + resolve_config_with_resolverconfig(nameservers, resolver_config, domain, DEFAULT_TIMEOUT).await } pub async fn resolve_config_with_resolverconfig( resolver_config: ResolverConfig, options: ResolverOpts, domain: &str, -) -> Result, ResolutionError> { + timeout: Duration, +) -> Result, Error> { let resolver = TokioAsyncResolver::tokio(resolver_config, options); - let lookup = resolver - .ipv6_lookup(domain) + let lookup = tokio::time::timeout(timeout, resolver.ipv6_lookup(domain)) .await - .map_err(ResolutionError)?; + .map_err(Error::Timeout)? + .map_err(Error::ResolutionError)?; let addrs = lookup.into_iter().map(|aaaa_record| aaaa_record.0); diff --git a/mullvad-ios/src/encrypted_dns_proxy.rs b/mullvad-ios/src/encrypted_dns_proxy.rs new file mode 100644 index 000000000000..a761df4cd9df --- /dev/null +++ b/mullvad-ios/src/encrypted_dns_proxy.rs @@ -0,0 +1,221 @@ +use crate::ProxyHandle; + +use mullvad_encrypted_dns_proxy::{config::ProxyConfig, config_resolver, Forwarder}; +use std::{ + collections::HashSet, + io, mem, + net::{Ipv4Addr, SocketAddr}, + ptr, +}; +use tokio::{net::TcpListener, task::JoinHandle}; + +pub struct EncryptedDnsProxyState { + /// Note that we rely on the randomness of the ordering of the items in the hashset to pick a + /// random configurations every time. + configurations: HashSet, + tried_configurations: HashSet, +} + +#[derive(Debug)] +pub enum Error { + /// Failed to initialize tokio runtime. + TokioRuntime, + /// Failed to bind a local listening socket, the one that will be forwarded through the proxy. + BindLocalSocket(io::Error), + /// Failed to get local listening address of the local listening socket. + GetBindAddr(io::Error), + /// Failed to initialize forwarder. + Forwarder(io::Error), + /// Failed to fetch a proxy configuration over DNS. + FetchConfig(config_resolver::Error), + /// Failed to initialize with a valid configuration. + NoConfigs, +} + +impl From for i32 { + fn from(err: Error) -> Self { + match err { + Error::TokioRuntime => -1, + Error::BindLocalSocket(_) => -2, + Error::GetBindAddr(_) => -3, + Error::Forwarder(_) => -4, + Error::FetchConfig(_) => -5, + Error::NoConfigs => -6, + } + } +} + +impl EncryptedDnsProxyState { + async fn start(&mut self) -> Result { + self.fetch_configs().await?; + let proxy_configuration = self.next_configuration().ok_or(Error::NoConfigs)?; + + let local_socket = Self::bind_local_addr() + .await + .map_err(Error::BindLocalSocket)?; + let bind_addr = local_socket.local_addr().map_err(Error::GetBindAddr)?; + let forwarder = Forwarder::connect(&proxy_configuration) + .await + .map_err(Error::Forwarder)?; + let join_handle = Box::new(tokio::spawn(async move { + if let Ok((client_conn, _)) = local_socket.accept().await { + let _ = forwarder.forward(client_conn).await; + } + })); + + Ok(ProxyHandle { + context: Box::into_raw(join_handle).cast(), + port: bind_addr.port(), + }) + } + + async fn bind_local_addr() -> io::Result { + let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); + TcpListener::bind(bind_addr).await + } + + /// Select a config. + /// Always select an obfuscated configuration, if there are any left untried. If no obfuscated + /// configurations exist, try plain configurations. The order is randomized due to the hash set + /// storing the configurations in a random order. + fn next_configuration(&mut self) -> Option { + if self.should_reset() { + self.reset(); + } + // TODO: currently, the randomized order of proxy config retrieval depends on the random + // iteration order of a given HashSet instance. Since for now, there will be only 2 + // different configurations, it barely matters. In the future, we should use `rand` + // instead, so that the behavior is explicit and clear. + + // First, try getting an obfuscated configuration, if there exist any. + let config = if let Some(obfuscated_config) = self + .configurations + .difference(&self.tried_configurations) + .find(|config| config.obfuscation.is_some()) + .cloned() + { + obfuscated_config + } else { + // If no obfuscated configurations exist, try the next untried configuration. + self.configurations + .difference(&self.tried_configurations) + .next() + .cloned()? + }; + + self.tried_configurations.insert(config.clone()); + Some(config) + } + + /// Fetch a config, but error out only when no existing configuration was there. + async fn fetch_configs(&mut self) -> Result<(), Error> { + match mullvad_encrypted_dns_proxy::config_resolver::resolve_default_config().await { + Ok(new_configs) => { + self.configurations = HashSet::from_iter(new_configs.into_iter()); + } + Err(err) => { + log::error!("Failed to fetch a new proxy configuration: {err:?}"); + if self.is_empty() { + return Err(Error::FetchConfig(err)); + } + } + } + Ok(()) + } + + fn is_empty(&self) -> bool { + self.configurations.is_empty() + } + + /// Checks if the `tried_configurations` set should be reset. + /// It should only be reset if the difference between `configurations` and + /// `tried_configurations` is an empty set - in this case all available configurations have + /// been tried. + fn should_reset(&self) -> bool { + self.configurations + .difference(&self.tried_configurations) + .count() + == 0 + } + + /// Clears the `tried_configurations` set. + fn reset(&mut self) { + self.tried_configurations.clear(); + } +} + +/// Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. +#[no_mangle] +pub unsafe extern "C" fn encrypted_dns_proxy_init() -> *mut EncryptedDnsProxyState { + let state = Box::new(EncryptedDnsProxyState { + configurations: Default::default(), + tried_configurations: Default::default(), + }); + Box::into_raw(state) +} + +/// This must be called only once to deallocate `EncryptedDnsProxyState`. +/// +/// # Safety +/// `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized +/// by `encrypted_dns_proxy_init`. This function is not thread safe, and should only be called +/// once. +#[no_mangle] +pub unsafe extern "C" fn encrypted_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) { + let _ = unsafe { Box::from_raw(ptr) }; +} + +/// # Safety +/// encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized +/// by `encrypted_dns_proxy_init`. This function is not thread safe. +/// `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This +/// function is not thread safe, but it can be called repeatedly. Each successful invocation should +/// clean up the resulting proxy via `[encrypted_dns_proxy_stop]`. +/// +/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to +/// deallocate the memory. +#[no_mangle] +pub unsafe extern "C" fn encrypted_dns_proxy_start( + encrypted_dns_proxy: *mut EncryptedDnsProxyState, + proxy_handle: *mut ProxyHandle, +) -> i32 { + let handle = match crate::mullvad_ios_runtime() { + Ok(handle) => handle, + Err(err) => { + log::error!("Cannot instantiate a tokio runtime: {}", err); + return Error::TokioRuntime.into(); + } + }; + + let mut encrypted_dns_proxy = unsafe { Box::from_raw(encrypted_dns_proxy) }; + let proxy_result = handle.block_on(encrypted_dns_proxy.start()); + mem::forget(encrypted_dns_proxy); + + match proxy_result { + Ok(handle) => unsafe { ptr::write(proxy_handle, handle) }, + Err(err) => { + let empty_handle = ProxyHandle { + context: ptr::null_mut(), + port: 0, + }; + unsafe { ptr::write(proxy_handle, empty_handle) } + log::error!("Failed to create a proxy connection: {err:?}"); + return err.into(); + } + } + + 0 +} + +/// # Safety +/// `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by +/// [`encrypted_dns_proxy_start`]. It should only ever be called once. +#[no_mangle] +pub unsafe extern "C" fn encrypted_dns_proxy_stop(proxy_config: *mut ProxyHandle) -> i32 { + let ptr = unsafe { (*proxy_config).context }; + if !ptr.is_null() { + let handle: Box> = unsafe { Box::from_raw(ptr.cast()) }; + handle.abort(); + } + 0i32 +} diff --git a/mullvad-ios/src/ephemeral_peer_proxy/mod.rs b/mullvad-ios/src/ephemeral_peer_proxy/mod.rs index 0cb85f2177b2..c69b7d6b3b80 100644 --- a/mullvad-ios/src/ephemeral_peer_proxy/mod.rs +++ b/mullvad-ios/src/ephemeral_peer_proxy/mod.rs @@ -1,3 +1,4 @@ +#![cfg(target_os = "ios")] pub mod ios_runtime; pub mod ios_tcp_connection; diff --git a/mullvad-ios/src/lib.rs b/mullvad-ios/src/lib.rs index d36a925788d0..4a24745b09c5 100644 --- a/mullvad-ios/src/lib.rs +++ b/mullvad-ios/src/lib.rs @@ -1,4 +1,5 @@ #![cfg(target_os = "ios")] +mod encrypted_dns_proxy; mod ephemeral_peer_proxy; mod shadowsocks_proxy; mod tunnel_obfuscator_proxy;