diff --git a/ios/Configurations/Api.xcconfig.template b/ios/Configurations/Api.xcconfig.template
index 8d3b16010917..0fb8b09e9007 100644
--- a/ios/Configurations/Api.xcconfig.template
+++ b/ios/Configurations/Api.xcconfig.template
@@ -8,6 +8,11 @@ API_HOST_NAME[config=Release] = api.$(HOST_NAME)
API_HOST_NAME[config=MockRelease] = api.$(HOST_NAME)
API_HOST_NAME[config=Staging] = api.$(HOST_NAME)
+ENCRYPTED_DNS_HOST_NAME[config=Debug] = frakta.eu
+ENCRYPTED_DNS_HOST_NAME[config=Release] = frakta.eu
+ENCRYPTED_DNS_HOST_NAME[config=MockRelease] = frakta.eu
+ENCRYPTED_DNS_HOST_NAME[config=Staging] = stagemole.frakta.eu
+
API_ENDPOINT[config=Debug] = 45.83.223.196:443
API_ENDPOINT[config=Release] = 45.83.223.196:443
API_ENDPOINT[config=MockRelease] = 45.83.223.196:443
diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
index 1f59b10f9233..8875096931e3 100644
--- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
+++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift
@@ -21,6 +21,8 @@ extension REST {
/// Default API endpoint.
public static let defaultAPIEndpoint = AnyIPEndpoint(string: infoDictionary["ApiEndpoint"] as! String)!
+ public static let encryptedDNSHostname = infoDictionary["EncryptedDnsHostName"] as! String
+
/// Disables API IP address cache when in staging environment and sticks to using default API endpoint instead.
public static let isStagingEnvironment = false
diff --git a/ios/MullvadREST/Info.plist b/ios/MullvadREST/Info.plist
index b66a823a4074..644beb120a8f 100644
--- a/ios/MullvadREST/Info.plist
+++ b/ios/MullvadREST/Info.plist
@@ -6,5 +6,7 @@
$(API_HOST_NAME)
ApiEndpoint
$(API_ENDPOINT)
+ EncryptedDnsHostName
+ $(ENCRYPTED_DNS_HOST_NAME)
diff --git a/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift b/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift
index 230743bdfa8b..2910a14eed96 100644
--- a/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift
+++ b/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift
@@ -22,7 +22,7 @@ public final class EncryptedDNSTransport: RESTTransport {
public init(urlSession: URLSession) {
self.urlSession = urlSession
- self.encryptedDnsProxy = EncryptedDNSProxy()
+ self.encryptedDnsProxy = EncryptedDNSProxy(domain: REST.encryptedDNSHostname)
}
public func stop() {
diff --git a/ios/MullvadRustRuntime/EncryptedDNSProxy.swift b/ios/MullvadRustRuntime/EncryptedDNSProxy.swift
index 690e2459d099..85c605fac5db 100644
--- a/ios/MullvadRustRuntime/EncryptedDNSProxy.swift
+++ b/ios/MullvadRustRuntime/EncryptedDNSProxy.swift
@@ -18,9 +18,11 @@ public class EncryptedDNSProxy {
private var stateLock = NSLock()
private var didStart = false
private let state: OpaquePointer
+ private let domain: String
- public init() {
- state = encrypted_dns_proxy_init()
+ public init(domain: String) {
+ self.domain = domain
+ state = encrypted_dns_proxy_init(domain)
proxyConfig = ProxyHandle(context: nil, port: 0)
}
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
index 26904b89dfe8..a45c6ed6c328 100644
--- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -33,8 +33,17 @@ extern const uint16_t CONFIG_SERVICE_PORT;
/**
* Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
+ *
+ * # Safety
+ *
+ * * [domain_name] must not be non-null.
+ *
+ * * [domain_name] pointer must be [valid](core::ptr#safety)
+ *
+ * * The caller must ensure that the pointer to the [domain_name] string contains a nul terminator
+ * at the end of the string.
*/
-struct EncryptedDnsProxyState *encrypted_dns_proxy_init(void);
+struct EncryptedDnsProxyState *encrypted_dns_proxy_init(const char *domain_name);
/**
* This must be called only once to deallocate `EncryptedDnsProxyState`.
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
index c3f73d7de992..676ef4b4940b 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift
@@ -45,9 +45,9 @@ class EditAccessMethodViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
- view.accessibilityIdentifier = .editAccessMethodView
view.backgroundColor = .secondaryColor
+ tableView.accessibilityIdentifier = .editAccessMethodView
tableView.backgroundColor = .secondaryColor
tableView.delegate = self
diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
index c2bbcfb73526..57d1340fa486 100644
--- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
+++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift
@@ -37,16 +37,19 @@ final class TransportMonitor: RESTTransportProvider {
tunnel.status == .connecting || tunnel.status == .reasserting || tunnel.status == .connected
}
- if let tunnel, shouldByPassVPN(tunnel: tunnel) {
+ if let tunnel, shouldBypassVPN(tunnel: tunnel) {
return PacketTunnelTransport(tunnel: tunnel)
} else {
return transportProvider.makeTransport()
}
}
- private func shouldByPassVPN(tunnel: any TunnelProtocol) -> Bool {
+ private func shouldBypassVPN(tunnel: any TunnelProtocol) -> Bool {
switch tunnel.status {
case .connected:
+ if case .error = tunnelManager.tunnelStatus.state {
+ return true
+ }
return tunnelManager.isConfigurationLoaded && tunnelManager.deviceState == .revoked
case .connecting, .reasserting:
diff --git a/ios/MullvadVPNTests/Info.plist b/ios/MullvadVPNTests/Info.plist
index 8eb7b6a836da..bd4375ba323a 100644
--- a/ios/MullvadVPNTests/Info.plist
+++ b/ios/MullvadVPNTests/Info.plist
@@ -2,6 +2,8 @@
+ EncryptedDnsHostName
+ $(ENCRYPTED_DNS_HOST_NAME)
ApiHostName
$(API_HOST_NAME)
ApiEndpoint
diff --git a/ios/MullvadVPNUITests/ConnectivityTests.swift b/ios/MullvadVPNUITests/ConnectivityTests.swift
index 32a219d05e83..6b66a2d5142c 100644
--- a/ios/MullvadVPNUITests/ConnectivityTests.swift
+++ b/ios/MullvadVPNUITests/ConnectivityTests.swift
@@ -15,6 +15,11 @@ class ConnectivityTests: LoggedOutUITestCase {
/// Verifies that the app still functions when API has been blocked
func testAPIConnectionViaBridges() throws {
+ let skipReason = """
+ This test is currently skipped because shadowsocks bridges cannot be reached
+ from the staging environment
+ """
+ try XCTSkipIf(true, skipReason)
firewallAPIClient.removeRules()
let hasTimeAccountNumber = getAccountWithTime()
@@ -138,6 +143,12 @@ class ConnectivityTests: LoggedOutUITestCase {
// swiftlint:disable function_body_length
/// Test that the app is functioning when API is down. To simulate API being down we create a dummy access method
func testAppStillFunctioningWhenAPIDown() throws {
+ let skipReason = """
+ This test is currently skipped due to a bug in iOS 18 where ATS shuts down the
+ connection to the API in the blocked state, despite being explicitly disabled,
+ and after the checks in SSLPinningURLSessionDelegate return no error.
+ """
+ try XCTSkipIf(true, skipReason)
let hasTimeAccountNumber = getAccountWithTime()
addTeardownBlock {
diff --git a/ios/MullvadVPNUITests/Info.plist b/ios/MullvadVPNUITests/Info.plist
index e01e57495e19..229e9483278b 100644
--- a/ios/MullvadVPNUITests/Info.plist
+++ b/ios/MullvadVPNUITests/Info.plist
@@ -10,6 +10,8 @@
$(API_ENDPOINT)
ApiHostName
$(API_HOST_NAME)
+ EncryptedDnsHostName
+ $(ENCRYPTED_DNS_HOST_NAME)
AttachAppLogsOnFailure
$(ATTACH_APP_LOGS_ON_FAILURE)
DisplayName
diff --git a/mullvad-daemon/src/api.rs b/mullvad-daemon/src/api.rs
index 2558dbfee86a..a0fef2c98428 100644
--- a/mullvad-daemon/src/api.rs
+++ b/mullvad-daemon/src/api.rs
@@ -609,7 +609,7 @@ impl AccessModeSelector {
ApiConnectionMode::Proxied(ProxyConfig::from(proxy))
}
AccessMethod::BuiltIn(BuiltInAccessMethod::EncryptedDnsProxy) => {
- if let Err(error) = encrypted_dns_proxy_cache.fetch_configs().await {
+ if let Err(error) = encrypted_dns_proxy_cache.fetch_configs("frakta.eu").await {
log::warn!("Failed to fetch new Encrypted DNS Proxy configurations");
log::debug!("{error:#?}");
}
diff --git a/mullvad-encrypted-dns-proxy/src/config_resolver.rs b/mullvad-encrypted-dns-proxy/src/config_resolver.rs
index b763183a1b1e..82edd886f529 100644
--- a/mullvad-encrypted-dns-proxy/src/config_resolver.rs
+++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs
@@ -61,11 +61,12 @@ pub fn default_resolvers() -> Vec {
]
}
-pub async fn resolve_default_config() -> Result, Error> {
- resolve_configs(&default_resolvers(), "frakta.eu").await
+/// Calls [resolve_configs] with a given `domain` using known DoH resolvers provided by [default_resolvers]
+pub async fn resolve_default_config(domain: &str) -> Result, Error> {
+ resolve_configs(&default_resolvers(), domain).await
}
-/// Look up the `domain` towards the given `resolvers`, and try to deserialize all the returned
+/// Looks 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],
diff --git a/mullvad-encrypted-dns-proxy/src/state.rs b/mullvad-encrypted-dns-proxy/src/state.rs
index daad7123beea..8b6c3c988627 100644
--- a/mullvad-encrypted-dns-proxy/src/state.rs
+++ b/mullvad-encrypted-dns-proxy/src/state.rs
@@ -58,9 +58,9 @@ impl EncryptedDnsProxyState {
Some(selected_config)
}
- /// Fetch a config, but error out only when no existing configuration was there.
- pub async fn fetch_configs(&mut self) -> Result<(), FetchConfigError> {
- match resolve_default_config().await {
+ /// Fetch a config from `domain`, but error out only when no existing configuration was there.
+ pub async fn fetch_configs(&mut self, domain: &str) -> Result<(), FetchConfigError> {
+ match resolve_default_config(domain).await {
Ok(new_configs) => {
self.configurations = HashSet::from_iter(new_configs.into_iter());
}
diff --git a/mullvad-ios/src/encrypted_dns_proxy.rs b/mullvad-ios/src/encrypted_dns_proxy.rs
index cf4219897e18..2aa83d833dc0 100644
--- a/mullvad-ios/src/encrypted_dns_proxy.rs
+++ b/mullvad-ios/src/encrypted_dns_proxy.rs
@@ -1,5 +1,6 @@
use crate::ProxyHandle;
+use libc::c_char;
use mullvad_encrypted_dns_proxy::state::{EncryptedDnsProxyState as State, FetchConfigError};
use mullvad_encrypted_dns_proxy::Forwarder;
use std::{
@@ -9,10 +10,13 @@ use std::{
};
use tokio::{net::TcpListener, task::JoinHandle};
+use std::ffi::CStr;
+
/// A thin wrapper around [`mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState`] that
/// can start a local forwarder (see [`Self::start`]).
pub struct EncryptedDnsProxyState {
state: State,
+ domain: String,
}
#[derive(Debug)]
@@ -47,7 +51,7 @@ impl From for i32 {
impl EncryptedDnsProxyState {
async fn start(&mut self) -> Result {
self.state
- .fetch_configs()
+ .fetch_configs(&self.domain)
.await
.map_err(Error::FetchConfig)?;
let proxy_configuration = self.state.next_configuration().ok_or(Error::NoConfigs)?;
@@ -78,10 +82,28 @@ impl EncryptedDnsProxyState {
}
/// Initializes a valid pointer to an instance of `EncryptedDnsProxyState`.
+///
+/// # Safety
+///
+/// * [domain_name] must not be non-null.
+///
+/// * [domain_name] pointer must be [valid](core::ptr#safety)
+///
+/// * The caller must ensure that the pointer to the [domain_name] string contains a nul terminator
+/// at the end of the string.
#[no_mangle]
-pub unsafe extern "C" fn encrypted_dns_proxy_init() -> *mut EncryptedDnsProxyState {
+pub unsafe extern "C" fn encrypted_dns_proxy_init(
+ domain_name: *const c_char,
+) -> *mut EncryptedDnsProxyState {
+ let domain = {
+ // SAFETY: domain_name points to a valid region of memory and contains a nul terminator.
+ let c_str = unsafe { CStr::from_ptr(domain_name) };
+ String::from_utf8_lossy(c_str.to_bytes())
+ };
+
let state = Box::new(EncryptedDnsProxyState {
state: State::default(),
+ domain: domain.into_owned(),
});
Box::into_raw(state)
}