Skip to content

Commit dfa53d8

Browse files
committed
Merge branch 'ios-use-encrypted-dns-proxy'
2 parents 9c0f7d7 + 9604d1a commit dfa53d8

File tree

6 files changed

+300
-18
lines changed

6 files changed

+300
-18
lines changed

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

+41-3
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,55 @@
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, and should only be called
32+
* once.
33+
*/
34+
void encrypted_dns_proxy_free(struct EncryptedDnsProxyState *ptr);
35+
36+
/**
37+
* # Safety
38+
* encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
39+
* by `encrypted_dns_proxy_init`. This function is not thread safe.
40+
* `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This
41+
* function is not thread safe, but it can be called repeatedly. Each successful invocation should
42+
* clean up the resulting proxy via `[encrypted_dns_proxy_stop]`.
43+
*
44+
* `proxy_handle` will only contain valid values if the return value is zero. It is still valid to
45+
* deallocate the memory.
46+
*/
47+
int32_t encrypted_dns_proxy_start(struct EncryptedDnsProxyState *encrypted_dns_proxy,
48+
struct ProxyHandle *proxy_handle);
49+
50+
/**
51+
* # Safety
52+
* `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by
53+
* [`encrypted_dns_proxy_start`]. It should only ever be called once.
54+
*/
55+
int32_t encrypted_dns_proxy_stop(struct ProxyHandle *proxy_config);
56+
1957
/**
2058
* Called by the Swift side to signal that the ephemeral peer exchange should be cancelled.
2159
* 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

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
async fn start(&mut self) -> Result<ProxyHandle, Error> {
50+
self.fetch_configs().await?;
51+
let proxy_configuration = self.next_configuration().ok_or(Error::NoConfigs)?;
52+
53+
let local_socket = Self::bind_local_addr()
54+
.await
55+
.map_err(Error::BindLocalSocket)?;
56+
let bind_addr = local_socket.local_addr().map_err(Error::GetBindAddr)?;
57+
let forwarder = Forwarder::connect(&proxy_configuration)
58+
.await
59+
.map_err(Error::Forwarder)?;
60+
let join_handle = Box::new(tokio::spawn(async move {
61+
if let Ok((client_conn, _)) = local_socket.accept().await {
62+
let _ = forwarder.forward(client_conn).await;
63+
}
64+
}));
65+
66+
Ok(ProxyHandle {
67+
context: Box::into_raw(join_handle).cast(),
68+
port: bind_addr.port(),
69+
})
70+
}
71+
72+
async fn bind_local_addr() -> io::Result<TcpListener> {
73+
let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0);
74+
TcpListener::bind(bind_addr).await
75+
}
76+
77+
/// Select a config.
78+
/// Always select an obfuscated configuration, if there are any left untried. If no obfuscated
79+
/// configurations exist, try plain configurations. The order is randomized due to the hash set
80+
/// storing the configurations in a random order.
81+
fn next_configuration(&mut self) -> Option<ProxyConfig> {
82+
if self.should_reset() {
83+
self.reset();
84+
}
85+
// TODO: currently, the randomized order of proxy config retrieval depends on the random
86+
// iteration order of a given HashSet instance. Since for now, there will be only 2
87+
// different configurations, it barely matters. In the future, we should use `rand`
88+
// instead, so that the behavior is explicit and clear.
89+
90+
// First, try getting an obfuscated configuration, if there exist any.
91+
let config = if let Some(obfuscated_config) = self
92+
.configurations
93+
.difference(&self.tried_configurations)
94+
.find(|config| config.obfuscation.is_some())
95+
.cloned()
96+
{
97+
obfuscated_config
98+
} else {
99+
// If no obfuscated configurations exist, try the next untried configuration.
100+
self.configurations
101+
.difference(&self.tried_configurations)
102+
.next()
103+
.cloned()?
104+
};
105+
106+
self.tried_configurations.insert(config.clone());
107+
Some(config)
108+
}
109+
110+
/// Fetch a config, but error out only when no existing configuration was there.
111+
async fn fetch_configs(&mut self) -> Result<(), Error> {
112+
match mullvad_encrypted_dns_proxy::config_resolver::resolve_default_config().await {
113+
Ok(new_configs) => {
114+
self.configurations = HashSet::from_iter(new_configs.into_iter());
115+
}
116+
Err(err) => {
117+
log::error!("Failed to fetch a new proxy configuration: {err:?}");
118+
if self.is_empty() {
119+
return Err(Error::FetchConfig(err));
120+
}
121+
}
122+
}
123+
Ok(())
124+
}
125+
126+
fn is_empty(&self) -> bool {
127+
self.configurations.is_empty()
128+
}
129+
130+
/// Checks if the `tried_configurations` set should be reset.
131+
/// It should only be reset if the difference between `configurations` and
132+
/// `tried_configurations` is an empty set - in this case all available configurations have
133+
/// been tried.
134+
fn should_reset(&self) -> bool {
135+
self.configurations
136+
.difference(&self.tried_configurations)
137+
.count()
138+
== 0
139+
}
140+
141+
/// Clears the `tried_configurations` set.
142+
fn reset(&mut self) {
143+
self.tried_configurations.clear();
144+
}
145+
}
146+
147+
/// Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
148+
#[no_mangle]
149+
pub unsafe extern "C" fn encrypted_dns_proxy_init() -> *mut EncryptedDnsProxyState {
150+
let state = Box::new(EncryptedDnsProxyState {
151+
configurations: Default::default(),
152+
tried_configurations: Default::default(),
153+
});
154+
Box::into_raw(state)
155+
}
156+
157+
/// This must be called only once to deallocate `EncryptedDnsProxyState`.
158+
///
159+
/// # Safety
160+
/// `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
161+
/// by `encrypted_dns_proxy_init`. This function is not thread safe, and should only be called
162+
/// once.
163+
#[no_mangle]
164+
pub unsafe extern "C" fn encrypted_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) {
165+
let _ = unsafe { Box::from_raw(ptr) };
166+
}
167+
168+
/// # Safety
169+
/// encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized
170+
/// by `encrypted_dns_proxy_init`. This function is not thread safe.
171+
/// `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This
172+
/// function is not thread safe, but it can be called repeatedly. Each successful invocation should
173+
/// clean up the resulting proxy via `[encrypted_dns_proxy_stop]`.
174+
///
175+
/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to
176+
/// deallocate the memory.
177+
#[no_mangle]
178+
pub unsafe extern "C" fn encrypted_dns_proxy_start(
179+
encrypted_dns_proxy: *mut EncryptedDnsProxyState,
180+
proxy_handle: *mut ProxyHandle,
181+
) -> i32 {
182+
let handle = match crate::mullvad_ios_runtime() {
183+
Ok(handle) => handle,
184+
Err(err) => {
185+
log::error!("Cannot instantiate a tokio runtime: {}", err);
186+
return Error::TokioRuntime.into();
187+
}
188+
};
189+
190+
let mut encrypted_dns_proxy = unsafe { Box::from_raw(encrypted_dns_proxy) };
191+
let proxy_result = handle.block_on(encrypted_dns_proxy.start());
192+
mem::forget(encrypted_dns_proxy);
193+
194+
match proxy_result {
195+
Ok(handle) => unsafe { ptr::write(proxy_handle, handle) },
196+
Err(err) => {
197+
let empty_handle = ProxyHandle {
198+
context: ptr::null_mut(),
199+
port: 0,
200+
};
201+
unsafe { ptr::write(proxy_handle, empty_handle) }
202+
log::error!("Failed to create a proxy connection: {err:?}");
203+
return err.into();
204+
}
205+
}
206+
207+
0
208+
}
209+
210+
/// # Safety
211+
/// `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by
212+
/// [`encrypted_dns_proxy_start`]. It should only ever be called once.
213+
#[no_mangle]
214+
pub unsafe extern "C" fn encrypted_dns_proxy_stop(proxy_config: *mut ProxyHandle) -> i32 {
215+
let ptr = unsafe { (*proxy_config).context };
216+
if !ptr.is_null() {
217+
let handle: Box<JoinHandle<()>> = unsafe { Box::from_raw(ptr.cast()) };
218+
handle.abort();
219+
}
220+
0i32
221+
}

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

0 commit comments

Comments
 (0)