]> git.ipfire.org Git - thirdparty/wireguard-apple.git/blame - Sources/WireGuardApp/Tunnel/TunnelsManager.swift
UI: When reloading tunnels, preserve '.waiting' state
[thirdparty/wireguard-apple.git] / Sources / WireGuardApp / Tunnel / TunnelsManager.swift
CommitLineData
8ebfc6af 1// SPDX-License-Identifier: MIT
13b72044 2// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
b63abc65
RC
3
4import Foundation
c8fba951
RC
5import NetworkExtension
6import os.log
b63abc65 7
046d1413 8protocol TunnelsManagerListDelegate: class {
d06cff2a
EK
9 func tunnelAdded(at index: Int)
10 func tunnelModified(at index: Int)
11 func tunnelMoved(from oldIndex: Int, to newIndex: Int)
11e44f9a 12 func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
20b49518
RC
13}
14
046d1413 15protocol TunnelsManagerActivationDelegate: class {
bf58159d
RC
16 func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
17 func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
18 func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
19 func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
20}
21
b63abc65 22class TunnelsManager {
9d5b376d 23 private var tunnels: [TunnelContainer]
046d1413
RC
24 weak var tunnelsListDelegate: TunnelsManagerListDelegate?
25 weak var activationDelegate: TunnelsManagerActivationDelegate?
631286e2
AM
26 private var statusObservationToken: NotificationToken?
27 private var waiteeObservationToken: NSKeyValueObservation?
28 private var configurationsObservationToken: NotificationToken?
b63abc65 29
c8fba951 30 init(tunnelProviders: [NETunnelProviderManager]) {
b2b5e0e3 31 tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
7a24f18e 32 startObservingTunnelStatuses()
2aad8cf0 33 startObservingTunnelConfigurations()
b63abc65
RC
34 }
35
89a564ce 36 static func create(completionHandler: @escaping (Result<TunnelsManager, TunnelsManagerError>) -> Void) {
b3515c93 37 #if targetEnvironment(simulator)
2a22c0f2 38 completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
b3515c93 39 #else
e4ac48bc 40 NETunnelProviderManager.loadAllFromPreferences { managers, error in
c8fba951 41 if let error = error {
ba1d0c05 42 wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
2582ddd6 43 completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels(systemError: error)))
c8fba951
RC
44 return
45 }
7b9d4cb9 46
8c3557a9
JD
47 var tunnelManagers = managers ?? []
48 var refs: Set<Data> = []
f852b6f9 49 var tunnelNames: Set<String> = []
8c3557a9 50 for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
f852b6f9
RC
51 if let tunnelName = tunnelManager.localizedDescription {
52 tunnelNames.insert(tunnelName)
53 }
9690365d
RC
54 guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue }
55 if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") {
1fecd8eb 56 tunnelManager.saveToPreferences { _ in }
8553723e 57 }
9690365d
RC
58 #if os(iOS)
59 let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
60 #elseif os(macOS)
377f2f04
JD
61 let passwordRef: Data?
62 if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
63 passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
64 } else {
65 passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
66 }
9690365d
RC
67 #else
68 #error("Unimplemented")
69 #endif
70 if let ref = passwordRef {
8c3557a9
JD
71 refs.insert(ref)
72 } else {
d976d159 73 wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
8c3557a9
JD
74 tunnelManager.removeFromPreferences { _ in }
75 tunnelManagers.remove(at: index)
76 }
8553723e 77 }
8c3557a9 78 Keychain.deleteReferences(except: refs)
5100e597 79 #if os(iOS)
f852b6f9 80 RecentTunnelsTracker.cleanupTunnels(except: tunnelNames)
5100e597 81 #endif
8553723e 82 completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
c8fba951 83 }
b3515c93 84 #endif
b63abc65 85 }
d36e7e27 86
2aad8cf0
RC
87 func reload() {
88 NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
89 guard let self = self else { return }
d36e7e27 90
2aad8cf0
RC
91 let loadedTunnelProviders = managers ?? []
92
54f45cb3 93 for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
4c1b2e12 94 if !loadedTunnelProviders.contains(where: { $0.isEquivalentTo(currentTunnel) }) {
2aad8cf0 95 // Tunnel was deleted outside the app
54f45cb3 96 self.tunnels.remove(at: index)
11e44f9a 97 self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
2aad8cf0
RC
98 }
99 }
100 for loadedTunnelProvider in loadedTunnelProviders {
4c1b2e12 101 if let matchingTunnel = self.tunnels.first(where: { loadedTunnelProvider.isEquivalentTo($0) }) {
2aad8cf0 102 matchingTunnel.tunnelProvider = loadedTunnelProvider
704de3b2 103 matchingTunnel.refreshStatus()
2aad8cf0
RC
104 } else {
105 // Tunnel was added outside the app
6331b81b
RC
106 if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
107 if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
108 loadedTunnelProvider.saveToPreferences { _ in }
109 }
110 }
2aad8cf0
RC
111 let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
112 self.tunnels.append(tunnel)
b2b5e0e3 113 self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
2aad8cf0
RC
114 self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
115 }
9098cd11
EK
116 }
117 }
9098cd11 118 }
b63abc65 119
89a564ce 120 func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, completionHandler: @escaping (Result<TunnelContainer, TunnelsManagerError>) -> Void) {
4ed64697 121 let tunnelName = tunnelConfiguration.name ?? ""
007d6d9c 122 if tunnelName.isEmpty {
8d26a3c5 123 completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
007d6d9c
JD
124 return
125 }
955de09c 126
7a24f18e 127 if tunnels.contains(where: { $0.name == tunnelName }) {
8d26a3c5 128 completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
92d3de1b
RC
129 return
130 }
c8fba951 131
c8fba951 132 let tunnelProviderManager = NETunnelProviderManager()
868fee04 133 tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
c8fba951
RC
134 tunnelProviderManager.isEnabled = true
135
062b4d4b 136 onDemandOption.apply(on: tunnelProviderManager)
39a067cb 137
5ed28907
RC
138 let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
139
e4ac48bc 140 tunnelProviderManager.saveToPreferences { [weak self] error in
6d57c8b6
AM
141 if let error = error {
142 wg_log(.error, message: "Add: Saving configuration failed: \(error)")
8c3557a9 143 (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
6d57c8b6 144 completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error)))
c8fba951
RC
145 return
146 }
7b9d4cb9 147
05d75053 148 guard let self = self else { return }
7b9d4cb9 149
5ed28907
RC
150 #if os(iOS)
151 // HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
152 // This is an ugly hack to reactivate the tunnel that has been deactivated like that.
153 if let activeTunnel = activeTunnel {
154 if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
155 self.startActivation(of: activeTunnel)
156 }
157 if activeTunnel.status == .active || activeTunnel.status == .activating {
158 activeTunnel.status = .restarting
159 }
160 }
161 #endif
162
05d75053
EK
163 let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
164 self.tunnels.append(tunnel)
b2b5e0e3 165 self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
05d75053
EK
166 self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
167 completionHandler(.success(tunnel))
b63abc65 168 }
b63abc65
RC
169 }
170
3afcee04 171 func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
373bb2ae
AM
172 // Temporarily pause observation of changes to VPN configurations to prevent the feedback
173 // loop that causes `reload()` to be called on each newly added tunnel, which significantly
174 // impacts performance.
175 configurationsObservationToken = nil
176
177 self.addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil) { [weak self] numSucceeded, error in
178 completionHandler(numSucceeded, error)
179
180 // Restart observation of changes to VPN configrations.
181 self?.startObservingTunnelConfigurations()
182
183 // Force reload all configurations to make sure that all tunnels are up to date.
184 self?.reload()
185 }
439f8f5a
RC
186 }
187
3afcee04 188 private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
5845db45 189 guard let head = tunnelConfigurations.first else {
3afcee04 190 completionHandler(numberSuccessful, lastError)
95101dce
JD
191 return
192 }
5845db45 193 let tail = tunnelConfigurations.dropFirst()
7a24f18e 194 add(tunnelConfiguration: head) { [weak self, tail] result in
95101dce 195 DispatchQueue.main.async {
89a564ce
RC
196 var numberSuccessfulCount = numberSuccessful
197 var lastError: TunnelsManagerError?
198 switch result {
199 case .failure(let error):
200 lastError = error
201 case .success:
202 numberSuccessfulCount = numberSuccessful + 1
203 }
204 self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessfulCount, lastError: lastError, completionHandler: completionHandler)
439f8f5a
RC
205 }
206 }
207 }
208
062b4d4b 209 func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
4ed64697 210 let tunnelName = tunnelConfiguration.name ?? ""
007d6d9c 211 if tunnelName.isEmpty {
8d26a3c5 212 completionHandler(TunnelsManagerError.tunnelNameEmpty)
007d6d9c
JD
213 return
214 }
c8fba951 215
c8fba951 216 let tunnelProviderManager = tunnel.tunnelProvider
f852b6f9
RC
217 let oldName = tunnelProviderManager.localizedDescription ?? ""
218 let isNameChanged = tunnelName != oldName
d06cff2a 219 if isNameChanged {
ed9b4c85 220 guard !tunnels.contains(where: { $0.name == tunnelName }) else {
8d26a3c5 221 completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
92d3de1b
RC
222 return
223 }
1870a3d3
RC
224 tunnel.name = tunnelName
225 }
bba6d2f9 226
66392386
RC
227 var isTunnelConfigurationChanged = false
228 if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
229 tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
230 isTunnelConfigurationChanged = true
231 }
c8fba951 232 tunnelProviderManager.isEnabled = true
7b9d4cb9 233
062b4d4b
RC
234 let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
235 onDemandOption.apply(on: tunnelProviderManager)
39a067cb 236
e4ac48bc 237 tunnelProviderManager.saveToPreferences { [weak self] error in
6d57c8b6 238 if let error = error {
8c3557a9 239 //TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
6d57c8b6
AM
240 wg_log(.error, message: "Modify: Saving configuration failed: \(error)")
241 completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
c8fba951
RC
242 return
243 }
05d75053 244 guard let self = self else { return }
05d75053
EK
245 if isNameChanged {
246 let oldIndex = self.tunnels.firstIndex(of: tunnel)!
b2b5e0e3 247 self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
05d75053
EK
248 let newIndex = self.tunnels.firstIndex(of: tunnel)!
249 self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
f852b6f9
RC
250 #if os(iOS)
251 RecentTunnelsTracker.handleTunnelRenamed(oldName: oldName, newName: tunnelName)
252 #endif
05d75053
EK
253 }
254 self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
bba6d2f9 255
66392386
RC
256 if isTunnelConfigurationChanged {
257 if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
258 // Turn off the tunnel, and then turn it back on, so the changes are made effective
259 tunnel.status = .restarting
260 (tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
261 }
05d75053 262 }
bba6d2f9 263
05d75053
EK
264 if isActivatingOnDemand {
265 // Reload tunnel after saving.
266 // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
267 tunnelProviderManager.loadFromPreferences { error in
268 tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
6d57c8b6
AM
269 if let error = error {
270 wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error)")
271 completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
272 } else {
273 completionHandler(nil)
2676ee01 274 }
2676ee01 275 }
05d75053
EK
276 } else {
277 completionHandler(nil)
c8fba951
RC
278 }
279 }
b63abc65
RC
280 }
281
8d26a3c5 282 func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
c8fba951 283 let tunnelProviderManager = tunnel.tunnelProvider
377f2f04
JD
284 #if os(macOS)
285 if tunnel.isTunnelAvailableToUser {
adbe0b06
RC
286 (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
287 }
377f2f04
JD
288 #elseif os(iOS)
289 (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
290 #else
291 #error("Unimplemented")
292 #endif
e4ac48bc 293 tunnelProviderManager.removeFromPreferences { [weak self] error in
6d57c8b6
AM
294 if let error = error {
295 wg_log(.error, message: "Remove: Saving configuration failed: \(error)")
296 completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error))
c8fba951
RC
297 return
298 }
9bc17034 299 if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
de14b76b 300 self.tunnels.remove(at: index)
11e44f9a 301 self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
c8fba951 302 }
1568ae57 303 completionHandler(nil)
f852b6f9
RC
304
305 #if os(iOS)
306 RecentTunnelsTracker.handleTunnelRemoved(tunnelName: tunnel.name)
307 #endif
b63abc65 308 }
b63abc65
RC
309 }
310
9bc17034 311 func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
373bb2ae
AM
312 // Temporarily pause observation of changes to VPN configurations to prevent the feedback
313 // loop that causes `reload()` to be called for each removed tunnel, which significantly
314 // impacts performance.
315 configurationsObservationToken = nil
316
317 removeMultiple(tunnels: ArraySlice(tunnels)) { [weak self] error in
318 completionHandler(error)
319
320 // Restart observation of changes to VPN configrations.
321 self?.startObservingTunnelConfigurations()
322
323 // Force reload all configurations to make sure that all tunnels are up to date.
324 self?.reload()
325 }
9bc17034
RC
326 }
327
328 private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
329 guard let head = tunnels.first else {
330 completionHandler(nil)
331 return
332 }
333 let tail = tunnels.dropFirst()
334 remove(tunnel: head) { [weak self, tail] error in
335 DispatchQueue.main.async {
336 if let error = error {
337 completionHandler(error)
338 } else {
339 self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
340 }
341 }
342 }
343 }
344
df9934a4
RC
345 func setOnDemandEnabled(_ isOnDemandEnabled: Bool, on tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
346 let tunnelProviderManager = tunnel.tunnelProvider
347 guard tunnelProviderManager.isOnDemandEnabled != isOnDemandEnabled else {
348 completionHandler(nil)
349 return
350 }
351 let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && isOnDemandEnabled
352 tunnelProviderManager.isOnDemandEnabled = isOnDemandEnabled
353 tunnelProviderManager.saveToPreferences { error in
354 if let error = error {
355 wg_log(.error, message: "Modify On-Demand: Saving configuration failed: \(error)")
356 completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
357 return
358 }
359 if isActivatingOnDemand {
360 tunnelProviderManager.loadFromPreferences { error in
361 tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
362 if let error = error {
363 wg_log(.error, message: "Modify On-Demand: Re-loading after saving configuration failed: \(error)")
364 completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
365 } else {
366 completionHandler(nil)
367 }
368 }
369 } else {
370 completionHandler(nil)
371 }
372 }
373 }
374
b63abc65
RC
375 func numberOfTunnels() -> Int {
376 return tunnels.count
377 }
378
379 func tunnel(at index: Int) -> TunnelContainer {
380 return tunnels[index]
381 }
a2daf093 382
393718df
RC
383 func mapTunnels<T>(transform: (TunnelContainer) throws -> T) rethrows -> [T] {
384 return try tunnels.map(transform)
385 }
386
eabeb8ff
RC
387 func index(of tunnel: TunnelContainer) -> Int? {
388 return tunnels.firstIndex(of: tunnel)
389 }
390
105eca7a 391 func tunnel(named tunnelName: String) -> TunnelContainer? {
7a24f18e 392 return tunnels.first { $0.name == tunnelName }
105eca7a
RC
393 }
394
e29cf19f
RC
395 func waitingTunnel() -> TunnelContainer? {
396 return tunnels.first { $0.status == .waiting }
397 }
398
3c804902
RC
399 func tunnelInOperation() -> TunnelContainer? {
400 if let waitingTunnelObject = waitingTunnel() {
401 return waitingTunnelObject
402 }
403 return tunnels.first { $0.status != .inactive }
404 }
405
bf58159d 406 func startActivation(of tunnel: TunnelContainer) {
9946d8f9 407 guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
d06cff2a 408 guard tunnel.status == .inactive else {
7a24f18e 409 activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
a2daf093
RC
410 return
411 }
e1b25835 412
f9239dae
RC
413 if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
414 alreadyWaitingTunnel.status = .inactive
415 }
416
1fd0c56f 417 if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
f9239dae
RC
418 wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
419 tunnel.status = .waiting
4e516d67 420 activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
f9239dae 421 if tunnelInOperation.status != .deactivating {
a261d84f
RC
422 if tunnelInOperation.isActivateOnDemandEnabled {
423 setOnDemandEnabled(false, on: tunnelInOperation) { [weak self] error in
424 guard error == nil else {
425 wg_log(.error, message: "Unable to activate tunnel '\(tunnel.name)' because on-demand could not be disabled on active tunnel '\(tunnel.name)'")
426 return
427 }
428 self?.startDeactivation(of: tunnelInOperation)
429 }
430 } else {
431 startDeactivation(of: tunnelInOperation)
432 }
f9239dae 433 }
1fd0c56f 434 return
e1b25835
RC
435 }
436
2a22c0f2
RC
437 #if targetEnvironment(simulator)
438 tunnel.status = .active
439 #else
7a24f18e 440 tunnel.startActivation(activationDelegate: activationDelegate)
2a22c0f2 441 #endif
f852b6f9
RC
442
443 #if os(iOS)
444 RecentTunnelsTracker.handleTunnelActivated(tunnelName: tunnel.name)
445 #endif
a2daf093
RC
446 }
447
8e7bfb15 448 func startDeactivation(of tunnel: TunnelContainer) {
f9239dae 449 tunnel.isAttemptingActivation = false
05d75053 450 guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
2a22c0f2
RC
451 #if targetEnvironment(simulator)
452 tunnel.status = .inactive
453 #else
a3e912a2 454 tunnel.startDeactivation()
2a22c0f2 455 #endif
a2daf093 456 }
59b9a6e5 457
923d039a 458 func refreshStatuses() {
de14b76b 459 tunnels.forEach { $0.refreshStatus() }
59b9a6e5 460 }
0dcb285b 461
4e516d67
RC
462 private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
463 waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
464 guard let self = self else { return }
465 if tunnel.status == .inactive {
466 if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
467 waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
468 }
469 self.waiteeObservationToken = nil
470 }
471 }
472 }
473
0dcb285b 474 private func startObservingTunnelStatuses() {
631286e2 475 statusObservationToken = NotificationCenter.default.observe(name: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
05d75053
EK
476 guard let self = self,
477 let session = statusChangeNotification.object as? NETunnelProviderSession,
478 let tunnelProvider = session.manager as? NETunnelProviderManager,
7720307f 479 let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
5971c197 480
ba1d0c05 481 wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
5971c197 482
bf58159d
RC
483 if tunnel.isAttemptingActivation {
484 if session.status == .connected {
485 tunnel.isAttemptingActivation = false
486 self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
487 } else if session.status == .disconnected {
488 tunnel.isAttemptingActivation = false
9098cd11 489 if let (title, message) = lastErrorTextFromNetworkExtension(for: tunnel) {
ec031b1f 490 self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
48552d26 491 } else {
ec031b1f 492 self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
48552d26 493 }
9946d8f9 494 }
8a916beb 495 }
5971c197 496
ecd66def
RC
497 if tunnel.status == .restarting && session.status == .disconnected {
498 tunnel.startActivation(activationDelegate: self.activationDelegate)
8a916beb
EK
499 return
500 }
5971c197 501
8a916beb 502 tunnel.refreshStatus()
0dcb285b
RC
503 }
504 }
505
2aad8cf0 506 func startObservingTunnelConfigurations() {
631286e2 507 configurationsObservationToken = NotificationCenter.default.observe(name: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
a796c6c4
RC
508 DispatchQueue.main.async { [weak self] in
509 // We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
510 // (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
511 // removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
512 // as a result of a tunnel getting added or removed outside of the app.
513 self?.reload()
514 }
2aad8cf0
RC
515 }
516 }
517
5a044e41
AM
518 static func tunnelNameIsLessThan(_ lhs: String, _ rhs: String) -> Bool {
519 return lhs.compare(rhs, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
b2b5e0e3 520 }
9098cd11 521}
48552d26 522
9098cd11
EK
523private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
524 guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
525 guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
526 guard let lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
527 guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
d36e7e27 528
e6e1795d
RC
529 if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
530 return extensionError.alertText
9946d8f9 531 }
e6e1795d
RC
532
533 return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
b63abc65 534}
c8fba951 535
a2daf093
RC
536class TunnelContainer: NSObject {
537 @objc dynamic var name: String
538 @objc dynamic var status: TunnelStatus
c60c29b9 539
0dcb285b 540 @objc dynamic var isActivateOnDemandEnabled: Bool
40f18de4 541 @objc dynamic var hasOnDemandRules: Bool
a2daf093 542
c4263da2
RC
543 var isAttemptingActivation = false {
544 didSet {
545 if isAttemptingActivation {
4e516d67 546 self.activationTimer?.invalidate()
c4263da2
RC
547 let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
548 guard let self = self else { return }
4e516d67
RC
549 wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
550 switch self.tunnelProvider.connection.status {
551 case .connected, .disconnected, .invalid:
552 self.activationTimer?.invalidate()
553 self.activationTimer = nil
554 default:
555 break
c4263da2 556 }
4e516d67 557 self.refreshStatus()
c4263da2
RC
558 }
559 self.activationTimer = activationTimer
3b295785 560 RunLoop.main.add(activationTimer, forMode: .common)
c4263da2
RC
561 }
562 }
563 }
48552d26 564 var activationAttemptId: String?
c4263da2 565 var activationTimer: Timer?
6ad3487a 566 var deactivationTimer: Timer?
e8d68396 567
40f18de4
RC
568 fileprivate var tunnelProvider: NETunnelProviderManager {
569 didSet {
570 isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
571 hasOnDemandRules = !(tunnelProvider.onDemandRules ?? []).isEmpty
572 }
573 }
a2daf093 574
8553723e 575 var tunnelConfiguration: TunnelConfiguration? {
2aad8cf0 576 return tunnelProvider.tunnelConfiguration
8553723e 577 }
7b9d4cb9 578
062b4d4b
RC
579 var onDemandOption: ActivateOnDemandOption {
580 return ActivateOnDemandOption(from: tunnelProvider)
8553723e 581 }
7b9d4cb9 582
377f2f04
JD
583 #if os(macOS)
584 var isTunnelAvailableToUser: Bool {
585 return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
586 }
587 #endif
588
007d6d9c 589 init(tunnel: NETunnelProviderManager) {
7a24f18e 590 name = tunnel.localizedDescription ?? "Unnamed"
a2daf093
RC
591 let status = TunnelStatus(from: tunnel.connection.status)
592 self.status = status
7a24f18e 593 isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
40f18de4 594 hasOnDemandRules = !(tunnel.onDemandRules ?? []).isEmpty
7a24f18e 595 tunnelProvider = tunnel
a2daf093 596 super.init()
a2daf093
RC
597 }
598
22625e8c
JD
599 func getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
600 guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
601 completionHandler(tunnelConfiguration)
602 return
603 }
cef39578 604 guard nil != (try? session.sendProviderMessage(Data([ UInt8(0) ]), responseHandler: {
22625e8c
JD
605 guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
606 completionHandler(self.tunnelConfiguration)
607 return
608 }
609 completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
610 })) else {
611 completionHandler(tunnelConfiguration)
612 return
613 }
614 }
615
923d039a 616 func refreshStatus() {
03ef79c0 617 if (status == .restarting) || (status == .waiting && tunnelProvider.connection.status == .disconnected) {
ecd66def
RC
618 return
619 }
6ad3487a 620 status = TunnelStatus(from: tunnelProvider.connection.status)
59b9a6e5
RC
621 }
622
501e412b 623 fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
d06cff2a 624 if recursionCount >= 8 {
ba1d0c05 625 wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
2582ddd6 626 activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors(lastSystemError: lastError!))
ecb6035d
JD
627 return
628 }
629
7a24f18e 630 wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
b2ab6b91 631
7a24f18e 632 status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
3bddab8a 633
d06cff2a 634 guard tunnelProvider.isEnabled else {
b2ab6b91
RC
635 // In case the tunnel had gotten disabled, re-enable and save it,
636 // then call this function again.
ba1d0c05 637 wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
b2ab6b91 638 tunnelProvider.isEnabled = true
e4ac48bc 639 tunnelProvider.saveToPreferences { [weak self] error in
bf58159d 640 guard let self = self else { return }
d06cff2a 641 if error != nil {
ba1d0c05 642 wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
2582ddd6 643 activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving(systemError: error!))
b2ab6b91
RC
644 return
645 }
f2000aa1 646 wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, invoking startActivation")
ed9b4c85 647 self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
b2ab6b91
RC
648 }
649 return
650 }
651
fd241fac 652 // Start the tunnel
b2ab6b91 653 do {
ba1d0c05 654 wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
7a24f18e 655 isAttemptingActivation = true
48552d26
RC
656 let activationAttemptId = UUID().uuidString
657 self.activationAttemptId = activationAttemptId
658 try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
ba1d0c05 659 wg_log(.debug, staticMessage: "startActivation: Success")
bf58159d 660 activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
d06cff2a 661 } catch let error {
7a24f18e 662 isAttemptingActivation = false
5ae9eec5 663 guard let systemError = error as? NEVPNError else {
ba1d0c05 664 wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
1568ae57 665 status = .inactive
2582ddd6 666 activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: error))
b2ab6b91
RC
667 return
668 }
5ae9eec5 669 guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
ba1d0c05 670 wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
f6a5dfea 671 status = .inactive
2582ddd6 672 activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: systemError))
f6a5dfea 673 return
b2ab6b91 674 }
ba1d0c05 675 wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
e4ac48bc 676 tunnelProvider.loadFromPreferences { [weak self] error in
bf58159d 677 guard let self = self else { return }
d06cff2a 678 if error != nil {
ba1d0c05 679 wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
bf58159d 680 self.status = .inactive
2582ddd6 681 activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading(systemError: systemError))
b2ab6b91
RC
682 return
683 }
f2000aa1 684 wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
501e412b 685 self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
b7aaae75 686 }
a2daf093
RC
687 }
688 }
689
a3e912a2 690 fileprivate func startDeactivation() {
1f3ec042 691 wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
e4ac48bc 692 (tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
a2daf093 693 }
a2daf093 694}
2aad8cf0
RC
695
696extension NETunnelProviderManager {
9d5b376d 697 private static var cachedConfigKey: UInt8 = 0
3bd611aa 698
2aad8cf0 699 var tunnelConfiguration: TunnelConfiguration? {
a26d620f
JD
700 if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
701 return cached
702 }
703 let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
704 if config != nil {
705 objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
706 }
707 return config
2aad8cf0 708 }
0e255654 709
868fee04
RC
710 func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
711 protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
712 localizedDescription = tunnelConfiguration.name
713 objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
714 }
4c1b2e12
RC
715
716 func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
377f2f04 717 return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
4c1b2e12 718 }
2aad8cf0 719}