@@ -7,26 +7,56 @@ actor Manager {
77 let cfg : ManagerConfig
88 let telemetryEnricher : TelemetryEnricher
99
10- let tunnelHandle : TunnelHandle
10+ let tunnelDaemon : TunnelDaemon
1111 let speaker : Speaker < Vpn_ManagerMessage , Vpn_TunnelMessage >
1212 var readLoop : Task < Void , any Error > !
1313
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+
1726 private let logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " manager " )
1827
1928 // swiftlint:disable:next function_body_length
2029 init ( cfg: ManagerConfig ) async throws ( ManagerError) {
2130 self . cfg = cfg
2231 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)
3060 do {
3161 let sessionConfig = URLSessionConfiguration . default
3262 // The tunnel might be asked to start before the network interfaces have woken up from sleep
@@ -35,7 +65,7 @@ actor Manager {
3565 sessionConfig. timeoutIntervalForRequest = 60
3666 sessionConfig. timeoutIntervalForResource = 300
3767 try await download (
38- src: dylibPath ,
68+ src: binaryPath ,
3969 dest: dest,
4070 urlSession: URLSession ( configuration: sessionConfig)
4171 ) { progress in
@@ -45,48 +75,46 @@ actor Manager {
4575 throw . download( error)
4676 }
4777 pushProgress ( stage: . validating)
48- let client = Client ( url: cfg. serverUrl)
49- let buildInfo : BuildInfoResponse
5078 do {
51- buildInfo = try await client . buildInfo ( )
79+ try Validator . validate ( path : dest )
5280 } 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)
5784 }
85+
86+ // Without this, the TUN fd isn't recognised as a socket in the
87+ // spawned process, and the tunnel fails to start.
5888 do {
59- try Validator . validate ( path : dest , expectedVersion : semver )
89+ try unsetCloseOnExec ( fd : cfg . tunFd )
6090 } catch {
61- throw . validation ( error)
91+ throw . cloexec ( error)
6292 }
6393
6494 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+ }
66100 } catch {
67101 throw . tunnelSetup( error)
68102 }
69103 speaker = await Speaker < Vpn_ManagerMessage , Vpn_TunnelMessage > (
70- writeFD: tunnelHandle . writeHandle,
71- readFD: tunnelHandle . readHandle
104+ writeFD: tunnelDaemon . writeHandle,
105+ readFD: tunnelDaemon . readHandle
72106 )
73107 do {
74108 try await speaker. handshake ( )
75109 } catch {
76110 throw . handshake( error)
77111 }
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- }
86112
87113 readLoop = Task { try await run ( ) }
88114 }
89115
116+ deinit { logger. debug ( " manager deinit " ) }
117+
90118 func run( ) async throws {
91119 do {
92120 for try await m in speaker {
@@ -99,14 +127,14 @@ actor Manager {
99127 }
100128 } catch {
101129 logger. error ( " tunnel read loop failed: \( error. localizedDescription, privacy: . public) " )
102- try await tunnelHandle . close ( )
130+ try await tunnelDaemon . close ( )
103131 try await NEXPCServerDelegate . cancelProvider ( error:
104132 makeNSError ( suffix: " Manager " , desc: " Tunnel read loop failed: \( error. localizedDescription) " )
105133 )
106134 return
107135 }
108136 logger. info ( " tunnel read loop exited " )
109- try await tunnelHandle . close ( )
137+ try await tunnelDaemon . close ( )
110138 try await NEXPCServerDelegate . cancelProvider ( error: nil )
111139 }
112140
@@ -204,6 +232,12 @@ actor Manager {
204232 if !stopResp. success {
205233 throw . errorResponse( msg: stopResp. errorMessage)
206234 }
235+ do {
236+ try await tunnelDaemon. close ( )
237+ } catch {
238+ throw . tunnelFail( error)
239+ }
240+ readLoop. cancel ( )
207241 }
208242
209243 // Retrieves the current state of all peers,
@@ -239,28 +273,32 @@ struct ManagerConfig {
239273
240274enum ManagerError : Error {
241275 case download( DownloadError )
242- case tunnelSetup( TunnelHandleError )
276+ case fileError( String )
277+ case tunnelSetup( TunnelDaemonError )
243278 case handshake( HandshakeError )
244279 case validation( ValidationError )
245280 case incorrectResponse( Vpn_TunnelMessage )
281+ case cloexec( POSIXError )
246282 case failedRPC( any Error )
247283 case serverInfo( String )
248284 case errorResponse( msg: String )
249- case noTunnelFileDescriptor
250- case noApp
251- case permissionDenied
252285 case tunnelFail( any Error )
286+ case belowMinimumCoderVersion( actualVersion: String )
253287
254288 var description : String {
255289 switch self {
256290 case let . download( err) :
257291 " Download error: \( err. localizedDescription) "
292+ case let . fileError( msg) :
293+ msg
258294 case let . tunnelSetup( err) :
259295 " Tunnel setup error: \( err. localizedDescription) "
260296 case let . handshake( err) :
261297 " Handshake error: \( err. localizedDescription) "
262298 case let . validation( err) :
263299 " Validation error: \( err. localizedDescription) "
300+ case let . cloexec( err) :
301+ " Failed to mark TUN fd as non-cloexec: \( err. localizedDescription) "
264302 case . incorrectResponse:
265303 " Received unexpected response over tunnel "
266304 case let . failedRPC( err) :
@@ -269,14 +307,13 @@ enum ManagerError: Error {
269307 msg
270308 case let . errorResponse( msg) :
271309 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 "
278310 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+ """
280317 }
281318 }
282319
@@ -297,9 +334,16 @@ func writeVpnLog(_ log: Vpn_Log) {
297334 case . UNRECOGNIZED: . info
298335 }
299336 let logger = Logger (
300- subsystem: " \( Bundle . main. bundleIdentifier!) .dylib " ,
337+ subsystem: " \( Bundle . main. bundleIdentifier!) .daemon " ,
301338 category: log. loggerNames. joined ( separator: " . " )
302339 )
303340 let fields = log. fields. map { " \( $0. name) : \( $0. value) " } . joined ( separator: " , " )
304341 logger. log ( level: level, " \( log. message, privacy: . public) \( fields. isEmpty ? " " : " : \( fields) " , privacy: . public) " )
305342}
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