Skip to content

Commit c7dbde8

Browse files
committed
chore: run coder connect networking from launchdaemon
1 parent 1737580 commit c7dbde8

18 files changed

+467
-425
lines changed

Coder-Desktop/Coder-Desktop/HelperService.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ extension CoderVPNService {
1212
func setupHelper() async {
1313
refreshHelperState()
1414
switch helperState {
15-
case .uninstalled, .failed:
16-
await installHelper()
17-
case .installed:
18-
uninstallHelper()
15+
case .uninstalled, .failed, .installed:
16+
await uninstallHelper()
1917
await installHelper()
2018
case .requiresApproval, .installing:
2119
break
@@ -63,10 +61,10 @@ extension CoderVPNService {
6361
helperState = .failed(.unknown(lastUnknownError?.localizedDescription ?? "Unknown"))
6462
}
6563

66-
private func uninstallHelper() {
64+
private func uninstallHelper() async {
6765
let daemon = SMAppService.daemon(plistName: plistName)
6866
do {
69-
try daemon.unregister()
67+
try await daemon.unregister()
7068
} catch let error as NSError {
7169
helperState = .failed(.init(error: error))
7270
} catch {

Coder-Desktop/Coder-Desktop/VPN/VPNService.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ enum VPNServiceError: Error, Equatable {
5757
@MainActor
5858
final class CoderVPNService: NSObject, VPNService {
5959
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
60-
lazy var xpc: VPNXPCInterface = .init(vpn: self)
60+
lazy var xpc: AppXPCListener = .init(vpn: self)
6161

6262
@Published var tunnelState: VPNServiceState = .disabled {
6363
didSet {
@@ -158,10 +158,10 @@ final class CoderVPNService: NSObject, VPNService {
158158
}
159159
}
160160

161-
func onExtensionPeerUpdate(_ data: Data) {
161+
func onExtensionPeerUpdate(_ diff: Data) {
162162
logger.info("network extension peer update")
163163
do {
164-
let msg = try Vpn_PeerUpdate(serializedBytes: data)
164+
let msg = try Vpn_PeerUpdate(serializedBytes: diff)
165165
debugPrint(msg)
166166
applyPeerUpdate(with: msg)
167167
} catch {
@@ -219,16 +219,18 @@ extension CoderVPNService {
219219
break
220220
// Non-connecting -> Connecting: Establish XPC
221221
case (_, .connecting):
222-
xpc.connect()
223-
xpc.ping()
222+
// Detached to run ASAP
223+
// TODO: Switch to `Task.immediate` once stable
224+
Task.detached { try? await self.xpc.ping() }
224225
tunnelState = .connecting
225226
// Non-connected -> Connected:
226227
// - Retrieve Peers
227228
// - Run `onStart` closure
228229
case (_, .connected):
229230
onStart?()
230-
xpc.connect()
231-
xpc.getPeerState()
231+
// Detached to run ASAP
232+
// TODO: Switch to `Task.immediate` once stable
233+
Task.detached { try? await self.xpc.getPeerState() }
232234
tunnelState = .connected
233235
// Any -> Reasserting
234236
case (_, .reasserting):

Coder-Desktop/Coder-Desktop/Views/LoginForm.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,13 @@ struct LoginForm: View {
190190
@discardableResult
191191
func validateURL(_ url: String) throws(LoginError) -> URL {
192192
guard let url = URL(string: url) else {
193-
throw LoginError.invalidURL
193+
throw .invalidURL
194194
}
195195
guard url.scheme == "https" else {
196-
throw LoginError.httpsRequired
196+
throw .httpsRequired
197197
}
198198
guard url.host != nil else {
199-
throw LoginError.noHost
199+
throw .noHost
200200
}
201201
return url
202202
}

Coder-Desktop/Coder-Desktop/XPCInterface.swift

Lines changed: 67 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,112 +3,98 @@ import NetworkExtension
33
import os
44
import VPNLib
55

6-
@objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable {
6+
@objc final class AppXPCListener: NSObject, AppXPCInterface, @unchecked Sendable {
77
private var svc: CoderVPNService
8-
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface")
9-
private var xpc: VPNXPCProtocol?
8+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppXPCListener")
9+
private var connection: NSXPCConnection?
1010

1111
init(vpn: CoderVPNService) {
1212
svc = vpn
1313
super.init()
1414
}
1515

16-
func connect() {
17-
logger.debug("VPN xpc connect called")
18-
guard xpc == nil else {
19-
logger.debug("VPN xpc already exists")
20-
return
16+
func connect() -> NSXPCConnection {
17+
if let connection {
18+
return connection
2119
}
22-
let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any]
23-
let machServiceName = networkExtDict?["NEMachServiceName"] as? String
24-
let xpcConn = NSXPCConnection(machServiceName: machServiceName!)
25-
xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self)
26-
xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self)
27-
guard let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol else {
28-
fatalError("invalid xpc cast")
29-
}
30-
xpc = proxy
31-
32-
logger.debug("connecting to machServiceName: \(machServiceName!)")
3320

34-
xpcConn.exportedObject = self
35-
xpcConn.invalidationHandler = { [logger] in
36-
Task { @MainActor in
37-
logger.error("VPN XPC connection invalidated.")
38-
self.xpc = nil
39-
self.connect()
40-
}
41-
}
42-
xpcConn.interruptionHandler = { [logger] in
43-
Task { @MainActor in
44-
logger.error("VPN XPC connection interrupted.")
45-
self.xpc = nil
46-
self.connect()
47-
}
21+
let connection = NSXPCConnection(
22+
machServiceName: helperAppMachServiceName,
23+
options: .privileged
24+
)
25+
connection.remoteObjectInterface = NSXPCInterface(with: HelperAppXPCInterface.self)
26+
connection.exportedInterface = NSXPCInterface(with: AppXPCInterface.self)
27+
connection.exportedObject = self
28+
connection.invalidationHandler = {
29+
self.logger.error("XPC connection invalidated")
30+
self.connection = nil
31+
_ = self.connect()
4832
}
49-
xpcConn.resume()
50-
}
51-
52-
func ping() {
53-
xpc?.ping {
54-
Task { @MainActor in
55-
self.logger.info("Connected to NE over XPC")
56-
}
33+
connection.interruptionHandler = {
34+
self.logger.error("XPC connection interrupted")
35+
self.connection = nil
36+
_ = self.connect()
5737
}
38+
logger.info("connecting to \(helperAppMachServiceName)")
39+
connection.resume()
40+
self.connection = connection
41+
return connection
5842
}
5943

60-
func getPeerState() {
61-
xpc?.getPeerState { data in
62-
Task { @MainActor in
63-
self.svc.onExtensionPeerState(data)
64-
}
44+
func onPeerUpdate(_ diff: Data, reply: @escaping () -> Void) {
45+
let reply = CompletionWrapper(reply)
46+
Task { @MainActor in
47+
svc.onExtensionPeerUpdate(diff)
48+
reply()
6549
}
6650
}
6751

68-
func onPeerUpdate(_ data: Data) {
52+
func onProgress(stage: ProgressStage, downloadProgress: DownloadProgress?, reply: @escaping () -> Void) {
53+
let reply = CompletionWrapper(reply)
6954
Task { @MainActor in
70-
svc.onExtensionPeerUpdate(data)
55+
svc.onProgress(stage: stage, downloadProgress: downloadProgress)
56+
reply()
7157
}
7258
}
59+
}
7360

74-
func onProgress(stage: ProgressStage, downloadProgress: DownloadProgress?) {
75-
Task { @MainActor in
76-
svc.onProgress(stage: stage, downloadProgress: downloadProgress)
61+
// These methods are called to request updatess from the Helper.
62+
extension AppXPCListener {
63+
func ping() async throws {
64+
let conn = connect()
65+
return try await withCheckedThrowingContinuation { continuation in
66+
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ err in
67+
self.logger.error("failed to connect to HelperXPC \(err.localizedDescription, privacy: .public)")
68+
continuation.resume(throwing: err)
69+
}) as? HelperAppXPCInterface else {
70+
self.logger.error("failed to get proxy for HelperXPC")
71+
continuation.resume(throwing: XPCError.wrongProxyType)
72+
return
73+
}
74+
proxy.ping {
75+
self.logger.info("Connected to Helper over XPC")
76+
continuation.resume()
77+
}
7778
}
7879
}
7980

80-
// The NE has verified the dylib and knows better than Gatekeeper
81-
func removeQuarantine(path: String, reply: @escaping (Bool) -> Void) {
82-
let reply = CallbackWrapper(reply)
83-
Task { @MainActor in
84-
let prompt = """
85-
Coder Desktop wants to execute code downloaded from \
86-
\(svc.serverAddress ?? "the Coder deployment"). The code has been \
87-
verified to be signed by Coder.
88-
"""
89-
let source = """
90-
do shell script "xattr -d com.apple.quarantine \(path)" \
91-
with prompt "\(prompt)" \
92-
with administrator privileges
93-
"""
94-
let success = await withCheckedContinuation { continuation in
95-
guard let script = NSAppleScript(source: source) else {
96-
continuation.resume(returning: false)
97-
return
98-
}
99-
// Run on a background thread
100-
Task.detached {
101-
var error: NSDictionary?
102-
script.executeAndReturnError(&error)
103-
if let error {
104-
self.logger.error("AppleScript error: \(error)")
105-
continuation.resume(returning: false)
106-
} else {
107-
continuation.resume(returning: true)
108-
}
81+
func getPeerState() async throws {
82+
let conn = connect()
83+
return try await withCheckedThrowingContinuation { continuation in
84+
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ err in
85+
self.logger.error("failed to connect to HelperXPC \(err.localizedDescription, privacy: .public)")
86+
continuation.resume(throwing: err)
87+
}) as? HelperAppXPCInterface else {
88+
self.logger.error("failed to get proxy for HelperXPC")
89+
continuation.resume(throwing: XPCError.wrongProxyType)
90+
return
91+
}
92+
proxy.getPeerState { data in
93+
Task { @MainActor in
94+
self.svc.onExtensionPeerState(data)
10995
}
96+
continuation.resume()
11097
}
111-
reply(success)
11298
}
11399
}
114100
}

0 commit comments

Comments
 (0)