@@ -7,26 +7,56 @@ actor Manager {
7
7
let cfg : ManagerConfig
8
8
let telemetryEnricher : TelemetryEnricher
9
9
10
- let tunnelHandle : TunnelHandle
10
+ let tunnelDaemon : TunnelDaemon
11
11
let speaker : Speaker < Vpn_ManagerMessage , Vpn_TunnelMessage >
12
12
var readLoop : Task < Void , any Error > !
13
13
14
- // /var/root/Downloads
15
- private let dest = FileManager . default. urls ( for: . downloadsDirectory, in: . userDomainMask)
16
- . first!. appending ( path: " coder-vpn.dylib " )
14
+ #if arch(arm64)
15
+ private static let binaryName = " coder-darwin-arm64 "
16
+ #else
17
+ private static let binaryName = " coder-darwin-amd64 "
18
+ #endif
19
+
20
+ // /var/root/Library/Application Support/com.coder.Coder-Desktop/coder-darwin-{arm64,amd64}
21
+ private let dest = try ? FileManager . default
22
+ . url ( for: . applicationSupportDirectory, in: . userDomainMask, appropriateFor: nil , create: true )
23
+ . appendingPathComponent ( Bundle . main. bundleIdentifier ?? " com.coder.Coder-Desktop " , isDirectory: true )
24
+ . appendingPathComponent ( binaryName)
25
+
17
26
private let logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " manager " )
18
27
19
28
// swiftlint:disable:next function_body_length
20
29
init ( cfg: ManagerConfig ) async throws ( ManagerError) {
21
30
self . cfg = cfg
22
31
telemetryEnricher = TelemetryEnricher ( )
23
- #if arch(arm64)
24
- let dylibPath = cfg. serverUrl. appending ( path: " bin/coder-vpn-darwin-arm64.dylib " )
25
- #elseif arch(x86_64)
26
- let dylibPath = cfg. serverUrl. appending ( path: " bin/coder-vpn-darwin-amd64.dylib " )
27
- #else
28
- fatalError ( " unknown architecture " )
29
- #endif
32
+ guard let dest else {
33
+ // This should never happen
34
+ throw . fileError( " Failed to create path for binary destination " +
35
+ " (/var/root/Library/Application Support/com.coder.Coder-Desktop) " )
36
+ }
37
+ do {
38
+ try FileManager . default. ensureDirectories ( for: dest)
39
+ } catch {
40
+ throw . fileError(
41
+ " Failed to create directories for binary destination ( \( dest) ): \( error. localizedDescription) "
42
+ )
43
+ }
44
+ let client = Client ( url: cfg. serverUrl)
45
+ let buildInfo : BuildInfoResponse
46
+ do {
47
+ buildInfo = try await client. buildInfo ( )
48
+ } catch {
49
+ throw . serverInfo( error. description)
50
+ }
51
+ guard let serverSemver = buildInfo. semver else {
52
+ throw . serverInfo( " invalid version: \( buildInfo. version) " )
53
+ }
54
+ guard Validator . minimumCoderVersion
55
+ . compare ( serverSemver, options: . numeric) != . orderedDescending
56
+ else {
57
+ throw . belowMinimumCoderVersion( actualVersion: serverSemver)
58
+ }
59
+ let binaryPath = cfg. serverUrl. appending ( path: " bin " ) . appending ( path: Manager . binaryName)
30
60
do {
31
61
let sessionConfig = URLSessionConfiguration . default
32
62
// The tunnel might be asked to start before the network interfaces have woken up from sleep
@@ -35,7 +65,7 @@ actor Manager {
35
65
sessionConfig. timeoutIntervalForRequest = 60
36
66
sessionConfig. timeoutIntervalForResource = 300
37
67
try await download (
38
- src: dylibPath ,
68
+ src: binaryPath ,
39
69
dest: dest,
40
70
urlSession: URLSession ( configuration: sessionConfig)
41
71
) { progress in
@@ -45,48 +75,46 @@ actor Manager {
45
75
throw . download( error)
46
76
}
47
77
pushProgress ( stage: . validating)
48
- let client = Client ( url: cfg. serverUrl)
49
- let buildInfo : BuildInfoResponse
50
78
do {
51
- buildInfo = try await client . buildInfo ( )
79
+ try Validator . validate ( path : dest )
52
80
} catch {
53
- throw . serverInfo( error. description)
54
- }
55
- guard let semver = buildInfo. semver else {
56
- throw . serverInfo( " invalid version: \( buildInfo. version) " )
81
+ // Cleanup unvalid binary
82
+ try ? FileManager . default. removeItem ( at: dest)
83
+ throw . validation( error)
57
84
}
85
+
86
+ // Without this, the TUN fd isn't recognised as a socket in the
87
+ // spawned process, and the tunnel fails to start.
58
88
do {
59
- try Validator . validate ( path : dest , expectedVersion : semver )
89
+ try unsetCloseOnExec ( fd : cfg . tunFd )
60
90
} catch {
61
- throw . validation ( error)
91
+ throw . cloexec ( error)
62
92
}
63
93
64
94
do {
65
- try tunnelHandle = TunnelHandle ( dylibPath: dest)
95
+ try tunnelDaemon = await TunnelDaemon ( binaryPath: dest) { err in
96
+ Task { try ? await NEXPCServerDelegate . cancelProvider ( error:
97
+ makeNSError ( suffix: " TunnelDaemon " , desc: " Tunnel daemon: \( err. description) " )
98
+ ) }
99
+ }
66
100
} catch {
67
101
throw . tunnelSetup( error)
68
102
}
69
103
speaker = await Speaker < Vpn_ManagerMessage , Vpn_TunnelMessage > (
70
- writeFD: tunnelHandle . writeHandle,
71
- readFD: tunnelHandle . readHandle
104
+ writeFD: tunnelDaemon . writeHandle,
105
+ readFD: tunnelDaemon . readHandle
72
106
)
73
107
do {
74
108
try await speaker. handshake ( )
75
109
} catch {
76
110
throw . handshake( error)
77
111
}
78
- do {
79
- try await tunnelHandle. openTunnelTask? . value
80
- } catch let error as TunnelHandleError {
81
- logger. error ( " failed to wait for dylib to open tunnel: \( error, privacy: . public) " )
82
- throw . tunnelSetup( error)
83
- } catch {
84
- fatalError ( " openTunnelTask must only throw TunnelHandleError " )
85
- }
86
112
87
113
readLoop = Task { try await run ( ) }
88
114
}
89
115
116
+ deinit { logger. debug ( " manager deinit " ) }
117
+
90
118
func run( ) async throws {
91
119
do {
92
120
for try await m in speaker {
@@ -99,14 +127,14 @@ actor Manager {
99
127
}
100
128
} catch {
101
129
logger. error ( " tunnel read loop failed: \( error. localizedDescription, privacy: . public) " )
102
- try await tunnelHandle . close ( )
130
+ try await tunnelDaemon . close ( )
103
131
try await NEXPCServerDelegate . cancelProvider ( error:
104
132
makeNSError ( suffix: " Manager " , desc: " Tunnel read loop failed: \( error. localizedDescription) " )
105
133
)
106
134
return
107
135
}
108
136
logger. info ( " tunnel read loop exited " )
109
- try await tunnelHandle . close ( )
137
+ try await tunnelDaemon . close ( )
110
138
try await NEXPCServerDelegate . cancelProvider ( error: nil )
111
139
}
112
140
@@ -204,6 +232,12 @@ actor Manager {
204
232
if !stopResp. success {
205
233
throw . errorResponse( msg: stopResp. errorMessage)
206
234
}
235
+ do {
236
+ try await tunnelDaemon. close ( )
237
+ } catch {
238
+ throw . tunnelFail( error)
239
+ }
240
+ readLoop. cancel ( )
207
241
}
208
242
209
243
// Retrieves the current state of all peers,
@@ -239,28 +273,32 @@ struct ManagerConfig {
239
273
240
274
enum ManagerError : Error {
241
275
case download( DownloadError )
242
- case tunnelSetup( TunnelHandleError )
276
+ case fileError( String )
277
+ case tunnelSetup( TunnelDaemonError )
243
278
case handshake( HandshakeError )
244
279
case validation( ValidationError )
245
280
case incorrectResponse( Vpn_TunnelMessage )
281
+ case cloexec( POSIXError )
246
282
case failedRPC( any Error )
247
283
case serverInfo( String )
248
284
case errorResponse( msg: String )
249
- case noTunnelFileDescriptor
250
- case noApp
251
- case permissionDenied
252
285
case tunnelFail( any Error )
286
+ case belowMinimumCoderVersion( actualVersion: String )
253
287
254
288
var description : String {
255
289
switch self {
256
290
case let . download( err) :
257
291
" Download error: \( err. localizedDescription) "
292
+ case let . fileError( msg) :
293
+ msg
258
294
case let . tunnelSetup( err) :
259
295
" Tunnel setup error: \( err. localizedDescription) "
260
296
case let . handshake( err) :
261
297
" Handshake error: \( err. localizedDescription) "
262
298
case let . validation( err) :
263
299
" Validation error: \( err. localizedDescription) "
300
+ case let . cloexec( err) :
301
+ " Failed to mark TUN fd as non-cloexec: \( err. localizedDescription) "
264
302
case . incorrectResponse:
265
303
" Received unexpected response over tunnel "
266
304
case let . failedRPC( err) :
@@ -269,14 +307,13 @@ enum ManagerError: Error {
269
307
msg
270
308
case let . errorResponse( msg) :
271
309
msg
272
- case . noTunnelFileDescriptor:
273
- " Could not find a tunnel file descriptor "
274
- case . noApp:
275
- " The VPN must be started with the app open during first-time setup. "
276
- case . permissionDenied:
277
- " Permission was not granted to execute the CoderVPN dylib "
278
310
case let . tunnelFail( err) :
279
- " Failed to communicate with dylib over tunnel: \( err. localizedDescription) "
311
+ " Failed to communicate with daemon over tunnel: \( err. localizedDescription) "
312
+ case let . belowMinimumCoderVersion( actualVersion) :
313
+ """
314
+ The Coder deployment must be version \( Validator . minimumCoderVersion)
315
+ or higher to use Coder Desktop. Current version: \( actualVersion)
316
+ """
280
317
}
281
318
}
282
319
@@ -297,9 +334,16 @@ func writeVpnLog(_ log: Vpn_Log) {
297
334
case . UNRECOGNIZED: . info
298
335
}
299
336
let logger = Logger (
300
- subsystem: " \( Bundle . main. bundleIdentifier!) .dylib " ,
337
+ subsystem: " \( Bundle . main. bundleIdentifier!) .daemon " ,
301
338
category: log. loggerNames. joined ( separator: " . " )
302
339
)
303
340
let fields = log. fields. map { " \( $0. name) : \( $0. value) " } . joined ( separator: " , " )
304
341
logger. log ( level: level, " \( log. message, privacy: . public) \( fields. isEmpty ? " " : " : \( fields) " , privacy: . public) " )
305
342
}
343
+
344
+ extension FileManager {
345
+ func ensureDirectories( for url: URL ) throws {
346
+ let dir = url. hasDirectoryPath ? url : url. deletingLastPathComponent ( )
347
+ try createDirectory ( at: dir, withIntermediateDirectories: true , attributes: nil )
348
+ }
349
+ }
0 commit comments