@@ -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,97 @@ 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
+ update. deletedAgents
151
+ . compactMap { UUID ( uuidData: $0. id) }
152
+ . forEach { agentID in
153
+ agents [ agentID] = nil
154
+ }
155
+ update. deletedWorkspaces
156
+ . compactMap { UUID ( uuidData: $0. id) }
157
+ . forEach { workspaceID in
158
+ workspaces [ workspaceID] = nil
159
+ for (id, agent) in agents where agent. wsID == workspaceID {
160
+ agents [ id] = nil
161
+ }
162
+ }
148
163
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
154
196
}
155
197
}
198
+ }
156
199
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. object as? NETunnelProviderSession else {
203
+ return
204
+ }
205
+ switch connection. status {
206
+ case . disconnected:
207
+ if terminating {
208
+ NSApp . reply ( toApplicationShouldTerminate: true )
209
+ }
210
+ connection. fetchLastDisconnectError { err in
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
+ }
160
230
}
161
231
}
0 commit comments