@@ -2,14 +2,12 @@ import NetworkExtension
22import os
33import SwiftUI
44import VPNLib
5- import VPNXPC
65
76@MainActor
87protocol VPNService : ObservableObject {
98 var state : VPNServiceState { get }
10- var agents : [ Agent ] { get }
9+ var agents : [ UUID : Agent ] { get }
1110 func start( ) async
12- // Stop must be idempotent
1311 func stop( ) async
1412 func configureTunnelProviderProtocol( proto: NETunnelProviderProtocol ? )
1513}
@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable {
2624 case internalError( String )
2725 case systemExtensionError( SystemExtensionState )
2826 case networkExtensionError( NetworkExtensionState )
29- case longTestError
3027
3128 var description : String {
3229 switch self {
33- case . longTestError:
34- " This is a long error to test the UI with long errors "
3530 case let . internalError( description) :
3631 " Internal Error: \( description) "
3732 case let . systemExtensionError( state) :
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
4742 var logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " vpn " )
4843 lazy var xpc : VPNXPCInterface = . init( vpn: self )
4944 var terminating = false
45+ var workspaces : [ UUID : String ] = [ : ]
5046
5147 @Published var tunnelState : VPNServiceState = . disabled
5248 @Published var sysExtnState : SystemExtensionState = . uninstalled
@@ -61,7 +57,7 @@ final class CoderVPNService: NSObject, VPNService {
6157 return tunnelState
6258 }
6359
64- @Published var agents : [ Agent ] = [ ]
60+ @Published var agents : [ UUID : Agent ] = [ : ]
6561
6662 // systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
6763 // garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
@@ -74,6 +70,16 @@ final class CoderVPNService: NSObject, VPNService {
7470 Task {
7571 await loadNetworkExtension ( )
7672 }
73+ NotificationCenter . default. addObserver (
74+ self ,
75+ selector: #selector( vpnDidUpdate ( _: ) ) ,
76+ name: . NEVPNStatusDidChange,
77+ object: nil
78+ )
79+ }
80+
81+ deinit {
82+ NotificationCenter . default. removeObserver ( self )
7783 }
7884
7985 func start( ) async {
@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService {
8490 return
8591 }
8692
93+ await enableNetworkExtension ( )
8794 // this ping is somewhat load bearing since it causes xpc to init
8895 xpc. ping ( )
89- tunnelState = . connecting
90- await enableNetworkExtension ( )
9196 logger. debug ( " network extension enabled " )
9297 }
9398
9499 func stop( ) async {
95100 guard tunnelState == . connected else { return }
96- tunnelState = . disconnecting
97101 await disableNetworkExtension ( )
98102 logger. info ( " network extension stopped " )
99103 }
@@ -131,31 +135,88 @@ final class CoderVPNService: NSObject, VPNService {
131135 }
132136
133137 func onExtensionPeerUpdate( _ data: Data ) {
134- // TODO: handle peer update
135138 logger. info ( " network extension peer update " )
136139 do {
137- let msg = try Vpn_TunnelMessage ( serializedBytes: data)
140+ let msg = try Vpn_PeerUpdate ( serializedBytes: data)
138141 debugPrint ( msg)
142+ applyPeerUpdate ( with: msg)
139143 } catch {
140144 logger. error ( " failed to decode peer update \( error) " )
141145 }
142146 }
143147
144- func onExtensionStart( ) {
145- logger. info ( " network extension reported started " )
146- tunnelState = . connected
147- }
148+ func applyPeerUpdate( with update: Vpn_PeerUpdate ) {
149+ // Delete agents
150+ let deletedWorkspaceIDs = Set ( update. deletedWorkspaces. compactMap { UUID ( uuidData: $0. id) } )
151+ let deletedAgentIDs = Set ( update. deletedAgents. compactMap { UUID ( uuidData: $0. id) } )
152+ for agentID in deletedAgentIDs {
153+ agents [ agentID] = nil
154+ }
155+ for workspaceID in deletedWorkspaceIDs {
156+ workspaces [ workspaceID] = nil
157+ for (id, agent) in agents where agent. wsID == workspaceID {
158+ agents [ id] = nil
159+ }
160+ }
148161
149- func onExtensionStop( ) {
150- logger. info ( " network extension reported stopped " )
151- tunnelState = . disabled
152- if terminating {
153- NSApp . reply ( toApplicationShouldTerminate: true )
162+ // Update workspaces
163+ for workspaceProto in update. upsertedWorkspaces {
164+ if let workspaceID = UUID ( uuidData: workspaceProto. id) {
165+ workspaces [ workspaceID] = workspaceProto. name
166+ }
167+ }
168+
169+ for agentProto in update. upsertedAgents {
170+ guard let agentID = UUID ( uuidData: agentProto. id) else {
171+ continue
172+ }
173+ guard let workspaceID = UUID ( uuidData: agentProto. workspaceID) else {
174+ continue
175+ }
176+ let workspaceName = workspaces [ workspaceID] ?? " Unknown Workspace "
177+ let newAgent = Agent (
178+ id: agentID,
179+ // If last handshake was not within last five minutes, the agent is unhealthy
180+ status: agentProto. lastHandshake. date > Date . now. addingTimeInterval ( - 300 ) ? . okay : . off,
181+ copyableDNS: agentProto. fqdn. first ?? " UNKNOWN " ,
182+ wsName: workspaceName,
183+ wsID: workspaceID
184+ )
185+
186+ agents [ agentID] = newAgent
154187 }
155188 }
189+ }
156190
157- func onExtensionError( _ error: NSError ) {
158- logger. error ( " network extension reported error: \( error) " )
159- tunnelState = . failed( . internalError( error. localizedDescription) )
191+ extension CoderVPNService {
192+ @objc private func vpnDidUpdate( _ notification: Notification ) {
193+ guard let connection = notification. object as? NETunnelProviderSession else {
194+ return
195+ }
196+ switch connection. status {
197+ case . disconnected:
198+ if terminating {
199+ NSApp . reply ( toApplicationShouldTerminate: true )
200+ }
201+ connection. fetchLastDisconnectError { err in
202+ self . tunnelState = if let err {
203+ . failed( . internalError( err. localizedDescription) )
204+ } else {
205+ . disabled
206+ }
207+ }
208+ case . connecting:
209+ tunnelState = . connecting
210+ case . connected:
211+ tunnelState = . connected
212+ case . reasserting:
213+ tunnelState = . connecting
214+ case . disconnecting:
215+ tunnelState = . disconnecting
216+ case . invalid:
217+ tunnelState = . failed( . networkExtensionError( . unconfigured) )
218+ @unknown default :
219+ tunnelState = . disabled
220+ }
160221 }
161222}
0 commit comments