Skip to content
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

Merged
merged 1 commit into from
Sep 25, 2024
Merged

Conversation

MarkusPettersson98
Copy link
Contributor

@MarkusPettersson98 MarkusPettersson98 commented Sep 23, 2024

This change is Reviewable

@MarkusPettersson98 MarkusPettersson98 marked this pull request as draft September 23, 2024 14:32
@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from bf15762 to 1922341 Compare September 23, 2024 14:35
@buggmagnet buggmagnet added the iOS Issues related to iOS label Sep 23, 2024
@pinkisemils
Copy link
Collaborator

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 hickory-resolver isn't actually taken into account - we should probably report it as an error to them.

@MarkusPettersson98 MarkusPettersson98 marked this pull request as ready for review September 24, 2024 14:09
Copy link
Member

@faern faern left a 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

Copy link
Member

@faern faern left a 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.

Copy link
Member

@dlon dlon left a 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?

@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from bd268b7 to 38aeb2b Compare September 24, 2024 17:45
Copy link
Collaborator

@pinkisemils pinkisemils left a 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.

Copy link
Contributor Author

@MarkusPettersson98 MarkusPettersson98 left a 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:

Copy link
Member

@faern faern left a 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.

Copy link
Member

@faern faern left a 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)

Copy link
Member

@faern faern left a 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.

Copy link
Member

@faern faern left a 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.

Copy link
Member

@faern faern left a 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.

Copy link
Contributor Author

@MarkusPettersson98 MarkusPettersson98 left a 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.

@buggmagnet buggmagnet requested review from dlon and faern September 25, 2024 08:07
Copy link
Contributor

@buggmagnet buggmagnet left a 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 ?

Copy link
Contributor

@buggmagnet buggmagnet left a 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)

Copy link
Contributor Author

@MarkusPettersson98 MarkusPettersson98 left a 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

Copy link
Contributor Author

@MarkusPettersson98 MarkusPettersson98 left a 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

Copy link
Contributor

@buggmagnet buggmagnet left a 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)

@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from dfe49e8 to 196e4a1 Compare September 25, 2024 11:26
Copy link
Contributor

@buggmagnet buggmagnet left a 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

@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from 196e4a1 to 456ed2d Compare September 25, 2024 11:51
buggmagnet
buggmagnet previously approved these changes Sep 25, 2024
Copy link
Contributor

@buggmagnet buggmagnet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

Reviewed all commit messages.
Reviewable status: 5 of 9 files reviewed, 5 unresolved discussions (waiting on @faern and @MarkusPettersson98)

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());
Copy link
Member

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?

@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from 7243087 to 4c27a41 Compare September 25, 2024 12:51
Copy link
Member

@faern faern left a 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 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

Avoidable without using the currently proposed code style. But too many reviewers here, so I'll see myself out.

@faern faern self-requested a review September 25, 2024 13:03
@pinkisemils pinkisemils force-pushed the ios-use-encrypted-dns-proxy branch from 5714d71 to 9604d1a Compare September 25, 2024 14:04
@pinkisemils pinkisemils merged commit dfa53d8 into main Sep 25, 2024
46 of 47 checks passed
@pinkisemils pinkisemils deleted the ios-use-encrypted-dns-proxy branch September 25, 2024 14:07
Copy link

🚨 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),
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
iOS Issues related to iOS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants