@@ -7,26 +7,41 @@ 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
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/Downloads/coder-darwin-{arm64,amd64}
1521 private let dest = FileManager . default. urls ( for: . downloadsDirectory, in: . userDomainMask)
16- . first!. appending ( path: " coder-vpn.dylib " )
22+ . first!. appending ( path: binaryName )
1723 private let logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " manager " )
1824
1925 // swiftlint:disable:next function_body_length
2026 init ( cfg: ManagerConfig ) async throws ( ManagerError) {
2127 self . cfg = cfg
2228 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
29+ let client = Client ( url: cfg. serverUrl)
30+ let buildInfo : BuildInfoResponse
31+ do {
32+ buildInfo = try await client. buildInfo ( )
33+ } catch {
34+ throw . serverInfo( error. description)
35+ }
36+ guard let serverSemver = buildInfo. semver else {
37+ throw . serverInfo( " invalid version: \( buildInfo. version) " )
38+ }
39+ guard SignatureValidator . minimumCoderVersion
40+ . compare ( serverSemver, options: . numeric) != . orderedDescending
41+ else {
42+ throw . belowMinimumCoderVersion( actualVersion: serverSemver)
43+ }
44+ let binaryPath = cfg. serverUrl. appending ( path: " bin " ) . appending ( path: Manager . binaryName)
3045 do {
3146 let sessionConfig = URLSessionConfiguration . default
3247 // The tunnel might be asked to start before the network interfaces have woken up from sleep
@@ -35,7 +50,7 @@ actor Manager {
3550 sessionConfig. timeoutIntervalForRequest = 60
3651 sessionConfig. timeoutIntervalForResource = 300
3752 try await download (
38- src: dylibPath ,
53+ src: binaryPath ,
3954 dest: dest,
4055 urlSession: URLSession ( configuration: sessionConfig)
4156 ) { progress in
@@ -45,48 +60,44 @@ actor Manager {
4560 throw . download( error)
4661 }
4762 pushProgress ( stage: . validating)
48- let client = Client ( url: cfg. serverUrl)
49- let buildInfo : BuildInfoResponse
5063 do {
51- buildInfo = try await client . buildInfo ( )
64+ try SignatureValidator . validate ( path : dest )
5265 } catch {
53- throw . serverInfo( error. description)
54- }
55- guard let semver = buildInfo. semver else {
56- throw . serverInfo( " invalid version: \( buildInfo. version) " )
66+ throw . validation( error)
5767 }
68+
69+ // Without this, the TUN fd isn't recognised as a socket in the
70+ // spawned process, and the tunnel fails to start.
5871 do {
59- try SignatureValidator . validate ( path : dest , expectedVersion : semver )
72+ try unsetCloseOnExec ( fd : cfg . tunFd )
6073 } catch {
61- throw . validation ( error)
74+ throw . cloexec ( error)
6275 }
6376
6477 do {
65- try tunnelHandle = TunnelHandle ( dylibPath: dest)
78+ try tunnelDaemon = await TunnelDaemon ( binaryPath: dest) { err in
79+ Task { try ? await NEXPCListenerDelegate . cancelProvider ( error:
80+ makeNSError ( suffix: " TunnelDaemon " , desc: " Tunnel daemon: \( err. description) " )
81+ ) }
82+ }
6683 } catch {
6784 throw . tunnelSetup( error)
6885 }
6986 speaker = await Speaker < Vpn_ManagerMessage , Vpn_TunnelMessage > (
70- writeFD: tunnelHandle . writeHandle,
71- readFD: tunnelHandle . readHandle
87+ writeFD: tunnelDaemon . writeHandle,
88+ readFD: tunnelDaemon . readHandle
7289 )
7390 do {
7491 try await speaker. handshake ( )
7592 } catch {
7693 throw . handshake( error)
7794 }
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- }
8695
8796 readLoop = Task { try await run ( ) }
8897 }
8998
99+ deinit { logger. debug ( " manager deinit " ) }
100+
90101 func run( ) async throws {
91102 do {
92103 for try await m in speaker {
@@ -99,14 +110,14 @@ actor Manager {
99110 }
100111 } catch {
101112 logger. error ( " tunnel read loop failed: \( error. localizedDescription, privacy: . public) " )
102- try await tunnelHandle . close ( )
113+ try await tunnelDaemon . close ( )
103114 try await NEXPCListenerDelegate . cancelProvider ( error:
104115 makeNSError ( suffix: " Manager " , desc: " Tunnel read loop failed: \( error. localizedDescription) " )
105116 )
106117 return
107118 }
108119 logger. info ( " tunnel read loop exited " )
109- try await tunnelHandle . close ( )
120+ try await tunnelDaemon . close ( )
110121 try await NEXPCListenerDelegate . cancelProvider ( error: nil )
111122 }
112123
@@ -204,6 +215,12 @@ actor Manager {
204215 if !stopResp. success {
205216 throw . errorResponse( msg: stopResp. errorMessage)
206217 }
218+ do {
219+ try await tunnelDaemon. close ( )
220+ } catch {
221+ throw . tunnelFail( error)
222+ }
223+ readLoop. cancel ( )
207224 }
208225
209226 // Retrieves the current state of all peers,
@@ -239,17 +256,16 @@ struct ManagerConfig {
239256
240257enum ManagerError : Error {
241258 case download( DownloadError )
242- case tunnelSetup( TunnelHandleError )
259+ case tunnelSetup( TunnelDaemonError )
243260 case handshake( HandshakeError )
244261 case validation( ValidationError )
245262 case incorrectResponse( Vpn_TunnelMessage )
263+ case cloexec( POSIXError )
246264 case failedRPC( any Error )
247265 case serverInfo( String )
248266 case errorResponse( msg: String )
249- case noTunnelFileDescriptor
250- case noApp
251- case permissionDenied
252267 case tunnelFail( any Error )
268+ case belowMinimumCoderVersion( actualVersion: String )
253269
254270 var description : String {
255271 switch self {
@@ -261,6 +277,8 @@ enum ManagerError: Error {
261277 " Handshake error: \( err. localizedDescription) "
262278 case let . validation( err) :
263279 " Validation error: \( err. localizedDescription) "
280+ case let . cloexec( err) :
281+ " Failed to mark TUN fd as non-cloexec: \( err. localizedDescription) "
264282 case . incorrectResponse:
265283 " Received unexpected response over tunnel "
266284 case let . failedRPC( err) :
@@ -269,14 +287,13 @@ enum ManagerError: Error {
269287 msg
270288 case let . errorResponse( msg) :
271289 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 "
278290 case let . tunnelFail( err) :
279- " Failed to communicate with dylib over tunnel: \( err. localizedDescription) "
291+ " Failed to communicate with daemon over tunnel: \( err. localizedDescription) "
292+ case let . belowMinimumCoderVersion( actualVersion) :
293+ """
294+ The Coder deployment must be version \( SignatureValidator . minimumCoderVersion)
295+ or higher to use Coder Desktop. Current version: \( actualVersion)
296+ """
280297 }
281298 }
282299
@@ -297,7 +314,7 @@ func writeVpnLog(_ log: Vpn_Log) {
297314 case . UNRECOGNIZED: . info
298315 }
299316 let logger = Logger (
300- subsystem: " \( Bundle . main. bundleIdentifier!) .dylib " ,
317+ subsystem: " \( Bundle . main. bundleIdentifier!) .daemon " ,
301318 category: log. loggerNames. joined ( separator: " . " )
302319 )
303320 let fields = log. fields. map { " \( $0. name) : \( $0. value) " } . joined ( separator: " , " )
0 commit comments