diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-01-16 15:52:16 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2025-04-24 10:41:25 +0200 |
| commit | 8cb9795c392a41c6adae249007e0155115697e04 (patch) | |
| tree | 0c0c0e5cf24b019be316ed07112b4bb78a3ef3f2 | |
| parent | e59bd99fe2d9671eaacfa8dd4c9ecd2b1c6aa682 (diff) | |
| download | mullvadvpn-8cb9795c392a41c6adae249007e0155115697e04.tar.xz mullvadvpn-8cb9795c392a41c6adae249007e0155115697e04.zip | |
Expose TransportSelector to mullvad-ios
58 files changed, 1408 insertions, 224 deletions
diff --git a/Cargo.lock b/Cargo.lock index e9aba835aa..24d4ca3ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2799,7 +2799,9 @@ dependencies = [ name = "mullvad-ios" version = "0.0.0" dependencies = [ + "async-trait", "cbindgen 0.28.0", + "futures", "hyper", "hyper-util", "libc", @@ -2807,6 +2809,7 @@ dependencies = [ "mockito", "mullvad-api", "mullvad-encrypted-dns-proxy", + "mullvad-types", "oslog", "serde_json", "shadowsocks-service", diff --git a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift index 73fd82f359..6e7dbb93ef 100644 --- a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift +++ b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift @@ -8,6 +8,7 @@ import Combine import MullvadSettings +import MullvadTypes public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource, @unchecked Sendable { public var directAccess: PersistentAccessMethod @@ -36,4 +37,27 @@ public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource, @unc public func infoHeaderConfig(for id: UUID) -> InfoHeaderConfig? { nil } + + public static var stub: AccessMethodRepositoryStub { + AccessMethodRepositoryStub(accessMethods: [ + PersistentAccessMethod( + id: UUID(), + name: "direct", + isEnabled: true, + proxyConfiguration: .direct + ), + PersistentAccessMethod( + id: UUID(), + name: "bridges", + isEnabled: true, + proxyConfiguration: .bridges + ), + PersistentAccessMethod( + id: UUID(), + name: "Encrypted DNS", + isEnabled: true, + proxyConfiguration: .encryptedDNS + ), + ]) + } } diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift index d115abc37a..b2a3972fb7 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift @@ -29,13 +29,6 @@ extension REST { /// Default network timeout for API requests. public static let defaultAPINetworkTimeout: Duration = .seconds(10) - - /// API context used for API requests via Rust runtime. - // swiftlint:disable:next force_try - public static let apiContext = try! MullvadApiContext( - host: defaultAPIHostname, - address: defaultAPIEndpoint - ) } // swiftlint:enable force_cast diff --git a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift index 7515b92c5c..ff3751c5bd 100644 --- a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift +++ b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift @@ -58,7 +58,16 @@ extension REST { } public func createAPIProxy() -> APIQuerying { + #if DEBUG + MullvadAPIProxy( + transportProvider: configuration.apiTransportProvider, + dispatchQueue: DispatchQueue(label: "MullvadAPIProxy.dispatchQueue"), + responseDecoder: Coding.makeJSONDecoder() + ) + + #else REST.APIProxy(configuration: configuration) + #endif } public func createAccountsProxy() -> RESTAccountHandling { diff --git a/ios/MullvadREST/Transport/AccessMethodIterator.swift b/ios/MullvadREST/Transport/AccessMethodIterator.swift index 0b55c7683a..91de54bbd7 100644 --- a/ios/MullvadREST/Transport/AccessMethodIterator.swift +++ b/ios/MullvadREST/Transport/AccessMethodIterator.swift @@ -9,8 +9,9 @@ import Combine import Foundation import MullvadSettings +import MullvadTypes -final class AccessMethodIterator: @unchecked Sendable { +final class AccessMethodIterator: @unchecked Sendable, SwiftConnectionModeProviding { private let dataSource: AccessMethodRepositoryDataSource private var index = 0 @@ -24,6 +25,10 @@ final class AccessMethodIterator: @unchecked Sendable { dataSource.fetchLastReachable().id } + public var domainName: String { + REST.encryptedDNSHostname + } + init(dataSource: AccessMethodRepositoryDataSource) { self.dataSource = dataSource @@ -63,4 +68,8 @@ final class AccessMethodIterator: @unchecked Sendable { return configurations[circularIndex] } } + + func accessMethods() -> [PersistentAccessMethod] { + dataSource.fetchAll() + } } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index 1fac755454..1232383f26 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -10,12 +10,7 @@ import Foundation import MullvadSettings import MullvadTypes -public protocol ShadowsocksLoaderProtocol: Sendable { - func load() throws -> ShadowsocksConfiguration - func clear() throws -} - -public final class ShadowsocksLoader: ShadowsocksLoaderProtocol, Sendable { +public final class ShadowsocksLoader: ShadowsocksLoaderProtocol, SwiftShadowsocksBridgeProviding, Sendable { let cache: ShadowsocksConfigurationCacheProtocol let relaySelector: ShadowsocksRelaySelectorProtocol let settingsUpdater: SettingsUpdater @@ -88,4 +83,8 @@ public final class ShadowsocksLoader: ShadowsocksLoaderProtocol, Sendable { cipher: bridgeConfiguration.cipher ) } + + public func bridge() -> ShadowsocksConfiguration? { + try? load() + } } diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift index 8daf686f73..53c22b36c9 100644 --- a/ios/MullvadREST/Transport/TransportStrategy.swift +++ b/ios/MullvadREST/Transport/TransportStrategy.swift @@ -8,6 +8,7 @@ import Foundation import Logging +@preconcurrency import MullvadRustRuntime import MullvadSettings import MullvadTypes @@ -49,12 +50,25 @@ public struct TransportStrategy: Equatable, Sendable { private let accessMethodIterator: AccessMethodIterator + private let connectionModeProviderProxy: SwiftConnectionModeProviderProxy + + public let opaqueAccessMethodSettingsWrapper: SwiftAccessMethodSettingsWrapper + public init( datasource: AccessMethodRepositoryDataSource, shadowsocksLoader: ShadowsocksLoaderProtocol ) { self.shadowsocksLoader = shadowsocksLoader self.accessMethodIterator = AccessMethodIterator(dataSource: datasource) + self.connectionModeProviderProxy = SwiftConnectionModeProviderProxy( + provider: accessMethodIterator, + domainName: REST.encryptedDNSHostname + ) + self + .opaqueAccessMethodSettingsWrapper = initAccessMethodSettingsWrapper( + methods: connectionModeProviderProxy + .accessMethods() + ) } /// Rotating between enabled configurations by what order they were added in diff --git a/ios/MullvadRESTTests/MullvadApiTests.swift b/ios/MullvadRESTTests/MullvadApiTests.swift index ab218623d2..0290f11e66 100644 --- a/ios/MullvadRESTTests/MullvadApiTests.swift +++ b/ios/MullvadRESTTests/MullvadApiTests.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // +import MullvadMockData @testable import MullvadREST import MullvadRustRuntime import MullvadTypes @@ -19,9 +20,27 @@ class MullvadApiTests: XCTestCase { let encoder = JSONEncoder() func makeApiProxy(port: UInt16) throws -> APIQuerying { - let context = try MullvadApiContext(host: "localhost", address: .ipv4( - .init(ip: IPv4Address.loopback, port: port) - ), disable_tls: true) + let shadowsocksLoader = ShadowsocksLoaderStub(configuration: ShadowsocksConfiguration( + address: .ipv4(.loopback), + port: 1080, + password: "123", + cipher: CipherIdentifiers.CHACHA20.description + )) + + let accessMethodsRepository = AccessMethodRepositoryStub.stub + + let context = try MullvadApiContext( + host: "localhost", + address: "\(IPv4Address.loopback.debugDescription):\(port)", + domain: REST.encryptedDNSHostname, + disableTls: true, + shadowsocksProvider: shadowsocksLoader, + accessMethodWrapper: initAccessMethodSettingsWrapper( + methods: accessMethodsRepository + .fetchAll() + ) + ) + let proxy = REST.MullvadAPIProxy( transportProvider: APITransportProvider( requestFactory: .init(apiContext: context) diff --git a/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift index c28f532ce0..969ae865a5 100644 --- a/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift +++ b/ios/MullvadRESTTests/ShadowsocksLoaderStub.swift @@ -11,7 +11,11 @@ import Foundation import MullvadSettings import MullvadTypes -struct ShadowsocksLoaderStub: ShadowsocksLoaderProtocol { +struct ShadowsocksLoaderStub: ShadowsocksLoaderProtocol, SwiftShadowsocksBridgeProviding { + func bridge() -> ShadowsocksConfiguration? { + try? load() + } + var configuration: ShadowsocksConfiguration var error: Error? diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift index a838b74499..6fdcef8db2 100644 --- a/ios/MullvadRESTTests/TransportStrategyTests.swift +++ b/ios/MullvadRESTTests/TransportStrategyTests.swift @@ -177,10 +177,12 @@ class TransportStrategyTests: XCTestCase { func testNoLoopOnFailureAtLoadingConfigurationWhenBridgeIsOnlyEnabled() { shadowsocksLoader.error = IOError.fileNotFound directAccess.isEnabled = false + encryptedDNS.isEnabled = false let transportStrategy = TransportStrategy( datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, + encryptedDNS, ]), shadowsocksLoader: shadowsocksLoader ) @@ -190,7 +192,7 @@ class TransportStrategyTests: XCTestCase { } } - func testUsesSocks5WithAuthenticationWhenItReaches() throws { + func testUsesSocks5WithAuthentication() throws { let username = "user" let password = "pass" let authentication = PersistentProxyConfiguration.SocksAuthentication @@ -203,10 +205,12 @@ class TransportStrategyTests: XCTestCase { port: 1080, authentication: authentication ) + encryptedDNS.isEnabled = false let transportStrategy = TransportStrategy( datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, + encryptedDNS, PersistentAccessMethod( id: UUID(), name: "", diff --git a/ios/MullvadRustRuntime/MullvadAccessMethodReceiver.swift b/ios/MullvadRustRuntime/MullvadAccessMethodReceiver.swift new file mode 100644 index 0000000000..c683a10af6 --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadAccessMethodReceiver.swift @@ -0,0 +1,42 @@ +// +// MullvadAccessMethodReceiver.swift +// MullvadRustRuntime +// +// Created by Marco Nikic on 2025-03-31. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Combine +import Foundation +import MullvadTypes + +public class MullvadAccessMethodReceiver { + private var cancellables = Set<Combine.AnyCancellable>() + let apiContext: MullvadApiContext + + public init( + apiContext: MullvadApiContext, + accessMethodsDataSource: AnyPublisher<[PersistentAccessMethod], Never>, + lastReachableDataSource: AnyPublisher<PersistentAccessMethod, Never> + ) { + self.apiContext = apiContext + + lastReachableDataSource.sink { [weak self] in + self?.saveLastReachable($0) + } + .store(in: &cancellables) + + accessMethodsDataSource.sink { [weak self] in + self?.updateAccessMethods($0) + }.store(in: &cancellables) + } + + private func saveLastReachable(_ lastReachable: PersistentAccessMethod) { + mullvad_api_use_access_method(apiContext.context, lastReachable.id.uuidString) + } + + private func updateAccessMethods(_ accessMethods: [PersistentAccessMethod]) { + let settingsWrapper = initAccessMethodSettingsWrapper(methods: accessMethods) + mullvad_api_update_access_methods(apiContext.context, settingsWrapper) + } +} diff --git a/ios/MullvadRustRuntime/MullvadApiContext.swift b/ios/MullvadRustRuntime/MullvadApiContext.swift index 2cdcb7b728..5517348cc1 100644 --- a/ios/MullvadRustRuntime/MullvadApiContext.swift +++ b/ios/MullvadRustRuntime/MullvadApiContext.swift @@ -8,19 +8,44 @@ import MullvadTypes -public struct MullvadApiContext: Sendable { +public struct MullvadApiContext: @unchecked Sendable { enum MullvadApiContextError: Error { case failedToConstructApiClient } public let context: SwiftApiContext + private let shadowsocksBridgeProvider: SwiftShadowsocksBridgeProviding! + private let shadowsocksBridgeProviderWrapper: SwiftShadowsocksLoaderWrapper! - public init(host: String, address: AnyIPEndpoint, disable_tls: Bool = false) throws { - context = switch disable_tls { + public init( + host: String, + address: String, + domain: String, + disableTls: Bool = false, + shadowsocksProvider: SwiftShadowsocksBridgeProviding, + accessMethodWrapper: SwiftAccessMethodSettingsWrapper + ) throws { + let bridgeProvider = SwiftShadowsocksBridgeProvider(provider: shadowsocksProvider) + self.shadowsocksBridgeProvider = bridgeProvider + self.shadowsocksBridgeProviderWrapper = initMullvadShadowsocksBridgeProvider(provider: bridgeProvider) + + context = switch disableTls { case true: - mullvad_api_init_new_tls_disabled(host, address.description) + mullvad_api_init_new_tls_disabled( + host, + address, + domain, + shadowsocksBridgeProviderWrapper, + accessMethodWrapper + ) case false: - mullvad_api_init_new(host, address.description) + mullvad_api_init_new( + host, + address, + domain, + shadowsocksBridgeProviderWrapper, + accessMethodWrapper + ) } if context._0 == nil { diff --git a/ios/MullvadRustRuntime/MullvadConnectionModeProvider.swift b/ios/MullvadRustRuntime/MullvadConnectionModeProvider.swift new file mode 100644 index 0000000000..f7d2e0238f --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadConnectionModeProvider.swift @@ -0,0 +1,99 @@ +// +// MullvadConnectionModeProvider.swift +// MullvadRustRuntime +// +// Created by Marco Nikic on 2025-02-20. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes + +// swiftlint:disable:next function_body_length +public func initAccessMethodSettingsWrapper(methods: [PersistentAccessMethod]) + -> SwiftAccessMethodSettingsWrapper { + // 1. Get all the built in access methods, it is expected that they are always available + let directMethod = methods.first(where: { $0.proxyConfiguration == .direct })! + let bridgesMethod = methods.first(where: { $0.proxyConfiguration == .bridges })! + let encryptedDNSMethod = methods.first(where: { $0.proxyConfiguration == .encryptedDNS })! + + // 2. Get the custom access methods + let defaultMethods: [PersistentProxyConfiguration] = [.direct, .bridges, .encryptedDNS] + let customMethods = methods.filter { defaultMethods.contains($0.proxyConfiguration) == false } + + // 3. Convert the builtin access methods + let directMethodRaw = convert_builtin_access_method_setting( + directMethod.id.uuidString, + directMethod.name, + directMethod.isEnabled, + UInt8(KindDirect.rawValue), + nil + ) + let bridgesMethodRaw = convert_builtin_access_method_setting( + bridgesMethod.id.uuidString, + bridgesMethod.name, + bridgesMethod.isEnabled, + UInt8(KindBridge.rawValue), + nil + ) + let encryptedDNSMethodRaw = convert_builtin_access_method_setting( + encryptedDNSMethod.id.uuidString, + encryptedDNSMethod.name, + encryptedDNSMethod.isEnabled, + UInt8(KindEncryptedDnsProxy.rawValue), + nil + ) + + var rawCustomMethods = ContiguousArray<UnsafeRawPointer?>([]) + // 4. Convert the custom access methods (all takes different parameters) + for method in customMethods { + if case let .shadowsocks(config) = method.proxyConfiguration { + let serverAddress = config.server.rawValue.map { $0 } + let shadowsocksConfiguration = new_shadowsocks_access_method_setting( + serverAddress, + UInt(serverAddress.count), + config.port, + config.password, + config.cipher.rawValue.rawValue + ) + let shadowsocksMethodRaw = convert_builtin_access_method_setting( + method.id.uuidString, + method.name, + method.isEnabled, + UInt8(KindShadowsocks.rawValue), + shadowsocksConfiguration + ) + rawCustomMethods.append(shadowsocksMethodRaw) + } + if case let .socks5(config) = method.proxyConfiguration { + let serverAddress = config.server.rawValue.map { $0 } + let socks5Configuration = new_socks5_access_method_setting( + serverAddress, + UInt(serverAddress.count), + config.port, + config.credential?.username, + config.credential?.password + ) + let socks5MethodRaw = convert_builtin_access_method_setting( + method.id.uuidString, + method.name, + method.isEnabled, + UInt8(KindSocks5Local.rawValue), + socks5Configuration + ) + rawCustomMethods.append(socks5MethodRaw) + } + } + + // 5. Reunite them all in one, and pass it to rust + return rawCustomMethods.withUnsafeMutableBufferPointer( + { + init_access_method_settings_wrapper( + directMethodRaw, + bridgesMethodRaw, + encryptedDNSMethodRaw, + $0.baseAddress!, + UInt(customMethods.count) + ) + } + ) +} diff --git a/ios/MullvadRustRuntime/MullvadShadowsocksBridgeProvider.swift b/ios/MullvadRustRuntime/MullvadShadowsocksBridgeProvider.swift new file mode 100644 index 0000000000..16f7f7ca34 --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadShadowsocksBridgeProvider.swift @@ -0,0 +1,29 @@ +// +// MullvadShadowsocksBridgeProvider.swift +// MullvadRustRuntime +// +// Created by Marco Nikic on 2025-03-24. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes + +public func initMullvadShadowsocksBridgeProvider(provider: SwiftShadowsocksBridgeProvider) + -> SwiftShadowsocksLoaderWrapper { + let rawProvider = Unmanaged.passUnretained(provider).toOpaque() + return init_swift_shadowsocks_loader_wrapper(rawProvider) +} + +@_cdecl("swift_get_shadowsocks_bridges") +func getShadowsocksBridges(rawBridgeProvider: UnsafeMutableRawPointer) -> UnsafeRawPointer? { + let bridgeProvider = Unmanaged<SwiftShadowsocksBridgeProvider>.fromOpaque(rawBridgeProvider).takeUnretainedValue() + guard let bridge = bridgeProvider.bridge() else { return nil } + let bridgeAddress = bridge.address.rawValue.map { $0 } + return new_shadowsocks_access_method_setting( + bridgeAddress, + UInt(bridgeAddress.count), + bridge.port, + bridge.password, + bridge.cipher + ) +} diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 4de4a02b37..6300b04902 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -6,6 +6,18 @@ #include <stdlib.h> /** + * Used by Swift to instruct which access method kind it is trying to convert + */ +enum SwiftAccessMethodKind { + KindDirect = 0, + KindBridge, + KindEncryptedDnsProxy, + KindShadowsocks, + KindSocks5Local, +}; +typedef uint8_t SwiftAccessMethodKind; + +/** * SAFETY: `TunnelObfuscatorProtocol` values must either be `0` or `1` */ enum TunnelObfuscatorProtocol { @@ -30,10 +42,24 @@ typedef struct RequestCancelHandle RequestCancelHandle; typedef struct RetryStrategy RetryStrategy; +typedef struct SwiftAccessMethodSettingsContext SwiftAccessMethodSettingsContext; + typedef struct SwiftApiContext { const struct ApiContext *_0; } SwiftApiContext; +typedef struct SwiftAccessMethodSettingsWrapper { + struct SwiftAccessMethodSettingsContext *_0; +} SwiftAccessMethodSettingsWrapper; + +typedef struct SwiftShadowsocksLoaderWrapperContext { + const void *shadowsocks_loader; +} SwiftShadowsocksLoaderWrapperContext; + +typedef struct SwiftShadowsocksLoaderWrapper { + struct SwiftShadowsocksLoaderWrapperContext _0; +} SwiftShadowsocksLoaderWrapper; + typedef struct SwiftCancelHandle { struct RequestCancelHandle *ptr; } SwiftCancelHandle; @@ -101,6 +127,22 @@ typedef struct EphemeralPeerParameters { extern const uint16_t CONFIG_SERVICE_PORT; /** + * Called by Swift to set the available access methods + */ +void mullvad_api_update_access_methods(struct SwiftApiContext api_context, + struct SwiftAccessMethodSettingsWrapper settings_wrapper); + +/** + * Called by Swift to update the currently used access methods + * + * # SAFETY + * `access_method_id` must point to a null terminated string in a UUID format + * + */ +void mullvad_api_use_access_method(struct SwiftApiContext api_context, + const char *access_method_id); + +/** * # Safety * * `host` must be a pointer to a null terminated string representing a hostname for Mullvad API host. @@ -114,8 +156,11 @@ extern const uint16_t CONFIG_SERVICE_PORT; * * This function is safe. */ -struct SwiftApiContext mullvad_api_init_new_tls_disabled(const uint8_t *host, - const uint8_t *address); +struct SwiftApiContext mullvad_api_init_new_tls_disabled(const char *host, + const char *address, + const char *domain, + struct SwiftShadowsocksLoaderWrapper bridge_provider, + struct SwiftAccessMethodSettingsWrapper settings_provider); /** * # Safety @@ -131,8 +176,63 @@ struct SwiftApiContext mullvad_api_init_new_tls_disabled(const uint8_t *host, * * This function is safe. */ -struct SwiftApiContext mullvad_api_init_new(const uint8_t *host, - const uint8_t *address); +struct SwiftApiContext mullvad_api_init_new(const char *host, + const char *address, + const char *domain, + struct SwiftShadowsocksLoaderWrapper bridge_provider, + struct SwiftAccessMethodSettingsWrapper settings_provider); + +/** + * # Safety + * + * `host` must be a pointer to a null terminated string representing a hostname for Mullvad API host. + * This hostname will be used for TLS validation but not used for domain name resolution. + * + * `address` must be a pointer to a null terminated string representing a socket address through which + * the Mullvad API can be reached directly. + * + * If a context cannot be constructed this function will panic since the call site would not be able + * to proceed in a meaningful way anyway. + * + * This function is safe. + */ +struct SwiftApiContext mullvad_api_init_inner(const char *host, + const char *address, + const char *domain, + bool disable_tls, + struct SwiftShadowsocksLoaderWrapper bridge_provider, + struct SwiftAccessMethodSettingsWrapper settings_provider); + +/** + * Converts parameters into a `Box<AccessMethodSetting>` raw representation that + * can be passed across the FFI boundary + * + * # SAFETY: + * `unique_identifier` and `name` must point to valid memory regions and contain NULL terminators. + * They are only valid for the duration of this call. + * + * `proxy_configuration` can be NULL, or must be a pointer gotten through + * either the `convert_shadowsocks` or `convert_socks5` methods. + */ +void *convert_builtin_access_method_setting(const char *unique_identifier, + const char *name, + bool is_enabled, + SwiftAccessMethodKind method_kind, + const void *proxy_configuration); + +/** + * Creates a wrapper around a `Settings` object that can be safely sent across the FFI boundary. + * + * # SAFETY + * `direct_method_raw`, `bridges_method_raw` and `encrypted_dns_method_raw` must be raw pointers + * resulting from a call to `convert_builtin_access_method_setting` + * `custom_methods_raw` is an array of pointers to instances of `AccessMethodSetting` + */ +struct SwiftAccessMethodSettingsWrapper init_access_method_settings_wrapper(const void *direct_method_raw, + const void *bridges_method_raw, + const void *encrypted_dns_method_raw, + const void *custom_methods_raw, + uintptr_t custom_method_count); /** * # Safety @@ -261,6 +361,35 @@ extern void mullvad_api_completion_finish(struct SwiftMullvadApiResponse respons struct CompletionCookie completion_cookie); /** + * Converts parameters into a boxed `Shadowsocks` configuration that is safe + * to send across the FFI boundary + * + * # SAFETY + * `address` must be a pointer to at least `address_len` bytes. + * `c_password` and `c_cipher` must be pointers to null terminated strings + */ +const void *new_shadowsocks_access_method_setting(const uint8_t *address, + uintptr_t address_len, + uint16_t port, + const char *c_password, + const char *c_cipher); + +/** + * Converts parameters into a boxed `Socks5Remote` configuration that is safe + * + * to send across the FFI boundary + * + * # SAFETY + * `address` must be a pointer to at least `address_len` bytes. + * `c_username` and `c_password` must be pointers to null terminated strings, or null + */ +const void *new_socks5_access_method_setting(const uint8_t *address, + uintptr_t address_len, + uint16_t port, + const char *c_username, + const char *c_password); + +/** * # Safety * * `method` must be a pointer to a null terminated string representing the http method. @@ -376,6 +505,26 @@ struct SwiftRetryStrategy mullvad_api_retry_strategy_exponential(uintptr_t max_r uint64_t max_delay_sec); /** + * Creates a `Shadowsocks` configuration. + * + * # SAFETY + * `rawBridgeProvider` **must** be provided by a call to `init_swift_shadowsocks_loader_wrapper` + * It is okay to persist it, and use it across multiple threads. + */ +extern const void *swift_get_shadowsocks_bridges(const void *rawBridgeProvider); + +/** + * Called by the Swift side in order to provide an object to rust that can create + * Shadowsocks configurations + * + * # SAFETY + * `shadowsocks_loader` **must be** pointing to a valid instance of a `SwiftShadowsocksBridgeProvider` + * That instance's lifetime has to be equivalent to a `'static` lifetime in Rust + * This function does not take ownership of `shadowsocks_loader` + */ +struct SwiftShadowsocksLoaderWrapper init_swift_shadowsocks_loader_wrapper(const void *shadowsocks_loader); + +/** * Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. * * # Safety diff --git a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift index ab8ea4fd47..35f97442f5 100644 --- a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift +++ b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift @@ -7,6 +7,7 @@ // import Combine +import MullvadTypes public protocol AccessMethodRepositoryDataSource: Sendable { /// Publisher that propagates a snapshot of all access methods upon modifications. diff --git a/ios/MullvadSettings/AccessMethodKind.swift b/ios/MullvadTypes/AccessMethodKind.swift index f01dd8b845..f01dd8b845 100644 --- a/ios/MullvadSettings/AccessMethodKind.swift +++ b/ios/MullvadTypes/AccessMethodKind.swift diff --git a/ios/MullvadSettings/PersistentAccessMethod.swift b/ios/MullvadTypes/PersistentAccessMethod.swift index bc00cbf2bb..8b9d6d57ae 100644 --- a/ios/MullvadSettings/PersistentAccessMethod.swift +++ b/ios/MullvadTypes/PersistentAccessMethod.swift @@ -7,7 +7,6 @@ // import Foundation -import MullvadTypes import Network /// Persistent access method container model. @@ -17,6 +16,11 @@ public struct PersistentAccessMethodStore: Codable { /// Persistent access method models. public var accessMethods: [PersistentAccessMethod] + + public init(lastReachableAccessMethod: PersistentAccessMethod, accessMethods: [PersistentAccessMethod]) { + self.lastReachableAccessMethod = lastReachableAccessMethod + self.accessMethods = accessMethods + } } /// Persistent access method model. @@ -59,7 +63,7 @@ public struct PersistentAccessMethod: Identifiable, Codable, Equatable { } /// Persistent proxy configuration. -public enum PersistentProxyConfiguration: Codable { +public enum PersistentProxyConfiguration: Codable, Equatable { /// Direct communication without proxy. case direct @@ -78,12 +82,12 @@ public enum PersistentProxyConfiguration: Codable { extension PersistentProxyConfiguration { /// Socks autentication method. - public enum SocksAuthentication: Codable { + public enum SocksAuthentication: Codable, Equatable { case noAuthentication case authentication(UserCredential) } - public struct UserCredential: Codable { + public struct UserCredential: Codable, Equatable { public let username: String public let password: String @@ -94,7 +98,7 @@ extension PersistentProxyConfiguration { } /// Socks v5 proxy configuration. - public struct SocksConfiguration: Codable { + public struct SocksConfiguration: Codable, Equatable { /// Proxy server address. public var server: AnyIPAddress @@ -128,7 +132,7 @@ extension PersistentProxyConfiguration { } /// Shadowsocks configuration. - public struct ShadowsocksConfiguration: Codable { + public struct ShadowsocksConfiguration: Codable, Equatable { /// Server address. public var server: AnyIPAddress diff --git a/ios/MullvadTypes/ShadowsocksBridgeProviding.swift b/ios/MullvadTypes/ShadowsocksBridgeProviding.swift new file mode 100644 index 0000000000..3d0610bf07 --- /dev/null +++ b/ios/MullvadTypes/ShadowsocksBridgeProviding.swift @@ -0,0 +1,25 @@ +// +// ShadowsocksBridgeProviding.swift +// MullvadTypes +// +// Created by Marco Nikic on 2025-03-24. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol SwiftShadowsocksBridgeProviding: Sendable { + func bridge() -> ShadowsocksConfiguration? +} + +public final class SwiftShadowsocksBridgeProvider: SwiftShadowsocksBridgeProviding, Sendable { + let provider: SwiftShadowsocksBridgeProviding + + public init(provider: SwiftShadowsocksBridgeProviding) { + self.provider = provider + } + + public func bridge() -> ShadowsocksConfiguration? { + provider.bridge() + } +} diff --git a/ios/MullvadSettings/ShadowsocksCipherOptions.swift b/ios/MullvadTypes/ShadowsocksCipherOptions.swift index 19ecfadd38..19ecfadd38 100644 --- a/ios/MullvadSettings/ShadowsocksCipherOptions.swift +++ b/ios/MullvadTypes/ShadowsocksCipherOptions.swift diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift b/ios/MullvadTypes/ShadowsocksConfiguration.swift index b8cf90b7cc..f54276348d 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift +++ b/ios/MullvadTypes/ShadowsocksConfiguration.swift @@ -7,9 +7,13 @@ // import Foundation -import MullvadTypes import Network +public protocol ShadowsocksLoaderProtocol: Sendable { + func load() throws -> ShadowsocksConfiguration + func clear() throws +} + public struct ShadowsocksConfiguration: Codable, Equatable, Sendable { public let address: AnyIPAddress public let port: UInt16 diff --git a/ios/MullvadTypes/SwiftConnectionModeProvider.swift b/ios/MullvadTypes/SwiftConnectionModeProvider.swift new file mode 100644 index 0000000000..95fc8f4b83 --- /dev/null +++ b/ios/MullvadTypes/SwiftConnectionModeProvider.swift @@ -0,0 +1,29 @@ +// +// SwiftConnectionModeProvider.swift +// MullvadTypes +// +// Created by Marco Nikic on 2025-02-19. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol SwiftConnectionModeProviding: Sendable { + var domainName: String { get } + + func accessMethods() -> [PersistentAccessMethod] +} + +public final class SwiftConnectionModeProviderProxy: SwiftConnectionModeProviding, Sendable { + let provider: SwiftConnectionModeProviding + public let domainName: String + + public init(provider: SwiftConnectionModeProviding, domainName: String) { + self.provider = provider + self.domainName = domainName + } + + public func accessMethods() -> [PersistentAccessMethod] { + provider.accessMethods() + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3e2407ccac..26406d4230 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -779,9 +779,15 @@ A935594C2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */; }; A939661B2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A939661A2CAE6CE1008128CA /* MigrationManagerMultiProcessUpgradeTests.swift */; }; A93969812CE606190032A7A0 /* Maybenot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9840BB32C69F78A0030F05E /* Maybenot.swift */; }; + A959E23E2D75F33300F95DDB /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; }; + A959E2412D75F39A00F95DDB /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; + A959E2422D75F3D200F95DDB /* AccessMethodKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */; }; + A959E2432D75F42500F95DDB /* ShadowsocksCipherOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */; }; A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */; }; A95EEE382B722DFC00A8A39B /* PingStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95EEE372B722DFC00A8A39B /* PingStats.swift */; }; + A96D0B452D675F0400DD6C59 /* MullvadConnectionModeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96D0B442D675F0400DD6C59 /* MullvadConnectionModeProvider.swift */; }; A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */; }; + A9711B2B2D662AE3003DA71D /* SwiftConnectionModeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9711B2A2D662AE3003DA71D /* SwiftConnectionModeProvider.swift */; }; A97275562CE36CAE00029F15 /* DaitaV2Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97275552CE36CAE00029F15 /* DaitaV2Parameters.swift */; }; A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */; }; A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */; }; @@ -789,6 +795,10 @@ A97D25B42B0CB59300946B2D /* TunnelObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25B32B0CB59300946B2D /* TunnelObfuscationStub.swift */; }; A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */; }; A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; }; + A98207EB2D9190F100654558 /* ShadowsocksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */; }; + A98207EF2D9192A300654558 /* ShadowsocksBridgeProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98207EE2D9192A300654558 /* ShadowsocksBridgeProviding.swift */; }; + A98207F12D91A0AC00654558 /* MullvadShadowsocksBridgeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98207F02D91A0AC00654558 /* MullvadShadowsocksBridgeProvider.swift */; }; + A98207F32D9ACE4C00654558 /* MullvadAccessMethodReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98207F22D9ACE4C00654558 /* MullvadAccessMethodReceiver.swift */; }; A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98502022B627B120061901E /* LocalNetworkProbe.swift */; }; A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; }; @@ -991,8 +1001,6 @@ F07C9D952B220C77006F1C5E /* libmullvad_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libmullvad_ios.a */; }; F07CFF2029F2720E008C0343 /* NewDeviceNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */; }; F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */; }; - F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */; }; - F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; }; F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */; }; F08B6B772C52878400D0A121 /* EphemeralPeerExchangeActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98F1B502C19C48D003C869E /* EphemeralPeerExchangeActorTests.swift */; }; F08B6B782C528B8A00D0A121 /* EphemeralPeerExchangingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F059197E2C454CE000C301F3 /* EphemeralPeerExchangingProtocol.swift */; }; @@ -1058,7 +1066,6 @@ F0D5591E2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; }; F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; }; F0D7FF8F2B31DF5900E0FDE5 /* AccessMethodRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */; }; - F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */; }; F0D8825B2B04F53600D3EF9A /* OutgoingConnectionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */; }; F0D8825C2B04F70E00D3EF9A /* OutgoingConnectionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */; }; F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */; }; @@ -1067,7 +1074,6 @@ F0DAC8AD2C16EFE400F80144 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */; }; F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */; }; F0DDE4162B220458006B57A7 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4112B220458006B57A7 /* TransportProvider.swift */; }; - F0DDE4182B220458006B57A7 /* ShadowsocksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */; }; F0DDE42A2B220A15006B57A7 /* Haversine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4272B220A15006B57A7 /* Haversine.swift */; }; F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4282B220A15006B57A7 /* RelaySelector.swift */; }; F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4292B220A15006B57A7 /* Midpoint.swift */; }; @@ -1375,6 +1381,13 @@ remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; remoteInfo = MullvadTypes; }; + A959E23F2D75F37600F95DDB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; + remoteInfo = MullvadTypes; + }; A9609B6D2D004D1F0065A3D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -2319,7 +2332,9 @@ A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActor.swift; sourceTree = "<group>"; }; A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorState.swift; sourceTree = "<group>"; }; A95EEE372B722DFC00A8A39B /* PingStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingStats.swift; sourceTree = "<group>"; }; + A96D0B442D675F0400DD6C59 /* MullvadConnectionModeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadConnectionModeProvider.swift; sourceTree = "<group>"; }; A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5UsernamePasswordCommand.swift; sourceTree = "<group>"; }; + A9711B2A2D662AE3003DA71D /* SwiftConnectionModeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftConnectionModeProvider.swift; sourceTree = "<group>"; }; A97275552CE36CAE00029F15 /* DaitaV2Parameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaitaV2Parameters.swift; sourceTree = "<group>"; }; A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscator.swift; sourceTree = "<group>"; }; A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscationStub.swift; sourceTree = "<group>"; }; @@ -2327,6 +2342,9 @@ A97D25B32B0CB59300946B2D /* TunnelObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationStub.swift; sourceTree = "<group>"; }; A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWgKeyData.swift; sourceTree = "<group>"; }; A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; }; + A98207EE2D9192A300654558 /* ShadowsocksBridgeProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksBridgeProviding.swift; sourceTree = "<group>"; }; + A98207F02D91A0AC00654558 /* MullvadShadowsocksBridgeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadShadowsocksBridgeProvider.swift; sourceTree = "<group>"; }; + A98207F22D9ACE4C00654558 /* MullvadAccessMethodReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAccessMethodReceiver.swift; sourceTree = "<group>"; }; A9840BB32C69F78A0030F05E /* Maybenot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Maybenot.swift; sourceTree = "<group>"; }; A98502022B627B120061901E /* LocalNetworkProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkProbe.swift; sourceTree = "<group>"; }; A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = "<group>"; }; @@ -2543,6 +2561,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A959E2412D75F39A00F95DDB /* MullvadTypes.framework in Frameworks */, 44A2625F2D63745000085380 /* WireGuardKitTypes in Frameworks */, 58FE25BB2AA72188003D1918 /* MullvadLogging.framework in Frameworks */, ); @@ -2989,6 +3008,8 @@ 581943F228F8014500B0CB5E /* MullvadTypes */ = { isa = PBXGroup; children = ( + F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */, + 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */, 584D26BE270C550B004EA533 /* AnyIPAddress.swift */, 586A951329013235007BAF2B /* AnyIPEndpoint.swift */, 06AC113628F83FD70037AF9A /* Cancellable.swift */, @@ -3011,6 +3032,7 @@ 7AB401842DA53D4E00522E17 /* NewAccountData.swift */, A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */, 58CC40EE24A601900019D96E /* ObserverList.swift */, + 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */, 58CAFA01298530DC00BE19F7 /* Promise.swift */, 449EBA242B975B7C00DFA4EB /* Protocols */, 5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */, @@ -3019,9 +3041,12 @@ 5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */, 581DA2722A1E227D0046ED47 /* RESTTypes.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, + 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */, F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */, A91614D02B108D1B00F416EB /* TransportLayer.swift */, 58E511E028DDB7F100B0BCDE /* WrappingError.swift */, + A9711B2A2D662AE3003DA71D /* SwiftConnectionModeProvider.swift */, + A98207EE2D9192A300654558 /* ShadowsocksBridgeProviding.swift */, ); path = MullvadTypes; sourceTree = "<group>"; @@ -3686,7 +3711,6 @@ 58B2FDD42AA71D2A003EB5C6 /* MullvadSettings */ = { isa = PBXGroup; children = ( - 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */, 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */, 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */, F0164EBB2B482E430020268D /* AppStorage.swift */, @@ -3705,12 +3729,10 @@ A9D96B192A8247C100A5C673 /* MigrationManager.swift */, 58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */, F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */, - 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */, 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */, 58FF2C02281BDE02009EF542 /* SettingsManager.swift */, 06410E03292D0F7100AFC18C /* SettingsParser.swift */, 06410E06292D108E00AFC18C /* SettingsStore.swift */, - 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */, A92ECC232A7802520052F1B1 /* StoredAccountData.swift */, A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */, A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */, @@ -4540,6 +4562,9 @@ F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */, F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */, 584023212A406BF5007B27AC /* TunnelObfuscator.swift */, + A96D0B442D675F0400DD6C59 /* MullvadConnectionModeProvider.swift */, + A98207F02D91A0AC00654558 /* MullvadShadowsocksBridgeProvider.swift */, + A98207F22D9ACE4C00654558 /* MullvadAccessMethodReceiver.swift */, ); path = MullvadRustRuntime; sourceTree = "<group>"; @@ -4748,7 +4773,6 @@ F0DC77A22B2314EF0087F09D /* Shadowsocks */ = { isa = PBXGroup; children = ( - F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */, F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */, F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */, F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */, @@ -5038,6 +5062,7 @@ buildRules = ( ); dependencies = ( + A959E2402D75F37600F95DDB /* PBXTargetDependency */, 58FE25BE2AA72188003D1918 /* PBXTargetDependency */, ); name = MullvadSettings; @@ -5797,7 +5822,6 @@ A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */, A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */, 06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */, - F0DDE4182B220458006B57A7 /* ShadowsocksConfiguration.swift in Sources */, 7AA7046A2C8EFE2B0045699D /* StoredRelays.swift in Sources */, F0E5B2F82C9C68CF0007F78C /* EncryptedDNSTransport.swift in Sources */, 06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */, @@ -6055,7 +6079,6 @@ F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */, 58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */, A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */, - F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */, 58B2FDE32AA71D5C003EB5C6 /* StoredDeviceData.swift in Sources */, 58B2FDDF2AA71D5C003EB5C6 /* DNSSettings.swift in Sources */, 58B2FDE02AA71D5C003EB5C6 /* TunnelSettings.swift in Sources */, @@ -6076,12 +6099,10 @@ 449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */, 58B2FDE72AA71D5C003EB5C6 /* SettingsStore.swift in Sources */, 44DD7D2D2B74E44A0005F67F /* QuantumResistanceSettings.swift in Sources */, - F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */, 58B2FDE92AA71D5C003EB5C6 /* SettingsParser.swift in Sources */, F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */, F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */, 58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */, - F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */, 7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */, 7A9F293B2CAC4443005F2089 /* InfoHeaderConfig.swift in Sources */, F0E61CAB2BF2911D000C4A95 /* MultihopSettings.swift in Sources */, @@ -6663,6 +6684,7 @@ buildActionMask = 2147483647; files = ( A91614D12B108D1B00F416EB /* TransportLayer.swift in Sources */, + A959E23E2D75F33300F95DDB /* PersistentAccessMethod.swift in Sources */, 58D22406294C90210029F5F8 /* IPv4Endpoint.swift in Sources */, 7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */, 58D22407294C90210029F5F8 /* IPv6Endpoint.swift in Sources */, @@ -6682,10 +6704,13 @@ A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */, A90C48692C36BF3900DCB94C /* TunnelProvider.swift in Sources */, 58D2240D294C90210029F5F8 /* CustomErrorDescriptionProtocol.swift in Sources */, + A98207EF2D9192A300654558 /* ShadowsocksBridgeProviding.swift in Sources */, 58D2240E294C90210029F5F8 /* Error+Chain.swift in Sources */, 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */, 58D2240F294C90210029F5F8 /* KeychainError.swift in Sources */, + A959E2422D75F3D200F95DDB /* AccessMethodKind.swift in Sources */, 58D22410294C90210029F5F8 /* Location.swift in Sources */, + A98207EB2D9190F100654558 /* ShadowsocksConfiguration.swift in Sources */, 58D22411294C90210029F5F8 /* MullvadEndpoint.swift in Sources */, 58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */, 7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */, @@ -6696,8 +6721,10 @@ 58D22414294C90210029F5F8 /* RelayLocation.swift in Sources */, 581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */, 449EBA262B975B9700DFA4EB /* EphemeralPeerReceiving.swift in Sources */, + A959E2432D75F42500F95DDB /* ShadowsocksCipherOptions.swift in Sources */, 58D22417294C90210029F5F8 /* FixedWidthInteger+Arithmetics.swift in Sources */, F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */, + A9711B2B2D662AE3003DA71D /* SwiftConnectionModeProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6839,8 +6866,11 @@ A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */, F0A89CB72D9D923300580C27 /* String+UnsafePointer.swift in Sources */, A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */, + A98207F12D91A0AC00654558 /* MullvadShadowsocksBridgeProvider.swift in Sources */, 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */, + A98207F32D9ACE4C00654558 /* MullvadAccessMethodReceiver.swift in Sources */, A9173C322C36CCDD00F6A08C /* EphemeralPeerReceiver.swift in Sources */, + A96D0B452D675F0400DD6C59 /* MullvadConnectionModeProvider.swift in Sources */, 7A99D3712D56222000891FF7 /* MullvadApiCancellable.swift in Sources */, A93969812CE606190032A7A0 /* Maybenot.swift in Sources */, F05919802C45515200C301F3 /* EphemeralPeerExchangeActor.swift in Sources */, @@ -7091,6 +7121,11 @@ target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; targetProxy = A9173C332C36CCFB00F6A08C /* PBXContainerItemProxy */; }; + A959E2402D75F37600F95DDB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; + targetProxy = A959E23F2D75F37600F95DDB /* PBXContainerItemProxy */; + }; A9609B6E2D004D1F0065A3D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 58FBDA9722A519BC00EB69A3 /* WireGuardGoBridge */; diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme index 8e3306fc2b..f2cc3507bd 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme @@ -310,6 +310,11 @@ value = "1" isEnabled = "YES"> </EnvironmentVariable> + <EnvironmentVariable + key = "RUST_BACKTRACE" + value = "1" + isEnabled = "YES"> + </EnvironmentVariable> </EnvironmentVariables> </LaunchAction> <ProfileAction diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift index c28598c383..6ebc7aa655 100644 --- a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift @@ -7,7 +7,7 @@ // import Foundation -import MullvadSettings +import MullvadTypes /// Type implementing access method proxy configuration testing. protocol ProxyConfigurationTesterProtocol { diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 16d40ce140..2b8b15042d 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -48,12 +48,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository() private(set) var appPreferences = AppPreferences() - private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol! + private(set) var shadowsocksLoader: ShadowsocksLoader! private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider! private(set) var ipOverrideRepository = IPOverrideRepository() private(set) var relaySelector: RelaySelectorWrapper! private var launchArguments = LaunchArguments() private var encryptedDNSTransport: EncryptedDNSTransport! + var apiContext: MullvadApiContext! + var accessMethodReceiver: MullvadAccessMethodReceiver! // MARK: - Application lifecycle @@ -77,8 +79,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD addressCache = REST.AddressCache(canWriteToCache: true, cacheDirectory: containerURL) addressCache.loadFromFile() - setUpProxies(containerURL: containerURL) - let ipOverrideWrapper = IPOverrideWrapper( relayCache: RelayCache(cacheDirectory: containerURL), ipOverrideRepository: ipOverrideRepository @@ -87,6 +87,38 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let tunnelSettingsListener = TunnelSettingsListener() let tunnelSettingsUpdater = SettingsUpdater(listener: tunnelSettingsListener) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) + let shadowsocksRelaySelector = ShadowsocksRelaySelector( + relayCache: ipOverrideWrapper + ) + + shadowsocksLoader = ShadowsocksLoader( + cache: shadowsocksCache, + relaySelector: shadowsocksRelaySelector, + settingsUpdater: tunnelSettingsUpdater + ) + + let transportStrategy = TransportStrategy( + datasource: accessMethodRepository, + shadowsocksLoader: shadowsocksLoader + ) + + // swiftlint:disable:next force_try + apiContext = try! MullvadApiContext( + host: REST.defaultAPIHostname, + address: REST.defaultAPIEndpoint.description, + domain: REST.encryptedDNSHostname, + shadowsocksProvider: shadowsocksLoader, + accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper + ) + + accessMethodReceiver = MullvadAccessMethodReceiver( + apiContext: apiContext, + accessMethodsDataSource: accessMethodRepository.accessMethodsPublisher, + lastReachableDataSource: accessMethodRepository.lastReachableAccessMethodPublisher + ) + + setUpProxies(containerURL: containerURL) let backgroundTaskProvider = BackgroundTaskProvider( backgroundTimeRemaining: application.backgroundTimeRemaining, application: application @@ -126,18 +158,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy: accountsProxy, transactionLog: .default ) - let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession(addressCache: addressCache)) - let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) - let shadowsocksRelaySelector = ShadowsocksRelaySelector( - relayCache: ipOverrideWrapper - ) - - shadowsocksLoader = ShadowsocksLoader( - cache: shadowsocksCache, - relaySelector: shadowsocksRelaySelector, - settingsUpdater: tunnelSettingsUpdater - ) + let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession(addressCache: addressCache)) encryptedDNSTransport = EncryptedDNSTransport(urlSession: urlSessionTransport.urlSession) configuredTransportProvider = ProxyConfigurationTransportProvider( @@ -146,11 +168,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD encryptedDNSTransport: encryptedDNSTransport ) - let transportStrategy = TransportStrategy( - datasource: accessMethodRepository, - shadowsocksLoader: shadowsocksLoader - ) - let transportProvider = TransportProvider( urlSessionTransport: urlSessionTransport, addressCache: addressCache, @@ -158,7 +175,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD encryptedDNSTransport: encryptedDNSTransport ) - let apiRequestFactory = MullvadApiRequestFactory(apiContext: REST.apiContext) + let apiRequestFactory = MullvadApiRequestFactory(apiContext: apiContext) let apiTransportProvider = APITransportProvider(requestFactory: apiRequestFactory) apiTransportMonitor = APITransportMonitor( @@ -208,8 +225,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD }, apiTransportProvider: REST.AnyAPITransportProvider { [weak self] in self?.apiTransportMonitor.makeTransport() - }, - addressCache: addressCache + }, addressCache: addressCache ) } else { proxyFactory = REST.ProxyFactory.makeProxyFactory( @@ -218,8 +234,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD }, apiTransportProvider: REST.AnyAPITransportProvider { [weak self] in self?.apiTransportMonitor.makeTransport() - }, - addressCache: addressCache + }, addressCache: addressCache ) } apiProxy = proxyFactory.createAPIProxy() diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift index 55591b8a08..cd456d2fc5 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift @@ -8,6 +8,7 @@ import Combine import MullvadSettings +import MullvadTypes import Routing import UIKit diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift index a0ea080a06..5eaefc97cb 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import MullvadSettings +import MullvadTypes protocol AccessMethodEditing: AnyObject { func accessMethodDidSave(_ accessMethod: PersistentAccessMethod) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift index c4d1894381..3d249c841a 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift @@ -8,6 +8,7 @@ import Combine import MullvadSettings +import MullvadTypes import Routing import UIKit diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift index ac10ffa484..e262f7bcab 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift @@ -8,6 +8,7 @@ import Combine import MullvadSettings +import MullvadTypes /// A concrete implementation of an API access list interactor. struct ListAccessMethodInteractor: ListAccessMethodInteractorProtocol { diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift index 571e26f3fd..814a3da68d 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodKind.swift @@ -8,6 +8,7 @@ import Foundation import MullvadSettings +import MullvadTypes /// A kind of API access method. enum AccessMethodKind: Equatable, Hashable, CaseIterable { diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentAccessMethod+ViewModel.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentAccessMethod+ViewModel.swift index 3bc58f51b3..1d699eed91 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentAccessMethod+ViewModel.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentAccessMethod+ViewModel.swift @@ -8,6 +8,7 @@ import Foundation import MullvadSettings +import MullvadTypes extension PersistentAccessMethod { /// Convert persistent model into view model. diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentProxyConfiguration+ViewModel.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentProxyConfiguration+ViewModel.swift index 81bd4e193f..10f806950c 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentProxyConfiguration+ViewModel.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/PersistentProxyConfiguration+ViewModel.swift @@ -7,7 +7,7 @@ // import Foundation -import MullvadSettings +import MullvadTypes extension PersistentProxyConfiguration { /// View model for socks configuration. diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift index c2a7b524ee..16711f31a4 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift @@ -7,6 +7,7 @@ // import MullvadSettings +import MullvadTypes import UIKit /// Type implementing the shadowsocks cipher picker. diff --git a/ios/MullvadVPNTests/MullvadSettings/APIAccessMethodsTests.swift b/ios/MullvadVPNTests/MullvadSettings/APIAccessMethodsTests.swift index 9c602d0eae..72e1ae4cb0 100644 --- a/ios/MullvadVPNTests/MullvadSettings/APIAccessMethodsTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/APIAccessMethodsTests.swift @@ -8,6 +8,7 @@ import Combine @testable import MullvadSettings +@testable import MullvadTypes import XCTest final class APIAccessMethodsTests: XCTestCase { diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift index b44927f931..391e637e23 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift @@ -8,6 +8,7 @@ @testable import MullvadREST @testable import MullvadMockData +@testable import MullvadRustRuntime @testable import MullvadSettings @testable import MullvadTypes @testable import WireGuardKitTypes @@ -25,8 +26,8 @@ class TunnelManagerTests: XCTestCase { var devicesProxy: DevicesProxyStub! var apiProxy: APIProxyStub! var addressCache: REST.AddressCache! - var transportProvider: TransportProvider! + var apiContext: MullvadApiContext! override static func setUp() { SettingsManager.unitTestStore = store @@ -43,6 +44,15 @@ class TunnelManagerTests: XCTestCase { accessTokenManager = AccessTokenManagerStub() devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey))) apiProxy = APIProxyStub() + let shadowsocksLoader = ShadowsocksLoader( + cache: ShadowsocksConfigurationCacheStub(), + relaySelector: ShadowsocksRelaySelectorStub(relays: .mock()), + settingsUpdater: SettingsUpdater(listener: TunnelSettingsListener()) + ) + let transportStrategy = TransportStrategy( + datasource: AccessMethodRepositoryStub.stub, + shadowsocksLoader: shadowsocksLoader + ) addressCache = REST.AddressCache( canWriteToCache: false, fileCache: MockFileCache(initialState: .fileNotFound) @@ -54,19 +64,16 @@ class TunnelManagerTests: XCTestCase { canWriteToCache: true, cacheDirectory: FileManager.default.temporaryDirectory ), - transportStrategy: TransportStrategy( - datasource: AccessMethodRepositoryStub(accessMethods: [PersistentAccessMethod( - id: UUID(), - name: "direct", - isEnabled: true, - proxyConfiguration: .direct - )]), - shadowsocksLoader: ShadowsocksLoader( - cache: ShadowsocksConfigurationCacheStub(), - relaySelector: ShadowsocksRelaySelectorStub(relays: .mock()), - settingsUpdater: SettingsUpdater(listener: TunnelSettingsListener()) - ) - ), encryptedDNSTransport: RESTTransportStub() + transportStrategy: transportStrategy, + encryptedDNSTransport: RESTTransportStub() + ) + + apiContext = try MullvadApiContext( + host: REST.defaultAPIHostname, + address: REST.defaultAPIEndpoint.description, + domain: REST.encryptedDNSHostname, + shadowsocksProvider: shadowsocksLoader, + accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper ) try SettingsManager.writeSettings(LatestTunnelSettings()) @@ -149,7 +156,7 @@ class TunnelManagerTests: XCTestCase { relaySelector: relaySelector, transportProvider: transportProvider, apiTransportProvider: APITransportProvider( - requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext) + requestFactory: MullvadApiRequestFactory(apiContext: apiContext) ) ) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost @@ -220,7 +227,7 @@ class TunnelManagerTests: XCTestCase { relaySelector: relaySelector, transportProvider: transportProvider, apiTransportProvider: APITransportProvider( - requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext) + requestFactory: MullvadApiRequestFactory(apiContext: apiContext) ) ) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 4b943396a6..90f1aa8d69 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -37,6 +37,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { EphemeralPeerReceiver(tunnelProvider: adapter, keyReceiver: self) }() + var apiContext: MullvadApiContext! + var accessMethodReceiver: MullvadAccessMethodReceiver! + // swiftlint:disable:next function_body_length override init() { Self.configureLogging() @@ -65,7 +68,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) let apiTransportProvider = APITransportProvider( - requestFactory: MullvadApiRequestFactory(apiContext: REST.apiContext) + requestFactory: MullvadApiRequestFactory(apiContext: apiContext) ) adapter = WgAdapter(packetTunnelProvider: self) @@ -241,13 +244,32 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { relayCache: ipOverrideWrapper ) + let shadowsocksLoader = ShadowsocksLoader( + cache: shadowsocksCache, + relaySelector: shadowsocksRelaySelector, + settingsUpdater: tunnelSettingsUpdater + ) + + let accessMethodRepository = AccessMethodRepository() + let transportStrategy = TransportStrategy( - datasource: AccessMethodRepository(), - shadowsocksLoader: ShadowsocksLoader( - cache: shadowsocksCache, - relaySelector: shadowsocksRelaySelector, - settingsUpdater: tunnelSettingsUpdater - ) + datasource: accessMethodRepository, + shadowsocksLoader: shadowsocksLoader + ) + + // swiftlint:disable:next force_try + apiContext = try! MullvadApiContext( + host: REST.defaultAPIHostname, + address: REST.defaultAPIEndpoint.description, + domain: REST.encryptedDNSHostname, + shadowsocksProvider: shadowsocksLoader, + accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper + ) + + accessMethodReceiver = MullvadAccessMethodReceiver( + apiContext: apiContext, + accessMethodsDataSource: accessMethodRepository.accessMethodsPublisher, + lastReachableDataSource: accessMethodRepository.lastReachableAccessMethodPublisher ) encryptedDNSTransport = EncryptedDNSTransport(urlSession: urlSession) diff --git a/mullvad-api/src/access_mode.rs b/mullvad-api/src/access_mode.rs index 25b739a014..ecd90b9d82 100644 --- a/mullvad-api/src/access_mode.rs +++ b/mullvad-api/src/access_mode.rs @@ -190,7 +190,7 @@ pub struct AccessModeConnectionModeProvider { } impl AccessModeConnectionModeProvider { - fn new( + pub fn new( handle: AccessModeSelectorHandle, initial_connection_mode: ApiConnectionMode, change_rx: mpsc::UnboundedReceiver<ApiConnectionMode>, @@ -234,6 +234,7 @@ 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, @@ -247,7 +248,10 @@ 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, - access_method_event_sender: mpsc::UnboundedSender<(AccessMethodEvent, oneshot::Sender<()>)>, + #[cfg(not(target_os = "ios"))] access_method_event_sender: mpsc::UnboundedSender<( + AccessMethodEvent, + oneshot::Sender<()>, + )>, ) -> Result<(AccessModeSelectorHandle, AccessModeConnectionModeProvider)> { let (cmd_tx, cmd_rx) = mpsc::unbounded(); @@ -273,6 +277,7 @@ 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, @@ -380,6 +385,24 @@ 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.current = resolved; + + log::info!( + "A new API access method has been selected: {name}", + name = self.current.setting.name + ); + } + + #[cfg(not(target_os = "ios"))] + fn notify_daemon(&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. @@ -402,18 +425,6 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> { .send(sender) .await; }); - - // Notify REST client - let _ = self - .connection_mode_provider_sender - .unbounded_send(resolved.connection_mode.clone()); - - self.current = resolved; - - log::info!( - "A new API access method has been selected: {name}", - name = self.current.setting.name - ); } /// Find the next access method to use. diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index d535ce7cbc..2d068fdf24 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -479,8 +479,8 @@ impl Runtime { ) } - pub fn handle(&mut self) -> &mut tokio::runtime::Handle { - &mut self.handle + pub fn handle(&self) -> &tokio::runtime::Handle { + &self.handle } pub fn availability_handle(&self) -> ApiAvailability { diff --git a/mullvad-ios/Cargo.toml b/mullvad-ios/Cargo.toml index 7a87c03079..519f7852c9 100644 --- a/mullvad-ios/Cargo.toml +++ b/mullvad-ios/Cargo.toml @@ -15,6 +15,7 @@ workspace = true api-override = ["mullvad-api/api-override"] [target.'cfg(target_os = "ios")'.dependencies] +futures = { workspace = true } libc = "0.2" log = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } @@ -29,8 +30,10 @@ talpid-types = { path = "../talpid-types" } talpid-tunnel-config-client = { path = "../talpid-tunnel-config-client" } mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" } mullvad-api = { path = "../mullvad-api", default-features = false } +mullvad-types = { path = "../mullvad-types" } serde_json = { workspace = true } mockito = "1.6.1" +async-trait = "0.1" shadowsocks-service = { workspace = true, features = [ "local", diff --git a/mullvad-ios/src/api_client/access_method_resolver.rs b/mullvad-ios/src/api_client/access_method_resolver.rs new file mode 100644 index 0000000000..5ce22cc5cc --- /dev/null +++ b/mullvad-ios/src/api_client/access_method_resolver.rs @@ -0,0 +1,89 @@ +use mullvad_api::{ + access_mode::AccessMethodResolver, + proxy::{ApiConnectionMode, ProxyConfig}, + ApiEndpoint, +}; +use mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState; +use mullvad_types::access_method::{AccessMethod, BuiltInAccessMethod}; +use talpid_types::net::{ + proxy::CustomProxy, AllowedClients, AllowedEndpoint, Endpoint, TransportProtocol, +}; +use tonic::async_trait; + +use super::shadowsocks_loader::SwiftShadowsocksLoaderWrapper; + +#[derive(Debug)] +pub struct SwiftAccessMethodResolver { + endpoint: ApiEndpoint, + domain: String, + state: EncryptedDnsProxyState, + bridge_provider: SwiftShadowsocksLoaderWrapper, +} + +impl SwiftAccessMethodResolver { + pub fn new( + endpoint: ApiEndpoint, + domain: String, + state: EncryptedDnsProxyState, + bridge_provider: SwiftShadowsocksLoaderWrapper, + ) -> Self { + Self { + endpoint, + domain, + state, + bridge_provider, + } + } +} + +#[async_trait] +impl AccessMethodResolver for SwiftAccessMethodResolver { + async fn resolve_access_method_setting( + &mut self, + access_method: &AccessMethod, + ) -> Option<(AllowedEndpoint, ApiConnectionMode)> { + let connection_mode = match access_method { + AccessMethod::BuiltIn(BuiltInAccessMethod::Direct) => ApiConnectionMode::Direct, + AccessMethod::BuiltIn(BuiltInAccessMethod::Bridge) => { + let bridge = self.bridge_provider.get_bridges()?; + let proxy = CustomProxy::Shadowsocks(bridge); + ApiConnectionMode::Proxied(ProxyConfig::from(proxy)) + } + AccessMethod::BuiltIn(BuiltInAccessMethod::EncryptedDnsProxy) => { + if let Err(error) = self.state.fetch_configs(self.domain.as_str()).await { + log::error!("{error:#?}"); + } + let Some(edp) = self.state.next_configuration() else { + log::warn!("Could not select next Encrypted DNS proxy config"); + return None; + }; + ApiConnectionMode::Proxied(ProxyConfig::from(edp)) + } + AccessMethod::Custom(config) => { + ApiConnectionMode::Proxied(ProxyConfig::from(config.clone())) + } + }; + + let allowed_endpoint = { + let endpoint = connection_mode.get_endpoint().unwrap_or_else(|| { + Endpoint::from_socket_address( + self.endpoint.address.unwrap(), + TransportProtocol::Tcp, + ) + }); + let clients = AllowedClients::All; + AllowedEndpoint { endpoint, clients } + }; + + Some((allowed_endpoint, connection_mode)) + } + + async fn default_connection_mode(&self) -> AllowedEndpoint { + // TODO: Call the iOS Address cache implementation instead of returning the default endpoint + let endpoint = ApiConnectionMode::Direct.get_endpoint().unwrap(); + AllowedEndpoint { + endpoint, + clients: AllowedClients::All, + } + } +} diff --git a/mullvad-ios/src/api_client/access_method_settings.rs b/mullvad-ios/src/api_client/access_method_settings.rs new file mode 100644 index 0000000000..35a90d1f0c --- /dev/null +++ b/mullvad-ios/src/api_client/access_method_settings.rs @@ -0,0 +1,193 @@ +use std::{ + ffi::{c_char, c_void}, + ptr::null_mut, + slice, +}; + +use mullvad_types::access_method::{ + AccessMethod, AccessMethodSetting, + BuiltInAccessMethod::{Bridge, Direct, EncryptedDnsProxy}, + Id, Settings, +}; +use talpid_types::net::proxy::{self, Shadowsocks, Socks5Remote}; + +use super::helpers::convert_c_string; + +/// Converts parameters into a `Box<AccessMethodSetting>` raw representation that +/// can be passed across the FFI boundary +/// +/// # SAFETY: +/// `unique_identifier` and `name` must point to valid memory regions and contain NULL terminators. +/// They are only valid for the duration of this call. +/// +/// `proxy_configuration` can be NULL, or must be a pointer gotten through +/// either the `convert_shadowsocks` or `convert_socks5` methods. +#[unsafe(no_mangle)] +unsafe extern "C" fn convert_builtin_access_method_setting( + unique_identifier: *const c_char, + name: *const c_char, + is_enabled: bool, + method_kind: SwiftAccessMethodKind, + proxy_configuration: *const c_void, +) -> *mut c_void { + match convert_builtin_access_method_setting_inner( + unique_identifier, + name, + is_enabled, + method_kind, + proxy_configuration, + ) { + Some(access_method) => Box::into_raw(Box::new(access_method)) as *mut c_void, + None => null_mut(), + } +} + +/// Converts parameters into an `AccessMethodSetting` +/// +/// This function copies the strings from the conversion of the variables +/// `unique_identifier`, `name`, and takes ownership of `proxy_configuration` +fn convert_builtin_access_method_setting_inner( + unique_identifier: *const c_char, + name: *const c_char, + enabled: bool, + method_kind: SwiftAccessMethodKind, + proxy_configuration: *const c_void, +) -> Option<AccessMethodSetting> { + // SAFETY: See `convert_builtin_access_method_setting` + let id = Id::from_string(unsafe { convert_c_string(unique_identifier) })?; + // SAFETY: See `convert_builtin_access_method_setting` + let name = unsafe { convert_c_string(name) }; + match method_kind { + SwiftAccessMethodKind::KindDirect => Some(AccessMethodSetting::with_id( + id, + name, + enabled, + AccessMethod::BuiltIn(Direct), + )), + SwiftAccessMethodKind::KindBridge => Some(AccessMethodSetting::with_id( + id, + name, + enabled, + AccessMethod::BuiltIn(Bridge), + )), + + SwiftAccessMethodKind::KindEncryptedDnsProxy => Some(AccessMethodSetting::with_id( + id, + name, + enabled, + AccessMethod::BuiltIn(EncryptedDnsProxy), + )), + + SwiftAccessMethodKind::KindShadowsocks => match proxy_configuration.is_null() { + true => None, + false => { + // SAFETY: See `convert_builtin_access_method_setting` + let configuration: Shadowsocks = + unsafe { *Box::from_raw(proxy_configuration as *mut _) }; + Some(AccessMethodSetting::with_id( + id, + name, + enabled, + AccessMethod::Custom(proxy::CustomProxy::Shadowsocks(configuration)), + )) + } + }, + SwiftAccessMethodKind::KindSocks5Local => match proxy_configuration.is_null() { + true => None, + false => { + // SAFETY: See `convert_builtin_access_method_setting` + let configuration: Socks5Remote = + unsafe { *Box::from_raw(proxy_configuration as *mut _) }; + Some(AccessMethodSetting::with_id( + id, + name, + enabled, + AccessMethod::Custom(proxy::CustomProxy::Socks5Remote(configuration)), + )) + } + }, + } +} + +/// Used by Swift to instruct which access method kind it is trying to convert +#[allow(dead_code)] +#[repr(u8)] +pub enum SwiftAccessMethodKind { + KindDirect = 0, + KindBridge, + KindEncryptedDnsProxy, + KindShadowsocks, + KindSocks5Local, +} + +/// Creates a wrapper around a `Settings` object that can be safely sent across the FFI boundary. +/// +/// # SAFETY +/// `direct_method_raw`, `bridges_method_raw` and `encrypted_dns_method_raw` must be raw pointers +/// resulting from a call to `convert_builtin_access_method_setting` +/// `custom_methods_raw` is an array of pointers to instances of `AccessMethodSetting` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_access_method_settings_wrapper( + direct_method_raw: *const c_void, + bridges_method_raw: *const c_void, + encrypted_dns_method_raw: *const c_void, + custom_methods_raw: *const c_void, + custom_method_count: usize, +) -> SwiftAccessMethodSettingsWrapper { + // SAFETY: See `init_access_method_settings_wrapper` + let (direct, mullvad_bridges, encrypted_dns_proxy) = unsafe { + ( + *Box::from_raw(direct_method_raw as *mut _), + *Box::from_raw(bridges_method_raw as *mut _), + *Box::from_raw(encrypted_dns_method_raw as *mut _), + ) + }; + + let custom = access_methods_from_raw_array(custom_methods_raw, custom_method_count); + let settings = Settings::new(direct, mullvad_bridges, encrypted_dns_proxy, custom); + let context = SwiftAccessMethodSettingsContext { settings }; + SwiftAccessMethodSettingsWrapper::new(context) +} + +/// Creates a vector of `AccessMethodSetting` objects from a C array +/// +/// SAFETY: `raw_array` must be aligned, non-null and initialized for `count` reads +unsafe fn access_methods_from_raw_array( + raw_array: *const c_void, + number_of_elements: usize, +) -> Vec<AccessMethodSetting> { + let raw_array: *mut *mut AccessMethodSetting = raw_array as _; + // SAFETY: See notice above + let slice = unsafe { slice::from_raw_parts(raw_array, number_of_elements) }; + slice + .iter() + .map(|&ptr| { + // SAFETY: `slice` is a slice of pointers to `AccessMethodSetting` created with `Box::into_raw` + *unsafe { Box::from_raw(ptr) } + }) + .collect() +} + +#[repr(C)] +pub struct SwiftAccessMethodSettingsWrapper(*mut SwiftAccessMethodSettingsContext); + +impl SwiftAccessMethodSettingsWrapper { + pub fn new(context: SwiftAccessMethodSettingsContext) -> SwiftAccessMethodSettingsWrapper { + SwiftAccessMethodSettingsWrapper(Box::into_raw(Box::new(context))) + } + + pub unsafe fn into_rust_context(self) -> Box<SwiftAccessMethodSettingsContext> { + Box::from_raw(self.0) + } +} + +#[derive(Debug)] +pub struct SwiftAccessMethodSettingsContext { + pub settings: Settings, +} + +impl SwiftAccessMethodSettingsContext { + pub fn convert_access_method(&self) -> Option<Settings> { + Some(self.settings.clone()) + } +} diff --git a/mullvad-ios/src/api_client/account.rs b/mullvad-ios/src/api_client/account.rs index 768832fcb7..36b601ad2d 100644 --- a/mullvad-ios/src/api_client/account.rs +++ b/mullvad-ios/src/api_client/account.rs @@ -41,7 +41,8 @@ pub unsafe extern "C" fn mullvad_ios_get_account( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See documentation for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; // SAFETY: See param documentation for `account_number`. let account_number = unsafe { CStr::from_ptr(account_number.cast()) } @@ -92,7 +93,8 @@ pub unsafe extern "C" fn mullvad_ios_create_account( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See notes for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; let completion = completion_handler.clone(); @@ -135,7 +137,8 @@ pub unsafe extern "C" fn mullvad_ios_delete_account( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See notes for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; // SAFETY: See param documentation for `account_number`. let account_number = unsafe { CStr::from_ptr(account_number.cast()) } diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs index 62060dfacb..ed37695f6f 100644 --- a/mullvad-ios/src/api_client/api.rs +++ b/mullvad-ios/src/api_client/api.rs @@ -38,7 +38,8 @@ pub unsafe extern "C" fn mullvad_ios_get_addresses( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See notes for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; let completion = completion_handler.clone(); @@ -81,7 +82,8 @@ pub unsafe extern "C" fn mullvad_ios_get_relays( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See notes for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; let mut maybe_etag: Option<String> = None; diff --git a/mullvad-ios/src/api_client/cancellation.rs b/mullvad-ios/src/api_client/cancellation.rs index 3c18340478..5ea2c902c1 100644 --- a/mullvad-ios/src/api_client/cancellation.rs +++ b/mullvad-ios/src/api_client/cancellation.rs @@ -16,11 +16,13 @@ impl SwiftCancelHandle { /// This consumes and nulls out the pointer. The caller is responsible for the pointer being valid /// when calling `to_handle`. + /// + /// SAFETY: + /// This call is safe as long as the pointer is only ever used from a single thread and the + /// instance of `SwiftCancelHandle` was created with a valid pointer to + /// `RequestCancelHandle`. unsafe fn into_handle(mut self) -> RequestCancelHandle { - // # Safety - // This call is safe as long as the pointer is only ever used from a single thread and the - // instance of `SwiftCancelHandle` was created with a valid pointer to - // `RequestCancelHandle`. + // SAFETY: See safety notes above let handle = unsafe { *Box::from_raw(self.ptr) }; self.ptr = null_mut(); @@ -67,6 +69,7 @@ extern "C" fn mullvad_api_cancel_task(handle_ptr: SwiftCancelHandle) { return; } + // SAFETY: See notes for `into_handle` let handle = unsafe { handle_ptr.into_handle() }; handle.cancel() } @@ -84,5 +87,6 @@ extern "C" fn mullvad_api_cancel_task_drop(handle_ptr: SwiftCancelHandle) { return; } + // SAFETY: See notes for `into_handle` let _handle = unsafe { handle_ptr.into_handle() }; } diff --git a/mullvad-ios/src/api_client/completion.rs b/mullvad-ios/src/api_client/completion.rs index db15a6b8b6..7689ad0f6a 100644 --- a/mullvad-ios/src/api_client/completion.rs +++ b/mullvad-ios/src/api_client/completion.rs @@ -23,6 +23,8 @@ extern "C" { pub struct CompletionCookie { inner: *mut std::ffi::c_void, } +/// SAFETY: Access to `CompletionCookie` should always be done through a `SwiftCompletionHandler` +/// It is safe to be used and sent from any threads. unsafe impl Send for CompletionCookie {} impl CompletionCookie { /// `inner` must be pointing to a valid instance of Swift object `MullvadApiCompletion`. @@ -54,6 +56,7 @@ impl SwiftCompletionHandler { return; }; + // SAFETY: See safety notes for `mullvad_api_completion_finish` unsafe { mullvad_api_completion_finish(response, cookie) }; } } diff --git a/mullvad-ios/src/api_client/helpers.rs b/mullvad-ios/src/api_client/helpers.rs new file mode 100644 index 0000000000..f331e56a9b --- /dev/null +++ b/mullvad-ios/src/api_client/helpers.rs @@ -0,0 +1,108 @@ +use std::{ + ffi::{c_char, c_void, CStr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, +}; + +use talpid_types::net::proxy::{Shadowsocks, Socks5Remote, SocksAuth}; + +/// Constructs a new IP address from a pointer containing bytes representing an IP address. +/// +/// SAFETY: `addr` pointer must be non-null, aligned, and point to at least addr_len bytes +pub(crate) unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<IpAddr> { + match addr_len { + 4 => { + // SAFETY: `addr` pointer must be non-null, aligned, and point to at least addr_len bytes + let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; + Some(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()) + } + 16 => { + // SAFETY: `addr` pointer must be non-null, aligned, and point to at least addr_len bytes + let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; + let mut addr_arr = [0u8; 16]; + addr_arr.as_mut_slice().copy_from_slice(bytes); + + Some(Ipv6Addr::from(addr_arr).into()) + } + anything_else => { + log::error!("Bad IP address length {anything_else}"); + None + } + } +} + +/// Converts a pointer to a C style string into an owned Rust `String` +/// +/// # SAFETY +/// `c_str` must point to a valid, null terminated C string. +pub unsafe fn convert_c_string(c_str: *const c_char) -> String { + // SAFETY: c_str points to a valid region of memory and contains a null terminator. + let str = unsafe { CStr::from_ptr(c_str) }; + String::from_utf8_lossy(str.to_bytes()).into_owned() +} + +/// Converts parameters into a boxed `Shadowsocks` configuration that is safe +/// to send across the FFI boundary +/// +/// # SAFETY +/// `address` must be a pointer to at least `address_len` bytes. +/// `c_password` and `c_cipher` must be pointers to null terminated strings +#[unsafe(no_mangle)] +pub unsafe extern "C" fn new_shadowsocks_access_method_setting( + address: *const u8, + address_len: usize, + port: u16, + c_password: *const c_char, + c_cipher: *const c_char, +) -> *const c_void { + let endpoint: SocketAddr = if let Some(ip_address) = parse_ip_addr(address, address_len) { + SocketAddr::new(ip_address, port) + } else { + return std::ptr::null(); + }; + + let password = convert_c_string(c_password); + let cipher = convert_c_string(c_cipher); + + let shadowsocks_configuration = Shadowsocks { + endpoint, + password, + cipher, + }; + + Box::into_raw(Box::new(shadowsocks_configuration)) as *mut c_void +} + +/// Converts parameters into a boxed `Socks5Remote` configuration that is safe +/// +/// to send across the FFI boundary +/// +/// # SAFETY +/// `address` must be a pointer to at least `address_len` bytes. +/// `c_username` and `c_password` must be pointers to null terminated strings, or null +#[unsafe(no_mangle)] +pub unsafe extern "C" fn new_socks5_access_method_setting( + address: *const u8, + address_len: usize, + port: u16, + c_username: *const c_char, + c_password: *const c_char, +) -> *const c_void { + let endpoint: SocketAddr = if let Some(ip_address) = parse_ip_addr(address, address_len) { + SocketAddr::new(ip_address, port) + } else { + return std::ptr::null(); + }; + + let auth = { + if c_username.is_null() || c_password.is_null() { + None + } else { + let username = convert_c_string(c_username); + let password = convert_c_string(c_password); + SocksAuth::new(username, password).ok() + } + }; + + let socks5_configuration = Socks5Remote { endpoint, auth }; + Box::into_raw(Box::new(socks5_configuration)) as *mut c_void +} diff --git a/mullvad-ios/src/api_client/mock.rs b/mullvad-ios/src/api_client/mock.rs index 1c8dd1a1e1..005a6c10c0 100644 --- a/mullvad-ios/src/api_client/mock.rs +++ b/mullvad-ios/src/api_client/mock.rs @@ -39,9 +39,11 @@ pub unsafe extern "C" fn mullvad_api_mock_get( response_code: usize, response_body: *const u8, ) -> SwiftServerMock { + // SAFETY: See notes above let path = unsafe { std::ffi::CStr::from_ptr(path.cast()) } .to_str() .unwrap(); + // SAFETY: See notes above let response_body = unsafe { std::ffi::CStr::from_ptr(response_body.cast()) } .to_str() .unwrap(); @@ -70,9 +72,11 @@ pub unsafe extern "C" fn mullvad_api_mock_post( response_code: usize, match_body: *const c_char, ) -> SwiftServerMock { + // SAFETY: See notes above let path = unsafe { std::ffi::CStr::from_ptr(path.cast()) } .to_str() .unwrap(); + // SAFETY: See notes above let match_body = unsafe { std::ffi::CStr::from_ptr(match_body.cast()) } .to_str() .unwrap(); @@ -96,9 +100,11 @@ pub unsafe extern "C" fn mullvad_api_mock_post( #[unsafe(no_mangle)] extern "C" fn mullvad_api_mock_drop(mock_ptr: SwiftServerMock) { if !mock_ptr.mock_ptr.is_null() { + // SAFETY: See notes above unsafe { drop(Box::from_raw(mock_ptr.mock_ptr as *mut Mock)) }; } if !mock_ptr.server_ptr.is_null() { + // SAFETY: See notes above unsafe { drop(Box::from_raw(mock_ptr.server_ptr as *mut ServerGuard)) }; } } diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs index 9ab6eef199..e8fe1ced24 100644 --- a/mullvad-ios/src/api_client/mod.rs +++ b/mullvad-ios/src/api_client/mod.rs @@ -1,22 +1,32 @@ -use std::{ffi::CStr, future::Future, sync::Arc}; +use std::{ffi::c_char, future::Future, sync::Arc}; +use access_method_resolver::SwiftAccessMethodResolver; +use access_method_settings::SwiftAccessMethodSettingsWrapper; +use helpers::convert_c_string; use mullvad_api::{ - proxy::{ApiConnectionMode, StaticConnectionModeProvider}, + access_mode::{AccessModeSelector, AccessModeSelectorHandle}, rest::{self, MullvadRestHandle}, ApiEndpoint, Runtime, }; +use mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState; +use mullvad_types::access_method::{Id, Settings}; use response::SwiftMullvadApiResponse; use retry_strategy::RetryStrategy; +use shadowsocks_loader::SwiftShadowsocksLoaderWrapper; use talpid_future::retry::retry_future; +mod access_method_resolver; +mod access_method_settings; mod account; mod api; mod cancellation; mod completion; +pub(super) mod helpers; mod mock; mod problem_report; mod response; mod retry_strategy; +mod shadowsocks_loader; #[repr(C)] pub struct SwiftApiContext(*const ApiContext); @@ -25,20 +35,82 @@ impl SwiftApiContext { SwiftApiContext(Arc::into_raw(Arc::new(context))) } - pub unsafe fn into_rust_context(self) -> Arc<ApiContext> { - Arc::increment_strong_count(self.0); - Arc::from_raw(self.0) + /// Extracts an `ApiContext` from `self` + /// + /// The `ApiContext` extracted is meant to live as long as the process it's used in. + pub fn rust_context(self) -> Arc<ApiContext> { + // SAFETY: This will never be deallocated + unsafe { + Arc::increment_strong_count(self.0); + Arc::from_raw(self.0) + } } } pub struct ApiContext { - _api_client: Runtime, + api_client: Runtime, rest_client: MullvadRestHandle, + access_mode_handler: AccessModeSelectorHandle, } impl ApiContext { pub fn rest_handle(&self) -> MullvadRestHandle { self.rest_client.clone() } + + /// Sets the access method referenced by `id` as currently in use. + /// + /// This function will block the current thread until it is complete, + /// make sure to not call this from a UI Thread if possible. + pub fn use_access_method(&self, id: Id) { + _ = self + .api_client + .handle() + .block_on(async { self.access_mode_handler.use_access_method(id).await }); + } + + /// Replaces the current set of access methods with `access_methods. + /// + /// This function will block the current thread until it is complete, + /// make sure to not call this from a UI Thread if possible. + pub fn update_access_methods(&self, access_methods: Settings) { + _ = self.api_client.handle().block_on(async { + self.access_mode_handler + .update_access_methods(access_methods) + .await + }); + } +} + +/// Called by Swift to set the available access methods +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_api_update_access_methods( + api_context: SwiftApiContext, + settings_wrapper: SwiftAccessMethodSettingsWrapper, +) { + let access_methods = settings_wrapper.into_rust_context().settings; + api_context + .rust_context() + .update_access_methods(access_methods); +} + +/// Called by Swift to update the currently used access methods +/// +/// # SAFETY +/// `access_method_id` must point to a null terminated string in a UUID format +/// +#[unsafe(no_mangle)] +pub unsafe extern "C" fn mullvad_api_use_access_method( + api_context: SwiftApiContext, + access_method_id: *const c_char, +) { + let api_context = api_context.rust_context(); + // SAFETY: See Safety notes for `convert_c_string` + let id = unsafe { convert_c_string(access_method_id) }; + + let Some(id) = Id::from_string(id) else { + return; + }; + api_context.use_access_method(id); } /// # Safety @@ -56,10 +128,20 @@ impl ApiContext { #[cfg(feature = "api-override")] #[no_mangle] pub extern "C" fn mullvad_api_init_new_tls_disabled( - host: *const u8, - address: *const u8, + host: *const c_char, + address: *const c_char, + domain: *const c_char, + bridge_provider: SwiftShadowsocksLoaderWrapper, + settings_provider: SwiftAccessMethodSettingsWrapper, ) -> SwiftApiContext { - mullvad_api_init_inner(host, address, true) + mullvad_api_init_inner( + host, + address, + domain, + true, + bridge_provider, + settings_provider, + ) } /// # Safety @@ -75,26 +157,61 @@ pub extern "C" fn mullvad_api_init_new_tls_disabled( /// /// This function is safe. #[no_mangle] -pub extern "C" fn mullvad_api_init_new(host: *const u8, address: *const u8) -> SwiftApiContext { +pub extern "C" fn mullvad_api_init_new( + host: *const c_char, + address: *const c_char, + domain: *const c_char, + bridge_provider: SwiftShadowsocksLoaderWrapper, + settings_provider: SwiftAccessMethodSettingsWrapper, +) -> SwiftApiContext { #[cfg(feature = "api-override")] - return mullvad_api_init_inner(host, address, false); + return mullvad_api_init_inner( + host, + address, + domain, + true, + bridge_provider, + settings_provider, + ); #[cfg(not(feature = "api-override"))] - return mullvad_api_init_inner(host, address); + mullvad_api_init_inner(host, address, domain, bridge_provider, settings_provider) } -fn mullvad_api_init_inner( - host: *const u8, - address: *const u8, +/// # Safety +/// +/// `host` must be a pointer to a null terminated string representing a hostname for Mullvad API host. +/// This hostname will be used for TLS validation but not used for domain name resolution. +/// +/// `address` must be a pointer to a null terminated string representing a socket address through which +/// the Mullvad API can be reached directly. +/// +/// If a context cannot be constructed this function will panic since the call site would not be able +/// to proceed in a meaningful way anyway. +/// +/// This function is safe. +#[unsafe(no_mangle)] +pub extern "C" fn mullvad_api_init_inner( + host: *const c_char, + address: *const c_char, + domain: *const c_char, #[cfg(feature = "api-override")] disable_tls: bool, + bridge_provider: SwiftShadowsocksLoaderWrapper, + settings_provider: SwiftAccessMethodSettingsWrapper, ) -> SwiftApiContext { - let host = unsafe { CStr::from_ptr(host.cast()) }; - let address = unsafe { CStr::from_ptr(address.cast()) }; - - let host = host.to_str().unwrap(); - let address = address.to_str().unwrap(); + // Safety: See notes for `convert_c_string` + let (host, address, domain) = unsafe { + ( + convert_c_string(host), + convert_c_string(address), + convert_c_string(domain), + ) + }; + // The iOS client provides a different default endpoint based on its configuration + // Debug and Release builds use the standard endpoints + // Staging builds will use the staging endpoint let endpoint = ApiEndpoint { - host: Some(String::from(host)), + host: Some(host), address: Some(address.parse().unwrap()), #[cfg(feature = "api-override")] disable_tls, @@ -104,16 +221,37 @@ fn mullvad_api_init_inner( let tokio_handle = crate::mullvad_ios_runtime().unwrap(); + // SAFETY: See notes for `into_rust_context` + let settings_context = unsafe { settings_provider.into_rust_context() }; + let access_method_settings = settings_context.convert_access_method().unwrap(); + let encrypted_dns_proxy_state = EncryptedDnsProxyState::default(); + + let method_resolver = SwiftAccessMethodResolver::new( + endpoint.clone(), + domain, + encrypted_dns_proxy_state, + bridge_provider, + ); + let api_context = tokio_handle.clone().block_on(async move { + let (access_mode_handler, access_mode_provider) = AccessModeSelector::spawn( + method_resolver, + access_method_settings, + #[cfg(feature = "api-override")] + endpoint.clone(), + ) + .await + .expect("Could now spawn AccessModeSelector"); + // 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); - let rest_client = api_client - .mullvad_rest_handle(StaticConnectionModeProvider::new(ApiConnectionMode::Direct)); + let rest_client = api_client.mullvad_rest_handle(access_mode_provider); ApiContext { - _api_client: api_client, + api_client, rest_client, + access_mode_handler, } }); diff --git a/mullvad-ios/src/api_client/problem_report.rs b/mullvad-ios/src/api_client/problem_report.rs index a12238c789..8c47da0eef 100644 --- a/mullvad-ios/src/api_client/problem_report.rs +++ b/mullvad-ios/src/api_client/problem_report.rs @@ -47,9 +47,11 @@ pub unsafe extern "C" fn mullvad_ios_send_problem_report( return SwiftCancelHandle::empty(); }; - let api_context = api_context.into_rust_context(); + let api_context = api_context.rust_context(); + // SAFETY: See safety notes for `into_rust` let retry_strategy = unsafe { retry_strategy.into_rust() }; + // SAFETY: See safety notes for `from_swift_parameters` let result = unsafe { ProblemReportRequest::from_swift_parameters(request) }; let Some(problem_report_request) = result else { let err = Error::ApiError( @@ -114,8 +116,6 @@ struct ProblemReportRequest { metadata: BTreeMap<String, String>, } -unsafe impl Send for SwiftProblemReportRequest {} - impl ProblemReportRequest { // SAFETY: the members of `SwiftProblemReportRequest` must point to null-terminated strings unsafe fn from_swift_parameters(request: SwiftProblemReportRequest) -> Option<Self> { @@ -183,8 +183,8 @@ impl Map { "value must not be null (violates safety contract)" ); - let key = unsafe { CStr::from_ptr(key) }; - let value = unsafe { CStr::from_ptr(value) }; + // SAFETY: See notes above + let (key, value) = unsafe { (CStr::from_ptr(key), CStr::from_ptr(value)) }; match key.to_str() { Ok(key_str) => match value.to_str() { diff --git a/mullvad-ios/src/api_client/shadowsocks_loader.rs b/mullvad-ios/src/api_client/shadowsocks_loader.rs new file mode 100644 index 0000000000..161ee8a30a --- /dev/null +++ b/mullvad-ios/src/api_client/shadowsocks_loader.rs @@ -0,0 +1,75 @@ +use std::ffi::c_void; +use talpid_types::net::proxy::Shadowsocks; + +extern "C" { + /// Creates a `Shadowsocks` configuration. + /// + /// # SAFETY + /// `rawBridgeProvider` **must** be provided by a call to `init_swift_shadowsocks_loader_wrapper` + /// It is okay to persist it, and use it across multiple threads. + pub fn swift_get_shadowsocks_bridges(rawBridgeProvider: *const c_void) -> *const c_void; +} + +#[derive(Debug)] +#[repr(C)] +pub struct SwiftShadowsocksLoaderWrapper(SwiftShadowsocksLoaderWrapperContext); +impl SwiftShadowsocksLoaderWrapper { + pub fn new(context: SwiftShadowsocksLoaderWrapperContext) -> SwiftShadowsocksLoaderWrapper { + SwiftShadowsocksLoaderWrapper(context) + } + + pub fn get_bridges(&self) -> Option<Shadowsocks> { + self.context_ref().get_bridges() + } + + fn context_ref(&self) -> &SwiftShadowsocksLoaderWrapperContext { + &self.0 + } +} + +// SAFETY: The context stored inside `SwiftShadowsocksLoaderWrapper` points to an object that is guaranteed to be thread safe +unsafe impl Sync for SwiftShadowsocksLoaderWrapper {} +// SAFETY: The context stored inside `SwiftShadowsocksLoaderWrapper` points to an object that is guaranteed to be Sendable +unsafe impl Send for SwiftShadowsocksLoaderWrapper {} + +#[derive(Debug)] +#[repr(C)] +pub struct SwiftShadowsocksLoaderWrapperContext { + // This pointer is a reference to a Swift object, and is only ever read by Rust. + // It is used to call that Swift object across the FFI + shadowsocks_loader: *const c_void, +} + +// SAFETY: `shadowsocks_loader` inside the `SwiftShadowsocksLoaderWrapperContext ` points to an object that is guaranteed to be thread safe +unsafe impl Sync for SwiftShadowsocksLoaderWrapperContext {} +// SAFETY: `shadowsocks_loader` inside the `SwiftShadowsocksLoaderWrapperContext ` points to an object that is guaranteed to be Sendable +unsafe impl Send for SwiftShadowsocksLoaderWrapperContext {} + +impl SwiftShadowsocksLoaderWrapperContext { + pub fn get_bridges(&self) -> Option<Shadowsocks> { + // SAFETY: See notice for `swift_get_shadowsocks_bridges` + let raw_configuration = unsafe { swift_get_shadowsocks_bridges(self.shadowsocks_loader) }; + if raw_configuration.is_null() { + return None; + } + // SAFETY: The pointer returned by `swift_get_shadowsocks_bridges` is guaranteed + // to point to a valid `Shadowsocks` configuration, and has been null-checked + let bridges: Shadowsocks = unsafe { *Box::from_raw(raw_configuration as *mut _) }; + Some(bridges) + } +} + +/// Called by the Swift side in order to provide an object to rust that can create +/// Shadowsocks configurations +/// +/// # SAFETY +/// `shadowsocks_loader` **must be** pointing to a valid instance of a `SwiftShadowsocksBridgeProvider` +/// That instance's lifetime has to be equivalent to a `'static` lifetime in Rust +/// This function does not take ownership of `shadowsocks_loader` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_swift_shadowsocks_loader_wrapper( + shadowsocks_loader: *const c_void, +) -> SwiftShadowsocksLoaderWrapper { + let context = SwiftShadowsocksLoaderWrapperContext { shadowsocks_loader }; + SwiftShadowsocksLoaderWrapper::new(context) +} diff --git a/mullvad-ios/src/encrypted_dns_proxy.rs b/mullvad-ios/src/encrypted_dns_proxy.rs index 3551d74179..f424e72bb0 100644 --- a/mullvad-ios/src/encrypted_dns_proxy.rs +++ b/mullvad-ios/src/encrypted_dns_proxy.rs @@ -118,6 +118,7 @@ pub unsafe extern "C" fn encrypted_dns_proxy_init( /// once. #[unsafe(no_mangle)] pub unsafe extern "C" fn encrypted_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) { + // SAFETY: See notes above let _ = unsafe { Box::from_raw(ptr) }; } @@ -143,17 +144,20 @@ pub unsafe extern "C" fn encrypted_dns_proxy_start( } }; + // SAFETY: See notes above let mut encrypted_dns_proxy = unsafe { Box::from_raw(encrypted_dns_proxy) }; let proxy_result = handle.block_on(encrypted_dns_proxy.start()); mem::forget(encrypted_dns_proxy); match proxy_result { + // SAFETY: `proxy_handle` is guaranteed to be a valid pointer Ok(handle) => unsafe { ptr::write(proxy_handle, handle) }, Err(err) => { let empty_handle = ProxyHandle { context: ptr::null_mut(), port: 0, }; + // SAFETY: `proxy_handle` is guaranteed to be a valid pointer unsafe { ptr::write(proxy_handle, empty_handle) } log::error!("Failed to create a proxy connection: {err:?}"); return err.into(); @@ -168,8 +172,10 @@ pub unsafe extern "C" fn encrypted_dns_proxy_start( /// [`encrypted_dns_proxy_start`]. It should only ever be called once. #[unsafe(no_mangle)] pub unsafe extern "C" fn encrypted_dns_proxy_stop(proxy_config: *mut ProxyHandle) -> i32 { + // SAFETY: See notes above let ptr = unsafe { (*proxy_config).context }; if !ptr.is_null() { + // SAFETY: `ptr` is guaranteed to be non-null and valid let handle: Box<JoinHandle<()>> = unsafe { Box::from_raw(ptr.cast()) }; handle.abort(); } diff --git a/mullvad-ios/src/ephemeral_peer_proxy/ios_tcp_connection.rs b/mullvad-ios/src/ephemeral_peer_proxy/ios_tcp_connection.rs index 1be9a18bf3..5b6de86a62 100644 --- a/mullvad-ios/src/ephemeral_peer_proxy/ios_tcp_connection.rs +++ b/mullvad-ios/src/ephemeral_peer_proxy/ios_tcp_connection.rs @@ -35,6 +35,7 @@ impl WgTcpConnectionFunctions { /// This function is safe to call so long as the function pointer is valid for its declared /// signature. pub unsafe fn open(&self, tunnel_handle: i32, address: *const u8, timeout: u64) -> i32 { + // SAFETY: See above unsafe { (self.open_fn)(tunnel_handle, address.cast(), timeout) } } @@ -42,6 +43,7 @@ impl WgTcpConnectionFunctions { /// This function is safe to call so long as the function pointer is valid for its declared /// signature. pub unsafe fn close(&self, tunnel_handle: i32, socket_handle: i32) -> i32 { + // SAFETY: See above unsafe { (self.close_fn)(tunnel_handle, socket_handle) } } @@ -54,6 +56,7 @@ impl WgTcpConnectionFunctions { .len() .try_into() .expect("Cannot receive a buffer larger than 2GiB"); + // SAFETY: See notes for this function unsafe { (self.recv_fn)(tunnel_handle, socket_handle, ptr.cast(), len) } } @@ -66,6 +69,7 @@ impl WgTcpConnectionFunctions { .len() .try_into() .expect("Cannot send a buffer larger than 2GiB"); + // SAFETY: See notes for this function unsafe { (self.send_fn)(tunnel_handle, socket_handle, ptr.cast(), len) } } } @@ -108,9 +112,9 @@ impl IosTcpProvider { let tunnel_handle = self.tunnel_handle; let timeout = self.timeout.as_secs(); let funcs = self.funcs; + // SAFETY: + // The `open_fn` function pointer in `funcs` must be valid. let result = tokio::task::spawn_blocking(move || unsafe { - // SAFETY - // The `open_fn` function pointer in `funcs` must be valid. funcs.open(tunnel_handle, address.as_ptr() as *const _, timeout) }) .await @@ -132,7 +136,7 @@ impl IosTcpProvider { impl Drop for IosTcpConnection { fn drop(&mut self) { - // Safety + // Safety: // `funcs.close_fn` must be a valid function pointer. unsafe { self.funcs.close(self.tunnel_handle, self.socket_handle) }; } @@ -163,7 +167,7 @@ impl AsyncWrite for IosTcpConnection { let data = buf.to_vec(); let funcs = self.funcs; let task = tokio::task::spawn_blocking(move || { - // Safety + // Safety: // `funcs.send_fn` must be a valid function pointer. let result = unsafe { funcs.send(tunnel_handle, socket_handle, data.as_slice()) }; if result < 0 { @@ -227,7 +231,7 @@ impl AsyncRead for IosTcpConnection { let funcs = self.funcs; let mut buffer = vec![0u8; buf.remaining()]; let task = tokio::task::spawn_blocking(move || { - // Safety + // Safety: // `funcs.receive_fn` must be a valid function pointer. let result = unsafe { funcs.receive(tunnel_handle, socket_handle, buffer.as_mut_slice()) }; diff --git a/mullvad-ios/src/ephemeral_peer_proxy/mod.rs b/mullvad-ios/src/ephemeral_peer_proxy/mod.rs index 7318cef649..9942c78f62 100644 --- a/mullvad-ios/src/ephemeral_peer_proxy/mod.rs +++ b/mullvad-ios/src/ephemeral_peer_proxy/mod.rs @@ -17,7 +17,7 @@ pub struct PacketTunnelBridge { impl PacketTunnelBridge { fn fail_exchange(self) { - // # Safety + // # Safety: // Call is safe as long as the `packet_tunnel` pointer is valid. Since a valid instance of // `PacketTunnelBridge` requires the packet tunnel pointer to be valid, it is assumed this // call is safe. @@ -42,7 +42,7 @@ impl PacketTunnelBridge { .as_ref() .map(|params| params as *const _) .unwrap_or(ptr::null()); - // # Safety + // # Safety: // The `packet_tunnel` pointer must be valid, much like the call in `fail_exchange`, but // since the other arguments here are non-null, these pointers (`preshared_ptr`, // `ephmerela_ptr` and `daita_ptr`) have to be valid too. Since they point to local @@ -53,6 +53,7 @@ impl PacketTunnelBridge { } } +// SAFETY: See notes for `EphemeralPeerExchange` unsafe impl Send for PacketTunnelBridge {} #[repr(C)] @@ -85,7 +86,7 @@ impl DaitaParameters { impl Drop for DaitaParameters { fn drop(&mut self) { - // # Safety + // # Safety: // `machines` pointer must be a valid pointer to a CString. This can be achieved by // ensuring that `DaitaParameters` are constructed via `DaitaParameters::new` and the // `machines` pointer is never written to. @@ -122,6 +123,7 @@ extern "C" { pub unsafe extern "C" fn cancel_ephemeral_peer_exchange( sender: *mut peer_exchange::ExchangeCancelToken, ) { + // SAFETY: See notes above let sender = unsafe { Box::from_raw(sender) }; sender.cancel(); } @@ -136,6 +138,7 @@ pub unsafe extern "C" fn cancel_ephemeral_peer_exchange( pub unsafe extern "C" fn drop_ephemeral_peer_exchange_token( sender: *mut peer_exchange::ExchangeCancelToken, ) { + // SAFETY: See notes above // drop the cancel token let _sender = unsafe { Box::from_raw(sender) }; } @@ -162,10 +165,10 @@ pub unsafe extern "C" fn request_ephemeral_peer( .init(); }); - // # Safety + // # Safety: // `public_key` pointer must be a valid pointer to 32 unsigned bytes. let pub_key: [u8; 32] = unsafe { ptr::read(public_key as *const [u8; 32]) }; - // # Safety + // # Safety: // `ephemeral_key` pointer must be a valid pointer to 32 unsigned bytes. let eph_key: [u8; 32] = unsafe { ptr::read(ephemeral_key as *const [u8; 32]) }; diff --git a/mullvad-ios/src/ephemeral_peer_proxy/peer_exchange.rs b/mullvad-ios/src/ephemeral_peer_proxy/peer_exchange.rs index f65ccd0166..56b2407438 100644 --- a/mullvad-ios/src/ephemeral_peer_proxy/peer_exchange.rs +++ b/mullvad-ios/src/ephemeral_peer_proxy/peer_exchange.rs @@ -48,7 +48,7 @@ pub struct EphemeralPeerExchange { peer_parameters: EphemeralPeerParameters, } -// # Safety +// # Safety: // This is safe because the void pointer in PacketTunnelBridge is valid for the lifetime of the // process where this type is intended to be used. unsafe impl Send for EphemeralPeerExchange {} diff --git a/mullvad-ios/src/lib.rs b/mullvad-ios/src/lib.rs index 2fea39f9c7..fa23672e29 100644 --- a/mullvad-ios/src/lib.rs +++ b/mullvad-ios/src/lib.rs @@ -1,6 +1,4 @@ #![cfg(target_os = "ios")] -#![allow(clippy::undocumented_unsafe_blocks)] - mod api_client; mod encrypted_dns_proxy; mod ephemeral_peer_proxy; diff --git a/mullvad-ios/src/shadowsocks_proxy/ffi.rs b/mullvad-ios/src/shadowsocks_proxy/ffi.rs index 229993445e..815862fb52 100644 --- a/mullvad-ios/src/shadowsocks_proxy/ffi.rs +++ b/mullvad-ios/src/shadowsocks_proxy/ffi.rs @@ -1,7 +1,7 @@ use super::{run_forwarding_proxy, ShadowsocksHandle}; +use crate::api_client::helpers::parse_ip_addr; use crate::ProxyHandle; - -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::SocketAddr; #[cfg(any(target_os = "macos", target_os = "ios"))] use std::sync::Once; @@ -35,17 +35,16 @@ pub unsafe extern "C" fn start_shadowsocks_proxy( .init(); }); - let forward_ip = if let Some(forward_address) = - unsafe { parse_ip_addr(forward_address, forward_address_len) } - { - forward_address - } else { - return -1; - }; + let forward_ip = + if let Some(forward_address) = { parse_ip_addr(forward_address, forward_address_len) } { + forward_address + } else { + return -1; + }; let forward_socket_addr = SocketAddr::new(forward_ip, forward_port); - let bridge_ip = if let Some(addr) = unsafe { parse_ip_addr(addr, addr_len) } { + let bridge_ip = if let Some(addr) = { parse_ip_addr(addr, addr_len) } { addr } else { return -1; @@ -53,12 +52,14 @@ pub unsafe extern "C" fn start_shadowsocks_proxy( let bridge_socket_addr = SocketAddr::new(bridge_ip, port); + // SAFETY: See notes for `parse_str` let password = if let Some(password) = unsafe { parse_str(password, password_len) } { password } else { return -1; }; + // SAFETY: See notes for `parse_str` let cipher = if let Some(cipher) = unsafe { parse_str(cipher, cipher_len) } { cipher } else { @@ -75,6 +76,8 @@ pub unsafe extern "C" fn start_shadowsocks_proxy( }; let handle = Box::new(handle); + // SAFETY: `proxy_config` is guaranteed to be writeable for the duration of this call. + // It does not overlap with `handle` unsafe { std::ptr::write( proxy_config, @@ -92,40 +95,19 @@ pub unsafe extern "C" fn start_shadowsocks_proxy( /// `start_shadowsocks_proxy`. #[unsafe(no_mangle)] pub unsafe extern "C" fn stop_shadowsocks_proxy(proxy_config: *mut ProxyHandle) -> i32 { + // SAFETY: `proxy_config` is guaranteed to be a valid pointer let context_ptr = unsafe { (*proxy_config).context }; if context_ptr.is_null() { return -1; } + // SAFETY: `context_ptr` is guaranteed to be a valid, non-null pointer let proxy_handle: Box<ShadowsocksHandle> = unsafe { Box::from_raw(context_ptr as *mut _) }; proxy_handle.stop(); + // SAFETY: `proxy_config` is guaranteed to be a valid pointer unsafe { (*proxy_config).context = std::ptr::null_mut() }; 0 } -/// Constructs a new IP address from a pointer containing bytes representing an IP address. -/// -/// SAFETY: `addr` must be a pointer to at least `addr_len` bytes. -unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<IpAddr> { - match addr_len { - 4 => { - // SAFETY: addr pointer must point to at least addr_len bytes - let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; - Some(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()) - } - 16 => { - // SAFETY: addr pointer must point to at least addr_len bytes - let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; - let mut addr_arr = [0u8; 16]; - addr_arr.as_mut_slice().copy_from_slice(bytes); - - Some(Ipv6Addr::from(addr_arr).into()) - } - anything_else => { - log::error!("Bad IP address length {anything_else}"); - None - } - } -} /// Allocates a new string with the contents of `data` if it contains only valid UTF-8 bytes. /// diff --git a/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs b/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs index d36e0c6ff4..d92ce28a42 100644 --- a/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs +++ b/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs @@ -1,9 +1,8 @@ use super::{TunnelObfuscatorHandle, TunnelObfuscatorRuntime}; use crate::ProxyHandle; -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - sync::Once, -}; +use std::{net::SocketAddr, sync::Once}; + +use crate::api_client::helpers::parse_ip_addr; static INIT_LOGGING: Once = Once::new(); @@ -58,36 +57,17 @@ pub unsafe extern "C" fn start_tunnel_obfuscator_proxy( #[unsafe(no_mangle)] pub unsafe extern "C" fn stop_tunnel_obfuscator_proxy(proxy_handle: *mut ProxyHandle) -> i32 { + // SAFETY: `proxy_config` is guaranteed to be a valid pointer let context_ptr = unsafe { (*proxy_handle).context }; if context_ptr.is_null() { return -1; } + // SAFETY: `context_ptr` is guaranteed to be a valid, non-null pointer let obfuscator_handle: Box<TunnelObfuscatorHandle> = unsafe { Box::from_raw(context_ptr as *mut _) }; obfuscator_handle.stop(); + // SAFETY: `proxy_config` is guaranteed to be a valid pointer unsafe { (*proxy_handle).context = std::ptr::null_mut() }; 0 } - -/// Constructs a new IP address from a pointer containing bytes representing an IP address. -/// -/// SAFETY: `addr` must be a pointer to at least `addr_len` bytes. -unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<IpAddr> { - match addr_len { - 4 => { - // SAFETY: addr pointer must point to at least addr_len bytes - let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; - Some(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()) - } - 16 => { - // SAFETY: addr pointer must point to at least addr_len bytes - let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) }; - let mut addr_arr = [0u8; 16]; - addr_arr.as_mut_slice().copy_from_slice(bytes); - - Some(Ipv6Addr::from(addr_arr).into()) - } - _ => None, - } -} |
