Reorganize VPNProvider related components
Reuse most code for native or custom providers.
This commit is contained in:
parent
e6e37cd528
commit
70f8c6cc2b
|
@ -192,6 +192,12 @@
|
||||||
0EA82A3E2190B2BC007960EB /* pia-2048.pem in Resources */ = {isa = PBXBuildFile; fileRef = 0E749F612178911C00BB2701 /* pia-2048.pem */; };
|
0EA82A3E2190B2BC007960EB /* pia-2048.pem in Resources */ = {isa = PBXBuildFile; fileRef = 0E749F612178911C00BB2701 /* pia-2048.pem */; };
|
||||||
0EA9F28926D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; };
|
0EA9F28926D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; };
|
||||||
0EA9F28A26D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; };
|
0EA9F28A26D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */; };
|
||||||
|
0EA9F28C26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */; };
|
||||||
|
0EA9F28D26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */; };
|
||||||
|
0EA9F28F26D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */; };
|
||||||
|
0EA9F29026D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */; };
|
||||||
|
0EA9F29526D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */; };
|
||||||
|
0EA9F29626D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */; };
|
||||||
0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; };
|
0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; };
|
||||||
0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; };
|
0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */; };
|
||||||
0EAC57392494277A00D0FCE0 /* MockVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */; };
|
0EAC57392494277A00D0FCE0 /* MockVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */; };
|
||||||
|
@ -574,6 +580,9 @@
|
||||||
0E94E8E925BACEBD0040BC30 /* DNSProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSProtocol.swift; sourceTree = "<group>"; };
|
0E94E8E925BACEBD0040BC30 /* DNSProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSProtocol.swift; sourceTree = "<group>"; };
|
||||||
0EA82A232190B220007960EB /* TunnelKitTests-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "TunnelKitTests-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
0EA82A232190B220007960EB /* TunnelKitTests-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "TunnelKitTests-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionVPNConfiguration.swift; sourceTree = "<group>"; };
|
0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionVPNConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionVPNProvider.swift; sourceTree = "<group>"; };
|
||||||
|
0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProviderIPC.swift; sourceTree = "<group>"; };
|
||||||
|
0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionLocator.swift; sourceTree = "<group>"; };
|
||||||
0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenVPNProvider.swift; sourceTree = "<group>"; };
|
0EAC57312494277A00D0FCE0 /* OpenVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenVPNProvider.swift; sourceTree = "<group>"; };
|
||||||
0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockVPNProvider.swift; sourceTree = "<group>"; };
|
0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockVPNProvider.swift; sourceTree = "<group>"; };
|
||||||
0EAC57332494277A00D0FCE0 /* VPNStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = "<group>"; };
|
0EAC57332494277A00D0FCE0 /* VPNStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1036,10 +1045,13 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */,
|
0EAC57322494277A00D0FCE0 /* MockVPNProvider.swift */,
|
||||||
|
0EA9F29426D6CA7E00E806E1 /* NetworkExtensionLocator.swift */,
|
||||||
0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */,
|
0EA9F28826D69FAE00E806E1 /* NetworkExtensionVPNConfiguration.swift */,
|
||||||
|
0EA9F28B26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift */,
|
||||||
0EAC57362494277A00D0FCE0 /* VPN.swift */,
|
0EAC57362494277A00D0FCE0 /* VPN.swift */,
|
||||||
0EAC57342494277A00D0FCE0 /* VPNConfiguration.swift */,
|
0EAC57342494277A00D0FCE0 /* VPNConfiguration.swift */,
|
||||||
0EAC57352494277A00D0FCE0 /* VPNProvider.swift */,
|
0EAC57352494277A00D0FCE0 /* VPNProvider.swift */,
|
||||||
|
0EA9F28E26D6AC6D00E806E1 /* VPNProviderIPC.swift */,
|
||||||
0EAC57332494277A00D0FCE0 /* VPNStatus.swift */,
|
0EAC57332494277A00D0FCE0 /* VPNStatus.swift */,
|
||||||
);
|
);
|
||||||
path = Manager;
|
path = Manager;
|
||||||
|
@ -1993,6 +2005,7 @@
|
||||||
0E23B3DE229749C600304C30 /* LinkProducer.swift in Sources */,
|
0E23B3DE229749C600304C30 /* LinkProducer.swift in Sources */,
|
||||||
0E23B42722982AF800304C30 /* CryptoAEAD.m in Sources */,
|
0E23B42722982AF800304C30 /* CryptoAEAD.m in Sources */,
|
||||||
0EE2F9F222918DA100F56F49 /* NETunnelInterface.swift in Sources */,
|
0EE2F9F222918DA100F56F49 /* NETunnelInterface.swift in Sources */,
|
||||||
|
0EA9F28C26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */,
|
||||||
0E23B46722982AF800304C30 /* Authenticator.swift in Sources */,
|
0E23B46722982AF800304C30 /* Authenticator.swift in Sources */,
|
||||||
0E23B43922982AF800304C30 /* OpenVPNError.swift in Sources */,
|
0E23B43922982AF800304C30 /* OpenVPNError.swift in Sources */,
|
||||||
0EAC573F2494277A00D0FCE0 /* VPNProvider.swift in Sources */,
|
0EAC573F2494277A00D0FCE0 /* VPNProvider.swift in Sources */,
|
||||||
|
@ -2054,6 +2067,7 @@
|
||||||
0E23B43722982AF800304C30 /* TLSBox.m in Sources */,
|
0E23B43722982AF800304C30 /* TLSBox.m in Sources */,
|
||||||
0EFEB4702006D3C800F81029 /* Allocation.m in Sources */,
|
0EFEB4702006D3C800F81029 /* Allocation.m in Sources */,
|
||||||
0EE2FA0A22918DA100F56F49 /* InterfaceObserver.swift in Sources */,
|
0EE2FA0A22918DA100F56F49 /* InterfaceObserver.swift in Sources */,
|
||||||
|
0EA9F28F26D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */,
|
||||||
0E23B43F22982AF800304C30 /* OpenVPNSession.swift in Sources */,
|
0E23B43F22982AF800304C30 /* OpenVPNSession.swift in Sources */,
|
||||||
0E23B44722982AF800304C30 /* OpenVPN.swift in Sources */,
|
0E23B44722982AF800304C30 /* OpenVPN.swift in Sources */,
|
||||||
0ECC60D82254981A0020BEAC /* ConfigurationError.swift in Sources */,
|
0ECC60D82254981A0020BEAC /* ConfigurationError.swift in Sources */,
|
||||||
|
@ -2066,6 +2080,7 @@
|
||||||
0E23B45922982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */,
|
0E23B45922982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */,
|
||||||
0EFEB45A2006D3C800F81029 /* TunnelInterface.swift in Sources */,
|
0EFEB45A2006D3C800F81029 /* TunnelInterface.swift in Sources */,
|
||||||
0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */,
|
0EAC57372494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */,
|
||||||
|
0EA9F29526D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */,
|
||||||
0E23B47322982AF800304C30 /* MSS.m in Sources */,
|
0E23B47322982AF800304C30 /* MSS.m in Sources */,
|
||||||
0EAC573D2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */,
|
0EAC573D2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */,
|
||||||
0E23B45D22982AF800304C30 /* ProtocolMacros.swift in Sources */,
|
0E23B45D22982AF800304C30 /* ProtocolMacros.swift in Sources */,
|
||||||
|
@ -2084,6 +2099,7 @@
|
||||||
0E23B3DF229749C600304C30 /* LinkProducer.swift in Sources */,
|
0E23B3DF229749C600304C30 /* LinkProducer.swift in Sources */,
|
||||||
0E23B42822982AF800304C30 /* CryptoAEAD.m in Sources */,
|
0E23B42822982AF800304C30 /* CryptoAEAD.m in Sources */,
|
||||||
0EE2F9F322918DA100F56F49 /* NETunnelInterface.swift in Sources */,
|
0EE2F9F322918DA100F56F49 /* NETunnelInterface.swift in Sources */,
|
||||||
|
0EA9F28D26D6A1EE00E806E1 /* NetworkExtensionVPNProvider.swift in Sources */,
|
||||||
0E23B46822982AF800304C30 /* Authenticator.swift in Sources */,
|
0E23B46822982AF800304C30 /* Authenticator.swift in Sources */,
|
||||||
0E23B43A22982AF800304C30 /* OpenVPNError.swift in Sources */,
|
0E23B43A22982AF800304C30 /* OpenVPNError.swift in Sources */,
|
||||||
0EAC57402494277A00D0FCE0 /* VPNProvider.swift in Sources */,
|
0EAC57402494277A00D0FCE0 /* VPNProvider.swift in Sources */,
|
||||||
|
@ -2145,6 +2161,7 @@
|
||||||
0E23B43822982AF800304C30 /* TLSBox.m in Sources */,
|
0E23B43822982AF800304C30 /* TLSBox.m in Sources */,
|
||||||
0EFEB4902006D7F300F81029 /* TunnelInterface.swift in Sources */,
|
0EFEB4902006D7F300F81029 /* TunnelInterface.swift in Sources */,
|
||||||
0EFEB49E2006D7F300F81029 /* Allocation.m in Sources */,
|
0EFEB49E2006D7F300F81029 /* Allocation.m in Sources */,
|
||||||
|
0EA9F29026D6AC6D00E806E1 /* VPNProviderIPC.swift in Sources */,
|
||||||
0E23B44022982AF800304C30 /* OpenVPNSession.swift in Sources */,
|
0E23B44022982AF800304C30 /* OpenVPNSession.swift in Sources */,
|
||||||
0E23B44822982AF800304C30 /* OpenVPN.swift in Sources */,
|
0E23B44822982AF800304C30 /* OpenVPN.swift in Sources */,
|
||||||
0ECC60D92254981A0020BEAC /* ConfigurationError.swift in Sources */,
|
0ECC60D92254981A0020BEAC /* ConfigurationError.swift in Sources */,
|
||||||
|
@ -2157,6 +2174,7 @@
|
||||||
0E23B45A22982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */,
|
0E23B45A22982AF800304C30 /* OpenVPNTunnelProvider.swift in Sources */,
|
||||||
0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */,
|
0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */,
|
||||||
0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */,
|
0EAC57382494277A00D0FCE0 /* OpenVPNProvider.swift in Sources */,
|
||||||
|
0EA9F29626D6CA7E00E806E1 /* NetworkExtensionLocator.swift in Sources */,
|
||||||
0E23B47422982AF800304C30 /* MSS.m in Sources */,
|
0E23B47422982AF800304C30 /* MSS.m in Sources */,
|
||||||
0EAC573E2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */,
|
0EAC573E2494277A00D0FCE0 /* VPNConfiguration.swift in Sources */,
|
||||||
0E23B45E22982AF800304C30 /* ProtocolMacros.swift in Sources */,
|
0E23B45E22982AF800304C30 /* ProtocolMacros.swift in Sources */,
|
||||||
|
|
|
@ -26,7 +26,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// :nodoc:
|
/// :nodoc:
|
||||||
public class MockVPNProvider: VPNProvider {
|
public class MockVPNProvider: VPNProvider, VPNProviderIPC {
|
||||||
|
|
||||||
|
// MARK: VPNProvider
|
||||||
|
|
||||||
public let isPrepared: Bool = true
|
public let isPrepared: Bool = true
|
||||||
|
|
||||||
public private(set) var isEnabled: Bool = false
|
public private(set) var isEnabled: Bool = false
|
||||||
|
@ -71,6 +74,8 @@ public class MockVPNProvider: VPNProvider {
|
||||||
completionHandler?()
|
completionHandler?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: VPNProviderIPC
|
||||||
|
|
||||||
public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) {
|
public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) {
|
||||||
let log = [String](repeating: "lorem ipsum", count: 1000).joined(separator: " ")
|
let log = [String](repeating: "lorem ipsum", count: 1000).joined(separator: " ")
|
||||||
completionHandler(log)
|
completionHandler(log)
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// NetworkExtensionLocator.swift
|
||||||
|
// TunnelKit
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 8/25/21.
|
||||||
|
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of TunnelKit.
|
||||||
|
//
|
||||||
|
// TunnelKit is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// TunnelKit is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with TunnelKit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
|
public protocol NetworkExtensionLocator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Looks up the VPN manager.
|
||||||
|
|
||||||
|
- Parameter completionHandler: The completion handler with a `NEVPNManager` or an error (if not found).
|
||||||
|
*/
|
||||||
|
func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NetworkExtensionNativeLocator: NetworkExtensionLocator {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: NetworkExtensionLocator
|
||||||
|
|
||||||
|
public func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) {
|
||||||
|
let manager = NEVPNManager.shared()
|
||||||
|
manager.loadFromPreferences { (error) in
|
||||||
|
guard error == nil else {
|
||||||
|
completionHandler(nil, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completionHandler(manager, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NetworkExtensionTunnelLocator: NetworkExtensionLocator {
|
||||||
|
private let bundleIdentifier: String
|
||||||
|
|
||||||
|
public init(bundleIdentifier: String) {
|
||||||
|
self.bundleIdentifier = bundleIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: NetworkExtensionLocator
|
||||||
|
|
||||||
|
public func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) {
|
||||||
|
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
|
||||||
|
guard error == nil else {
|
||||||
|
completionHandler(nil, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let manager = managers?.first {
|
||||||
|
guard let ptm = $0.protocolConfiguration as? NETunnelProviderProtocol else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (ptm.providerBundleIdentifier == self.bundleIdentifier)
|
||||||
|
}
|
||||||
|
completionHandler(manager ?? NETunnelProviderManager(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
//
|
||||||
|
// NetworkExtensionVPNProvider.swift
|
||||||
|
// TunnelKit
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 6/15/18.
|
||||||
|
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of TunnelKit.
|
||||||
|
//
|
||||||
|
// TunnelKit is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// TunnelKit is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with TunnelKit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import NetworkExtension
|
||||||
|
import SwiftyBeaver
|
||||||
|
|
||||||
|
private let log = SwiftyBeaver.self
|
||||||
|
|
||||||
|
public class NetworkExtensionVPNProvider: VPNProvider {
|
||||||
|
private var manager: NEVPNManager?
|
||||||
|
|
||||||
|
private let locator: NetworkExtensionLocator
|
||||||
|
|
||||||
|
private var lastNotifiedStatus: VPNStatus?
|
||||||
|
|
||||||
|
public init(locator: NetworkExtensionLocator) {
|
||||||
|
self.locator = locator
|
||||||
|
|
||||||
|
let nc = NotificationCenter.default
|
||||||
|
nc.addObserver(self, selector: #selector(vpnDidUpdate(_:)), name: .NEVPNStatusDidChange, object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(vpnDidReinstall(_:)), name: .NEVPNConfigurationChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: VPNProvider
|
||||||
|
|
||||||
|
public var isPrepared: Bool {
|
||||||
|
return manager != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isEnabled: Bool {
|
||||||
|
guard let manager = manager else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return manager.isEnabled && manager.isOnDemandEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
public var status: VPNStatus {
|
||||||
|
guard let neStatus = manager?.connection.status else {
|
||||||
|
return .disconnected
|
||||||
|
}
|
||||||
|
switch neStatus {
|
||||||
|
case .connected:
|
||||||
|
return .connected
|
||||||
|
|
||||||
|
case .connecting, .reasserting:
|
||||||
|
return .connecting
|
||||||
|
|
||||||
|
case .disconnecting:
|
||||||
|
return .disconnecting
|
||||||
|
|
||||||
|
case .disconnected, .invalid:
|
||||||
|
return .disconnected
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
return .disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func prepare(completionHandler: (() -> Void)?) {
|
||||||
|
locator.lookup { manager, error in
|
||||||
|
self.manager = manager
|
||||||
|
NotificationCenter.default.post(name: VPN.didPrepare, object: nil)
|
||||||
|
completionHandler?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
||||||
|
guard let configuration = configuration as? NetworkExtensionVPNConfiguration else {
|
||||||
|
fatalError("Not a NetworkExtensionVPNConfiguration")
|
||||||
|
}
|
||||||
|
locator.lookup { manager, error in
|
||||||
|
guard let manager = manager else {
|
||||||
|
completionHandler?(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.manager = manager
|
||||||
|
manager.localizedDescription = configuration.title
|
||||||
|
manager.protocolConfiguration = configuration.protocolConfiguration
|
||||||
|
manager.onDemandRules = configuration.onDemandRules
|
||||||
|
manager.isOnDemandEnabled = true
|
||||||
|
manager.isEnabled = true
|
||||||
|
manager.saveToPreferences { error in
|
||||||
|
guard error == nil else {
|
||||||
|
manager.isOnDemandEnabled = false
|
||||||
|
manager.isEnabled = false
|
||||||
|
completionHandler?(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.loadFromPreferences { error in
|
||||||
|
completionHandler?(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func connect(completionHandler: ((Error?) -> Void)?) {
|
||||||
|
do {
|
||||||
|
try manager?.connection.startVPNTunnel()
|
||||||
|
completionHandler?(nil)
|
||||||
|
} catch let e {
|
||||||
|
completionHandler?(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func disconnect(completionHandler: ((Error?) -> Void)?) {
|
||||||
|
guard let manager = manager else {
|
||||||
|
completionHandler?(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.connection.stopVPNTunnel()
|
||||||
|
manager.isOnDemandEnabled = false
|
||||||
|
manager.isEnabled = false
|
||||||
|
manager.saveToPreferences(completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
||||||
|
guard let configuration = configuration as? NetworkExtensionVPNConfiguration else {
|
||||||
|
fatalError("Not a NetworkExtensionVPNConfiguration")
|
||||||
|
}
|
||||||
|
install(configuration: configuration) { error in
|
||||||
|
guard error == nil else {
|
||||||
|
completionHandler?(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let connectBlock = {
|
||||||
|
self.connect(completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
if self.status != .disconnected {
|
||||||
|
self.manager?.connection.stopVPNTunnel()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + CoreConfiguration.reconnectionDelay, execute: connectBlock)
|
||||||
|
} else {
|
||||||
|
connectBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func uninstall(completionHandler: (() -> Void)?) {
|
||||||
|
locator.lookup { manager, error in
|
||||||
|
guard let manager = manager else {
|
||||||
|
completionHandler?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.connection.stopVPNTunnel()
|
||||||
|
manager.removeFromPreferences { error in
|
||||||
|
self.manager = nil
|
||||||
|
completionHandler?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Helpers
|
||||||
|
|
||||||
|
public func lookup(completionHandler: @escaping (NEVPNManager?, Error?) -> Void) {
|
||||||
|
locator.lookup(completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Notifications
|
||||||
|
|
||||||
|
@objc private func vpnDidUpdate(_ notification: Notification) {
|
||||||
|
guard let connection = notification.object as? NETunnelProviderSession else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.debug("VPN status did change: \(connection.status.rawValue)")
|
||||||
|
|
||||||
|
let status = self.status
|
||||||
|
if let last = lastNotifiedStatus {
|
||||||
|
guard status != last else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastNotifiedStatus = status
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: VPN.didChangeStatus, object: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func vpnDidReinstall(_ notification: Notification) {
|
||||||
|
NotificationCenter.default.post(name: VPN.didReinstall, object: self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,26 +82,4 @@ public protocol VPNProvider: AnyObject {
|
||||||
- Parameter completionHandler: The completion handler.
|
- Parameter completionHandler: The completion handler.
|
||||||
*/
|
*/
|
||||||
func uninstall(completionHandler: (() -> Void)?)
|
func uninstall(completionHandler: (() -> Void)?)
|
||||||
|
|
||||||
/**
|
|
||||||
Request a debug log from the VPN.
|
|
||||||
|
|
||||||
- Parameter fallback: The block resolving to a fallback `String` if no debug log is available.
|
|
||||||
- Parameter completionHandler: The completion handler with the debug log.
|
|
||||||
*/
|
|
||||||
func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void)
|
|
||||||
|
|
||||||
/**
|
|
||||||
Requests the current received/sent bytes count from the VPN.
|
|
||||||
|
|
||||||
- Parameter completionHandler: The completion handler with an optional received/sent bytes count.
|
|
||||||
*/
|
|
||||||
func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void)
|
|
||||||
|
|
||||||
/**
|
|
||||||
Requests the server configuration from the VPN.
|
|
||||||
|
|
||||||
- Parameter completionHandler: The completion handler with an optional configuration object.
|
|
||||||
*/
|
|
||||||
func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// VPNProviderIPC.swift
|
||||||
|
// TunnelKit
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 8/25/21.
|
||||||
|
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of TunnelKit.
|
||||||
|
//
|
||||||
|
// TunnelKit is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// TunnelKit is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with TunnelKit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol VPNProviderIPC {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Request a debug log from the VPN.
|
||||||
|
|
||||||
|
- Parameter fallback: The block resolving to a fallback `String` if no debug log is available.
|
||||||
|
- Parameter completionHandler: The completion handler with the debug log.
|
||||||
|
*/
|
||||||
|
func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void)
|
||||||
|
|
||||||
|
/**
|
||||||
|
Requests the current received/sent bytes count from the VPN.
|
||||||
|
|
||||||
|
- Parameter completionHandler: The completion handler with an optional received/sent bytes count.
|
||||||
|
*/
|
||||||
|
func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void)
|
||||||
|
|
||||||
|
/**
|
||||||
|
Requests the server configuration from the VPN.
|
||||||
|
|
||||||
|
- Parameter completionHandler: The completion handler with an optional configuration object.
|
||||||
|
*/
|
||||||
|
func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void)
|
||||||
|
}
|
|
@ -27,154 +27,55 @@ import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
/// :nodoc:
|
/// :nodoc:
|
||||||
public class OpenVPNProvider: VPNProvider {
|
public class OpenVPNProvider: VPNProvider, VPNProviderIPC {
|
||||||
private let bundleIdentifier: String
|
private let provider: NetworkExtensionVPNProvider
|
||||||
|
|
||||||
private var manager: NETunnelProviderManager?
|
|
||||||
|
|
||||||
private var lastNotifiedStatus: VPNStatus?
|
|
||||||
|
|
||||||
public init(bundleIdentifier: String) {
|
public init(bundleIdentifier: String) {
|
||||||
self.bundleIdentifier = bundleIdentifier
|
provider = NetworkExtensionVPNProvider(locator: NetworkExtensionTunnelLocator(bundleIdentifier: bundleIdentifier))
|
||||||
|
|
||||||
let nc = NotificationCenter.default
|
|
||||||
nc.addObserver(self, selector: #selector(vpnDidUpdate(_:)), name: .NEVPNStatusDidChange, object: nil)
|
|
||||||
nc.addObserver(self, selector: #selector(vpnDidReinstall(_:)), name: .NEVPNConfigurationChange, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
NotificationCenter.default.removeObserver(self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: VPNProvider
|
// MARK: VPNProvider
|
||||||
|
|
||||||
public var isPrepared: Bool {
|
public var isPrepared: Bool {
|
||||||
return manager != nil
|
return provider.isPrepared
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isEnabled: Bool {
|
public var isEnabled: Bool {
|
||||||
guard let manager = manager else {
|
return provider.isEnabled
|
||||||
return false
|
|
||||||
}
|
|
||||||
return manager.isEnabled && manager.isOnDemandEnabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var status: VPNStatus {
|
public var status: VPNStatus {
|
||||||
guard let neStatus = manager?.connection.status else {
|
return provider.status
|
||||||
return .disconnected
|
|
||||||
}
|
|
||||||
switch neStatus {
|
|
||||||
case .connected:
|
|
||||||
return .connected
|
|
||||||
|
|
||||||
case .connecting, .reasserting:
|
|
||||||
return .connecting
|
|
||||||
|
|
||||||
case .disconnecting:
|
|
||||||
return .disconnecting
|
|
||||||
|
|
||||||
case .disconnected, .invalid:
|
|
||||||
return .disconnected
|
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
return .disconnected
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func prepare(completionHandler: (() -> Void)?) {
|
public func prepare(completionHandler: (() -> Void)?) {
|
||||||
find(with: bundleIdentifier) {
|
provider.prepare(completionHandler: completionHandler)
|
||||||
self.manager = $0
|
|
||||||
NotificationCenter.default.post(name: VPN.didPrepare, object: nil)
|
|
||||||
completionHandler?()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
public func install(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
||||||
guard let configuration = configuration as? NetworkExtensionVPNConfiguration else {
|
provider.install(configuration: configuration, completionHandler: completionHandler)
|
||||||
fatalError("Not a NetworkExtensionVPNConfiguration")
|
|
||||||
}
|
|
||||||
find(with: bundleIdentifier) {
|
|
||||||
guard let manager = $0 else {
|
|
||||||
completionHandler?(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.manager = manager
|
|
||||||
manager.localizedDescription = configuration.title
|
|
||||||
manager.protocolConfiguration = configuration.protocolConfiguration
|
|
||||||
manager.onDemandRules = configuration.onDemandRules
|
|
||||||
manager.isOnDemandEnabled = true
|
|
||||||
manager.isEnabled = true
|
|
||||||
manager.saveToPreferences { (error) in
|
|
||||||
guard error == nil else {
|
|
||||||
manager.isOnDemandEnabled = false
|
|
||||||
manager.isEnabled = false
|
|
||||||
completionHandler?(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.loadFromPreferences { (error) in
|
|
||||||
completionHandler?(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func connect(completionHandler: ((Error?) -> Void)?) {
|
public func connect(completionHandler: ((Error?) -> Void)?) {
|
||||||
do {
|
provider.connect(completionHandler: completionHandler)
|
||||||
try manager?.connection.startVPNTunnel()
|
|
||||||
completionHandler?(nil)
|
|
||||||
} catch let e {
|
|
||||||
completionHandler?(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func disconnect(completionHandler: ((Error?) -> Void)?) {
|
public func disconnect(completionHandler: ((Error?) -> Void)?) {
|
||||||
guard let manager = manager else {
|
provider.disconnect(completionHandler: completionHandler)
|
||||||
completionHandler?(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.connection.stopVPNTunnel()
|
|
||||||
manager.isOnDemandEnabled = false
|
|
||||||
manager.isEnabled = false
|
|
||||||
manager.saveToPreferences(completionHandler: completionHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
public func reconnect(configuration: VPNConfiguration, completionHandler: ((Error?) -> Void)?) {
|
||||||
guard let configuration = configuration as? NetworkExtensionVPNConfiguration else {
|
provider.reconnect(configuration: configuration, completionHandler: completionHandler)
|
||||||
fatalError("Not a NetworkExtensionVPNConfiguration")
|
|
||||||
}
|
|
||||||
install(configuration: configuration) { (error) in
|
|
||||||
guard error == nil else {
|
|
||||||
completionHandler?(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let connectBlock = {
|
|
||||||
self.connect(completionHandler: completionHandler)
|
|
||||||
}
|
|
||||||
if self.status != .disconnected {
|
|
||||||
self.manager?.connection.stopVPNTunnel()
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + CoreConfiguration.reconnectionDelay, execute: connectBlock)
|
|
||||||
} else {
|
|
||||||
connectBlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func uninstall(completionHandler: (() -> Void)?) {
|
public func uninstall(completionHandler: (() -> Void)?) {
|
||||||
find(with: bundleIdentifier) { (manager) in
|
provider.uninstall(completionHandler: completionHandler)
|
||||||
guard let manager = manager else {
|
|
||||||
completionHandler?()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.connection.stopVPNTunnel()
|
|
||||||
manager.removeFromPreferences { (error) in
|
|
||||||
self.manager = nil
|
|
||||||
completionHandler?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: VPNProviderIPC
|
||||||
|
|
||||||
public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) {
|
public func requestDebugLog(fallback: (() -> String)?, completionHandler: @escaping (String) -> Void) {
|
||||||
guard status != .disconnected else {
|
guard provider.status != .disconnected else {
|
||||||
completionHandler(fallback?() ?? "")
|
completionHandler(fallback?() ?? "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -190,9 +91,8 @@ public class OpenVPNProvider: VPNProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) {
|
public func requestBytesCount(completionHandler: @escaping ((UInt, UInt)?) -> Void) {
|
||||||
find(with: bundleIdentifier) {
|
provider.lookup { manager, error in
|
||||||
self.manager = $0
|
guard let session = manager?.connection as? NETunnelProviderSession else {
|
||||||
guard let session = self.manager?.connection as? NETunnelProviderSession else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
}
|
}
|
||||||
|
@ -221,9 +121,8 @@ public class OpenVPNProvider: VPNProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) {
|
public func requestServerConfiguration(completionHandler: @escaping (Any?) -> Void) {
|
||||||
find(with: bundleIdentifier) {
|
provider.lookup { manager, error in
|
||||||
self.manager = $0
|
guard let session = manager?.connection as? NETunnelProviderSession else {
|
||||||
guard let session = self.manager?.connection as? NETunnelProviderSession else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
}
|
}
|
||||||
|
@ -251,26 +150,9 @@ public class OpenVPNProvider: VPNProvider {
|
||||||
|
|
||||||
// MARK: Helpers
|
// MARK: Helpers
|
||||||
|
|
||||||
private func find(with bundleIdentifier: String, completionHandler: @escaping (NETunnelProviderManager?) -> Void) {
|
|
||||||
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
|
|
||||||
guard error == nil else {
|
|
||||||
completionHandler(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let manager = managers?.first {
|
|
||||||
guard let ptm = $0.protocolConfiguration as? NETunnelProviderProtocol else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (ptm.providerBundleIdentifier == bundleIdentifier)
|
|
||||||
}
|
|
||||||
completionHandler(manager ?? NETunnelProviderManager())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findAndRequestDebugLog(completionHandler: @escaping (String?) -> Void) {
|
private func findAndRequestDebugLog(completionHandler: @escaping (String?) -> Void) {
|
||||||
find(with: bundleIdentifier) {
|
provider.lookup { manager, error in
|
||||||
self.manager = $0
|
guard let session = manager?.connection as? NETunnelProviderSession else {
|
||||||
guard let session = self.manager?.connection as? NETunnelProviderSession else {
|
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -292,27 +174,4 @@ public class OpenVPNProvider: VPNProvider {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
|
||||||
|
|
||||||
@objc private func vpnDidUpdate(_ notification: Notification) {
|
|
||||||
// guard let connection = notification.object as? NETunnelProviderSession else {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// log.debug("VPN status did change: \(connection.status.rawValue)")
|
|
||||||
|
|
||||||
let status = self.status
|
|
||||||
if let last = lastNotifiedStatus {
|
|
||||||
guard status != last else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastNotifiedStatus = status
|
|
||||||
|
|
||||||
NotificationCenter.default.post(name: VPN.didChangeStatus, object: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func vpnDidReinstall(_ notification: Notification) {
|
|
||||||
NotificationCenter.default.post(name: VPN.didReinstall, object: self)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue