diff options
| author | Emīls <emils@mullvad.net> | 2026-04-23 23:10:39 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2026-04-23 23:13:15 +0200 |
| commit | ad4dc46ac1da2398a5fa954e5efce9f4d6346b75 (patch) | |
| tree | 916d4e72819a9a1fee7e85cec03663ce54b80f67 | |
| parent | e68ffb15280f0ed93cc447da83ee9ee615fb3b21 (diff) | |
| download | mullvadvpn-ad4dc46ac1da2398a5fa954e5efce9f4d6346b75.tar.xz mullvadvpn-ad4dc46ac1da2398a5fa954e5efce9f4d6346b75.zip | |
Further split GotaTun apart from WireGuardGoios-gotatun-switching-improvement
5 files changed, 355 insertions, 190 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 425342cd1d..9c479da4da 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -1184,6 +1184,9 @@ F9E3BCF72DD35B78009986C3 /* ListAccessViewModelBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */; }; F9EDB26C2EC4C0480015DE36 /* CustomListInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9EDB26B2EC4C0420015DE36 /* CustomListInteractorTests.swift */; }; F9EDB26D2EC4C0CD0015DE36 /* CustomListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */; }; + 2F169CDAE6E8C601F85C543E /* TunnelImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24C2F93919B5526A741ADA8 /* TunnelImplementation.swift */; }; + D56CD86C4435C3F6AF2A2662 /* WireGuardGoTunnelImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68970B0A1DEEA01FB25D9E /* WireGuardGoTunnelImplementation.swift */; }; + E79BD7E565693BF2BDA5D9E6 /* GotaTunTunnelImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F678736F5403E23B77CA9990 /* GotaTunTunnelImplementation.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2731,6 +2734,9 @@ F9C579C72E8FE10400C90C50 /* LocationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItemView.swift; sourceTree = "<group>"; }; F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessViewModelBridge.swift; sourceTree = "<group>"; }; F9EDB26B2EC4C0420015DE36 /* CustomListInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListInteractorTests.swift; sourceTree = "<group>"; }; + B24C2F93919B5526A741ADA8 /* TunnelImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelImplementation.swift; sourceTree = "<group>"; }; + DD68970B0A1DEEA01FB25D9E /* WireGuardGoTunnelImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardGoTunnelImplementation.swift; sourceTree = "<group>"; }; + F678736F5403E23B77CA9990 /* GotaTunTunnelImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GotaTunTunnelImplementation.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -4160,6 +4166,7 @@ isa = PBXGroup; children = ( E10A0001000000000000AA01 /* GotaTunActor.swift */, + F678736F5403E23B77CA9990 /* GotaTunTunnelImplementation.swift */, ); path = GotaTunAdapter; sourceTree = "<group>"; @@ -4382,6 +4389,8 @@ 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */, 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, + B24C2F93919B5526A741ADA8 /* TunnelImplementation.swift */, + DD68970B0A1DEEA01FB25D9E /* WireGuardGoTunnelImplementation.swift */, ); path = PacketTunnelProvider; sourceTree = "<group>"; @@ -6986,6 +6995,9 @@ 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */, 06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */, E10A0001000000000000AB01 /* GotaTunActor.swift in Sources */, + 2F169CDAE6E8C601F85C543E /* TunnelImplementation.swift in Sources */, + D56CD86C4435C3F6AF2A2662 /* WireGuardGoTunnelImplementation.swift in Sources */, + E79BD7E565693BF2BDA5D9E6 /* GotaTunTunnelImplementation.swift in Sources */, E10A0002000000000000AB04 /* PacketTunnelDebugSettings.swift in Sources */, 58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */, 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, diff --git a/ios/PacketTunnel/GotaTunAdapter/GotaTunTunnelImplementation.swift b/ios/PacketTunnel/GotaTunAdapter/GotaTunTunnelImplementation.swift new file mode 100644 index 0000000000..ca2e8865a2 --- /dev/null +++ b/ios/PacketTunnel/GotaTunAdapter/GotaTunTunnelImplementation.swift @@ -0,0 +1,53 @@ +// +// GotaTunTunnelImplementation.swift +// PacketTunnel +// +// Created by Mullvad VPN. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import MullvadLogging +import MullvadREST +@preconcurrency import NetworkExtension +import PacketTunnelCore + +/// GotaTun tunnel implementation. +/// Unlike WireGuardGo, this implementation does NOT use an external state observer. +/// State transitions are handled internally by the GotaTun actor. +final class GotaTunTunnelImplementation: TunnelImplementation, @unchecked Sendable { + private let logger = Logger(label: "GotaTunTunnelImplementation") + + private let _actor = GotaTunActor() + var actor: any PacketTunnelActorProtocol { _actor } + + func setUp( + provider: NEPacketTunnelProvider, + internalQueue: DispatchQueue, + ipOverrideWrapper: IPOverrideWrapper, + settingsReader: sending TunnelSettingsManager, + apiTransportProvider: APITransportProvider + ) { + // GotaTun-specific setup will go here when the actor is no longer a stub. + // The actor will internally manage state transitions, path observation, + // and system API calls through its own mechanisms. + } + + func startTunnel(options: StartOptions) async { + // NO startObservingActorState() - this is the key architectural difference. + // The GotaTun actor handles state internally. + actor.start(options: options) + } + + func stopTunnel() async { + actor.stop() + await actor.waitUntilDisconnected() + } + + func sleep() async { + actor.onSleep() + } + + func wake() { + actor.onWake() + } +} diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index fd2ee58de0..9b8ddcb482 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -19,23 +19,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") private let providerLogger: Logger - private var actor: (any PacketTunnelActorProtocol)! + /// The selected tunnel implementation (WireGuardGo or GotaTun). + private var implementation: TunnelImplementation! private var appMessageHandler: AppMessageHandler! - private var stateObserverTask: AnyTask? private var deviceChecker: DeviceChecker! - private var adapter: WgAdapter! - private var relaySelector: RelaySelectorWrapper! - private var ephemeralPeerExchangingPipeline: EphemeralPeerExchangingPipeline! private var newAppVersionSystemNoticationHandler: NewAppVersionSystemNotificationHandler! private let tunnelSettingsUpdater: SettingsUpdater - private let defaultPathObserver: PacketTunnelPathObserver private var migrationManager: MigrationManager let migrationFailureIterator = REST.RetryStrategy.failedMigrationRecovery.makeDelayIterator() private let tunnelSettingsListener = TunnelSettingsListener() - private lazy var ephemeralPeerReceiver = { - EphemeralPeerReceiver(tunnelProvider: adapter, keyReceiver: self) - }() var apiContext: MullvadApiContext! var accessMethodReceiver: MullvadAccessMethodReceiver! @@ -55,8 +48,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { tunnelSettingsUpdater = SettingsUpdater(listener: tunnelSettingsListener) migrationManager = MigrationManager(cacheDirectory: containerURL) - defaultPathObserver = PacketTunnelPathObserver(eventQueue: internalQueue) - super.init() performSettingsMigration() @@ -99,29 +90,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { #if DEBUG if PacketTunnelDebugSettings.useGotaTun { - providerLogger.info("Using GotaTunActor (debug)") - actor = GotaTunActor() + providerLogger.info("Using GotaTun implementation (debug)") + implementation = GotaTunTunnelImplementation() } else { - setUpWireGuardActor( - ipOverrideWrapper: ipOverrideWrapper, - settingsReader: settingsReader, - apiTransportProvider: apiTransportProvider - ) + let wgImpl = WireGuardGoTunnelImplementation() + wgImpl.onDeviceCheck = { [weak self] in self?.startDeviceCheck() } + implementation = wgImpl } #else - setUpWireGuardActor( - ipOverrideWrapper: ipOverrideWrapper, - settingsReader: settingsReader, - apiTransportProvider: apiTransportProvider - ) + let wgImpl = WireGuardGoTunnelImplementation() + wgImpl.onDeviceCheck = { [weak self] in self?.startDeviceCheck() } + implementation = wgImpl #endif + implementation.setUp( + provider: self, + internalQueue: internalQueue, + ipOverrideWrapper: ipOverrideWrapper, + settingsReader: settingsReader, + apiTransportProvider: apiTransportProvider + ) + let apiRequestProxy = APIRequestProxy( dispatchQueue: internalQueue, transportProvider: apiTransportProvider ) appMessageHandler = AppMessageHandler( - packetTunnelActor: actor, + packetTunnelActor: implementation.actor, apiRequestProxy: apiRequestProxy ) @@ -142,8 +137,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) { let startOptions = parseStartOptions(options ?? [:]) - startObservingActorState() - // Run device check during tunnel startup. // This check is allowed to push new key to server if there are some issues with it. startDeviceCheck(rotateKeyOnMismatch: true) @@ -157,8 +150,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { "Failed to configure tunnel with initial config: \(error)" ) } else { - self.providerLogger.debug("Starting actor after initial configuration is applied") - self.actor.start(options: startOptions) + self.providerLogger.debug("Starting tunnel implementation after initial configuration is applied") + Task { await self.implementation.startTunnel(options: startOptions) } } self.internalQueue.async { completionHandler(error) @@ -169,10 +162,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { override func stopTunnel(with reason: NEProviderStopReason) async { providerLogger.debug("stopTunnel: \(ProviderStopReasonWrapper(reason: reason))") - actor.stop() - await actor.waitUntilDisconnected() - - stopObservingActorState() + await implementation.stopTunnel() } override func handleAppMessage(_ messageData: Data) async -> Data? { @@ -180,11 +170,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } override func sleep() async { - actor.onSleep() + await implementation.sleep() } override func wake() { - actor.onWake() + implementation.wake() } private func performSettingsMigration() { @@ -266,60 +256,6 @@ 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)" @@ -385,76 +321,6 @@ extension PacketTunnelProvider { } } -// MARK: - Network path monitor observing - -extension PacketTunnelProvider { - - private func startDefaultPathObserver() { - providerLogger.trace("Start default path observer.") - - defaultPathObserver.start { [weak self] networkPath in - self?.actor.updateNetworkReachability(networkPathStatus: networkPath) - } - } - - private func stopDefaultPathObserver() { - providerLogger.trace("Stop default path observer.") - - defaultPathObserver.stop() - } -} - -// MARK: - State observer - -extension PacketTunnelProvider { - private func startObservingActorState() { - stopObservingActorState() - - stateObserverTask = Task { - let stateStream = await self.actor.observedStates - var lastConnectionAttempt: UInt = 0 - - for await newState in stateStream { - if case .connected = newState { - // Instead of setting the `reasserting` flag to true when we lose connectivity, we can wait until we restore connectivity. This will decrease the amount of path updates that are issued. There's also no need to signal to the system that anything is broken - instead we just want to invalidate old sockets when a new relay connection is up - the only way to do that is to toggle this flag. - self.reasserting = true - self.reasserting = false - } - - switch newState { - case let .reconnecting(observedConnectionState), let .connecting(observedConnectionState): - let connectionAttempt = observedConnectionState.connectionAttemptCount - - // Start device check every second failure attempt to connect. - if lastConnectionAttempt != connectionAttempt, connectionAttempt > 0, - connectionAttempt.isMultiple(of: 2) - { - startDeviceCheck() - } - - // Cache last connection attempt to filter out repeating calls. - lastConnectionAttempt = connectionAttempt - - case let .negotiatingEphemeralPeer(observedConnectionState, privateKey): - await ephemeralPeerExchangingPipeline.startNegotiation( - observedConnectionState, - privateKey: privateKey - ) - case .disconnected: - stopDefaultPathObserver() - case .initial, .connected, .disconnecting, .error: - break - } - } - } - } - - private func stopObservingActorState() { - stateObserverTask?.cancel() - stateObserverTask = nil - } -} - // MARK: - Device check extension PacketTunnelProvider { @@ -472,7 +338,7 @@ extension PacketTunnelProvider { switch error { case is DeviceCheckError: providerLogger.error("\(error.description) Forcing a log out") - actor.setErrorState(reason: .deviceLoggedOut) + implementation.actor.setErrorState(reason: .deviceLoggedOut) default: providerLogger .error( @@ -483,13 +349,13 @@ extension PacketTunnelProvider { case let .success(keyRotationResult): if let blockedStateReason = keyRotationResult.blockedStateReason { providerLogger.error("Entering blocked state after unsuccessful device check: \(blockedStateReason)") - actor.setErrorState(reason: blockedStateReason) + implementation.actor.setErrorState(reason: blockedStateReason) return } switch keyRotationResult.keyRotationStatus { case let .attempted(date), let .succeeded(date): - actor.notifyKeyRotation(date: date) + implementation.actor.notifyKeyRotation(date: date) case .noAction: break } @@ -497,32 +363,3 @@ extension PacketTunnelProvider { } } -extension PacketTunnelProvider: EphemeralPeerReceiving { - func receivePostQuantumKey( - _ key: WireGuard.PreSharedKey, - ephemeralKey: WireGuard.PrivateKey, - daitaParameters: MullvadTypes.DaitaV2Parameters? - ) async { - await ephemeralPeerExchangingPipeline.receivePostQuantumKey( - key, - ephemeralKey: ephemeralKey, - daitaParameters: daitaParameters - ) - } - - public func receiveEphemeralPeerPrivateKey( - _ ephemeralPeerPrivateKey: WireGuard.PrivateKey, - daitaParameters: MullvadTypes.DaitaV2Parameters? - ) async { - await ephemeralPeerExchangingPipeline.receiveEphemeralPeerPrivateKey( - ephemeralPeerPrivateKey, - daitaParameters: daitaParameters - ) - } - - func ephemeralPeerExchangeFailed() { - // Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail - // and it will not try to reconnect - actor.reconnect(to: .random, reconnectReason: .connectionLoss) - } -} diff --git a/ios/PacketTunnel/PacketTunnelProvider/TunnelImplementation.swift b/ios/PacketTunnel/PacketTunnelProvider/TunnelImplementation.swift new file mode 100644 index 0000000000..aebb52af6d --- /dev/null +++ b/ios/PacketTunnel/PacketTunnelProvider/TunnelImplementation.swift @@ -0,0 +1,41 @@ +// +// TunnelImplementation.swift +// PacketTunnel +// +// Created by Mullvad VPN. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +@preconcurrency import NetworkExtension +import PacketTunnelCore + +/// Protocol for tunnel backend implementations. +/// The picker (`PacketTunnelProvider`) delegates all lifecycle events to the active implementation. +protocol TunnelImplementation: AnyObject { + /// The underlying actor that handles tunnel state. + var actor: any PacketTunnelActorProtocol { get } + + /// Called once after init to wire up dependencies. + /// The `provider` reference is the real `NEPacketTunnelProvider` that iOS instantiated, + /// needed for system calls like `reasserting` and `setTunnelNetworkSettings`. + func setUp( + provider: NEPacketTunnelProvider, + internalQueue: DispatchQueue, + ipOverrideWrapper: IPOverrideWrapper, + settingsReader: sending TunnelSettingsManager, + apiTransportProvider: APITransportProvider + ) + + /// Called when the tunnel is starting, after initial network settings have been applied. + func startTunnel(options: StartOptions) async + + /// Called when the tunnel is stopping. Must wait until disconnected before returning. + func stopTunnel() async + + /// Called when the device is going to sleep. + func sleep() async + + /// Called when the device wakes up. + func wake() +} diff --git a/ios/PacketTunnel/PacketTunnelProvider/WireGuardGoTunnelImplementation.swift b/ios/PacketTunnel/PacketTunnelProvider/WireGuardGoTunnelImplementation.swift new file mode 100644 index 0000000000..21814c1a27 --- /dev/null +++ b/ios/PacketTunnel/PacketTunnelProvider/WireGuardGoTunnelImplementation.swift @@ -0,0 +1,222 @@ +// +// WireGuardGoTunnelImplementation.swift +// PacketTunnel +// +// Created by Mullvad VPN. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadREST +import MullvadRustRuntime +import MullvadSettings +import MullvadTypes +@preconcurrency import NetworkExtension +import PacketTunnelCore + +/// WireGuardGo tunnel implementation. +/// Owns the WgAdapter, TunnelMonitor, state observer, path observer, +/// and ephemeral peer exchange pipeline. +final class WireGuardGoTunnelImplementation: TunnelImplementation, @unchecked Sendable { + private let logger = Logger(label: "WireGuardGoTunnelImplementation") + private weak var provider: NEPacketTunnelProvider? + + // WG-specific infrastructure + private var adapter: WgAdapter! + private var relaySelector: RelaySelectorWrapper! + private var ephemeralPeerExchangingPipeline: EphemeralPeerExchangingPipeline! + private var stateObserverTask: AnyTask? + private var defaultPathObserver: PacketTunnelPathObserver! + private lazy var ephemeralPeerReceiver: EphemeralPeerReceiver = { + EphemeralPeerReceiver(tunnelProvider: adapter, keyReceiver: self) + }() + + private var _actor: PacketTunnelActor! + var actor: any PacketTunnelActorProtocol { _actor } + + /// Callback to trigger a device check from the picker (shared infrastructure). + var onDeviceCheck: (() -> Void)? + + func setUp( + provider: NEPacketTunnelProvider, + internalQueue: DispatchQueue, + ipOverrideWrapper: IPOverrideWrapper, + settingsReader: sending TunnelSettingsManager, + apiTransportProvider: APITransportProvider + ) { + self.provider = provider + + defaultPathObserver = PacketTunnelPathObserver(eventQueue: internalQueue) + + adapter = WgAdapter(packetTunnelProvider: provider) + + 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() + } + ) + } + + // MARK: - Lifecycle + + func startTunnel(options: StartOptions) async { + startObservingActorState() + actor.start(options: options) + } + + func stopTunnel() async { + actor.stop() + await actor.waitUntilDisconnected() + stopObservingActorState() + } + + func sleep() async { + actor.onSleep() + } + + func wake() { + actor.onWake() + } +} + +// MARK: - State observer + +extension WireGuardGoTunnelImplementation { + private func startObservingActorState() { + stopObservingActorState() + + stateObserverTask = Task { + let stateStream = await self._actor.observedStates + var lastConnectionAttempt: UInt = 0 + + for await newState in stateStream { + if case .connected = newState { + // Toggle reasserting to invalidate old sockets when a new relay connection is up. + self.provider?.reasserting = true + self.provider?.reasserting = false + } + + switch newState { + case let .reconnecting(observedConnectionState), let .connecting(observedConnectionState): + let connectionAttempt = observedConnectionState.connectionAttemptCount + + // Start device check every second failure attempt to connect. + if lastConnectionAttempt != connectionAttempt, connectionAttempt > 0, + connectionAttempt.isMultiple(of: 2) + { + onDeviceCheck?() + } + + // Cache last connection attempt to filter out repeating calls. + lastConnectionAttempt = connectionAttempt + + case let .negotiatingEphemeralPeer(observedConnectionState, privateKey): + await ephemeralPeerExchangingPipeline.startNegotiation( + observedConnectionState, + privateKey: privateKey + ) + case .disconnected: + stopDefaultPathObserver() + case .initial, .connected, .disconnecting, .error: + break + } + } + } + } + + private func stopObservingActorState() { + stateObserverTask?.cancel() + stateObserverTask = nil + } +} + +// MARK: - Network path monitor observing + +extension WireGuardGoTunnelImplementation { + private func startDefaultPathObserver() { + logger.trace("Start default path observer.") + + defaultPathObserver.start { [weak self] networkPath in + self?._actor.updateNetworkReachability(networkPathStatus: networkPath) + } + } + + private func stopDefaultPathObserver() { + logger.trace("Stop default path observer.") + + defaultPathObserver.stop() + } +} + +// MARK: - EphemeralPeerReceiving + +extension WireGuardGoTunnelImplementation: EphemeralPeerReceiving { + func receivePostQuantumKey( + _ key: WireGuard.PreSharedKey, + ephemeralKey: WireGuard.PrivateKey, + daitaParameters: MullvadTypes.DaitaV2Parameters? + ) async { + await ephemeralPeerExchangingPipeline.receivePostQuantumKey( + key, + ephemeralKey: ephemeralKey, + daitaParameters: daitaParameters + ) + } + + public func receiveEphemeralPeerPrivateKey( + _ ephemeralPeerPrivateKey: WireGuard.PrivateKey, + daitaParameters: MullvadTypes.DaitaV2Parameters? + ) async { + await ephemeralPeerExchangingPipeline.receiveEphemeralPeerPrivateKey( + ephemeralPeerPrivateKey, + daitaParameters: daitaParameters + ) + } + + func ephemeralPeerExchangeFailed() { + // Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail + // and it will not try to reconnect + actor.reconnect(to: .random, reconnectReason: .connectionLoss) + } +} |
