summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2024-09-25 16:07:19 +0200
committerEmīls <emils@mullvad.net>2024-09-25 16:07:19 +0200
commitdfa53d8a2001dffae8210d5135dcbcfcc4da93d9 (patch)
tree100a56e6c8000c0da9da7657cfa7842327f31efa
parent9c0f7d7c583fcab63efc94fd2a591d1ac605637d (diff)
parent9604d1ae258032da1f53267aaf1e361aeb562e66 (diff)
downloadmullvadvpn-dfa53d8a2001dffae8210d5135dcbcfcc4da93d9.tar.xz
mullvadvpn-dfa53d8a2001dffae8210d5135dcbcfcc4da93d9.zip
Merge branch 'ios-use-encrypted-dns-proxy'
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h44
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/mod.rs3
-rw-r--r--mullvad-encrypted-dns-proxy/src/config_resolver.rs48
-rw-r--r--mullvad-ios/src/encrypted_dns_proxy.rs221
-rw-r--r--mullvad-ios/src/ephemeral_peer_proxy/mod.rs1
-rw-r--r--mullvad-ios/src/lib.rs1
6 files changed, 300 insertions, 18 deletions
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
index c42d2ae840..7aa7cbb3e8 100644
--- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -5,18 +5,56 @@
#include <stdint.h>
#include <stdlib.h>
-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 cd93d2989e..bd75fd25eb 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 2f3734aac7..5ff689b307 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<Nameserver> {
]
}
+pub async fn resolve_default_config() -> Result<Vec<config::ProxyConfig>, 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<Vec<config::ProxyConfig>, ResolutionError> {
- let mut resolver_config = ResolverConfig::new();
+) -> Result<Vec<config::ProxyConfig>, 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<Vec<config::ProxyConfig>, ResolutionError> {
+ timeout: Duration,
+) -> Result<Vec<config::ProxyConfig>, 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 0000000000..a761df4cd9
--- /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<ProxyConfig>,
+ tried_configurations: HashSet<ProxyConfig>,
+}
+
+#[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<Error> 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<ProxyHandle, Error> {
+ 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<TcpListener> {
+ 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<ProxyConfig> {
+ 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<JoinHandle<()>> = 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 0cb85f2177..c69b7d6b3b 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 d36a925788..4a24745b09 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;