Skip to content

Commit 1737580

Browse files
committed
chore: make helper launchdaemon approval mandatory
1 parent faaa0af commit 1737580

File tree

9 files changed

+162
-156
lines changed

9 files changed

+162
-156
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ struct DesktopApp: App {
2626
SettingsView<CoderVPNService>()
2727
.environmentObject(appDelegate.vpn)
2828
.environmentObject(appDelegate.state)
29-
.environmentObject(appDelegate.helper)
3029
.environmentObject(appDelegate.autoUpdater)
3130
}
3231
.windowResizability(.contentSize)
@@ -48,13 +47,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4847
let fileSyncDaemon: MutagenDaemon
4948
let urlHandler: URLHandler
5049
let notifDelegate: NotifDelegate
51-
let helper: HelperService
5250
let autoUpdater: UpdaterService
5351

5452
override init() {
5553
notifDelegate = NotifDelegate()
5654
vpn = CoderVPNService()
57-
helper = HelperService()
5855
autoUpdater = UpdaterService()
5956
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
6057
vpn.onStart = {
@@ -95,10 +92,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
9592
image: "MenuBarIcon",
9693
onAppear: {
9794
// If the VPN is enabled, it's likely the token isn't expired
98-
guard self.vpn.state != .connected, self.state.hasSession else { return }
9995
Task { @MainActor in
96+
guard self.vpn.state != .connected, self.state.hasSession else { return }
10097
await self.state.handleTokenExpiry()
10198
}
99+
// If the Helper is pending approval, we should check if it's
100+
// been approved when the tray is opened.
101+
Task { @MainActor in
102+
guard self.vpn.state == .failed(.helperError(.requiresApproval)) else { return }
103+
self.vpn.refreshHelperState()
104+
}
102105
}, content: {
103106
VPNMenu<CoderVPNService, MutagenDaemon>().frame(width: 256)
104107
.environmentObject(self.vpn)
@@ -119,6 +122,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
119122
if await !vpn.loadNetworkExtensionConfig() {
120123
state.reconfigure()
121124
}
125+
await vpn.setupHelper()
122126
}
123127
}
124128

Coder-Desktop/Coder-Desktop/HelperService.swift

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,84 @@
11
import os
22
import ServiceManagement
33

4-
// Whilst the GUI app installs the helper, the System Extension communicates
5-
// with it over XPC
6-
@MainActor
7-
class HelperService: ObservableObject {
8-
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperService")
9-
let plistName = "com.coder.Coder-Desktop.Helper.plist"
10-
@Published var state: HelperState = .uninstalled {
11-
didSet {
12-
logger.info("helper daemon state set: \(self.state.description, privacy: .public)")
13-
}
14-
}
4+
extension CoderVPNService {
5+
var plistName: String { "com.coder.Coder-Desktop.Helper.plist" }
156

16-
init() {
17-
update()
7+
func refreshHelperState() {
8+
let daemon = SMAppService.daemon(plistName: plistName)
9+
helperState = HelperState(status: daemon.status)
1810
}
1911

20-
func update() {
21-
let daemon = SMAppService.daemon(plistName: plistName)
22-
state = HelperState(status: daemon.status)
12+
func setupHelper() async {
13+
refreshHelperState()
14+
switch helperState {
15+
case .uninstalled, .failed:
16+
await installHelper()
17+
case .installed:
18+
uninstallHelper()
19+
await installHelper()
20+
case .requiresApproval, .installing:
21+
break
22+
}
2323
}
2424

25-
func install() {
26-
let daemon = SMAppService.daemon(plistName: plistName)
27-
do {
28-
try daemon.register()
29-
} catch let error as NSError {
30-
self.state = .failed(.init(error: error))
31-
} catch {
32-
state = .failed(.unknown(error.localizedDescription))
25+
private func installHelper() async {
26+
// Worst case, this setup takes a few seconds. We'll show a loading
27+
// indicator in the meantime.
28+
helperState = .installing
29+
var lastUnknownError: Error?
30+
// Registration may fail with a permissions error if it was
31+
// just unregistered, so we retry a few times.
32+
for _ in 0 ... 10 {
33+
let daemon = SMAppService.daemon(plistName: plistName)
34+
do {
35+
try daemon.register()
36+
helperState = HelperState(status: daemon.status)
37+
return
38+
} catch {
39+
if daemon.status == .requiresApproval {
40+
helperState = .requiresApproval
41+
return
42+
}
43+
let helperError = HelperError(error: error as NSError)
44+
switch helperError {
45+
case .alreadyRegistered:
46+
helperState = .installed
47+
return
48+
case .launchDeniedByUser, .invalidSignature:
49+
// Something weird happened, we should update the UI
50+
helperState = .failed(helperError)
51+
return
52+
case .unknown:
53+
// Likely intermittent permissions error, we'll retry
54+
lastUnknownError = error
55+
logger.warning("failed to register helper: \(helperError.localizedDescription)")
56+
}
57+
58+
// Short delay before retrying
59+
try? await Task.sleep(for: .milliseconds(500))
60+
}
3361
}
34-
state = HelperState(status: daemon.status)
62+
// Give up, update the UI with the error
63+
helperState = .failed(.unknown(lastUnknownError?.localizedDescription ?? "Unknown"))
3564
}
3665

37-
func uninstall() {
66+
private func uninstallHelper() {
3867
let daemon = SMAppService.daemon(plistName: plistName)
3968
do {
4069
try daemon.unregister()
4170
} catch let error as NSError {
42-
self.state = .failed(.init(error: error))
71+
helperState = .failed(.init(error: error))
4372
} catch {
44-
state = .failed(.unknown(error.localizedDescription))
73+
helperState = .failed(.unknown(error.localizedDescription))
4574
}
46-
state = HelperState(status: daemon.status)
75+
helperState = HelperState(status: daemon.status)
4776
}
4877
}
4978

5079
enum HelperState: Equatable {
5180
case uninstalled
81+
case installing
5282
case installed
5383
case requiresApproval
5484
case failed(HelperError)
@@ -57,6 +87,8 @@ enum HelperState: Equatable {
5787
switch self {
5888
case .uninstalled:
5989
"Uninstalled"
90+
case .installing:
91+
"Installing"
6092
case .installed:
6193
"Installed"
6294
case .requiresApproval:

Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
8181
state = .connecting
8282
}
8383

84+
func updateHelperState() {}
85+
8486
var startWhenReady: Bool = false
8587
}

Coder-Desktop/Coder-Desktop/VPN/VPNService.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ enum VPNServiceError: Error, Equatable {
3636
case internalError(String)
3737
case systemExtensionError(SystemExtensionState)
3838
case networkExtensionError(NetworkExtensionState)
39+
case helperError(HelperState)
3940

4041
var description: String {
4142
switch self {
@@ -45,6 +46,8 @@ enum VPNServiceError: Error, Equatable {
4546
"SystemExtensionError: \(state.description)"
4647
case let .networkExtensionError(state):
4748
"NetworkExtensionError: \(state.description)"
49+
case let .helperError(state):
50+
"HelperError: \(state.description)"
4851
}
4952
}
5053

@@ -67,6 +70,13 @@ final class CoderVPNService: NSObject, VPNService {
6770
@Published var sysExtnState: SystemExtensionState = .uninstalled
6871
@Published var neState: NetworkExtensionState = .unconfigured
6972
var state: VPNServiceState {
73+
// The ordering here is important. The button to open the settings page
74+
// where the helper is approved is a no-op if the user has a settings
75+
// window on the page where the system extension is approved.
76+
// So, we want to ensure the helper settings button is clicked first.
77+
guard helperState == .installed else {
78+
return .failed(.helperError(helperState))
79+
}
7080
guard sysExtnState == .installed else {
7181
return .failed(.systemExtensionError(sysExtnState))
7282
}
@@ -80,6 +90,8 @@ final class CoderVPNService: NSObject, VPNService {
8090
return tunnelState
8191
}
8292

93+
@Published var helperState: HelperState = .uninstalled
94+
8395
@Published var progress: VPNProgress = .init(stage: .initial, downloadProgress: nil)
8496

8597
@Published var menuState: VPNMenuState = .init()
@@ -107,6 +119,14 @@ final class CoderVPNService: NSObject, VPNService {
107119
return
108120
}
109121

122+
// We have to manually fetch the helper state,
123+
// and we don't want to start the VPN
124+
// if the helper is not ready.
125+
refreshHelperState()
126+
if helperState != .installed {
127+
return
128+
}
129+
110130
menuState.clear()
111131
await startTunnel()
112132
logger.debug("network extension enabled")

Coder-Desktop/Coder-Desktop/Views/Settings/ExperimentalTab.swift

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

Coder-Desktop/Coder-Desktop/Views/Settings/HelperSection.swift

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

Coder-Desktop/Coder-Desktop/Views/Settings/Settings.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ struct SettingsView<VPN: VPNService>: View {
1313
.tabItem {
1414
Label("Network", systemImage: "dot.radiowaves.left.and.right")
1515
}.tag(SettingsTab.network)
16-
ExperimentalTab()
17-
.tabItem {
18-
Label("Experimental", systemImage: "gearshape.2")
19-
}.tag(SettingsTab.experimental)
20-
2116
}.frame(width: 600)
2217
.frame(maxHeight: 500)
2318
.scrollContentBackground(.hidden)
@@ -28,5 +23,4 @@ struct SettingsView<VPN: VPNService>: View {
2823
enum SettingsTab: Int {
2924
case general
3025
case network
31-
case experimental
3226
}

Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
124124
// Prevent starting the VPN before the user has approved the system extension.
125125
vpn.state == .failed(.systemExtensionError(.needsUserApproval)) ||
126126
// Prevent starting the VPN without a VPN configuration.
127-
vpn.state == .failed(.networkExtensionError(.unconfigured))
127+
vpn.state == .failed(.networkExtensionError(.unconfigured)) ||
128+
// Prevent starting the VPN before the Helper is approved
129+
vpn.state == .failed(.helperError(.requiresApproval)) ||
130+
// Prevent starting the VPN before the Helper is installed
131+
vpn.state == .failed(.helperError(.installing))
128132
)
129133
}
130134
}

0 commit comments

Comments
 (0)