Move all source files to Sources/ and rename WireGuardKit targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user