aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardApp/Tunnel
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-12-02 12:27:39 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-12-03 13:32:24 +0100
commitec574085703ea1c8b2d4538596961beb910c4382 (patch)
tree73cf8bbdb74fe5575606664bccd0232ffa911803 /Sources/WireGuardApp/Tunnel
parentWireGuardKit: Assert that resolutionResults must not contain failures (diff)
downloadwireguard-apple-ec574085703ea1c8b2d4538596961beb910c4382.tar.xz
wireguard-apple-ec574085703ea1c8b2d4538596961beb910c4382.zip
Move all source files to `Sources/` and rename WireGuardKit targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Diffstat (limited to 'Sources/WireGuardApp/Tunnel')
-rw-r--r--Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift133
-rw-r--r--Sources/WireGuardApp/Tunnel/MockTunnels.swift49
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift185
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelErrors.swift107
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelStatus.swift63
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelsManager.swift807
6 files changed, 1344 insertions, 0 deletions
diff --git a/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift b/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift
new file mode 100644
index 0000000..d44e1d6
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+
+enum ActivateOnDemandOption: Equatable {
+ case off
+ case wiFiInterfaceOnly(ActivateOnDemandSSIDOption)
+ case nonWiFiInterfaceOnly
+ case anyInterface(ActivateOnDemandSSIDOption)
+}
+
+#if os(iOS)
+private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .cellular
+#elseif os(macOS)
+private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
+#else
+#error("Unimplemented")
+#endif
+
+enum ActivateOnDemandSSIDOption: Equatable {
+ case anySSID
+ case onlySpecificSSIDs([String])
+ case exceptSpecificSSIDs([String])
+}
+
+extension ActivateOnDemandOption {
+ func apply(on tunnelProviderManager: NETunnelProviderManager) {
+ let rules: [NEOnDemandRule]?
+ switch self {
+ case .off:
+ rules = nil
+ case .wiFiInterfaceOnly(let ssidOption):
+ rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleDisconnect(interfaceType: nonWiFiInterfaceType)]
+ case .nonWiFiInterfaceOnly:
+ rules = [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType), NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
+ case .anyInterface(let ssidOption):
+ if case .anySSID = ssidOption {
+ rules = [NEOnDemandRuleConnect(interfaceType: .any)]
+ } else {
+ rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType)]
+ }
+ }
+ tunnelProviderManager.onDemandRules = rules
+ tunnelProviderManager.isOnDemandEnabled = self != .off
+ }
+
+ init(from tunnelProviderManager: NETunnelProviderManager) {
+ if tunnelProviderManager.isOnDemandEnabled, let onDemandRules = tunnelProviderManager.onDemandRules {
+ self = ActivateOnDemandOption.create(from: onDemandRules)
+ } else {
+ self = .off
+ }
+ }
+
+ private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
+ switch rules.count {
+ case 0:
+ return .off
+ case 1:
+ let rule = rules[0]
+ guard rule.action == .connect else { return .off }
+ return .anyInterface(.anySSID)
+ case 2:
+ guard let connectRule = rules.first(where: { $0.action == .connect }) else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
+ return .off
+ }
+ guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
+ return .off
+ }
+ if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
+ return .wiFiInterfaceOnly(.anySSID)
+ } else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
+ return .nonWiFiInterfaceOnly
+ } else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
+ return .off
+ }
+ case 3:
+ guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
+ guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
+ let ssids = ssidRule.ssidMatch!
+ switch (ssidRule.action, nonWiFiRule.action) {
+ case (.connect, .connect):
+ return .anyInterface(.onlySpecificSSIDs(ssids))
+ case (.connect, .disconnect):
+ return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
+ case (.disconnect, .connect):
+ return .anyInterface(.exceptSpecificSSIDs(ssids))
+ case (.disconnect, .disconnect):
+ return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
+ default:
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
+ return .off
+ }
+ default:
+ wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
+ return .off
+ }
+ }
+}
+
+private extension NEOnDemandRuleConnect {
+ convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
+ self.init()
+ interfaceTypeMatch = interfaceType
+ ssidMatch = ssids
+ }
+}
+
+private extension NEOnDemandRuleDisconnect {
+ convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
+ self.init()
+ interfaceTypeMatch = interfaceType
+ ssidMatch = ssids
+ }
+}
+
+private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDemandRule] {
+ switch option {
+ case .anySSID:
+ return [NEOnDemandRuleConnect(interfaceType: .wiFi)]
+ case .onlySpecificSSIDs(let ssids):
+ assert(!ssids.isEmpty)
+ return [NEOnDemandRuleConnect(interfaceType: .wiFi, ssids: ssids),
+ NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
+ case .exceptSpecificSSIDs(let ssids):
+ return [NEOnDemandRuleDisconnect(interfaceType: .wiFi, ssids: ssids),
+ NEOnDemandRuleConnect(interfaceType: .wiFi)]
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/MockTunnels.swift b/Sources/WireGuardApp/Tunnel/MockTunnels.swift
new file mode 100644
index 0000000..1ffa99c
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/MockTunnels.swift
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+import WireGuardKit
+
+// Creates mock tunnels for the iOS Simulator.
+
+#if targetEnvironment(simulator)
+class MockTunnels {
+ static let tunnelNames = [
+ "demo",
+ "edgesecurity",
+ "home",
+ "office",
+ "infra-fr",
+ "infra-us",
+ "krantz",
+ "metheny",
+ "frisell"
+ ]
+ static let address = "192.168.%d.%d/32"
+ static let dnsServers = ["8.8.8.8", "8.8.4.4"]
+ static let endpoint = "demo.wireguard.com:51820"
+ static let allowedIPs = "0.0.0.0/0"
+
+ static func createMockTunnels() -> [NETunnelProviderManager] {
+ return tunnelNames.map { tunnelName -> NETunnelProviderManager in
+
+ var interface = InterfaceConfiguration(privateKey: PrivateKey())
+ interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
+ interface.dns = dnsServers.map { DNSServer(from: $0)! }
+
+ var peer = PeerConfiguration(publicKey: PrivateKey().publicKey)
+ peer.endpoint = Endpoint(from: endpoint)
+ peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
+
+ let tunnelConfiguration = TunnelConfiguration(name: tunnelName, interface: interface, peers: [peer])
+
+ let tunnelProviderManager = NETunnelProviderManager()
+ tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
+ tunnelProviderManager.localizedDescription = tunnelConfiguration.name
+ tunnelProviderManager.isEnabled = true
+
+ return tunnelProviderManager
+ }
+ }
+}
+#endif
diff --git a/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift b/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift
new file mode 100644
index 0000000..fcd6a31
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+extension TunnelConfiguration {
+ convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
+ var interfaceConfiguration: InterfaceConfiguration?
+ var peerConfigurations = [PeerConfiguration]()
+
+ var lines = uapiConfig.split(separator: "\n")
+ lines.append("")
+
+ var parserState = ParserState.inInterfaceSection
+ var attributes = [String: String]()
+
+ for line in lines {
+ var key = ""
+ var value = ""
+
+ if !line.isEmpty {
+ guard let equalsIndex = line.firstIndex(of: "=") else { throw ParseError.invalidLine(line) }
+ key = String(line[..<equalsIndex])
+ value = String(line[line.index(equalsIndex, offsetBy: 1)...])
+ }
+
+ if line.isEmpty || key == "public_key" {
+ // Previous section has ended; process the attributes collected so far
+ if parserState == .inInterfaceSection {
+ let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
+ guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
+ interfaceConfiguration = interface
+ parserState = .inPeerSection
+ } else if parserState == .inPeerSection {
+ let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
+ peerConfigurations.append(peer)
+ }
+ attributes.removeAll()
+ if line.isEmpty {
+ break
+ }
+ }
+
+ if let presentValue = attributes[key] {
+ if key == "allowed_ip" {
+ attributes[key] = presentValue + "," + value
+ } else {
+ throw ParseError.multipleEntriesForKey(key)
+ }
+ } else {
+ attributes[key] = value
+ }
+
+ let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
+ let peerSectionKeys: Set<String> = ["public_key", "preshared_key", "allowed_ip", "endpoint", "persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec", "rx_bytes", "tx_bytes", "protocol_version"]
+
+ if parserState == .inInterfaceSection {
+ guard interfaceSectionKeys.contains(key) else {
+ throw ParseError.interfaceHasUnrecognizedKey(key)
+ }
+ }
+ if parserState == .inPeerSection {
+ guard peerSectionKeys.contains(key) else {
+ throw ParseError.peerHasUnrecognizedKey(key)
+ }
+ }
+ }
+
+ let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
+ let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
+ if peerPublicKeysArray.count != peerPublicKeysSet.count {
+ throw ParseError.multiplePeersWithSamePublicKey
+ }
+
+ interfaceConfiguration?.addresses = base?.interface.addresses ?? []
+ interfaceConfiguration?.dns = base?.interface.dns ?? []
+ interfaceConfiguration?.mtu = base?.interface.mtu
+
+ if let interfaceConfiguration = interfaceConfiguration {
+ self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
+ } else {
+ throw ParseError.noInterface
+ }
+ }
+
+ private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
+ guard let privateKeyString = attributes["private_key"] else {
+ throw ParseError.interfaceHasNoPrivateKey
+ }
+ guard let privateKey = PrivateKey(hexKey: privateKeyString) else {
+ throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
+ }
+ var interface = InterfaceConfiguration(privateKey: privateKey)
+ if let listenPortString = attributes["listen_port"] {
+ guard let listenPort = UInt16(listenPortString) else {
+ throw ParseError.interfaceHasInvalidListenPort(listenPortString)
+ }
+ if listenPort != 0 {
+ interface.listenPort = listenPort
+ }
+ }
+ return interface
+ }
+
+ private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
+ guard let publicKeyString = attributes["public_key"] else {
+ throw ParseError.peerHasNoPublicKey
+ }
+ guard let publicKey = PublicKey(hexKey: publicKeyString) else {
+ throw ParseError.peerHasInvalidPublicKey(publicKeyString)
+ }
+ var peer = PeerConfiguration(publicKey: publicKey)
+ if let preSharedKeyString = attributes["preshared_key"] {
+ guard let preSharedKey = PreSharedKey(hexKey: preSharedKeyString) else {
+ throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
+ }
+ // TODO(zx2c4): does the compiler optimize this away?
+ var accumulator: UInt8 = 0
+ for index in 0..<preSharedKey.rawValue.count {
+ accumulator |= preSharedKey.rawValue[index]
+ }
+ if accumulator != 0 {
+ peer.preSharedKey = preSharedKey
+ }
+ }
+ if let allowedIPsString = attributes["allowed_ip"] {
+ var allowedIPs = [IPAddressRange]()
+ for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
+ guard let allowedIP = IPAddressRange(from: allowedIPString) else {
+ throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
+ }
+ allowedIPs.append(allowedIP)
+ }
+ peer.allowedIPs = allowedIPs
+ }
+ if let endpointString = attributes["endpoint"] {
+ guard let endpoint = Endpoint(from: endpointString) else {
+ throw ParseError.peerHasInvalidEndpoint(endpointString)
+ }
+ peer.endpoint = endpoint
+ }
+ if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
+ guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
+ throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
+ }
+ if persistentKeepAlive != 0 {
+ peer.persistentKeepAlive = persistentKeepAlive
+ }
+ }
+ if let rxBytesString = attributes["rx_bytes"] {
+ guard let rxBytes = UInt64(rxBytesString) else {
+ throw ParseError.peerHasInvalidTransferBytes(rxBytesString)
+ }
+ if rxBytes != 0 {
+ peer.rxBytes = rxBytes
+ }
+ }
+ if let txBytesString = attributes["tx_bytes"] {
+ guard let txBytes = UInt64(txBytesString) else {
+ throw ParseError.peerHasInvalidTransferBytes(txBytesString)
+ }
+ if txBytes != 0 {
+ peer.txBytes = txBytes
+ }
+ }
+ if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
+ var lastHandshakeTimeSince1970: TimeInterval = 0
+ guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
+ throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
+ }
+ if lastHandshakeTimeSec != 0 {
+ lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
+ if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
+ guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
+ throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
+ }
+ lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
+ }
+ peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
+ }
+ }
+ return peer
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelErrors.swift b/Sources/WireGuardApp/Tunnel/TunnelErrors.swift
new file mode 100644
index 0000000..941ab61
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelErrors.swift
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+
+enum TunnelsManagerError: WireGuardAppError {
+ case tunnelNameEmpty
+ case tunnelAlreadyExistsWithThatName
+ case systemErrorOnListingTunnels(systemError: Error)
+ case systemErrorOnAddTunnel(systemError: Error)
+ case systemErrorOnModifyTunnel(systemError: Error)
+ case systemErrorOnRemoveTunnel(systemError: Error)
+
+ var alertText: AlertText {
+ switch self {
+ case .tunnelNameEmpty:
+ return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
+ case .tunnelAlreadyExistsWithThatName:
+ return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
+ case .systemErrorOnListingTunnels(let systemError):
+ return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
+ case .systemErrorOnAddTunnel(let systemError):
+ return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
+ case .systemErrorOnModifyTunnel(let systemError):
+ return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
+ case .systemErrorOnRemoveTunnel(let systemError):
+ return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
+ }
+ }
+}
+
+enum TunnelsManagerActivationAttemptError: WireGuardAppError {
+ case tunnelIsNotInactive
+ case failedWhileStarting(systemError: Error) // startTunnel() throwed
+ case failedWhileSaving(systemError: Error) // save config after re-enabling throwed
+ case failedWhileLoading(systemError: Error) // reloading config throwed
+ case failedBecauseOfTooManyErrors(lastSystemError: Error) // recursion limit reached
+
+ var alertText: AlertText {
+ switch self {
+ case .tunnelIsNotInactive:
+ return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
+ case .failedWhileStarting(let systemError),
+ .failedWhileSaving(let systemError),
+ .failedWhileLoading(let systemError),
+ .failedBecauseOfTooManyErrors(let systemError):
+ return (tr("alertTunnelActivationSystemErrorTitle"),
+ tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
+ }
+ }
+}
+
+enum TunnelsManagerActivationError: WireGuardAppError {
+ case activationFailed(wasOnDemandEnabled: Bool)
+ case activationFailedWithExtensionError(title: String, message: String, wasOnDemandEnabled: Bool)
+
+ var alertText: AlertText {
+ switch self {
+ case .activationFailed(let wasOnDemandEnabled):
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
+ case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
+ return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
+ }
+ }
+}
+
+extension PacketTunnelProviderError: WireGuardAppError {
+ var alertText: AlertText {
+ switch self {
+ case .savedProtocolConfigurationIsInvalid:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSavedConfigFailureMessage"))
+ case .dnsResolutionFailure:
+ return (tr("alertTunnelDNSFailureTitle"), tr("alertTunnelDNSFailureMessage"))
+ case .couldNotStartBackend:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationBackendFailureMessage"))
+ case .couldNotDetermineFileDescriptor:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFileDescriptorFailureMessage"))
+ case .couldNotSetNetworkSettings:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSetNetworkSettingsMessage"))
+ }
+ }
+}
+
+extension Error {
+ var localizedUIString: String {
+ if let systemError = self as? NEVPNError {
+ switch systemError {
+ case NEVPNError.configurationInvalid:
+ return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
+ case NEVPNError.configurationDisabled:
+ return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
+ case NEVPNError.connectionFailed:
+ return tr("alertSystemErrorMessageTunnelConnectionFailed")
+ case NEVPNError.configurationStale:
+ return tr("alertSystemErrorMessageTunnelConfigurationStale")
+ case NEVPNError.configurationReadWriteFailed:
+ return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
+ case NEVPNError.configurationUnknown:
+ return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
+ default:
+ return ""
+ }
+ } else {
+ return localizedDescription
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelStatus.swift b/Sources/WireGuardApp/Tunnel/TunnelStatus.swift
new file mode 100644
index 0000000..547aa9f
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelStatus.swift
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import NetworkExtension
+
+@objc enum TunnelStatus: Int {
+ case inactive
+ case activating
+ case active
+ case deactivating
+ case reasserting // Not a possible state at present
+ case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
+ case waiting // Waiting for another tunnel to be brought down
+
+ init(from systemStatus: NEVPNStatus) {
+ switch systemStatus {
+ case .connected:
+ self = .active
+ case .connecting:
+ self = .activating
+ case .disconnected:
+ self = .inactive
+ case .disconnecting:
+ self = .deactivating
+ case .reasserting:
+ self = .reasserting
+ case .invalid:
+ self = .inactive
+ @unknown default:
+ fatalError()
+ }
+ }
+}
+
+extension TunnelStatus: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+ case .inactive: return "inactive"
+ case .activating: return "activating"
+ case .active: return "active"
+ case .deactivating: return "deactivating"
+ case .reasserting: return "reasserting"
+ case .restarting: return "restarting"
+ case .waiting: return "waiting"
+ }
+ }
+}
+
+extension NEVPNStatus: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+ case .connected: return "connected"
+ case .connecting: return "connecting"
+ case .disconnected: return "disconnected"
+ case .disconnecting: return "disconnecting"
+ case .reasserting: return "reasserting"
+ case .invalid: return "invalid"
+ @unknown default:
+ fatalError()
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelsManager.swift b/Sources/WireGuardApp/Tunnel/TunnelsManager.swift
new file mode 100644
index 0000000..af6a9bb
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelsManager.swift
@@ -0,0 +1,807 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import NetworkExtension
+import os.log
+import WireGuardKit
+
+protocol TunnelsManagerListDelegate: class {
+ func tunnelAdded(at index: Int)
+ func tunnelModified(at index: Int)
+ func tunnelMoved(from oldIndex: Int, to newIndex: Int)
+ func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
+}
+
+protocol TunnelsManagerActivationDelegate: class {
+ func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
+ func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
+ func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
+ func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
+}
+
+class TunnelsManager {
+ fileprivate var tunnels: [TunnelContainer]
+ weak var tunnelsListDelegate: TunnelsManagerListDelegate?
+ weak var activationDelegate: TunnelsManagerActivationDelegate?
+ private var statusObservationToken: AnyObject?
+ private var waiteeObservationToken: AnyObject?
+ private var configurationsObservationToken: AnyObject?
+ private var catalinaWorkaround: Any?
+
+ init(tunnelProviders: [NETunnelProviderManager]) {
+ tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ startObservingTunnelStatuses()
+ startObservingTunnelConfigurations()
+ #if os(macOS)
+ if #available(macOS 10.15, *) {
+ self.catalinaWorkaround = CatalinaWorkaround(tunnelsManager: self)
+ }
+ #endif
+ }
+
+ static func create(completionHandler: @escaping (Result<TunnelsManager, TunnelsManagerError>) -> Void) {
+ #if targetEnvironment(simulator)
+ completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
+ #else
+ NETunnelProviderManager.loadAllFromPreferences { managers, error in
+ if let error = error {
+ wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
+ completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels(systemError: error)))
+ return
+ }
+
+ var tunnelManagers = managers ?? []
+ var refs: Set<Data> = []
+ var tunnelNames: Set<String> = []
+ for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
+ if let tunnelName = tunnelManager.localizedDescription {
+ tunnelNames.insert(tunnelName)
+ }
+ guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue }
+ if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") {
+ tunnelManager.saveToPreferences { _ in }
+ }
+ #if os(iOS)
+ let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
+ #elseif os(macOS)
+ let passwordRef: Data?
+ if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
+ passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
+ } else {
+ passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
+ }
+ #else
+ #error("Unimplemented")
+ #endif
+ if let ref = passwordRef {
+ refs.insert(ref)
+ } else {
+ wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
+ tunnelManager.removeFromPreferences { _ in }
+ tunnelManagers.remove(at: index)
+ }
+ }
+ #if os(macOS)
+ if #available(macOS 10.15, *) {
+ // Don't delete orphaned keychain refs. We need them to restore tunnels as a workaround.
+ } else {
+ Keychain.deleteReferences(except: refs)
+ }
+ #else
+ Keychain.deleteReferences(except: refs)
+ #endif
+ #if os(iOS)
+ RecentTunnelsTracker.cleanupTunnels(except: tunnelNames)
+ #endif
+ completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
+ }
+ #endif
+ }
+
+ func reload() {
+ NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
+ guard let self = self else { return }
+
+ let loadedTunnelProviders = managers ?? []
+
+ for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
+ if !loadedTunnelProviders.contains(where: { $0.isEquivalentTo(currentTunnel) }) {
+ // Tunnel was deleted outside the app
+ self.tunnels.remove(at: index)
+ self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
+ }
+ }
+ for loadedTunnelProvider in loadedTunnelProviders {
+ if let matchingTunnel = self.tunnels.first(where: { loadedTunnelProvider.isEquivalentTo($0) }) {
+ matchingTunnel.tunnelProvider = loadedTunnelProvider
+ matchingTunnel.refreshStatus()
+ } else {
+ // Tunnel was added outside the app
+ if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
+ if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
+ loadedTunnelProvider.saveToPreferences { _ in }
+ }
+ }
+ let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ }
+ }
+ }
+ }
+
+ func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, completionHandler: @escaping (Result<TunnelContainer, TunnelsManagerError>) -> Void) {
+ let tunnelName = tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
+ return
+ }
+
+ if tunnels.contains(where: { $0.name == tunnelName }) {
+ completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
+ return
+ }
+
+ let tunnelProviderManager = NETunnelProviderManager()
+ tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
+ tunnelProviderManager.isEnabled = true
+
+ onDemandOption.apply(on: tunnelProviderManager)
+
+ let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
+
+ tunnelProviderManager.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
+ return
+ }
+
+ guard let self = self else { return }
+
+ #if os(iOS)
+ // HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
+ // This is an ugly hack to reactivate the tunnel that has been deactivated like that.
+ if let activeTunnel = activeTunnel {
+ if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
+ self.startActivation(of: activeTunnel)
+ }
+ if activeTunnel.status == .active || activeTunnel.status == .activating {
+ activeTunnel.status = .restarting
+ }
+ }
+ #endif
+
+ let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ completionHandler(.success(tunnel))
+ }
+ }
+
+ func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
+ addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
+ }
+
+ private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
+ guard let head = tunnelConfigurations.first else {
+ completionHandler(numberSuccessful, lastError)
+ return
+ }
+ let tail = tunnelConfigurations.dropFirst()
+ add(tunnelConfiguration: head) { [weak self, tail] result in
+ DispatchQueue.main.async {
+ var numberSuccessfulCount = numberSuccessful
+ var lastError: TunnelsManagerError?
+ switch result {
+ case .failure(let error):
+ lastError = error
+ case .success:
+ numberSuccessfulCount = numberSuccessful + 1
+ }
+ self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessfulCount, lastError: lastError, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ let tunnelName = tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(TunnelsManagerError.tunnelNameEmpty)
+ return
+ }
+
+ let tunnelProviderManager = tunnel.tunnelProvider
+ let oldName = tunnelProviderManager.localizedDescription ?? ""
+ let isNameChanged = tunnelName != oldName
+ if isNameChanged {
+ guard !tunnels.contains(where: { $0.name == tunnelName }) else {
+ completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
+ return
+ }
+ tunnel.name = tunnelName
+ }
+
+ var isTunnelConfigurationChanged = false
+ if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
+ tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
+ isTunnelConfigurationChanged = true
+ }
+ tunnelProviderManager.isEnabled = true
+
+ let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
+ onDemandOption.apply(on: tunnelProviderManager)
+
+ tunnelProviderManager.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ //TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
+ wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
+ return
+ }
+ guard let self = self else { return }
+ if isNameChanged {
+ let oldIndex = self.tunnels.firstIndex(of: tunnel)!
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ let newIndex = self.tunnels.firstIndex(of: tunnel)!
+ self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelRenamed(oldName: oldName, newName: tunnelName)
+ #endif
+ }
+ self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
+
+ if isTunnelConfigurationChanged {
+ if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
+ // Turn off the tunnel, and then turn it back on, so the changes are made effective
+ tunnel.status = .restarting
+ (tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
+ }
+ }
+
+ if isActivatingOnDemand {
+ // Reload tunnel after saving.
+ // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
+ tunnelProviderManager.loadFromPreferences { error in
+ tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
+ guard error == nil else {
+ wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
+ return
+ }
+ completionHandler(nil)
+ }
+ } else {
+ completionHandler(nil)
+ }
+ }
+ }
+
+ func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ let tunnelProviderManager = tunnel.tunnelProvider
+ #if os(macOS)
+ if tunnel.isTunnelAvailableToUser {
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ }
+ #elseif os(iOS)
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ #else
+ #error("Unimplemented")
+ #endif
+ tunnelProviderManager.removeFromPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
+ return
+ }
+ if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
+ self.tunnels.remove(at: index)
+ self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
+ }
+ completionHandler(nil)
+
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelRemoved(tunnelName: tunnel.name)
+ #endif
+ }
+ }
+
+ func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
+ }
+
+ private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ guard let head = tunnels.first else {
+ completionHandler(nil)
+ return
+ }
+ let tail = tunnels.dropFirst()
+ remove(tunnel: head) { [weak self, tail] error in
+ DispatchQueue.main.async {
+ if let error = error {
+ completionHandler(error)
+ } else {
+ self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
+ }
+ }
+ }
+ }
+
+ func numberOfTunnels() -> Int {
+ return tunnels.count
+ }
+
+ func tunnel(at index: Int) -> TunnelContainer {
+ return tunnels[index]
+ }
+
+ func mapTunnels<T>(transform: (TunnelContainer) throws -> T) rethrows -> [T] {
+ return try tunnels.map(transform)
+ }
+
+ func index(of tunnel: TunnelContainer) -> Int? {
+ return tunnels.firstIndex(of: tunnel)
+ }
+
+ func tunnel(named tunnelName: String) -> TunnelContainer? {
+ return tunnels.first { $0.name == tunnelName }
+ }
+
+ func waitingTunnel() -> TunnelContainer? {
+ return tunnels.first { $0.status == .waiting }
+ }
+
+ func tunnelInOperation() -> TunnelContainer? {
+ if let waitingTunnelObject = waitingTunnel() {
+ return waitingTunnelObject
+ }
+ return tunnels.first { $0.status != .inactive }
+ }
+
+ func startActivation(of tunnel: TunnelContainer) {
+ guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
+ guard tunnel.status == .inactive else {
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
+ return
+ }
+
+ if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
+ alreadyWaitingTunnel.status = .inactive
+ }
+
+ if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
+ wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
+ tunnel.status = .waiting
+ activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
+ if tunnelInOperation.status != .deactivating {
+ startDeactivation(of: tunnelInOperation)
+ }
+ return
+ }
+
+ #if targetEnvironment(simulator)
+ tunnel.status = .active
+ #else
+ tunnel.startActivation(activationDelegate: activationDelegate)
+ #endif
+
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelActivated(tunnelName: tunnel.name)
+ #endif
+ }
+
+ func startDeactivation(of tunnel: TunnelContainer) {
+ tunnel.isAttemptingActivation = false
+ guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
+ #if targetEnvironment(simulator)
+ tunnel.status = .inactive
+ #else
+ tunnel.startDeactivation()
+ #endif
+ }
+
+ func refreshStatuses() {
+ tunnels.forEach { $0.refreshStatus() }
+ }
+
+ private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
+ waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
+ guard let self = self else { return }
+ if tunnel.status == .inactive {
+ if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
+ waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
+ }
+ self.waiteeObservationToken = nil
+ }
+ }
+ }
+
+ private func startObservingTunnelStatuses() {
+ statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
+ guard let self = self,
+ let session = statusChangeNotification.object as? NETunnelProviderSession,
+ let tunnelProvider = session.manager as? NETunnelProviderManager,
+ let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
+
+ wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
+
+ if tunnel.isAttemptingActivation {
+ if session.status == .connected {
+ tunnel.isAttemptingActivation = false
+ self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
+ } else if session.status == .disconnected {
+ tunnel.isAttemptingActivation = false
+ if let (title, message) = lastErrorTextFromNetworkExtension(for: tunnel) {
+ self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
+ } else {
+ self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
+ }
+ }
+ }
+
+ if tunnel.status == .restarting && session.status == .disconnected {
+ tunnel.startActivation(activationDelegate: self.activationDelegate)
+ return
+ }
+
+ tunnel.refreshStatus()
+ }
+ }
+
+ func startObservingTunnelConfigurations() {
+ configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
+ DispatchQueue.main.async { [weak self] in
+ // We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
+ // (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
+ // removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
+ // as a result of a tunnel getting added or removed outside of the app.
+ self?.reload()
+ }
+ }
+ }
+
+ static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
+ return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
+ }
+}
+
+private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
+ guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
+ guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
+ guard let lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
+ guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
+
+ if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
+ return extensionError.alertText
+ }
+
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
+}
+
+class TunnelContainer: NSObject {
+ @objc dynamic var name: String
+ @objc dynamic var status: TunnelStatus
+
+ @objc dynamic var isActivateOnDemandEnabled: Bool
+
+ var isAttemptingActivation = false {
+ didSet {
+ if isAttemptingActivation {
+ self.activationTimer?.invalidate()
+ let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
+ switch self.tunnelProvider.connection.status {
+ case .connected, .disconnected, .invalid:
+ self.activationTimer?.invalidate()
+ self.activationTimer = nil
+ default:
+ break
+ }
+ self.refreshStatus()
+ }
+ self.activationTimer = activationTimer
+ RunLoop.main.add(activationTimer, forMode: .common)
+ }
+ }
+ }
+ var activationAttemptId: String?
+ var activationTimer: Timer?
+ var deactivationTimer: Timer?
+
+ fileprivate var tunnelProvider: NETunnelProviderManager
+
+ var tunnelConfiguration: TunnelConfiguration? {
+ return tunnelProvider.tunnelConfiguration
+ }
+
+ var onDemandOption: ActivateOnDemandOption {
+ return ActivateOnDemandOption(from: tunnelProvider)
+ }
+
+ #if os(macOS)
+ var isTunnelAvailableToUser: Bool {
+ return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
+ }
+ #endif
+
+ init(tunnel: NETunnelProviderManager) {
+ name = tunnel.localizedDescription ?? "Unnamed"
+ let status = TunnelStatus(from: tunnel.connection.status)
+ self.status = status
+ isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
+ tunnelProvider = tunnel
+ super.init()
+ }
+
+ func getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
+ guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
+ completionHandler(tunnelConfiguration)
+ return
+ }
+ guard nil != (try? session.sendProviderMessage(Data([ UInt8(0) ]), responseHandler: {
+ guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
+ completionHandler(self.tunnelConfiguration)
+ return
+ }
+ completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
+ })) else {
+ completionHandler(tunnelConfiguration)
+ return
+ }
+ }
+
+ func refreshStatus() {
+ if status == .restarting {
+ return
+ }
+ status = TunnelStatus(from: tunnelProvider.connection.status)
+ isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
+ }
+
+ fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
+ if recursionCount >= 8 {
+ wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors(lastSystemError: lastError!))
+ return
+ }
+
+ wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
+
+ status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
+
+ guard tunnelProvider.isEnabled else {
+ // In case the tunnel had gotten disabled, re-enable and save it,
+ // then call this function again.
+ wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
+ tunnelProvider.isEnabled = true
+ tunnelProvider.saveToPreferences { [weak self] error in
+ guard let self = self else { return }
+ if error != nil {
+ wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving(systemError: error!))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, invoking startActivation")
+ self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
+ }
+ return
+ }
+
+ // Start the tunnel
+ do {
+ wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
+ isAttemptingActivation = true
+ let activationAttemptId = UUID().uuidString
+ self.activationAttemptId = activationAttemptId
+ try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
+ wg_log(.debug, staticMessage: "startActivation: Success")
+ activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
+ } catch let error {
+ isAttemptingActivation = false
+ guard let systemError = error as? NEVPNError else {
+ wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
+ status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: error))
+ return
+ }
+ guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
+ wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
+ status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: systemError))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
+ tunnelProvider.loadFromPreferences { [weak self] error in
+ guard let self = self else { return }
+ if error != nil {
+ wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
+ self.status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading(systemError: systemError))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
+ self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
+ }
+ }
+ }
+
+ fileprivate func startDeactivation() {
+ wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
+ (tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
+ }
+}
+
+extension NETunnelProviderManager {
+ fileprivate static var cachedConfigKey: UInt8 = 0
+
+ var tunnelConfiguration: TunnelConfiguration? {
+ if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
+ return cached
+ }
+ let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
+ if config != nil {
+ objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ return config
+ }
+
+ func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
+ protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
+ localizedDescription = tunnelConfiguration.name
+ objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+
+ func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
+ return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
+ }
+}
+
+#if os(macOS)
+@available(macOS 10.15, *)
+class CatalinaWorkaround {
+
+ // In macOS Catalina, for some users, the tunnels get deleted arbitrarily
+ // by the OS. It's not clear what triggers that.
+
+ // As a workaround, in macOS Catalina, when we realize that tunnels have been
+ // deleted outside the app, we reinstate those tunnels using the information
+ // in the keychain.
+
+ unowned let tunnelsManager: TunnelsManager
+ private var configChangeSubscriber: Any?
+
+ struct ReinstationData {
+ let tunnelConfiguration: TunnelConfiguration
+ let keychainPasswordRef: Data
+ }
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+
+ // Attempt reinstation when there's a change in tunnel configurations,
+ // which indicates that tunnels may have been deleted outside the app.
+ // We use debounce to wait for all change notifications to arrive
+ // before attempting to reinstate, so that we don't have saveToPreferences
+ // being called while another saveToPreferences is in progress.
+ self.configChangeSubscriber = NotificationCenter.default
+ .publisher(for: .NEVPNConfigurationChange, object: nil)
+ .debounce(for: .seconds(1), scheduler: RunLoop.main)
+ .subscribe(on: RunLoop.main)
+ .sink { [weak self] _ in
+ self?.reinstateTunnelsDeletedOutsideApp()
+ }
+
+ // Attempt reinstation on app launch
+ reinstateTunnelsDeletedOutsideApp()
+ }
+
+ func reinstateTunnelsDeletedOutsideApp() {
+ let rd = reinstationDataForTunnelsDeletedOutsideApp()
+ reinstateTunnels(ArraySlice(rd), completionHandler: nil)
+ }
+
+ private func reinstateTunnels(_ rdArray: ArraySlice<ReinstationData>, completionHandler: (() -> Void)?) {
+ guard let head = rdArray.first else {
+ completionHandler?()
+ return
+ }
+ let tail = rdArray.dropFirst()
+ self.tunnelsManager.reinstateTunnel(reinstationData: head) { _ in
+ DispatchQueue.main.async {
+ self.reinstateTunnels(tail, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ private func reinstationDataForTunnelsDeletedOutsideApp() -> [ReinstationData] {
+ let knownRefs: [Data] = self.tunnelsManager.tunnels
+ .compactMap { $0.tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol }
+ .compactMap { $0.passwordReference }
+ let knownRefsSet: Set<Data> = Set(knownRefs)
+ var result: CFTypeRef?
+ let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
+ kSecMatchLimit as String: kSecMatchLimitAll,
+ kSecReturnAttributes as String: true,
+ kSecReturnPersistentRef as String: true] as CFDictionary,
+ &result)
+ guard ret == errSecSuccess, let resultDicts = result as? [[String: Any]] else { return [] }
+ let labelPrefix = "WireGuard Tunnel: "
+ var reinstationData: [ReinstationData] = []
+ for resultDict in resultDicts {
+ guard let ref = resultDict[kSecValuePersistentRef as String] as? Data else { continue }
+ guard let label = resultDict[kSecAttrLabel as String] as? String else { continue }
+ guard label.hasPrefix(labelPrefix) else { continue }
+ if !knownRefsSet.contains(ref) {
+ let tunnelName = String(label.dropFirst(labelPrefix.count))
+ if let configStr = Keychain.openReference(called: ref),
+ let config = try? TunnelConfiguration(fromWgQuickConfig: configStr, called: tunnelName) {
+ reinstationData.append(ReinstationData(tunnelConfiguration: config, keychainPasswordRef: ref))
+ }
+ }
+ }
+ return reinstationData
+ }
+}
+#endif
+
+#if os(macOS)
+@available(macOS 10.15, *)
+extension TunnelsManager {
+ fileprivate func reinstateTunnel(reinstationData: CatalinaWorkaround.ReinstationData, completionHandler: @escaping (Bool) -> Void) {
+ let tunnelName = reinstationData.tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(false)
+ return
+ }
+
+ if tunnels.contains(where: { $0.name == tunnelName }) {
+ completionHandler(false)
+ return
+ }
+
+ let tunnelProviderProtocol = NETunnelProviderProtocol()
+ guard let appId = Bundle.main.bundleIdentifier else { fatalError() }
+ tunnelProviderProtocol.providerBundleIdentifier = "\(appId).network-extension"
+ tunnelProviderProtocol.passwordReference = reinstationData.keychainPasswordRef
+ tunnelProviderProtocol.providerConfiguration = ["UID": getuid()]
+ tunnelProviderProtocol.serverAddress = {
+ let endpoints = reinstationData.tunnelConfiguration.peers.compactMap { $0.endpoint }
+ if endpoints.count == 1 {
+ return endpoints[0].stringRepresentation
+ } else if endpoints.isEmpty {
+ return "Unspecified"
+ } else {
+ return "Multiple endpoints"
+ }
+ }()
+
+ let tunnelProvider = NETunnelProviderManager()
+ tunnelProvider.localizedDescription = tunnelName
+ tunnelProvider.protocolConfiguration = tunnelProviderProtocol
+ objc_setAssociatedObject(tunnelProvider, &NETunnelProviderManager.cachedConfigKey, reinstationData.tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ tunnelProvider.isEnabled = true
+
+ tunnelProvider.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Reinstate: Saving configuration failed: \(error!)")
+ completionHandler(false)
+ return
+ }
+
+ guard let self = self else { return }
+
+ let tunnel = TunnelContainer(tunnel: tunnelProvider)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ completionHandler(true)
+ }
+ }
+}
+#endif