-
Notifications
You must be signed in to change notification settings - Fork 389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use encrypted dns proxy in mullvad-ios
#6846
Conversation
bf15762
to
1922341
Compare
We believe this is finally ready for review. With these changes, I am introducing an FFI to be used by the iOS codebase. It also adds a kind of a retry strategy. This was a joint effort between @MarkusPettersson98 and me. Whilst in the weeds, we found that the timeout in the resolver config for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed in the office, please see this document for the latest spec. I believe the only difference from the code you propose here, is how we pick XOR over plain: https://mullvad.atlassian.net/wiki/spaces/CLIEN/pages/3736502288/Encrypted+DNS+proxy
Reviewable status: 0 of 9 files reviewed, all discussions resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 9 files reviewed, 1 unresolved discussion (waiting on @MarkusPettersson98)
mullvad-encrypted-dns-proxy/src/config/mod.rs
line 79 at r1 (raw file):
pub obfuscation: Option<ObfuscationConfig>, pub r#type: ProxyType,
As discussed in the office: Remove this field and instead derive proxy type (to prefer XOR over plain) via the obfuscation
field above instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 9 files reviewed, 2 unresolved discussions (waiting on @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 154 at r2 (raw file):
#[no_mangle] pub unsafe extern "C" fn encrytped_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) { mem::forget(unsafe { Box::from_raw(ptr) })
Should forget
be called here?
bd268b7
to
38aeb2b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 9 files reviewed, 2 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 154 at r2 (raw file):
Previously, dlon (David Lönnhager) wrote…
Should
forget
be called here?
No, we should not. This was most certainly not my best judgement call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 5 of 9 files at r1, 3 of 4 files at r2, all commit messages.
Reviewable status: 0 of 9 files reviewed, 6 unresolved discussions (waiting on @dlon and @faern)
mullvad-encrypted-dns-proxy/src/config/mod.rs
line 79 at r1 (raw file):
Previously, faern (Linus Färnstrand) wrote…
As discussed in the office: Remove this field and instead derive proxy type (to prefer XOR over plain) via the
obfuscation
field above instead.
Done
mullvad-ios/src/encrypted_dns_proxy.rs
line 113 at r3 (raw file):
self.tried_configurations.insert(config.clone()); Some(config) }
Nit What do you think about appying some DRY? Nice to not need the explicit early return in the middle of the function imho.
fn next_configuration(&mut self) -> Option<ProxyConfig> {
let mut difference = self.configurations.difference(&self.tried_configurations);
let config = difference
// prefer obfuscated proxy configurations.
.find(|config| config.obfuscation.is_some())
.or_else(|| difference.next())
.cloned()?;
self.tried_configurations.insert(config.clone());
Some(config)
}
Code quote:
fn next_configuration(&mut self) -> Option<ProxyConfig> {
if let Some(xor_config) = self
.configurations
.difference(&self.tried_configurations)
// prefer obfuscated proxy configurations.
.find(|config| config.obfuscation.is_some())
.cloned()
{
self.tried_configurations.insert(xor_config.clone());
return Some(xor_config);
}
let config = self
.configurations
.difference(&self.tried_configurations)
.next()
.cloned()?;
self.tried_configurations.insert(config.clone());
Some(config)
}
mullvad-ios/src/encrypted_dns_proxy.rs
line 159 at r3 (raw file):
/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to /// deallocate the memory. ///
Nit: Trailing blank line
Code quote:
/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to
/// deallocate the memory.
///
mullvad-ios/src/encrypted_dns_proxy.rs
line 170 at r3 (raw file):
log::error!("Cannot instantiate a tokio runtime: {}", err); return -1; }
Do we want to promote -1
error code to a variant of the Error
enum defined in this module? Currently, just looking at the conversion from Error
to an error code / i32
implies that there is a variant missing
impl From<Error> for i32 {
fn from(err: Error) -> Self {
match err {
Error::BindLocalSocket(_) => -2,
Error::GetBindAddr(_) => -3,
Error::Forwarder(_) => -4,
Error::FetchConfig(_) => -5,
Error::NoConfigs => -6,
}
}
}
Let's say we add Error::Runtime
, we could get rid of the 'magic' error value from the encrypted_dns_proxy_start
function
#[derive(Debug)]
pub enum Error {
/// Failed to initialize the tokio runtime.
Runtime,
/// 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 new.
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::Runtime => -1,
Error::BindLocalSocket(_) => -2,
Error::GetBindAddr(_) => -3,
Error::Forwarder(_) => -4,
Error::FetchConfig(_) => -5,
Error::NoConfigs => -6,
}
}
}
...
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::Runtime.into();
}
};
...
Code quote:
Err(err) => {
log::error!("Cannot instantiate a tokio runtime: {}", err);
return -1;
}
mullvad-ios/src/encrypted_dns_proxy.rs
line 188 at r3 (raw file):
} /// SAFETY:
Nit:
/// # Safety
Code quote:
/// SAFETY:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 9 files reviewed, 6 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
mullvad-encrypted-dns-proxy/src/config/mod.rs
line 40 at r3 (raw file):
/// Type of a proxy configuration. Derived from the 2nd hextet of an IPv6 address in network byte /// order. E.g. an IPv6 address such as `7f7f:2323::` would have a proxy type value of `0x2323`. #[derive(PartialEq, Debug, Eq, Hash, Clone)]
These extra impls can be removed now, when we decided to not expose this type publicly. The crate does not need those impls internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 2 of 4 files at r2.
Reviewable status: 2 of 9 files reviewed, 6 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 2 of 9 files reviewed, 7 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 16 at r3 (raw file):
/// random configurations every time. configurations: HashSet<ProxyConfig>, tried_configurations: HashSet<ProxyConfig>,
Emils and I have touched on this topic in the office. I just want to point out that we want the exact same proxy selection algorithm on all other platforms. So if you decide to merge this into mullvad-ios
now it has to be moved later. We are not going to duplicate the entire selection algorithm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 2 of 9 files reviewed, 7 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 113 at r3 (raw file):
Previously, MarkusPettersson98 (Markus Pettersson) wrote…
Nit What do you think about appying some DRY? Nice to not need the explicit early return in the middle of the function imho.
fn next_configuration(&mut self) -> Option<ProxyConfig> { let mut difference = self.configurations.difference(&self.tried_configurations); let config = difference // prefer obfuscated proxy configurations. .find(|config| config.obfuscation.is_some()) .or_else(|| difference.next()) .cloned()?; self.tried_configurations.insert(config.clone()); Some(config) }
Computing the difference fewer times would indeed be nice. Not because of the computational cost, but because it's currently done three times and it's a lot of code and the difference is never given a name. If it's given a name it can be easier to read, and less code as well.
If the insertion into tried_configurations
can be in a single place that would also be very nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 2 of 9 files reviewed, 8 unresolved discussions (waiting on @dlon and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 65 at r3 (raw file):
self.fetch_configs().await?; if self.should_reset() { self.reset();
IMO, the reset is part of the selection algorithm which is "owned" by the next_configuration
method. I feel like it would be more logical if the reset happens there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 2 of 9 files reviewed, 8 unresolved discussions (waiting on @dlon and @faern)
mullvad-encrypted-dns-proxy/src/config/mod.rs
line 40 at r3 (raw file):
Previously, faern (Linus Färnstrand) wrote…
These extra impls can be removed now, when we decided to not expose this type publicly. The crate does not need those impls internally.
Done.
mullvad-ios/src/encrypted_dns_proxy.rs
line 16 at r3 (raw file):
Previously, faern (Linus Färnstrand) wrote…
Emils and I have touched on this topic in the office. I just want to point out that we want the exact same proxy selection algorithm on all other platforms. So if you decide to merge this into
mullvad-ios
now it has to be moved later. We are not going to duplicate the entire selection algorithm.
Yes, that makes sense. I also think it makes sense to have it in mullvad-ios
for now and move it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 2 of 9 files at r1, 1 of 4 files at r2.
Reviewable status: 4 of 9 files reviewed, 12 unresolved discussions (waiting on @dlon, @faern, and @MarkusPettersson98)
mullvad-encrypted-dns-proxy/src/config_resolver.rs
line 65 at r3 (raw file):
pub async fn resolve_default_config() -> Result<Vec<config::ProxyConfig>, Error> { resolve_configs(&default_resolvers(), "frakta.eu").await
nit:
If we ever want to this be usable in staging (or other) environments, we should either let the call site (via the FFI) pass the domain to resolve, or use a configuration flag to modify this.
But I'm okay with this for a v1, just food for thought here.
mullvad-ios/Cargo.toml
line 13 at r3 (raw file):
workspace = true [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
nit:
This shouldn't be necessary right ?
mullvad-ios/src/encrypted_dns_proxy.rs
line 27 at r3 (raw file):
/// Failed to initialize forwarder. Forwarder(io::Error), /// Failed to fetch new.
Failed to fetch a new configuration I imagine ?
mullvad-ios/src/encrypted_dns_proxy.rs
line 77 at r3 (raw file):
.await .map_err(Error::Forwarder)?; let join_handle = Box::new(tokio::spawn(async move {
Will this be created on the shared tokio runtime that was gotten in encrypted_dns_proxy_start
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to separate the FFI from the implementation from a design point of view, especially if we want to have this shared with other clients. They do not need for example, the shared rust runtime from iOS
Reviewable status: 4 of 9 files reviewed, 12 unresolved discussions (waiting on @dlon, @faern, and @MarkusPettersson98)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 4 of 9 files reviewed, 12 unresolved discussions (waiting on @buggmagnet, @dlon, and @faern)
mullvad-ios/src/encrypted_dns_proxy.rs
line 113 at r3 (raw file):
Previously, faern (Linus Färnstrand) wrote…
Computing the difference fewer times would indeed be nice. Not because of the computational cost, but because it's currently done three times and it's a lot of code and the difference is never given a name. If it's given a name it can be easier to read, and less code as well.
If the insertion into
tried_configurations
can be in a single place that would also be very nice.
As Emils pointed out to me, find
may exhaust the iterator if there are no obfuscator configs, thus calling next
on the iterator at that point will yield None
. Here is an example to prove that:
#[test]
fn test_differences() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(1);
set.insert(2);
let mut other_set = HashSet::new();
other_set.insert(1);
let mut difference = set.difference(&other_set);
difference.find(|x| **x == 3);
assert_eq!(difference.next(), Some(2).as_ref())
}
It is solvable by recomputing the difference in the closure passed to or_else
. But we will still potentially recompute it as we do in the current implementation, FYI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will figure this out when we implement this for other clients. For now, we've decided to keep it as is for simplicity.
Reviewable status: 3 of 9 files reviewed, 8 unresolved discussions (waiting on @buggmagnet and @faern)
mullvad-encrypted-dns-proxy/src/config_resolver.rs
line 65 at r3 (raw file):
Previously, buggmagnet wrote…
nit:
If we ever want to this be usable in staging (or other) environments, we should either let the call site (via the FFI) pass the domain to resolve, or use a configuration flag to modify this.But I'm okay with this for a v1, just food for thought here.
That's an interesting idea, but out of scope for this iteration :)
mullvad-ios/src/encrypted_dns_proxy.rs
line 27 at r3 (raw file):
Previously, buggmagnet wrote…
Failed to fetch a new configuration I imagine ?
Yes!
mullvad-ios/src/encrypted_dns_proxy.rs
line 65 at r3 (raw file):
Previously, faern (Linus Färnstrand) wrote…
IMO, the reset is part of the selection algorithm which is "owned" by the
next_configuration
method. I feel like it would be more logical if the reset happens there.
Correct, implemented
mullvad-ios/src/encrypted_dns_proxy.rs
line 77 at r3 (raw file):
Previously, buggmagnet wrote…
Will this be created on the shared tokio runtime that was gotten in
encrypted_dns_proxy_start
?
Yes
mullvad-ios/Cargo.toml
line 13 at r3 (raw file):
Previously, buggmagnet wrote…
nit:
This shouldn't be necessary right ?
You're right
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me @MarkusPettersson98 👍
Reviewable status: 3 of 9 files reviewed, 5 unresolved discussions (waiting on @faern and @MarkusPettersson98)
dfe49e8
to
196e4a1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 1 files at r4, 1 of 3 files at r6.
Reviewable status: 5 of 9 files reviewed, 5 unresolved discussions (waiting on @faern and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 195 at r6 (raw file):
/// # Safety /// `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by
We should mention thread safety here too
196e4a1
to
456ed2d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed all commit messages.
Reviewable status: 5 of 9 files reviewed, 5 unresolved discussions (waiting on @faern and @MarkusPettersson98)
456ed2d
to
6c1db7c
Compare
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is important for the random order promises we make. If self.configurations
were not re-created but rather the same HashSet
was cleared and re-filled, then we would be stuck in trying proxies in the same order every time. Maybe this line should document that it's important for that reason?
7243087
to
4c27a41
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 4 of 9 files reviewed, 3 unresolved discussions (waiting on @buggmagnet and @MarkusPettersson98)
mullvad-ios/src/encrypted_dns_proxy.rs
line 113 at r3 (raw file):
Previously, MarkusPettersson98 (Markus Pettersson) wrote…
As Emils pointed out to me,
find
may exhaust the iterator if there are no obfuscator configs, thus callingnext
on the iterator at that point will yieldNone
. Here is an example to prove that:#[test] fn test_differences() { use std::collections::HashSet; let mut set = HashSet::new(); set.insert(1); set.insert(2); let mut other_set = HashSet::new(); other_set.insert(1); let mut difference = set.difference(&other_set); difference.find(|x| **x == 3); assert_eq!(difference.next(), Some(2).as_ref()) }It is solvable by recomputing the difference in the closure passed to
or_else
. But we will still potentially recompute it as we do in the current implementation, FYI
Avoidable without using the currently proposed code style. But too many reviewers here, so I'll see myself out.
5714d71
to
9604d1a
Compare
🚨 End to end tests failed. Please check the failed workflow run. |
self.0.source() | ||
match self { | ||
Self::ResolutionError(ref err) => Some(err), | ||
Self::Timeout(ref err) => Some(err), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't both return the inner error as our source and use the inner errors Display
implementation up in our Display
implementation. That makes the error stack have two entries that are identical. It will end up like this:
Error: Timeout reached
Caused by: Timeout reached
If you use an inner error as your own message in Display
then in your implementation of Error::source
you need to return inner_error.source()
as your source. To cut out this repetition.
This change is