Skip to content

Commit 15f2bcc

Browse files
feat: add XPC communication to Network Extension (#29)
Co-authored-by: Ethan Dickson <[email protected]>
1 parent f3123f1 commit 15f2bcc

12 files changed

+350
-65
lines changed

Coder Desktop/Coder Desktop/Coder_Desktop.entitlements

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<true/>
1111
<key>com.apple.security.app-sandbox</key>
1212
<true/>
13+
<key>com.apple.security.application-groups</key>
14+
<array>
15+
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop</string>
16+
</array>
1317
<key>com.apple.security.files.user-selected.read-only</key>
1418
<true/>
1519
<key>com.apple.security.network.client</key>

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4949

5050
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
5151
Task {
52-
await vpn.stop()
53-
NSApp.reply(toApplicationShouldTerminate: true)
52+
await vpn.quit()
5453
}
5554
return .terminateLater
5655
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NetworkExtension</key>
6+
<dict>
7+
<key>NEMachServiceName</key>
8+
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop.VPN</string>
9+
</dict>
10+
</dict>
11+
</plist>

Coder Desktop/Coder Desktop/VPNService.swift

+61-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import NetworkExtension
22
import os
33
import SwiftUI
4+
import VPNLib
5+
import VPNXPC
46

57
@MainActor
68
protocol VPNService: ObservableObject {
@@ -43,6 +45,9 @@ enum VPNServiceError: Error, Equatable {
4345
@MainActor
4446
final class CoderVPNService: NSObject, VPNService {
4547
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
48+
lazy var xpc: VPNXPCInterface = .init(vpn: self)
49+
var terminating = false
50+
4651
@Published var tunnelState: VPNServiceState = .disabled
4752
@Published var sysExtnState: SystemExtensionState = .uninstalled
4853
@Published var neState: NetworkExtensionState = .unconfigured
@@ -71,46 +76,45 @@ final class CoderVPNService: NSObject, VPNService {
7176
}
7277
}
7378

74-
var startTask: Task<Void, Never>?
7579
func start() async {
76-
if await startTask?.value != nil {
80+
switch tunnelState {
81+
case .disabled, .failed:
82+
break
83+
default:
7784
return
7885
}
79-
startTask = Task {
80-
tunnelState = .connecting
81-
await enableNetworkExtension()
8286

83-
// TODO: enable communication with the NetworkExtension to track state and agents. For
84-
// now, just pretend it worked...
85-
tunnelState = .connected
86-
}
87-
defer { startTask = nil }
88-
await startTask?.value
87+
// this ping is somewhat load bearing since it causes xpc to init
88+
xpc.ping()
89+
tunnelState = .connecting
90+
await enableNetworkExtension()
91+
logger.debug("network extension enabled")
8992
}
9093

91-
var stopTask: Task<Void, Never>?
9294
func stop() async {
93-
// Wait for a start operation to finish first
94-
await startTask?.value
95-
guard state == .connected else { return }
96-
if await stopTask?.value != nil {
97-
return
98-
}
99-
stopTask = Task {
100-
tunnelState = .disconnecting
101-
await disableNetworkExtension()
95+
guard tunnelState == .connected else { return }
96+
tunnelState = .disconnecting
97+
await disableNetworkExtension()
98+
logger.info("network extension stopped")
99+
}
102100

103-
// TODO: determine when the NetworkExtension is completely disconnected
104-
tunnelState = .disabled
101+
// Instructs the service to stop the VPN and then quit once the stop event
102+
// is read over XPC.
103+
// MUST only be called from `NSApplicationDelegate.applicationShouldTerminate`
104+
// MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
105+
func quit() async {
106+
guard tunnelState == .connected else {
107+
NSApp.reply(toApplicationShouldTerminate: true)
108+
return
105109
}
106-
defer { stopTask = nil }
107-
await stopTask?.value
110+
terminating = true
111+
await stop()
108112
}
109113

110114
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
111115
Task {
112-
if proto != nil {
113-
await configureNetworkExtension(proto: proto!)
116+
if let proto {
117+
await configureNetworkExtension(proto: proto)
114118
// this just configures the VPN, it doesn't enable it
115119
tunnelState = .disabled
116120
} else {
@@ -119,10 +123,39 @@ final class CoderVPNService: NSObject, VPNService {
119123
neState = .unconfigured
120124
tunnelState = .disabled
121125
} catch {
122-
logger.error("failed to remoing network extension: \(error)")
126+
logger.error("failed to remove network extension: \(error)")
123127
neState = .failed(error.localizedDescription)
124128
}
125129
}
126130
}
127131
}
132+
133+
func onExtensionPeerUpdate(_ data: Data) {
134+
// TODO: handle peer update
135+
logger.info("network extension peer update")
136+
do {
137+
let msg = try Vpn_TunnelMessage(serializedBytes: data)
138+
debugPrint(msg)
139+
} catch {
140+
logger.error("failed to decode peer update \(error)")
141+
}
142+
}
143+
144+
func onExtensionStart() {
145+
logger.info("network extension reported started")
146+
tunnelState = .connected
147+
}
148+
149+
func onExtensionStop() {
150+
logger.info("network extension reported stopped")
151+
tunnelState = .disabled
152+
if terminating {
153+
NSApp.reply(toApplicationShouldTerminate: true)
154+
}
155+
}
156+
157+
func onExtensionError(_ error: NSError) {
158+
logger.error("network extension reported error: \(error)")
159+
tunnelState = .failed(.internalError(error.localizedDescription))
160+
}
128161
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Foundation
2+
import os
3+
import VPNXPC
4+
5+
@objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable {
6+
private var svc: CoderVPNService
7+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface")
8+
private let xpc: VPNXPCProtocol
9+
10+
init(vpn: CoderVPNService) {
11+
svc = vpn
12+
13+
let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any]
14+
let machServiceName = networkExtDict?["NEMachServiceName"] as? String
15+
let xpcConn = NSXPCConnection(machServiceName: machServiceName!)
16+
xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self)
17+
xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self)
18+
guard let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol else {
19+
fatalError("invalid xpc cast")
20+
}
21+
xpc = proxy
22+
23+
super.init()
24+
25+
xpcConn.exportedObject = self
26+
xpcConn.invalidationHandler = { [logger] in
27+
Task { @MainActor in
28+
logger.error("XPC connection invalidated.")
29+
}
30+
}
31+
xpcConn.interruptionHandler = { [logger] in
32+
Task { @MainActor in
33+
logger.error("XPC connection interrupted.")
34+
}
35+
}
36+
xpcConn.resume()
37+
}
38+
39+
func ping() {
40+
xpc.ping {
41+
Task { @MainActor in
42+
self.logger.info("Connected to NE over XPC")
43+
}
44+
}
45+
}
46+
47+
func onPeerUpdate(_ data: Data) {
48+
Task { @MainActor in
49+
svc.onExtensionPeerUpdate(data)
50+
}
51+
}
52+
53+
func onStart() {
54+
Task { @MainActor in
55+
svc.onExtensionStart()
56+
}
57+
}
58+
59+
func onStop() {
60+
Task { @MainActor in
61+
svc.onExtensionStop()
62+
}
63+
}
64+
65+
func onError(_ err: NSError) {
66+
Task { @MainActor in
67+
svc.onExtensionError(err)
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)