Skip to content

Commit ab4af62

Browse files
committed
chore: support operating the VPN without the app
1 parent f53fadd commit ab4af62

File tree

8 files changed

+113
-55
lines changed

8 files changed

+113
-55
lines changed

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4747
}
4848
}
4949

50+
// MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
5051
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
5152
Task {
52-
await vpn.quit()
53+
await vpn.stop()
54+
NSApp.reply(toApplicationShouldTerminate: true)
5355
}
5456
return .terminateLater
5557
}

Coder Desktop/Coder Desktop/NetworkExtension.swift

+5-23
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ enum NetworkExtensionState: Equatable {
2424
/// An actor that handles configuring, enabling, and disabling the VPN tunnel via the
2525
/// NetworkExtension APIs.
2626
extension CoderVPNService {
27-
// Updates the UI if a previous configuration exists
28-
func loadNetworkExtension() async {
29-
do {
30-
try await getTunnelManager()
31-
neState = .disabled
32-
} catch {
33-
neState = .unconfigured
34-
}
35-
}
36-
3727
func configureNetworkExtension(proto: NETunnelProviderProtocol) async {
3828
// removing the old tunnels, rather than reconfiguring ensures that configuration changes
3929
// are picked up.
@@ -74,39 +64,31 @@ extension CoderVPNService {
7464
func enableNetworkExtension() async {
7565
do {
7666
let tm = try await getTunnelManager()
77-
if !tm.isEnabled {
78-
tm.isEnabled = true
79-
try await tm.saveToPreferences()
80-
logger.debug("saved tunnel with enabled=true")
81-
}
8267
try tm.connection.startVPNTunnel()
8368
} catch {
84-
logger.error("enable network extension: \(error)")
69+
logger.error("start tunnel: \(error)")
8570
neState = .failed(error.localizedDescription)
8671
return
8772
}
88-
logger.debug("enabled and started tunnel")
73+
logger.debug("started tunnel")
8974
neState = .enabled
9075
}
9176

9277
func disableNetworkExtension() async {
9378
do {
9479
let tm = try await getTunnelManager()
9580
tm.connection.stopVPNTunnel()
96-
tm.isEnabled = false
97-
98-
try await tm.saveToPreferences()
9981
} catch {
100-
logger.error("disable network extension: \(error)")
82+
logger.error("stop tunnel: \(error)")
10183
neState = .failed(error.localizedDescription)
10284
return
10385
}
104-
logger.debug("saved tunnel with enabled=false")
86+
logger.debug("stopped tunnel")
10587
neState = .disabled
10688
}
10789

10890
@discardableResult
109-
private func getTunnelManager() async throws(VPNServiceError) -> NETunnelProviderManager {
91+
func getTunnelManager() async throws(VPNServiceError) -> NETunnelProviderManager {
11092
var tunnels: [NETunnelProviderManager] = []
11193
do {
11294
tunnels = try await NETunnelProviderManager.loadAllFromPreferences()

Coder Desktop/Coder Desktop/SystemExtension.swift

+62-5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ protocol SystemExtensionAsyncRecorder: Sendable {
2929
extension CoderVPNService: SystemExtensionAsyncRecorder {
3030
func recordSystemExtensionState(_ state: SystemExtensionState) async {
3131
sysExtnState = state
32+
if state == .uninstalled {
33+
installSystemExtension()
34+
}
3235
if state == .installed {
36+
do {
37+
try await getTunnelManager()
38+
neState = .disabled
39+
} catch {
40+
neState = .unconfigured
41+
}
3342
// system extension was successfully installed, so we don't need the delegate any more
3443
systemExtnDelegate = nil
3544
}
@@ -64,7 +73,21 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
6473
return extensionBundle
6574
}
6675

67-
func installSystemExtension() {
76+
func checkSystemExtensionStatus() {
77+
logger.info("checking SystemExtension status")
78+
guard let bundleID = extensionBundle.bundleIdentifier else {
79+
logger.error("Bundle has no identifier")
80+
return
81+
}
82+
let request = OSSystemExtensionRequest.propertiesRequest(forExtensionWithIdentifier: bundleID, queue: .main)
83+
let delegate = SystemExtensionDelegate(asyncDelegate: self)
84+
request.delegate = delegate
85+
systemExtnDelegate = delegate
86+
OSSystemExtensionManager.shared.submitRequest(request)
87+
logger.info("submitted SystemExtension properties request with bundleID: \(bundleID)")
88+
}
89+
90+
private func installSystemExtension() {
6891
logger.info("activating SystemExtension")
6992
guard let bundleID = extensionBundle.bundleIdentifier else {
7093
logger.error("Bundle has no identifier")
@@ -74,11 +97,9 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
7497
forExtensionWithIdentifier: bundleID,
7598
queue: .main
7699
)
77-
let delegate = SystemExtensionDelegate(asyncDelegate: self)
78-
systemExtnDelegate = delegate
79-
request.delegate = delegate
100+
request.delegate = systemExtnDelegate
80101
OSSystemExtensionManager.shared.submitRequest(request)
81-
logger.info("submitted SystemExtension request with bundleID: \(bundleID)")
102+
logger.info("submitted SystemExtension activate request with bundleID: \(bundleID)")
82103
}
83104
}
84105

@@ -88,6 +109,8 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
88109
NSObject, OSSystemExtensionRequestDelegate
89110
{
90111
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn-installer")
112+
// TODO: Refactor this to use a continuation, so the result of a request can be
113+
// 'await'd for the determined state
91114
private var asyncDelegate: AsyncDelegate
92115

93116
init(asyncDelegate: AsyncDelegate) {
@@ -138,4 +161,38 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
138161
logger.info("Replacing \(request.identifier) v\(existing.bundleShortVersion) with v\(`extension`.bundleShortVersion)")
139162
return .replace
140163
}
164+
165+
public func request(
166+
_: OSSystemExtensionRequest,
167+
foundProperties properties: [OSSystemExtensionProperties]
168+
) {
169+
// In debug builds we always replace the SE to test
170+
// changes made without bumping the version
171+
#if DEBUG
172+
Task { [asyncDelegate] in
173+
await asyncDelegate.recordSystemExtensionState(.uninstalled)
174+
}
175+
return
176+
#else
177+
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
178+
let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
179+
180+
let versionMatches = properties.contains { sysex in
181+
sysex.isEnabled
182+
&& sysex.bundleVersion == version
183+
&& sysex.bundleShortVersion == shortVersion
184+
}
185+
if versionMatches {
186+
Task { [asyncDelegate] in
187+
await asyncDelegate.recordSystemExtensionState(.installed)
188+
}
189+
return
190+
}
191+
192+
// Either uninstalled or needs replacing
193+
Task { [asyncDelegate] in
194+
await asyncDelegate.recordSystemExtensionState(.uninstalled)
195+
}
196+
#endif
197+
}
141198
}

Coder Desktop/Coder Desktop/VPNService.swift

+27-21
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ enum VPNServiceError: Error, Equatable {
4141
final class CoderVPNService: NSObject, VPNService {
4242
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
4343
lazy var xpc: VPNXPCInterface = .init(vpn: self)
44-
var terminating = false
4544
var workspaces: [UUID: String] = [:]
4645

4746
@Published var tunnelState: VPNServiceState = .disabled
@@ -66,10 +65,7 @@ final class CoderVPNService: NSObject, VPNService {
6665

6766
override init() {
6867
super.init()
69-
installSystemExtension()
70-
Task {
71-
await loadNetworkExtension()
72-
}
68+
checkSystemExtensionStatus()
7369
NotificationCenter.default.addObserver(
7470
self,
7571
selector: #selector(vpnDidUpdate(_:)),
@@ -82,6 +78,11 @@ final class CoderVPNService: NSObject, VPNService {
8278
NotificationCenter.default.removeObserver(self)
8379
}
8480

81+
func clearPeers() {
82+
agents = [:]
83+
workspaces = [:]
84+
}
85+
8586
func start() async {
8687
switch tunnelState {
8788
case .disabled, .failed:
@@ -102,19 +103,6 @@ final class CoderVPNService: NSObject, VPNService {
102103
logger.info("network extension stopped")
103104
}
104105

105-
// Instructs the service to stop the VPN and then quit once the stop event
106-
// is read over XPC.
107-
// MUST only be called from `NSApplicationDelegate.applicationShouldTerminate`
108-
// MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
109-
func quit() async {
110-
guard tunnelState == .connected else {
111-
NSApp.reply(toApplicationShouldTerminate: true)
112-
return
113-
}
114-
terminating = true
115-
await stop()
116-
}
117-
118106
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
119107
Task {
120108
if let proto {
@@ -145,6 +133,22 @@ final class CoderVPNService: NSObject, VPNService {
145133
}
146134
}
147135

136+
func onExtensionPeerState(_ data: Data?) {
137+
logger.info("network extension peer state")
138+
guard let data else {
139+
logger.error("could not retrieve peer state from network extension")
140+
return
141+
}
142+
do {
143+
let msg = try Vpn_PeerUpdate(serializedBytes: data)
144+
debugPrint(msg)
145+
clearPeers()
146+
applyPeerUpdate(with: msg)
147+
} catch {
148+
logger.error("failed to decode peer update \(error)")
149+
}
150+
}
151+
148152
func applyPeerUpdate(with update: Vpn_PeerUpdate) {
149153
// Delete agents
150154
update.deletedAgents
@@ -204,9 +208,6 @@ extension CoderVPNService {
204208
}
205209
switch connection.status {
206210
case .disconnected:
207-
if terminating {
208-
NSApp.reply(toApplicationShouldTerminate: true)
209-
}
210211
connection.fetchLastDisconnectError { err in
211212
self.tunnelState = if let err {
212213
.failed(.internalError(err.localizedDescription))
@@ -217,6 +218,11 @@ extension CoderVPNService {
217218
case .connecting:
218219
tunnelState = .connecting
219220
case .connected:
221+
// If we moved from disabled to connected, then the NE was already
222+
// running, and we need to request the current peer state
223+
if self.tunnelState == .disabled {
224+
xpc.getPeerState()
225+
}
220226
tunnelState = .connected
221227
case .reasserting:
222228
tunnelState = .connecting

Coder Desktop/Coder Desktop/XPCInterface.swift

+8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ import VPNLib
4545
}
4646
}
4747

48+
func getPeerState() {
49+
xpc.getPeerState { data in
50+
Task { @MainActor in
51+
self.svc.onExtensionPeerState(data)
52+
}
53+
}
54+
}
55+
4856
func onPeerUpdate(_ data: Data) {
4957
Task { @MainActor in
5058
svc.onExtensionPeerUpdate(data)

Coder Desktop/VPN/Manager.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ actor Manager {
194194

195195
// Retrieves the current state of all peers,
196196
// as required when starting the app whilst the network extension is already running
197-
func getPeerInfo() async throws(ManagerError) -> Vpn_PeerUpdate {
197+
func getPeerState() async throws(ManagerError) -> Vpn_PeerUpdate {
198198
logger.info("sending peer state request")
199199
let resp: Vpn_TunnelMessage
200200
do {

Coder Desktop/VPN/XPCInterface.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import VPNLib
2020
}
2121
}
2222

23-
func getPeerInfo(with reply: @escaping () -> Void) {
24-
// TODO: Retrieve from Manager
25-
reply()
23+
func getPeerState(with reply: @escaping (Data?) -> Void) {
24+
let reply = CallbackWrapper(reply)
25+
Task {
26+
let data = try? await manager?.getPeerState().serializedData()
27+
reply(data)
28+
}
2629
}
2730

2831
func ping(with reply: @escaping () -> Void) {

Coder Desktop/VPNLib/XPC.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
@preconcurrency
44
@objc public protocol VPNXPCProtocol {
5-
func getPeerInfo(with reply: @escaping () -> Void)
5+
func getPeerState(with reply: @escaping (Data?) -> Void)
66
func ping(with reply: @escaping () -> Void)
77
}
88

0 commit comments

Comments
 (0)