@@ -2,14 +2,12 @@ import NetworkExtension
2
2
import os
3
3
import SwiftUI
4
4
import VPNLib
5
- import VPNXPC
6
5
7
6
@MainActor
8
7
protocol VPNService : ObservableObject {
9
8
var state : VPNServiceState { get }
10
- var agents : [ Agent ] { get }
9
+ var agents : [ UUID : Agent ] { get }
11
10
func start( ) async
12
- // Stop must be idempotent
13
11
func stop( ) async
14
12
func configureTunnelProviderProtocol( proto: NETunnelProviderProtocol ? )
15
13
}
@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable {
26
24
case internalError( String )
27
25
case systemExtensionError( SystemExtensionState )
28
26
case networkExtensionError( NetworkExtensionState )
29
- case longTestError
30
27
31
28
var description : String {
32
29
switch self {
33
- case . longTestError:
34
- " This is a long error to test the UI with long errors "
35
30
case let . internalError( description) :
36
31
" Internal Error: \( description) "
37
32
case let . systemExtensionError( state) :
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
47
42
var logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " vpn " )
48
43
lazy var xpc : VPNXPCInterface = . init( vpn: self )
49
44
var terminating = false
45
+ var workspaces : [ UUID : String ] = [ : ]
50
46
51
47
@Published var tunnelState : VPNServiceState = . disabled
52
48
@Published var sysExtnState : SystemExtensionState = . uninstalled
@@ -61,7 +57,7 @@ final class CoderVPNService: NSObject, VPNService {
61
57
return tunnelState
62
58
}
63
59
64
- @Published var agents : [ Agent ] = [ ]
60
+ @Published var agents : [ UUID : Agent ] = [ : ]
65
61
66
62
// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
67
63
// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
@@ -74,6 +70,16 @@ final class CoderVPNService: NSObject, VPNService {
74
70
Task {
75
71
await loadNetworkExtension ( )
76
72
}
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 )
77
83
}
78
84
79
85
func start( ) async {
@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService {
84
90
return
85
91
}
86
92
93
+ await enableNetworkExtension ( )
87
94
// this ping is somewhat load bearing since it causes xpc to init
88
95
xpc. ping ( )
89
- tunnelState = . connecting
90
- await enableNetworkExtension ( )
91
96
logger. debug ( " network extension enabled " )
92
97
}
93
98
94
99
func stop( ) async {
95
100
guard tunnelState == . connected else { return }
96
- tunnelState = . disconnecting
97
101
await disableNetworkExtension ( )
98
102
logger. info ( " network extension stopped " )
99
103
}
@@ -131,31 +135,88 @@ final class CoderVPNService: NSObject, VPNService {
131
135
}
132
136
133
137
func onExtensionPeerUpdate( _ data: Data ) {
134
- // TODO: handle peer update
135
138
logger. info ( " network extension peer update " )
136
139
do {
137
- let msg = try Vpn_TunnelMessage ( serializedBytes: data)
140
+ let msg = try Vpn_PeerUpdate ( serializedBytes: data)
138
141
debugPrint ( msg)
142
+ applyPeerUpdate ( with: msg)
139
143
} catch {
140
144
logger. error ( " failed to decode peer update \( error) " )
141
145
}
142
146
}
143
147
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
+ }
148
161
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
154
187
}
155
188
}
189
+ }
156
190
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
+ }
160
221
}
161
222
}
0 commit comments