summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift56
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift330
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift179
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift118
5 files changed, 369 insertions, 326 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index ccf1018d05..9fc62308e8 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -395,6 +395,9 @@
7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */; };
7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307AD82A8CD8DA0017618B /* Duration.swift */; };
7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */; };
+ 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */; };
+ 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; };
+ 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; };
7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; };
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; };
7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */; };
@@ -1312,6 +1315,9 @@
7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = "<group>"; };
7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = "<group>"; };
+ 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderManager.swift; sourceTree = "<group>"; };
+ 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; };
+ 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; };
7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = "<group>"; };
@@ -1985,8 +1991,11 @@
583FE02629C1ADB6006E85F9 /* SimulatorTunnelProvider */ = {
isa = PBXGroup;
children = (
+ 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */,
58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */,
587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */,
+ 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */,
+ 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */,
);
path = SimulatorTunnelProvider;
sourceTree = "<group>";
@@ -3736,6 +3745,7 @@
587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */,
F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */,
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
+ 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */,
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */,
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */,
@@ -3835,6 +3845,7 @@
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */,
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
+ 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */,
7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */,
7A9CCCBB2A96302800DD6A34 /* InAppPurchaseCoordinator.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
@@ -3868,6 +3879,7 @@
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
+ 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
7AE47E522A17972A000418DA /* CustomAlertViewController.swift in Sources */,
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */,
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift
new file mode 100644
index 0000000000..a1324df09f
--- /dev/null
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift
@@ -0,0 +1,56 @@
+//
+// SimulatorTunnelInfo.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-09-07.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+#if targetEnvironment(simulator)
+
+import Foundation
+import NetworkExtension
+
+final class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol {
+ func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {
+ SimulatorTunnelProvider.shared.handleAppMessage(
+ messageData,
+ completionHandler: responseHandler
+ )
+ }
+}
+
+/// A mock struct for tunnel configuration and connection
+struct SimulatorTunnelInfo {
+ /// A unique identifier for the configuration
+ var identifier = UUID().uuidString
+
+ /// An associated VPN connection.
+ /// Intentionally initialized with a `SimulatorTunnelProviderSession` subclass which
+ /// implements the necessary protocol
+ var connection: SimulatorVPNConnection = SimulatorTunnelProviderSession()
+
+ /// Whether configuration is enabled
+ var isEnabled = false
+
+ /// Whether on-demand VPN is enabled
+ var isOnDemandEnabled = false
+
+ /// On-demand VPN rules
+ var onDemandRules = [NEOnDemandRule]()
+
+ /// Protocol configuration
+ var protocolConfiguration: NEVPNProtocol? {
+ didSet {
+ connection.protocolConfiguration = protocolConfiguration ?? NEVPNProtocol()
+ }
+ }
+
+ /// Tunnel description
+ var localizedDescription: String?
+
+ /// Designated initializer
+ init() {}
+}
+
+#endif
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
index a2eb3d5f43..debaf16949 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
@@ -6,11 +6,11 @@
// Copyright © 2020 Mullvad VPN AB. All rights reserved.
//
+#if targetEnvironment(simulator)
+
import Foundation
import NetworkExtension
-// MARK: - Formal conformances
-
protocol VPNConnectionProtocol: NSObject {
var status: NEVPNStatus { get }
var connectedDate: Date? { get }
@@ -46,12 +46,8 @@ extension NEVPNConnection: VPNConnectionProtocol {}
extension NETunnelProviderSession: VPNTunnelProviderSessionProtocol {}
extension NETunnelProviderManager: VPNTunnelProviderManagerProtocol {}
-#if targetEnvironment(simulator)
-
-// MARK: - NEPacketTunnelProvider stubs
-
class SimulatorTunnelProviderDelegate {
- fileprivate(set) var connection: SimulatorVPNConnection?
+ var connection: SimulatorVPNConnection?
var protocolConfiguration: NEVPNProtocol {
connection?.protocolConfiguration ?? NEVPNProtocol()
@@ -101,327 +97,9 @@ final class SimulatorTunnelProvider {
private init() {}
- fileprivate func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
+ func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
delegate.handleAppMessage(messageData, completionHandler: completionHandler)
}
}
-// MARK: - NEVPNConnection stubs
-
-class SimulatorVPNConnection: NSObject, VPNConnectionProtocol {
- // Protocol configuration is automatically synced by `SimulatorTunnelInfo`
- fileprivate var protocolConfiguration = NEVPNProtocol()
-
- private let lock = NSRecursiveLock()
- private var _status: NEVPNStatus = .disconnected
- private var _reasserting = false
- private var _connectedDate: Date?
-
- private(set) var status: NEVPNStatus {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return _status
- }
- set {
- lock.lock()
-
- if _status != newValue {
- _status = newValue
-
- // Send notification while holding the lock. This should enable the receiver
- // to fetch the `SimulatorVPNConnection.status` before the concurrent code gets
- // opportunity to change it again.
- postStatusDidChangeNotification()
- }
-
- lock.unlock()
- }
- }
-
- var reasserting: Bool {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return _reasserting
- }
- set {
- lock.lock()
-
- if _reasserting != newValue {
- _reasserting = newValue
-
- if newValue {
- status = .reasserting
- } else {
- status = .connected
- }
- }
-
- lock.unlock()
- }
- }
-
- private(set) var connectedDate: Date? {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return _connectedDate
- }
- set {
- lock.lock()
- _connectedDate = newValue
- lock.unlock()
- }
- }
-
- func startVPNTunnel() throws {
- try startVPNTunnel(options: nil)
- }
-
- func startVPNTunnel(options: [String: NSObject]?) throws {
- SimulatorTunnelProvider.shared.delegate.connection = self
-
- status = .connecting
-
- SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { error in
- if error == nil {
- self.status = .connected
- self.connectedDate = Date()
- } else {
- self.status = .disconnected
- self.connectedDate = nil
- }
- }
- }
-
- func stopVPNTunnel() {
- status = .disconnecting
-
- SimulatorTunnelProvider.shared.delegate.stopTunnel(with: .userInitiated) {
- self.status = .disconnected
- self.connectedDate = nil
- }
- }
-
- private func postStatusDidChangeNotification() {
- NotificationCenter.default.post(name: .NEVPNStatusDidChange, object: self)
- }
-}
-
-// MARK: - NETunnelProviderSession stubs
-
-final class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol {
- func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {
- SimulatorTunnelProvider.shared.handleAppMessage(
- messageData,
- completionHandler: responseHandler
- )
- }
-}
-
-// MARK: - NETunnelProviderManager stubs
-
-/// A mock struct for tunnel configuration and connection
-private struct SimulatorTunnelInfo {
- /// A unique identifier for the configuration
- var identifier = UUID().uuidString
-
- /// An associated VPN connection.
- /// Intentionally initialized with a `SimulatorTunnelProviderSession` subclass which
- /// implements the necessary protocol
- var connection: SimulatorVPNConnection = SimulatorTunnelProviderSession()
-
- /// Whether configuration is enabled
- var isEnabled = false
-
- /// Whether on-demand VPN is enabled
- var isOnDemandEnabled = false
-
- /// On-demand VPN rules
- var onDemandRules = [NEOnDemandRule]()
-
- /// Protocol configuration
- var protocolConfiguration: NEVPNProtocol? {
- didSet {
- connection.protocolConfiguration = protocolConfiguration ?? NEVPNProtocol()
- }
- }
-
- /// Tunnel description
- var localizedDescription: String?
-
- /// Designated initializer
- init() {}
-}
-
-final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerProtocol {
- static let tunnelsLock = NSRecursiveLock()
- fileprivate static var tunnels = [SimulatorTunnelInfo]()
-
- private let lock = NSLock()
- private var tunnelInfo: SimulatorTunnelInfo
- private var identifier: String {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.identifier
- }
-
- var isOnDemandEnabled: Bool {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.isOnDemandEnabled
- }
- set {
- lock.lock()
- tunnelInfo.isOnDemandEnabled = newValue
- lock.unlock()
- }
- }
-
- var onDemandRules: [NEOnDemandRule] {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.onDemandRules
- }
- set {
- lock.lock()
- tunnelInfo.onDemandRules = newValue
- lock.unlock()
- }
- }
-
- var isEnabled: Bool {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.isEnabled
- }
- set {
- lock.lock()
- tunnelInfo.isEnabled = newValue
- lock.unlock()
- }
- }
-
- var protocolConfiguration: NEVPNProtocol? {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.protocolConfiguration
- }
- set {
- lock.lock()
- tunnelInfo.protocolConfiguration = newValue
- lock.unlock()
- }
- }
-
- var localizedDescription: String? {
- get {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.localizedDescription
- }
- set {
- lock.lock()
- tunnelInfo.localizedDescription = newValue
- lock.unlock()
- }
- }
-
- var connection: SimulatorVPNConnection {
- lock.lock()
- defer { lock.unlock() }
-
- return tunnelInfo.connection
- }
-
- static func loadAllFromPreferences(completionHandler: (
- [SimulatorTunnelProviderManager]?,
- Error?
- ) -> Void) {
- Self.tunnelsLock.lock()
- let tunnelProviders = tunnels.map { tunnelInfo in
- SimulatorTunnelProviderManager(tunnelInfo: tunnelInfo)
- }
- Self.tunnelsLock.unlock()
-
- completionHandler(tunnelProviders, nil)
- }
-
- override required init() {
- tunnelInfo = SimulatorTunnelInfo()
- super.init()
- }
-
- private init(tunnelInfo: SimulatorTunnelInfo) {
- self.tunnelInfo = tunnelInfo
- super.init()
- }
-
- func loadFromPreferences(completionHandler: (Error?) -> Void) {
- var error: NEVPNError?
-
- Self.tunnelsLock.lock()
-
- if let savedTunnel = Self.tunnels.first(where: { $0.identifier == self.identifier }) {
- tunnelInfo = savedTunnel
- } else {
- error = NEVPNError(.configurationInvalid)
- }
-
- Self.tunnelsLock.unlock()
-
- completionHandler(error)
- }
-
- func saveToPreferences(completionHandler: ((Error?) -> Void)?) {
- Self.tunnelsLock.lock()
-
- if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) {
- Self.tunnels[index] = tunnelInfo
- } else {
- Self.tunnels.append(tunnelInfo)
- }
-
- Self.tunnelsLock.unlock()
-
- completionHandler?(nil)
- }
-
- func removeFromPreferences(completionHandler: ((Error?) -> Void)?) {
- var error: NEVPNError?
-
- Self.tunnelsLock.lock()
-
- if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) {
- Self.tunnels.remove(at: index)
- } else {
- error = NEVPNError(.configurationReadWriteFailed)
- }
-
- Self.tunnelsLock.unlock()
-
- completionHandler?(error)
- }
-
- override func isEqual(_ object: Any?) -> Bool {
- guard let other = object as? Self else { return false }
- return self.identifier == other.identifier
- }
-}
-
-// swiftlint:disable:next file_length
#endif
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift
new file mode 100644
index 0000000000..c8bc1fe7fe
--- /dev/null
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift
@@ -0,0 +1,179 @@
+//
+// SimulatorTunnelProviderManager.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-09-07.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+#if targetEnvironment(simulator)
+
+import Foundation
+import NetworkExtension
+
+final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerProtocol {
+ static let tunnelsLock = NSRecursiveLock()
+ fileprivate static var tunnels = [SimulatorTunnelInfo]()
+
+ private let lock = NSLock()
+ private var tunnelInfo: SimulatorTunnelInfo
+ private var identifier: String {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.identifier
+ }
+
+ var isOnDemandEnabled: Bool {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.isOnDemandEnabled
+ }
+ set {
+ lock.lock()
+ tunnelInfo.isOnDemandEnabled = newValue
+ lock.unlock()
+ }
+ }
+
+ var onDemandRules: [NEOnDemandRule] {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.onDemandRules
+ }
+ set {
+ lock.lock()
+ tunnelInfo.onDemandRules = newValue
+ lock.unlock()
+ }
+ }
+
+ var isEnabled: Bool {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.isEnabled
+ }
+ set {
+ lock.lock()
+ tunnelInfo.isEnabled = newValue
+ lock.unlock()
+ }
+ }
+
+ var protocolConfiguration: NEVPNProtocol? {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.protocolConfiguration
+ }
+ set {
+ lock.lock()
+ tunnelInfo.protocolConfiguration = newValue
+ lock.unlock()
+ }
+ }
+
+ var localizedDescription: String? {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.localizedDescription
+ }
+ set {
+ lock.lock()
+ tunnelInfo.localizedDescription = newValue
+ lock.unlock()
+ }
+ }
+
+ var connection: SimulatorVPNConnection {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return tunnelInfo.connection
+ }
+
+ static func loadAllFromPreferences(completionHandler: (
+ [SimulatorTunnelProviderManager]?,
+ Error?
+ ) -> Void) {
+ Self.tunnelsLock.lock()
+ let tunnelProviders = tunnels.map { tunnelInfo in
+ SimulatorTunnelProviderManager(tunnelInfo: tunnelInfo)
+ }
+ Self.tunnelsLock.unlock()
+
+ completionHandler(tunnelProviders, nil)
+ }
+
+ override required init() {
+ tunnelInfo = SimulatorTunnelInfo()
+ super.init()
+ }
+
+ private init(tunnelInfo: SimulatorTunnelInfo) {
+ self.tunnelInfo = tunnelInfo
+ super.init()
+ }
+
+ func loadFromPreferences(completionHandler: (Error?) -> Void) {
+ var error: NEVPNError?
+
+ Self.tunnelsLock.lock()
+
+ if let savedTunnel = Self.tunnels.first(where: { $0.identifier == self.identifier }) {
+ tunnelInfo = savedTunnel
+ } else {
+ error = NEVPNError(.configurationInvalid)
+ }
+
+ Self.tunnelsLock.unlock()
+
+ completionHandler(error)
+ }
+
+ func saveToPreferences(completionHandler: ((Error?) -> Void)?) {
+ Self.tunnelsLock.lock()
+
+ if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) {
+ Self.tunnels[index] = tunnelInfo
+ } else {
+ Self.tunnels.append(tunnelInfo)
+ }
+
+ Self.tunnelsLock.unlock()
+
+ completionHandler?(nil)
+ }
+
+ func removeFromPreferences(completionHandler: ((Error?) -> Void)?) {
+ var error: NEVPNError?
+
+ Self.tunnelsLock.lock()
+
+ if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) {
+ Self.tunnels.remove(at: index)
+ } else {
+ error = NEVPNError(.configurationReadWriteFailed)
+ }
+
+ Self.tunnelsLock.unlock()
+
+ completionHandler?(error)
+ }
+
+ override func isEqual(_ object: Any?) -> Bool {
+ guard let other = object as? Self else { return false }
+ return self.identifier == other.identifier
+ }
+}
+
+#endif
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift
new file mode 100644
index 0000000000..6b235d285c
--- /dev/null
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift
@@ -0,0 +1,118 @@
+//
+// SimulatorVPNConnection.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-09-07.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+#if targetEnvironment(simulator)
+
+import Foundation
+import NetworkExtension
+
+class SimulatorVPNConnection: NSObject, VPNConnectionProtocol {
+ // Protocol configuration is automatically synced by `SimulatorTunnelInfo`
+ var protocolConfiguration = NEVPNProtocol()
+
+ private let lock = NSRecursiveLock()
+ private var _status: NEVPNStatus = .disconnected
+ private var _reasserting = false
+ private var _connectedDate: Date?
+
+ private(set) var status: NEVPNStatus {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return _status
+ }
+ set {
+ lock.lock()
+
+ if _status != newValue {
+ _status = newValue
+
+ // Send notification while holding the lock. This should enable the receiver
+ // to fetch the `SimulatorVPNConnection.status` before the concurrent code gets
+ // opportunity to change it again.
+ postStatusDidChangeNotification()
+ }
+
+ lock.unlock()
+ }
+ }
+
+ var reasserting: Bool {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return _reasserting
+ }
+ set {
+ lock.lock()
+
+ if _reasserting != newValue {
+ _reasserting = newValue
+
+ if newValue {
+ status = .reasserting
+ } else {
+ status = .connected
+ }
+ }
+
+ lock.unlock()
+ }
+ }
+
+ private(set) var connectedDate: Date? {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+
+ return _connectedDate
+ }
+ set {
+ lock.lock()
+ _connectedDate = newValue
+ lock.unlock()
+ }
+ }
+
+ func startVPNTunnel() throws {
+ try startVPNTunnel(options: nil)
+ }
+
+ func startVPNTunnel(options: [String: NSObject]?) throws {
+ SimulatorTunnelProvider.shared.delegate.connection = self
+
+ status = .connecting
+
+ SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { error in
+ if error == nil {
+ self.status = .connected
+ self.connectedDate = Date()
+ } else {
+ self.status = .disconnected
+ self.connectedDate = nil
+ }
+ }
+ }
+
+ func stopVPNTunnel() {
+ status = .disconnecting
+
+ SimulatorTunnelProvider.shared.delegate.stopTunnel(with: .userInitiated) {
+ self.status = .disconnected
+ self.connectedDate = nil
+ }
+ }
+
+ private func postStatusDidChangeNotification() {
+ NotificationCenter.default.post(name: .NEVPNStatusDidChange, object: self)
+ }
+}
+
+#endif