]>
Commit | Line | Data |
---|---|---|
8ebfc6af | 1 | // SPDX-License-Identifier: MIT |
13b72044 | 2 | // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. |
b63abc65 RC |
3 | |
4 | import Foundation | |
c8fba951 RC |
5 | import NetworkExtension |
6 | import os.log | |
b63abc65 | 7 | |
046d1413 | 8 | protocol 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 | 15 | protocol 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 | 22 | class 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 |
523 | private 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 |
536 | class 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 | |
696 | extension 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 | } |