diff options
| author | Emīls <emils@mullvad.net> | 2026-04-09 21:38:07 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2026-04-09 21:38:07 +0200 |
| commit | ffe05e6dfcc78d619fb63de24f19a681118ffc6d (patch) | |
| tree | eb9075c989fe2878ac9f8367655c3bd70b0b5625 | |
| parent | 71727c995248d09f6f18f6b28091f3786f2c6902 (diff) | |
| parent | b124b0b9b71d62dcf25f25bf7e0d42932cfca39a (diff) | |
| download | mullvadvpn-ffe05e6dfcc78d619fb63de24f19a681118ffc6d.tar.xz mullvadvpn-ffe05e6dfcc78d619fb63de24f19a681118ffc6d.zip | |
Merge branch 'add-skeleton-class-to-have-2-separate-implementations-of-the-ios-1534'
7 files changed, 280 insertions, 47 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ce42041058..719e918ca2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + E10A0001000000000000AB01 /* GotaTunActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0001000000000000AA01 /* GotaTunActor.swift */; }; + E10A0002000000000000AB01 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; }; + E10A0002000000000000AB02 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; }; + E10A0002000000000000AB03 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; }; + E10A0002000000000000AB04 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; }; + E10A0002000000000000AB05 /* PacketTunnelDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */; }; 0107F40B2F5B02580012451B /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 0107F40A2F5B02580012451B /* WireGuardKitTypes */; }; 0107F40D2F5B02840012451B /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; 0107F4142F5B02D70012451B /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; }; @@ -1713,6 +1719,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + E10A0001000000000000AA01 /* GotaTunActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GotaTunActor.swift; sourceTree = "<group>"; }; + E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelDebugSettings.swift; sourceTree = "<group>"; }; 0107F3F82F56E3ED0012451B /* RelayCacheTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTrackerTests.swift; sourceTree = "<group>"; }; 0107F4212F5B97F30012451B /* RelayListCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayListCacheTests.swift; sourceTree = "<group>"; }; 014E8C2F2F294FB000837D0A /* relays-test-data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "relays-test-data.json"; sourceTree = "<group>"; }; @@ -3840,6 +3848,7 @@ 58C76A072A33850E00100D75 /* ApplicationTarget.swift */, F0EF2B762F4C801D00C7ECA7 /* AppResetManager.swift */, F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */, + E10A0002000000000000AA01 /* PacketTunnelDebugSettings.swift */, F0AF894A2F55A77800DE9740 /* UITestSettingsResetPolicy.swift */, ); path = Shared; @@ -4137,11 +4146,20 @@ path = MullvadVPN; sourceTree = "<group>"; }; + E10A0001000000000000AC01 /* GotaTunAdapter */ = { + isa = PBXGroup; + children = ( + E10A0001000000000000AA01 /* GotaTunActor.swift */, + ); + path = GotaTunAdapter; + sourceTree = "<group>"; + }; 58CE5E7A224146470008646E /* PacketTunnel */ = { isa = PBXGroup; children = ( F09D616A2F5AD5E200C85C9E /* Localizable.xcstrings */, 58915D662A25F9F20066445B /* DeviceCheck */, + E10A0001000000000000AC01 /* GotaTunAdapter */, 7A9ED2A82F3CC057005BC0D9 /* Notifications */, 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */, F059197A2C45404500C301F3 /* PostQuantum */, @@ -6304,6 +6322,7 @@ A9C7B62C2EB9F71D002CABB1 /* LoggerBuilderTests.swift in Sources */, A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */, F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */, + E10A0002000000000000AB01 /* PacketTunnelDebugSettings.swift in Sources */, 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */, A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */, ); @@ -6321,6 +6340,7 @@ F0E61CAA2BF2911D000C4A95 /* TunnelSettingsV5.swift in Sources */, 7A5869BD2B56EF7300640D27 /* IPOverride.swift in Sources */, 58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */, + E10A0002000000000000AB02 /* PacketTunnelDebugSettings.swift in Sources */, F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */, 58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */, A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */, @@ -6465,6 +6485,7 @@ 586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */, 7A5468AC2C6A55B100590086 /* LocationRelays.swift in Sources */, 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, + E10A0002000000000000AB03 /* PacketTunnelDebugSettings.swift in Sources */, 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 58C76A0B2A338E4300100D75 /* BackgroundTask.swift in Sources */, @@ -6942,6 +6963,8 @@ 58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */, 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */, 06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */, + E10A0001000000000000AB01 /* GotaTunActor.swift in Sources */, + E10A0002000000000000AB04 /* PacketTunnelDebugSettings.swift in Sources */, 58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */, 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */, @@ -7033,6 +7056,7 @@ buildActionMask = 2147483647; files = ( 7AED35CC2BD13F60002A67D1 /* ApplicationConfiguration.swift in Sources */, + E10A0002000000000000AB05 /* PacketTunnelDebugSettings.swift in Sources */, 7AED35CD2BD13FC4002A67D1 /* ApplicationTarget.swift in Sources */, 58D22402294C90050029F5F8 /* Logger+Errors.swift in Sources */, 58D22404294C90050029F5F8 /* Date+LogFormat.swift in Sources */, diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index cc732d7d32..8040e064b8 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -277,6 +277,20 @@ class AccountViewController: UIViewController, @unchecked Sendable { ) ) + #if DEBUG + let gotaTunEnabled = PacketTunnelDebugSettings.useGotaTun + sheetController.addAction( + UIAlertAction( + title: "Use GotaTun: \(gotaTunEnabled ? "ON" : "OFF")", + style: .default, + handler: { [weak self] _ in + PacketTunnelDebugSettings.useGotaTun = !gotaTunEnabled + self?.interactor.tunnelManager.reapplyTunnelConfiguration() + } + ) + ) + #endif + sheetController.addAction( UIAlertAction( title: "Cancel", diff --git a/ios/PacketTunnel/GotaTunAdapter/GotaTunActor.swift b/ios/PacketTunnel/GotaTunAdapter/GotaTunActor.swift new file mode 100644 index 0000000000..7b90e62816 --- /dev/null +++ b/ios/PacketTunnel/GotaTunAdapter/GotaTunActor.swift @@ -0,0 +1,84 @@ +// +// GotaTunActor.swift +// PacketTunnel +// +// Created by Mullvad VPN. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadTypes +import Network +import PacketTunnelCore + +/// Stub actor for GotaTun tunnel implementation. +/// Implements `PacketTunnelActorProtocol` with no-op methods — the real +/// GotaTun logic will be filled in later. +final class GotaTunActor: PacketTunnelActorProtocol, @unchecked Sendable { + private let logger = Logger(label: "GotaTunActor") + + var observedState: ObservedState { + get async { .disconnected } + } + + var observedStates: AsyncStream<ObservedState> { + get async { + AsyncStream { continuation in + continuation.yield(.disconnected) + continuation.finish() + } + } + } + + init() { + logger.info("GotaTunActor initialized (stub)") + } + + func start(options: StartOptions) { + logger.info("start called (no-op)") + } + + func stop() { + logger.info("stop called (no-op)") + } + + func waitUntilDisconnected() async { + logger.info("waitUntilDisconnected called (no-op)") + } + + func onSleep() { + logger.info("onSleep called (no-op)") + } + + func onWake() { + logger.info("onWake called (no-op)") + } + + func updateNetworkReachability(networkPathStatus: NWPath.Status) { + logger.info("updateNetworkReachability called (no-op)") + } + + func reconnect(to nextRelays: NextRelays, reconnectReason: ActorReconnectReason) { + logger.info("reconnect called (no-op)") + } + + func notifyKeyRotation(date: Date?) { + logger.info("notifyKeyRotation called (no-op)") + } + + func setErrorState(reason: BlockedStateReason) { + logger.info("setErrorState called (no-op)") + } + + func notifyEphemeralPeerNegotiated() { + logger.info("notifyEphemeralPeerNegotiated called (no-op)") + } + + func changeEphemeralPeerNegotiationState( + configuration: EphemeralPeerNegotiationState, + reconfigurationSemaphore: OneshotChannel + ) { + logger.info("changeEphemeralPeerNegotiationState called (no-op)") + } +} diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index b49c3521ba..adda7fb7d9 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -20,7 +20,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") private let providerLogger: Logger - private var actor: PacketTunnelActor! + private var actor: (any PacketTunnelActorProtocol)! private var appMessageHandler: AppMessageHandler! private var stateObserverTask: AnyTask? private var deviceChecker: DeviceChecker! @@ -90,17 +90,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) ) - adapter = WgAdapter(packetTunnelProvider: self) - - let pinger = TunnelPinger(pingProvider: adapter.icmpPingProvider, replyQueue: internalQueue) - - let tunnelMonitor = TunnelMonitor( - eventQueue: internalQueue, - pinger: pinger, - tunnelDeviceInfo: adapter, - timings: TunnelMonitorTimings() - ) - let proxyFactory = REST.ProxyFactory.makeProxyFactory( apiTransportProvider: apiTransportProvider ) @@ -108,23 +97,25 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { let devicesProxy = proxyFactory.createDevicesProxy() deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy) - relaySelector = RelaySelectorWrapper( - relayCache: ipOverrideWrapper - ) - actor = PacketTunnelActor( - timings: PacketTunnelActorTimings(), - tunnelAdapter: adapter, - tunnelMonitor: tunnelMonitor, - defaultPathObserver: defaultPathObserver, - blockedStateErrorMapper: BlockedStateErrorMapper(), - relaySelector: relaySelector, - settingsReader: settingsReader, - protocolObfuscator: ProtocolObfuscator<TunnelObfuscator>() - ) - - // Since PacketTunnelActor depends on the path observer, start observing after actor has been initalized. - startDefaultPathObserver() + #if DEBUG + if PacketTunnelDebugSettings.useGotaTun { + providerLogger.info("Using GotaTunActor (debug)") + actor = GotaTunActor() + } else { + setUpWireGuardActor( + ipOverrideWrapper: ipOverrideWrapper, + settingsReader: settingsReader, + apiTransportProvider: apiTransportProvider + ) + } + #else + setUpWireGuardActor( + ipOverrideWrapper: ipOverrideWrapper, + settingsReader: settingsReader, + apiTransportProvider: apiTransportProvider + ) + #endif let apiRequestProxy = APIRequestProxy( dispatchQueue: internalQueue, @@ -135,25 +126,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { apiRequestProxy: apiRequestProxy ) - ephemeralPeerExchangingPipeline = EphemeralPeerExchangingPipeline( - EphemeralPeerExchangeActor( - packetTunnel: ephemeralPeerReceiver, - onFailure: self.ephemeralPeerExchangeFailed, - iteratorProvider: { REST.RetryStrategy.postQuantumKeyExchange.makeDelayIterator() } - ), - onUpdateConfiguration: { [unowned self] configuration in - let channel = OneshotChannel() - actor.changeEphemeralPeerNegotiationState( - configuration: configuration, - reconfigurationSemaphore: channel - ) - await channel.receive() - }, - onFinish: { [unowned self] in - actor.notifyEphemeralPeerNegotiated() - } - ) - newAppVersionSystemNoticationHandler = NewAppVersionSystemNotificationHandler( appVersionService: AppVersionService( urlSession: URLSession.shared, @@ -295,6 +267,60 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) } + private func setUpWireGuardActor( + ipOverrideWrapper: IPOverrideWrapper, + settingsReader: sending TunnelSettingsManager, + apiTransportProvider: APITransportProvider + ) { + adapter = WgAdapter(packetTunnelProvider: self) + + let pinger = TunnelPinger(pingProvider: adapter.icmpPingProvider, replyQueue: internalQueue) + + let tunnelMonitor = TunnelMonitor( + eventQueue: internalQueue, + pinger: pinger, + tunnelDeviceInfo: adapter, + timings: TunnelMonitorTimings() + ) + + relaySelector = RelaySelectorWrapper( + relayCache: ipOverrideWrapper + ) + + actor = PacketTunnelActor( + timings: PacketTunnelActorTimings(), + tunnelAdapter: adapter, + tunnelMonitor: tunnelMonitor, + defaultPathObserver: defaultPathObserver, + blockedStateErrorMapper: BlockedStateErrorMapper(), + relaySelector: relaySelector, + settingsReader: settingsReader, + protocolObfuscator: ProtocolObfuscator<TunnelObfuscator>() + ) + + // Since PacketTunnelActor depends on the path observer, start observing after actor has been initalized. + startDefaultPathObserver() + + ephemeralPeerExchangingPipeline = EphemeralPeerExchangingPipeline( + EphemeralPeerExchangeActor( + packetTunnel: ephemeralPeerReceiver, + onFailure: self.ephemeralPeerExchangeFailed, + iteratorProvider: { REST.RetryStrategy.postQuantumKeyExchange.makeDelayIterator() } + ), + onUpdateConfiguration: { [unowned self] configuration in + let channel = OneshotChannel() + actor.changeEphemeralPeerNegotiationState( + configuration: configuration, + reconfigurationSemaphore: channel + ) + await channel.receive() + }, + onFinish: { [unowned self] in + actor.notifyEphemeralPeerNegotiated() + } + ) + } + private func initialTunnelNetworkSettings() -> NETunnelNetworkSettings { let settings = NEPacketTunnelNetworkSettings( tunnelRemoteAddress: "\(IPv4Address.loopback)" diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift index 934b1be621..449d8e238c 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift @@ -7,10 +7,37 @@ // import Foundation +import MullvadTypes +import Network public protocol PacketTunnelActorProtocol { + // State observation var observedState: ObservedState { get async } + var observedStates: AsyncStream<ObservedState> { get async } + // Lifecycle + func start(options: StartOptions) + func stop() + func waitUntilDisconnected() async + + // Sleep cycle + func onSleep() + func onWake() + + // Network + func updateNetworkReachability(networkPathStatus: NWPath.Status) + + // Reconnection & key rotation func reconnect(to nextRelays: NextRelays, reconnectReason: ActorReconnectReason) func notifyKeyRotation(date: Date?) + + // Error state + func setErrorState(reason: BlockedStateReason) + + // Ephemeral peer negotiation + func notifyEphemeralPeerNegotiated() + func changeEphemeralPeerNegotiationState( + configuration: EphemeralPeerNegotiationState, + reconfigurationSemaphore: OneshotChannel + ) } diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift index 523acb6acc..2d83536912 100644 --- a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift @@ -7,10 +7,35 @@ // import Foundation +import MullvadTypes +import Network import PacketTunnelCore import XCTest struct PacketTunnelActorStub: PacketTunnelActorProtocol { + var observedStates: AsyncStream<ObservedState> { + get async { + AsyncStream { continuation in + continuation.yield(innerState) + continuation.finish() + } + } + } + + func start(options: StartOptions) {} + func stop() {} + func waitUntilDisconnected() async {} + func onSleep() {} + func onWake() {} + func updateNetworkReachability(networkPathStatus: NWPath.Status) {} + func setErrorState(reason: BlockedStateReason) {} + func notifyEphemeralPeerNegotiated() {} + + func changeEphemeralPeerNegotiationState( + configuration: EphemeralPeerNegotiationState, + reconfigurationSemaphore: OneshotChannel + ) {} + let innerState: ObservedState = .disconnected var stateExpectation: XCTestExpectation? var reconnectExpectation: XCTestExpectation? diff --git a/ios/Shared/PacketTunnelDebugSettings.swift b/ios/Shared/PacketTunnelDebugSettings.swift new file mode 100644 index 0000000000..8aa1a7ba60 --- /dev/null +++ b/ios/Shared/PacketTunnelDebugSettings.swift @@ -0,0 +1,33 @@ +// +// PacketTunnelDebugSettings.swift +// MullvadVPN +// +// Created by Mullvad VPN. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +#if DEBUG + /// Debug settings for switching between packet tunnel implementations. + /// Stored in the shared App Group UserDefaults so both the main app and the + /// packet tunnel extension can access them. + enum PacketTunnelDebugSettings { + private static let useGotaTunKey = "PacketTunnelDebugSettings.useGotaTun" + + private static var sharedDefaults: UserDefaults? { + UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier) + } + + /// Whether the GotaTun adapter should be used instead of WireGuard. + /// Defaults to `false` if the shared container is unavailable. + static var useGotaTun: Bool { + get { + sharedDefaults?.bool(forKey: useGotaTunKey) ?? false + } + set { + sharedDefaults?.set(newValue, forKey: useGotaTunKey) + } + } + } +#endif |
