- Notifications
You must be signed in to change notification settings - Fork5
feat: pass agent updates to UI#35
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -2,14 +2,12 @@ import NetworkExtension | ||
import os | ||
import SwiftUI | ||
import VPNLib | ||
@MainActor | ||
protocol VPNService: ObservableObject { | ||
var state: VPNServiceState { get } | ||
var agents: [UUID:Agent] { get } | ||
func start() async | ||
func stop() async | ||
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) | ||
} | ||
@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable { | ||
case internalError(String) | ||
case systemExtensionError(SystemExtensionState) | ||
case networkExtensionError(NetworkExtensionState) | ||
var description: String { | ||
switch self { | ||
case let .internalError(description): | ||
"Internal Error: \(description)" | ||
case let .systemExtensionError(state): | ||
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService { | ||
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") | ||
lazy var xpc: VPNXPCInterface = .init(vpn: self) | ||
var terminating = false | ||
var workspaces: [UUID: String] = [:] | ||
@Published var tunnelState: VPNServiceState = .disabled | ||
@Published var sysExtnState: SystemExtensionState = .uninstalled | ||
@@ -61,7 +57,7 @@ final class CoderVPNService: NSObject, VPNService { | ||
return tunnelState | ||
} | ||
@Published var agents: [UUID:Agent] = [:] | ||
// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get | ||
// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework | ||
@@ -74,6 +70,16 @@ final class CoderVPNService: NSObject, VPNService { | ||
Task { | ||
await loadNetworkExtension() | ||
} | ||
NotificationCenter.default.addObserver( | ||
self, | ||
selector: #selector(vpnDidUpdate(_:)), | ||
name: .NEVPNStatusDidChange, | ||
object: nil | ||
) | ||
} | ||
deinit { | ||
NotificationCenter.default.removeObserver(self) | ||
} | ||
func start() async { | ||
@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService { | ||
return | ||
} | ||
await enableNetworkExtension() | ||
// this ping is somewhat load bearing since it causes xpc to init | ||
xpc.ping() | ||
logger.debug("network extension enabled") | ||
} | ||
func stop() async { | ||
guard tunnelState == .connected else { return } | ||
await disableNetworkExtension() | ||
logger.info("network extension stopped") | ||
} | ||
@@ -131,31 +135,97 @@ final class CoderVPNService: NSObject, VPNService { | ||
} | ||
func onExtensionPeerUpdate(_ data: Data) { | ||
logger.info("network extension peer update") | ||
do { | ||
let msg = tryVpn_PeerUpdate(serializedBytes: data) | ||
debugPrint(msg) | ||
applyPeerUpdate(with: msg) | ||
} catch { | ||
logger.error("failed to decode peer update \(error)") | ||
} | ||
} | ||
func applyPeerUpdate(with update: Vpn_PeerUpdate) { | ||
// Delete agents | ||
update.deletedAgents | ||
.compactMap { UUID(uuidData: $0.id) } | ||
.forEach { agentID in | ||
agents[agentID] = nil | ||
} | ||
update.deletedWorkspaces | ||
.compactMap { UUID(uuidData: $0.id) } | ||
.forEach { workspaceID in | ||
workspaces[workspaceID] = nil | ||
for (id, agent) in agents where agent.wsID == workspaceID { | ||
agents[id] = nil | ||
} | ||
} | ||
// Update workspaces | ||
for workspaceProto in update.upsertedWorkspaces { | ||
if let workspaceID = UUID(uuidData: workspaceProto.id) { | ||
workspaces[workspaceID] = workspaceProto.name | ||
} | ||
} | ||
for agentProto in update.upsertedAgents { | ||
guard let agentID = UUID(uuidData: agentProto.id) else { | ||
continue | ||
} | ||
guard let workspaceID = UUID(uuidData: agentProto.workspaceID) else { | ||
continue | ||
} | ||
let workspaceName = workspaces[workspaceID] ?? "Unknown Workspace" | ||
let newAgent = Agent( | ||
id: agentID, | ||
name: agentProto.name, | ||
// If last handshake was not within last five minutes, the agent is unhealthy | ||
status: agentProto.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .off, | ||
copyableDNS: agentProto.fqdn.first ?? "UNKNOWN", | ||
wsName: workspaceName, | ||
wsID: workspaceID | ||
) | ||
// An existing agent with the same name, belonging to the same workspace | ||
// is from a previous workspace build, and should be removed. | ||
agents | ||
.filter { $0.value.name == agentProto.name && $0.value.wsID == workspaceID } | ||
.forEach { agents[$0.key] = nil } | ||
agents[agentID] = newAgent | ||
} | ||
} | ||
} | ||
extension CoderVPNService { | ||
@objc private func vpnDidUpdate(_ notification: Notification) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. macOS can tell us when the Network Extension changes state, including if there was an error that caused it to disconnect, e.g. NE crashes, | ||
guard let connection = notification.object as? NETunnelProviderSession else { | ||
return | ||
} | ||
switch connection.status { | ||
case .disconnected: | ||
if terminating { | ||
NSApp.reply(toApplicationShouldTerminate: true) | ||
} | ||
connection.fetchLastDisconnectError { err in | ||
self.tunnelState = if let err { | ||
.failed(.internalError(err.localizedDescription)) | ||
} else { | ||
.disabled | ||
} | ||
} | ||
case .connecting: | ||
tunnelState = .connecting | ||
case .connected: | ||
tunnelState = .connected | ||
case .reasserting: | ||
tunnelState = .connecting | ||
case .disconnecting: | ||
tunnelState = .disconnecting | ||
case .invalid: | ||
tunnelState = .failed(.networkExtensionError(.unconfigured)) | ||
@unknown default: | ||
tunnelState = .disabled | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -12,3 +12,22 @@ final class Inspection<V> { | ||
} | ||
} | ||
} | ||
extension UUID { | ||
var uuidData: Data { | ||
withUnsafePointer(to: uuid) { | ||
Data(bytes: $0, count: MemoryLayout.size(ofValue: uuid)) | ||
} | ||
} | ||
init?(uuidData: Data) { | ||
guard uuidData.count == 16 else { | ||
return nil | ||
} | ||
var uuid: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Very funny: there's no fixed size arrays in Swift (probably cause of objc), so | ||
withUnsafeMutableBytes(of: &uuid) { | ||
$0.copyBytes(from: uuidData) | ||
} | ||
self.init(uuid: uuid) | ||
} | ||
} |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.