@@ -2,14 +2,12 @@ import NetworkExtension
22import os
33import SwiftUI
44import VPNLib
5- import VPNXPC
65
76@MainActor
87protocol VPNService : ObservableObject {
98var state : VPNServiceState { get }
10- var agents : [ Agent ] { get }
9+ var agents : [ UUID : Agent ] { get }
1110func start( ) async
12- // Stop must be idempotent
1311func stop( ) async
1412func configureTunnelProviderProtocol( proto: NETunnelProviderProtocol ? )
1513}
@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable {
2624case internalError( String )
2725case systemExtensionError( SystemExtensionState )
2826case networkExtensionError( NetworkExtensionState )
29- case longTestError
3027
3128var description : String {
3229switch self {
33- case . longTestError:
34- " This is a long error to test the UI with long errors "
3530case let . internalError( description) :
3631" Internal Error: \( description) "
3732case let . systemExtensionError( state) :
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
4742var logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " vpn " )
4843 lazyvar xpc : VPNXPCInterface = . init( vpn: self )
4944var 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 {
6157return 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 {
7470Task {
7571await 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
7985func start( ) async {
@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService {
8490return
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
9499func stop( ) async {
95100guard tunnelState== . connectedelse { return }
96- tunnelState= . disconnecting
97101await disableNetworkExtension ( )
98102 logger. info ( " network extension stopped " )
99103}
@@ -131,31 +135,97 @@ final class CoderVPNService: NSObject, VPNService {
131135}
132136
133137func onExtensionPeerUpdate( _ data: Data ) {
134- // TODO: handle peer update
135138 logger. info ( " network extension peer update " )
136139do {
137- let msg = try Vpn_TunnelMessage ( serializedBytes: data)
140+ let msg = try Vpn_PeerUpdate ( serializedBytes: data)
138141debugPrint ( 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+ update. deletedAgents
151+ . compactMap { UUID ( uuidData: $0. id) }
152+ . forEach { agentIDin
153+ agents [ agentID] = nil
154+ }
155+ update. deletedWorkspaces
156+ . compactMap { UUID ( uuidData: $0. id) }
157+ . forEach { workspaceIDin
158+ workspaces [ workspaceID] = nil
159+ for (id, agent) in agentswhere agent. wsID== workspaceID{
160+ agents [ id] = nil
161+ }
162+ }
148163
149- func onExtensionStop( ) {
150- logger. info ( " network extension reported stopped " )
151- tunnelState= . disabled
152- if terminating{
153- NSApp . reply ( toApplicationShouldTerminate: true )
164+ // Update workspaces
165+ for workspaceProto in update. upsertedWorkspaces{
166+ if let workspaceID= UUID ( uuidData: workspaceProto. id) {
167+ workspaces [ workspaceID] = workspaceProto. name
168+ }
169+ }
170+
171+ for agentProto in update. upsertedAgents{
172+ guard let agentID= UUID ( uuidData: agentProto. id) else {
173+ continue
174+ }
175+ guard let workspaceID= UUID ( uuidData: agentProto. workspaceID) else {
176+ continue
177+ }
178+ let workspaceName = workspaces [ workspaceID] ?? " Unknown Workspace "
179+ let newAgent = Agent (
180+ id: agentID,
181+ name: agentProto. name,
182+ // If last handshake was not within last five minutes, the agent is unhealthy
183+ status: agentProto. lastHandshake. date> Date . now. addingTimeInterval ( - 300 ) ? . okay: . off,
184+ copyableDNS: agentProto. fqdn. first?? " UNKNOWN " ,
185+ wsName: workspaceName,
186+ wsID: workspaceID
187+ )
188+
189+ // An existing agent with the same name, belonging to the same workspace
190+ // is from a previous workspace build, and should be removed.
191+ agents
192+ . filter { $0. value. name== agentProto. name && $0. value. wsID== workspaceID}
193+ . forEach { agents [ $0. key] = nil }
194+
195+ agents [ agentID] = newAgent
154196}
155197}
198+ }
156199
157- func onExtensionError( _ error: NSError ) {
158- logger. error ( " network extension reported error: \( error) " )
159- tunnelState= . failed( . internalError( error. localizedDescription) )
200+ extension CoderVPNService {
201+ @objc private func vpnDidUpdate( _ notification: Notification ) {
202+ guard let connection= notification. objectas? NETunnelProviderSession else {
203+ return
204+ }
205+ switch connection. status{
206+ case . disconnected:
207+ if terminating{
208+ NSApp . reply ( toApplicationShouldTerminate: true )
209+ }
210+ connection. fetchLastDisconnectError { errin
211+ self . tunnelState= if let err{
212+ . failed( . internalError( err. localizedDescription) )
213+ } else {
214+ . disabled
215+ }
216+ }
217+ case . connecting:
218+ tunnelState= . connecting
219+ case . connected:
220+ tunnelState= . connected
221+ case . reasserting:
222+ tunnelState= . connecting
223+ case . disconnecting:
224+ tunnelState= . disconnecting
225+ case . invalid:
226+ tunnelState= . failed( . networkExtensionError( . unconfigured) )
227+ @unknown default :
228+ tunnelState= . disabled
229+ }
160230}
161231}