Skip to content

Commit 2bfe5bd

Browse files
fix: unquarantine dylib after download (#38)
1 parent df3d755 commit 2bfe5bd

14 files changed

+145
-54
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/AuthButton.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct AuthButton<VPN: VPNService, S: Session>: View {
1717
}
1818
} label: {
1919
ButtonRowView {
20-
Text(session.hasSession ? "Sign Out" : "Sign In")
20+
Text(session.hasSession ? "Sign out" : "Sign in")
2121
}
2222
}.buttonStyle(.plain)
2323
}

Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 20 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("Approve in System Settings") }
54+
}.buttonStyle(.plain)
55+
} else {
56+
AuthButton<VPN, S>()
57+
}
5658
Button {
5759
openSettings()
5860
appActivate()
@@ -84,10 +86,19 @@ 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+
// Sourced from:
96+
// https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751?permalink_comment_id=5261757
97+
// We'll need to ensure this continues to work in future macOS versions
98+
// swiftlint:disable:next line_length
99+
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.ExtensionsPreferences?extensionPointIdentifier=com.apple.system_extension.network_extension.extension-point")!)
100+
}
101+
91102
#Preview {
92103
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
93104
.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/VPNMenuTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct VPNMenuTests {
2727
let toggle = try view.find(ViewType.Toggle.self)
2828
#expect(toggle.isDisabled())
2929
#expect(throws: Never.self) { try view.find(text: "Sign in to use CoderVPN") }
30-
#expect(throws: Never.self) { try view.find(button: "Sign In") }
30+
#expect(throws: Never.self) { try view.find(button: "Sign in") }
3131
}
3232
}
3333
}

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: 37 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,9 @@ 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+
makeNSError(suffix: "Manager", desc: "Tunnel read loop failed: \(error.localizedDescription)")
95+
)
8996
return
9097
}
9198
logger.info("tunnel read loop exited")
@@ -227,6 +234,9 @@ enum ManagerError: Error {
227234
case serverInfo(String)
228235
case errorResponse(msg: String)
229236
case noTunnelFileDescriptor
237+
case noApp
238+
case permissionDenied
239+
case tunnelFail(any Error)
230240

231241
var description: String {
232242
switch self {
@@ -248,6 +258,12 @@ enum ManagerError: Error {
248258
msg
249259
case .noTunnelFileDescriptor:
250260
"Could not find a tunnel file descriptor"
261+
case .noApp:
262+
"The VPN must be started with the app open during first-time setup."
263+
case .permissionDenied:
264+
"Permission was not granted to execute the CoderVPN dylib"
265+
case let .tunnelFail(err):
266+
"Failed to communicate with dylib over tunnel: \(err)"
251267
}
252268
}
253269
}
@@ -272,3 +288,23 @@ func writeVpnLog(_ log: Vpn_Log) {
272288
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
273289
logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
274290
}
291+
292+
private func removeQuarantine(_ dest: URL) async throws(ManagerError) {
293+
var flag: AnyObject?
294+
let file = NSURL(fileURLWithPath: dest.path)
295+
try? file.getResourceValue(&flag, forKey: kCFURLQuarantinePropertiesKey as URLResourceKey)
296+
if flag != nil {
297+
guard let conn = globalXPCListenerDelegate.conn else {
298+
throw .noApp
299+
}
300+
// Wait for unsandboxed app to accept our file
301+
let success = await withCheckedContinuation { [dest] continuation in
302+
conn.removeQuarantine(path: dest.path) { success in
303+
continuation.resume(returning: success)
304+
}
305+
}
306+
if !success {
307+
throw .permissionDenied
308+
}
309+
}
310+
}

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