Skip to content

feat: add XPC communication to Network Extension #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Coder Desktop/Coder Desktop/Coder_Desktop.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
Expand Down
3 changes: 1 addition & 2 deletions Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
Task {
await vpn.stop()
NSApp.reply(toApplicationShouldTerminate: true)
await vpn.quit()
}
return .terminateLater
}
Expand Down
11 changes: 11 additions & 0 deletions Coder Desktop/Coder Desktop/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NetworkExtension</key>
<dict>
<key>NEMachServiceName</key>
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop.VPN</string>
</dict>
</dict>
</plist>
89 changes: 61 additions & 28 deletions Coder Desktop/Coder Desktop/VPNService.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import NetworkExtension
import os
import SwiftUI
import VPNLib
import VPNXPC

@MainActor
protocol VPNService: ObservableObject {
Expand Down Expand Up @@ -43,6 +45,9 @@ enum VPNServiceError: Error, Equatable {
@MainActor
final class CoderVPNService: NSObject, VPNService {
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
lazy var xpc: VPNXPCInterface = .init(vpn: self)
var terminating = false

@Published var tunnelState: VPNServiceState = .disabled
@Published var sysExtnState: SystemExtensionState = .uninstalled
@Published var neState: NetworkExtensionState = .unconfigured
Expand Down Expand Up @@ -71,46 +76,45 @@ final class CoderVPNService: NSObject, VPNService {
}
}

var startTask: Task<Void, Never>?
func start() async {
if await startTask?.value != nil {
switch tunnelState {
case .disabled, .failed:
break
default:
return
}
startTask = Task {
tunnelState = .connecting
await enableNetworkExtension()

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

var stopTask: Task<Void, Never>?
func stop() async {
// Wait for a start operation to finish first
await startTask?.value
guard state == .connected else { return }
if await stopTask?.value != nil {
return
}
stopTask = Task {
tunnelState = .disconnecting
await disableNetworkExtension()
guard tunnelState == .connected else { return }
tunnelState = .disconnecting
await disableNetworkExtension()
logger.info("network extension stopped")
}

// TODO: determine when the NetworkExtension is completely disconnected
tunnelState = .disabled
// Instructs the service to stop the VPN and then quit once the stop event
// is read over XPC.
// MUST only be called from `NSApplicationDelegate.applicationShouldTerminate`
// MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
func quit() async {
guard tunnelState == .connected else {
NSApp.reply(toApplicationShouldTerminate: true)
return
}
defer { stopTask = nil }
await stopTask?.value
terminating = true
await stop()
}

func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
Task {
if proto != nil {
await configureNetworkExtension(proto: proto!)
if let proto {
await configureNetworkExtension(proto: proto)
// this just configures the VPN, it doesn't enable it
tunnelState = .disabled
} else {
Expand All @@ -119,10 +123,39 @@ final class CoderVPNService: NSObject, VPNService {
neState = .unconfigured
tunnelState = .disabled
} catch {
logger.error("failed to remoing network extension: \(error)")
logger.error("failed to remove network extension: \(error)")
neState = .failed(error.localizedDescription)
}
}
}
}

func onExtensionPeerUpdate(_ data: Data) {
// TODO: handle peer update
logger.info("network extension peer update")
do {
let msg = try Vpn_TunnelMessage(serializedBytes: data)
debugPrint(msg)
} catch {
logger.error("failed to decode peer update \(error)")
}
}

func onExtensionStart() {
logger.info("network extension reported started")
tunnelState = .connected
}

func onExtensionStop() {
logger.info("network extension reported stopped")
tunnelState = .disabled
if terminating {
NSApp.reply(toApplicationShouldTerminate: true)
}
}

func onExtensionError(_ error: NSError) {
logger.error("network extension reported error: \(error)")
tunnelState = .failed(.internalError(error.localizedDescription))
}
}
70 changes: 70 additions & 0 deletions Coder Desktop/Coder Desktop/XPCInterface.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation
import os
import VPNXPC

@objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable {
private var svc: CoderVPNService
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNXPCInterface")
private let xpc: VPNXPCProtocol

init(vpn: CoderVPNService) {
svc = vpn

let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any]
let machServiceName = networkExtDict?["NEMachServiceName"] as? String
let xpcConn = NSXPCConnection(machServiceName: machServiceName!)
xpcConn.remoteObjectInterface = NSXPCInterface(with: VPNXPCProtocol.self)
xpcConn.exportedInterface = NSXPCInterface(with: VPNXPCClientCallbackProtocol.self)
guard let proxy = xpcConn.remoteObjectProxy as? VPNXPCProtocol else {
fatalError("invalid xpc cast")
}
xpc = proxy

super.init()

xpcConn.exportedObject = self
xpcConn.invalidationHandler = { [logger] in
Task { @MainActor in
logger.error("XPC connection invalidated.")
}
}
xpcConn.interruptionHandler = { [logger] in
Task { @MainActor in
logger.error("XPC connection interrupted.")
}
}
xpcConn.resume()
}

func ping() {
xpc.ping {
Task { @MainActor in
self.logger.info("Connected to NE over XPC")
}
}
}

func onPeerUpdate(_ data: Data) {
Task { @MainActor in
svc.onExtensionPeerUpdate(data)
}
}

func onStart() {
Task { @MainActor in
svc.onExtensionStart()
}
}

func onStop() {
Task { @MainActor in
svc.onExtensionStop()
}
}

func onError(_ err: NSError) {
Task { @MainActor in
svc.onExtensionError(err)
}
}
}
Loading
Loading