Invoke VPNManager methods with a Profile object (#309)
The VPNConfiguration parameter is opaque and tightly coupled to TunnelKit. Connecting to a Profile makes infinitely more sense, beyond simplifying the VPNManager class. Configuration building is fully delegated to the strategy (as it has to be). - VPNManager takes Profile and produces VPNConfigurationParameters - VPNManagerStrategy takes VPNConfigurationParameters (abstract) - TunnelKitVPNManagerStrategy takes VPNConfigurationParameters and produces TunnelKitVPNConfiguration internally
This commit is contained in:
parent
36cad41529
commit
3a59ac7684
|
@ -85,7 +85,7 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "PassepartoutCore",
|
name: "PassepartoutCore",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "GenericJSON", package: "generic-json-swift") // FIXME: arch, drop this
|
.product(name: "GenericJSON", package: "generic-json-swift")
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// MARK: App extensions
|
// MARK: App extensions
|
||||||
|
|
|
@ -52,3 +52,9 @@ extension TimeInterval {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimeInterval {
|
||||||
|
public var dispatchTimeInterval: DispatchTimeInterval {
|
||||||
|
.nanoseconds(Int(self * Double(NSEC_PER_SEC)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -43,4 +43,20 @@ extension Profile {
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(_ id: UUID = UUID(), name: String, configuration: OpenVPN.Configuration) {
|
||||||
|
let header = Header(
|
||||||
|
uuid: id,
|
||||||
|
name: name,
|
||||||
|
providerName: nil
|
||||||
|
)
|
||||||
|
self.init(header, configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ header: Header, configuration: OpenVPN.Configuration) {
|
||||||
|
self.header = header
|
||||||
|
currentVPNProtocol = .openVPN
|
||||||
|
host = Host()
|
||||||
|
host?.ovpnSettings = OpenVPNSettings(configuration: configuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,4 +39,20 @@ extension Profile {
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(_ id: UUID = UUID(), name: String, configuration: WireGuard.Configuration) {
|
||||||
|
let header = Header(
|
||||||
|
uuid: id,
|
||||||
|
name: name,
|
||||||
|
providerName: nil
|
||||||
|
)
|
||||||
|
self.init(header, configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ header: Header, configuration: WireGuard.Configuration) {
|
||||||
|
self.header = header
|
||||||
|
currentVPNProtocol = .wireGuard
|
||||||
|
host = Host()
|
||||||
|
host?.wgSettings = WireGuardSettings(configuration: configuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutCore
|
import PassepartoutCore
|
||||||
import TunnelKitOpenVPN
|
|
||||||
import TunnelKitWireGuard
|
|
||||||
|
|
||||||
public protocol ProfileSubtype {
|
public protocol ProfileSubtype {
|
||||||
var vpnProtocols: [VPNProtocolType] { get }
|
var vpnProtocols: [VPNProtocolType] { get }
|
||||||
|
@ -59,24 +57,6 @@ public struct Profile: Identifiable, Codable, Equatable {
|
||||||
currentVPNProtocol = .openVPN
|
currentVPNProtocol = .openVPN
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ id: UUID = UUID(), name: String, configuration: OpenVPN.Configuration) {
|
|
||||||
let header = Header(
|
|
||||||
uuid: id,
|
|
||||||
name: name,
|
|
||||||
providerName: nil
|
|
||||||
)
|
|
||||||
self.init(header, configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ id: UUID = UUID(), name: String, configuration: WireGuard.Configuration) {
|
|
||||||
let header = Header(
|
|
||||||
uuid: id,
|
|
||||||
name: name,
|
|
||||||
providerName: nil
|
|
||||||
)
|
|
||||||
self.init(header, configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ id: UUID = UUID(), name: String, provider: Profile.Provider) {
|
init(_ id: UUID = UUID(), name: String, provider: Profile.Provider) {
|
||||||
let header = Header(
|
let header = Header(
|
||||||
uuid: id,
|
uuid: id,
|
||||||
|
@ -86,20 +66,6 @@ public struct Profile: Identifiable, Codable, Equatable {
|
||||||
self.init(header, provider: provider)
|
self.init(header, provider: provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(_ header: Header, configuration: OpenVPN.Configuration) {
|
|
||||||
self.header = header
|
|
||||||
currentVPNProtocol = .openVPN
|
|
||||||
host = Host()
|
|
||||||
host?.ovpnSettings = OpenVPNSettings(configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(_ header: Header, configuration: WireGuard.Configuration) {
|
|
||||||
self.header = header
|
|
||||||
currentVPNProtocol = .wireGuard
|
|
||||||
host = Host()
|
|
||||||
host?.wgSettings = WireGuardSettings(configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(_ header: Header, provider: Profile.Provider) {
|
public init(_ header: Header, provider: Profile.Provider) {
|
||||||
guard let firstVPNProtocol = provider.vpnSettings.keys.first else {
|
guard let firstVPNProtocol = provider.vpnSettings.keys.first else {
|
||||||
fatalError("No VPN protocols defined in provider")
|
fatalError("No VPN protocols defined in provider")
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
//
|
|
||||||
// VPNConfiguration.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 6/22/22.
|
|
||||||
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout 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.
|
|
||||||
//
|
|
||||||
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import TunnelKitManager
|
|
||||||
|
|
||||||
public typealias VPNConfiguration = (neConfiguration: NetworkExtensionConfiguration, neExtra: NetworkExtensionExtra)
|
|
|
@ -25,11 +25,13 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutCore
|
import PassepartoutCore
|
||||||
import TunnelKitManager
|
import PassepartoutProviders
|
||||||
|
|
||||||
public struct VPNConfigurationParameters {
|
public struct VPNConfigurationParameters {
|
||||||
public let profile: Profile
|
public let profile: Profile
|
||||||
|
|
||||||
|
public let providerManager: ProviderManager
|
||||||
|
|
||||||
public var title: String {
|
public var title: String {
|
||||||
profile.header.name
|
profile.header.name
|
||||||
}
|
}
|
||||||
|
@ -52,12 +54,14 @@ public struct VPNConfigurationParameters {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ profile: Profile,
|
_ profile: Profile,
|
||||||
|
providerManager: ProviderManager,
|
||||||
preferences: VPNPreferences,
|
preferences: VPNPreferences,
|
||||||
passwordReference: Data?,
|
passwordReference: Data?,
|
||||||
withNetworkSettings: Bool,
|
withNetworkSettings: Bool,
|
||||||
withCustomRules: Bool
|
withCustomRules: Bool
|
||||||
) {
|
) {
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
|
self.providerManager = providerManager
|
||||||
self.preferences = preferences
|
self.preferences = preferences
|
||||||
self.passwordReference = passwordReference
|
self.passwordReference = passwordReference
|
||||||
self.withNetworkSettings = withNetworkSettings
|
self.withNetworkSettings = withNetworkSettings
|
||||||
|
|
|
@ -65,10 +65,9 @@ extension VPNManager {
|
||||||
if let newPassword {
|
if let newPassword {
|
||||||
profile.account.password = newPassword
|
profile.account.password = newPassword
|
||||||
}
|
}
|
||||||
let cfg = try vpnConfiguration(withProfile: profile)
|
|
||||||
|
|
||||||
profileManager.activateProfile(profile)
|
profileManager.activateProfile(profile)
|
||||||
await reconnect(cfg)
|
await reconnect(profile)
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,14 +98,13 @@ extension VPNManager {
|
||||||
|
|
||||||
pp_log.info("Connecting to: \(profile.logDescription) @ \(newServer.logDescription)")
|
pp_log.info("Connecting to: \(profile.logDescription) @ \(newServer.logDescription)")
|
||||||
profile.setProviderServer(newServer)
|
profile.setProviderServer(newServer)
|
||||||
let cfg = try vpnConfiguration(withProfile: profile)
|
|
||||||
|
|
||||||
profileManager.activateProfile(profile)
|
profileManager.activateProfile(profile)
|
||||||
guard !profileManager.isCurrentProfile(profileId) else {
|
guard !profileManager.isCurrentProfile(profileId) else {
|
||||||
pp_log.debug("Active profile is current, will reconnect via observation")
|
pp_log.debug("Active profile is current, will reconnect via observation")
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
await reconnect(cfg)
|
await reconnect(profile)
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,13 +116,12 @@ extension VPNManager {
|
||||||
|
|
||||||
pp_log.info("Modifying active profile")
|
pp_log.info("Modifying active profile")
|
||||||
block(&profile)
|
block(&profile)
|
||||||
let cfg = try vpnConfiguration(withProfile: profile)
|
|
||||||
|
|
||||||
profileManager.activateProfile(profile)
|
profileManager.activateProfile(profile)
|
||||||
guard !profileManager.isCurrentProfile(profile.id) else {
|
guard !profileManager.isCurrentProfile(profile.id) else {
|
||||||
pp_log.debug("Active profile is current, will reinstate via observation")
|
pp_log.debug("Active profile is current, will reinstate via observation")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await reinstate(cfg)
|
await reinstate(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,16 +84,28 @@ public final class VPNManager: ObservableObject {
|
||||||
currentState = ObservableVPNState()
|
currentState = ObservableVPNState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func reinstate(_ configuration: VPNConfiguration) async {
|
func reinstate(_ profile: Profile) async {
|
||||||
pp_log.info("Reinstating VPN")
|
pp_log.info("Reinstating VPN")
|
||||||
clearLastError()
|
clearLastError()
|
||||||
await strategy.reinstate(configuration: configuration)
|
do {
|
||||||
|
let parameters = try vpnConfigurationParameters(withProfile: profile)
|
||||||
|
await strategy.reinstate(parameters)
|
||||||
|
} catch {
|
||||||
|
pp_log.error("Unable to build configuration: \(error)")
|
||||||
|
configurationError.send((profile, error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconnect(_ configuration: VPNConfiguration) async {
|
func reconnect(_ profile: Profile) async {
|
||||||
pp_log.info("Reconnecting VPN (with new configuration)")
|
pp_log.info("Reconnecting VPN (with new configuration)")
|
||||||
clearLastError()
|
clearLastError()
|
||||||
await strategy.connect(configuration: configuration)
|
do {
|
||||||
|
let parameters = try vpnConfigurationParameters(withProfile: profile)
|
||||||
|
await strategy.connect(parameters)
|
||||||
|
} catch {
|
||||||
|
pp_log.error("Unable to build configuration: \(error)")
|
||||||
|
configurationError.send((profile, error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reconnect() async {
|
public func reconnect() async {
|
||||||
|
@ -139,7 +151,11 @@ extension VPNManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func observeStrategy() {
|
private func observeStrategy() {
|
||||||
strategy.observe(into: MutableObservableVPNState(currentState))
|
strategy.observe(into: MutableObservableVPNState(currentState)) { profile, error in
|
||||||
|
|
||||||
|
// UI is certainly interested in configuration errors
|
||||||
|
self.configurationError.send((profile, error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func observeProfileManager() {
|
private func observeProfileManager() {
|
||||||
|
@ -233,72 +249,51 @@ extension VPNManager {
|
||||||
guard isHandled else {
|
guard isHandled else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let cfg = vpnConfigurationWithCurrentProfile() else {
|
guard profileManager.isActiveProfile(newProfile.id) else {
|
||||||
|
pp_log.info("Skipping VPN reaction, current profile is not active")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if shouldReconnect {
|
if shouldReconnect {
|
||||||
await reconnect(cfg)
|
await reconnect(newProfile)
|
||||||
} else {
|
} else {
|
||||||
await reinstate(cfg)
|
await reinstate(newProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Configuration
|
// MARK: Configuration
|
||||||
|
|
||||||
extension VPNManager {
|
private extension VPNManager {
|
||||||
func vpnConfigurationWithCurrentProfile() -> VPNConfiguration? {
|
func vpnConfigurationParameters(withProfile profile: Profile) throws -> VPNConfigurationParameters {
|
||||||
do {
|
if profile.requiresCredentials {
|
||||||
guard profileManager.isCurrentProfileActive() else {
|
guard !profile.account.isEmpty else {
|
||||||
pp_log.info("Skipping VPN configuration, current profile is not active")
|
throw PassepartoutError.missingAccount
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return try vpnConfiguration(withProfile: profileManager.currentProfile.value)
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func vpnConfiguration(withProfile profile: Profile) throws -> VPNConfiguration {
|
// specific provider customizations
|
||||||
do {
|
var newPassword: String?
|
||||||
if profile.requiresCredentials {
|
if let providerName = profile.providerName {
|
||||||
guard !profile.account.isEmpty else {
|
switch providerName {
|
||||||
throw PassepartoutError.missingAccount
|
case .mullvad:
|
||||||
}
|
newPassword = "m"
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// specific provider customizations
|
|
||||||
var newPassword: String?
|
|
||||||
if let providerName = profile.providerName {
|
|
||||||
switch providerName {
|
|
||||||
case .mullvad:
|
|
||||||
newPassword = "m"
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: must commit password to keychain (tunnel needs a password reference)
|
|
||||||
profileManager.savePassword(forProfile: profile, newPassword: newPassword)
|
|
||||||
|
|
||||||
let parameters = VPNConfigurationParameters(
|
|
||||||
profile,
|
|
||||||
preferences: vpnPreferences,
|
|
||||||
passwordReference: profileManager.passwordReference(forProfile: profile),
|
|
||||||
withNetworkSettings: isNetworkSettingsSupported(),
|
|
||||||
withCustomRules: isOnDemandRulesSupported()
|
|
||||||
)
|
|
||||||
|
|
||||||
return try strategy.vpnConfiguration(parameters, providerManager: providerManager)
|
|
||||||
} catch {
|
|
||||||
pp_log.error("Unable to build VPNConfiguration: \(error)")
|
|
||||||
|
|
||||||
// UI is certainly interested in configuration errors
|
|
||||||
configurationError.send((profile, error))
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: must commit password to keychain (tunnel needs a password reference)
|
||||||
|
profileManager.savePassword(forProfile: profile, newPassword: newPassword)
|
||||||
|
|
||||||
|
return VPNConfigurationParameters(
|
||||||
|
profile,
|
||||||
|
providerManager: providerManager,
|
||||||
|
preferences: vpnPreferences,
|
||||||
|
passwordReference: profileManager.passwordReference(forProfile: profile),
|
||||||
|
withNetworkSettings: isNetworkSettingsSupported(),
|
||||||
|
withCustomRules: isOnDemandRulesSupported()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,11 @@ import PassepartoutCore
|
||||||
import PassepartoutProviders
|
import PassepartoutProviders
|
||||||
|
|
||||||
public protocol VPNManagerStrategy {
|
public protocol VPNManagerStrategy {
|
||||||
func observe(into state: MutableObservableVPNState)
|
func observe(into state: MutableObservableVPNState, onConfigurationError: @escaping (Profile, Error) -> Void)
|
||||||
|
|
||||||
func reinstate(configuration: VPNConfiguration) async
|
func reinstate(_ parameters: VPNConfigurationParameters) async
|
||||||
|
|
||||||
func connect(configuration: VPNConfiguration) async
|
func connect(_ parameters: VPNConfigurationParameters) async
|
||||||
|
|
||||||
func reconnect() async
|
func reconnect() async
|
||||||
|
|
||||||
|
@ -44,9 +44,4 @@ public protocol VPNManagerStrategy {
|
||||||
func serverConfiguration(forProtocol vpnProtocol: VPNProtocolType) -> Any?
|
func serverConfiguration(forProtocol vpnProtocol: VPNProtocolType) -> Any?
|
||||||
|
|
||||||
func debugLogURL(forProtocol vpnProtocol: VPNProtocolType) -> URL?
|
func debugLogURL(forProtocol vpnProtocol: VPNProtocolType) -> URL?
|
||||||
|
|
||||||
func vpnConfiguration(
|
|
||||||
_ parameters: VPNConfigurationParameters,
|
|
||||||
providerManager: ProviderManager
|
|
||||||
) throws -> VPNConfiguration
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import TunnelKitManager
|
||||||
import TunnelKitOpenVPN
|
import TunnelKitOpenVPN
|
||||||
|
|
||||||
extension Profile.OpenVPNSettings: TunnelKitConfigurationProviding {
|
extension Profile.OpenVPNSettings: TunnelKitConfigurationProviding {
|
||||||
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> VPNConfiguration {
|
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> TunnelKitVPNConfiguration {
|
||||||
var customBuilder = configuration.builder()
|
var customBuilder = configuration.builder()
|
||||||
|
|
||||||
// tolerate widest range of certificates
|
// tolerate widest range of certificates
|
||||||
|
|
|
@ -30,7 +30,7 @@ import TunnelKitManager
|
||||||
import TunnelKitWireGuard
|
import TunnelKitWireGuard
|
||||||
|
|
||||||
extension Profile.WireGuardSettings: TunnelKitConfigurationProviding {
|
extension Profile.WireGuardSettings: TunnelKitConfigurationProviding {
|
||||||
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> VPNConfiguration {
|
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> TunnelKitVPNConfiguration {
|
||||||
var customBuilder = configuration.builder()
|
var customBuilder = configuration.builder()
|
||||||
|
|
||||||
// network settings
|
// network settings
|
||||||
|
|
|
@ -33,8 +33,10 @@ import TunnelKitCore
|
||||||
import TunnelKitManager
|
import TunnelKitManager
|
||||||
import TunnelKitOpenVPNCore
|
import TunnelKitOpenVPNCore
|
||||||
|
|
||||||
|
typealias TunnelKitVPNConfiguration = (neConfiguration: NetworkExtensionConfiguration, neExtra: NetworkExtensionExtra)
|
||||||
|
|
||||||
protocol TunnelKitConfigurationProviding {
|
protocol TunnelKitConfigurationProviding {
|
||||||
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> VPNConfiguration
|
func tunnelKitConfiguration(_ appGroup: String, parameters: VPNConfigurationParameters) throws -> TunnelKitVPNConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy where VPNType.Configuration == NetworkExtensionConfiguration, VPNType.Extra == NetworkExtensionExtra {
|
public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy where VPNType.Configuration == NetworkExtensionConfiguration, VPNType.Extra == NetworkExtensionExtra {
|
||||||
|
@ -49,8 +51,6 @@ public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let reconnectionSeconds = 2
|
|
||||||
|
|
||||||
private let appGroup: String
|
private let appGroup: String
|
||||||
|
|
||||||
private let tunnelBundleIdentifier: (VPNProtocolType) -> String
|
private let tunnelBundleIdentifier: (VPNProtocolType) -> String
|
||||||
|
@ -59,12 +59,16 @@ public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy
|
||||||
|
|
||||||
private let vpn: VPNType
|
private let vpn: VPNType
|
||||||
|
|
||||||
|
private let reconnectionInterval: TimeInterval
|
||||||
|
|
||||||
private let dataCountInterval: TimeInterval
|
private let dataCountInterval: TimeInterval
|
||||||
|
|
||||||
// MARK: State
|
// MARK: State
|
||||||
|
|
||||||
private var currentState: MutableObservableVPNState?
|
private var currentState: MutableObservableVPNState?
|
||||||
|
|
||||||
|
private var onConfigurationError: ((Profile, Error) -> Void)?
|
||||||
|
|
||||||
private let vpnState = CurrentValueSubject<AtomicState, Never>(.init())
|
private let vpnState = CurrentValueSubject<AtomicState, Never>(.init())
|
||||||
|
|
||||||
private var dataCountTimer: AnyCancellable?
|
private var dataCountTimer: AnyCancellable?
|
||||||
|
@ -79,6 +83,7 @@ public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy
|
||||||
appGroup: String,
|
appGroup: String,
|
||||||
tunnelBundleIdentifier: @escaping (VPNProtocolType) -> String,
|
tunnelBundleIdentifier: @escaping (VPNProtocolType) -> String,
|
||||||
vpn: VPNType,
|
vpn: VPNType,
|
||||||
|
reconnectionInterval: TimeInterval = 2.0,
|
||||||
dataCountInterval: TimeInterval = 3.0
|
dataCountInterval: TimeInterval = 3.0
|
||||||
) {
|
) {
|
||||||
self.appGroup = appGroup
|
self.appGroup = appGroup
|
||||||
|
@ -88,6 +93,7 @@ public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy
|
||||||
}
|
}
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
self.vpn = vpn
|
self.vpn = vpn
|
||||||
|
self.reconnectionInterval = reconnectionInterval
|
||||||
self.dataCountInterval = dataCountInterval
|
self.dataCountInterval = dataCountInterval
|
||||||
|
|
||||||
registerNotification(withName: VPNNotification.didReinstall) {
|
registerNotification(withName: VPNNotification.didReinstall) {
|
||||||
|
@ -115,8 +121,9 @@ public final class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
|
|
||||||
extension TunnelKitVPNManagerStrategy {
|
extension TunnelKitVPNManagerStrategy {
|
||||||
public func observe(into state: MutableObservableVPNState) {
|
public func observe(into state: MutableObservableVPNState, onConfigurationError: @escaping (Profile, Error) -> Void) {
|
||||||
currentState = state
|
currentState = state
|
||||||
|
self.onConfigurationError = onConfigurationError
|
||||||
|
|
||||||
// use this to drop redundant NE notifications
|
// use this to drop redundant NE notifications
|
||||||
vpnState
|
vpnState
|
||||||
|
@ -128,7 +135,10 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reinstate(configuration: VPNConfiguration) async {
|
public func reinstate(_ parameters: VPNConfigurationParameters) async {
|
||||||
|
guard let configuration = try? vpnConfiguration(withParameters: parameters) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let vpnType = configuration.neConfiguration as? VPNProtocolProviding else {
|
guard let vpnType = configuration.neConfiguration as? VPNProtocolProviding else {
|
||||||
fatalError("Configuration must implement VPNProtocolProviding")
|
fatalError("Configuration must implement VPNProtocolProviding")
|
||||||
}
|
}
|
||||||
|
@ -148,7 +158,10 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func connect(configuration: VPNConfiguration) async {
|
public func connect(_ parameters: VPNConfigurationParameters) async {
|
||||||
|
guard let configuration = try? vpnConfiguration(withParameters: parameters) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let vpnType = configuration.neConfiguration as? VPNProtocolProviding else {
|
guard let vpnType = configuration.neConfiguration as? VPNProtocolProviding else {
|
||||||
fatalError("Configuration must implement VPNProtocolProviding")
|
fatalError("Configuration must implement VPNProtocolProviding")
|
||||||
}
|
}
|
||||||
|
@ -162,7 +175,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
bundleIdentifier,
|
bundleIdentifier,
|
||||||
configuration: configuration.neConfiguration,
|
configuration: configuration.neConfiguration,
|
||||||
extra: configuration.neExtra,
|
extra: configuration.neExtra,
|
||||||
after: .seconds(reconnectionSeconds)
|
after: reconnectionInterval.dispatchTimeInterval
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log.error("Unable to connect: \(error)")
|
pp_log.error("Unable to connect: \(error)")
|
||||||
|
@ -171,7 +184,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
|
|
||||||
public func reconnect() async {
|
public func reconnect() async {
|
||||||
try? await vpn.reconnect(
|
try? await vpn.reconnect(
|
||||||
after: .seconds(reconnectionSeconds)
|
after: reconnectionInterval.dispatchTimeInterval
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,8 +205,8 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
extension TunnelKitVPNManagerStrategy {
|
private extension TunnelKitVPNManagerStrategy {
|
||||||
private func onVPNReinstall(_ notification: Notification) {
|
func onVPNReinstall(_ notification: Notification) {
|
||||||
guard isRelevantNotification(notification) else {
|
guard isRelevantNotification(notification) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -204,7 +217,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onVPNStatus(_ notification: Notification) {
|
func onVPNStatus(_ notification: Notification) {
|
||||||
|
|
||||||
// assume first notified identifier to be the relevant one
|
// assume first notified identifier to be the relevant one
|
||||||
// in order to restore VPN status on app launch
|
// in order to restore VPN status on app launch
|
||||||
|
@ -239,7 +252,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
currentState?.lastError = error
|
currentState?.lastError = error
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onVPNFail(_ notification: Notification) {
|
func onVPNFail(_ notification: Notification) {
|
||||||
vpnState.send(AtomicState(
|
vpnState.send(AtomicState(
|
||||||
isEnabled: notification.vpnIsEnabled,
|
isEnabled: notification.vpnIsEnabled,
|
||||||
vpnStatus: vpnState.value.vpnStatus
|
vpnStatus: vpnState.value.vpnStatus
|
||||||
|
@ -247,7 +260,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
currentState?.lastError = notification.vpnError
|
currentState?.lastError = notification.vpnError
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isRelevantNotification(_ notification: Notification) -> Bool {
|
func isRelevantNotification(_ notification: Notification) -> Bool {
|
||||||
guard let notificationTunnelIdentifier = notification.vpnBundleIdentifier else {
|
guard let notificationTunnelIdentifier = notification.vpnBundleIdentifier else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -260,7 +273,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
|
|
||||||
// MARK: Data count
|
// MARK: Data count
|
||||||
|
|
||||||
private func onDataCount(_: Date) {
|
func onDataCount(_: Date) {
|
||||||
switch vpnState.value.vpnStatus {
|
switch vpnState.value.vpnStatus {
|
||||||
case .connected:
|
case .connected:
|
||||||
guard let currentDataCount = currentDataCount else {
|
guard let currentDataCount = currentDataCount else {
|
||||||
|
@ -273,7 +286,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startCountingData() {
|
func startCountingData() {
|
||||||
guard dataCountTimer == nil else {
|
guard dataCountTimer == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -284,7 +297,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopCountingData() {
|
func stopCountingData() {
|
||||||
dataCountTimer?.cancel()
|
dataCountTimer?.cancel()
|
||||||
dataCountTimer = nil
|
dataCountTimer = nil
|
||||||
|
|
||||||
|
@ -292,6 +305,45 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Configuration
|
||||||
|
|
||||||
|
private extension TunnelKitVPNManagerStrategy {
|
||||||
|
func vpnConfiguration(withParameters parameters: VPNConfigurationParameters) throws -> TunnelKitVPNConfiguration {
|
||||||
|
let profile = parameters.profile
|
||||||
|
do {
|
||||||
|
switch profile.currentVPNProtocol {
|
||||||
|
case .openVPN:
|
||||||
|
let settings: Profile.OpenVPNSettings
|
||||||
|
if profile.isProvider {
|
||||||
|
settings = try profile.providerOpenVPNSettings(withManager: parameters.providerManager)
|
||||||
|
} else {
|
||||||
|
guard let hostSettings = profile.hostOpenVPNSettings else {
|
||||||
|
fatalError("Profile currentVPNProtocol is OpenVPN, but host has no OpenVPN settings")
|
||||||
|
}
|
||||||
|
settings = hostSettings
|
||||||
|
}
|
||||||
|
return try settings.tunnelKitConfiguration(appGroup, parameters: parameters)
|
||||||
|
|
||||||
|
case .wireGuard:
|
||||||
|
let settings: Profile.WireGuardSettings
|
||||||
|
if profile.isProvider {
|
||||||
|
settings = try profile.providerWireGuardSettings(withManager: parameters.providerManager)
|
||||||
|
} else {
|
||||||
|
guard let hostSettings = profile.hostWireGuardSettings else {
|
||||||
|
fatalError("Profile currentVPNProtocol is WireGuard, but host has no WireGuard settings")
|
||||||
|
}
|
||||||
|
settings = hostSettings
|
||||||
|
}
|
||||||
|
return try settings.tunnelKitConfiguration(appGroup, parameters: parameters)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
pp_log.error("Unable to build TunnelKitVPNConfiguration: \(error)")
|
||||||
|
onConfigurationError?(profile, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Pulled
|
// MARK: Pulled
|
||||||
|
|
||||||
extension TunnelKitVPNManagerStrategy {
|
extension TunnelKitVPNManagerStrategy {
|
||||||
|
@ -314,10 +366,12 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
return defaults.wireGuardURLForDebugLog(appGroup: appGroup)
|
return defaults.wireGuardURLForDebugLog(appGroup: appGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Callbacks
|
// MARK: Callbacks
|
||||||
|
|
||||||
private func lastError(withBundleIdentifier bundleIdentifier: String?) -> Error? {
|
private extension TunnelKitVPNManagerStrategy {
|
||||||
|
func lastError(withBundleIdentifier bundleIdentifier: String?) -> Error? {
|
||||||
switch bundleIdentifier {
|
switch bundleIdentifier {
|
||||||
case tunnelBundleIdentifier(.openVPN):
|
case tunnelBundleIdentifier(.openVPN):
|
||||||
return defaults.openVPNLastError
|
return defaults.openVPNLastError
|
||||||
|
@ -330,7 +384,7 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentDataCount: DataCount? {
|
var currentDataCount: DataCount? {
|
||||||
switch currentBundleIdentifier {
|
switch currentBundleIdentifier {
|
||||||
case tunnelBundleIdentifier(.openVPN):
|
case tunnelBundleIdentifier(.openVPN):
|
||||||
return defaults.openVPNDataCount
|
return defaults.openVPNDataCount
|
||||||
|
@ -340,36 +394,3 @@ extension TunnelKitVPNManagerStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Configuration
|
|
||||||
|
|
||||||
extension TunnelKitVPNManagerStrategy {
|
|
||||||
public func vpnConfiguration(_ parameters: VPNConfigurationParameters, providerManager: ProviderManager) throws -> VPNConfiguration {
|
|
||||||
let profile = parameters.profile
|
|
||||||
switch profile.currentVPNProtocol {
|
|
||||||
case .openVPN:
|
|
||||||
let settings: Profile.OpenVPNSettings
|
|
||||||
if profile.isProvider {
|
|
||||||
settings = try profile.providerOpenVPNSettings(withManager: providerManager)
|
|
||||||
} else {
|
|
||||||
guard let hostSettings = profile.hostOpenVPNSettings else {
|
|
||||||
fatalError("Profile currentVPNProtocol is OpenVPN, but host has no OpenVPN settings")
|
|
||||||
}
|
|
||||||
settings = hostSettings
|
|
||||||
}
|
|
||||||
return try settings.tunnelKitConfiguration(appGroup, parameters: parameters)
|
|
||||||
|
|
||||||
case .wireGuard:
|
|
||||||
let settings: Profile.WireGuardSettings
|
|
||||||
if profile.isProvider {
|
|
||||||
settings = try profile.providerWireGuardSettings(withManager: providerManager)
|
|
||||||
} else {
|
|
||||||
guard let hostSettings = profile.hostWireGuardSettings else {
|
|
||||||
fatalError("Profile currentVPNProtocol is WireGuard, but host has no WireGuard settings")
|
|
||||||
}
|
|
||||||
settings = hostSettings
|
|
||||||
}
|
|
||||||
return try settings.tunnelKitConfiguration(appGroup, parameters: parameters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ final class ProvidersTests: XCTestCase {
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
let model = NSManagedObjectModel.mergedModel(from: [.module])!
|
|
||||||
persistence = ProvidersPersistence(withName: "ProvidersTests", cloudKit: false, author: nil)
|
persistence = ProvidersPersistence(withName: "ProvidersTests", cloudKit: false, author: nil)
|
||||||
|
|
||||||
let remoteStrategy = APIRemoteProvidersStrategy(
|
let remoteStrategy = APIRemoteProvidersStrategy(
|
||||||
|
|
Loading…
Reference in New Issue