refactor(apple): Split IPC and VPN config into separate classes (#8279)

The current `VPNConfigurationManager` class is too large and handles 3
separate things:

- Encoding IPC messages
- Sending IPC messages
- Loading/storing the VPN configuration

With this PR, these are split out into:

- ProviderMessage class
- IPCClient class
- VPNConfigurationManager class

These are then use directly from our `Store` in order to load their
state upon `init()`, set the relevant properties, and thus views are
updated accordingly.

A couple minor bugs are fixed as well as part of the refactor.

### Tested: macOS

- [x] Sign in
- [x] Sign out
- [x] Yanking the VPN configuration while signed in / out
- [x] Yanking the system extension while signed in / out
- [x] Denying the VPN configuration
- [x] Denying Notifications
- [x] Denying System Extension

### Tested: iOS

- [x] Sign in
- [x] Sign out
- [x] Yanking the VPN configuration while signed in / out
- [x] Yanking the system extension while signed in / out
- [x] Denying the VPN configuration
- [x] Denying Notifications

---------

Signed-off-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Jamil
2025-02-28 04:35:26 +00:00
committed by GitHub
parent 1bd8051aae
commit c9f085c102
16 changed files with 655 additions and 569 deletions

View File

@@ -76,6 +76,7 @@ struct FirezoneApp: App {
func applicationDidFinishLaunching(_: Notification) {
if let store {
menuBar = MenuBar(store: store)
AppView.subscribeToGlobalEvents(store: store)
}
// SwiftUI will show the first window group, so close it on launch

View File

@@ -0,0 +1,263 @@
//
// IPCClient.swift
// (c) 2024 Firezone, Inc.
// LICENSE: Apache-2.0
//
import CryptoKit
import Foundation
import NetworkExtension
class IPCClient {
enum Error: Swift.Error {
case invalidNotification
case decodeIPCDataFailed
case noIPCData
case invalidStatus(NEVPNStatus)
var localizedDescription: String {
switch self {
case .invalidNotification:
return "NEVPNStatusDidChange notification doesn't seem to be valid."
case .decodeIPCDataFailed:
return "Decoding IPC data failed."
case .noIPCData:
return "No IPC data returned from the XPC connection!"
case .invalidStatus(let status):
return "The IPC operation couldn't complete because the VPN status is \(status)."
}
}
}
// IPC only makes sense if there's a valid session. Session in this case refers to the `connection` field of
// the NETunnelProviderManager instance.
let session: NETunnelProviderSession
// Track the "version" of the resource list so we can more efficiently
// retrieve it from the Provider
var resourceListHash = Data()
// Cache resources on this side of the IPC barrier so we can
// return them to callers when they haven't changed.
var resourcesListCache: ResourceList = ResourceList.loading
init(session: NETunnelProviderSession) {
self.session = session
}
// Encoder used to send messages to the tunnel
let encoder = {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
return encoder
}()
func start(token: String? = nil) throws {
var options: [String: NSObject] = [:]
// Pass token if provided
if let token = token {
options.merge(["token": token as NSObject]) { _, new in new }
}
// Pass pre-1.4.0 Firezone ID if it exists. Pre 1.4.0 clients will have this
// persisted to the app side container URL.
if let id = FirezoneId.load(.pre140) {
options.merge(["id": id as NSObject]) { _, new in new }
}
try session().startTunnel(options: options)
}
func signOut() throws {
try session([.connected, .connecting, .reasserting]).stopTunnel()
try session().sendProviderMessage(encoder.encode(ProviderMessage.signOut))
}
func stop() throws {
try session([.connected, .connecting, .reasserting]).stopTunnel()
}
func toggleInternetResource(enabled: Bool) throws {
try session([.connected]).sendProviderMessage(
encoder.encode(ProviderMessage.internetResourceEnabled(enabled)))
}
func fetchResources() async throws -> ResourceList {
return try await withCheckedThrowingContinuation { continuation in
do {
// Request list of resources from the provider. We send the hash of the resource list we already have.
// If it differs, we'll get the full list in the callback. If not, we'll get nil.
try session([.connected]).sendProviderMessage(
encoder.encode(ProviderMessage.getResourceList(resourceListHash))) { data in
guard let data = data
else {
// No data returned; Resources haven't changed
continuation.resume(returning: self.resourcesListCache)
return
}
// Save hash to compare against
self.resourceListHash = Data(SHA256.hash(data: data))
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let decoded = try decoder.decode([Resource].self, from: data)
self.resourcesListCache = ResourceList.loaded(decoded)
continuation.resume(returning: self.resourcesListCache)
} catch {
continuation.resume(throwing: error)
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
func clearLogs() async throws {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(encoder.encode(ProviderMessage.clearLogs)) { _ in
continuation.resume()
}
} catch {
continuation.resume(throwing: error)
}
}
}
func getLogFolderSize() async throws -> Int64 {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(
encoder.encode(ProviderMessage.getLogFolderSize)
) { data in
guard let data = data
else {
continuation
.resume(throwing: Error.noIPCData)
return
}
data.withUnsafeBytes { rawBuffer in
continuation.resume(returning: rawBuffer.load(as: Int64.self))
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
// Call this with a closure that will append each chunk to a buffer
// of some sort, like a file. The completed buffer is a valid Apple Archive
// in AAR format.
func exportLogs(
appender: @escaping (LogChunk) -> Void,
errorHandler: @escaping (Error) -> Void
) {
let decoder = PropertyListDecoder()
func loop() {
do {
try session().sendProviderMessage(
encoder.encode(ProviderMessage.exportLogs)
) { data in
guard let data = data
else {
errorHandler(Error.noIPCData)
return
}
guard let chunk = try? decoder.decode(
LogChunk.self, from: data
)
else {
errorHandler(Error.decodeIPCDataFailed)
return
}
appender(chunk)
if !chunk.done {
// Continue
loop()
}
}
} catch {
Log.error(error)
}
}
// Start exporting
loop()
}
func consumeStopReason() async throws -> NEProviderStopReason? {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(
encoder.encode(ProviderMessage.consumeStopReason)
) { data in
guard let data = data,
let reason = String(data: data, encoding: .utf8),
let rawValue = Int(reason)
else {
continuation.resume(returning: nil)
return
}
continuation.resume(returning: NEProviderStopReason(rawValue: rawValue))
}
} catch {
continuation.resume(throwing: error)
}
}
}
// Subscribe to system notifications about our VPN status changing
// and let our handler know about them.
func subscribeToVPNStatusUpdates(handler: @escaping @MainActor (NEVPNStatus) async throws -> Void) {
Task {
for await notification in NotificationCenter.default.notifications(named: .NEVPNStatusDidChange) {
guard let session = notification.object as? NETunnelProviderSession
else {
Log.error(Error.invalidNotification)
return
}
if session.status == .disconnected {
// Reset resource list
resourceListHash = Data()
resourcesListCache = ResourceList.loading
}
do { try await handler(session.status) } catch { Log.error(error) }
}
}
}
func sessionStatus() -> NEVPNStatus {
return session.status
}
private func session(_ requiredStatuses: Set<NEVPNStatus> = []) throws -> NETunnelProviderSession {
if requiredStatuses.isEmpty || requiredStatuses.contains(session.status) {
return session
}
throw Error.invalidStatus(session.status)
}
}

View File

@@ -105,8 +105,8 @@ public final class Log {
// because these happen often due to code signing requirements.
private static func shouldCaptureError(_ err: Error) -> Bool {
#if DEBUG
if let err = err as? VPNConfigurationManagerError,
case VPNConfigurationManagerError.noIPCData = err {
if let err = err as? IPCClient.Error,
case IPCClient.Error.noIPCData = err {
return false
}
#endif

View File

@@ -28,7 +28,7 @@ enum LogExporter {
static func export(
to archiveURL: URL,
with vpnConfigurationManager: VPNConfigurationManager
with ipcClient: IPCClient
) async throws {
guard let logFolderURL = SharedAccess.logFolderURL
else {
@@ -53,7 +53,7 @@ enum LogExporter {
// 3. Await tunnel log export from tunnel process
try await withCheckedThrowingContinuation { continuation in
vpnConfigurationManager.exportLogs(
ipcClient.exportLogs(
appender: { chunk in
do {
// Append each chunk to the archive

View File

@@ -7,151 +7,41 @@
// Abstracts the nitty gritty of loading and saving to our
// VPN configuration in system preferences.
// TODO: Refactor to fix file length
// swiftlint:disable file_length
import CryptoKit
import Foundation
import NetworkExtension
enum VPNConfigurationManagerError: Error {
case managerNotInitialized
case cannotLoad
case decodeIPCDataFailed
case invalidNotification
case noIPCData
case invalidStatus(NEVPNStatus)
case savedProtocolConfigurationIsInvalid
var localizedDescription: String {
switch self {
case .managerNotInitialized:
return "Manager doesn't seem initialized."
case .decodeIPCDataFailed:
return "Decoding IPC data failed."
case .invalidNotification:
return "NEVPNStatusDidChange notification doesn't seem to be valid."
case .cannotLoad:
return "Could not load VPN configurations!"
case .noIPCData:
return "No IPC data returned from the XPC connection!"
case .invalidStatus(let status):
return "The IPC operation couldn't complete because the VPN status is \(status)."
return "NETunnelProviderManager is not yet initialized. Race condition?"
case .savedProtocolConfigurationIsInvalid:
return "Saved protocol configuration is invalid. Check types?"
}
}
}
public enum VPNConfigurationManagerKeys {
static let actorName = "actorName"
static let authBaseURL = "authBaseURL"
static let apiURL = "apiURL"
public static let accountSlug = "accountSlug"
public static let logFilter = "logFilter"
public static let internetResourceEnabled = "internetResourceEnabled"
}
public enum TunnelMessage: Codable {
case getResourceList(Data)
case signOut
case internetResourceEnabled(Bool)
case clearLogs
case getLogFolderSize
case exportLogs
case consumeStopReason
enum CodingKeys: String, CodingKey {
case type
case value
}
enum MessageType: String, Codable {
case getResourceList
case signOut
case internetResourceEnabled
case clearLogs
case getLogFolderSize
case exportLogs
case consumeStopReason
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(MessageType.self, forKey: .type)
switch type {
case .internetResourceEnabled:
let value = try container.decode(Bool.self, forKey: .value)
self = .internetResourceEnabled(value)
case .getResourceList:
let value = try container.decode(Data.self, forKey: .value)
self = .getResourceList(value)
case .signOut:
self = .signOut
case .clearLogs:
self = .clearLogs
case .getLogFolderSize:
self = .getLogFolderSize
case .exportLogs:
self = .exportLogs
case .consumeStopReason:
self = .consumeStopReason
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .internetResourceEnabled(let value):
try container.encode(MessageType.internetResourceEnabled, forKey: .type)
try container.encode(value, forKey: .value)
case .getResourceList(let value):
try container.encode(MessageType.getResourceList, forKey: .type)
try container.encode(value, forKey: .value)
case .signOut:
try container.encode(MessageType.signOut, forKey: .type)
case .clearLogs:
try container.encode(MessageType.clearLogs, forKey: .type)
case .getLogFolderSize:
try container.encode(MessageType.getLogFolderSize, forKey: .type)
case .exportLogs:
try container.encode(MessageType.exportLogs, forKey: .type)
case .consumeStopReason:
try container.encode(MessageType.consumeStopReason, forKey: .type)
}
}
}
// TODO: Refactor this to remove the lint ignore
// swiftlint:disable:next type_body_length
public class VPNConfigurationManager {
// Connect status updates with our listeners
private var tunnelObservingTasks: [Task<Void, Never>] = []
// Track the "version" of the resource list so we can more efficiently
// retrieve it from the Provider
private var resourceListHash = Data()
// Cache resources on this side of the IPC barrier so we can
// return them to callers when they haven't changed.
private var resourcesListCache: ResourceList = ResourceList.loading
public enum Keys {
static let actorName = "actorName"
static let authBaseURL = "authBaseURL"
static let apiURL = "apiURL"
public static let accountSlug = "accountSlug"
public static let logFilter = "logFilter"
public static let internetResourceEnabled = "internetResourceEnabled"
}
// Persists our tunnel settings
private var manager: NETunnelProviderManager?
// Indicates if the internet resource is currently enabled
public var internetResourceEnabled: Bool = false
// Encoder used to send messages to the tunnel
private let encoder = {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
return encoder
}()
let manager: NETunnelProviderManager
public static let bundleIdentifier: String = "\(Bundle.main.bundleIdentifier!).network-extension"
private let bundleDescription = "Firezone"
static let bundleDescription = "Firezone"
// Initialize and save a new VPN configuration in system Preferences
func create() async throws {
init() async throws {
let protocolConfiguration = NETunnelProviderProtocol()
let manager = NETunnelProviderManager()
let settings = Settings.defaultValue
@@ -159,119 +49,100 @@ public class VPNConfigurationManager {
protocolConfiguration.providerConfiguration = settings.toProviderConfiguration()
protocolConfiguration.providerBundleIdentifier = VPNConfigurationManager.bundleIdentifier
protocolConfiguration.serverAddress = settings.apiURL
manager.localizedDescription = bundleDescription
manager.localizedDescription = VPNConfigurationManager.bundleDescription
manager.protocolConfiguration = protocolConfiguration
// Save the new VPN configuration to System Preferences and reload it,
// which should update our status from nil -> disconnected.
// If the user denied the operation, the status will be .invalid
do {
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
self.manager = manager
} catch let error as NSError {
if error.domain == "NEVPNErrorDomain" && error.code == 5 {
// Silence error when the user doesn't click "Allow" on the VPN
// permission dialog
Log.info("VPN permission was denied by the user")
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
return
}
throw error
}
self.manager = manager
}
func loadFromPreferences(
vpnStateUpdateHandler: @escaping @MainActor (NEVPNStatus, Settings?, String?, NEProviderStopReason?) async -> Void
) async throws {
init(from manager: NETunnelProviderManager) {
self.manager = manager
}
static func load() async throws -> VPNConfigurationManager? {
// loadAllFromPreferences() returns list of VPN configurations created by our main app's bundle ID.
// Since our bundle ID can change (by us), find the one that's current and ignore the others.
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
Log.log("\(#function): \(managers.count) tunnel managers found")
for manager in managers where manager.localizedDescription == bundleDescription {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.cannotLoad
}
// Update our state
self.manager = manager
let settings = Settings.fromProviderConfiguration(providerConfiguration)
let actorName = providerConfiguration[VPNConfigurationManagerKeys.actorName]
if let internetResourceEnabled = providerConfiguration[
VPNConfigurationManagerKeys.internetResourceEnabled
]?.data(using: .utf8) {
self.internetResourceEnabled = (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabled)) ?? false
}
let status = manager.connection.status
// Configure our Telemetry environment
Telemetry.setEnvironmentOrClose(settings.apiURL)
Telemetry.accountSlug = providerConfiguration[VPNConfigurationManagerKeys.accountSlug]
// Share what we found with our caller
await vpnStateUpdateHandler(status, settings, actorName, nil)
// Stop looking for our tunnel
break
return VPNConfigurationManager(from: manager)
}
// If no tunnel configuration was found, update state to
// prompt user to create one.
if manager == nil {
await vpnStateUpdateHandler(.invalid, nil, nil, nil)
}
// Hook up status updates
subscribeToVPNStatusUpdates(handler: vpnStateUpdateHandler)
return nil
}
func saveAuthResponse(_ authResponse: AuthResponse) async throws {
guard let manager = manager,
let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
var providerConfiguration = protocolConfiguration.providerConfiguration
func actorName() throws -> String? {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.managerNotInitialized
throw VPNConfigurationManagerError.savedProtocolConfigurationIsInvalid
}
providerConfiguration[VPNConfigurationManagerKeys.actorName] = authResponse.actorName
providerConfiguration[VPNConfigurationManagerKeys.accountSlug] = authResponse.accountSlug
return providerConfiguration[Keys.actorName]
}
func internetResourceEnabled() throws -> Bool? {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.savedProtocolConfigurationIsInvalid
}
// TODO: Store Bool directly in VPN Configuration
if providerConfiguration[Keys.internetResourceEnabled] == "true" {
return true
}
if providerConfiguration[Keys.internetResourceEnabled] == "false" {
return false
}
return nil
}
func save(authResponse: AuthResponse) async throws {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
var providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.savedProtocolConfigurationIsInvalid
}
providerConfiguration[Keys.actorName] = authResponse.actorName
providerConfiguration[Keys.accountSlug] = authResponse.accountSlug
// Configure our Telemetry environment, closing if we're definitely not running against Firezone infrastructure.
Telemetry.accountSlug = providerConfiguration[Keys.accountSlug]
protocolConfiguration.providerConfiguration = providerConfiguration
manager.protocolConfiguration = protocolConfiguration
// We always set this to true when starting the tunnel in case our tunnel
// was disabled by the system for some reason.
// Always set this to true when starting the tunnel in case our tunnel was disabled by the system.
manager.isEnabled = true
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
}
func saveSettings(_ settings: Settings) async throws {
guard let manager = manager,
let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
func save(settings: Settings) async throws {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.managerNotInitialized
throw VPNConfigurationManagerError.savedProtocolConfigurationIsInvalid
}
var newProviderConfiguration = settings.toProviderConfiguration()
// Don't clobber existing actorName
newProviderConfiguration[VPNConfigurationManagerKeys.actorName] =
providerConfiguration[VPNConfigurationManagerKeys.actorName]
newProviderConfiguration[Keys.actorName] = providerConfiguration[Keys.actorName]
protocolConfiguration.providerConfiguration = newProviderConfiguration
protocolConfiguration.serverAddress = settings.apiURL
manager.protocolConfiguration = protocolConfiguration
// We always set this to true when starting the tunnel in case our tunnel
// was disabled by the system for some reason.
manager.isEnabled = true
try await manager.saveToPreferences()
@@ -281,236 +152,17 @@ public class VPNConfigurationManager {
Telemetry.setEnvironmentOrClose(settings.apiURL)
}
func start(token: String? = nil) throws {
var options: [String: NSObject] = [:]
// Pass token if provided
if let token = token {
options.merge(["token": token as NSObject]) { _, new in new }
}
// Pass pre-1.4.0 Firezone ID if it exists. Pre 1.4.0 clients will have this
// persisted to the app side container URL.
if let id = FirezoneId.load(.pre140) {
options.merge(["id": id as NSObject]) { _, new in new }
}
try session().startTunnel(options: options)
}
func signOut() throws {
try session([.connected, .connecting, .reasserting]).stopTunnel()
try session().sendProviderMessage(encoder.encode(TunnelMessage.signOut))
}
func stop() throws {
try session([.connected, .connecting, .reasserting]).stopTunnel()
}
func updateInternetResourceState() throws {
try session([.connected]).sendProviderMessage(
encoder.encode(TunnelMessage.internetResourceEnabled(internetResourceEnabled)))
}
func toggleInternetResource(enabled: Bool) throws {
internetResourceEnabled = enabled
try updateInternetResourceState()
}
func fetchResources() async throws -> ResourceList {
return try await withCheckedThrowingContinuation { continuation in
do {
// Request list of resources from the provider. We send the hash of the resource list we already have.
// If it differs, we'll get the full list in the callback. If not, we'll get nil.
try session([.connected]).sendProviderMessage(
encoder.encode(TunnelMessage.getResourceList(resourceListHash))) { data in
guard let data = data
else {
// No data returned; Resources haven't changed
continuation.resume(returning: self.resourcesListCache)
return
}
// Save hash to compare against
self.resourceListHash = Data(SHA256.hash(data: data))
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let decoded = try decoder.decode([Resource].self, from: data)
self.resourcesListCache = ResourceList.loaded(decoded)
continuation.resume(returning: self.resourcesListCache)
} catch {
continuation.resume(throwing: error)
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
func clearLogs() async throws {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(encoder.encode(TunnelMessage.clearLogs)) { _ in
continuation.resume()
}
} catch {
continuation.resume(throwing: error)
}
}
}
func getLogFolderSize() async throws -> Int64 {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(
encoder.encode(TunnelMessage.getLogFolderSize)
) { data in
guard let data = data
else {
continuation
.resume(throwing: VPNConfigurationManagerError.noIPCData)
return
}
data.withUnsafeBytes { rawBuffer in
continuation.resume(returning: rawBuffer.load(as: Int64.self))
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
// Call this with a closure that will append each chunk to a buffer
// of some sort, like a file. The completed buffer is a valid Apple Archive
// in AAR format.
func exportLogs(
appender: @escaping (LogChunk) -> Void,
errorHandler: @escaping (VPNConfigurationManagerError) -> Void
) {
let decoder = PropertyListDecoder()
func loop() {
do {
try session().sendProviderMessage(
encoder.encode(TunnelMessage.exportLogs)
) { data in
guard let data = data
else {
errorHandler(VPNConfigurationManagerError.noIPCData)
return
}
guard let chunk = try? decoder.decode(
LogChunk.self, from: data
)
else {
errorHandler(VPNConfigurationManagerError.decodeIPCDataFailed)
return
}
appender(chunk)
if !chunk.done {
// Continue
loop()
}
}
} catch {
Log.error(error)
}
}
// Start exporting
loop()
}
func consumeStopReason() async throws -> NEProviderStopReason? {
return try await withCheckedThrowingContinuation { continuation in
do {
try session().sendProviderMessage(
encoder.encode(TunnelMessage.consumeStopReason)
) { data in
guard let data = data,
let reason = String(data: data, encoding: .utf8),
let rawValue = Int(reason)
else {
continuation.resume(returning: nil)
return
}
continuation.resume(returning: NEProviderStopReason(rawValue: rawValue))
}
} catch {
continuation.resume(throwing: error)
}
}
}
private func session(_ requiredStatuses: Set<NEVPNStatus> = []) throws -> NETunnelProviderSession {
guard let session = manager?.connection as? NETunnelProviderSession
func settings() throws -> Settings {
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
else {
throw VPNConfigurationManagerError.managerNotInitialized
throw VPNConfigurationManagerError.savedProtocolConfigurationIsInvalid
}
if requiredStatuses.isEmpty || requiredStatuses.contains(session.status) {
return session
}
throw VPNConfigurationManagerError.invalidStatus(session.status)
return Settings.fromProviderConfiguration(providerConfiguration)
}
// Subscribe to system notifications about our VPN status changing
// and let our handler know about them.
private func subscribeToVPNStatusUpdates(
handler: @escaping @MainActor (NEVPNStatus, Settings?, String?, NEProviderStopReason?
) async -> Void) {
Log.log("\(#function)")
for task in tunnelObservingTasks {
task.cancel()
}
tunnelObservingTasks.removeAll()
tunnelObservingTasks.append(
Task {
for await notification in NotificationCenter.default.notifications(
named: .NEVPNStatusDidChange
) {
guard let session = notification.object as? NETunnelProviderSession
else {
Log.error(VPNConfigurationManagerError.invalidNotification)
return
}
var reason: NEProviderStopReason?
if session.status == .disconnected {
// Reset resource list
resourceListHash = Data()
resourcesListCache = ResourceList.loading
// Attempt to consume the last stopped reason
do { reason = try await consumeStopReason() } catch { Log.error(error) }
}
await handler(session.status, nil, nil, reason)
}
}
)
func session() -> NETunnelProviderSession? {
return manager.connection as? NETunnelProviderSession
}
}

View File

@@ -0,0 +1,77 @@
//
// ProviderMessage.swift
// (c) 2024 Firezone, Inc.
// LICENSE: Apache-2.0
//
// Encodes / Decodes messages to the provider service.
import Foundation
public enum ProviderMessage: Codable {
case getResourceList(Data)
case signOut
case internetResourceEnabled(Bool)
case clearLogs
case getLogFolderSize
case exportLogs
case consumeStopReason
enum CodingKeys: String, CodingKey {
case type
case value
}
enum MessageType: String, Codable {
case getResourceList
case signOut
case internetResourceEnabled
case clearLogs
case getLogFolderSize
case exportLogs
case consumeStopReason
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(MessageType.self, forKey: .type)
switch type {
case .internetResourceEnabled:
let value = try container.decode(Bool.self, forKey: .value)
self = .internetResourceEnabled(value)
case .getResourceList:
let value = try container.decode(Data.self, forKey: .value)
self = .getResourceList(value)
case .signOut:
self = .signOut
case .clearLogs:
self = .clearLogs
case .getLogFolderSize:
self = .getLogFolderSize
case .exportLogs:
self = .exportLogs
case .consumeStopReason:
self = .consumeStopReason
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .internetResourceEnabled(let value):
try container.encode(MessageType.internetResourceEnabled, forKey: .type)
try container.encode(value, forKey: .value)
case .getResourceList(let value):
try container.encode(MessageType.getResourceList, forKey: .type)
try container.encode(value, forKey: .value)
case .signOut:
try container.encode(MessageType.signOut, forKey: .type)
case .clearLogs:
try container.encode(MessageType.clearLogs, forKey: .type)
case .getLogFolderSize:
try container.encode(MessageType.getLogFolderSize, forKey: .type)
case .exportLogs:
try container.encode(MessageType.exportLogs, forKey: .type)
case .consumeStopReason:
try container.encode(MessageType.consumeStopReason, forKey: .type)
}
}
}

View File

@@ -31,14 +31,14 @@ struct Settings: Equatable {
static func fromProviderConfiguration(_ providerConfiguration: [String: Any]?) -> Settings {
if let providerConfiguration = providerConfiguration as? [String: String] {
return Settings(
authBaseURL: providerConfiguration[VPNConfigurationManagerKeys.authBaseURL]
authBaseURL: providerConfiguration[VPNConfigurationManager.Keys.authBaseURL]
?? Settings.defaultValue.authBaseURL,
apiURL: providerConfiguration[VPNConfigurationManagerKeys.apiURL]
apiURL: providerConfiguration[VPNConfigurationManager.Keys.apiURL]
?? Settings.defaultValue.apiURL,
logFilter: providerConfiguration[VPNConfigurationManagerKeys.logFilter]
logFilter: providerConfiguration[VPNConfigurationManager.Keys.logFilter]
?? Settings.defaultValue.logFilter,
internetResourceEnabled: getInternetResourceEnabled(
internetResourceEnabled: providerConfiguration[VPNConfigurationManagerKeys.internetResourceEnabled])
internetResourceEnabled: providerConfiguration[VPNConfigurationManager.Keys.internetResourceEnabled])
)
} else {
return Settings.defaultValue
@@ -62,10 +62,10 @@ struct Settings: Equatable {
}
return [
VPNConfigurationManagerKeys.authBaseURL: authBaseURL,
VPNConfigurationManagerKeys.apiURL: apiURL,
VPNConfigurationManagerKeys.logFilter: logFilter,
VPNConfigurationManagerKeys.internetResourceEnabled: string
VPNConfigurationManager.Keys.authBaseURL: authBaseURL,
VPNConfigurationManager.Keys.apiURL: apiURL,
VPNConfigurationManager.Keys.logFilter: logFilter,
VPNConfigurationManager.Keys.internetResourceEnabled: string
]
}

View File

@@ -20,7 +20,7 @@ public final class Store: ObservableObject {
@Published private(set) var actorName: String?
// Make our tunnel configuration convenient for SettingsView to consume
@Published var settings: Settings
@Published private(set) var settings = Settings.defaultValue
// Enacapsulate Tunnel status here to make it easier for other components
// to observe
@@ -28,54 +28,61 @@ public final class Store: ObservableObject {
@Published private(set) var decision: UNAuthorizationStatus?
@Published private(set) var internetResourceEnabled: Bool?
#if os(macOS)
// Track whether our system extension has been installed (macOS)
@Published private(set) var systemExtensionStatus: SystemExtensionStatus?
#endif
let vpnConfigurationManager: VPNConfigurationManager
private var sessionNotification: SessionNotification
private var cancellables: Set<AnyCancellable> = []
let sessionNotification = SessionNotification()
private var resourcesTimer: Timer?
private var resourceUpdateTask: Task<Void, Never>?
private var vpnConfigurationManager: VPNConfigurationManager?
public init() {
// Initialize all stored properties
self.settings = Settings.defaultValue
self.sessionNotification = SessionNotification()
self.vpnConfigurationManager = VPNConfigurationManager()
self.sessionNotification.signInHandler = {
Task {
do { try await WebAuthSession.signIn(store: self) } catch { Log.error(error) }
}
}
// Load our state from the system. Based on what's loaded, we may need to ask the user for permission for things.
initNotifications()
initSystemExtension()
initVPNConfiguration()
}
func initNotifications() {
Task {
// Load user's decision whether to allow / disallow notifications
self.decision = await self.sessionNotification.loadAuthorizationStatus()
}
}
// Load VPN configuration and system extension status
do {
try await self.bindToVPNConfigurationUpdates()
let vpnConfigurationStatus = self.status
func initSystemExtension() {
#if os(macOS)
let systemExtensionStatus = try await self.checkedSystemExtensionStatus()
if systemExtensionStatus != .installed
|| vpnConfigurationStatus == .invalid {
// Show the main Window if VPN permission needs to be granted
AppView.WindowDefinition.main.openWindow()
} else {
AppView.WindowDefinition.main.window()?.close()
}
Task {
do {
self.systemExtensionStatus = try await self.checkSystemExtensionStatus()
} catch {
Log.error(error)
}
}
#endif
}
if vpnConfigurationStatus == .disconnected {
// Try to connect on start
try self.vpnConfigurationManager.start()
func initVPNConfiguration() {
Task {
do {
// Try to load existing configuration
if let manager = try await VPNConfigurationManager.load() {
self.vpnConfigurationManager = manager
self.settings = try manager.settings()
try await setupTunnelObservers(autoStart: true)
} else {
status = .invalid
}
} catch {
Log.error(error)
@@ -83,56 +90,65 @@ public final class Store: ObservableObject {
}
}
public func internetResourceEnabled() -> Bool {
self.vpnConfigurationManager.internetResourceEnabled
func setupTunnelObservers(autoStart: Bool = false) async throws {
let statusChangeHandler: (NEVPNStatus) async throws -> Void = { [weak self] status in
try await self?.handleStatusChange(newStatus: status)
}
try ipcClient().subscribeToVPNStatusUpdates(handler: statusChangeHandler)
if autoStart && status == .disconnected {
// Try to connect on start
try ipcClient().start()
}
try await handleStatusChange(newStatus: ipcClient().sessionStatus())
}
func bindToVPNConfigurationUpdates() async throws {
// Load our existing VPN configuration and set an update handler
try await self.vpnConfigurationManager.loadFromPreferences(
vpnStateUpdateHandler: { @MainActor [weak self] status, settings, actorName, stopReason in
guard let self else { return }
func handleStatusChange(newStatus: NEVPNStatus) async throws {
status = newStatus
self.status = status
if status == .connected {
// Load saved actorName
actorName = try? manager().actorName()
if let settings {
self.settings = settings
}
// Load saved internet resource status
internetResourceEnabled = try? manager().internetResourceEnabled()
if let actorName {
self.actorName = actorName
}
if status == .connected {
self.beginUpdatingResources { resourceList in
self.resourceList = resourceList
}
}
if status == .disconnected {
self.endUpdatingResources()
self.resourceList = ResourceList.loading
}
// Load Resources
beginUpdatingResources()
} else {
endUpdatingResources()
}
#if os(macOS)
// On macOS we must show notifications from the UI process. On iOS, we've already initiated the notification
// from the tunnel process, because the UI process is not guaranteed to be alive.
if status == .disconnected,
stopReason == .authenticationCanceled {
// On macOS we must show notifications from the UI process. On iOS, we've already initiated the notification
// from the tunnel process, because the UI process is not guaranteed to be alive.
if status == .disconnected {
do {
let reason = try await ipcClient().consumeStopReason()
if reason == .authenticationCanceled {
await self.sessionNotification.showSignedOutAlertmacOS()
}
#endif
} catch {
Log.error(error)
}
)
}
// When this happens, it's because either our VPN configuration or System Extension (or both) were removed.
// So load the system extension status again to determine which view to load.
if status == .invalid {
self.systemExtensionStatus = try await checkSystemExtensionStatus()
}
#endif
}
#if os(macOS)
func checkedSystemExtensionStatus() async throws -> SystemExtensionStatus {
func checkSystemExtensionStatus() async throws -> SystemExtensionStatus {
let checker = SystemExtensionManager()
let status =
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
checker.checkStatus(
identifier: VPNConfigurationManager.bundleIdentifier,
continuation: continuation
@@ -145,8 +161,6 @@ public final class Store: ObservableObject {
try await installSystemExtension()
}
self.systemExtensionStatus = status
return status
}
@@ -157,7 +171,6 @@ public final class Store: ObservableObject {
// See https://developer.apple.com/documentation/systemextensions/installing-system-extensions-and-drivers
self.systemExtensionStatus =
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
installer.installSystemExtension(
identifier: VPNConfigurationManager.bundleIdentifier,
continuation: continuation
@@ -166,12 +179,29 @@ public final class Store: ObservableObject {
}
#endif
func grantVPNPermission() async throws {
func installVPNConfiguration() async throws {
// Create a new VPN configuration in system settings.
try await self.vpnConfigurationManager.create()
self.vpnConfigurationManager = try await VPNConfigurationManager()
// Reload our state
try await bindToVPNConfigurationUpdates()
try await setupTunnelObservers()
}
func ipcClient() throws -> IPCClient {
guard let session = try manager().session()
else {
throw VPNConfigurationManagerError.managerNotInitialized
}
return IPCClient(session: session)
}
func manager() throws -> VPNConfigurationManager {
guard let vpnConfigurationManager
else {
throw VPNConfigurationManagerError.managerNotInitialized
}
return vpnConfigurationManager
}
func grantNotifications() async throws {
@@ -182,34 +212,48 @@ public final class Store: ObservableObject {
return URL(string: settings.authBaseURL)
}
private func start(token: String? = nil) throws {
try self.vpnConfigurationManager.start(token: token)
}
func stop() throws {
try self.vpnConfigurationManager.stop()
try ipcClient().stop()
}
func signIn(authResponse: AuthResponse) async throws {
// Save actorName
self.actorName = authResponse.actorName
try await self.vpnConfigurationManager.saveSettings(settings)
try await self.vpnConfigurationManager.saveAuthResponse(authResponse)
try await manager().save(authResponse: authResponse)
// Bring the tunnel up and send it a token to start
try self.vpnConfigurationManager.start(token: authResponse.token)
try ipcClient().start(token: authResponse.token)
}
func signOut() throws {
try self.vpnConfigurationManager.signOut()
try ipcClient().signOut()
}
func clearLogs() async throws {
try await ipcClient().clearLogs()
}
func saveSettings(_ newSettings: Settings) async throws {
try await manager().save(settings: newSettings)
self.settings = newSettings
}
func toggleInternetResource() async throws {
internetResourceEnabled = !(internetResourceEnabled ?? false)
settings.internetResourceEnabled = internetResourceEnabled
try ipcClient().toggleInternetResource(enabled: internetResourceEnabled == true)
try await manager().save(settings: settings)
}
private func start(token: String? = nil) throws {
try ipcClient().start(token: token)
}
// Network Extensions don't have a 2-way binding up to the GUI process,
// so we need to periodically ask the tunnel process for them.
func beginUpdatingResources(callback: @escaping @MainActor (ResourceList) -> Void) {
Log.log("\(#function)")
private func beginUpdatingResources() {
if self.resourcesTimer != nil {
// Prevent duplicate timer scheduling. This will happen if the system sends us two .connected status updates
// in a row, which can happen occasionally.
@@ -219,11 +263,17 @@ public final class Store: ObservableObject {
// Define the Timer's closure
let updateResources: @Sendable (Timer) -> Void = { _ in
Task {
do {
let resources = try await self.vpnConfigurationManager.fetchResources()
await callback(resources)
} catch {
Log.error(error)
await MainActor.run {
self.resourceUpdateTask?.cancel()
self.resourceUpdateTask = Task {
if !Task.isCancelled {
do {
self.resourceList = try await self.ipcClient().fetchResources()
} catch {
Log.error(error)
}
}
}
}
}
}
@@ -240,20 +290,10 @@ public final class Store: ObservableObject {
updateResources(timer)
}
func endUpdatingResources() {
private func endUpdatingResources() {
resourceUpdateTask?.cancel()
resourcesTimer?.invalidate()
resourcesTimer = nil
}
func save(_ newSettings: Settings) async throws {
try await self.vpnConfigurationManager.saveSettings(newSettings)
self.settings = newSettings
}
func toggleInternetResource(enabled: Bool) async throws {
try self.vpnConfigurationManager.toggleInternetResource(enabled: enabled)
var newSettings = settings
newSettings.internetResourceEnabled = self.vpnConfigurationManager.internetResourceEnabled
try await save(newSettings)
resourceList = ResourceList.loading
}
}

View File

@@ -22,6 +22,29 @@ public struct AppView: View {
@EnvironmentObject var store: Store
#if os(macOS)
// This is a static function because the Environment Object is not present at initialization time when we want to
// subscribe the AppView to certain Store properties to control the main window lifecycle which SwiftUI doesn't
// handle.
private static var cancellables: Set<AnyCancellable> = []
public static func subscribeToGlobalEvents(store: Store) {
store.$status
.combineLatest(store.$systemExtensionStatus)
.receive(on: DispatchQueue.main)
.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main) // Prevents flurry of windows from opening
.sink(receiveValue: { status, systemExtensionStatus in
// Open window in case permissions are revoked
if status == .invalid || systemExtensionStatus != .installed {
WindowDefinition.main.openWindow()
}
// Close window upon launch for day-to-day use
if status != .invalid && systemExtensionStatus == .installed && FirezoneId.load(.pre140) != nil {
WindowDefinition.main.window()?.close()
}
})
.store(in: &cancellables)
}
public enum WindowDefinition: String, CaseIterable {
case main
case settings

View File

@@ -8,6 +8,10 @@
import SwiftUI
import Combine
#if os(macOS)
import SystemExtensions
#endif
struct GrantVPNView: View {
@EnvironmentObject var store: Store
@EnvironmentObject var errorHandler: GlobalErrorHandler
@@ -36,7 +40,7 @@ struct GrantVPNView: View {
.imageScale(.large)
Spacer()
Button("Grant VPN Permission") {
grantVPNPermission()
installVPNConfiguration()
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
@@ -108,7 +112,7 @@ struct GrantVPNView: View {
Spacer()
Button(
action: {
grantVPNPermission()
installVPNConfiguration()
},
label: {
Label("Grant VPN Permission", systemImage: "network.badge.shield.half.filled")
@@ -144,10 +148,21 @@ struct GrantVPNView: View {
}
}
func grantVPNPermission() {
func installVPNConfiguration() {
Task {
do {
try await store.grantVPNPermission()
try await store.installVPNConfiguration()
} catch let error as NSError {
if error.domain == "NEVPNErrorDomain" && error.code == 5 {
// Warn when the user doesn't click "Allow" on the VPN dialog
let alert = NSAlert()
alert.messageText = "Permission required."
alert.informativeText =
"Firezone requires permission to install VPN configurations. Without it, all functionality will be disabled."
_ = alert.runModal()
} else {
throw error
}
} catch {
Log.error(error)
await macOSAlert.show(for: error)
@@ -161,10 +176,10 @@ struct GrantVPNView: View {
#endif
#if os(iOS)
func grantVPNPermission() {
func installVPNConfiguration() {
Task {
do {
try await store.grantVPNPermission()
try await store.installVPNConfiguration()
} catch {
Log.error(error)

View File

@@ -23,7 +23,7 @@ public final class MenuBar: NSObject, ObservableObject {
var statusItem: NSStatusItem
var lastShownFavorites: [Resource] = []
var lastShownOthers: [Resource] = []
var wasInternetResourceEnabled: Bool = false
var wasInternetResourceEnabled: Bool?
var cancellables: Set<AnyCancellable> = []
var updateChecker: UpdateChecker = UpdateChecker()
var updateMenuDisplayed: Bool = false
@@ -285,7 +285,7 @@ public final class MenuBar: NSObject, ObservableObject {
}
}
lastShownFavorites = newFavorites
wasInternetResourceEnabled = store.internetResourceEnabled()
wasInternetResourceEnabled = store.internetResourceEnabled
}
func populateOtherResourcesMenu(_ newOthers: [Resource]) {
@@ -313,7 +313,7 @@ public final class MenuBar: NSObject, ObservableObject {
}
}
lastShownOthers = newOthers
wasInternetResourceEnabled = store.internetResourceEnabled()
wasInternetResourceEnabled = store.internetResourceEnabled
}
func updateStatusItemIcon() {
@@ -467,7 +467,7 @@ public final class MenuBar: NSObject, ObservableObject {
return false
}
return wasInternetResourceEnabled != store.internetResourceEnabled()
return wasInternetResourceEnabled != store.internetResourceEnabled
}
func refreshUpdateItem() {
@@ -503,7 +503,7 @@ public final class MenuBar: NSObject, ObservableObject {
}
func internetResourceTitle(resource: Resource) -> String {
let status = store.internetResourceEnabled() ? StatusSymbol.enabled : StatusSymbol.disabled
let status = store.internetResourceEnabled == true ? StatusSymbol.enabled : StatusSymbol.disabled
return status + " " + resource.name
}
@@ -526,7 +526,7 @@ public final class MenuBar: NSObject, ObservableObject {
}
func internetResourceToggleTitle() -> String {
store.internetResourceEnabled() ? "Disable this resource" : "Enable this resource"
store.internetResourceEnabled == true ? "Disable this resource" : "Enable this resource"
}
// TODO: Refactor this when refactoring for macOS 13
@@ -700,7 +700,17 @@ public final class MenuBar: NSObject, ObservableObject {
// the system extension here too just in case. It's a no-op if already
// installed.
try await store.installSystemExtension()
try await store.grantVPNPermission()
try await store.installVPNConfiguration()
} catch let error as NSError {
if error.domain == "NEVPNErrorDomain" && error.code == 5 {
// Warn when the user doesn't click "Allow" on the VPN dialog
let alert = NSAlert()
alert.messageText =
"Firezone requires permission to install VPN configurations. Without it, all functionality will be disabled."
_ = alert.runModal()
} else {
throw error
}
} catch {
Log.error(error)
await macOSAlert.show(for: error)
@@ -756,7 +766,7 @@ public final class MenuBar: NSObject, ObservableObject {
@objc func internetResourceToggle(_ sender: NSMenuItem) {
Task {
do {
try await store.toggleInternetResource(enabled: !store.internetResourceEnabled())
try await store.toggleInternetResource()
} catch {
Log.error(error)
}

View File

@@ -235,7 +235,7 @@ struct ToggleInternetResourceButton: View {
@EnvironmentObject var store: Store
private func toggleResourceEnabledText() -> String {
if store.internetResourceEnabled() {
if store.internetResourceEnabled == true {
"Disable this resource"
} else {
"Enable this resource"
@@ -247,7 +247,7 @@ struct ToggleInternetResourceButton: View {
action: {
Task {
do {
try await store.toggleInternetResource(enabled: !store.internetResourceEnabled())
try await store.toggleInternetResource()
} catch {
Log.error(error)
}

View File

@@ -81,7 +81,7 @@ struct ResourceSection: View {
@EnvironmentObject var store: Store
private func internetResourceTitle(resource: Resource) -> String {
let status = store.internetResourceEnabled() ? StatusSymbol.enabled : StatusSymbol.disabled
let status = store.internetResourceEnabled == true ? StatusSymbol.enabled : StatusSymbol.disabled
return status + " " + resource.name
}

View File

@@ -522,13 +522,13 @@ public struct SettingsView: View {
do {
try await LogExporter.export(
to: destinationURL,
with: store.vpnConfigurationManager
with: store.ipcClient()
)
window.contentViewController?.presentingViewController?.dismiss(self)
} catch {
if let error = error as? VPNConfigurationManagerError,
case VPNConfigurationManagerError.noIPCData = error {
if let error = error as? IPCClient.Error,
case IPCClient.Error.noIPCData = error {
Log.warning("\(#function): Error exporting logs: \(error). Is the XPC service running?")
} else {
Log.error(error)
@@ -584,7 +584,7 @@ public struct SettingsView: View {
try self.store.signOut()
}
try await store.save(settings)
try await store.saveSettings(settings)
} catch {
Log.error(error)
}
@@ -613,7 +613,7 @@ public struct SettingsView: View {
do {
#if os(macOS)
let providerLogFolderSize = try await store.vpnConfigurationManager.getLogFolderSize()
let providerLogFolderSize = try await store.ipcClient().getLogFolderSize()
let totalSize = logFolderSize + providerLogFolderSize
#else
let totalSize = logFolderSize
@@ -627,8 +627,8 @@ public struct SettingsView: View {
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
} catch {
if let error = error as? VPNConfigurationManagerError,
case VPNConfigurationManagerError.noIPCData = error {
if let error = error as? IPCClient.Error,
case IPCClient.Error.noIPCData = error {
// Will happen if the extension is not enabled
Log.warning("\(#function): Unable to count logs: \(error). Is the XPC service running?")
} else {
@@ -648,7 +648,7 @@ public struct SettingsView: View {
try Log.clear(in: SharedAccess.logFolderURL)
#if os(macOS)
try await store.vpnConfigurationManager.clearLogs()
try await store.clearLogs()
#endif
}
}

View File

@@ -168,7 +168,12 @@ struct macOSAlert { // swiftlint:disable:this type_name
// Code 12
case .requestSuperseded:
// This will happen if the user repeatedly clicks "Enable ..."
return nil
return """
You must enable the FirezoneNetworkExtension System Extension in System Settings to continue. Until you do,
all functionality will be disabled.
For more information and troubleshooting, please contact your administrator.
"""
// Code 13
case .authorizationRequired:

View File

@@ -67,14 +67,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard
let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?
.providerConfiguration as? [String: String],
let logFilter = providerConfiguration[VPNConfigurationManagerKeys.logFilter]
let logFilter = providerConfiguration[VPNConfigurationManager.Keys.logFilter]
else {
throw PacketTunnelProviderError
.savedProtocolConfigurationIsInvalid("providerConfiguration.logFilter")
}
// Hydrate telemetry account slug
guard let accountSlug = providerConfiguration[VPNConfigurationManagerKeys.accountSlug]
guard let accountSlug = providerConfiguration[VPNConfigurationManager.Keys.accountSlug]
else {
// This can happen if the user deletes the VPN configuration while it's
// connected. The system will try to restart us with a fresh config
@@ -88,7 +88,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let internetResourceEnabled: Bool =
if let internetResourceEnabledJSON = providerConfiguration[
VPNConfigurationManagerKeys.internetResourceEnabled]?.data(using: .utf8) {
VPNConfigurationManager.Keys.internetResourceEnabled]?.data(using: .utf8) {
(try? JSONDecoder().decode(Bool.self, from: internetResourceEnabledJSON )) ?? false
} else {
false
@@ -165,11 +165,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
// It would be helpful to be able to encapsulate Errors here. To do that
// we need to update TunnelMessage to encode/decode Result to and from Data.
// we need to update ProviderMessage to encode/decode Result to and from Data.
override func handleAppMessage(_ message: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let tunnelMessage = try? PropertyListDecoder().decode(TunnelMessage.self, from: message) else { return }
guard let providerMessage = try? PropertyListDecoder().decode(ProviderMessage.self, from: message) else { return }
switch tunnelMessage {
switch providerMessage {
case .internetResourceEnabled(let value):
adapter?.setInternetResourceEnabled(value)
case .signOut: