From d8ad6989a5caf4a58dab843505be0160972fbb38 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 18:24:23 -0600 Subject: [PATCH 01/16] feat: add XPC communication to Network Extension --- .../Coder Desktop.xcodeproj/project.pbxproj | 158 +++++++++++++++++- .../Coder Desktop/Coder_DesktopApp.swift | 6 +- Coder Desktop/Coder Desktop/VPNService.swift | 25 ++- Coder Desktop/VPN/PacketTunnelProvider.swift | 4 +- Coder Desktop/VPN/VpnXPCInterface.swift | 101 +++++++++++ Coder Desktop/VPN/main.swift | 18 ++ Coder Desktop/VPNXPC/Info.plist | 11 ++ Coder Desktop/VPNXPC/VPNXPC.entitlements | 8 + Coder Desktop/VPNXPC/VPNXPCProtocol.swift | 27 +++ Coder Desktop/VPNXPC/main.swift | 39 +++++ 10 files changed, 383 insertions(+), 14 deletions(-) create mode 100644 Coder Desktop/VPN/VpnXPCInterface.swift create mode 100644 Coder Desktop/VPNXPC/Info.plist create mode 100644 Coder Desktop/VPNXPC/VPNXPC.entitlements create mode 100644 Coder Desktop/VPNXPC/VPNXPCProtocol.swift create mode 100644 Coder Desktop/VPNXPC/main.swift diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 6a26723..16e5d8c 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 3B0916612D41B9690064DEA8 /* VPNXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; }; 9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; @@ -27,6 +28,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 3B09165F2D41B9690064DEA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3B0916542D41B9690064DEA8; + remoteInfo = VPNXPC; + }; 961679102CFF100E00B2B6DF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; @@ -107,6 +115,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 3B0916622D41B9690064DEA8 /* Embed XPC Services */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; + dstSubfolderSpec = 16; + files = ( + 3B0916612D41B9690064DEA8 /* VPNXPC.xpc in Embed XPC Services */, + ); + name = "Embed XPC Services"; + runOnlyForDeploymentPostprocessing = 0; + }; 961679422CFF117300B2B6DF /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -132,6 +151,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = VPNXPC.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Coder Desktop.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9616790F2CFF100E00B2B6DF /* Coder DesktopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 961679192CFF100E00B2B6DF /* Coder DesktopUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -144,6 +164,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 3B0916652D41B9690064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 3B0916542D41B9690064DEA8 /* VPNXPC */; + }; AA3B3DB62D2D23860099996A /* Exceptions for "VPNLib" folder in "VPNLib" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -171,6 +198,14 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 3B0916562D41B9690064DEA8 /* VPNXPC */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 3B0916652D41B9690064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */, + ); + path = VPNXPC; + sourceTree = ""; + }; 961678FE2CFF100D00B2B6DF /* Coder Desktop */ = { isa = PBXFileSystemSynchronizedRootGroup; path = "Coder Desktop"; @@ -223,6 +258,13 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 3B0916522D41B9690064DEA8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 961678F92CFF100D00B2B6DF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -308,6 +350,7 @@ AA3B3DAE2D2D23860099996A /* VPNLibTests */, AA3B40922D2FC8560099996A /* CoderSDK */, AA3B409E2D2FC8560099996A /* CoderSDKTests */, + 3B0916562D41B9690064DEA8 /* VPNXPC */, 961679312CFF117300B2B6DF /* Frameworks */, 961678FD2CFF100D00B2B6DF /* Products */, ); @@ -324,6 +367,7 @@ AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */, AA3B40912D2FC8560099996A /* CoderSDK.framework */, AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */, + 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */, ); name = Products; sourceTree = ""; @@ -356,6 +400,28 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 3B0916542D41B9690064DEA8 /* VPNXPC */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3B0916662D41B9690064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */; + buildPhases = ( + 3B0916512D41B9690064DEA8 /* Sources */, + 3B0916522D41B9690064DEA8 /* Frameworks */, + 3B0916532D41B9690064DEA8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 3B0916562D41B9690064DEA8 /* VPNXPC */, + ); + name = VPNXPC; + packageProductDependencies = ( + ); + productName = VPNXPC; + productReference = 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */; + productType = "com.apple.product-type.xpc-service"; + }; 961678FB2CFF100D00B2B6DF /* Coder Desktop */ = { isa = PBXNativeTarget; buildConfigurationList = 961679232CFF100F00B2B6DF /* Build configuration list for PBXNativeTarget "Coder Desktop" */; @@ -364,6 +430,7 @@ 961678F92CFF100D00B2B6DF /* Frameworks */, 961678FA2CFF100D00B2B6DF /* Resources */, 961679422CFF117300B2B6DF /* Embed System Extensions */, + 3B0916622D41B9690064DEA8 /* Embed XPC Services */, ); buildRules = ( ); @@ -371,6 +438,7 @@ AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */, 9616793C2CFF117300B2B6DF /* PBXTargetDependency */, AA3B40A32D2FC8560099996A /* PBXTargetDependency */, + 3B0916602D41B9690064DEA8 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 961678FE2CFF100D00B2B6DF /* Coder Desktop */, @@ -566,6 +634,9 @@ LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { + 3B0916542D41B9690064DEA8 = { + CreatedOnToolsVersion = 16.2; + }; 961678FB2CFF100D00B2B6DF = { CreatedOnToolsVersion = 16.1; }; @@ -626,11 +697,19 @@ AA3B3DA72D2D23860099996A /* VPNLibTests */, AA3B40902D2FC8560099996A /* CoderSDK */, AA3B40972D2FC8560099996A /* CoderSDKTests */, + 3B0916542D41B9690064DEA8 /* VPNXPC */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 3B0916532D41B9690064DEA8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 961678FA2CFF100D00B2B6DF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -690,6 +769,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 3B0916512D41B9690064DEA8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 961678F82CFF100D00B2B6DF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -749,6 +835,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 3B0916602D41B9690064DEA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3B0916542D41B9690064DEA8 /* VPNXPC */; + targetProxy = 3B09165F2D41B9690064DEA8 /* PBXContainerItemProxy */; + }; 961679112CFF100E00B2B6DF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; @@ -811,6 +902,54 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 3B0916632D41B9690064DEA8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = VPNXPC/VPNXPC.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4399GN35BJ; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNXPC/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNXPC; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.VPNXPC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 3B0916642D41B9690064DEA8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = VPNXPC/VPNXPC.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4399GN35BJ; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNXPC/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNXPC; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.VPNXPC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 961679212CFF100F00B2B6DF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1148,7 +1287,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1185,7 +1324,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1248,6 +1387,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; @@ -1256,7 +1396,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1285,6 +1425,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; @@ -1293,7 +1434,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1354,6 +1495,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3B0916662D41B9690064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3B0916632D41B9690064DEA8 /* Debug */, + 3B0916642D41B9690064DEA8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 961678F72CFF100D00B2B6DF /* Build configuration list for PBXProject "Coder Desktop" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 7edc8c6..21c31be 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -20,18 +20,18 @@ struct DesktopApp: App { @MainActor class AppDelegate: NSObject, NSApplicationDelegate { private var menuBarExtra: FluidMenuBarExtra? - let vpn: PreviewVPN + let vpn: CoderVPNService let session: PreviewSession override init() { + vpn = CoderVPNService() // TODO: Replace with real implementations - vpn = PreviewVPN() session = PreviewSession() } func applicationDidFinishLaunching(_: Notification) { menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") { - VPNMenu().frame(width: 256) + VPNMenu().frame(width: 256) .environmentObject(self.vpn) .environmentObject(self.session) } diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index cfc484b..97838ba 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -1,6 +1,7 @@ import NetworkExtension -import os import SwiftUI +import os +import VPNXPC @MainActor protocol VPNService: ObservableObject { @@ -43,6 +44,7 @@ enum VPNServiceError: Error, Equatable { @MainActor final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") + var xpcConn: NSXPCConnection @Published var tunnelState: VPNServiceState = .disabled @Published var sysExtnState: SystemExtensionState = .uninstalled @Published var neState: NetworkExtensionState = .unconfigured @@ -64,6 +66,10 @@ final class CoderVPNService: NSObject, VPNService { var systemExtnDelegate: SystemExtensionDelegate? override init() { + xpcConn = NSXPCConnection(serviceName: "com.coder.Coder-Desktop.VPNXPC") + xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) + xpcConn.resume() + super.init() installSystemExtension() } @@ -76,10 +82,14 @@ final class CoderVPNService: NSObject, VPNService { startTask = Task { tunnelState = .connecting await enableNetworkExtension() + logger.debug("network extension enabled") + + if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { + proxy.start() { result in + self.tunnelState = .connected + } + } - // 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 @@ -95,9 +105,12 @@ final class CoderVPNService: NSObject, VPNService { } stopTask = Task { tunnelState = .disconnecting + + if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { + proxy.stop() { result in } + } await disableNetworkExtension() - - // TODO: determine when the NetworkExtension is completely disconnected + logger.info("network extension stopped") tunnelState = .disabled } defer { stopTask = nil } diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index 8f3e3ca..c01d468 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -53,18 +53,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { with: self, cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!) ) + GlobalXPCListenerDelegate.vpnXPCInterface.setManager(manager) } completionHandler(nil) } override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) { logger.debug("stopTunnel called") - guard manager == nil else { + guard manager !== nil else { logger.error("stopTunnel called with nil Manager") completionHandler() return } manager = nil + GlobalXPCListenerDelegate.vpnXPCInterface.setManager(nil) completionHandler() } diff --git a/Coder Desktop/VPN/VpnXPCInterface.swift b/Coder Desktop/VPN/VpnXPCInterface.swift new file mode 100644 index 0000000..de705ca --- /dev/null +++ b/Coder Desktop/VPN/VpnXPCInterface.swift @@ -0,0 +1,101 @@ +import Foundation +import os.log +import VPNXPC + +final class CallbackWrapper: @unchecked Sendable { + private let block: (NSError?) -> Void + + init(_ block: @escaping (NSError?) -> Void) { + self.block = block + } + + func call(_ error: NSError?) { + // Just forward to the original block + block(error) + } +} + +@objc final class VPNXPCInterface: 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() + manager = newManager + managerLock.unlock() + } + + func getManager() -> Manager? { + managerLock.lock() + let m = manager + managerLock.unlock() + return m + } + + func start(with reply: @escaping (NSError?) -> Void) { + // Convert Obj-C block to a Swift @Sendable closure. + let safeReply = CallbackWrapper(reply) + let manager = getManager() + + guard let manager = manager else { + // If somehow `start(...)` is called but no Manager is set + reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Manager not set" + ])) + return + } + + // We must call the async actor method from a Task. + Task { + do { + try await manager.startVPN() + await MainActor.run { + safeReply.call(nil) + } + } catch { + await MainActor.run { + safeReply.call(error as NSError) + } + } + } + } + + func stop(with reply: @escaping (NSError?) -> Void) { + // Convert Obj-C block to a Swift @Sendable closure. + let safeReply = CallbackWrapper(reply) + let manager = getManager() + + guard let manager = manager else { + // If somehow `start(...)` is called but no Manager is set + reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Manager not set" + ])) + return + } + + Task { + do { + try await manager.stopVPN() + await MainActor.run { + safeReply.call(nil) + } + } catch { + await MainActor.run { + safeReply.call(error as NSError) + } + } + } + } + +// func getPeerInfo(with reply: @escaping (Bool, String?) -> Void) { +// Task { +// do { +// try await manager.getPeerInfo() +// reply(true, nil) +// } catch { +// reply(false, "\(error)") +// } +// } +// } +} diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 410c838..172ddfa 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -1,5 +1,23 @@ import Foundation import NetworkExtension +import VPNXPC + +final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, Sendable { + let vpnXPCInterface = VPNXPCInterface() + + func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + newConnection.exportedInterface = NSXPCInterface(with: VPNXPCProtocol.self) + newConnection.exportedObject = vpnXPCInterface + + newConnection.resume() + return true + } +} + +internal let GlobalXPCListenerDelegate = XPCListenerDelegate() +let xpcListener = NSXPCListener(machServiceName: "com.coder.Coder-Desktop.VPNXPC") +xpcListener.delegate = GlobalXPCListenerDelegate +xpcListener.resume() autoreleasepool { NEProvider.startSystemExtensionMode() diff --git a/Coder Desktop/VPNXPC/Info.plist b/Coder Desktop/VPNXPC/Info.plist new file mode 100644 index 0000000..c123a5d --- /dev/null +++ b/Coder Desktop/VPNXPC/Info.plist @@ -0,0 +1,11 @@ + + + + + XPCService + + ServiceType + Application + + + diff --git a/Coder Desktop/VPNXPC/VPNXPC.entitlements b/Coder Desktop/VPNXPC/VPNXPC.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/Coder Desktop/VPNXPC/VPNXPC.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Coder Desktop/VPNXPC/VPNXPCProtocol.swift b/Coder Desktop/VPNXPC/VPNXPCProtocol.swift new file mode 100644 index 0000000..0013ecd --- /dev/null +++ b/Coder Desktop/VPNXPC/VPNXPCProtocol.swift @@ -0,0 +1,27 @@ +import Foundation + +@preconcurrency +@objc public protocol VPNXPCProtocol { + func start(with reply: @escaping (NSError?) -> Void) + func stop(with reply: @escaping (NSError?) -> Void) +} + +/* + To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this: + + connectionToService = NSXPCConnection(serviceName: "com.coder.Coder-Desktop.VPNXPC") + connectionToService.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) + connectionToService.resume() + + Once you have a connection to the service, you can use it like this: + + if let proxy = connectionToService.remoteObjectProxy as? VPNXPCProtocol { + proxy.performCalculation(firstNumber: 23, secondNumber: 19) { result in + NSLog("Result of calculation is: \(result)") + } + } + + And, when you are finished with the service, clean up the connection like this: + + connectionToService.invalidate() +*/ diff --git a/Coder Desktop/VPNXPC/main.swift b/Coder Desktop/VPNXPC/main.swift new file mode 100644 index 0000000..6f8dc6b --- /dev/null +++ b/Coder Desktop/VPNXPC/main.swift @@ -0,0 +1,39 @@ +// +// main.swift +// VPNXPC +// +// Created by Colin Adler on 1/22/25. +// + +import Foundation + +//class ServiceDelegate: NSObject, NSXPCListenerDelegate { +// +// /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. +// func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { +// +// // Configure the connection. +// // First, set the interface that the exported object implements. +// newConnection.exportedInterface = NSXPCInterface(with: VPNXPCProtocol.self) +// +// // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. +// let exportedObject = VPNXPC() +// newConnection.exportedObject = exportedObject +// +// // Resuming the connection allows the system to deliver more incoming messages. +// newConnection.resume() +// +// // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. +// return true +// } +//} +// +//// Create the delegate for the service. +//let delegate = ServiceDelegate() +// +//// Set up the one NSXPCListener for this service. It will handle all incoming connections. +//let listener = NSXPCListener.service() +//listener.delegate = delegate +// +//// Resuming the serviceListener starts this service. This method does not return. +//listener.resume() From 12e803c27c16dda2cb045d1e2920f8edc2139bf1 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 18:27:41 -0600 Subject: [PATCH 02/16] fixup --- Coder Desktop/Coder Desktop/VPNService.swift | 2 - Coder Desktop/VPNXPC/main.swift | 39 -------------------- 2 files changed, 41 deletions(-) delete mode 100644 Coder Desktop/VPNXPC/main.swift diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 97838ba..6b8fe67 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -83,13 +83,11 @@ final class CoderVPNService: NSObject, VPNService { tunnelState = .connecting await enableNetworkExtension() logger.debug("network extension enabled") - if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { proxy.start() { result in self.tunnelState = .connected } } - } defer { startTask = nil } await startTask?.value diff --git a/Coder Desktop/VPNXPC/main.swift b/Coder Desktop/VPNXPC/main.swift deleted file mode 100644 index 6f8dc6b..0000000 --- a/Coder Desktop/VPNXPC/main.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// main.swift -// VPNXPC -// -// Created by Colin Adler on 1/22/25. -// - -import Foundation - -//class ServiceDelegate: NSObject, NSXPCListenerDelegate { -// -// /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. -// func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { -// -// // Configure the connection. -// // First, set the interface that the exported object implements. -// newConnection.exportedInterface = NSXPCInterface(with: VPNXPCProtocol.self) -// -// // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. -// let exportedObject = VPNXPC() -// newConnection.exportedObject = exportedObject -// -// // Resuming the connection allows the system to deliver more incoming messages. -// newConnection.resume() -// -// // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. -// return true -// } -//} -// -//// Create the delegate for the service. -//let delegate = ServiceDelegate() -// -//// Set up the one NSXPCListener for this service. It will handle all incoming connections. -//let listener = NSXPCListener.service() -//listener.delegate = delegate -// -//// Resuming the serviceListener starts this service. This method does not return. -//listener.resume() From 38a9388a4f652e23e278349067b15262c87a2cb8 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 18:29:53 -0600 Subject: [PATCH 03/16] fmt --- Coder Desktop/Coder Desktop/VPNService.swift | 7 +++---- Coder Desktop/VPN/VpnXPCInterface.swift | 12 ++++++------ Coder Desktop/VPN/main.swift | 4 ++-- Coder Desktop/VPNXPC/VPNXPCProtocol.swift | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 6b8fe67..396ddba 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -1,6 +1,6 @@ import NetworkExtension -import SwiftUI import os +import SwiftUI import VPNXPC @MainActor @@ -84,7 +84,7 @@ final class CoderVPNService: NSObject, VPNService { await enableNetworkExtension() logger.debug("network extension enabled") if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { - proxy.start() { result in + proxy.start { _ in self.tunnelState = .connected } } @@ -103,9 +103,8 @@ final class CoderVPNService: NSObject, VPNService { } stopTask = Task { tunnelState = .disconnecting - if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { - proxy.stop() { result in } + proxy.stop { _ in } } await disableNetworkExtension() logger.info("network extension stopped") diff --git a/Coder Desktop/VPN/VpnXPCInterface.swift b/Coder Desktop/VPN/VpnXPCInterface.swift index de705ca..166875e 100644 --- a/Coder Desktop/VPN/VpnXPCInterface.swift +++ b/Coder Desktop/VPN/VpnXPCInterface.swift @@ -25,7 +25,7 @@ final class CallbackWrapper: @unchecked Sendable { manager = newManager managerLock.unlock() } - + func getManager() -> Manager? { managerLock.lock() let m = manager @@ -37,15 +37,15 @@ final class CallbackWrapper: @unchecked Sendable { // Convert Obj-C block to a Swift @Sendable closure. let safeReply = CallbackWrapper(reply) let manager = getManager() - + guard let manager = manager else { // If somehow `start(...)` is called but no Manager is set reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Manager not set" + NSLocalizedDescriptionKey: "Manager not set", ])) return } - + // We must call the async actor method from a Task. Task { do { @@ -69,11 +69,11 @@ final class CallbackWrapper: @unchecked Sendable { guard let manager = manager else { // If somehow `start(...)` is called but no Manager is set reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Manager not set" + NSLocalizedDescriptionKey: "Manager not set", ])) return } - + Task { do { try await manager.stopVPN() diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 172ddfa..9d93f28 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -5,7 +5,7 @@ import VPNXPC final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, Sendable { let vpnXPCInterface = VPNXPCInterface() - func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { newConnection.exportedInterface = NSXPCInterface(with: VPNXPCProtocol.self) newConnection.exportedObject = vpnXPCInterface @@ -14,7 +14,7 @@ final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, Sendable { } } -internal let GlobalXPCListenerDelegate = XPCListenerDelegate() +let GlobalXPCListenerDelegate = XPCListenerDelegate() let xpcListener = NSXPCListener(machServiceName: "com.coder.Coder-Desktop.VPNXPC") xpcListener.delegate = GlobalXPCListenerDelegate xpcListener.resume() diff --git a/Coder Desktop/VPNXPC/VPNXPCProtocol.swift b/Coder Desktop/VPNXPC/VPNXPCProtocol.swift index 0013ecd..e0e10dd 100644 --- a/Coder Desktop/VPNXPC/VPNXPCProtocol.swift +++ b/Coder Desktop/VPNXPC/VPNXPCProtocol.swift @@ -24,4 +24,4 @@ import Foundation And, when you are finished with the service, clean up the connection like this: connectionToService.invalidate() -*/ + */ From 90e352d3733fb01af3866fe425711acbe4a9a94d Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 18:49:41 -0600 Subject: [PATCH 04/16] fix xpc target --- .../Coder Desktop.xcodeproj/project.pbxproj | 134 +++++++----------- .../xcschemes/Coder Desktop.xcscheme | 11 ++ Coder Desktop/VPN/PacketTunnelProvider.swift | 4 +- Coder Desktop/VPN/main.swift | 4 +- Coder Desktop/VPNXPC/Info.plist | 11 -- Coder Desktop/VPNXPC/Protocol.swift | 7 + Coder Desktop/VPNXPC/VPNXPC.entitlements | 8 -- Coder Desktop/VPNXPC/VPNXPC.h | 2 + Coder Desktop/VPNXPC/VPNXPCProtocol.swift | 27 ---- 9 files changed, 77 insertions(+), 131 deletions(-) delete mode 100644 Coder Desktop/VPNXPC/Info.plist create mode 100644 Coder Desktop/VPNXPC/Protocol.swift delete mode 100644 Coder Desktop/VPNXPC/VPNXPC.entitlements create mode 100644 Coder Desktop/VPNXPC/VPNXPC.h delete mode 100644 Coder Desktop/VPNXPC/VPNXPCProtocol.swift diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 16e5d8c..a372109 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 3B0916612D41B9690064DEA8 /* VPNXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; }; 9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; @@ -28,13 +27,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 3B09165F2D41B9690064DEA8 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 3B0916542D41B9690064DEA8; - remoteInfo = VPNXPC; - }; 961679102CFF100E00B2B6DF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; @@ -121,11 +113,20 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( - 3B0916612D41B9690064DEA8 /* VPNXPC.xpc in Embed XPC Services */, ); name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; + 3B0916872D41C8010064DEA8 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 961679422CFF117300B2B6DF /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -151,7 +152,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = VPNXPC.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libVPNXPC.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Coder Desktop.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9616790F2CFF100E00B2B6DF /* Coder DesktopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 961679192CFF100E00B2B6DF /* Coder DesktopUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -164,13 +165,6 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 3B0916652D41B9690064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 3B0916542D41B9690064DEA8 /* VPNXPC */; - }; AA3B3DB62D2D23860099996A /* Exceptions for "VPNLib" folder in "VPNLib" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -198,11 +192,8 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 3B0916562D41B9690064DEA8 /* VPNXPC */ = { + 3B0916902D41C8380064DEA8 /* VPNXPC */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 3B0916652D41B9690064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */, - ); path = VPNXPC; sourceTree = ""; }; @@ -258,7 +249,7 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 3B0916522D41B9690064DEA8 /* Frameworks */ = { + 3B09168D2D41C8380064DEA8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -350,7 +341,7 @@ AA3B3DAE2D2D23860099996A /* VPNLibTests */, AA3B40922D2FC8560099996A /* CoderSDK */, AA3B409E2D2FC8560099996A /* CoderSDKTests */, - 3B0916562D41B9690064DEA8 /* VPNXPC */, + 3B0916902D41C8380064DEA8 /* VPNXPC */, 961679312CFF117300B2B6DF /* Frameworks */, 961678FD2CFF100D00B2B6DF /* Products */, ); @@ -367,7 +358,7 @@ AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */, AA3B40912D2FC8560099996A /* CoderSDK.framework */, AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */, - 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */, + 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */, ); name = Products; sourceTree = ""; @@ -383,6 +374,13 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 3B09168B2D41C8380064DEA8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; AA3B3D9C2D2D23860099996A /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -400,27 +398,27 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 3B0916542D41B9690064DEA8 /* VPNXPC */ = { + 3B09168E2D41C8380064DEA8 /* VPNXPC */ = { isa = PBXNativeTarget; - buildConfigurationList = 3B0916662D41B9690064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */; + buildConfigurationList = 3B0916952D41C8380064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */; buildPhases = ( - 3B0916512D41B9690064DEA8 /* Sources */, - 3B0916522D41B9690064DEA8 /* Frameworks */, - 3B0916532D41B9690064DEA8 /* Resources */, + 3B09168B2D41C8380064DEA8 /* Headers */, + 3B09168C2D41C8380064DEA8 /* Sources */, + 3B09168D2D41C8380064DEA8 /* Frameworks */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( - 3B0916562D41B9690064DEA8 /* VPNXPC */, + 3B0916902D41C8380064DEA8 /* VPNXPC */, ); name = VPNXPC; packageProductDependencies = ( ); productName = VPNXPC; - productReference = 3B0916552D41B9690064DEA8 /* VPNXPC.xpc */; - productType = "com.apple.product-type.xpc-service"; + productReference = 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */; + productType = "com.apple.product-type.library.dynamic"; }; 961678FB2CFF100D00B2B6DF /* Coder Desktop */ = { isa = PBXNativeTarget; @@ -431,6 +429,7 @@ 961678FA2CFF100D00B2B6DF /* Resources */, 961679422CFF117300B2B6DF /* Embed System Extensions */, 3B0916622D41B9690064DEA8 /* Embed XPC Services */, + 3B0916872D41C8010064DEA8 /* Embed Frameworks */, ); buildRules = ( ); @@ -438,7 +437,6 @@ AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */, 9616793C2CFF117300B2B6DF /* PBXTargetDependency */, AA3B40A32D2FC8560099996A /* PBXTargetDependency */, - 3B0916602D41B9690064DEA8 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 961678FE2CFF100D00B2B6DF /* Coder Desktop */, @@ -634,8 +632,9 @@ LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { - 3B0916542D41B9690064DEA8 = { + 3B09168E2D41C8380064DEA8 = { CreatedOnToolsVersion = 16.2; + LastSwiftMigration = 1620; }; 961678FB2CFF100D00B2B6DF = { CreatedOnToolsVersion = 16.1; @@ -697,19 +696,12 @@ AA3B3DA72D2D23860099996A /* VPNLibTests */, AA3B40902D2FC8560099996A /* CoderSDK */, AA3B40972D2FC8560099996A /* CoderSDKTests */, - 3B0916542D41B9690064DEA8 /* VPNXPC */, + 3B09168E2D41C8380064DEA8 /* VPNXPC */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 3B0916532D41B9690064DEA8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 961678FA2CFF100D00B2B6DF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -769,7 +761,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 3B0916512D41B9690064DEA8 /* Sources */ = { + 3B09168C2D41C8380064DEA8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -835,11 +827,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 3B0916602D41B9690064DEA8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 3B0916542D41B9690064DEA8 /* VPNXPC */; - targetProxy = 3B09165F2D41B9690064DEA8 /* PBXContainerItemProxy */; - }; 961679112CFF100E00B2B6DF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; @@ -902,51 +889,36 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 3B0916632D41B9690064DEA8 /* Debug */ = { + 3B0916962D41C8380064DEA8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = VPNXPC/VPNXPC.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4399GN35BJ; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = VPNXPC/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = VPNXPC; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.VPNXPC"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + MACOSX_DEPLOYMENT_TARGET = 15.2; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Debug; }; - 3B0916642D41B9690064DEA8 /* Release */ = { + 3B0916972D41C8380064DEA8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = VPNXPC/VPNXPC.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4399GN35BJ; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = VPNXPC/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = VPNXPC; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.VPNXPC"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + MACOSX_DEPLOYMENT_TARGET = 15.2; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -1495,11 +1467,11 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 3B0916662D41B9690064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */ = { + 3B0916952D41C8380064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */ = { isa = XCConfigurationList; buildConfigurations = ( - 3B0916632D41B9690064DEA8 /* Debug */, - 3B0916642D41B9690064DEA8 /* Release */, + 3B0916962D41C8380064DEA8 /* Debug */, + 3B0916972D41C8380064DEA8 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme index c17080f..8250068 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme +++ b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme @@ -90,6 +90,17 @@ ReferencedContainer = "container:Coder Desktop.xcodeproj"> + + + + - - - - XPCService - - ServiceType - Application - - - diff --git a/Coder Desktop/VPNXPC/Protocol.swift b/Coder Desktop/VPNXPC/Protocol.swift new file mode 100644 index 0000000..9564607 --- /dev/null +++ b/Coder Desktop/VPNXPC/Protocol.swift @@ -0,0 +1,7 @@ +import Foundation + +@preconcurrency +@objc public protocol VPNXPCProtocol { + func start(with reply: @escaping (NSError?) -> Void) + func stop(with reply: @escaping (NSError?) -> Void) +} diff --git a/Coder Desktop/VPNXPC/VPNXPC.entitlements b/Coder Desktop/VPNXPC/VPNXPC.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/Coder Desktop/VPNXPC/VPNXPC.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/Coder Desktop/VPNXPC/VPNXPC.h b/Coder Desktop/VPNXPC/VPNXPC.h new file mode 100644 index 0000000..cc32ecf --- /dev/null +++ b/Coder Desktop/VPNXPC/VPNXPC.h @@ -0,0 +1,2 @@ +#import + diff --git a/Coder Desktop/VPNXPC/VPNXPCProtocol.swift b/Coder Desktop/VPNXPC/VPNXPCProtocol.swift deleted file mode 100644 index e0e10dd..0000000 --- a/Coder Desktop/VPNXPC/VPNXPCProtocol.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -@preconcurrency -@objc public protocol VPNXPCProtocol { - func start(with reply: @escaping (NSError?) -> Void) - func stop(with reply: @escaping (NSError?) -> Void) -} - -/* - To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this: - - connectionToService = NSXPCConnection(serviceName: "com.coder.Coder-Desktop.VPNXPC") - connectionToService.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) - connectionToService.resume() - - Once you have a connection to the service, you can use it like this: - - if let proxy = connectionToService.remoteObjectProxy as? VPNXPCProtocol { - proxy.performCalculation(firstNumber: 23, secondNumber: 19) { result in - NSLog("Result of calculation is: \(result)") - } - } - - And, when you are finished with the service, clean up the connection like this: - - connectionToService.invalidate() - */ From 61e0a2912a2c3358548591a962f693f062c4d983 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 18:53:58 -0600 Subject: [PATCH 05/16] fix xpc module --- Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj | 4 ++-- Coder Desktop/VPN/Manager.swift | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index a372109..ed1ac7c 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -898,7 +898,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; EXECUTABLE_PREFIX = lib; - MACOSX_DEPLOYMENT_TARGET = 15.2; + MACOSX_DEPLOYMENT_TARGET = 14.6; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -915,7 +915,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; EXECUTABLE_PREFIX = lib; - MACOSX_DEPLOYMENT_TARGET = 15.2; + MACOSX_DEPLOYMENT_TARGET = 14.6; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 6.0; diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index bd598a0..c528793 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -113,7 +113,6 @@ actor Manager { } } - // TODO: Call via XPC func startVPN() async throws(ManagerError) { logger.info("sending start rpc") guard let tunFd = ptp.tunnelFileDescriptor else { @@ -137,10 +136,8 @@ actor Manager { if !startResp.success { throw .errorResponse(msg: startResp.errorMessage) } - // TODO: notify app over XPC } - // TODO: Call via XPC func stopVPN() async throws(ManagerError) { logger.info("sending stop rpc") let resp: Vpn_TunnelMessage @@ -157,7 +154,6 @@ actor Manager { if !stopResp.success { throw .errorResponse(msg: stopResp.errorMessage) } - // TODO: notify app over XPC } // TODO: Call via XPC From c0e5813b055a0a8be07b0b9abea6e2ca8a299857 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 19:19:03 -0600 Subject: [PATCH 06/16] change to framework --- .../Coder Desktop.xcodeproj/project.pbxproj | 135 ++++++++++++++---- Coder Desktop/VPNXPC/VPNXPC.h | 16 +++ 2 files changed, 123 insertions(+), 28 deletions(-) diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index ed1ac7c..2f5bf7c 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 3B0916A92D41CFD50064DEA8 /* VPNXPC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */; }; + 3B0916AA2D41CFD50064DEA8 /* VPNXPC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; }; 9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; }; @@ -27,6 +29,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 3B0916A72D41CFD50064DEA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3B0916A02D41CFD50064DEA8; + remoteInfo = VPNXPC; + }; 961679102CFF100E00B2B6DF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 961678F42CFF100D00B2B6DF /* Project object */; @@ -123,6 +132,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 3B0916AA2D41CFD50064DEA8 /* VPNXPC.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -152,7 +162,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libVPNXPC.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VPNXPC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Coder Desktop.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9616790F2CFF100E00B2B6DF /* Coder DesktopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 961679192CFF100E00B2B6DF /* Coder DesktopUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Coder DesktopUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -165,6 +175,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 3B0916AB2D41CFD50064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + publicHeaders = ( + VPNXPC.h, + ); + target = 3B0916A02D41CFD50064DEA8 /* VPNXPC */; + }; AA3B3DB62D2D23860099996A /* Exceptions for "VPNLib" folder in "VPNLib" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -192,8 +209,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 3B0916902D41C8380064DEA8 /* VPNXPC */ = { + 3B0916A22D41CFD50064DEA8 /* VPNXPC */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 3B0916AB2D41CFD50064DEA8 /* Exceptions for "VPNXPC" folder in "VPNXPC" target */, + ); path = VPNXPC; sourceTree = ""; }; @@ -249,7 +269,7 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 3B09168D2D41C8380064DEA8 /* Frameworks */ = { + 3B09169E2D41CFD50064DEA8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -263,6 +283,7 @@ AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */, AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */, AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */, + 3B0916A92D41CFD50064DEA8 /* VPNXPC.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -341,7 +362,7 @@ AA3B3DAE2D2D23860099996A /* VPNLibTests */, AA3B40922D2FC8560099996A /* CoderSDK */, AA3B409E2D2FC8560099996A /* CoderSDKTests */, - 3B0916902D41C8380064DEA8 /* VPNXPC */, + 3B0916A22D41CFD50064DEA8 /* VPNXPC */, 961679312CFF117300B2B6DF /* Frameworks */, 961678FD2CFF100D00B2B6DF /* Products */, ); @@ -358,7 +379,7 @@ AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */, AA3B40912D2FC8560099996A /* CoderSDK.framework */, AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */, - 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */, + 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */, ); name = Products; sourceTree = ""; @@ -374,7 +395,7 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 3B09168B2D41C8380064DEA8 /* Headers */ = { + 3B09169C2D41CFD50064DEA8 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( @@ -398,27 +419,28 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 3B09168E2D41C8380064DEA8 /* VPNXPC */ = { + 3B0916A02D41CFD50064DEA8 /* VPNXPC */ = { isa = PBXNativeTarget; - buildConfigurationList = 3B0916952D41C8380064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */; + buildConfigurationList = 3B0916AC2D41CFD50064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */; buildPhases = ( - 3B09168B2D41C8380064DEA8 /* Headers */, - 3B09168C2D41C8380064DEA8 /* Sources */, - 3B09168D2D41C8380064DEA8 /* Frameworks */, + 3B09169C2D41CFD50064DEA8 /* Headers */, + 3B09169D2D41CFD50064DEA8 /* Sources */, + 3B09169E2D41CFD50064DEA8 /* Frameworks */, + 3B09169F2D41CFD50064DEA8 /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( - 3B0916902D41C8380064DEA8 /* VPNXPC */, + 3B0916A22D41CFD50064DEA8 /* VPNXPC */, ); name = VPNXPC; packageProductDependencies = ( ); productName = VPNXPC; - productReference = 3B09168F2D41C8380064DEA8 /* libVPNXPC.dylib */; - productType = "com.apple.product-type.library.dynamic"; + productReference = 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */; + productType = "com.apple.product-type.framework"; }; 961678FB2CFF100D00B2B6DF /* Coder Desktop */ = { isa = PBXNativeTarget; @@ -437,6 +459,7 @@ AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */, 9616793C2CFF117300B2B6DF /* PBXTargetDependency */, AA3B40A32D2FC8560099996A /* PBXTargetDependency */, + 3B0916A82D41CFD50064DEA8 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 961678FE2CFF100D00B2B6DF /* Coder Desktop */, @@ -632,7 +655,7 @@ LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { - 3B09168E2D41C8380064DEA8 = { + 3B0916A02D41CFD50064DEA8 = { CreatedOnToolsVersion = 16.2; LastSwiftMigration = 1620; }; @@ -696,12 +719,19 @@ AA3B3DA72D2D23860099996A /* VPNLibTests */, AA3B40902D2FC8560099996A /* CoderSDK */, AA3B40972D2FC8560099996A /* CoderSDKTests */, - 3B09168E2D41C8380064DEA8 /* VPNXPC */, + 3B0916A02D41CFD50064DEA8 /* VPNXPC */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 3B09169F2D41CFD50064DEA8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 961678FA2CFF100D00B2B6DF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -761,7 +791,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 3B09168C2D41C8380064DEA8 /* Sources */ = { + 3B09169D2D41CFD50064DEA8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -827,6 +857,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 3B0916A82D41CFD50064DEA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3B0916A02D41CFD50064DEA8 /* VPNXPC */; + targetProxy = 3B0916A72D41CFD50064DEA8 /* PBXContainerItemProxy */; + }; 961679112CFF100E00B2B6DF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; @@ -889,36 +924,80 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 3B0916962D41C8380064DEA8 /* Debug */ = { + 3B0916AD2D41CFD50064DEA8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 4399GN35BJ; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; - EXECUTABLE_PREFIX = lib; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 14.6; - PRODUCT_NAME = "$(TARGET_NAME)"; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "--APPLICATION-IDENTIFIER-.VPNXPC"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; }; name = Debug; }; - 3B0916972D41C8380064DEA8 /* Release */ = { + 3B0916AE2D41CFD50064DEA8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 4399GN35BJ; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; - EXECUTABLE_PREFIX = lib; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 14.6; - PRODUCT_NAME = "$(TARGET_NAME)"; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "--APPLICATION-IDENTIFIER-.VPNXPC"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 6.0; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; }; name = Release; }; @@ -1467,11 +1546,11 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 3B0916952D41C8380064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */ = { + 3B0916AC2D41CFD50064DEA8 /* Build configuration list for PBXNativeTarget "VPNXPC" */ = { isa = XCConfigurationList; buildConfigurations = ( - 3B0916962D41C8380064DEA8 /* Debug */, - 3B0916972D41C8380064DEA8 /* Release */, + 3B0916AD2D41CFD50064DEA8 /* Debug */, + 3B0916AE2D41CFD50064DEA8 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Coder Desktop/VPNXPC/VPNXPC.h b/Coder Desktop/VPNXPC/VPNXPC.h index cc32ecf..abd6bac 100644 --- a/Coder Desktop/VPNXPC/VPNXPC.h +++ b/Coder Desktop/VPNXPC/VPNXPC.h @@ -1,2 +1,18 @@ +// +// VPNXPC.h +// VPNXPC +// +// Created by Colin Adler on 1/22/25. +// + #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 + + From 11a79a92eb4f3338dacde8ae94e76509af1667a4 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 22 Jan 2025 19:21:55 -0600 Subject: [PATCH 07/16] test --- Coder Desktop/VPNXPC/VPNXPC.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Coder Desktop/VPNXPC/VPNXPC.h b/Coder Desktop/VPNXPC/VPNXPC.h index abd6bac..0fb9c0e 100644 --- a/Coder Desktop/VPNXPC/VPNXPC.h +++ b/Coder Desktop/VPNXPC/VPNXPC.h @@ -1,10 +1,3 @@ -// -// VPNXPC.h -// VPNXPC -// -// Created by Colin Adler on 1/22/25. -// - #import //! Project version number for VPNXPC. From dc56a960805df3e272591494936f35c539be2e80 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 23 Jan 2025 15:43:56 -0600 Subject: [PATCH 08/16] XPC fixes --- .../Coder Desktop.xcodeproj/project.pbxproj | 18 ++------ Coder Desktop/Coder Desktop/Info.plist | 11 +++++ Coder Desktop/Coder Desktop/VPNService.swift | 45 ++++++++++++++----- Coder Desktop/VPN/Manager.swift | 28 +++++++++--- Coder Desktop/VPN/PacketTunnelProvider.swift | 22 ++++++--- Coder Desktop/VPN/VpnXPCInterface.swift | 22 +++------ Coder Desktop/VPN/main.swift | 31 ++++++++++++- Coder Desktop/VPNXPC/Protocol.swift | 9 ++++ 8 files changed, 130 insertions(+), 56 deletions(-) create mode 100644 Coder Desktop/Coder Desktop/Info.plist diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 2549ee0..4e4474f 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 3B0916A92D41CFD50064DEA8 /* VPNXPC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */; }; - 3B0916AA2D41CFD50064DEA8 /* VPNXPC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0916A12D41CFD50064DEA8 /* VPNXPC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; }; 9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA2C690F2D34F6920059AFAF /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = AA2C690E2D34F6920059AFAF /* LaunchAtLogin */; }; @@ -137,17 +136,6 @@ name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; - 3B0916872D41C8010064DEA8 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B0916AA2D41CFD50064DEA8 /* VPNXPC.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 961679422CFF117300B2B6DF /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1015,7 +1003,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 14.6; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; @@ -1054,7 +1042,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 14.6; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; @@ -1207,6 +1195,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Coder Desktop/Info.plist"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -1239,6 +1228,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Coder Desktop/Info.plist"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Coder Desktop/Coder Desktop/Info.plist b/Coder Desktop/Coder Desktop/Info.plist new file mode 100644 index 0000000..8609906 --- /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 2099260..489d16b 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -1,6 +1,7 @@ import NetworkExtension import os import SwiftUI +import VPNLib import VPNXPC @MainActor @@ -42,7 +43,7 @@ enum VPNServiceError: Error, Equatable { } @MainActor -final class CoderVPNService: NSObject, VPNService { +final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientCallbackProtocol { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") var xpcConn: NSXPCConnection @Published var tunnelState: VPNServiceState = .disabled @@ -66,11 +67,19 @@ final class CoderVPNService: NSObject, VPNService { var systemExtnDelegate: SystemExtensionDelegate? override init() { - xpcConn = NSXPCConnection(serviceName: "com.coder.Coder-Desktop.VPNXPC") + let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any] + let machServiceName = networkExtDict?["NEMachServiceName"] as? String + xpcConn = NSXPCConnection(serviceName: machServiceName!) xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) - xpcConn.resume() + xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self) super.init() + xpcConn.exportedObject = self +// xpcConn.invalidationHandler = { +// // self.logger.error("XPC connection invalidated.") +// print("XPC connection invalidated") +// } + xpcConn.resume() installSystemExtension() Task { await loadNetworkExtension() @@ -86,11 +95,6 @@ final class CoderVPNService: NSObject, VPNService { tunnelState = .connecting await enableNetworkExtension() logger.debug("network extension enabled") - if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { - proxy.start { _ in - self.tunnelState = .connected - } - } } defer { startTask = nil } await startTask?.value @@ -106,9 +110,6 @@ final class CoderVPNService: NSObject, VPNService { } stopTask = Task { tunnelState = .disconnecting - if let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol { - proxy.stop { _ in } - } await disableNetworkExtension() logger.info("network extension stopped") tunnelState = .disabled @@ -135,4 +136,26 @@ final class CoderVPNService: NSObject, VPNService { } } } + + func onPeerUpdate(_ data: Data) { + // TODO: handle peer update + do { + let msg = try Vpn_TunnelMessage(serializedBytes: data) + debugPrint(msg) + } catch { + logger.error("failed to decode peer update \(error)") + } + } + + func onStart() { + logger.info("network extension reported started") + tunnelState = .connected + } + + func onStop() { + logger.info("network extension reported stopped") + tunnelState = .disabled + } + + func onError(_: NSError) {} } diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index 3bab3a0..4c18e32 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") @@ -85,12 +85,18 @@ actor Manager { } catch { logger.error("tunnel read loop failed: \(error)") try await tunnelHandle.close() - // TODO: Notify app over XPC + if let connection = globalXPCListenerDelegate.getActiveConnection() { + let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol + client?.onError(error as NSError) + } return } logger.info("tunnel read loop exited") try await tunnelHandle.close() - // TODO: Notify app over XPC + if let connection = globalXPCListenerDelegate.getActiveConnection() { + let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol + client?.onStop() + } } func handleMessage(_ msg: Vpn_TunnelMessage) { @@ -100,7 +106,16 @@ actor Manager { } switch msgType { case .peerUpdate: - {}() // TODO: Send over XPC + if let connection = globalXPCListenerDelegate.getActiveConnection() { + // We can call back to the client + do { + let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol + let data = try msg.peerUpdate.serializedData() + client!.onPeerUpdate(data) + } catch { + logger.error("failed to send peer update to client: \(error)") + } + } case let .log(logMsg): writeVpnLog(logMsg) case .networkSettings, .start, .stop: @@ -165,10 +180,9 @@ actor Manager { } } - // 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 { @@ -181,7 +195,7 @@ actor Manager { guard case .peerUpdate = resp.msg else { throw .incorrectResponse(resp) } - // TODO: pass to app over XPC + return resp.peerUpdate } } diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index a47318e..363dd29 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -1,6 +1,6 @@ import NetworkExtension -import VPNLib import os +import VPNLib /* From */ let CTLIOCGINFO: UInt = 0xC064_4E03 @@ -16,7 +16,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { _ = strcpy($0, "com.apple.net.utun_control") } } - for fd: Int32 in 0...1024 { + for fd: Int32 in 0 ... 1024 { var addr = sockaddr_ctl() var ret: Int32 = -1 var len = socklen_t(MemoryLayout.size(ofValue: addr)) @@ -57,12 +57,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { manager = try await Manager( with: self, cfg: .init( - apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!) + apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")! + ) ) globalXPCListenerDelegate.vpnXPCInterface.setManager(manager) - completionHandler(nil) + try await manager?.startVPN() + completionHandler.callAsFunction(nil) } catch { - completionHandler(error) + completionHandler.callAsFunction(error as NSError) logger.error("error starting manager: \(error.description, privacy: .public)") } } @@ -77,6 +79,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { completionHandler() return } + + let managerCopy = manager + Task { + do throws(ManagerError) { + try await managerCopy?.stopVPN() + } catch { + logger.error("error stopping manager: \(error.description, privacy: .public)") + } + } + manager = nil globalXPCListenerDelegate.vpnXPCInterface.setManager(nil) completionHandler() diff --git a/Coder Desktop/VPN/VpnXPCInterface.swift b/Coder Desktop/VPN/VpnXPCInterface.swift index 166875e..cbc21a1 100644 --- a/Coder Desktop/VPN/VpnXPCInterface.swift +++ b/Coder Desktop/VPN/VpnXPCInterface.swift @@ -1,20 +1,8 @@ import Foundation import os.log +import VPNLib import VPNXPC -final class CallbackWrapper: @unchecked Sendable { - private let block: (NSError?) -> Void - - init(_ block: @escaping (NSError?) -> Void) { - self.block = block - } - - func call(_ error: NSError?) { - // Just forward to the original block - block(error) - } -} - @objc final class VPNXPCInterface: NSObject, VPNXPCProtocol, @unchecked Sendable { private var manager: Manager? private let managerLock = NSLock() @@ -51,11 +39,11 @@ final class CallbackWrapper: @unchecked Sendable { do { try await manager.startVPN() await MainActor.run { - safeReply.call(nil) + safeReply.callAsFunction(nil) } } catch { await MainActor.run { - safeReply.call(error as NSError) + safeReply.callAsFunction(error as NSError) } } } @@ -78,11 +66,11 @@ final class CallbackWrapper: @unchecked Sendable { do { try await manager.stopVPN() await MainActor.run { - safeReply.call(nil) + safeReply.callAsFunction(nil) } } catch { await MainActor.run { - safeReply.call(error as NSError) + safeReply.callAsFunction(error as NSError) } } } diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 459100d..69b036e 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -2,20 +2,47 @@ import Foundation import NetworkExtension import VPNXPC -final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, Sendable { +final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sendable { let vpnXPCInterface = VPNXPCInterface() + var activeConnection: NSXPCConnection? + var connMutex: NSLock = .init() + + func getActiveConnection() -> NSXPCConnection? { + connMutex.lock() + defer { connMutex.unlock() } + return activeConnection + } + + 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 + self?.setActiveConnection(nil) + } + 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") +} + +print(serviceName) let globalXPCListenerDelegate = XPCListenerDelegate() -let xpcListener = NSXPCListener(machServiceName: "com.coder.Coder-Desktop.VPNXPC") +let xpcListener = NSXPCListener(machServiceName: serviceName) xpcListener.delegate = globalXPCListenerDelegate xpcListener.resume() diff --git a/Coder Desktop/VPNXPC/Protocol.swift b/Coder Desktop/VPNXPC/Protocol.swift index 9564607..d9988d1 100644 --- a/Coder Desktop/VPNXPC/Protocol.swift +++ b/Coder Desktop/VPNXPC/Protocol.swift @@ -5,3 +5,12 @@ import Foundation func start(with reply: @escaping (NSError?) -> Void) func stop(with reply: @escaping (NSError?) -> 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) +} From b43a407fb3446be925ae6baa0609149bd50d6153 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 23 Jan 2025 16:09:02 -0600 Subject: [PATCH 09/16] fixes --- Coder Desktop/Coder Desktop/VPNService.swift | 5 +- Coder Desktop/VPN/PacketTunnelProvider.swift | 5 +- Coder Desktop/VPN/VpnXPCInterface.swift | 66 +------------------- Coder Desktop/VPN/main.swift | 6 +- Coder Desktop/VPNXPC/Protocol.swift | 3 +- 5 files changed, 14 insertions(+), 71 deletions(-) diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 489d16b..b2edd0a 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -139,6 +139,7 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC func onPeerUpdate(_ data: Data) { // TODO: handle peer update + logger.info("network extension peer update") do { let msg = try Vpn_TunnelMessage(serializedBytes: data) debugPrint(msg) @@ -157,5 +158,7 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC tunnelState = .disabled } - func onError(_: NSError) {} + func onError(_ error: NSError) { + logger.info("network extension reported error: \(error)") + } } diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index 363dd29..42cd786 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -54,6 +54,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { Task { // TODO: Retrieve access URL & Token via Keychain do throws(ManagerError) { + logger.debug("creating manager") manager = try await Manager( with: self, cfg: .init( @@ -61,7 +62,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) ) globalXPCListenerDelegate.vpnXPCInterface.setManager(manager) - try await manager?.startVPN() + logger.debug("calling manager.startVPN") + try await manager!.startVPN() + logger.debug("vpn started") completionHandler.callAsFunction(nil) } catch { completionHandler.callAsFunction(error as NSError) diff --git a/Coder Desktop/VPN/VpnXPCInterface.swift b/Coder Desktop/VPN/VpnXPCInterface.swift index cbc21a1..61159ee 100644 --- a/Coder Desktop/VPN/VpnXPCInterface.swift +++ b/Coder Desktop/VPN/VpnXPCInterface.swift @@ -21,69 +21,7 @@ import VPNXPC return m } - func start(with reply: @escaping (NSError?) -> Void) { - // Convert Obj-C block to a Swift @Sendable closure. - let safeReply = CallbackWrapper(reply) - let manager = getManager() - - guard let manager = manager else { - // If somehow `start(...)` is called but no Manager is set - reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Manager not set", - ])) - return - } - - // We must call the async actor method from a Task. - Task { - do { - try await manager.startVPN() - await MainActor.run { - safeReply.callAsFunction(nil) - } - } catch { - await MainActor.run { - safeReply.callAsFunction(error as NSError) - } - } - } + func getPeerInfo(with reply: @escaping () -> Void) { + reply() } - - func stop(with reply: @escaping (NSError?) -> Void) { - // Convert Obj-C block to a Swift @Sendable closure. - let safeReply = CallbackWrapper(reply) - let manager = getManager() - - guard let manager = manager else { - // If somehow `start(...)` is called but no Manager is set - reply(NSError(domain: "VPNXPC", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Manager not set", - ])) - return - } - - Task { - do { - try await manager.stopVPN() - await MainActor.run { - safeReply.callAsFunction(nil) - } - } catch { - await MainActor.run { - safeReply.callAsFunction(error as NSError) - } - } - } - } - -// func getPeerInfo(with reply: @escaping (Bool, String?) -> Void) { -// Task { -// do { -// try await manager.getPeerInfo() -// reply(true, nil) -// } catch { -// reply(false, "\(error)") -// } -// } -// } } diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 69b036e..b8ff0a2 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -34,10 +34,10 @@ final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sen } guard - let netExt = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any], - let serviceName = netExt["NEMachServiceName"] as? String + let netExt = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any], + let serviceName = netExt["NEMachServiceName"] as? String else { - fatalError("Missing NEMachServiceName in Info.plist") + fatalError("Missing NEMachServiceName in Info.plist") } print(serviceName) diff --git a/Coder Desktop/VPNXPC/Protocol.swift b/Coder Desktop/VPNXPC/Protocol.swift index d9988d1..813fd56 100644 --- a/Coder Desktop/VPNXPC/Protocol.swift +++ b/Coder Desktop/VPNXPC/Protocol.swift @@ -2,8 +2,7 @@ import Foundation @preconcurrency @objc public protocol VPNXPCProtocol { - func start(with reply: @escaping (NSError?) -> Void) - func stop(with reply: @escaping (NSError?) -> Void) + func getPeerInfo(with reply: @escaping () -> Void) } @preconcurrency From a48b691ee0ab6e5032109c3178ad751c366786e2 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 24 Jan 2025 18:55:33 +1100 Subject: [PATCH 10/16] add app group entitlement --- .../Coder Desktop.xcodeproj/project.pbxproj | 16 +++++++++++++--- .../Coder Desktop/Coder_Desktop.entitlements | 4 ++++ Coder Desktop/Coder Desktop/VPNService.swift | 2 +- Coder Desktop/VPN/PacketTunnelProvider.swift | 4 ++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 4e4474f..60b854c 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -217,6 +217,13 @@ ); target = 9616792F2CFF117300B2B6DF /* VPN */; }; + AAC385C72D4339C600F6DFB4 /* Exceptions for "Coder Desktop" folder in "Coder Desktop" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -230,6 +237,9 @@ }; 961678FE2CFF100D00B2B6DF /* Coder Desktop */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + AAC385C72D4339C600F6DFB4 /* Exceptions for "Coder Desktop" folder in "Coder Desktop" target */, + ); path = "Coder Desktop"; sourceTree = ""; }; @@ -994,7 +1004,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1396,7 +1406,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1506,7 +1516,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements b/Coder Desktop/Coder Desktop/Coder_Desktop.entitlements index 91f1361..7d90a16 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/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index b2edd0a..705e7a5 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -69,7 +69,7 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC override init() { let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any] let machServiceName = networkExtDict?["NEMachServiceName"] as? String - xpcConn = NSXPCConnection(serviceName: machServiceName!) + xpcConn = NSXPCConnection(machServiceName: machServiceName!) xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self) diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index 42cd786..d7e5c41 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -65,9 +65,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { logger.debug("calling manager.startVPN") try await manager!.startVPN() logger.debug("vpn started") - completionHandler.callAsFunction(nil) + completionHandler(nil) } catch { - completionHandler.callAsFunction(error as NSError) + completionHandler(error as NSError) logger.error("error starting manager: \(error.description, privacy: .public)") } } From c4c9f3f5783319480f222b1ade7a4e1632569d05 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 29 Jan 2025 23:47:01 -0600 Subject: [PATCH 11/16] working now --- Coder Desktop/Coder Desktop/VPNService.swift | 28 +++---- .../Coder Desktop/XPCInterface.swift | 77 +++++++++++++++++++ Coder Desktop/VPN/Manager.swift | 77 ++++++++++--------- Coder Desktop/VPN/PacketTunnelProvider.swift | 27 +++++-- Coder Desktop/VPN/VPN.entitlements | 3 + ...nXPCInterface.swift => XPCInterface.swift} | 6 +- Coder Desktop/VPN/main.swift | 14 +++- Coder Desktop/VPNXPC/Protocol.swift | 1 + 8 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 Coder Desktop/Coder Desktop/XPCInterface.swift rename Coder Desktop/VPN/{VpnXPCInterface.swift => XPCInterface.swift} (79%) diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 705e7a5..2f773db 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -43,9 +43,11 @@ enum VPNServiceError: Error, Equatable { } @MainActor -final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientCallbackProtocol { +final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") - var xpcConn: NSXPCConnection + // TODO: better init maybe? kinda wonky + lazy var xpc = VPNXPCInterface(vpn: self) + @Published var tunnelState: VPNServiceState = .disabled @Published var sysExtnState: SystemExtensionState = .uninstalled @Published var neState: NetworkExtensionState = .unconfigured @@ -67,19 +69,7 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC var systemExtnDelegate: SystemExtensionDelegate? override init() { - let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any] - let machServiceName = networkExtDict?["NEMachServiceName"] as? String - xpcConn = NSXPCConnection(machServiceName: machServiceName!) - xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self) - xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self) - super.init() - xpcConn.exportedObject = self -// xpcConn.invalidationHandler = { -// // self.logger.error("XPC connection invalidated.") -// print("XPC connection invalidated") -// } - xpcConn.resume() installSystemExtension() Task { await loadNetworkExtension() @@ -91,6 +81,8 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC if await startTask?.value != nil { return } + // this ping is somewhat load bearing since it causes xpc to init + xpc.ping() startTask = Task { tunnelState = .connecting await enableNetworkExtension() @@ -137,7 +129,7 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC } } - func onPeerUpdate(_ data: Data) { + func onExtensionPeerUpdate(_ data: Data) { // TODO: handle peer update logger.info("network extension peer update") do { @@ -148,17 +140,17 @@ final class CoderVPNService: NSObject, VPNService, @preconcurrency VPNXPCClientC } } - func onStart() { + func onExtensionStart() { logger.info("network extension reported started") tunnelState = .connected } - func onStop() { + func onExtensionStop() { logger.info("network extension reported stopped") tunnelState = .disabled } - func onError(_ error: NSError) { + func onExtensionError(_ error: NSError) { logger.info("network extension reported error: \(error)") } } diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder Desktop/Coder Desktop/XPCInterface.swift new file mode 100644 index 0000000..f51bc55 --- /dev/null +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -0,0 +1,77 @@ +import os +import Foundation +import XPCHub +import VPNXPC + +@objc final class VPNXPCInterface: NSObject, XPCClientCallbackProtocol, @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 = { [weak self] in + guard let self = self else { return } + Task { @MainActor in + self.logger.error("XPC connection invalidated.") + } + } + xpcConn.interruptionHandler = { [weak self] in + guard let self = self else { return } + Task { @MainActor in + self.logger.error("XPC connection interrupted.") + } + } + xpcConn.resume() + + xpc.ping { + print("Got response from XPC") + } + } + + func ping() { + xpc.ping { + Task { @MainActor in + print("Got response from 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 4c18e32..b436af9 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -1,8 +1,9 @@ import CoderSDK import NetworkExtension -import os import VPNLib +import XPCHub import VPNXPC +import os actor Manager { let ptp: PacketTunnelProvider @@ -69,6 +70,7 @@ actor Manager { } catch { fatalError("openTunnelTask must only throw TunnelHandleError") } + readLoop = Task { try await run() } } @@ -85,17 +87,15 @@ actor Manager { } catch { logger.error("tunnel read loop failed: \(error)") try await tunnelHandle.close() - if let connection = globalXPCListenerDelegate.getActiveConnection() { - let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol - client?.onError(error as NSError) + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onError(error as NSError) } return } logger.info("tunnel read loop exited") try await tunnelHandle.close() - if let connection = globalXPCListenerDelegate.getActiveConnection() { - let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol - client?.onStop() + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStop() } } @@ -106,12 +106,10 @@ actor Manager { } switch msgType { case .peerUpdate: - if let connection = globalXPCListenerDelegate.getActiveConnection() { - // We can call back to the client + if let conn = globalXPCListenerDelegate.getActiveConnection() { do { - let client = connection.remoteObjectProxy as? VPNXPCClientCallbackProtocol let data = try msg.peerUpdate.serializedData() - client!.onPeerUpdate(data) + conn.onPeerUpdate(data) } catch { logger.error("failed to send peer update to client: \(error)") } @@ -140,35 +138,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) } + 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) } @@ -186,9 +191,10 @@ actor Manager { 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) } @@ -240,17 +246,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 d7e5c41..72604fe 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 +import os /* From */ let CTLIOCGINFO: UInt = 0xC064_4E03 @@ -16,7 +17,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { _ = strcpy($0, "com.apple.net.utun_control") } } - for fd: Int32 in 0 ... 1024 { + for fd: Int32 in 0...1024 { var addr = sockaddr_ctl() var ret: Int32 = -1 var len = socklen_t(MemoryLayout.size(ofValue: addr)) @@ -44,7 +45,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(nil) @@ -54,21 +55,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { Task { // TODO: Retrieve access URL & Token via Keychain do throws(ManagerError) { - logger.debug("creating manager") + logger.info("creating manager") manager = try await Manager( with: self, cfg: .init( - apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")! + apiToken: "qGg1rDGWzL-a814TWDGcTDOs4AX7laDEI", + serverUrl: .init(string: "https://dev.coder.com")! ) ) globalXPCListenerDelegate.vpnXPCInterface.setManager(manager) logger.debug("calling manager.startVPN") - try await manager!.startVPN() + // try await manager!.startVPN() logger.debug("vpn started") + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStart() + } else { + logger.info("no active connection") + } completionHandler(nil) } catch { - completionHandler(error as NSError) logger.error("error starting manager: \(error.description, privacy: .public)") + completionHandler(error as NSError) } } } @@ -83,6 +90,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { return } + if let conn = globalXPCListenerDelegate.getActiveConnection() { + conn.onStop() + } else { + logger.info("no active connection") + } + let managerCopy = manager Task { do throws(ManagerError) { diff --git a/Coder Desktop/VPN/VPN.entitlements b/Coder Desktop/VPN/VPN.entitlements index a515bd3..0fcb1c2 100644 --- a/Coder Desktop/VPN/VPN.entitlements +++ b/Coder Desktop/VPN/VPN.entitlements @@ -14,5 +14,8 @@ com.apple.security.network.client + com.apple.security.network.server + + diff --git a/Coder Desktop/VPN/VpnXPCInterface.swift b/Coder Desktop/VPN/XPCInterface.swift similarity index 79% rename from Coder Desktop/VPN/VpnXPCInterface.swift rename to Coder Desktop/VPN/XPCInterface.swift index 61159ee..c61c49f 100644 --- a/Coder Desktop/VPN/VpnXPCInterface.swift +++ b/Coder Desktop/VPN/XPCInterface.swift @@ -3,7 +3,7 @@ import os.log import VPNLib import VPNXPC -@objc final class VPNXPCInterface: NSObject, VPNXPCProtocol, @unchecked Sendable { +@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") @@ -24,4 +24,8 @@ import VPNXPC 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 b8ff0a2..07e58aa 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -1,16 +1,21 @@ import Foundation import NetworkExtension import VPNXPC +import os + +let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider") final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sendable { - let vpnXPCInterface = VPNXPCInterface() + let vpnXPCInterface = XPCInterface() var activeConnection: NSXPCConnection? var connMutex: NSLock = .init() - func getActiveConnection() -> NSXPCConnection? { + func getActiveConnection() -> VPNXPCClientCallbackProtocol? { connMutex.lock() defer { connMutex.unlock() } - return activeConnection + + let client = activeConnection?.remoteObjectProxy as? VPNXPCClientCallbackProtocol + return client } func setActiveConnection(_ connection: NSXPCConnection?) { @@ -24,8 +29,10 @@ final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sen 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() @@ -40,7 +47,6 @@ else { fatalError("Missing NEMachServiceName in Info.plist") } -print(serviceName) let globalXPCListenerDelegate = XPCListenerDelegate() let xpcListener = NSXPCListener(machServiceName: serviceName) xpcListener.delegate = globalXPCListenerDelegate diff --git a/Coder Desktop/VPNXPC/Protocol.swift b/Coder Desktop/VPNXPC/Protocol.swift index 813fd56..598a905 100644 --- a/Coder Desktop/VPNXPC/Protocol.swift +++ b/Coder Desktop/VPNXPC/Protocol.swift @@ -3,6 +3,7 @@ import Foundation @preconcurrency @objc public protocol VPNXPCProtocol { func getPeerInfo(with reply: @escaping () -> Void) + func ping(with reply: @escaping () -> Void) } @preconcurrency From 7724bc6e895ae5d934d7d81bec945bd01d507412 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 30 Jan 2025 00:06:03 -0600 Subject: [PATCH 12/16] lint --- Coder Desktop/Coder Desktop/XPCInterface.swift | 8 ++++---- Coder Desktop/VPN/Manager.swift | 4 ++-- Coder Desktop/VPN/PacketTunnelProvider.swift | 4 ++-- Coder Desktop/VPN/main.swift | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder Desktop/Coder Desktop/XPCInterface.swift index f51bc55..8d578c1 100644 --- a/Coder Desktop/Coder Desktop/XPCInterface.swift +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -1,7 +1,7 @@ -import os import Foundation -import XPCHub +import os import VPNXPC +import XPCHub @objc final class VPNXPCInterface: NSObject, XPCClientCallbackProtocol, @unchecked Sendable { private var svc: CoderVPNService @@ -25,13 +25,13 @@ import VPNXPC xpcConn.exportedObject = self xpcConn.invalidationHandler = { [weak self] in - guard let self = self else { return } + guard let self else { return } Task { @MainActor in self.logger.error("XPC connection invalidated.") } } xpcConn.interruptionHandler = { [weak self] in - guard let self = self else { return } + guard let self else { return } Task { @MainActor in self.logger.error("XPC connection interrupted.") } diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index f85061c..948c60f 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -1,9 +1,9 @@ import CoderSDK import NetworkExtension +import os import VPNLib -import XPCHub import VPNXPC -import os +import XPCHub actor Manager { let ptp: PacketTunnelProvider diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index 72604fe..4d0ce1e 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -1,7 +1,7 @@ import NetworkExtension +import os import VPNLib import VPNXPC -import os /* From */ let CTLIOCGINFO: UInt = 0xC064_4E03 @@ -17,7 +17,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { _ = strcpy($0, "com.apple.net.utun_control") } } - for fd: Int32 in 0...1024 { + for fd: Int32 in 0 ... 1024 { var addr = sockaddr_ctl() var ret: Int32 = -1 var len = socklen_t(MemoryLayout.size(ofValue: addr)) diff --git a/Coder Desktop/VPN/main.swift b/Coder Desktop/VPN/main.swift index 07e58aa..d350d8d 100644 --- a/Coder Desktop/VPN/main.swift +++ b/Coder Desktop/VPN/main.swift @@ -1,7 +1,7 @@ import Foundation import NetworkExtension -import VPNXPC import os +import VPNXPC let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider") @@ -13,7 +13,7 @@ final class XPCListenerDelegate: NSObject, NSXPCListenerDelegate, @unchecked Sen func getActiveConnection() -> VPNXPCClientCallbackProtocol? { connMutex.lock() defer { connMutex.unlock() } - + let client = activeConnection?.remoteObjectProxy as? VPNXPCClientCallbackProtocol return client } From 44f191f445df4658b539a0f6caeb72b76fb8dbbe Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 30 Jan 2025 00:12:46 -0600 Subject: [PATCH 13/16] remove unused import --- Coder Desktop/VPN/Manager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index 948c60f..3e0cfe0 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -3,7 +3,6 @@ import NetworkExtension import os import VPNLib import VPNXPC -import XPCHub actor Manager { let ptp: PacketTunnelProvider From 31f0b3be9bcc6ada1e4554b85b627198b387c5bd Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 30 Jan 2025 17:35:05 +1100 Subject: [PATCH 14/16] fix import --- Coder Desktop/Coder Desktop/XPCInterface.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Coder Desktop/Coder Desktop/XPCInterface.swift b/Coder Desktop/Coder Desktop/XPCInterface.swift index 8d578c1..a437385 100644 --- a/Coder Desktop/Coder Desktop/XPCInterface.swift +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -1,9 +1,8 @@ import Foundation import os import VPNXPC -import XPCHub -@objc final class VPNXPCInterface: NSObject, XPCClientCallbackProtocol, @unchecked Sendable { +@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 From 207da24edb43fdb3862592712aa5842a77370a77 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 30 Jan 2025 19:34:36 +1100 Subject: [PATCH 15/16] force review --- .../Coder Desktop/Coder_DesktopApp.swift | 3 +- Coder Desktop/Coder Desktop/VPNService.swift | 59 +++++++++---------- .../Coder Desktop/XPCInterface.swift | 16 ++--- Coder Desktop/VPN/PacketTunnelProvider.swift | 6 +- Coder Desktop/VPN/XPCInterface.swift | 5 +- 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 4bec8d2..b952e98 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/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 5ad1fb6..d024c5d 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -45,8 +45,8 @@ enum VPNServiceError: Error, Equatable { @MainActor final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") - // TODO: better init maybe? kinda wonky - lazy var xpc = VPNXPCInterface(vpn: self) + lazy var xpc: VPNXPCInterface = .init(vpn: self) + var terminating = false @Published var tunnelState: VPNServiceState = .disabled @Published var sysExtnState: SystemExtensionState = .uninstalled @@ -76,44 +76,39 @@ final class CoderVPNService: NSObject, VPNService { } } - var startTask: Task? func start() async { - if await startTask?.value != nil { - return - } + guard tunnelState == .disabled else { return } // this ping is somewhat load bearing since it causes xpc to init xpc.ping() - startTask = Task { - tunnelState = .connecting - await enableNetworkExtension() - logger.debug("network extension enabled") - } - defer { startTask = nil } - await startTask?.value + 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 { + guard tunnelState == .connected else { return } + tunnelState = .disconnecting + await disableNetworkExtension() + 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 } - stopTask = Task { - tunnelState = .disconnecting - await disableNetworkExtension() - logger.info("network extension stopped") - tunnelState = .disabled - } - 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 { @@ -122,7 +117,7 @@ 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) } } @@ -148,9 +143,13 @@ final class CoderVPNService: NSObject, VPNService { func onExtensionStop() { logger.info("network extension reported stopped") tunnelState = .disabled + if terminating { + NSApp.reply(toApplicationShouldTerminate: true) + } } func onExtensionError(_ error: NSError) { - logger.info("network extension reported error: \(error)") + 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 index a437385..6c0861c 100644 --- a/Coder Desktop/Coder Desktop/XPCInterface.swift +++ b/Coder Desktop/Coder Desktop/XPCInterface.swift @@ -23,29 +23,23 @@ import VPNXPC super.init() xpcConn.exportedObject = self - xpcConn.invalidationHandler = { [weak self] in - guard let self else { return } + xpcConn.invalidationHandler = { [logger] in Task { @MainActor in - self.logger.error("XPC connection invalidated.") + logger.error("XPC connection invalidated.") } } - xpcConn.interruptionHandler = { [weak self] in - guard let self else { return } + xpcConn.interruptionHandler = { [logger] in Task { @MainActor in - self.logger.error("XPC connection interrupted.") + logger.error("XPC connection interrupted.") } } xpcConn.resume() - - xpc.ping { - print("Got response from XPC") - } } func ping() { xpc.ping { Task { @MainActor in - print("Got response from XPC") + self.logger.info("Connected to NE over XPC") } } } diff --git a/Coder Desktop/VPN/PacketTunnelProvider.swift b/Coder Desktop/VPN/PacketTunnelProvider.swift index ca2f8e3..33020cd 100644 --- a/Coder Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder Desktop/VPN/PacketTunnelProvider.swift @@ -84,7 +84,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { if let conn = globalXPCListenerDelegate.getActiveConnection() { conn.onStart() } else { - logger.info("no active connection") + logger.info("no active XPC connection") } completionHandler(nil) } catch { @@ -92,7 +92,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { if let conn = globalXPCListenerDelegate.getActiveConnection() { conn.onError(error as NSError) } else { - logger.info("no active connection") + logger.info("no active XPC connection") } completionHandler(error as NSError) } @@ -119,7 +119,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { if let conn = globalXPCListenerDelegate.getActiveConnection() { conn.onStop() } else { - logger.info("no active connection") + logger.info("no active XPC connection") } globalXPCListenerDelegate.vpnXPCInterface.setManager(nil) completionHandler() diff --git a/Coder Desktop/VPN/XPCInterface.swift b/Coder Desktop/VPN/XPCInterface.swift index c61c49f..3520fe8 100644 --- a/Coder Desktop/VPN/XPCInterface.swift +++ b/Coder Desktop/VPN/XPCInterface.swift @@ -10,14 +10,15 @@ import VPNXPC func setManager(_ newManager: Manager?) { managerLock.lock() + defer { managerLock.unlock() } manager = newManager - managerLock.unlock() } func getManager() -> Manager? { managerLock.lock() + defer { managerLock.unlock() } let m = manager - managerLock.unlock() + return m } From 139157dabf35059892d819cf11a205c2e0459bf4 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 30 Jan 2025 19:44:39 +1100 Subject: [PATCH 16/16] fixup --- Coder Desktop/Coder Desktop/VPNService.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index d024c5d..3506e10 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -77,7 +77,13 @@ final class CoderVPNService: NSObject, VPNService { } func start() async { - guard tunnelState == .disabled else { return } + switch tunnelState { + case .disabled, .failed: + break + default: + return + } + // this ping is somewhat load bearing since it causes xpc to init xpc.ping() tunnelState = .connecting