Skip to content

Commit df3d755

Browse files
chore: support operating the VPN without the app (#36)
1 parent 10c2109 commit df3d755

File tree

9 files changed

+81
-50
lines changed

9 files changed

+81
-50
lines changed

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4747
}
4848
}
4949

50+
// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
51+
// or return `.terminateNow`
5052
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
53+
if !settings.stopVPNOnQuit { return .terminateNow }
5154
Task {
52-
await vpn.quit()
55+
await vpn.stop()
56+
NSApp.reply(toApplicationShouldTerminate: true)
5357
}
5458
return .terminateLater
5559
}

Coder Desktop/Coder Desktop/NetworkExtension.swift

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ 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 {
27+
func hasNetworkExtensionConfig() async -> Bool {
2928
do {
30-
try await getTunnelManager()
31-
neState = .disabled
29+
_ = try await getTunnelManager()
30+
return true
3231
} catch {
33-
neState = .unconfigured
32+
return false
3433
}
3534
}
3635

@@ -71,37 +70,29 @@ extension CoderVPNService {
7170
}
7271
}
7372

74-
func enableNetworkExtension() async {
73+
func startTunnel() async {
7574
do {
7675
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-
}
8276
try tm.connection.startVPNTunnel()
8377
} catch {
84-
logger.error("enable network extension: \(error)")
78+
logger.error("start tunnel: \(error)")
8579
neState = .failed(error.localizedDescription)
8680
return
8781
}
88-
logger.debug("enabled and started tunnel")
82+
logger.debug("started tunnel")
8983
neState = .enabled
9084
}
9185

92-
func disableNetworkExtension() async {
86+
func stopTunnel() async {
9387
do {
9488
let tm = try await getTunnelManager()
9589
tm.connection.stopVPNTunnel()
96-
tm.isEnabled = false
97-
98-
try await tm.saveToPreferences()
9990
} catch {
100-
logger.error("disable network extension: \(error)")
91+
logger.error("stop tunnel: \(error)")
10192
neState = .failed(error.localizedDescription)
10293
return
10394
}
104-
logger.debug("saved tunnel with enabled=false")
95+
logger.debug("stopped tunnel")
10596
neState = .disabled
10697
}
10798

Coder Desktop/Coder Desktop/State.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ class Settings: ObservableObject {
104104
}
105105
}
106106

107+
@AppStorage(Keys.stopVPNOnQuit) var stopVPNOnQuit = true
108+
107109
init(store: UserDefaults = .standard) {
108110
self.store = store
109111
_literalHeaders = Published(
@@ -116,6 +118,7 @@ class Settings: ObservableObject {
116118
enum Keys {
117119
static let useLiteralHeaders = "UseLiteralHeaders"
118120
static let literalHeaders = "LiteralHeaders"
121+
static let stopVPNOnQuit = "StopVPNOnQuit"
119122
}
120123
}
121124

Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 31 additions & 21 deletions
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
@@ -68,8 +67,14 @@ final class CoderVPNService: NSObject, VPNService {
6867
super.init()
6968
installSystemExtension()
7069
Task {
71-
await loadNetworkExtension()
70+
neState = if await hasNetworkExtensionConfig() {
71+
.disabled
72+
} else {
73+
.unconfigured
74+
}
7275
}
76+
xpc.connect()
77+
xpc.getPeerState()
7378
NotificationCenter.default.addObserver(
7479
self,
7580
selector: #selector(vpnDidUpdate(_:)),
@@ -82,6 +87,11 @@ final class CoderVPNService: NSObject, VPNService {
8287
NotificationCenter.default.removeObserver(self)
8388
}
8489

90+
func clearPeers() {
91+
agents = [:]
92+
workspaces = [:]
93+
}
94+
8595
func start() async {
8696
switch tunnelState {
8797
case .disabled, .failed:
@@ -90,31 +100,18 @@ final class CoderVPNService: NSObject, VPNService {
90100
return
91101
}
92102

93-
await enableNetworkExtension()
94-
// this ping is somewhat load bearing since it causes xpc to init
103+
await startTunnel()
104+
xpc.connect()
95105
xpc.ping()
96106
logger.debug("network extension enabled")
97107
}
98108

99109
func stop() async {
100110
guard tunnelState == .connected else { return }
101-
await disableNetworkExtension()
111+
await stopTunnel()
102112
logger.info("network extension stopped")
103113
}
104114

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-
118115
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
119116
Task {
120117
if let proto {
@@ -145,6 +142,22 @@ final class CoderVPNService: NSObject, VPNService {
145142
}
146143
}
147144

145+
func onExtensionPeerState(_ data: Data?) {
146+
guard let data else {
147+
logger.error("could not retrieve peer state from network extension, it may not be running")
148+
return
149+
}
150+
logger.info("received network extension peer state")
151+
do {
152+
let msg = try Vpn_PeerUpdate(serializedBytes: data)
153+
debugPrint(msg)
154+
clearPeers()
155+
applyPeerUpdate(with: msg)
156+
} catch {
157+
logger.error("failed to decode peer update \(error)")
158+
}
159+
}
160+
148161
func applyPeerUpdate(with update: Vpn_PeerUpdate) {
149162
// Delete agents
150163
update.deletedAgents
@@ -204,9 +217,6 @@ extension CoderVPNService {
204217
}
205218
switch connection.status {
206219
case .disconnected:
207-
if terminating {
208-
NSApp.reply(toApplicationShouldTerminate: true)
209-
}
210220
connection.fetchLastDisconnectError { err in
211221
self.tunnelState = if let err {
212222
.failed(.internalError(err.localizedDescription))

Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import LaunchAtLogin
22
import SwiftUI
33

44
struct GeneralTab: View {
5+
@EnvironmentObject var settings: Settings
56
var body: some View {
67
Form {
78
Section {
89
LaunchAtLogin.Toggle("Launch at Login")
910
}
11+
Section {
12+
Toggle(isOn: $settings.stopVPNOnQuit) {
13+
Text("Stop VPN on Quit")
14+
}
15+
}
1016
}.formStyle(.grouped)
1117
}
1218
}

Coder Desktop/Coder Desktop/XPCInterface.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import VPNLib
66
@objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable {
77
private var svc: CoderVPNService
88
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface")
9-
private let xpc: VPNXPCProtocol
9+
private var xpc: VPNXPCProtocol?
1010

1111
init(vpn: CoderVPNService) {
1212
svc = vpn
13+
super.init()
14+
}
1315

16+
func connect() {
17+
guard xpc == nil else {
18+
return
19+
}
1420
let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any]
1521
let machServiceName = networkExtDict?["NEMachServiceName"] as? String
1622
let xpcConn = NSXPCConnection(machServiceName: machServiceName!)
@@ -21,30 +27,38 @@ import VPNLib
2127
}
2228
xpc = proxy
2329

24-
super.init()
25-
2630
xpcConn.exportedObject = self
2731
xpcConn.invalidationHandler = { [logger] in
2832
Task { @MainActor in
2933
logger.error("XPC connection invalidated.")
34+
self.xpc = nil
3035
}
3136
}
3237
xpcConn.interruptionHandler = { [logger] in
3338
Task { @MainActor in
3439
logger.error("XPC connection interrupted.")
40+
self.xpc = nil
3541
}
3642
}
3743
xpcConn.resume()
3844
}
3945

4046
func ping() {
41-
xpc.ping {
47+
xpc?.ping {
4248
Task { @MainActor in
4349
self.logger.info("Connected to NE over XPC")
4450
}
4551
}
4652
}
4753

54+
func getPeerState() {
55+
xpc?.getPeerState { data in
56+
Task { @MainActor in
57+
self.svc.onExtensionPeerState(data)
58+
}
59+
}
60+
}
61+
4862
func onPeerUpdate(_ data: Data) {
4963
Task { @MainActor in
5064
svc.onExtensionPeerUpdate(data)

Coder Desktop/VPN/Manager.swift

Lines changed: 1 addition & 1 deletion
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

Lines changed: 6 additions & 3 deletions
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

Lines changed: 1 addition & 1 deletion
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)
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