Skip to content

Commit 8f00c6a

Browse files
committed
chore: use slim binary over dylib
1 parent 6687411 commit 8f00c6a

File tree

10 files changed

+473
-218
lines changed

10 files changed

+473
-218
lines changed

Coder-Desktop/Coder-DesktopHelper/Manager.swift

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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 NEXPCServerDelegate.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 NEXPCServerDelegate.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 NEXPCServerDelegate.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

240257
enum 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: ", ")

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
}

Coder-Desktop/VPNLib/Speaker.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
8686
}
8787
}
8888

89+
deinit { logger.debug("speaker deinit") }
90+
8991
/// Does the VPN Protocol handshake and validates the result
9092
public func handshake() async throws(HandshakeError) {
9193
let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role,
@@ -227,7 +229,7 @@ actor Handshaker {
227229
return try validateHeader(theirsString)
228230
} catch {
229231
writeFD.closeFile()
230-
dispatch.close()
232+
dispatch.close(flags: [.stop])
231233
throw error
232234
}
233235
}

0 commit comments

Comments
 (0)