Move all source files to Sources/ and rename WireGuardKit targets

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
This commit is contained in:
Andrej Mihajlov
2020-12-03 13:32:24 +01:00
parent 9c38a1b897
commit ec57408570
209 changed files with 54 additions and 58 deletions
@@ -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)]
}
}
@@ -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
@@ -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
}
}
@@ -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
}
}
}
@@ -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()
}
}
}
@@ -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