From 0437112017140a5deb4abf60f05d0b71c2ea8b0d Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Thu, 18 Jan 2024 17:01:38 -0500 Subject: [PATCH 1/5] Re-use the `getTunnelManager()` method in `setupVpn()`. --- .../Sources/OutlineTunnel/OutlineVpn.swift | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift index 9418fb1a169..4678d0a7274 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift @@ -124,9 +124,9 @@ public class OutlineVpn: NSObject { // MARK: Helpers private func startVpn(_ tunnelId: String?, configJson: [String: Any]?, isAutoConnect: Bool, _ completion: @escaping(Callback)) { - setupVpn() { error in - if error != nil { - DDLogError("Failed to setup VPN: \(String(describing: error))") + setupVpn() { success in + guard success else { + DDLogError("Failed to setup VPN") return completion(ErrorCode.vpnPermissionNotGranted); } let message = [MessageKey.action: Action.start, MessageKey.tunnelId: tunnelId ?? ""]; @@ -174,19 +174,14 @@ public class OutlineVpn: NSObject { // Adds a VPN configuration to the user preferences if no Outline profile is present. Otherwise // enables the existing configuration. - private func setupVpn(completion: @escaping(Error?) -> Void) { - NETunnelProviderManager.loadAllFromPreferences() { (managers, error) in - if let error = error { - DDLogError("Failed to load VPN configuration: \(error)") - return completion(error) - } + private func setupVpn(completion: @escaping(Bool) -> Void) { + getTunnelManager() { tunnelManager in var manager: NETunnelProviderManager! - if let managers = managers, managers.count > 0 { - manager = managers.first + if let manager = tunnelManager { let hasOnDemandRules = !(manager.onDemandRules?.isEmpty ?? true) if manager.isEnabled && hasOnDemandRules { self.tunnelManager = manager - return completion(nil) + return completion(true) } } else { let config = NETunnelProviderProtocol() @@ -204,14 +199,18 @@ public class OutlineVpn: NSObject { manager.saveToPreferences() { error in if let error = error { DDLogError("Failed to save VPN configuration: \(error)") - return completion(error) + return completion(false) } self.observeVpnStatusChange(manager!) self.tunnelManager = manager NotificationCenter.default.post(name: .NEVPNConfigurationChange, object: nil) // Workaround for https://forums.developer.apple.com/thread/25928 self.tunnelManager?.loadFromPreferences() { error in - completion(error) + if let error = error { + DDLogError("Failed to get tunnel manage: \(error)") + return completion(false) + } + return completion(true) } } } From a6fc3a524b822dc619a0a683d17bd2a1661342b3 Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Thu, 18 Jan 2024 18:04:36 -0500 Subject: [PATCH 2/5] Move autoconnect logic to `NETunnelProviderManager` extension. --- .../NETunnelProviderManager+Outline.swift | 35 +++++++++++++++++++ .../Sources/OutlineTunnel/OutlineVpn.swift | 9 ++--- 2 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift new file mode 100644 index 00000000000..d4e3254ac51 --- /dev/null +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift @@ -0,0 +1,35 @@ +// Copyright 2024 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import NetworkExtension + +public extension NETunnelProviderManager { + var autoConnect: Bool { + get { + let hasOnDemandRules = !(self.onDemandRules?.isEmpty ?? true) + return self.isEnabled && hasOnDemandRules + } + set { + if newValue { + let connectRule = NEOnDemandRuleConnect() + connectRule.interfaceTypeMatch = .any + self.onDemandRules = [connectRule] + } else { + self.onDemandRules = nil + } + self.isEnabled = newValue + } + } +} diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift index 4678d0a7274..7542d40c672 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift @@ -178,8 +178,7 @@ public class OutlineVpn: NSObject { getTunnelManager() { tunnelManager in var manager: NETunnelProviderManager! if let manager = tunnelManager { - let hasOnDemandRules = !(manager.onDemandRules?.isEmpty ?? true) - if manager.isEnabled && hasOnDemandRules { + if manager.autoConnect { self.tunnelManager = manager return completion(true) } @@ -191,11 +190,7 @@ public class OutlineVpn: NSObject { manager = NETunnelProviderManager() manager.protocolConfiguration = config } - // Set an on-demand rule to connect to any available network to implement auto-connect on boot - let connectRule = NEOnDemandRuleConnect() - connectRule.interfaceTypeMatch = .any - manager.onDemandRules = [connectRule] - manager.isEnabled = true + manager.autoConnect = true manager.saveToPreferences() { error in if let error = error { DDLogError("Failed to save VPN configuration: \(error)") From 48b59bce82398676e41288590be06b80ab25a09e Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Thu, 18 Jan 2024 18:13:14 -0500 Subject: [PATCH 3/5] No need to unwrap `manager` as it's non-null. --- .../OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift index 7542d40c672..eb1dcc464ad 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift @@ -196,7 +196,7 @@ public class OutlineVpn: NSObject { DDLogError("Failed to save VPN configuration: \(error)") return completion(false) } - self.observeVpnStatusChange(manager!) + self.observeVpnStatusChange(manager) self.tunnelManager = manager NotificationCenter.default.post(name: .NEVPNConfigurationChange, object: nil) // Workaround for https://forums.developer.apple.com/thread/25928 From 4241c9bcff42f4f140e9029777fc3aa76ed167fb Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Fri, 19 Jan 2024 12:15:54 -0500 Subject: [PATCH 4/5] Remove existing managers if they are old/stale and instead create a new one. --- .../NETunnelProviderManager+Outline.swift | 16 +++ .../Sources/OutlineTunnel/OutlineVpn.swift | 112 ++++++++++++------ 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift index d4e3254ac51..9e4d0ee2ec5 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift @@ -15,7 +15,23 @@ import Foundation import NetworkExtension +public enum TunnelProviderKeys { + static let keyVersion = "version" +} + public extension NETunnelProviderManager { + // Checks if the configuration has gone stale, which means clients should discard it. + var isStale: Bool { + // When moving from macOS to Mac Catalyst, we need to delete the existing profile and create a new + // one. We track such "stale" profiles by a version on the provider configuration. + if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol { + var providerConfig: [String: Any] = protocolConfiguration.providerConfiguration ?? [:] + let version = providerConfig[TunnelProviderKeys.keyVersion, default: 0] as! Int + return version != 1 + } + return true + } + var autoConnect: Bool { get { let hasOnDemandRules = !(self.onDemandRules?.isEmpty ?? true) diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift index eb1dcc464ad..baf3d2fa99c 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/OutlineVpn.swift @@ -21,6 +21,7 @@ import Tun2socks public class OutlineVpn: NSObject { public static let shared = OutlineVpn() private static let kVpnExtensionBundleId = "\(Bundle.main.bundleIdentifier!).VpnExtension" + private static let kVpnServerAddress = "Outline" public typealias Callback = (ErrorCode) -> Void public typealias VpnStatusObserver = (NEVPNStatus, String) -> Void @@ -175,38 +176,29 @@ public class OutlineVpn: NSObject { // Adds a VPN configuration to the user preferences if no Outline profile is present. Otherwise // enables the existing configuration. private func setupVpn(completion: @escaping(Bool) -> Void) { - getTunnelManager() { tunnelManager in - var manager: NETunnelProviderManager! - if let manager = tunnelManager { - if manager.autoConnect { - self.tunnelManager = manager - return completion(true) - } - } else { - let config = NETunnelProviderProtocol() - config.providerBundleIdentifier = OutlineVpn.kVpnExtensionBundleId - config.serverAddress = "Outline" + getOrCreateTunnelManager() { manager in + guard let manager else { + DDLogError("Failed to setup tunnel manager") + return completion(false) + } - manager = NETunnelProviderManager() - manager.protocolConfiguration = config + guard manager.autoConnect else { + manager.autoConnect = true + return self.saveTunnelManager(manager, completion) } - manager.autoConnect = true - manager.saveToPreferences() { error in - if let error = error { - DDLogError("Failed to save VPN configuration: \(error)") - return completion(false) - } - self.observeVpnStatusChange(manager) - self.tunnelManager = manager - NotificationCenter.default.post(name: .NEVPNConfigurationChange, object: nil) - // Workaround for https://forums.developer.apple.com/thread/25928 - self.tunnelManager?.loadFromPreferences() { error in - if let error = error { - DDLogError("Failed to get tunnel manage: \(error)") - return completion(false) - } - return completion(true) - } + + self.tunnelManager = manager + return completion(true) + } + } + + private func getOrCreateTunnelManager(_ completion: @escaping ((NETunnelProviderManager?) -> Void)) { + getTunnelManager() { manager in + if let manager = manager { + return completion(manager) + } + self.createTunnelManager() { newManager in + return completion(newManager) } } } @@ -220,6 +212,27 @@ public class OutlineVpn: NSObject { } } + // Creates the application's tunnel provider manager and saves it in the VPN preferences. + private func createTunnelManager(_ completion: @escaping ((NETunnelProviderManager?) -> Void)) { + let config = NETunnelProviderProtocol() + config.providerBundleIdentifier = OutlineVpn.kVpnExtensionBundleId + config.serverAddress = OutlineVpn.kVpnServerAddress + config.providerConfiguration = [TunnelProviderKeys.keyVersion: 1] + + let manager = NETunnelProviderManager() + manager.protocolConfiguration = config + manager.autoConnect = true + + self.saveTunnelManager(manager) { success in + guard success else { + DDLogError("Failed to create new tunnel manager") + return completion(nil) + } + DDLogInfo("Created new tunnel manager") + return completion(manager) + } + } + // Retrieves the application's tunnel provider manager from the VPN preferences. private func getTunnelManager(_ completion: @escaping ((NETunnelProviderManager?) -> Void)) { NETunnelProviderManager.loadAllFromPreferences() { (managers, error) in @@ -227,11 +240,42 @@ public class OutlineVpn: NSObject { completion(nil) return DDLogError("Failed to get tunnel manager: \(String(describing: error))") } - var manager: NETunnelProviderManager? - if managers!.count > 0 { - manager = managers!.first + + DDLogInfo("Loaded \(managers!.count) tunnel managers") + guard managers!.count > 0 else { + return completion(nil) + } + let manager: NETunnelProviderManager = managers!.first! + if manager.isStale { + DDLogInfo("Removing stale tunnel manager") + manager.removeFromPreferences() { _ in + return completion(nil) + } + } else { + return completion(manager) + } + } + } + + // Updates the application's tunnel provider manager in the VPN preferences. + private func saveTunnelManager(_ manager: NETunnelProviderManager, _ completion: @escaping ((Bool) -> Void)) { + manager.saveToPreferences() { error in + guard error == nil else { + DDLogError("Failed to save VPN configuration: \(error)") + return completion(false) + } + self.tunnelManager = manager + self.observeVpnStatusChange(self.tunnelManager!) + NotificationCenter.default.post(name: .NEVPNConfigurationChange, object: nil) + // Workaround for https://forums.developer.apple.com/thread/25928 + self.tunnelManager?.loadFromPreferences() { error in + if let error = error { + DDLogError("Failed to get tunnel manager: \(error)") + return completion(false) + } + DDLogInfo("Saved VPN configuration") + return completion(true) } - completion(manager) } } From 8e790d3637c18959af80385a43812964d3b65350 Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Fri, 19 Jan 2024 12:16:43 -0500 Subject: [PATCH 5/5] Only consider managers stale on Catalyst, not on iOS. --- .../NETunnelProviderManager+Outline.swift | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift index 9e4d0ee2ec5..620629c6a83 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnel/NETunnelProviderManager+Outline.swift @@ -22,14 +22,19 @@ public enum TunnelProviderKeys { public extension NETunnelProviderManager { // Checks if the configuration has gone stale, which means clients should discard it. var isStale: Bool { - // When moving from macOS to Mac Catalyst, we need to delete the existing profile and create a new - // one. We track such "stale" profiles by a version on the provider configuration. - if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol { - var providerConfig: [String: Any] = protocolConfiguration.providerConfiguration ?? [:] - let version = providerConfig[TunnelProviderKeys.keyVersion, default: 0] as! Int - return version != 1 - } - return true + #if targetEnvironment(macCatalyst) + // When migrating from macOS to Mac Catalyst, we can't use managers created by the macOS app. + // Instead, we need to create a new one. We track such "stale" managers by a version on the + // provider configuration. + if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol { + var providerConfig: [String: Any] = protocolConfiguration.providerConfiguration ?? [:] + let version = providerConfig[TunnelProviderKeys.keyVersion, default: 0] as! Int + return version != 1 + } + return true + #else + return false + #endif } var autoConnect: Bool {