diff options
| author | Andrew Bulhak <andrew.bulhak@mullvad.net> | 2025-07-15 17:14:46 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-08-12 14:49:43 +0200 |
| commit | 6210557bbb92934be1e0697a2129642ef95c5dd3 (patch) | |
| tree | bbdd14a370c7d38b183d5f8e1901404ec4d32f2b | |
| parent | cf0cb9934d732b045803c0affce291d11c6251fa (diff) | |
| download | mullvadvpn-6210557bbb92934be1e0697a2129642ef95c5dd3.tar.xz mullvadvpn-6210557bbb92934be1e0697a2129642ef95c5dd3.zip | |
Feed access method UUID back from Rust to Swift, and save it
| -rw-r--r-- | ios/MullvadRustRuntime/MullvadApiContext.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadRustRuntime/include/mullvad_rust_runtime.h | 15 | ||||
| -rw-r--r-- | ios/MullvadSettings/AccessMethodRepository.swift | 10 | ||||
| -rw-r--r-- | ios/MullvadSettings/MullvadAccessMethodChangeListening.swift | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 1 | ||||
| -rw-r--r-- | mullvad-api/src/access_mode.rs | 43 | ||||
| -rw-r--r-- | mullvad-ios/src/api_client/mod.rs | 45 | ||||
| -rw-r--r-- | mullvad-types/src/access_method.rs | 4 |
9 files changed, 121 insertions, 36 deletions
diff --git a/ios/MullvadRustRuntime/MullvadApiContext.swift b/ios/MullvadRustRuntime/MullvadApiContext.swift index 7600b79e70..1f1712c624 100644 --- a/ios/MullvadRustRuntime/MullvadApiContext.swift +++ b/ios/MullvadRustRuntime/MullvadApiContext.swift @@ -6,18 +6,28 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // +import MullvadSettings import MullvadTypes -public struct MullvadApiContext: @unchecked Sendable { +func onAccessChangeCallback(selfPtr: UnsafeRawPointer?, bytes: UnsafePointer<UInt8>?) { + guard let selfPtr, let bytes else { return } + let context = selfPtr.assumingMemoryBound(to: MullvadApiContext.self).pointee + + let uuid = NSUUID(uuidBytes: bytes) as UUID + context.accessMethodChangeListener?.accessMethodChangedTo(uuid) +} + +public class MullvadApiContext: @unchecked Sendable { enum Error: Swift.Error { case failedToConstructApiClient } - public let context: SwiftApiContext + public private(set) var context: SwiftApiContext! private let shadowsocksBridgeProvider: SwiftShadowsocksBridgeProviding! private let shadowsocksBridgeProviderWrapper: SwiftShadowsocksLoaderWrapper! private let addressCacheWrapper: SwiftAddressCacheWrapper! private let addressCacheProvider: AddressCacheProviding! + public var accessMethodChangeListener: MullvadAccessMethodChangeListening? public init( host: String, @@ -36,6 +46,7 @@ public struct MullvadApiContext: @unchecked Sendable { self.addressCacheProvider = defaultAddressCache self.addressCacheWrapper = iniSwiftAddressCacheWrapper(provider: defaultAddressCache) + context = nil context = switch disableTls { case true: mullvad_api_init_new_tls_disabled( @@ -44,7 +55,9 @@ public struct MullvadApiContext: @unchecked Sendable { domain, shadowsocksBridgeProviderWrapper, accessMethodWrapper, - addressCacheWrapper + addressCacheWrapper, + onAccessChangeCallback, + Unmanaged.passRetained(self).toOpaque() ) case false: mullvad_api_init_new( @@ -53,7 +66,9 @@ public struct MullvadApiContext: @unchecked Sendable { domain, shadowsocksBridgeProviderWrapper, accessMethodWrapper, - addressCacheWrapper + addressCacheWrapper, + onAccessChangeCallback, + Unmanaged.passRetained(self).toOpaque() ) } diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 751e58aae0..36b49ea915 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -169,7 +169,10 @@ struct SwiftApiContext mullvad_api_init_new_tls_disabled(const char *host, const char *domain, struct SwiftShadowsocksLoaderWrapper bridge_provider, struct SwiftAccessMethodSettingsWrapper settings_provider, - struct SwiftAddressCacheWrapper address_cache); + struct SwiftAddressCacheWrapper address_cache, + void (*access_method_change_callback)(const void*, + const uint8_t*), + const void *access_method_change_context); /** * # Safety @@ -190,7 +193,10 @@ struct SwiftApiContext mullvad_api_init_new(const char *host, const char *domain, struct SwiftShadowsocksLoaderWrapper bridge_provider, struct SwiftAccessMethodSettingsWrapper settings_provider, - struct SwiftAddressCacheWrapper address_cache); + struct SwiftAddressCacheWrapper address_cache, + void (*access_method_change_callback)(const void*, + const uint8_t*), + const void *access_method_change_context); /** * # Safety @@ -212,7 +218,10 @@ struct SwiftApiContext mullvad_api_init_inner(const char *host, bool disable_tls, struct SwiftShadowsocksLoaderWrapper bridge_provider, struct SwiftAccessMethodSettingsWrapper settings_provider, - struct SwiftAddressCacheWrapper address_cache); + struct SwiftAddressCacheWrapper address_cache, + void (*access_method_change_callback)(const void*, + const uint8_t*), + const void *access_method_change_context); /** * Converts parameters into a `Box<AccessMethodSetting>` raw representation that diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift index 72cfc0668e..32c7ba4e5c 100644 --- a/ios/MullvadSettings/AccessMethodRepository.swift +++ b/ios/MullvadSettings/AccessMethodRepository.swift @@ -175,3 +175,13 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol, @unchecked SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) } } + +extension AccessMethodRepository: MullvadAccessMethodChangeListening { + public func accessMethodChangedTo(_ uuid: UUID) { + guard let method = accessMethodsSubject.value.first(where: { $0.id == uuid }) else { + logger.warning("Change reported to method with unknown ID: \(uuid)") + return + } + save(method) + } +} diff --git a/ios/MullvadSettings/MullvadAccessMethodChangeListening.swift b/ios/MullvadSettings/MullvadAccessMethodChangeListening.swift new file mode 100644 index 0000000000..679d5e66fb --- /dev/null +++ b/ios/MullvadSettings/MullvadAccessMethodChangeListening.swift @@ -0,0 +1,12 @@ +// +// MullvadAccessMethodChangeListening.swift +// MullvadVPN +// +// Created by Andrew Bulhak on 2025-07-03. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +// A protocol that listens for notifications of when the current access method has changed. It receives only the UUID of the new method. +public protocol MullvadAccessMethodChangeListening: AnyObject { + func accessMethodChangedTo(_ uuid: UUID) +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index dcc97b34e1..9cd08dd97d 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */; }; 447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */; }; 447F3D8B2CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */; }; + 4483EC372E26A53D007E5473 /* MullvadAccessMethodChangeListening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4483EC352E2693D5007E5473 /* MullvadAccessMethodChangeListening.swift */; }; 449275422C3570CA000526DE /* ICMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275412C3570CA000526DE /* ICMP.swift */; }; 4495ECD12D0B170700A7358B /* UDPOverTCPObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD02D0B16F700A7358B /* UDPOverTCPObfuscationSettingsPage.swift */; }; 4495ECD52D131A4800A7358B /* ShadowsocksObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */; }; @@ -1648,6 +1649,7 @@ 4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChoiceList.swift; sourceTree = "<group>"; }; 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsViewModel.swift; sourceTree = "<group>"; }; 447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsView.swift; sourceTree = "<group>"; }; + 4483EC352E2693D5007E5473 /* MullvadAccessMethodChangeListening.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAccessMethodChangeListening.swift; sourceTree = "<group>"; }; 449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = "<group>"; }; 449275432C3C3029000526DE /* TunnelPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelPinger.swift; sourceTree = "<group>"; }; 4495ECD02D0B16F700A7358B /* UDPOverTCPObfuscationSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsPage.swift; sourceTree = "<group>"; }; @@ -3785,6 +3787,7 @@ 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */, 068CE5732927B7A400A068BB /* Migration.swift */, A9D96B192A8247C100A5C673 /* MigrationManager.swift */, + 4483EC352E2693D5007E5473 /* MullvadAccessMethodChangeListening.swift */, 58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */, F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */, 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */, @@ -6207,6 +6210,7 @@ A93181A12B727ED700E341D2 /* TunnelSettingsV4.swift in Sources */, 58FE25BF2AA72311003D1918 /* MigrationManager.swift in Sources */, 58B2FDEF2AA720C4003EB5C6 /* ApplicationTarget.swift in Sources */, + 4483EC372E26A53D007E5473 /* MullvadAccessMethodChangeListening.swift in Sources */, A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */, 58B2FDDE2AA71D5C003EB5C6 /* Migration.swift in Sources */, F05769BB2C6661EE00D9778B /* TunnelSettingsStrategy.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 2590e2f1e7..dee24eaa89 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -118,6 +118,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accessMethodsDataSource: accessMethodRepository.accessMethodsPublisher, lastReachableDataSource: accessMethodRepository.lastReachableAccessMethodPublisher ) + apiContext.accessMethodChangeListener = accessMethodRepository setUpProxies(containerURL: containerURL) let backgroundTaskProvider = BackgroundTaskProvider( diff --git a/mullvad-api/src/access_mode.rs b/mullvad-api/src/access_mode.rs index 666488f59b..465d98bc95 100644 --- a/mullvad-api/src/access_mode.rs +++ b/mullvad-api/src/access_mode.rs @@ -26,6 +26,7 @@ pub enum Message { ), } +#[derive(Debug)] pub enum AccessMethodEvent { /// A [`AccessMethodEvent::New`] event is emitted when the active access /// method changes. @@ -234,7 +235,6 @@ pub struct AccessModeSelector<B: AccessMethodResolver> { cmd_rx: mpsc::UnboundedReceiver<Message>, method_resolver: B, access_method_settings: Settings, - #[cfg(not(target_os = "ios"))] access_method_event_sender: mpsc::UnboundedSender<(AccessMethodEvent, oneshot::Sender<()>)>, connection_mode_provider_sender: mpsc::UnboundedSender<ApiConnectionMode>, current: ResolvedConnectionMode, @@ -248,10 +248,7 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { #[cfg_attr(not(feature = "api-override"), allow(unused_mut))] mut access_method_settings: Settings, #[cfg(feature = "api-override")] api_endpoint: ApiEndpoint, - #[cfg(not(target_os = "ios"))] access_method_event_sender: mpsc::UnboundedSender<( - AccessMethodEvent, - oneshot::Sender<()>, - )>, + access_method_event_sender: mpsc::UnboundedSender<(AccessMethodEvent, oneshot::Sender<()>)>, ) -> Result<(AccessModeSelectorHandle, AccessModeConnectionModeProvider)> { let (cmd_tx, cmd_rx) = mpsc::unbounded(); @@ -277,7 +274,6 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { cmd_rx, method_resolver, access_method_settings, - #[cfg(not(target_os = "ios"))] access_method_event_sender, connection_mode_provider_sender: change_tx, current: initial_connection_mode, @@ -385,13 +381,7 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { async fn set_current(&mut self, access_method: AccessMethodSetting) { let resolved = Self::resolve_with_default(&access_method, &mut self.method_resolver).await; - #[cfg(not(target_os = "ios"))] - self.notify_daemon(&resolved); - - // Notify REST client - let _ = self - .connection_mode_provider_sender - .unbounded_send(resolved.connection_mode.clone()); + self.notify_connection_mode(resolved.clone()); self.current = resolved; @@ -401,8 +391,7 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { ); } - #[cfg(not(target_os = "ios"))] - fn notify_daemon(&mut self, resolved: &ResolvedConnectionMode) { + fn notify_connection_mode(&mut self, resolved: ResolvedConnectionMode) { // Note: If the daemon is busy waiting for a call to this function // to complete while we wait for the daemon to fully handle this // `NewAccessMethodEvent`, then we find ourselves in a deadlock. @@ -410,21 +399,21 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { // `MullvadRestHandle`, which will call and await `next` on a Stream // created from this `AccessModeSelector` instance. As such, the // completion channel is discarded in this instance. - let setting = resolved.setting.clone(); - #[cfg(not(target_os = "android"))] - let endpoint = resolved.endpoint.clone(); + let access_method_event = AccessMethodEvent::New { + setting: resolved.setting, + connection_mode: resolved.connection_mode.clone(), + #[cfg(not(target_os = "android"))] + endpoint: resolved.endpoint.clone(), + }; let sender = self.access_method_event_sender.clone(); - let connection_mode = resolved.connection_mode.clone(); tokio::spawn(async move { - let _ = AccessMethodEvent::New { - setting, - connection_mode, - #[cfg(not(target_os = "android"))] - endpoint, - } - .send(sender) - .await; + let _ = access_method_event.send(sender).await; }); + + // Notify REST client + let _ = self + .connection_mode_provider_sender + .unbounded_send(resolved.connection_mode); } /// Find the next access method to use. diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs index dfa25d0b44..ac64d88871 100644 --- a/mullvad-ios/src/api_client/mod.rs +++ b/mullvad-ios/src/api_client/mod.rs @@ -1,12 +1,13 @@ -use std::{ffi::c_char, future::Future, sync::Arc}; +use std::{ffi::c_char, ffi::c_void, ffi::CStr, future::Future, sync::Arc}; use crate::get_string; use access_method_resolver::SwiftAccessMethodResolver; use access_method_settings::SwiftAccessMethodSettingsWrapper; use address_cache_provider::SwiftAddressCacheWrapper; +use futures::{channel::{mpsc, oneshot}, StreamExt}; use mullvad_api::{ ApiEndpoint, Runtime, - access_mode::{AccessModeSelector, AccessModeSelectorHandle}, + access_mode::{AccessMethodEvent, AccessModeSelector, AccessModeSelectorHandle}, rest::{self, MullvadRestHandle}, }; use mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState; @@ -85,6 +86,14 @@ impl ApiContext { } } +/// An opaque pointer that exists only to be passed from the caller to a callback through the ABI +struct ForeignPtr { + ptr: *const c_void, +} +/// allow this to be passed across thread boundaries +unsafe impl Send for ForeignPtr {} +unsafe impl Sync for ForeignPtr {} + /// Called by Swift to set the available access methods #[unsafe(no_mangle)] pub unsafe extern "C" fn mullvad_api_update_access_methods( @@ -138,6 +147,8 @@ pub extern "C" fn mullvad_api_init_new_tls_disabled( bridge_provider: SwiftShadowsocksLoaderWrapper, settings_provider: SwiftAccessMethodSettingsWrapper, address_cache: SwiftAddressCacheWrapper, + access_method_change_callback: Option<unsafe extern "C" fn(*const c_void, * const u8)>, + access_method_change_context: *const c_void, ) -> SwiftApiContext { mullvad_api_init_inner( host, @@ -147,6 +158,8 @@ pub extern "C" fn mullvad_api_init_new_tls_disabled( bridge_provider, settings_provider, address_cache, + access_method_change_callback, + access_method_change_context, ) } @@ -170,6 +183,8 @@ pub extern "C" fn mullvad_api_init_new( bridge_provider: SwiftShadowsocksLoaderWrapper, settings_provider: SwiftAccessMethodSettingsWrapper, address_cache: SwiftAddressCacheWrapper, + access_method_change_callback: Option<unsafe extern "C" fn(*const c_void, * const u8)>, + access_method_change_context: *const c_void, ) -> SwiftApiContext { #[cfg(feature = "api-override")] return mullvad_api_init_inner( @@ -180,6 +195,8 @@ pub extern "C" fn mullvad_api_init_new( bridge_provider, settings_provider, address_cache, + access_method_change_callback, + access_method_change_context, ); #[cfg(not(feature = "api-override"))] mullvad_api_init_inner( @@ -189,6 +206,8 @@ pub extern "C" fn mullvad_api_init_new( bridge_provider, settings_provider, address_cache, + access_method_change_callback, + access_method_change_context, ) } @@ -213,6 +232,8 @@ pub extern "C" fn mullvad_api_init_inner( bridge_provider: SwiftShadowsocksLoaderWrapper, settings_provider: SwiftAccessMethodSettingsWrapper, address_cache: SwiftAddressCacheWrapper, + access_method_change_callback: Option<unsafe extern "C" fn(*const c_void, * const u8)>, + access_method_change_context: *const c_void, ) -> SwiftApiContext { // Safety: See notes for `get_string` let (host, address, domain) = @@ -245,16 +266,36 @@ pub extern "C" fn mullvad_api_init_inner( address_cache, ); + let access_method_change_ctx: ForeignPtr = ForeignPtr { ptr: access_method_change_context }; let api_context = tokio_handle.clone().block_on(async move { + let (tx, mut rx) = mpsc::unbounded::<(AccessMethodEvent, oneshot::Sender<()>)>(); let (access_mode_handler, access_mode_provider) = AccessModeSelector::spawn( method_resolver, access_method_settings, #[cfg(feature = "api-override")] endpoint.clone(), + tx, ) .await .expect("Could now spawn AccessModeSelector"); + tokio::spawn(async move { + let access_method_change_ctx = access_method_change_ctx; + // SAFETY: The callback is expected to be called from the Swift side + if let Some(callback) = access_method_change_callback { + while let Some((event, _sender)) = rx.next().await { + let AccessMethodEvent::New { setting, connection_mode, endpoint } = event else { continue }; + let uuid = setting.get_id(); + let uuid_bytes = uuid.as_bytes(); + // SAFETY: The callback is expected to be safe to call + unsafe { callback(access_method_change_ctx.ptr, uuid_bytes.as_ptr()) }; + } + } + }); + + // TODO: do something with rx, and somehow let the `AccessMethodEvent`s it + // receives be sent back to the Swift side + // It is imperative that the REST runtime is created within an async context, otherwise // ApiAvailability panics. let api_client = mullvad_api::Runtime::new(tokio_handle, &endpoint); diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs index 4f1229d126..f823e5d83d 100644 --- a/mullvad-types/src/access_method.rs +++ b/mullvad-types/src/access_method.rs @@ -195,6 +195,10 @@ impl Id { use std::str::FromStr; uuid::Uuid::from_str(&id).ok().map(Self) } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } } impl std::fmt::Display for Id { |
