diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 13f7086..1814c11 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -49,6 +49,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { name: .NEVPNStatusDidChange, object: nil ) + Task { + // If there's no NE config, then the user needs to sign in. + // However, they might have a session from a previous install, so we + // need to clear it. + if await !vpn.loadNetworkExtensionConfig() { + state.clearSession() + } + } } // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` diff --git a/Coder Desktop/Coder Desktop/NetworkExtension.swift b/Coder Desktop/Coder Desktop/NetworkExtension.swift index effd194..70e69b2 100644 --- a/Coder Desktop/Coder Desktop/NetworkExtension.swift +++ b/Coder Desktop/Coder Desktop/NetworkExtension.swift @@ -24,13 +24,16 @@ enum NetworkExtensionState: Equatable { /// An actor that handles configuring, enabling, and disabling the VPN tunnel via the /// NetworkExtension APIs. extension CoderVPNService { - func loadNetworkExtensionConfig() async { + // Attempts to load the NetworkExtension configuration, returning true if successful. + func loadNetworkExtensionConfig() async -> Bool { do { let tm = try await getTunnelManager() neState = .disabled serverAddress = tm.protocolConfiguration?.serverAddress + return true } catch { neState = .unconfigured + return false } } diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 1e29ae7..0a12ccb 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -35,6 +35,8 @@ enum VPNServiceError: Error, Equatable { state.description } } + + var localizedDescription: String { description } } @MainActor @@ -67,9 +69,6 @@ final class CoderVPNService: NSObject, VPNService { override init() { super.init() installSystemExtension() - Task { - await loadNetworkExtensionConfig() - } } deinit { diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder Desktop/Coder Desktop/Views/LoginForm.swift index 5614df5..f31ee36 100644 --- a/Coder Desktop/Coder Desktop/Views/LoginForm.swift +++ b/Coder Desktop/Coder Desktop/Views/LoginForm.swift @@ -204,6 +204,8 @@ enum LoginError: Error { "Could not authenticate with Coder deployment:\n\(err.description)" } } + + var localizedDescription: String { description } } enum LoginPage { diff --git a/Coder Desktop/Coder Desktop/Views/VPNMenu.swift b/Coder Desktop/Coder Desktop/Views/VPNMenu.swift index c0a983c..e2f6771 100644 --- a/Coder Desktop/Coder Desktop/Views/VPNMenu.swift +++ b/Coder Desktop/Coder Desktop/Views/VPNMenu.swift @@ -89,6 +89,7 @@ struct VPNMenu: View { !state.hasSession || vpn.state == .connecting || vpn.state == .disconnecting || + // Prevent starting the VPN before the user has approved the system extension. vpn.state == .failed(.systemExtensionError(.needsUserApproval)) } } diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift index 881ae99..5f2a6a0 100644 --- a/Coder Desktop/CoderSDK/Client.swift +++ b/Coder Desktop/CoderSDK/Client.swift @@ -134,4 +134,6 @@ public enum ClientError: Error { "Failed to encode body: \(error)" } } + + public var localizedDescription: String { description } } diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index 95be4b2..92c0688 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -276,6 +276,8 @@ enum ManagerError: Error { "Failed to communicate with dylib over tunnel: \(err)" } } + + var localizedDescription: String { description } } func writeVpnLog(_ log: Vpn_Log) { diff --git a/Coder Desktop/VPN/TunnelHandle.swift b/Coder Desktop/VPN/TunnelHandle.swift index 720758e..bebe5fa 100644 --- a/Coder Desktop/VPN/TunnelHandle.swift +++ b/Coder Desktop/VPN/TunnelHandle.swift @@ -82,6 +82,8 @@ enum TunnelHandleError: Error { case let .close(errs): "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))" } } + + var localizedDescription: String { description } } enum OpenTunnelError: Int32 { diff --git a/Coder Desktop/VPNLib/Download.swift b/Coder Desktop/VPNLib/Download.swift index 4782b93..8d854a3 100644 --- a/Coder Desktop/VPNLib/Download.swift +++ b/Coder Desktop/VPNLib/Download.swift @@ -11,7 +11,7 @@ public enum ValidationError: Error { case missingInfoPList case invalidVersion(version: String?) - public var errorDescription: String? { + public var description: String { switch self { case .fileNotFound: "The file does not exist." @@ -31,6 +31,8 @@ public enum ValidationError: Error { "Info.plist is not embedded within the dylib." } } + + public var localizedDescription: String { description } } public class SignatureValidator { @@ -156,7 +158,7 @@ public enum DownloadError: Error { case networkError(any Error) case fileOpError(any Error) - var localizedDescription: String { + public var description: String { switch self { case let .unexpectedStatusCode(code): "Unexpected HTTP status code: \(code)" @@ -168,4 +170,6 @@ public enum DownloadError: Error { "Received non-HTTP response" } } + + public var localizedDescription: String { description } } diff --git a/Coder Desktop/VPNLib/Receiver.swift b/Coder Desktop/VPNLib/Receiver.swift index 8151c3c..699d46f 100644 --- a/Coder Desktop/VPNLib/Receiver.swift +++ b/Coder Desktop/VPNLib/Receiver.swift @@ -75,9 +75,18 @@ actor Receiver { } } -enum ReceiveError: Error { +public enum ReceiveError: Error { case readError(String) case invalidLength + + public var description: String { + switch self { + case let .readError(err): "read error: \(err)" + case .invalidLength: "invalid message length" + } + } + + public var localizedDescription: String { description } } func deserializeLen(_ data: Data) throws -> UInt32 { diff --git a/Coder Desktop/VPNLib/Speaker.swift b/Coder Desktop/VPNLib/Speaker.swift index 27dbf2b..b53f50a 100644 --- a/Coder Desktop/VPNLib/Speaker.swift +++ b/Coder Desktop/VPNLib/Speaker.swift @@ -290,6 +290,19 @@ public enum HandshakeError: Error { case wrongRole(String) case invalidVersion(String) case unsupportedVersion([ProtoVersion]) + + public var description: String { + switch self { + case let .readError(err): "read error: \(err)" + case let .writeError(err): "write error: \(err)" + case let .invalidHeader(err): "invalid header: \(err)" + case let .wrongRole(err): "wrong role: \(err)" + case let .invalidVersion(err): "invalid version: \(err)" + case let .unsupportedVersion(versions): "unsupported version: \(versions)" + } + } + + public var localizedDescription: String { description } } public struct RPCRequest: Sendable { @@ -314,6 +327,18 @@ enum RPCError: Error { case notAResponse case unknownResponseID(UInt64) case shutdown + + var description: String { + switch self { + case .missingRPC: "missing RPC field" + case .notARequest: "not a request" + case .notAResponse: "not a response" + case let .unknownResponseID(id): "unknown response ID: \(id)" + case .shutdown: "RPC secretary has been shutdown" + } + } + + var localizedDescription: String { description } } /// An actor to record outgoing RPCs and route their replies to the original sender