passepartout-apple/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift

353 lines
10 KiB
Swift
Raw Normal View History

2022-04-12 13:09:14 +00:00
//
// VPNManager.swift
2022-04-12 13:09:14 +00:00
// Passepartout
//
// Created by Davide De Rosa on 2/9/22.
2023-03-17 15:56:19 +00:00
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
2022-04-12 13:09:14 +00:00
//
// 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 Combine
import Foundation
2022-06-23 21:31:01 +00:00
import PassepartoutCore
import PassepartoutProviders
2022-04-12 13:09:14 +00:00
@MainActor
public final class VPNManager: ObservableObject {
2022-04-12 13:09:14 +00:00
// MARK: Initialization
2023-03-17 20:55:47 +00:00
private let store: KeyValueStore
2023-03-17 20:55:47 +00:00
let profileManager: ProfileManager
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
let providerManager: ProviderManager
private let strategy: VPNManagerStrategy
2023-03-17 20:55:47 +00:00
public var isNetworkSettingsSupported: () -> Bool
2023-03-17 20:55:47 +00:00
public var isOnDemandRulesSupported: () -> Bool
2022-04-12 13:09:14 +00:00
// MARK: State
2023-03-17 20:55:47 +00:00
2022-06-23 21:31:01 +00:00
public let currentState: ObservableVPNState
2023-03-17 20:55:47 +00:00
public let didUpdatePreferences = PassthroughSubject<VPNPreferences, Never>()
public private(set) var lastError: Error? {
2022-04-12 13:09:14 +00:00
get {
currentState.lastError
}
set {
currentState.lastError = newValue
}
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
// MARK: Internals
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private var lastProfile: Profile = .placeholder
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private var cancellables: Set<AnyCancellable> = []
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
public init(
store: KeyValueStore,
profileManager: ProfileManager,
2022-04-12 13:09:14 +00:00
providerManager: ProviderManager,
strategy: VPNManagerStrategy
) {
self.store = store
2022-04-12 13:09:14 +00:00
self.profileManager = profileManager
self.providerManager = providerManager
self.strategy = strategy
isNetworkSettingsSupported = { true }
isOnDemandRulesSupported = { true }
2022-04-12 13:09:14 +00:00
2022-06-23 21:31:01 +00:00
currentState = ObservableVPNState()
2022-04-12 13:09:14 +00:00
}
2023-07-02 10:51:50 +00:00
func reinstate(_ profile: Profile) async throws {
2022-04-12 13:09:14 +00:00
pp_log.info("Reinstating VPN")
clearLastError()
do {
let parameters = try vpnConfigurationParameters(withProfile: profile)
try await strategy.reinstate(parameters)
} catch {
pp_log.error("Unable to build configuration: \(error)")
2023-07-02 10:51:50 +00:00
throw error
}
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2023-07-02 10:51:50 +00:00
func reconnect(_ profile: Profile) async throws {
pp_log.info("Reconnecting VPN (with new configuration)")
clearLastError()
do {
let parameters = try vpnConfigurationParameters(withProfile: profile)
try await strategy.connect(parameters)
} catch {
pp_log.error("Unable to build configuration: \(error)")
2023-07-02 10:51:50 +00:00
throw error
}
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
public func reconnect() async {
pp_log.info("Reconnecting VPN")
clearLastError()
await strategy.reconnect()
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
public func disable() async {
pp_log.info("Disabling VPN")
clearLastError()
2022-04-12 13:09:14 +00:00
await strategy.disconnect()
}
public func uninstall() async {
pp_log.info("Uninstalling VPN")
clearLastError()
2022-04-12 13:09:14 +00:00
await strategy.removeConfigurations()
}
public func serverConfiguration(forProtocol vpnProtocol: VPNProtocolType) -> Any? {
2022-09-04 18:09:31 +00:00
strategy.serverConfiguration(forProtocol: vpnProtocol)
2022-04-12 13:09:14 +00:00
}
public func debugLogURL(forProtocol vpnProtocol: VPNProtocolType) -> URL? {
2022-09-04 18:09:31 +00:00
strategy.debugLogURL(forProtocol: vpnProtocol)
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
private func clearLastError() {
guard currentState.lastError != nil else {
return
}
currentState.lastError = nil
}
2022-04-12 13:09:14 +00:00
}
// MARK: Observation
extension VPNManager {
public func observeUpdates() {
observeStrategy()
observeProfileManager()
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private func observeStrategy() {
2023-07-02 10:51:50 +00:00
strategy.observe(into: MutableObservableVPNState(currentState))
2022-04-12 13:09:14 +00:00
}
private func observeProfileManager() {
profileManager.didUpdateActiveProfile
.dropFirst()
2022-04-12 13:09:14 +00:00
.removeDuplicates()
2023-09-08 14:20:01 +00:00
.sink { [weak self] newId in
2022-04-12 13:09:14 +00:00
Task {
2023-09-08 14:20:01 +00:00
await self?.willUpdateActiveId(newId)
2022-04-12 13:09:14 +00:00
}
}.store(in: &cancellables)
profileManager.currentProfile.$value
.dropFirst()
2022-04-12 13:09:14 +00:00
.removeDuplicates()
2023-09-08 14:20:01 +00:00
.sink { [weak self] newProfile in
2022-04-12 13:09:14 +00:00
Task {
2023-07-02 10:51:50 +00:00
do {
2023-09-08 14:20:01 +00:00
try await self?.willUpdateCurrentProfile(newProfile)
2023-07-02 10:51:50 +00:00
} catch {
pp_log.error("Unable to apply profile update: \(error)")
}
2022-04-12 13:09:14 +00:00
}
}.store(in: &cancellables)
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private func willUpdateActiveId(_ newId: UUID?) async {
guard let newId = newId else {
2022-04-12 13:09:14 +00:00
pp_log.info("No active profile, disconnecting VPN...")
await disable()
return
}
pp_log.debug("Active profile: \(newId)")
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2023-07-02 10:51:50 +00:00
private func willUpdateCurrentProfile(_ newProfile: Profile) async throws {
2022-04-12 13:09:14 +00:00
defer {
lastProfile = newProfile
}
// ignore if VPN disabled
guard currentState.isEnabled else {
pp_log.debug("Ignoring updates, VPN is disabled")
return
}
// ignore non-active profiles
guard profileManager.isActiveProfile(newProfile.id) else {
pp_log.debug("Ignoring updates, profile \(newProfile.logDescription) is not active")
return
}
// ignore profile changes, react on changes within same profile
guard newProfile.id == lastProfile.id else {
return
}
pp_log.debug("Active profile updated: \(newProfile.header.name)")
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var isHandled = false
var shouldReconnect = false
let notDisconnected = (currentState.vpnStatus != .disconnected)
// do not reconnect if connected
if newProfile.isProvider {
// server changed?
if newProfile.providerServerId != lastProfile.providerServerId {
pp_log.info("Provider server changed: \(newProfile.providerServerId?.description ?? "nil")")
2022-04-12 13:09:14 +00:00
isHandled = true
shouldReconnect = notDisconnected
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
// endpoint changed?
else if newProfile.providerCustomEndpoint != lastProfile.providerCustomEndpoint {
pp_log.info("Provider endpoint changed: \(newProfile.providerCustomEndpoint?.description ?? "automatic")")
2022-04-12 13:09:14 +00:00
isHandled = true
shouldReconnect = notDisconnected
}
} else {
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
// endpoint changed?
if newProfile.hostCustomEndpoint != lastProfile.hostCustomEndpoint {
pp_log.info("Host endpoint changed: \(newProfile.hostCustomEndpoint?.description ?? "automatic")")
isHandled = true
shouldReconnect = notDisconnected
}
}
if !isHandled {
if newProfile.onDemand != lastProfile.onDemand {
pp_log.info("On-demand settings changed")
isHandled = true
shouldReconnect = false
}
}
guard isHandled else {
return
}
guard profileManager.isActiveProfile(newProfile.id) else {
pp_log.info("Skipping VPN reaction, current profile is not active")
2022-04-12 13:09:14 +00:00
return
}
if shouldReconnect {
2023-07-02 10:51:50 +00:00
try await reconnect(newProfile)
2022-04-12 13:09:14 +00:00
} else {
2023-07-02 10:51:50 +00:00
try await reinstate(newProfile)
2022-04-12 13:09:14 +00:00
}
}
}
2023-05-24 16:19:47 +00:00
// MARK: Configuration
private extension VPNManager {
func vpnConfigurationParameters(withProfile profile: Profile) throws -> VPNConfigurationParameters {
if profile.requiresCredentials {
guard !profile.account.isEmpty else {
2023-07-02 10:51:50 +00:00
throw Passepartout.VPNError.missingAccount(profile: profile)
2023-05-24 16:19:47 +00:00
}
}
// specific provider customizations
var newPassword: String?
if let providerName = profile.providerName {
switch providerName {
case .mullvad:
newPassword = "m"
2023-05-24 16:19:47 +00:00
default:
break
2023-05-24 16:19:47 +00:00
}
}
// 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()
)
2023-05-24 16:19:47 +00:00
}
}
// MARK: KeyValueStore
extension VPNManager {
public var tunnelLogPath: String? {
get {
store.value(forLocation: StoreKey.tunnelLogPath)
}
set {
store.setValue(newValue, forLocation: StoreKey.tunnelLogPath)
didUpdatePreferences.send(vpnPreferences)
}
}
public var tunnelLogFormat: String? {
get {
store.value(forLocation: StoreKey.tunnelLogFormat)
}
set {
store.setValue(newValue, forLocation: StoreKey.tunnelLogFormat)
didUpdatePreferences.send(vpnPreferences)
}
}
2023-03-17 20:55:47 +00:00
public var masksPrivateData: Bool {
get {
store.value(forLocation: StoreKey.masksPrivateData) ?? true
}
set {
store.setValue(newValue, forLocation: StoreKey.masksPrivateData)
didUpdatePreferences.send(vpnPreferences)
}
}
2023-05-24 16:19:47 +00:00
private var vpnPreferences: VPNPreferences {
.init(
tunnelLogPath: tunnelLogPath,
tunnelLogFormat: tunnelLogFormat,
masksPrivateData: masksPrivateData
)
}
}
private extension VPNManager {
2023-09-08 20:18:41 +00:00
enum StoreKey: String, KeyStoreDomainLocation {
case tunnelLogPath
2023-03-17 20:55:47 +00:00
case tunnelLogFormat
2023-03-17 20:55:47 +00:00
case masksPrivateData
2023-03-17 20:55:47 +00:00
var domain: String {
"Passepartout.VPNManager"
}
}
}