diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index b952e982..4e7cebc7 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -47,9 +47,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` + // or return `.terminateNow` func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { + if !settings.stopVPNOnQuit { return .terminateNow } Task { - await vpn.quit() + await vpn.stop() + NSApp.reply(toApplicationShouldTerminate: true) } return .terminateLater } diff --git a/Coder Desktop/Coder Desktop/NetworkExtension.swift b/Coder Desktop/Coder Desktop/NetworkExtension.swift index 28aa78f1..16d18bb4 100644 --- a/Coder Desktop/Coder Desktop/NetworkExtension.swift +++ b/Coder Desktop/Coder Desktop/NetworkExtension.swift @@ -24,13 +24,12 @@ enum NetworkExtensionState: Equatable { /// An actor that handles configuring, enabling, and disabling the VPN tunnel via the /// NetworkExtension APIs. extension CoderVPNService { - // Updates the UI if a previous configuration exists - func loadNetworkExtension() async { + func hasNetworkExtensionConfig() async -> Bool { do { - try await getTunnelManager() - neState = .disabled + _ = try await getTunnelManager() + return true } catch { - neState = .unconfigured + return false } } @@ -71,37 +70,29 @@ extension CoderVPNService { } } - func enableNetworkExtension() async { + func startTunnel() async { do { let tm = try await getTunnelManager() - if !tm.isEnabled { - tm.isEnabled = true - try await tm.saveToPreferences() - logger.debug("saved tunnel with enabled=true") - } try tm.connection.startVPNTunnel() } catch { - logger.error("enable network extension: \(error)") + logger.error("start tunnel: \(error)") neState = .failed(error.localizedDescription) return } - logger.debug("enabled and started tunnel") + logger.debug("started tunnel") neState = .enabled } - func disableNetworkExtension() async { + func stopTunnel() async { do { let tm = try await getTunnelManager() tm.connection.stopVPNTunnel() - tm.isEnabled = false - - try await tm.saveToPreferences() } catch { - logger.error("disable network extension: \(error)") + logger.error("stop tunnel: \(error)") neState = .failed(error.localizedDescription) return } - logger.debug("saved tunnel with enabled=false") + logger.debug("stopped tunnel") neState = .disabled } diff --git a/Coder Desktop/Coder Desktop/State.swift b/Coder Desktop/Coder Desktop/State.swift index cb51450c..c98a09f1 100644 --- a/Coder Desktop/Coder Desktop/State.swift +++ b/Coder Desktop/Coder Desktop/State.swift @@ -104,6 +104,8 @@ class Settings: ObservableObject { } } + @AppStorage(Keys.stopVPNOnQuit) var stopVPNOnQuit = true + init(store: UserDefaults = .standard) { self.store = store _literalHeaders = Published( @@ -116,6 +118,7 @@ class Settings: ObservableObject { enum Keys { static let useLiteralHeaders = "UseLiteralHeaders" static let literalHeaders = "LiteralHeaders" + static let stopVPNOnQuit = "StopVPNOnQuit" } } diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 60e7ace3..657d9949 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -41,7 +41,6 @@ enum VPNServiceError: Error, Equatable { final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") lazy var xpc: VPNXPCInterface = .init(vpn: self) - var terminating = false var workspaces: [UUID: String] = [:] @Published var tunnelState: VPNServiceState = .disabled @@ -68,8 +67,14 @@ final class CoderVPNService: NSObject, VPNService { super.init() installSystemExtension() Task { - await loadNetworkExtension() + neState = if await hasNetworkExtensionConfig() { + .disabled + } else { + .unconfigured + } } + xpc.connect() + xpc.getPeerState() NotificationCenter.default.addObserver( self, selector: #selector(vpnDidUpdate(_:)), @@ -82,6 +87,11 @@ final class CoderVPNService: NSObject, VPNService { NotificationCenter.default.removeObserver(self) } + func clearPeers() { + agents = [:] + workspaces = [:] + } + func start() async { switch tunnelState { case .disabled, .failed: @@ -90,31 +100,18 @@ final class CoderVPNService: NSObject, VPNService { return } - await enableNetworkExtension() - // this ping is somewhat load bearing since it causes xpc to init + await startTunnel() + xpc.connect() xpc.ping() logger.debug("network extension enabled") } func stop() async { guard tunnelState == .connected else { return } - await disableNetworkExtension() + await stopTunnel() logger.info("network extension stopped") } - // Instructs the service to stop the VPN and then quit once the stop event - // is read over XPC. - // MUST only be called from `NSApplicationDelegate.applicationShouldTerminate` - // MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` - func quit() async { - guard tunnelState == .connected else { - NSApp.reply(toApplicationShouldTerminate: true) - return - } - terminating = true - await stop() - } - func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) { Task { if let proto { @@ -145,6 +142,22 @@ final class CoderVPNService: NSObject, VPNService { } } + func onExtensionPeerState(_ data: Data?) { + guard let data else { + logger.error("could not retrieve peer state from network extension, it may not be running") + return + } + logger.info("received network extension peer state") + do { + let msg = try Vpn_PeerUpdate(serializedBytes: data) + debugPrint(msg) + clearPeers() + applyPeerUpdate(with: msg) + } catch { + logger.error("failed to decode peer update \(error)") + } + } + func applyPeerUpdate(with update: Vpn_PeerUpdate) { // Delete agents update.deletedAgents @@ -204,9 +217,6 @@ extension CoderVPNService { } switch connection.status { case .disconnected: - if terminating { - NSApp.reply(toApplicationShouldTerminate: true) - } connection.fetchLastDisconnectError { err in self.tunnelState = if let err { .failed(.internalError(err.localizedDescription)) diff --git a/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift b/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift index 0c1bb9e1..1dc1cf9c 100644 --- a/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift +++ b/Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift @@ -2,11 +2,17 @@ import LaunchAtLogin import SwiftUI struct GeneralTab: View { + @EnvironmentObject var settings: Settings var body: some View { Form { Section { LaunchAtLogin.Toggle("Launch at Login") } + Section { + Toggle(isOn: $settings.stopVPNOnQuit) { + Text("Stop VPN on Quit") + } + } }.formStyle(.grouped) } } diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder Desktop/Coder Desktop/XPCInterface.swift index 4bdc2b22..74baab5a 100644 --- a/Coder Desktop/Coder Desktop/XPCInterface.swift +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -6,11 +6,17 @@ import VPNLib @objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable { private var svc: CoderVPNService private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface") - private let xpc: VPNXPCProtocol + private var xpc: VPNXPCProtocol? init(vpn: CoderVPNService) { svc = vpn + super.init() + } + func connect() { + guard xpc == nil else { + return + } let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any] let machServiceName = networkExtDict?["NEMachServiceName"] as? String let xpcConn = NSXPCConnection(machServiceName: machServiceName!) @@ -21,30 +27,38 @@ import VPNLib } xpc = proxy - super.init() - xpcConn.exportedObject = self xpcConn.invalidationHandler = { [logger] in Task { @MainActor in logger.error("XPC connection invalidated.") + self.xpc = nil } } xpcConn.interruptionHandler = { [logger] in Task { @MainActor in logger.error("XPC connection interrupted.") + self.xpc = nil } } xpcConn.resume() } func ping() { - xpc.ping { + xpc?.ping { Task { @MainActor in self.logger.info("Connected to NE over XPC") } } } + func getPeerState() { + xpc?.getPeerState { data in + Task { @MainActor in + self.svc.onExtensionPeerState(data) + } + } + } + func onPeerUpdate(_ data: Data) { Task { @MainActor in svc.onExtensionPeerUpdate(data) diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index ee2adc50..c9388183 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -194,7 +194,7 @@ actor Manager { // Retrieves the current state of all peers, // as required when starting the app whilst the network extension is already running - func getPeerInfo() async throws(ManagerError) -> Vpn_PeerUpdate { + func getPeerState() async throws(ManagerError) -> Vpn_PeerUpdate { logger.info("sending peer state request") let resp: Vpn_TunnelMessage do { diff --git a/Coder Desktop/VPN/XPCInterface.swift b/Coder Desktop/VPN/XPCInterface.swift index 6ecb1199..d83f7d79 100644 --- a/Coder Desktop/VPN/XPCInterface.swift +++ b/Coder Desktop/VPN/XPCInterface.swift @@ -20,9 +20,12 @@ import VPNLib } } - func getPeerInfo(with reply: @escaping () -> Void) { - // TODO: Retrieve from Manager - reply() + func getPeerState(with reply: @escaping (Data?) -> Void) { + let reply = CallbackWrapper(reply) + Task { + let data = try? await manager?.getPeerState().serializedData() + reply(data) + } } func ping(with reply: @escaping () -> Void) { diff --git a/Coder Desktop/VPNLib/XPC.swift b/Coder Desktop/VPNLib/XPC.swift index ffbf6d85..eda8ab01 100644 --- a/Coder Desktop/VPNLib/XPC.swift +++ b/Coder Desktop/VPNLib/XPC.swift @@ -2,7 +2,7 @@ import Foundation @preconcurrency @objc public protocol VPNXPCProtocol { - func getPeerInfo(with reply: @escaping () -> Void) + func getPeerState(with reply: @escaping (Data?) -> Void) func ping(with reply: @escaping () -> Void) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy