Skip to content

Commit f008801

Browse files
committed
chore: use slim binary over dylib
1 parent 8670f11 commit f008801

File tree

10 files changed

+499
-219
lines changed

10 files changed

+499
-219
lines changed

Coder-Desktop/Coder-DesktopHelper/Manager.swift

Lines changed: 88 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,54 @@ 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("Failed to create directories for binary destination: \(error.localizedDescription)")
41+
}
42+
let client = Client(url: cfg.serverUrl)
43+
let buildInfo: BuildInfoResponse
44+
do {
45+
buildInfo = try await client.buildInfo()
46+
} catch {
47+
throw .serverInfo(error.description)
48+
}
49+
guard let serverSemver = buildInfo.semver else {
50+
throw .serverInfo("invalid version: \(buildInfo.version)")
51+
}
52+
guard Validator.minimumCoderVersion
53+
.compare(serverSemver, options: .numeric) != .orderedDescending
54+
else {
55+
throw .belowMinimumCoderVersion(actualVersion: serverSemver)
56+
}
57+
let binaryPath = cfg.serverUrl.appending(path: "bin").appending(path: Manager.binaryName)
3058
do {
3159
let sessionConfig = URLSessionConfiguration.default
3260
// The tunnel might be asked to start before the network interfaces have woken up from sleep
@@ -35,7 +63,7 @@ actor Manager {
3563
sessionConfig.timeoutIntervalForRequest = 60
3664
sessionConfig.timeoutIntervalForResource = 300
3765
try await download(
38-
src: dylibPath,
66+
src: binaryPath,
3967
dest: dest,
4068
urlSession: URLSession(configuration: sessionConfig)
4169
) { progress in
@@ -45,48 +73,46 @@ actor Manager {
4573
throw .download(error)
4674
}
4775
pushProgress(stage: .validating)
48-
let client = Client(url: cfg.serverUrl)
49-
let buildInfo: BuildInfoResponse
5076
do {
51-
buildInfo = try await client.buildInfo()
77+
try Validator.validate(path: dest)
5278
} catch {
53-
throw .serverInfo(error.description)
54-
}
55-
guard let semver = buildInfo.semver else {
56-
throw .serverInfo("invalid version: \(buildInfo.version)")
79+
// Cleanup unvalid binary
80+
try? FileManager.default.removeItem(at: dest)
81+
throw .validation(error)
5782
}
83+
84+
// Without this, the TUN fd isn't recognised as a socket in the
85+
// spawned process, and the tunnel fails to start.
5886
do {
59-
try Validator.validate(path: dest, expectedVersion: semver)
87+
try unsetCloseOnExec(fd: cfg.tunFd)
6088
} catch {
61-
throw .validation(error)
89+
throw .cloexec(error)
6290
}
6391

6492
do {
65-
try tunnelHandle = TunnelHandle(dylibPath: dest)
93+
try tunnelDaemon = await TunnelDaemon(binaryPath: dest) { err in
94+
Task { try? await NEXPCServerDelegate.cancelProvider(error:
95+
makeNSError(suffix: "TunnelDaemon", desc: "Tunnel daemon: \(err.description)")
96+
) }
97+
}
6698
} catch {
6799
throw .tunnelSetup(error)
68100
}
69101
speaker = await Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>(
70-
writeFD: tunnelHandle.writeHandle,
71-
readFD: tunnelHandle.readHandle
102+
writeFD: tunnelDaemon.writeHandle,
103+
readFD: tunnelDaemon.readHandle
72104
)
73105
do {
74106
try await speaker.handshake()
75107
} catch {
76108
throw .handshake(error)
77109
}
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-
}
86110

87111
readLoop = Task { try await run() }
88112
}
89113

114+
deinit { logger.debug("manager deinit") }
115+
90116
func run() async throws {
91117
do {
92118
for try await m in speaker {
@@ -99,14 +125,14 @@ actor Manager {
99125
}
100126
} catch {
101127
logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)")
102-
try await tunnelHandle.close()
128+
try await tunnelDaemon.close()
103129
try await NEXPCServerDelegate.cancelProvider(error:
104130
makeNSError(suffix: "Manager", desc: "Tunnel read loop failed: \(error.localizedDescription)")
105131
)
106132
return
107133
}
108134
logger.info("tunnel read loop exited")
109-
try await tunnelHandle.close()
135+
try await tunnelDaemon.close()
110136
try await NEXPCServerDelegate.cancelProvider(error: nil)
111137
}
112138

@@ -204,6 +230,12 @@ actor Manager {
204230
if !stopResp.success {
205231
throw .errorResponse(msg: stopResp.errorMessage)
206232
}
233+
do {
234+
try await tunnelDaemon.close()
235+
} catch {
236+
throw .tunnelFail(error)
237+
}
238+
readLoop.cancel()
207239
}
208240

209241
// Retrieves the current state of all peers,
@@ -239,28 +271,32 @@ struct ManagerConfig {
239271

240272
enum ManagerError: Error {
241273
case download(DownloadError)
242-
case tunnelSetup(TunnelHandleError)
274+
case fileError(String)
275+
case tunnelSetup(TunnelDaemonError)
243276
case handshake(HandshakeError)
244277
case validation(ValidationError)
245278
case incorrectResponse(Vpn_TunnelMessage)
279+
case cloexec(POSIXError)
246280
case failedRPC(any Error)
247281
case serverInfo(String)
248282
case errorResponse(msg: String)
249-
case noTunnelFileDescriptor
250-
case noApp
251-
case permissionDenied
252283
case tunnelFail(any Error)
284+
case belowMinimumCoderVersion(actualVersion: String)
253285

254286
var description: String {
255287
switch self {
256288
case let .download(err):
257289
"Download error: \(err.localizedDescription)"
290+
case let .fileError(msg):
291+
msg
258292
case let .tunnelSetup(err):
259293
"Tunnel setup error: \(err.localizedDescription)"
260294
case let .handshake(err):
261295
"Handshake error: \(err.localizedDescription)"
262296
case let .validation(err):
263297
"Validation error: \(err.localizedDescription)"
298+
case let .cloexec(err):
299+
"Failed to mark TUN fd as non-cloexec: \(err.localizedDescription)"
264300
case .incorrectResponse:
265301
"Received unexpected response over tunnel"
266302
case let .failedRPC(err):
@@ -269,14 +305,13 @@ enum ManagerError: Error {
269305
msg
270306
case let .errorResponse(msg):
271307
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"
278308
case let .tunnelFail(err):
279-
"Failed to communicate with dylib over tunnel: \(err.localizedDescription)"
309+
"Failed to communicate with daemon over tunnel: \(err.localizedDescription)"
310+
case let .belowMinimumCoderVersion(actualVersion):
311+
"""
312+
The Coder deployment must be version \(Validator.minimumCoderVersion)
313+
or higher to use Coder Desktop. Current version: \(actualVersion)
314+
"""
280315
}
281316
}
282317

@@ -297,9 +332,16 @@ func writeVpnLog(_ log: Vpn_Log) {
297332
case .UNRECOGNIZED: .info
298333
}
299334
let logger = Logger(
300-
subsystem: "\(Bundle.main.bundleIdentifier!).dylib",
335+
subsystem: "\(Bundle.main.bundleIdentifier!).daemon",
301336
category: log.loggerNames.joined(separator: ".")
302337
)
303338
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
304339
logger.log(level: level, "\(log.message, privacy: .public)\(fields.isEmpty ? "" : ": \(fields)", privacy: .public)")
305340
}
341+
342+
extension FileManager {
343+
func ensureDirectories(for url: URL) throws {
344+
let dir = url.hasDirectoryPath ? url : url.deletingLastPathComponent()
345+
try createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
346+
}
347+
}

Coder-Desktop/Coder-DesktopHelper/TunnelHandle.swift

Lines changed: 0 additions & 116 deletions
This file was deleted.

Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ struct LoginTests {
134134
username: "admin"
135135
)
136136
let buildInfo = BuildInfoResponse(
137-
version: "v2.20.0"
137+
version: "v2.24.2"
138138
)
139139

140140
try Mock(

Coder-Desktop/VPNLib/Download.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ extension DownloadManager: URLSessionDownloadDelegate {
102102
return
103103
}
104104
guard httpResponse.statusCode != 304 else {
105-
// We already have the latest dylib downloaded in dest
105+
// We already have the latest binary downloaded in dest
106106
continuation.resume()
107107
return
108108
}

Coder-Desktop/VPNLib/Receiver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ actor Receiver<RecvMsg: Message> {
6969
},
7070
onCancel: {
7171
self.logger.debug("async stream canceled")
72-
self.dispatch.close()
72+
self.dispatch.close(flags: [.stop])
7373
}
7474
)
7575
}

0 commit comments

Comments
 (0)