diff --git a/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements b/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements index 91f1361a..7d90a161 100644 --- a/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements +++ b/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements @@ -10,6 +10,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.coder.Coder-Desktop + com.apple.security.files.user-selected.read-only com.apple.security.network.client diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 4bec8d24..b952e982 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -49,8 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { Task { - await vpn.stop() - NSApp.reply(toApplicationShouldTerminate: true) + await vpn.quit() } return .terminateLater } diff --git a/Coder Desktop/Coder Desktop/Info.plist b/Coder Desktop/Coder Desktop/Info.plist new file mode 100644 index 00000000..8609906b --- /dev/null +++ b/Coder Desktop/Coder Desktop/Info.plist @@ -0,0 +1,11 @@ + + + + + NetworkExtension + + NEMachServiceName + $(TeamIdentifierPrefix)com.coder.Coder-Desktop.VPN + + + diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 46c3cabb..3506e103 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -1,6 +1,8 @@ import NetworkExtension import os import SwiftUI +import VPNLib +import VPNXPC @MainActor protocol VPNService: ObservableObject { @@ -43,6 +45,9 @@ enum VPNServiceError: Error, Equatable { @MainActor final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") + lazy var xpc: VPNXPCInterface = .init(vpn: self) + var terminating = false + @Published var tunnelState: VPNServiceState = .disabled @Published var sysExtnState: SystemExtensionState = .uninstalled @Published var neState: NetworkExtensionState = .unconfigured @@ -71,46 +76,45 @@ final class CoderVPNService: NSObject, VPNService { } } - var startTask: Task? func start() async { - if await startTask?.value != nil { + switch tunnelState { + case .disabled, .failed: + break + default: return } - startTask = Task { - tunnelState = .connecting - await enableNetworkExtension() - // TODO: enable communication with the NetworkExtension to track state and agents. For - // now, just pretend it worked... - tunnelState = .connected - } - defer { startTask = nil } - await startTask?.value + // this ping is somewhat load bearing since it causes xpc to init + xpc.ping() + tunnelState = .connecting + await enableNetworkExtension() + logger.debug("network extension enabled") } - var stopTask: Task? func stop() async { - // Wait for a start operation to finish first - await startTask?.value - guard state == .connected else { return } - if await stopTask?.value != nil { - return - } - stopTask = Task { - tunnelState = .disconnecting - await disableNetworkExtension() + guard tunnelState == .connected else { return } + tunnelState = .disconnecting + await disableNetworkExtension() + logger.info("network extension stopped") + } - // TODO: determine when the NetworkExtension is completely disconnected - tunnelState = .disabled + // 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 } - defer { stopTask = nil } - await stopTask?.value + terminating = true + await stop() } func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) { Task { - if proto != nil { - await configureNetworkExtension(proto: proto!) + if let proto { + await configureNetworkExtension(proto: proto) // this just configures the VPN, it doesn't enable it tunnelState = .disabled } else { @@ -119,10 +123,39 @@ final class CoderVPNService: NSObject, VPNService { neState = .unconfigured tunnelState = .disabled } catch { - logger.error("failed to remoing network extension: \(error)") + logger.error("failed to remove network extension: \(error)") neState = .failed(error.localizedDescription) } } } } + + func onExtensionPeerUpdate(_ data: Data) { + // TODO: handle peer update + logger.info("network extension peer update") + do { + let msg = try Vpn_TunnelMessage(serializedBytes: data) + debugPrint(msg) + } catch { + logger.error("failed to decode peer update \(error)") + } + } + + func onExtensionStart() { + logger.info("network extension reported started") + tunnelState = .connected + } + + func onExtensionStop() { + logger.info("network extension reported stopped") + tunnelState = .disabled + if terminating { + NSApp.reply(toApplicationShouldTerminate: true) + } + } + + func onExtensionError(_ error: NSError) { + logger.error("network extension reported error: \(error)") + tunnelState = .failed(.internalError(error.localizedDescription)) + } } diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder Desktop/Coder Desktop/XPCInterface.swift new file mode 100644 index 00000000..6c0861c6 --- /dev/null +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -0,0 +1,70 @@ +import Foundation +import os +import VPNXPC + +@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 + + init(vpn: CoderVPNService) { + svc = vpn + + let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any] + let machServiceName = networkExtDict?["NEMachServiceName"] as? String + let xpcConn = NSXPCConnection(machServiceName: machServiceName!) + xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) + xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self) + guard let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol else { + fatalError("invalid xpc cast") + } + xpc = proxy + + super.init() + + xpcConn.exportedObject = self + xpcConn.invalidationHandler = { [logger] in + Task { @MainActor in + logger.error("XPC connection invalidated.") + } + } + xpcConn.interruptionHandler = { [logger] in + Task { @MainActor in + logger.error("XPC connection interrupted.") + } + } + xpcConn.resume() + } + + func ping() { + xpc.ping { + Task { @MainActor in + self.logger.info("Connected to NE over XPC") + } + } + } + + func onPeerUpdate(_ data: Data) { + Task { @MainActor in + svc.onExtensionPeerUpdate(data) + } + } + + func onStart() { + Task { @MainActor in + svc.onExtensionStart() + } + } + + func onStop() { + Task { @MainActor in + svc.onExtensionStop() + } + } + + func onError(_ err: NSError) { + Task { @MainActor in + svc.onExtensionError(err) + } + } +} diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index 9a3e35cb..05a42412 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -2,6 +2,7 @@ import CoderSDK import NetworkExtension import os import VPNLib +import VPNXPC actor Manager { let ptp: PacketTunnelProvider @@ -10,7 +11,6 @@ actor Manager { let tunnelHandle: TunnelHandle let speaker: Speaker var readLoop: Task! - // TODO: XPC Speaker private let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) .first!.appending(path: "coder-vpn.dylib") @@ -69,6 +69,7 @@ actor Manager { } catch { fatalError("openTunnelTask must only throw TunnelHandleError") } + readLoop = Task { try await run() } } @@ -85,12 +86,16 @@ actor Manager { } catch { logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)") try await tunnelHandle.close() - // TODO: Notify app over XPC + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onError(error as NSError) + } return } logger.info("tunnel read loop exited") try await tunnelHandle.close() - // TODO: Notify app over XPC + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStop() + } } func handleMessage(_ msg: Vpn_TunnelMessage) { @@ -100,7 +105,14 @@ actor Manager { } switch msgType { case .peerUpdate: - {}() // TODO: Send over XPC + if let conn = globalXPCListenerDelegate.getActiveConnection() { + do { + let data = try msg.peerUpdate.serializedData() + conn.onPeerUpdate(data) + } catch { + logger.error("failed to send peer update to client: \(error)") + } + } case let .log(logMsg): writeVpnLog(logMsg) case .networkSettings, .start, .stop: @@ -138,36 +150,42 @@ actor Manager { func startVPN() async throws(ManagerError) { logger.info("sending start rpc") guard let tunFd = ptp.tunnelFileDescriptor else { + logger.error("no fd") throw .noTunnelFileDescriptor } let resp: Vpn_TunnelMessage do { - resp = try await speaker.unaryRPC(.with { msg in - msg.start = .with { req in - req.tunnelFileDescriptor = tunFd - req.apiToken = cfg.apiToken - req.coderURL = cfg.serverUrl.absoluteString - } - }) + resp = try await speaker.unaryRPC( + .with { msg in + msg.start = .with { req in + req.tunnelFileDescriptor = tunFd + req.apiToken = cfg.apiToken + req.coderURL = cfg.serverUrl.absoluteString + } + }) } catch { + logger.error("rpc failed \(error)") throw .failedRPC(error) } guard case let .start(startResp) = resp.msg else { + logger.error("incorrect response") throw .incorrectResponse(resp) } if !startResp.success { + logger.error("no success") throw .errorResponse(msg: startResp.errorMessage) } - // TODO: notify app over XPC + logger.info("startVPN done") } func stopVPN() async throws(ManagerError) { logger.info("sending stop rpc") let resp: Vpn_TunnelMessage do { - resp = try await speaker.unaryRPC(.with { msg in - msg.stop = .init() - }) + resp = try await speaker.unaryRPC( + .with { msg in + msg.stop = .init() + }) } catch { throw .failedRPC(error) } @@ -177,26 +195,25 @@ actor Manager { if !stopResp.success { throw .errorResponse(msg: stopResp.errorMessage) } - // TODO: notify app over XPC } - // TODO: Call via XPC // 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) { + func getPeerInfo() async throws(ManagerError) -> Vpn_PeerUpdate { logger.info("sending peer state request") let resp: Vpn_TunnelMessage do { - resp = try await speaker.unaryRPC(.with { msg in - msg.getPeerUpdate = .init() - }) + resp = try await speaker.unaryRPC( + .with { msg in + msg.getPeerUpdate = .init() + }) } catch { throw .failedRPC(error) } guard case .peerUpdate = resp.msg else { throw .incorrectResponse(resp) } - // TODO: pass to app over XPC + return resp.peerUpdate } } @@ -241,17 +258,18 @@ enum ManagerError: Error { } func writeVpnLog(_ log: Vpn_Log) { - let level: OSLogType = switch log.level { - case .info: .info - case .debug: .debug - // warn == error - case .warn: .error - case .error: .error - // critical == fatal == fault - case .critical: .fault - case .fatal: .fault - case .UNRECOGNIZED: .info - } + let level: OSLogType = + switch log.level { + case .info: .info + case .debug: .debug + // warn == error + case .warn: .error + case .error: .error + // critical == fatal == fault + case .critical: .fault + case .fatal: .fault + case .UNRECOGNIZED: .info + } let logger = Logger( subsystem: "\(Bundle.main.bundleIdentifier!).dylib", category: log.loggerNames.joined(separator: ".") diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index e548d8c5..33020cd7 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -1,6 +1,7 @@ import NetworkExtension import os import VPNLib +import VPNXPC /* From */ let CTLIOCGINFO: UInt = 0xC064_4E03 @@ -46,7 +47,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { override func startTunnel( options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void ) { - logger.debug("startTunnel called") + logger.info("startTunnel called") guard manager == nil else { logger.error("startTunnel called with non-nil Manager") completionHandler(PTPError.alreadyRunning) @@ -76,13 +77,24 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { apiToken: token, serverUrl: .init(string: baseAccessURL)! ) ) + globalXPCListenerDelegate.vpnXPCInterface.setManager(manager) logger.debug("starting vpn") try await manager!.startVPN() logger.info("vpn started") + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStart() + } else { + logger.info("no active XPC connection") + } completionHandler(nil) } catch { - completionHandler(error) logger.error("error starting manager: \(error.description, privacy: .public)") + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onError(error as NSError) + } else { + logger.info("no active XPC connection") + } + completionHandler(error as NSError) } } } @@ -104,6 +116,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } catch { logger.error("error stopping manager: \(error.description, privacy: .public)") } + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStop() + } else { + logger.info("no active XPC connection") + } + globalXPCListenerDelegate.vpnXPCInterface.setManager(nil) completionHandler() } self.manager = nil diff --git a/Coder Desktop/VPN/XPCInterface.swift b/Coder Desktop/VPN/XPCInterface.swift new file mode 100644 index 00000000..3520fe8e --- /dev/null +++ b/Coder Desktop/VPN/XPCInterface.swift @@ -0,0 +1,32 @@ +import Foundation +import os.log +import VPNLib +import VPNXPC + +@objc final class XPCInterface: NSObject, VPNXPCProtocol, @unchecked Sendable { + private var manager: Manager? + private let managerLock = NSLock() + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface") + + func setManager(_ newManager: Manager?) { + managerLock.lock() + defer { managerLock.unlock() } + manager = newManager + } + + func getManager() -> Manager? { + managerLock.lock() + defer { managerLock.unlock() } + let m = manager + + return m + } + + func getPeerInfo(with reply: @escaping () -> Void) { + reply() + } + + func ping(with reply: @escaping () -> Void) { + reply() + } +} diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 410c8389..d350d8dd 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -1,5 +1,56 @@ import Foundation import NetworkExtension +import os +import VPNXPC + +let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider") + +final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sendable { + let vpnXPCInterface = XPCInterface() + var activeConnection: NSXPCConnection? + var connMutex: NSLock = .init() + + func getActiveConnection() -> VPNXPCClientCallbackProtocol? { + connMutex.lock() + defer { connMutex.unlock() } + + let client = activeConnection?.remoteObjectProxy as? VPNXPCClientCallbackProtocol + return client + } + + func setActiveConnection(_ connection: NSXPCConnection?) { + connMutex.lock() + defer { connMutex.unlock() } + activeConnection = connection + } + + func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + newConnection.exportedInterface = NSXPCInterface(with: VPNXPCProtocol.self) + newConnection.exportedObject = vpnXPCInterface + newConnection.remoteObjectInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self) + newConnection.invalidationHandler = { [weak self] in + logger.info("active connection dead") + self?.setActiveConnection(nil) + } + logger.info("new active connection") + setActiveConnection(newConnection) + + newConnection.resume() + return true + } +} + +guard + let netExt = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any], + let serviceName = netExt["NEMachServiceName"] as? String +else { + fatalError("Missing NEMachServiceName in Info.plist") +} + +let globalXPCListenerDelegate = XPCListenerDelegate() +let xpcListener = NSXPCListener(machServiceName: serviceName) +xpcListener.delegate = globalXPCListenerDelegate +xpcListener.resume() autoreleasepool { NEProvider.startSystemExtensionMode() diff --git a/Coder Desktop/VPNXPC/Protocol.swift b/Coder Desktop/VPNXPC/Protocol.swift new file mode 100644 index 00000000..598a9051 --- /dev/null +++ b/Coder Desktop/VPNXPC/Protocol.swift @@ -0,0 +1,16 @@ +import Foundation + +@preconcurrency +@objc public protocol VPNXPCProtocol { + func getPeerInfo(with reply: @escaping () -> Void) + func ping(with reply: @escaping () -> Void) +} + +@preconcurrency +@objc public protocol VPNXPCClientCallbackProtocol { + /// Called when the server has a status update to share + func onPeerUpdate(_ data: Data) + func onStart() + func onStop() + func onError(_ err: NSError) +} diff --git a/Coder Desktop/VPNXPC/VPNXPC.h b/Coder Desktop/VPNXPC/VPNXPC.h new file mode 100644 index 00000000..0fb9c0e4 --- /dev/null +++ b/Coder Desktop/VPNXPC/VPNXPC.h @@ -0,0 +1,11 @@ +#import + +//! Project version number for VPNXPC. +FOUNDATION_EXPORT double VPNXPCVersionNumber; + +//! Project version string for VPNXPC. +FOUNDATION_EXPORT const unsigned char VPNXPCVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Coder Desktop/project.yml b/Coder Desktop/project.yml index 67bba409..2c23c886 100644 --- a/Coder Desktop/project.yml +++ b/Coder Desktop/project.yml @@ -119,6 +119,8 @@ targets: com.apple.security.app-sandbox: true com.apple.security.files.user-selected.read-only: true com.apple.security.network.client: true + com.apple.security.application-groups: + - $(TeamIdentifierPrefix)com.coder.Coder-Desktop settings: base: ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Sets the app icon to "AppIcon". @@ -144,6 +146,8 @@ targets: dependencies: - target: CoderSDK embed: true + - target: VPNXPC + embed: true - target: VPN embed: without-signing # Embed without signing. - package: FluidMenuBarExtra @@ -216,6 +220,8 @@ targets: embed: true - target: CoderSDK embed: true + - target: VPNXPC + embed: true - sdk: NetworkExtension.framework VPNLib: @@ -294,3 +300,19 @@ targets: base: TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop" PRODUCT_BUNDLE_IDENTIFIER: com.coder.Coder-Desktop.CoderSDKTests + + VPNXPC: + type: framework + platform: macOS + sources: + - path: VPNXPC + settings: + base: + INFOPLIST_KEY_NSHumanReadableCopyright: "" + PRODUCT_NAME: "$(TARGET_NAME:c99extidentifier)" + SWIFT_EMIT_LOC_STRINGS: YES + GENERATE_INFOPLIST_FILE: YES + DYLIB_COMPATIBILITY_VERSION: 1 + DYLIB_CURRENT_VERSION: 1 + DYLIB_INSTALL_NAME_BASE: "@rpath" + dependencies: [] 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