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)
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