Skip to content

Commit 87ec698

Browse files
committed
fix: unquarantine dylib after download
1 parent df3d755 commit 87ec698

File tree

12 files changed

+161
-52
lines changed

12 files changed

+161
-52
lines changed

Coder Desktop/Coder Desktop/Coder_Desktop.entitlements

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,9 @@
88
</array>
99
<key>com.apple.developer.system-extension.install</key>
1010
<true/>
11-
<key>com.apple.security.app-sandbox</key>
12-
<true/>
1311
<key>com.apple.security.application-groups</key>
1412
<array>
1513
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop</string>
1614
</array>
17-
<key>com.apple.security.files.user-selected.read-only</key>
18-
<true/>
19-
<key>com.apple.security.network.client</key>
20-
<true/>
2115
</dict>
2216
</plist>

Coder Desktop/Coder Desktop/NetworkExtension.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ enum NetworkExtensionState: Equatable {
2424
/// An actor that handles configuring, enabling, and disabling the VPN tunnel via the
2525
/// NetworkExtension APIs.
2626
extension CoderVPNService {
27-
func hasNetworkExtensionConfig() async -> Bool {
27+
func loadNetworkExtensionConfig() async {
2828
do {
29-
_ = try await getTunnelManager()
30-
return true
29+
let tm = try await getTunnelManager()
30+
neState = .disabled
31+
serverAddress = tm.protocolConfiguration?.serverAddress
3132
} catch {
32-
return false
33+
neState = .unconfigured
3334
}
3435
}
3536

Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,13 @@ final class CoderVPNService: NSObject, VPNService {
6363
// only stores a weak reference to the delegate.
6464
var systemExtnDelegate: SystemExtensionDelegate<CoderVPNService>?
6565

66+
var serverAddress: String?
67+
6668
override init() {
6769
super.init()
6870
installSystemExtension()
6971
Task {
70-
neState = if await hasNetworkExtensionConfig() {
71-
.disabled
72-
} else {
73-
.unconfigured
74-
}
72+
await loadNetworkExtensionConfig()
7573
}
7674
xpc.connect()
7775
xpc.getPeerState()
@@ -115,6 +113,7 @@ final class CoderVPNService: NSObject, VPNService {
115113
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
116114
Task {
117115
if let proto {
116+
serverAddress = proto.serverAddress
118117
await configureNetworkExtension(proto: proto)
119118
// this just configures the VPN, it doesn't enable it
120119
tunnelState = .disabled

Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,7 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
3131
Text("Workspace Agents")
3232
.font(.headline)
3333
.foregroundColor(.gray)
34-
if session.hasSession {
35-
VPNState<VPN>()
36-
} else {
37-
Text("Sign in to use CoderVPN")
38-
.font(.body)
39-
.foregroundColor(.gray)
40-
}
34+
VPNState<VPN, S>()
4135
}.padding([.horizontal, .top], Theme.Size.trayInset)
4236
Agents<VPN, S>()
4337
// Trailing stack
@@ -52,7 +46,15 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
5246
}.buttonStyle(.plain)
5347
TrayDivider()
5448
}
55-
AuthButton<VPN, S>()
49+
if vpn.state == .failed(.systemExtensionError(.needsUserApproval)) {
50+
Button {
51+
openSystemExtensionSettings()
52+
} label: {
53+
ButtonRowView { Text("Open System Preferences") }
54+
}.buttonStyle(.plain)
55+
} else {
56+
AuthButton<VPN, S>()
57+
}
5658
Button {
5759
openSettings()
5860
appActivate()
@@ -84,10 +86,18 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
8486
private var vpnDisabled: Bool {
8587
!session.hasSession ||
8688
vpn.state == .connecting ||
87-
vpn.state == .disconnecting
89+
vpn.state == .disconnecting ||
90+
vpn.state == .failed(.systemExtensionError(.needsUserApproval))
8891
}
8992
}
9093

94+
func openSystemExtensionSettings() {
95+
// TODO: Check this still works in a new macOS version
96+
// https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751?permalink_comment_id=5261757
97+
// swiftlint:disable:next line_length
98+
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.ExtensionsPreferences?extensionPointIdentifier=com.apple.system_extension.network_extension.extension-point")!)
99+
}
100+
91101
#Preview {
92102
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
93103
.environmentObject(PreviewVPN())

Coder Desktop/Coder Desktop/Views/VPNState.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
import SwiftUI
22

3-
struct VPNState<VPN: VPNService>: View {
3+
struct VPNState<VPN: VPNService, S: Session>: View {
44
@EnvironmentObject var vpn: VPN
5+
@EnvironmentObject var session: S
56

67
let inspection = Inspection<Self>()
78

89
var body: some View {
910
Group {
10-
switch vpn.state {
11-
case .disabled:
12-
Text("Enable CoderVPN to see agents")
11+
switch (vpn.state, session.hasSession) {
12+
case (.failed(.systemExtensionError(.needsUserApproval)), _):
13+
Text("Awaiting System Extension Approval")
14+
.font(.body)
15+
.foregroundStyle(.gray)
16+
case (_, false):
17+
Text("Sign in to use CoderVPN")
1318
.font(.body)
1419
.foregroundColor(.gray)
15-
case .connecting, .disconnecting:
20+
case (.disabled, _):
21+
Text("Enable CoderVPN to see agents")
22+
.font(.body)
23+
.foregroundStyle(.gray)
24+
case (.connecting, _), (.disconnecting, _):
1625
HStack {
1726
Spacer()
1827
ProgressView(
1928
vpn.state == .connecting ? "Starting CoderVPN..." : "Stopping CoderVPN..."
2029
).padding()
2130
Spacer()
2231
}
23-
case let .failed(vpnErr):
32+
case let (.failed(vpnErr), _):
2433
Text("\(vpnErr.description)")
2534
.font(.headline)
2635
.foregroundColor(.red)

Coder Desktop/Coder Desktop/XPCInterface.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,39 @@ import VPNLib
6464
svc.onExtensionPeerUpdate(data)
6565
}
6666
}
67+
68+
// The NE has verified the dylib and knows better than Gatekeeper
69+
func removeQuarantine(path: String, reply: @escaping (Bool) -> Void) {
70+
let reply = CallbackWrapper(reply)
71+
Task { @MainActor in
72+
let prompt = """
73+
Coder Desktop wants to execute code downloaded from \
74+
\(svc.serverAddress ?? "the Coder deployment"). The code has been \
75+
verified to be signed by Coder.
76+
"""
77+
let source = """
78+
do shell script "xattr -d com.apple.quarantine \(path)" \
79+
with prompt "\(prompt)" \
80+
with administrator privileges
81+
"""
82+
let success = await withCheckedContinuation { continuation in
83+
guard let script = NSAppleScript(source: source) else {
84+
continuation.resume(returning: false)
85+
return
86+
}
87+
// Run on a background thread
88+
Task.detached {
89+
var error: NSDictionary?
90+
script.executeAndReturnError(&error)
91+
if let error {
92+
self.logger.error("AppleScript error: \(error)")
93+
continuation.resume(returning: false)
94+
} else {
95+
continuation.resume(returning: true)
96+
}
97+
}
98+
}
99+
reply(success)
100+
}
101+
}
67102
}

Coder Desktop/Coder DesktopTests/VPNStateTests.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import ViewInspector
77
@Suite(.timeLimit(.minutes(1)))
88
struct VPNStateTests {
99
let vpn: MockVPNService
10-
let sut: VPNState<MockVPNService>
10+
let session: MockSession
11+
let sut: VPNState<MockVPNService, MockSession>
1112
let view: any View
1213

1314
init() {
1415
vpn = MockVPNService()
15-
sut = VPNState<MockVPNService>()
16-
view = sut.environmentObject(vpn)
16+
sut = VPNState<MockVPNService, MockSession>()
17+
session = MockSession()
18+
session.hasSession = true
19+
view = sut.environmentObject(vpn).environmentObject(session)
1720
}
1821

1922
@Test

Coder Desktop/VPN/Manager.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ actor Manager {
4646
} catch {
4747
throw .validation(error)
4848
}
49+
50+
// HACK: The downloaded dylib may be quarantined, but we've validated it's signature
51+
// so it's safe to execute. However, this SE must be sandboxed, so we defer to the app.
52+
try await removeQuarantine(dest)
53+
4954
do {
5055
try tunnelHandle = TunnelHandle(dylibPath: dest)
5156
} catch {
@@ -85,7 +90,13 @@ actor Manager {
8590
} catch {
8691
logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)")
8792
try await tunnelHandle.close()
88-
ptp.cancelTunnelWithError(error)
93+
ptp.cancelTunnelWithError(
94+
NSError(
95+
domain: "\(Bundle.main.bundleIdentifier!).Manager",
96+
code: -1,
97+
userInfo: [NSLocalizedDescriptionKey: "Tunnel read loop failed: \(error.localizedDescription)"]
98+
)
99+
)
89100
return
90101
}
91102
logger.info("tunnel read loop exited")
@@ -227,6 +238,9 @@ enum ManagerError: Error {
227238
case serverInfo(String)
228239
case errorResponse(msg: String)
229240
case noTunnelFileDescriptor
241+
case noApp
242+
case permissionDenied
243+
case tunnelFail(any Error)
230244

231245
var description: String {
232246
switch self {
@@ -248,6 +262,12 @@ enum ManagerError: Error {
248262
msg
249263
case .noTunnelFileDescriptor:
250264
"Could not find a tunnel file descriptor"
265+
case .noApp:
266+
"The VPN must be started with the app open during first-time setup."
267+
case .permissionDenied:
268+
"Permission was not granted to execute the CoderVPN dylib"
269+
case let .tunnelFail(err):
270+
"Failed to communicate with dylib over tunnel: \(err)"
251271
}
252272
}
253273
}
@@ -272,3 +292,23 @@ func writeVpnLog(_ log: Vpn_Log) {
272292
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
273293
logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
274294
}
295+
296+
private func removeQuarantine(_ dest: URL) async throws(ManagerError) {
297+
var flag: AnyObject?
298+
let file = NSURL(fileURLWithPath: dest.path)
299+
try? file.getResourceValue(&flag, forKey: kCFURLQuarantinePropertiesKey as URLResourceKey)
300+
if flag != nil {
301+
guard let conn = globalXPCListenerDelegate.conn else {
302+
throw .noApp
303+
}
304+
// Wait for unsandboxed app to accept our file
305+
let success = await withCheckedContinuation { [dest] continuation in
306+
conn.removeQuarantine(path: dest.path) { success in
307+
continuation.resume(returning: success)
308+
}
309+
}
310+
if !success {
311+
throw .permissionDenied
312+
}
313+
}
314+
}

Coder Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,47 +43,73 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4343
return nil
4444
}
4545

46+
// swiftlint:disable:next function_body_length
4647
override func startTunnel(
4748
options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void
4849
) {
4950
logger.info("startTunnel called")
5051
guard manager == nil else {
5152
logger.error("startTunnel called with non-nil Manager")
52-
completionHandler(PTPError.alreadyRunning)
53+
completionHandler(
54+
NSError(
55+
domain: "\(Bundle.main.bundleIdentifier!).PTP",
56+
code: -1,
57+
userInfo: [NSLocalizedDescriptionKey: "Already running"]
58+
)
59+
)
5360
return
5461
}
5562
guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
5663
let baseAccessURL = proto.serverAddress
5764
else {
5865
logger.error("startTunnel called with nil protocolConfiguration")
59-
completionHandler(PTPError.missingConfiguration)
66+
completionHandler(
67+
NSError(
68+
domain: "\(Bundle.main.bundleIdentifier!).PTP",
69+
code: -1,
70+
userInfo: [NSLocalizedDescriptionKey: "Missing Configuration"]
71+
)
72+
)
6073
return
6174
}
6275
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
6376
guard let token = proto.providerConfiguration?["token"] as? String else {
6477
logger.error("startTunnel called with nil token")
65-
completionHandler(PTPError.missingToken)
78+
completionHandler(
79+
NSError(
80+
domain: "\(Bundle.main.bundleIdentifier!).PTP",
81+
code: -1,
82+
userInfo: [NSLocalizedDescriptionKey: "Missing Token"]
83+
)
84+
)
6685
return
6786
}
6887
logger.debug("retrieved token & access URL")
6988
let completionHandler = CallbackWrapper(completionHandler)
7089
Task {
7190
do throws(ManagerError) {
7291
logger.debug("creating manager")
73-
manager = try await Manager(
92+
let manager = try await Manager(
7493
with: self,
7594
cfg: .init(
7695
apiToken: token, serverUrl: .init(string: baseAccessURL)!
7796
)
7897
)
7998
globalXPCListenerDelegate.vpnXPCInterface.manager = manager
8099
logger.debug("starting vpn")
81-
try await manager!.startVPN()
100+
try await manager.startVPN()
82101
logger.info("vpn started")
102+
self.manager = manager
83103
completionHandler(nil)
84104
} catch {
85105
logger.error("error starting manager: \(error.description, privacy: .public)")
86-
completionHandler(error as NSError)
106+
completionHandler(
107+
NSError(
108+
domain: "\(Bundle.main.bundleIdentifier!).Manager",
109+
code: -1,
110+
userInfo: [NSLocalizedDescriptionKey: error.description]
111+
)
112+
)
87113
}
88114
}
89115
}
@@ -152,9 +178,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
152178
try await setTunnelNetworkSettings(currentSettings)
153179
}
154180
}
155-
156-
enum PTPError: Error {
157-
case alreadyRunning
158-
case missingConfiguration
159-
case missingToken
160-
}

Coder Desktop/VPNLib/Util.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
public struct CallbackWrapper<T, U>: @unchecked Sendable {
2-
private let block: (T?) -> U
2+
private let block: (T) -> U
33

4-
public init(_ block: @escaping (T?) -> U) {
4+
public init(_ block: @escaping (T) -> U) {
55
self.block = block
66
}
77

8-
public func callAsFunction(_ error: T?) -> U {
8+
public func callAsFunction(_ error: T) -> U {
99
block(error)
1010
}
1111
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy