-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathVPNService.swift
207 lines (186 loc) · 6.55 KB
/
VPNService.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import NetworkExtension
import os
import SwiftUI
import VPNLib
@MainActor
protocol VPNService: ObservableObject {
var state: VPNServiceState { get }
var menuState: VPNMenuState { get }
func start() async
func stop() async
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?)
}
enum VPNServiceState: Equatable {
case disabled
case connecting
case disconnecting
case connected
case failed(VPNServiceError)
var canBeStarted: Bool {
switch self {
// A tunnel failure should not prevent a reconnect attempt
case .disabled, .failed:
true
default:
false
}
}
}
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):
"SystemExtensionError: \(state.description)"
case let .networkExtensionError(state):
"NetworkExtensionError: \(state.description)"
}
}
var localizedDescription: String { description }
}
@MainActor
final class CoderVPNService: NSObject, VPNService {
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
lazy var xpc: VPNXPCInterface = .init(vpn: self)
@Published var tunnelState: VPNServiceState = .disabled
@Published var sysExtnState: SystemExtensionState = .uninstalled
@Published var neState: NetworkExtensionState = .unconfigured
var state: VPNServiceState {
guard sysExtnState == .installed else {
return .failed(.systemExtensionError(sysExtnState))
}
guard neState == .enabled || neState == .disabled else {
return .failed(.networkExtensionError(neState))
}
if startWhenReady, tunnelState.canBeStarted {
startWhenReady = false
Task { await start() }
}
return tunnelState
}
@Published var menuState: VPNMenuState = .init()
// Whether the VPN should start as soon as possible
var startWhenReady: Bool = false
// 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
// only stores a weak reference to the delegate.
var systemExtnDelegate: SystemExtensionDelegate<CoderVPNService>?
var serverAddress: String?
override init() {
super.init()
}
func start() async {
switch tunnelState {
case .disabled, .failed:
break
default:
return
}
menuState.clear()
await startTunnel()
logger.debug("network extension enabled")
}
func stop() async {
guard tunnelState == .connected else { return }
await stopTunnel()
logger.info("network extension stopped")
}
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?) {
Task {
if let proto {
serverAddress = proto.serverAddress
await configureNetworkExtension(proto: proto)
// this just configures the VPN, it doesn't enable it
tunnelState = .disabled
} else {
do {
try await removeNetworkExtension()
neState = .unconfigured
tunnelState = .disabled
} catch {
logger.error("failed to remove network extension: \(error)")
neState = .failed(error.localizedDescription)
}
}
}
}
func onExtensionPeerUpdate(_ data: Data) {
logger.info("network extension peer update")
do {
let msg = try Vpn_PeerUpdate(serializedBytes: data)
debugPrint(msg)
applyPeerUpdate(with: msg)
} catch {
logger.error("failed to decode peer update \(error)")
}
}
func onExtensionPeerState(_ data: Data?) {
guard let data else {
logger.error("could not retrieve peer state from network extension, it may not be running")
return
}
logger.info("received network extension peer state")
do {
let msg = try Vpn_PeerUpdate(serializedBytes: data)
debugPrint(msg)
menuState.clear()
applyPeerUpdate(with: msg)
} catch {
logger.error("failed to decode peer update \(error)")
}
}
func applyPeerUpdate(with update: Vpn_PeerUpdate) {
// Delete agents
update.deletedAgents.forEach { menuState.deleteAgent(withId: $0.id) }
update.deletedWorkspaces.forEach { menuState.deleteWorkspace(withId: $0.id) }
// Upsert workspaces before agents to populate agent workspace names
update.upsertedWorkspaces.forEach { menuState.upsertWorkspace($0) }
update.upsertedAgents.forEach { menuState.upsertAgent($0) }
}
}
extension CoderVPNService {
public func vpnDidUpdate(_ connection: NETunnelProviderSession) {
switch (tunnelState, connection.status) {
// Any -> Disconnected: Update UI w/ error if present
case (_, .disconnected):
connection.fetchLastDisconnectError { err in
self.tunnelState = if let err {
.failed(.internalError(err.localizedDescription))
} else {
.disabled
}
}
// Connecting -> Connecting: no-op
case (.connecting, .connecting):
break
// Connected -> Connected: no-op
case (.connected, .connected):
break
// Non-connecting -> Connecting: Establish XPC
case (_, .connecting):
xpc.connect()
xpc.ping()
tunnelState = .connecting
// Non-connected -> Connected: Retrieve Peers
case (_, .connected):
xpc.connect()
xpc.getPeerState()
tunnelState = .connected
// Any -> Reasserting
case (_, .reasserting):
tunnelState = .connecting
// Any -> Disconnecting
case (_, .disconnecting):
tunnelState = .disconnecting
// Any -> Invalid
case (_, .invalid):
tunnelState = .failed(.networkExtensionError(.unconfigured))
@unknown default:
tunnelState = .disabled
}
}
}