Skip to content

Commit 15f2bcc

Browse files
feat: add XPC communication to Network Extension (#29)
Co-authored-by: Ethan Dickson <ethan@coder.com>
1 parent f3123f1 commit 15f2bcc

File tree

12 files changed

+350
-65
lines changed

12 files changed

+350
-65
lines changed

Coder Desktop/Coder Desktop/Coder_Desktop.entitlements

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 2 deletions
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
}
Lines changed: 11 additions & 0 deletions
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

Lines changed: 61 additions & 28 deletions
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
}
Lines changed: 70 additions & 0 deletions
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)
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