Skip to content

Commit 196e4a1

Browse files
committed
Add FFI for encrypted-dns-proxy for iOS
1 parent bea8e15 commit 196e4a1

File tree

6 files changed

+281
-18
lines changed

6 files changed

+281
-18
lines changed

Diff for: ios/MullvadRustRuntime/include/mullvad_rust_runtime.h

+38-3
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,52 @@
55
#include <stdint.h>
66
#include <stdlib.h>
77

8-
typedef struct EphemeralPeerCancelToken {
9-
void *context;
10-
} EphemeralPeerCancelToken;
8+
typedef struct EncryptedDnsProxyState EncryptedDnsProxyState;
119

1210
typedef struct ProxyHandle {
1311
void *context;
1412
uint16_t port;
1513
} ProxyHandle;
1614

15+
typedef struct EphemeralPeerCancelToken {
16+
void *context;
17+
} EphemeralPeerCancelToken;
18+
1719
extern const uint16_t CONFIG_SERVICE_PORT;
1820

21+
/**
22+
* Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
23+
*/
24+
struct EncryptedDnsProxyState *encrypted_dns_proxy_init(void);
25+
26+
/**
27+
* This must be called only once to deallocate `EncryptedDnsProxyState`.
28+
*
29+
* # Safety
30+
* `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
31+
* by `encrypted_dns_proxy_init`. This function is not thread safe.
32+
*/
33+
void encrypted_dns_proxy_free(struct EncryptedDnsProxyState *ptr);
34+
35+
/**
36+
* # Safety
37+
* encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
38+
* by `encrypted_dns_proxy_init`. This function is not thread safe.
39+
* `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`
40+
*
41+
* `proxy_handle` will only contain valid values if the return value is zero. It is still valid to
42+
* deallocate the memory.
43+
*/
44+
int32_t encrypted_dns_proxy_start(struct EncryptedDnsProxyState *encrypted_dns_proxy,
45+
struct ProxyHandle *proxy_handle);
46+
47+
/**
48+
* # Safety
49+
* `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by
50+
* [`encrypted_dns_proxy_start`].
51+
*/
52+
int32_t encrypted_dns_proxy_stop(struct ProxyHandle *proxy_config);
53+
1954
/**
2055
* Called by the Swift side to signal that the ephemeral peer exchange should be cancelled.
2156
* After this call, the cancel token is no longer valid.

Diff for: mullvad-encrypted-dns-proxy/src/config/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ impl std::error::Error for Error {}
3939
/// order. E.g. an IPv6 address such as `7f7f:2323::` would have a proxy type value of `0x2323`.
4040
#[derive(PartialEq, Debug)]
4141
enum ProxyType {
42+
/// Plain proxy type
4243
Plain,
44+
/// XorV1 - deprecated
4345
XorV1,
46+
/// XorV2
4447
XorV2,
4548
}
4649

Diff for: mullvad-encrypted-dns-proxy/src/config_resolver.rs

+33-15
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,39 @@ use crate::config;
44
use core::fmt;
55
use hickory_resolver::{config::*, error::ResolveError, TokioAsyncResolver};
66
use rustls::ClientConfig;
7-
use std::{net::IpAddr, sync::Arc};
7+
use std::{net::IpAddr, sync::Arc, time::Duration};
8+
use tokio::time::error::Elapsed;
89

910
/// The port to connect to the DoH resolvers over.
1011
const RESOLVER_PORT: u16 = 443;
12+
const DEFAULT_TIMEOUT: Duration = std::time::Duration::from_secs(10);
1113

1214
pub struct Nameserver {
1315
pub name: String,
1416
pub addr: Vec<IpAddr>,
1517
}
1618

1719
#[derive(Debug)]
18-
pub struct ResolutionError(ResolveError);
20+
pub enum Error {
21+
ResolutionError(ResolveError),
22+
Timeout(Elapsed),
23+
}
1924

20-
impl fmt::Display for ResolutionError {
25+
impl fmt::Display for Error {
2126
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22-
self.0.fmt(f)
27+
match self {
28+
Error::ResolutionError(err) => err.fmt(f),
29+
Error::Timeout(err) => err.fmt(f),
30+
}
2331
}
2432
}
2533

26-
impl std::error::Error for ResolutionError {
34+
impl std::error::Error for Error {
2735
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28-
self.0.source()
36+
match self {
37+
Self::ResolutionError(ref err) => Some(err),
38+
Self::Timeout(ref err) => Some(err),
39+
}
2940
}
3041
}
3142

@@ -50,13 +61,17 @@ pub fn default_resolvers() -> Vec<Nameserver> {
5061
]
5162
}
5263

64+
pub async fn resolve_default_config() -> Result<Vec<config::ProxyConfig>, Error> {
65+
resolve_configs(&default_resolvers(), "frakta.eu").await
66+
}
67+
5368
/// Look up the `domain` towards the given `resolvers`, and try to deserialize all the returned
5469
/// AAAA records into [`ProxyConfig`](config::ProxyConfig)s.
5570
pub async fn resolve_configs(
5671
resolvers: &[Nameserver],
5772
domain: &str,
58-
) -> Result<Vec<config::ProxyConfig>, ResolutionError> {
59-
let mut resolver_config = ResolverConfig::new();
73+
) -> Result<Vec<config::ProxyConfig>, Error> {
74+
let mut nameservers = ResolverConfig::new();
6075
for resolver in resolvers.iter() {
6176
let ns_config_group = NameServerConfigGroup::from_ips_https(
6277
&resolver.addr,
@@ -66,25 +81,28 @@ pub async fn resolve_configs(
6681
)
6782
.into_inner();
6883
for ns_config in ns_config_group {
69-
resolver_config.add_name_server(ns_config);
84+
nameservers.add_name_server(ns_config);
7085
}
7186
}
7287

73-
resolver_config.set_tls_client_config(Arc::new(client_config_tls12()));
88+
nameservers.set_tls_client_config(Arc::new(client_config_tls12()));
89+
let mut resolver_config: ResolverOpts = Default::default();
7490

75-
resolve_config_with_resolverconfig(resolver_config, Default::default(), domain).await
91+
resolver_config.timeout = Duration::from_secs(5);
92+
resolve_config_with_resolverconfig(nameservers, resolver_config, domain, DEFAULT_TIMEOUT).await
7693
}
7794

7895
pub async fn resolve_config_with_resolverconfig(
7996
resolver_config: ResolverConfig,
8097
options: ResolverOpts,
8198
domain: &str,
82-
) -> Result<Vec<config::ProxyConfig>, ResolutionError> {
99+
timeout: Duration,
100+
) -> Result<Vec<config::ProxyConfig>, Error> {
83101
let resolver = TokioAsyncResolver::tokio(resolver_config, options);
84-
let lookup = resolver
85-
.ipv6_lookup(domain)
102+
let lookup = tokio::time::timeout(timeout, resolver.ipv6_lookup(domain))
86103
.await
87-
.map_err(ResolutionError)?;
104+
.map_err(Error::Timeout)?
105+
.map_err(Error::ResolutionError)?;
88106

89107
let addrs = lookup.into_iter().map(|aaaa_record| aaaa_record.0);
90108

Diff for: mullvad-ios/src/encrypted_dns_proxy.rs

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
use crate::ProxyHandle;
2+
3+
use mullvad_encrypted_dns_proxy::{config::ProxyConfig, config_resolver, Forwarder};
4+
use std::{
5+
collections::HashSet,
6+
io, mem,
7+
net::{Ipv4Addr, SocketAddr},
8+
ptr,
9+
};
10+
use tokio::{net::TcpListener, task::JoinHandle};
11+
12+
pub struct EncryptedDnsProxyState {
13+
/// Note that we rely on the randomness of the ordering of the items in the hashset to pick a
14+
/// random configurations every time.
15+
configurations: HashSet<ProxyConfig>,
16+
tried_configurations: HashSet<ProxyConfig>,
17+
}
18+
19+
#[derive(Debug)]
20+
pub enum Error {
21+
/// Failed to initialize tokio runtime.
22+
TokioRuntime,
23+
/// Failed to bind a local listening socket, the one that will be forwarded through the proxy.
24+
BindLocalSocket(io::Error),
25+
/// Failed to get local listening address of the local listening socket.
26+
GetBindAddr(io::Error),
27+
/// Failed to initialize forwarder.
28+
Forwarder(io::Error),
29+
/// Failed to fetch a proxy configuration over DNS.
30+
FetchConfig(config_resolver::Error),
31+
/// Failed to initialize with a valid configuration.
32+
NoConfigs,
33+
}
34+
35+
impl From<Error> for i32 {
36+
fn from(err: Error) -> Self {
37+
match err {
38+
Error::TokioRuntime => -1,
39+
Error::BindLocalSocket(_) => -2,
40+
Error::GetBindAddr(_) => -3,
41+
Error::Forwarder(_) => -4,
42+
Error::FetchConfig(_) => -5,
43+
Error::NoConfigs => -6,
44+
}
45+
}
46+
}
47+
48+
impl EncryptedDnsProxyState {
49+
fn is_empty(&self) -> bool {
50+
self.configurations.is_empty()
51+
}
52+
53+
fn should_reset(&self) -> bool {
54+
self.configurations
55+
.difference(&self.tried_configurations)
56+
.count()
57+
== 0
58+
}
59+
60+
fn reset(&mut self) {
61+
self.tried_configurations.clear();
62+
}
63+
64+
async fn start(&mut self) -> Result<ProxyHandle, Error> {
65+
self.fetch_configs().await?;
66+
let proxy_configuration = self.next_configuration().ok_or(Error::NoConfigs)?;
67+
68+
let local_socket = Self::bind_local_addr()
69+
.await
70+
.map_err(Error::BindLocalSocket)?;
71+
let bind_addr = local_socket.local_addr().map_err(Error::GetBindAddr)?;
72+
let forwarder = Forwarder::connect(&proxy_configuration)
73+
.await
74+
.map_err(Error::Forwarder)?;
75+
let join_handle = Box::new(tokio::spawn(async move {
76+
if let Ok((client_conn, _)) = local_socket.accept().await {
77+
let _ = forwarder.forward(client_conn).await;
78+
}
79+
}));
80+
81+
Ok(ProxyHandle {
82+
context: Box::into_raw(join_handle).cast(),
83+
port: bind_addr.port(),
84+
})
85+
}
86+
87+
async fn bind_local_addr() -> io::Result<TcpListener> {
88+
let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0);
89+
TcpListener::bind(bind_addr).await
90+
}
91+
92+
fn next_configuration(&mut self) -> Option<ProxyConfig> {
93+
if self.should_reset() {
94+
self.reset();
95+
}
96+
97+
if let Some(xor_config) = self
98+
.configurations
99+
.difference(&self.tried_configurations)
100+
// prefer obfuscated proxy configurations.
101+
.find(|config| config.obfuscation.is_some())
102+
.cloned()
103+
{
104+
self.tried_configurations.insert(xor_config.clone());
105+
return Some(xor_config);
106+
}
107+
108+
let config = self
109+
.configurations
110+
.difference(&self.tried_configurations)
111+
.next()
112+
.cloned()?;
113+
self.tried_configurations.insert(config.clone());
114+
Some(config)
115+
}
116+
117+
/// Fetch a config, but error out only when no existing configuration was there.
118+
async fn fetch_configs(&mut self) -> Result<(), Error> {
119+
match mullvad_encrypted_dns_proxy::config_resolver::resolve_default_config().await {
120+
Ok(new_configs) => {
121+
self.configurations = HashSet::from_iter(new_configs.into_iter());
122+
}
123+
Err(err) => {
124+
log::error!("Failed to fetch a new proxy configuration: {err:?}");
125+
if self.is_empty() {
126+
return Err(Error::FetchConfig(err));
127+
}
128+
}
129+
}
130+
Ok(())
131+
}
132+
}
133+
134+
/// Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
135+
#[no_mangle]
136+
pub unsafe extern "C" fn encrypted_dns_proxy_init() -> *mut EncryptedDnsProxyState {
137+
let state = Box::new(EncryptedDnsProxyState {
138+
configurations: Default::default(),
139+
tried_configurations: Default::default(),
140+
});
141+
Box::into_raw(state)
142+
}
143+
144+
/// This must be called only once to deallocate `EncryptedDnsProxyState`.
145+
///
146+
/// # Safety
147+
/// `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
148+
/// by `encrypted_dns_proxy_init`. This function is not thread safe.
149+
#[no_mangle]
150+
pub unsafe extern "C" fn encrypted_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) {
151+
let _ = unsafe { Box::from_raw(ptr) };
152+
}
153+
154+
/// # Safety
155+
/// encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
156+
/// by `encrypted_dns_proxy_init`. This function is not thread safe.
157+
/// `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`
158+
///
159+
/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to
160+
/// deallocate the memory.
161+
#[no_mangle]
162+
pub unsafe extern "C" fn encrypted_dns_proxy_start(
163+
encrypted_dns_proxy: *mut EncryptedDnsProxyState,
164+
proxy_handle: *mut ProxyHandle,
165+
) -> i32 {
166+
let handle = match crate::mullvad_ios_runtime() {
167+
Ok(handle) => handle,
168+
Err(err) => {
169+
log::error!("Cannot instantiate a tokio runtime: {}", err);
170+
return Error::TokioRuntime.into();
171+
}
172+
};
173+
174+
let mut encrypted_dns_proxy = unsafe { Box::from_raw(encrypted_dns_proxy) };
175+
let proxy_result = handle.block_on(encrypted_dns_proxy.start());
176+
mem::forget(encrypted_dns_proxy);
177+
178+
match proxy_result {
179+
Ok(handle) => unsafe { ptr::write(proxy_handle, handle) },
180+
Err(err) => {
181+
let empty_handle = ProxyHandle {
182+
context: ptr::null_mut(),
183+
port: 0,
184+
};
185+
unsafe { ptr::write(proxy_handle, empty_handle) }
186+
log::error!("Failed to create a proxy connection: {err:?}");
187+
return err.into();
188+
}
189+
}
190+
191+
0
192+
}
193+
194+
/// # Safety
195+
/// `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by
196+
/// [`encrypted_dns_proxy_start`].
197+
#[no_mangle]
198+
pub unsafe extern "C" fn encrypted_dns_proxy_stop(proxy_config: *mut ProxyHandle) -> i32 {
199+
let ptr = unsafe { (*proxy_config).context };
200+
if !ptr.is_null() {
201+
let handle: Box<JoinHandle<()>> = unsafe { Box::from_raw(ptr.cast()) };
202+
handle.abort();
203+
}
204+
0i32
205+
}

Diff for: mullvad-ios/src/ephemeral_peer_proxy/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![cfg(target_os = "ios")]
12
pub mod ios_runtime;
23
pub mod ios_tcp_connection;
34

Diff for: mullvad-ios/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![cfg(target_os = "ios")]
2+
mod encrypted_dns_proxy;
23
mod ephemeral_peer_proxy;
34
mod shadowsocks_proxy;
45
mod tunnel_obfuscator_proxy;

0 commit comments

Comments
 (0)