diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2024-12-09 16:00:34 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-12-09 16:42:48 +0100 |
| commit | d71e66a93dde36afb192bcf24e40c70596d85e2f (patch) | |
| tree | a414d1c664125f9d8a19a484928b97a2ddfed575 | |
| parent | edaf51e5a559112b9547359717a02fc80de1c1f9 (diff) | |
| download | mullvadvpn-dynamic-include-all-networks.tar.xz mullvadvpn-dynamic-include-all-networks.zip | |
Add dynamic toggles for IANdynamic-include-all-networks
9 files changed, 284 insertions, 9 deletions
diff --git a/ios/MullvadTypes/Protocols/EphemeralPeerReceiver.swift b/ios/MullvadTypes/Protocols/EphemeralPeerReceiver.swift index 6f208bc869..bd60aebc62 100644 --- a/ios/MullvadTypes/Protocols/EphemeralPeerReceiver.swift +++ b/ios/MullvadTypes/Protocols/EphemeralPeerReceiver.swift @@ -22,7 +22,7 @@ public class EphemeralPeerReceiver: EphemeralPeerReceiving, TunnelProvider { unowned let tunnelProvider: any TunnelProvider let keyReceiver: any EphemeralPeerReceiving - public init(tunnelProvider: TunnelProvider, keyReceiver: any EphemeralPeerReceiving ) { + public init(tunnelProvider: TunnelProvider, keyReceiver: any EphemeralPeerReceiving) { self.tunnelProvider = tunnelProvider self.keyReceiver = keyReceiver } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index fd58a53daf..11284db6fe 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -850,6 +850,8 @@ A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */; }; A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */; }; A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */; }; + A9B97ECD2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B97ECC2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift */; }; + A9B97ECE2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B97ECC2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift */; }; A9BA08312BA32FA9005A7A2D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */; }; A9BA08322BA32FB6005A7A2D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */; }; A9BD4D552CA58C3700C8A0E6 /* RESTTransportStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */; }; @@ -2119,6 +2121,7 @@ A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; }; A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = "<group>"; }; A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemorySettingsStore.swift; sourceTree = "<group>"; }; + A9B97ECC2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTunnelConfigurationOperation.swift; sourceTree = "<group>"; }; A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsTests.swift; sourceTree = "<group>"; }; A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListPage.swift; sourceTree = "<group>"; }; A9C308392C19DDA7008715F1 /* MullvadPostQuantum+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadPostQuantum+Stubs.swift"; sourceTree = "<group>"; }; @@ -2780,6 +2783,7 @@ 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */, 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */, + A9B97ECC2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift */, ); path = TunnelManager; sourceTree = "<group>"; @@ -5589,6 +5593,7 @@ A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */, A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */, A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */, + A9B97ECE2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift in Sources */, A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */, 7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */, A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */, @@ -6083,6 +6088,7 @@ 7AF10EB22ADE859200C090B9 /* AlertViewController.swift in Sources */, 587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */, F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */, + A9B97ECD2D072CB70001A3ED /* UpdateTunnelConfigurationOperation.swift in Sources */, 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, 58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, @@ -7300,6 +7306,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -7335,6 +7342,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -7978,6 +7986,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -8808,6 +8817,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 6a32e1192f..6871c024f9 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -115,8 +115,12 @@ class StartTunnelOperation: ResultOperation<Void> { let protocolConfig = NETunnelProviderProtocol() protocolConfig.providerBundleIdentifier = ApplicationTarget.packetTunnel.bundleIdentifier protocolConfig.serverAddress = "" - protocolConfig.includeAllNetworks = true - protocolConfig.excludeLocalNetworks = false + + let includeAllNetworks = UserDefaults.standard.bool(forKey: "includeAllNetworks") + let excludeLocalNetworks = UserDefaults.standard.bool(forKey: "excludeLocalNetworks") + + protocolConfig.includeAllNetworks = includeAllNetworks + protocolConfig.excludeLocalNetworks = excludeLocalNetworks let alwaysOnRule = NEOnDemandRuleConnect() alwaysOnRule.interfaceTypeMatch = .any diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 9489998ef6..f7cd642172 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -41,6 +41,7 @@ protocol TunnelProtocol: AnyObject { func saveToPreferences(_ completion: @escaping (Error?) -> Void) func removeFromPreferences(completion: @escaping (Error?) -> Void) + func updatePreferences(_ completion: @escaping (Error?) -> Void) func setConfiguration(_ configuration: TunnelConfiguration) func start(options: [String: NSObject]?) throws @@ -148,6 +149,16 @@ final class Tunnel: TunnelProtocol, Equatable { configuration.apply(to: tunnelProvider) } + func updatePreferences(_ completion: @escaping (Error?) -> Void) { + tunnelProvider.loadFromPreferences { [weak self] error in + if let error { + completion(error) + } else { + self?.tunnelProvider.saveToPreferences(completionHandler: completion) + } + } + } + func saveToPreferences(_ completion: @escaping (Error?) -> Void) { tunnelProvider.saveToPreferences { error in if let error { diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 4703fe3cda..b8acbb3870 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -217,6 +217,42 @@ final class TunnelManager: StorePaymentObserver { operationQueue.addOperation(loadTunnelOperation) } + func updateTunnel(completionHandler: ((Error?) -> Void)? = nil) { + let operation = UpdateTunnelConfigurationOperation( + interactor: TunnelInteractorProxy(self), + dispatchQueue: internalQueue, + completionHandler: { [weak self] result in + guard let self else { return } + + DispatchQueue.main.async { + if let error = result.error { + self.logger.error( + error: error, + message: "Failed to Update the tunnel." + ) + + let tunnelError = StartTunnelError(underlyingError: error) + + self.observerList.notify { observer in + observer.tunnelManager(self, didFailWithError: tunnelError) + } + } + + completionHandler?(result.error) + } + } + ) + + operation.addObserver(BackgroundObserver( + backgroundTaskProvider: backgroundTaskProvider, + name: "Update tunnel Configuration", + cancelUponExpiration: true + )) + operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnel.category)) + + operationQueue.addOperation(operation) + } + func startTunnel(completionHandler: ((Error?) -> Void)? = nil) { let operation = StartTunnelOperation( dispatchQueue: internalQueue, diff --git a/ios/MullvadVPN/TunnelManager/UpdateTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateTunnelConfigurationOperation.swift new file mode 100644 index 0000000000..9d43291ebd --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/UpdateTunnelConfigurationOperation.swift @@ -0,0 +1,122 @@ +// +// UpdateTunnelConfigurationOperation.swift +// MullvadVPN +// +// Created by Marco Nikic on 2024-12-09. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import NetworkExtension +import Operations +import PacketTunnelCore + +class UpdateTunnelConfigurationOperation: ResultOperation<Void> { + typealias EncodeErrorHandler = (Error) -> Void + + private let logger = Logger(label: "UpdateTunnelOperation") + private let interactor: TunnelInteractor + + init(interactor: TunnelInteractor, dispatchQueue: DispatchQueue, completionHandler: @escaping CompletionHandler) { + self.interactor = interactor + + super.init(dispatchQueue: dispatchQueue, completionQueue: dispatchQueue, completionHandler: completionHandler) + } + + override func main() { + guard case .loggedIn = interactor.deviceState else { + finish(result: .failure(InvalidDeviceStateError())) + return + } + + switch interactor.tunnelStatus.state { + case .disconnected, .disconnecting: + finish(result: .success(())) + + case .connected, .error, .waitingForConnectivity, .connecting, .reconnecting, .negotiatingEphemeralPeer, + .pendingReconnect: + makeTunnelProviderAndUpdateTunnel { error in + self.finish(result: error.map { .failure($0) } ?? .success(())) + } + } + } + + private func makeTunnelProviderAndUpdateTunnel(completionHandler: @escaping (Error?) -> Void) { + updateTunnelProvider { result in + self.dispatchQueue.async { + do { + try self.updateTunnel(tunnel: result.get()) + } catch { + completionHandler(error) + } + } + } + } + + private func updateTunnel(tunnel: any TunnelProtocol) throws { + let selectedRelays = try? interactor.selectRelays() + var tunnelOptions = PacketTunnelOptions() + + do { + if let selectedRelays { + try tunnelOptions.setSelectedRelays(selectedRelays) + } + } catch { + logger.error( + error: error, + message: "Failed to encode the selector result." + ) + } + + interactor.setTunnel(tunnel, shouldRefreshTunnelState: true) + + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .connecting( + selectedRelays, + isPostQuantum: interactor.settings.tunnelQuantumResistance.isEnabled, + isDaita: interactor.settings.daita.daitaState.isEnabled + ) + } + + _ = tunnel.reconnectTunnel(to: .current) { [weak self] result in + self?.finish(result: result) + } + } + + private func updateTunnelProvider(completionHandler: @escaping (Result<any TunnelProtocol, Error>) -> Void) { + let persistentTunnels = interactor.getPersistentTunnels() + let tunnel = persistentTunnels.first! + let configuration = Self.makeTunnelConfiguration() + +// tunnel.removeFromPreferences(completion: { _ in }) + tunnel.setConfiguration(configuration) + tunnel.updatePreferences { error in + completionHandler(error.map { .failure($0) } ?? .success(tunnel)) + } + } + + private class func makeTunnelConfiguration() -> TunnelConfiguration { + let protocolConfig = NETunnelProviderProtocol() + protocolConfig.providerBundleIdentifier = ApplicationTarget.packetTunnel.bundleIdentifier + protocolConfig.serverAddress = "" + + let includeAllNetworks = UserDefaults.standard.bool(forKey: "includeAllNetworks") + let excludeLocalNetworks = UserDefaults.standard.bool(forKey: "excludeLocalNetworks") + + protocolConfig.includeAllNetworks = includeAllNetworks + protocolConfig.excludeLocalNetworks = excludeLocalNetworks + + let alwaysOnRule = NEOnDemandRuleConnect() + alwaysOnRule.interfaceTypeMatch = .any + + return TunnelConfiguration( + isEnabled: true, + localizedDescription: "WireGuard", + protocolConfiguration: protocolConfig, + onDemandRules: [alwaysOnRule], + isOnDemandEnabled: true + ) + } +} diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index b95376590f..288f327978 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -17,6 +17,7 @@ enum TunnelControlAction { case disconnect case cancel case reconnect + case updateTunnel case selectLocation } @@ -32,6 +33,41 @@ final class TunnelControlView: UIView { private let cityLabel = makeBoldTextLabel(ofSize: 34) private let countryLabel = makeBoldTextLabel(ofSize: 34) + private let includeAllNetworksStackView: UIStackView = { + let view = UIStackView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isAccessibilityElement = false + view.accessibilityTraits = .summaryElement + view.axis = .horizontal + view.spacing = 8 + return view + }() + + private let excludeLocalNetworksStackView: UIStackView = { + let view = UIStackView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isAccessibilityElement = false + view.accessibilityTraits = .summaryElement + view.axis = .horizontal + view.spacing = 8 + return view + }() + + private let includeAllNetworksLabel = makeBoldTextLabel(ofSize: 15) + private let excludeLocalNetworksLabel = makeBoldTextLabel(ofSize: 15) + + private let includeAllNetworksToggle: UISwitch = { + let switchButton = UISwitch() + switchButton.isOn = UserDefaults.standard.bool(forKey: "includeAllNetworks") + return switchButton + }() + + private let excludeLocalNetworksToggle: UISwitch = { + let switchButton = UISwitch() + switchButton.isOn = UserDefaults.standard.bool(forKey: "excludeLocalNetworks") + return switchButton + }() + private let activityIndicator: SpinnerActivityIndicatorView = { let activityIndicator = SpinnerActivityIndicatorView(style: .large) activityIndicator.translatesAutoresizingMaskIntoConstraints = false @@ -41,6 +77,16 @@ final class TunnelControlView: UIView { return activityIndicator }() + private let debugContainerView: UIStackView = { + let view = UIStackView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isAccessibilityElement = false + view.accessibilityTraits = .summaryElement + view.axis = .vertical + view.spacing = 8 + return view + }() + private let locationContainerView: UIStackView = { let view = UIStackView() view.translatesAutoresizingMaskIntoConstraints = false @@ -137,6 +183,13 @@ final class TunnelControlView: UIView { let tunnelState = model.tunnelStatus.state secureLabel.text = model.secureLabelText secureLabel.textColor = tunnelState.textColorForSecureLabel + + includeAllNetworksLabel.text = "Include All Networks" + includeAllNetworksLabel.textColor = .white + + excludeLocalNetworksLabel.text = "Exclude Local Networks" + excludeLocalNetworksLabel.textColor = .white + selectLocationButtonBlurView.isEnabled = model.enableButtons connectButtonBlurView.isEnabled = model.enableButtons cityLabel.attributedText = attributedStringForLocation(string: model.city) @@ -274,11 +327,23 @@ final class TunnelControlView: UIView { // MARK: - Private private func addSubviews() { + for subview in [includeAllNetworksLabel, includeAllNetworksToggle] { + includeAllNetworksStackView.addArrangedSubview(subview) + } + + for subview in [excludeLocalNetworksLabel, excludeLocalNetworksToggle] { + excludeLocalNetworksStackView.addArrangedSubview(subview) + } + + for subview in [includeAllNetworksStackView, excludeLocalNetworksStackView] { + debugContainerView.addArrangedSubview(subview) + } + for subview in [secureLabel, countryLabel, cityLabel, connectionPanel] { locationContainerView.addArrangedSubview(subview) } - for subview in [activityIndicator, buttonsStackView, locationContainerView] { + for subview in [activityIndicator, buttonsStackView, locationContainerView, debugContainerView] { containerView.addSubview(subview) } @@ -290,15 +355,22 @@ final class TunnelControlView: UIView { containerView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), containerView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - locationContainerView.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor), - locationContainerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - locationContainerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + debugContainerView.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor), + debugContainerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + debugContainerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - activityIndicator.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), locationContainerView.topAnchor.constraint( - equalTo: activityIndicator.bottomAnchor, + greaterThanOrEqualTo: debugContainerView.bottomAnchor, constant: 22 ), + locationContainerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + locationContainerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), +// locationContainerView.topAnchor.constraint( +// equalTo: activityIndicator.bottomAnchor, +// constant: 22 +// ), buttonsStackView.topAnchor.constraint( equalTo: locationContainerView.bottomAnchor, @@ -336,6 +408,9 @@ final class TunnelControlView: UIView { action: #selector(handleSelectLocation), for: .touchUpInside ) + + includeAllNetworksToggle.addTarget(self, action: #selector(toggleIncludeAllNetwork), for: .valueChanged) + excludeLocalNetworksToggle.addTarget(self, action: #selector(toggleExcludeLocalNetworks), for: .valueChanged) } private func setArrangedButtons(_ newButtons: [UIView]) { @@ -405,6 +480,16 @@ final class TunnelControlView: UIView { @objc private func handleSelectLocation() { actionHandler?(.selectLocation) } + + @objc private func toggleIncludeAllNetwork() { + UserDefaults.standard.set(includeAllNetworksToggle.isOn, forKey: "includeAllNetworks") + actionHandler?(.updateTunnel) + } + + @objc private func toggleExcludeLocalNetworks() { + UserDefaults.standard.set(excludeLocalNetworksToggle.isOn, forKey: "excludeLocalNetworks") + actionHandler?(.updateTunnel) + } } private extension TunnelState { diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 7cf879f3bf..42b5dc5397 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -84,6 +84,9 @@ class TunnelViewController: UIViewController, RootContainment { case .disconnect: self?.interactor.stopTunnel() + case .updateTunnel: + self?.interactor.updateTunnel() + case .reconnect: self?.interactor.reconnectTunnel(selectNewRelay: true) diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift index 47b75fd7d5..479b5b3257 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift @@ -64,6 +64,10 @@ final class TunnelViewControllerInteractor { self.tunnelObserver = tunnelObserver } + func updateTunnel() { + tunnelManager.updateTunnel() + } + func startTunnel() { tunnelManager.startTunnel() } |
