Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fa2f58aad | |||
| 1eddb2c86d | |||
| ddccd6da4c | |||
| aa0b288436 | |||
| 71568d1899 | |||
| 06a783f50a | |||
| 465c22f769 | |||
| b7e5638681 | |||
| 66b4aedd08 | |||
| 105eca7adc | |||
| e3801308cb | |||
| 05d0429c50 | |||
| fa9a4921a8 | |||
| c827a00307 | |||
| dcfa9473e9 | |||
| 782dd2ea4e | |||
| c9267ba634 | |||
| 8d26a3c536 | |||
| 7631844fbe | |||
| 046b540e53 | |||
| f6faffa4c1 | |||
| 290bd192a0 | |||
| bf86731879 | |||
| 4e386e85e4 | |||
| 046d1413ec | |||
| e1b258353c | |||
| cdd132d7d0 | |||
| 679d63294d | |||
| d01d46fde8 | |||
| a3bc306b6e | |||
| 3e1772ccd0 | |||
| b946cbc0f3 | |||
| 8590a8804a | |||
| a117e3ae3e | |||
| ef2012330f | |||
| 6bb25782c1 | |||
| 1dac181803 | |||
| d556729705 | |||
| b1ae13652c | |||
| 203d6b4596 | |||
| 2676ee0169 | |||
| c60c29b93c | |||
| bdb2411ebc | |||
| 923d039a78 | |||
| abb0312d38 | |||
| f1b8de7312 | |||
| cc122d7463 | |||
| 39a067cb96 | |||
| c5e8b05e8b | |||
| 4b7094d652 | |||
| 0f03ffc920 | |||
| 1502bd42d3 | |||
| 290f83d5ef | |||
| e8d68396ca | |||
| 8e7bfb15ed | |||
| 95456ec956 | |||
| 7485474c4c | |||
| 1d63509b92 | |||
| f3e32ab737 | |||
| 59b9a6e5d2 | |||
| 3136fe0e2c | |||
| a1070d2b29 | |||
| 5ee4d392b5 | |||
| c17e4a27a2 | |||
| 8409b7e929 | |||
| 651ffa0c51 | |||
| 4404bb2b7d | |||
| e66cf5264a | |||
| af58bfcb00 | |||
| 2f7e437202 | |||
| 62573e2ad7 | |||
| a5f9dc4821 | |||
| 7827147bc8 | |||
| a473dfe4f8 | |||
| fb6a7f6007 | |||
| f7b04b0b5d | |||
| c88c660b51 | |||
| ec2d67ea00 |
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
static var networkExtensionLogFileURL: URL? {
|
||||
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
|
||||
os_log("Can't obtain app group id from bundle", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
||||
os_log("Can't obtain shared folder URL", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
return sharedFolderURL.appendingPathComponent("last-activated-tunnel-log.txt")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch(let e) {
|
||||
os_log("Failed to delete file '%{public}@': %{public}@", log: OSLog.default, type: .debug, url.absoluteString, e.localizedDescription)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -4,11 +4,18 @@
|
||||
import Foundation
|
||||
|
||||
@available(OSX 10.14, iOS 12.0, *)
|
||||
class TunnelConfiguration: Codable {
|
||||
final class TunnelConfiguration: Codable {
|
||||
var interface: InterfaceConfiguration
|
||||
var peers: [PeerConfiguration] = []
|
||||
init(interface: InterfaceConfiguration) {
|
||||
let peers: [PeerConfiguration]
|
||||
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
|
||||
self.interface = interface
|
||||
self.peers = peers
|
||||
|
||||
let peerPublicKeysArray = peers.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
|
||||
fatalError("Two or more peers cannot have the same public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +28,6 @@ struct InterfaceConfiguration: Codable {
|
||||
var mtu: UInt16?
|
||||
var dns: [DNSServer] = []
|
||||
|
||||
var publicKey: Data {
|
||||
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
}
|
||||
|
||||
init(name: String, privateKey: Data) {
|
||||
self.name = name
|
||||
self.privateKey = privateKey
|
||||
@@ -87,4 +87,15 @@ extension Endpoint {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func hostname() -> String? {
|
||||
switch (host) {
|
||||
case .name(let hostname, _):
|
||||
return hostname
|
||||
case .ipv4(_):
|
||||
return nil
|
||||
case .ipv6(_):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
assert(!tunnelConfiguration.interface.name.isEmpty)
|
||||
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
|
||||
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [
|
||||
"tunnelConfiguration": serializedTunnelConfiguration,
|
||||
"tunnelConfigurationVersion": 1
|
||||
]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints.first!.stringRepresentation()
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
}
|
||||
username = tunnelConfiguration.interface.name
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
|
||||
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PacketTunnelOptionKey: String {
|
||||
|
||||
case interfaceName, wireguardSettings, remoteAddress, dnsServers, mtu,
|
||||
|
||||
// IPv4 settings
|
||||
ipv4Addresses, ipv4SubnetMasks,
|
||||
ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks,
|
||||
ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks,
|
||||
|
||||
// IPv6 settings
|
||||
ipv6Addresses, ipv6NetworkPrefixLengths,
|
||||
ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths,
|
||||
ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String {
|
||||
subscript(key: PacketTunnelOptionKey) -> Value? {
|
||||
get {
|
||||
return self[key.rawValue]
|
||||
}
|
||||
set(value) {
|
||||
self[key.rawValue] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
|
||||
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */; };
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
|
||||
@@ -42,8 +42,21 @@
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; };
|
||||
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
|
||||
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
|
||||
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */; };
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
|
||||
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -80,14 +93,16 @@
|
||||
/* Begin PBXFileReference section */
|
||||
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
|
||||
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; };
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension.entitlements; sourceTree = "<group>"; };
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionKey.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; };
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; };
|
||||
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
|
||||
6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; };
|
||||
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -120,6 +135,8 @@
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
|
||||
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
|
||||
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; };
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; };
|
||||
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@@ -127,6 +144,10 @@
|
||||
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
|
||||
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
|
||||
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetReachability.swift; sourceTree = "<group>"; };
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -152,9 +173,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C1E218352EF000F85AD /* Info.plist */,
|
||||
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */,
|
||||
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */,
|
||||
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */,
|
||||
);
|
||||
path = WireGuardNetworkExtension;
|
||||
sourceTree = "<group>";
|
||||
@@ -162,7 +186,9 @@
|
||||
6F5D0C432183B4A4000F85AD /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
|
||||
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -227,8 +253,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
|
||||
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
|
||||
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */,
|
||||
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
|
||||
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */,
|
||||
);
|
||||
path = VPN;
|
||||
sourceTree = "<group>";
|
||||
@@ -255,6 +281,8 @@
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FE254FA219C10800028284D /* ZipImporter.swift */,
|
||||
6FE254FE219C60290028284D /* ZipExporter.swift */,
|
||||
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */,
|
||||
6FDEF7F421863B6100D8FBF6 /* 3rdparty */,
|
||||
);
|
||||
@@ -308,10 +336,11 @@
|
||||
6F919ED3218C65C50023B400 /* Resources */,
|
||||
6F6899A32180445A0012E523 /* Crypto */,
|
||||
6F6899AA218099D00012E523 /* ConfigFile */,
|
||||
6F7774E6217201E0006A79B3 /* Model */,
|
||||
6F7774DD217181B1006A79B3 /* UI */,
|
||||
6F7774ED21722D0C006A79B3 /* VPN */,
|
||||
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
|
||||
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
|
||||
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
|
||||
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
|
||||
6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
|
||||
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
|
||||
@@ -355,6 +384,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
|
||||
buildPhases = (
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
|
||||
6F5D0C16218352EF000F85AD /* Sources */,
|
||||
6F5D0C17218352EF000F85AD /* Frameworks */,
|
||||
6F5D0C18218352EF000F85AD /* Resources */,
|
||||
@@ -410,6 +440,9 @@
|
||||
CreatedOnToolsVersion = 9.4.1;
|
||||
LastSwiftMigration = 1000;
|
||||
SystemCapabilities = {
|
||||
com.apple.ApplicationGroups.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
@@ -480,6 +513,25 @@
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Extract wireguard-go Version";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -487,7 +539,15 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
|
||||
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */,
|
||||
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
|
||||
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
|
||||
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
|
||||
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
|
||||
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
|
||||
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -499,30 +559,35 @@
|
||||
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
|
||||
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
|
||||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
|
||||
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
|
||||
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
|
||||
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
|
||||
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
|
||||
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
|
||||
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
|
||||
6F6899A62180447E0012E523 /* x25519.c in Sources */,
|
||||
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */,
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
|
||||
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
|
||||
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
|
||||
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
|
||||
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
|
||||
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
|
||||
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
|
||||
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
|
||||
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
|
||||
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
|
||||
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
|
||||
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
|
||||
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
VERSION_NAME = 0.0.20181104
|
||||
VERSION_ID = 4
|
||||
VERSION_ID = 5
|
||||
|
||||
@@ -26,16 +26,16 @@ class WgQuickConfigFileParser {
|
||||
|
||||
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["PrivateKey"] else { return nil }
|
||||
guard let privateKeyString = attributes["privatekey"] else { return nil }
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { return nil }
|
||||
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
|
||||
// other wg fields
|
||||
if let listenPortString = attributes["ListenPort"] {
|
||||
if let listenPortString = attributes["listenport"] {
|
||||
guard let listenPort = UInt16(listenPortString) else { return nil }
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
// wg-quick fields
|
||||
if let addressesString = attributes["Address"] {
|
||||
if let addressesString = attributes["address"] {
|
||||
var addresses: [IPAddressRange] = []
|
||||
for addressString in addressesString.split(separator: ",") {
|
||||
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
|
||||
@@ -44,7 +44,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["DNS"] {
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers: [DNSServer] = []
|
||||
for dnsServerString in dnsString.split(separator: ",") {
|
||||
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
|
||||
@@ -53,7 +53,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["MTU"] {
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else { return nil }
|
||||
interface.mtu = mtu
|
||||
}
|
||||
@@ -62,15 +62,15 @@ class WgQuickConfigFileParser {
|
||||
|
||||
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["PublicKey"] else { return nil }
|
||||
guard let publicKeyString = attributes["publickey"] else { return nil }
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { return nil }
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
// wg fields
|
||||
if let preSharedKeyString = attributes["PreSharedKey"] {
|
||||
if let preSharedKeyString = attributes["presharedkey"] {
|
||||
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil }
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
if let allowedIPsString = attributes["AllowedIPs"] {
|
||||
if let allowedIPsString = attributes["allowedips"] {
|
||||
var allowedIPs: [IPAddressRange] = []
|
||||
for allowedIPString in allowedIPsString.split(separator: ",") {
|
||||
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
@@ -79,11 +79,11 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["Endpoint"] {
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else { return nil }
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["PersistentKeepalive"] {
|
||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
@@ -113,9 +113,9 @@ class WgQuickConfigFileParser {
|
||||
|
||||
if let equalsIndex = line.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces)
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces).lowercased()
|
||||
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["Address", "AllowedIPs", "DNS"]
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
@@ -157,8 +157,7 @@ class WgQuickConfigFileParser {
|
||||
}
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
return tunnelConfiguration
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
|
||||
@@ -25,3 +25,9 @@ struct Curve25519 {
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
|
||||
extension InterfaceConfiguration {
|
||||
var publicKey: Data {
|
||||
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
@@ -120,5 +122,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -361,6 +361,7 @@ class TunnelViewModel {
|
||||
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
||||
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
|
||||
excludePrivateIPsValue = isOn
|
||||
}
|
||||
}
|
||||
@@ -441,9 +442,42 @@ class TunnelViewModel {
|
||||
return .error("Two or more peers cannot have the same public key")
|
||||
}
|
||||
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
|
||||
tunnelConfiguration.peers = peerConfigurations
|
||||
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
return .saved(tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Activate on demand
|
||||
|
||||
extension TunnelViewModel {
|
||||
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
|
||||
switch (activateOnDemandOption) {
|
||||
case .none:
|
||||
return "Off"
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
return "Wi-Fi or cellular"
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
return "Wi-Fi only"
|
||||
case .useOnDemandOverCellularOnly:
|
||||
return "Cellular only"
|
||||
}
|
||||
}
|
||||
|
||||
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
|
||||
if let activateOnDemandSetting = activateOnDemandSetting {
|
||||
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
|
||||
return .useOnDemandOverWiFiOrCellular
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var mainVC: MainViewController?
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = UIColor.white
|
||||
@@ -27,14 +27,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
defer {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
os_log("Failed to remove item from Inbox: %{public}@", log: OSLog.default, type: .debug, url.absoluteString)
|
||||
}
|
||||
}
|
||||
mainVC?.tunnelsListVC?.importFromFile(url: url)
|
||||
_ = FileManager.deleteFile(at: url)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
mainVC?.refreshTunnelConnectionStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: State restoration
|
||||
|
||||
extension AppDelegate {
|
||||
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
viewControllerWithRestorationIdentifierPath identifierComponents: [String],
|
||||
coder: NSCoder) -> UIViewController? {
|
||||
guard let vcIdentifier = identifierComponents.last else { return nil }
|
||||
if (vcIdentifier == "MainVC") {
|
||||
return MainViewController()
|
||||
} else if (vcIdentifier == "TunnelsListVC") {
|
||||
return TunnelsListTableViewController()
|
||||
} else if (vcIdentifier.hasPrefix("TunnelDetailVC:")) {
|
||||
let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
|
||||
if let tunnelsManager = mainVC?.tunnelsManager {
|
||||
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
|
||||
return TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
}
|
||||
} else {
|
||||
// Show it when tunnelsManager is available
|
||||
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,54 +5,10 @@ import UIKit
|
||||
import os.log
|
||||
|
||||
class ErrorPresenter {
|
||||
static func errorMessage(for error: Error) -> (String, String)? {
|
||||
switch (error) {
|
||||
|
||||
// TunnelManagementError
|
||||
case TunnelManagementError.tunnelAlreadyExistsWithThatName:
|
||||
return ("Name already exists", "A tunnel with that name already exists")
|
||||
case TunnelManagementError.tunnelInvalidName:
|
||||
return ("Name already exists", "The tunnel name is invalid")
|
||||
case TunnelManagementError.vpnSystemErrorOnAddTunnel:
|
||||
return ("Unable to create tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnModifyTunnel:
|
||||
return ("Unable to modify tunnel", "Internal error")
|
||||
case TunnelManagementError.vpnSystemErrorOnRemoveTunnel:
|
||||
return ("Unable to remove tunnel", "Internal error")
|
||||
|
||||
// TunnelActivationError
|
||||
case TunnelActivationError.dnsResolutionFailed:
|
||||
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
|
||||
case TunnelActivationError.tunnelActivationFailed:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(let otherTunnelStatus):
|
||||
let statusString: String = {
|
||||
switch (otherTunnelStatus) {
|
||||
case .active: fallthrough
|
||||
case .reasserting: fallthrough
|
||||
case .restarting:
|
||||
return "active"
|
||||
case .activating: fallthrough
|
||||
case .resolvingEndpointDomains:
|
||||
return "being activated"
|
||||
case .deactivating:
|
||||
return "being deactivated"
|
||||
case .inactive:
|
||||
fatalError()
|
||||
}
|
||||
}()
|
||||
return ("Activation failure", "Another tunnel is currently \(statusString)")
|
||||
|
||||
default:
|
||||
os_log("ErrorPresenter: Error not presented: %{public}@", log: OSLog.default, type: .error, "\(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func showErrorAlert(error: Error, from sourceVC: UIViewController?,
|
||||
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?,
|
||||
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
|
||||
guard let sourceVC = sourceVC else { return }
|
||||
guard let (title, message) = ErrorPresenter.errorMessage(for: error) else { return }
|
||||
let (title, message) = error.alertText()
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
|
||||
onDismissal?()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
import UIKit
|
||||
|
||||
class MainViewController: UISplitViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
|
||||
var tunnelsListVC: TunnelsListTableViewController?
|
||||
|
||||
init() {
|
||||
@@ -19,6 +23,11 @@ class MainViewController: UISplitViewController {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.viewControllers = [ masterNC, detailNC ]
|
||||
|
||||
// State restoration
|
||||
self.restorationIdentifier = "MainVC"
|
||||
masterNC.restorationIdentifier = "MasterNC"
|
||||
detailNC.restorationIdentifier = "DetailNC"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -30,10 +39,62 @@ class MainViewController: UISplitViewController {
|
||||
|
||||
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
|
||||
self.preferredDisplayMode = .allVisible
|
||||
|
||||
// Create the tunnels manager, and when it's ready, inform tunnelsListVC
|
||||
TunnelsManager.create { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
let tunnelsManager: TunnelsManager = result.value!
|
||||
guard let s = self else { return }
|
||||
|
||||
s.tunnelsManager = tunnelsManager
|
||||
s.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
|
||||
|
||||
tunnelsManager.activationDelegate = s
|
||||
|
||||
s.onTunnelsManagerReady?(tunnelsManager)
|
||||
s.onTunnelsManagerReady = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: TunnelsManagerActivationDelegate {
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError) {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController {
|
||||
func refreshTunnelConnectionStatuses() {
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
tunnelsManager.refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
func openForEditing(configFileURL: URL) {
|
||||
tunnelsListVC?.openForEditing(configFileURL: configFileURL)
|
||||
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
|
||||
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] (tunnelsManager) in
|
||||
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
if let self = self {
|
||||
if (animated) {
|
||||
self.showDetailViewController(tunnelDetailNC, sender: self)
|
||||
} else {
|
||||
UIView.performWithoutAnimation {
|
||||
self.showDetailViewController(tunnelDetailNC, sender: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
showTunnelDetailBlock(tunnelsManager)
|
||||
} else {
|
||||
onTunnelsManagerReady = showTunnelDetailBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ class SettingsTableViewController: UITableViewController {
|
||||
case iosAppVersion = "WireGuard for iOS"
|
||||
case goBackendVersion = "WireGuard Go Backend"
|
||||
case exportZipArchive = "Export zip archive"
|
||||
case exportLogFile = "Export log file"
|
||||
}
|
||||
|
||||
let settingsFieldsBySection: [[SettingsFields]] = [
|
||||
[.iosAppVersion, .goBackendVersion],
|
||||
[.exportZipArchive]
|
||||
[.exportZipArchive],
|
||||
[.exportLogFile]
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager?
|
||||
@@ -63,55 +65,73 @@ class SettingsTableViewController: UITableViewController {
|
||||
}
|
||||
|
||||
func exportConfigurationsAsZipFile(sourceView: UIView) {
|
||||
guard let tunnelsManager = tunnelsManager, tunnelsManager.numberOfTunnels() > 0 else {
|
||||
showErrorAlert(title: "Nothing to export", message: "There are no tunnels to export")
|
||||
return
|
||||
}
|
||||
var inputsToArchiver: [(fileName: String, contents: Data)] = []
|
||||
var usedNames: Set<String> = []
|
||||
for i in 0 ..< tunnelsManager.numberOfTunnels() {
|
||||
guard let tunnelConfiguration = tunnelsManager.tunnel(at: i).tunnelConfiguration() else { continue }
|
||||
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
|
||||
let name = tunnelConfiguration.interface.name
|
||||
var nameToCheck = name
|
||||
var i = 0
|
||||
while (usedNames.contains(nameToCheck)) {
|
||||
i = i + 1
|
||||
nameToCheck = "\(name)\(i)"
|
||||
}
|
||||
usedNames.insert(nameToCheck)
|
||||
inputsToArchiver.append((fileName: "\(nameToCheck).conf", contents: contents))
|
||||
}
|
||||
}
|
||||
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return
|
||||
}
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
} catch {
|
||||
os_log("Failed to delete file: %{public}@ : %{public}@", log: OSLog.default, type: .error, destinationURL.absoluteString, error.localizedDescription)
|
||||
}
|
||||
|
||||
do {
|
||||
try ZipArchive.archive(inputs: inputsToArchiver, to: destinationURL)
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
// popoverPresentationController shall be non-nil on the iPad
|
||||
activityVC.popoverPresentationController?.sourceView = sourceView
|
||||
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
present(activityVC, animated: true)
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to export", message: "There was an error exporting the tunnel configuration archive: \(String(describing: error))")
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
|
||||
let count = tunnelsManager.numberOfTunnels()
|
||||
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() }
|
||||
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] (error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
|
||||
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
|
||||
self?.present(fileExportVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
func exportLogForLastActivatedTunnel(sourceView: UIView) {
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return
|
||||
}
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
|
||||
let timeStampString = dateFormatter.string(from: Date())
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt")
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
|
||||
if (FileManager.default.fileExists(atPath: destinationURL.path)) {
|
||||
let isDeleted = FileManager.deleteFile(at: destinationURL)
|
||||
if (!isDeleted) {
|
||||
ErrorPresenter.showErrorAlert(title: "No log available", message: "The pre-existing log could not be cleared", from: self)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL,
|
||||
FileManager.default.fileExists(atPath: networkExtensionLogFileURL.path) else {
|
||||
ErrorPresenter.showErrorAlert(title: "No log available", message: "Please activate a tunnel and then export the log", from: self)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: networkExtensionLogFileURL, to: destinationURL)
|
||||
} catch {
|
||||
os_log("Failed to copy file: %{public}@ to %{public}@: %{public}@", log: OSLog.default, type: .error, networkExtensionLogFileURL.absoluteString, destinationURL.absoluteString, error.localizedDescription)
|
||||
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The log could not be copied", from: self)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
// popoverPresentationController shall be non-nil on the iPad
|
||||
activityVC.popoverPresentationController?.sourceView = sourceView
|
||||
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
activityVC.completionWithItemsHandler = { (_, _, _, _) in
|
||||
// Remove the exported log file after the activity has completed
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
}
|
||||
self.present(activityVC, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +152,8 @@ extension SettingsTableViewController {
|
||||
return "About"
|
||||
case 1:
|
||||
return "Export configurations"
|
||||
case 2:
|
||||
return "Tunnel log"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -152,14 +174,21 @@ extension SettingsTableViewController {
|
||||
cell.value = WIREGUARD_GO_VERSION
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .exportZipArchive)
|
||||
} else if (field == .exportZipArchive) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .exportLogFile)
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
|
||||
cell.buttonText = field.rawValue
|
||||
cell.onTapped = { [weak self] in
|
||||
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.id)
|
||||
|
||||
// State restoration
|
||||
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
}
|
||||
|
||||
@objc func editTapped() {
|
||||
@@ -52,14 +56,6 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
present(editNC, animated: true)
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
|
||||
onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
|
||||
@@ -95,7 +91,7 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
|
||||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 3 + tunnelViewModel.peersData.count
|
||||
return 4 + tunnelViewModel.peersData.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@@ -112,7 +108,11 @@ extension TunnelDetailTableViewController {
|
||||
// Peer
|
||||
let peerData = tunnelViewModel.peersData[section - 2]
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// Activate on demand
|
||||
return 1
|
||||
} else {
|
||||
assert(section == (3 + numberOfPeerSections))
|
||||
// Delete tunnel
|
||||
return 1
|
||||
}
|
||||
@@ -130,6 +130,9 @@ extension TunnelDetailTableViewController {
|
||||
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// On-Demand Activation
|
||||
return "On-Demand Activation"
|
||||
} else {
|
||||
// Delete tunnel
|
||||
return nil
|
||||
@@ -152,16 +155,15 @@ extension TunnelDetailTableViewController {
|
||||
if (isOn) {
|
||||
s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: s)
|
||||
DispatchQueue.main.async {
|
||||
cell.statusSwitch.isOn = false
|
||||
}
|
||||
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
|
||||
DispatchQueue.main.async {
|
||||
cell.statusSwitch.isOn = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.tunnelsManager.startDeactivation(of: s.tunnel) { error in
|
||||
print("Error while deactivating: \(String(describing: error))")
|
||||
}
|
||||
s.tunnelsManager.startDeactivation(of: s.tunnel)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
@@ -183,8 +185,13 @@ extension TunnelDetailTableViewController {
|
||||
cell.key = field.rawValue
|
||||
cell.value = peerData[field]
|
||||
return cell
|
||||
} else if (section < (3 + numberOfPeerSections)) {
|
||||
// On-Demand Activation
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewActivateOnDemandCell.id, for: indexPath) as! TunnelDetailTableViewActivateOnDemandCell
|
||||
cell.tunnel = self.tunnel
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (2 + numberOfPeerSections))
|
||||
assert(section == (3 + numberOfPeerSections))
|
||||
// Delete configuration
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
|
||||
cell.buttonText = "Delete tunnel"
|
||||
@@ -259,10 +266,10 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
|
||||
text = "Deactivating"
|
||||
case .reasserting:
|
||||
text = "Reactivating"
|
||||
case .resolvingEndpointDomains:
|
||||
text = "Resolving domains"
|
||||
case .restarting:
|
||||
text = "Restarting"
|
||||
case .waiting:
|
||||
text = "Waiting"
|
||||
}
|
||||
textLabel?.text = text
|
||||
DispatchQueue.main.async { [weak statusSwitch] in
|
||||
@@ -394,3 +401,36 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
|
||||
hasDestructiveAction = false
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell {
|
||||
static let id: String = "TunnelDetailTableViewActivateOnDemandCell"
|
||||
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
update(from: tunnel?.activateOnDemandSetting())
|
||||
onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] (tunnel, _) in
|
||||
self?.update(from: tunnel.activateOnDemandSetting())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onDemandStatusObservervationToken: AnyObject?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
textLabel?.text = "Activate on demand"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(from activateOnDemandSetting: ActivateOnDemandSetting?) {
|
||||
detailTextLabel?.text = TunnelViewModel.activateOnDemandDetailText(for: activateOnDemandSetting)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
detailTextLabel?.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,23 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
.deletePeer
|
||||
]
|
||||
|
||||
let activateOnDemandOptions: [ActivateOnDemandOption] = [
|
||||
.useOnDemandOverWiFiOrCellular,
|
||||
.useOnDemandOverWiFiOnly,
|
||||
.useOnDemandOverCellularOnly
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer?
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting
|
||||
|
||||
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
|
||||
// Use this initializer to edit an existing tunnel.
|
||||
tunnelsManager = tm
|
||||
tunnel = t
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
|
||||
activateOnDemandSetting = t.activateOnDemandSetting()
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
@@ -44,6 +52,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
tunnelsManager = tm
|
||||
tunnel = nil
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
|
||||
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
@@ -58,12 +67,12 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
|
||||
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.allowsSelection = false
|
||||
|
||||
self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id)
|
||||
self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id)
|
||||
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
|
||||
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
|
||||
self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.id)
|
||||
}
|
||||
|
||||
@objc func saveTapped() {
|
||||
@@ -77,7 +86,9 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
case .saved(let tunnelConfiguration):
|
||||
if let tunnel = tunnel {
|
||||
// We're modifying an existing tunnel
|
||||
tunnelsManager.modify(tunnel: tunnel, with: tunnelConfiguration) { [weak self] (error) in
|
||||
tunnelsManager.modify(tunnel: tunnel,
|
||||
tunnelConfiguration: tunnelConfiguration,
|
||||
activateOnDemandSetting: activateOnDemandSetting) { [weak self] (error) in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
@@ -87,14 +98,14 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
}
|
||||
} else {
|
||||
// We're adding a new tunnel
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] (tunnel, error) in
|
||||
if let error = error {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
|
||||
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
let tunnel: TunnelContainer = result.value!
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
if let tunnel = tunnel {
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,14 +116,6 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
dismiss(animated: true, completion: nil)
|
||||
self.delegate?.tunnelEditingCancelled()
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDataSource
|
||||
@@ -122,7 +125,7 @@ extension TunnelEditTableViewController {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
return numberOfInterfaceSections + numberOfPeerSections + 1
|
||||
return numberOfInterfaceSections + numberOfPeerSections + 1 /* Add Peer */ + 1 /* On-Demand */
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@@ -138,9 +141,16 @@ extension TunnelEditTableViewController {
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
|
||||
return peerFieldsToShow.count
|
||||
} else {
|
||||
} else if (section < (numberOfInterfaceSections + numberOfPeerSections + 1)) {
|
||||
// Add peer
|
||||
return 1
|
||||
} else {
|
||||
// On-Demand Rules
|
||||
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
|
||||
return 4
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +164,12 @@ extension TunnelEditTableViewController {
|
||||
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
|
||||
// Peer
|
||||
return "Peer"
|
||||
} else {
|
||||
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
|
||||
// Add peer
|
||||
return nil
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
return "On-Demand Activation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,8 +357,7 @@ extension TunnelEditTableViewController {
|
||||
}
|
||||
return cell
|
||||
}
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections))
|
||||
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
|
||||
// Add peer
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
|
||||
cell.buttonText = "Add peer"
|
||||
@@ -365,6 +377,37 @@ extension TunnelEditTableViewController {
|
||||
}, completion: nil)
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
if (row == 0) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
|
||||
cell.message = "Activate on demand"
|
||||
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
cell.onSwitchToggled = { [weak self] (isOn) in
|
||||
guard let s = self else { return }
|
||||
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
if (isOn) {
|
||||
s.activateOnDemandSetting.isActivateOnDemandEnabled = true
|
||||
if (s.activateOnDemandSetting.activateOnDemandOption == .none) {
|
||||
s.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
|
||||
}
|
||||
s.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
} else {
|
||||
s.activateOnDemandSetting.isActivateOnDemandEnabled = false
|
||||
s.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(row < 4)
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSelectionListCell.id, for: indexPath) as! TunnelEditTableViewSelectionListCell
|
||||
let rowOption = activateOnDemandOptions[row - 1]
|
||||
let selectedOption = activateOnDemandSetting.activateOnDemandOption
|
||||
assert(selectedOption != .none)
|
||||
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
|
||||
cell.isChecked = (selectedOption == rowOption)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +449,42 @@ extension TunnelEditTableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section == (numberOfInterfaceSections + numberOfPeerSections + 1)) {
|
||||
return (row > 0) ? indexPath : nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let numberOfInterfaceSections = interfaceFieldsBySection.count
|
||||
let numberOfPeerSections = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
|
||||
assert(row > 0)
|
||||
|
||||
let option = activateOnDemandOptions[row - 1]
|
||||
assert(option != .none)
|
||||
activateOnDemandSetting.activateOnDemandOption = option
|
||||
|
||||
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
tableView.reloadRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewKeyValueCell"
|
||||
var key: String {
|
||||
@@ -488,6 +567,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
|
||||
isValueValid = true
|
||||
keyboardType = .default
|
||||
onValueChanged = nil
|
||||
onValueBeingEdited = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,3 +744,30 @@ class TunnelEditTableViewSwitchCell: UITableViewCell {
|
||||
isOn = false
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelEditTableViewSelectionListCell: UITableViewCell {
|
||||
static let id: String = "TunnelEditTableViewSelectionListCell"
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
var isChecked: Bool {
|
||||
didSet {
|
||||
accessoryType = isChecked ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
isChecked = false
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import UserNotifications
|
||||
|
||||
class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
|
||||
var busyIndicator: UIActivityIndicatorView?
|
||||
var centeredAddButton: BorderedTextButton?
|
||||
@@ -38,50 +38,66 @@ class TunnelsListTableViewController: UIViewController {
|
||||
busyIndicator.startAnimating()
|
||||
self.busyIndicator = busyIndicator
|
||||
|
||||
// Create the tunnels manager, and when it's ready, create the tableView
|
||||
TunnelsManager.create { [weak self] tunnelsManager in
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
guard let s = self else { return }
|
||||
// State restoration
|
||||
self.restorationIdentifier = "TunnelsListVC"
|
||||
}
|
||||
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.rowHeight = 60
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
|
||||
func setTunnelsManager(tunnelsManager: TunnelsManager) {
|
||||
if (self.tunnelsManager != nil) {
|
||||
// If a tunnels manager is already set, do nothing
|
||||
return
|
||||
}
|
||||
|
||||
s.view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.leftAnchor.constraint(equalTo: s.view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: s.view.rightAnchor),
|
||||
tableView.topAnchor.constraint(equalTo: s.view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: s.view.bottomAnchor)
|
||||
])
|
||||
tableView.dataSource = s
|
||||
tableView.delegate = s
|
||||
s.tableView = tableView
|
||||
// Create the table view
|
||||
|
||||
// Add an add button, centered
|
||||
let centeredAddButton = BorderedTextButton()
|
||||
centeredAddButton.title = "Add a tunnel"
|
||||
centeredAddButton.isHidden = true
|
||||
s.view.addSubview(centeredAddButton)
|
||||
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centeredAddButton.centerXAnchor.constraint(equalTo: s.view.centerXAnchor),
|
||||
centeredAddButton.centerYAnchor.constraint(equalTo: s.view.centerYAnchor)
|
||||
])
|
||||
centeredAddButton.onTapped = { [weak self] in
|
||||
self?.addButtonTapped(sender: centeredAddButton)
|
||||
}
|
||||
s.centeredAddButton = centeredAddButton
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.rowHeight = 60
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
|
||||
|
||||
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
|
||||
busyIndicator.stopAnimating()
|
||||
self.view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
|
||||
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
||||
])
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
self.tableView = tableView
|
||||
|
||||
tunnelsManager.delegate = s
|
||||
s.tunnelsManager = tunnelsManager
|
||||
s.onTunnelsManagerReady?(tunnelsManager)
|
||||
s.onTunnelsManagerReady = nil
|
||||
// Add button at the center
|
||||
|
||||
let centeredAddButton = BorderedTextButton()
|
||||
centeredAddButton.title = "Add a tunnel"
|
||||
centeredAddButton.isHidden = true
|
||||
self.view.addSubview(centeredAddButton)
|
||||
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
|
||||
])
|
||||
centeredAddButton.onTapped = { [weak self] in
|
||||
self?.addButtonTapped(sender: centeredAddButton)
|
||||
}
|
||||
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
|
||||
self.centeredAddButton = centeredAddButton
|
||||
|
||||
// Hide the busy indicator
|
||||
|
||||
self.busyIndicator?.stopAnimating()
|
||||
|
||||
// Keep track of the tunnels manager
|
||||
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnelsManager.tunnelsListDelegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_: Bool) {
|
||||
// Remove selection when getting back to the list view on iPhone
|
||||
if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
|
||||
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,26 +142,6 @@ class TunnelsListTableViewController: UIViewController {
|
||||
self.present(settingsNC, animated: true)
|
||||
}
|
||||
|
||||
func openForEditing(configFileURL: URL) {
|
||||
let tunnelConfiguration: TunnelConfiguration?
|
||||
let name = configFileURL.deletingPathExtension().lastPathComponent
|
||||
do {
|
||||
let fileContents = try String(contentsOf: configFileURL)
|
||||
try tunnelConfiguration = WgQuickConfigFileParser.parse(fileContents, name: name)
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
tunnelConfiguration?.interface.name = name
|
||||
if let tunnelsManager = tunnelsManager {
|
||||
presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
} else {
|
||||
onTunnelsManagerReady = { [weak self] tunnelsManager in
|
||||
self?.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
@@ -168,77 +164,37 @@ class TunnelsListTableViewController: UIViewController {
|
||||
self.present(scanQRCodeNC, animated: true)
|
||||
}
|
||||
|
||||
func showErrorAlert(title: String, message: String) {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default)
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(okAction)
|
||||
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func importFromFile(url: URL) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
if (url.pathExtension == "zip") {
|
||||
var unarchivedFiles: [(fileName: String, contents: Data)] = []
|
||||
do {
|
||||
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
|
||||
} catch ZipArchiveError.cantOpenInputZipFile {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "The zip archive could not be read.")
|
||||
return
|
||||
} catch ZipArchiveError.badArchive {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "Bad or corrupt zip archive.")
|
||||
return
|
||||
} catch (let error) {
|
||||
showErrorAlert(title: "Unable to read zip archive", message: "Unexpected error: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
|
||||
for (i, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
|
||||
let fileBaseName = URL(string: unarchivedFile.fileName)?.deletingPathExtension().lastPathComponent
|
||||
if let trimmedName = fileBaseName?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmedName.isEmpty {
|
||||
unarchivedFiles[i].fileName = trimmedName
|
||||
} else {
|
||||
unarchivedFiles.remove(at: i)
|
||||
}
|
||||
}
|
||||
if (unarchivedFiles.isEmpty) {
|
||||
showErrorAlert(title: "No tunnels in zip archive", message: "No .conf tunnel files were found inside the zip archive.")
|
||||
return
|
||||
}
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
unarchivedFiles.sort { $0.fileName < $1.fileName }
|
||||
var lastFileName: String?
|
||||
var configs: [TunnelConfiguration] = []
|
||||
for file in unarchivedFiles {
|
||||
if file.fileName == lastFileName {
|
||||
continue
|
||||
}
|
||||
lastFileName = file.fileName
|
||||
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
|
||||
continue
|
||||
}
|
||||
configs.append(tunnelConfig)
|
||||
}
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs) { [weak self] (numberSuccessful) in
|
||||
if numberSuccessful == unarchivedFiles.count {
|
||||
ZipImporter.importConfigFiles(from: url) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
self?.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
|
||||
message: "Created \(numberSuccessful) of \(unarchivedFiles.count) tunnels from zip archive")
|
||||
let configs: [TunnelConfiguration?] = result.value!
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] (numberSuccessful) in
|
||||
if numberSuccessful == configs.count {
|
||||
return
|
||||
}
|
||||
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
|
||||
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
|
||||
from: self)
|
||||
}
|
||||
}
|
||||
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let fileContents = try? String(contentsOf: url),
|
||||
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration.")
|
||||
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
|
||||
message: "An error occured when importing the tunnel configuration.",
|
||||
from: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,8 +213,8 @@ extension TunnelsListTableViewController: UIDocumentPickerDelegate {
|
||||
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
|
||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
|
||||
completionHandler: (() -> Void)?) {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
|
||||
if let error = error {
|
||||
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
@@ -296,9 +252,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tunnelsManager.startDeactivation(of: tunnel) { [weak s] error in
|
||||
s?.showErrorAlert(title: "Deactivation error", message: "Error while bringing down tunnel: \(String(describing: error))")
|
||||
}
|
||||
tunnelsManager.startDeactivation(of: tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,12 +264,12 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
|
||||
tunnel: tunnel)
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
|
||||
}
|
||||
|
||||
@@ -339,7 +293,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
|
||||
// MARK: TunnelsManagerDelegate
|
||||
|
||||
extension TunnelsListTableViewController: TunnelsManagerDelegate {
|
||||
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
func tunnelAdded(at index: Int) {
|
||||
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
struct ActivateOnDemandSetting {
|
||||
var isActivateOnDemandEnabled: Bool
|
||||
var activateOnDemandOption: ActivateOnDemandOption
|
||||
}
|
||||
|
||||
enum ActivateOnDemandOption {
|
||||
case none // Valid only when isActivateOnDemandEnabled is false
|
||||
case useOnDemandOverWiFiOrCellular
|
||||
case useOnDemandOverWiFiOnly
|
||||
case useOnDemandOverCellularOnly
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
|
||||
let rules: [NEOnDemandRule]?
|
||||
let connectRule = NEOnDemandRuleConnect()
|
||||
let disconnectRule = NEOnDemandRuleDisconnect()
|
||||
switch (activateOnDemandOption) {
|
||||
case .none:
|
||||
rules = nil
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
rules = [connectRule]
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
connectRule.interfaceTypeMatch = .wiFi
|
||||
disconnectRule.interfaceTypeMatch = .cellular
|
||||
rules = [connectRule, disconnectRule]
|
||||
case .useOnDemandOverCellularOnly:
|
||||
connectRule.interfaceTypeMatch = .cellular
|
||||
disconnectRule.interfaceTypeMatch = .wiFi
|
||||
rules = [connectRule, disconnectRule]
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch (rules.count) {
|
||||
case 0:
|
||||
activateOnDemandOption = .none
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOrCellular
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if (connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular) {
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOnly
|
||||
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) {
|
||||
activateOnDemandOption = .useOnDemandOverCellularOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
self.activateOnDemandOption = activateOnDemandOption
|
||||
if (activateOnDemandOption == .none) {
|
||||
self.isActivateOnDemandEnabled = false
|
||||
} else {
|
||||
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import SystemConfiguration
|
||||
|
||||
class InternetReachability {
|
||||
|
||||
enum Status {
|
||||
case unknown
|
||||
case notReachable
|
||||
case reachableOverWiFi
|
||||
case reachableOverCellular
|
||||
}
|
||||
|
||||
static func currentStatus() -> Status {
|
||||
var status: Status = .unknown
|
||||
if let reachabilityRef = InternetReachability.reachabilityRef() {
|
||||
var flags = SCNetworkReachabilityFlags(rawValue: 0)
|
||||
SCNetworkReachabilityGetFlags(reachabilityRef, &flags)
|
||||
status = Status(reachabilityFlags: flags)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
private static func reachabilityRef() -> SCNetworkReachability? {
|
||||
let addrIn = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
|
||||
sin_family: sa_family_t(AF_INET),
|
||||
sin_port: 0,
|
||||
sin_addr: in_addr(s_addr: 0),
|
||||
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
|
||||
return withUnsafePointer(to: addrIn) { (addrInPtr) -> SCNetworkReachability? in
|
||||
addrInPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { (addrPtr) -> SCNetworkReachability? in
|
||||
return SCNetworkReachabilityCreateWithAddress(nil, addrPtr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InternetReachability.Status {
|
||||
init(reachabilityFlags flags: SCNetworkReachabilityFlags) {
|
||||
var status: InternetReachability.Status = .notReachable
|
||||
if (flags.contains(.reachable)) {
|
||||
if (flags.contains(.isWWAN)) {
|
||||
status = .reachableOverCellular
|
||||
} else {
|
||||
status = .reachableOverWiFi
|
||||
}
|
||||
}
|
||||
self = status
|
||||
}
|
||||
}
|
||||
@@ -5,33 +5,61 @@ import Foundation
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
protocol TunnelsManagerDelegate: class {
|
||||
protocol TunnelsManagerListDelegate: class {
|
||||
func tunnelAdded(at: Int)
|
||||
func tunnelModified(at: Int)
|
||||
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
|
||||
func tunnelRemoved(at: Int)
|
||||
}
|
||||
|
||||
enum TunnelActivationError: Error {
|
||||
case dnsResolutionFailed
|
||||
case tunnelActivationFailed
|
||||
case attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: TunnelStatus)
|
||||
case attemptingActivationWhenTunnelIsNotInactive
|
||||
case attemptingDeactivationWhenTunnelIsInactive
|
||||
protocol TunnelsManagerActivationDelegate: class {
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError)
|
||||
}
|
||||
|
||||
enum TunnelManagementError: Error {
|
||||
enum TunnelsManagerError: WireGuardAppError {
|
||||
// Tunnels list management
|
||||
case tunnelNameEmpty
|
||||
case tunnelAlreadyExistsWithThatName
|
||||
case tunnelInvalidName
|
||||
case vpnSystemErrorOnListingTunnels
|
||||
case vpnSystemErrorOnAddTunnel
|
||||
case vpnSystemErrorOnModifyTunnel
|
||||
case vpnSystemErrorOnRemoveTunnel
|
||||
|
||||
// Tunnel activation
|
||||
case tunnelActivationAttemptFailed // startTunnel() throwed
|
||||
case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed
|
||||
case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .tunnelNameEmpty:
|
||||
return ("No name provided", "Can't create tunnel with an empty name")
|
||||
case .tunnelAlreadyExistsWithThatName:
|
||||
return ("Name already exists", "A tunnel with that name already exists")
|
||||
case .vpnSystemErrorOnListingTunnels:
|
||||
return ("Unable to list tunnels", "Internal error")
|
||||
case .vpnSystemErrorOnAddTunnel:
|
||||
return ("Unable to create tunnel", "Internal error")
|
||||
case .vpnSystemErrorOnModifyTunnel:
|
||||
return ("Unable to modify tunnel", "Internal error")
|
||||
case .vpnSystemErrorOnRemoveTunnel:
|
||||
return ("Unable to remove tunnel", "Internal error")
|
||||
|
||||
case .tunnelActivationAttemptFailed:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case .tunnelActivationFailedInternalError:
|
||||
return ("Activation failure", "The tunnel could not be activated due to an internal error")
|
||||
case .tunnelActivationFailedNoInternetConnection:
|
||||
return ("Activation failure", "No internet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TunnelsManager {
|
||||
|
||||
private var tunnels: [TunnelContainer]
|
||||
weak var delegate: TunnelsManagerDelegate?
|
||||
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||
|
||||
private var isAddingTunnel: Bool = false
|
||||
private var isModifyingTunnel: Bool = false
|
||||
@@ -41,30 +69,33 @@ class TunnelsManager {
|
||||
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
}
|
||||
|
||||
static func create(completionHandler: @escaping (TunnelsManager?) -> Void) {
|
||||
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
// NETunnelProviderManager APIs don't work on the simulator
|
||||
completionHandler(TunnelsManager(tunnelProviders: []))
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: [])))
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
|
||||
if let error = error {
|
||||
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnListingTunnels))
|
||||
return
|
||||
}
|
||||
completionHandler(TunnelsManager(tunnelProviders: managers ?? []))
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) {
|
||||
func add(tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
|
||||
completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
||||
return
|
||||
}
|
||||
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,19 +105,21 @@ class TunnelsManager {
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isAddingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(nil, TunnelManagementError.vpnSystemErrorOnAddTunnel)
|
||||
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnAddTunnel))
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
s.tunnels.append(tunnel)
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
s.delegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(tunnel, nil)
|
||||
s.tunnelsListDelegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,17 +134,18 @@ class TunnelsManager {
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
self.add(tunnelConfiguration: head) { [weak self, tail] (_, error) in
|
||||
self.add(tunnelConfiguration: head) { [weak self, tail] (result) in
|
||||
DispatchQueue.main.async {
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (error == nil ? 1 : 0), completionHandler: completionHandler)
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, with tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
|
||||
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.interface.name
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,7 +156,7 @@ class TunnelsManager {
|
||||
var oldName: String?
|
||||
if (isNameChanged) {
|
||||
if self.tunnels.contains(where: { $0.name == tunnelName }) {
|
||||
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
|
||||
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
|
||||
return
|
||||
}
|
||||
oldName = tunnel.name
|
||||
@@ -132,11 +166,14 @@ class TunnelsManager {
|
||||
tunnelProviderManager.localizedDescription = tunnelName
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
let isActivatingOnDemand = (!tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled)
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] (error) in
|
||||
defer { self?.isModifyingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnModifyTunnel)
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
@@ -144,21 +181,35 @@ class TunnelsManager {
|
||||
let oldIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.sort { $0.name < $1.name }
|
||||
let newIndex = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.delegate?.tunnelMoved(at: oldIndex, to: newIndex)
|
||||
s.tunnelsListDelegate?.tunnelMoved(at: oldIndex, to: newIndex)
|
||||
}
|
||||
s.delegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
s.tunnelsListDelegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
|
||||
|
||||
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.beginRestart()
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
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 {
|
||||
os_log("Modify: Re-loading after saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
|
||||
return
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelManagementError?) -> Void) {
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
|
||||
isDeletingTunnel = true
|
||||
@@ -167,13 +218,13 @@ class TunnelsManager {
|
||||
defer { self?.isDeletingTunnel = false }
|
||||
guard (error == nil) else {
|
||||
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(TunnelManagementError.vpnSystemErrorOnRemoveTunnel)
|
||||
completionHandler(TunnelsManagerError.vpnSystemErrorOnRemoveTunnel)
|
||||
return
|
||||
}
|
||||
if let s = self {
|
||||
let index = s.tunnels.firstIndex(of: tunnel)!
|
||||
s.tunnels.remove(at: index)
|
||||
s.delegate?.tunnelRemoved(at: index)
|
||||
s.tunnelsListDelegate?.tunnelRemoved(at: index)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
@@ -187,57 +238,49 @@ class TunnelsManager {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
guard (tunnel.status == .inactive) else {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenTunnelIsNotInactive)
|
||||
return
|
||||
}
|
||||
for t in tunnels {
|
||||
if t.status != .inactive {
|
||||
completionHandler(TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: t.status))
|
||||
return
|
||||
}
|
||||
}
|
||||
tunnel.startActivation(completionHandler: completionHandler)
|
||||
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
||||
return self.tunnels.first(where: { $0.name == tunnelName })
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
|
||||
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
guard (tunnel.status == .inactive) else {
|
||||
return
|
||||
}
|
||||
|
||||
func _startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
tunnel.onActivationCommitted = { [weak self] (success) in
|
||||
if (!success) {
|
||||
let error = (InternetReachability.currentStatus() == .notReachable ?
|
||||
TunnelsManagerError.tunnelActivationFailedNoInternetConnection :
|
||||
TunnelsManagerError.tunnelActivationFailedInternalError)
|
||||
self?.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: error)
|
||||
}
|
||||
}
|
||||
tunnel.startActivation(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
|
||||
tunnel.status = .waiting
|
||||
tunnelInOperation.onDeactivationComplete = {
|
||||
_startActivation(of: tunnel, completionHandler: completionHandler)
|
||||
}
|
||||
startDeactivation(of: tunnelInOperation)
|
||||
} else {
|
||||
_startActivation(of: tunnel, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func startDeactivation(of tunnel: TunnelContainer) {
|
||||
if (tunnel.status == .inactive) {
|
||||
completionHandler(TunnelActivationError.attemptingDeactivationWhenTunnelIsInactive)
|
||||
return
|
||||
}
|
||||
tunnel.startDeactivation()
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
assert(!tunnelConfiguration.interface.name.isEmpty)
|
||||
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
|
||||
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [
|
||||
"tunnelConfiguration": serializedTunnelConfiguration,
|
||||
"tunnelConfigurationVersion": 1
|
||||
]
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
|
||||
if endpoints.count == 1 {
|
||||
serverAddress = endpoints.first!.stringRepresentation()
|
||||
} else if endpoints.isEmpty {
|
||||
serverAddress = "Unspecified"
|
||||
} else {
|
||||
serverAddress = "Multiple endpoints"
|
||||
func refreshStatuses() {
|
||||
for t in tunnels {
|
||||
t.refreshStatus()
|
||||
}
|
||||
username = tunnelConfiguration.interface.name
|
||||
}
|
||||
|
||||
func tunnelConfiguration() -> TunnelConfiguration? {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
|
||||
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,18 +288,29 @@ class TunnelContainer: NSObject {
|
||||
@objc dynamic var name: String
|
||||
@objc dynamic var status: TunnelStatus
|
||||
|
||||
@objc dynamic var isActivateOnDemandEnabled: Bool {
|
||||
didSet {
|
||||
if (isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isAttemptingActivation: Bool = false
|
||||
var onActivationCommitted: ((Bool) -> Void)?
|
||||
var onDeactivationComplete: (() -> Void)?
|
||||
|
||||
fileprivate let tunnelProvider: NETunnelProviderManager
|
||||
private var statusObservationToken: AnyObject?
|
||||
|
||||
private var dnsResolver: DNSResolver?
|
||||
|
||||
init(tunnel: NETunnelProviderManager) {
|
||||
self.name = tunnel.localizedDescription ?? "Unnamed"
|
||||
let status = TunnelStatus(from: tunnel.connection.status)
|
||||
self.status = status
|
||||
self.isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
||||
self.tunnelProvider = tunnel
|
||||
super.init()
|
||||
if (status != .inactive) {
|
||||
if (status != .inactive || isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
@@ -265,62 +319,39 @@ class TunnelContainer: NSObject {
|
||||
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
|
||||
}
|
||||
|
||||
fileprivate func startActivation(completionHandler: @escaping (Error?) -> Void) {
|
||||
assert(status == .inactive || status == .restarting)
|
||||
assert(self.dnsResolver == nil)
|
||||
func activateOnDemandSetting() -> ActivateOnDemandSetting {
|
||||
return ActivateOnDemandSetting(from: tunnelProvider)
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
let status = TunnelStatus(from: self.tunnelProvider.connection.status)
|
||||
self.status = status
|
||||
self.isActivateOnDemandEnabled = self.tunnelProvider.isOnDemandEnabled
|
||||
if (status != .inactive || isActivateOnDemandEnabled) {
|
||||
startObservingTunnelStatus()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func startActivation(completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
assert(status == .inactive || status == .restarting || status == .waiting)
|
||||
|
||||
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
|
||||
// Resolve DNS and start the tunnel
|
||||
let dnsResolver = DNSResolver(endpoints: endpoints)
|
||||
let resolvedEndpoints = dnsResolver.resolveWithoutNetworkRequests()
|
||||
if let resolvedEndpoints = resolvedEndpoints {
|
||||
// If we don't have to make a DNS network request, we never
|
||||
// change the status to .resolvingEndpointDomains
|
||||
startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
} else {
|
||||
status = .resolvingEndpointDomains
|
||||
self.dnsResolver = dnsResolver
|
||||
dnsResolver.resolve { [weak self] resolvedEndpoints in
|
||||
guard let s = self else { return }
|
||||
assert(s.status == .resolvingEndpointDomains)
|
||||
s.dnsResolver = nil
|
||||
guard let resolvedEndpoints = resolvedEndpoints else {
|
||||
s.status = .inactive
|
||||
completionHandler(TunnelActivationError.dnsResolutionFailed)
|
||||
return
|
||||
}
|
||||
s.startActivation(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
onDeactivationComplete = nil
|
||||
isAttemptingActivation = true
|
||||
startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func startActivation(recursionCount: UInt = 0,
|
||||
lastError: Error? = nil,
|
||||
tunnelConfiguration: TunnelConfiguration,
|
||||
resolvedEndpoints: [Endpoint?],
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
if (recursionCount >= 8) {
|
||||
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
|
||||
completionHandler(TunnelActivationError.tunnelActivationFailed)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// resolvedEndpoints should contain only IP addresses, not any named endpoints
|
||||
assert(resolvedEndpoints.allSatisfy { (resolvedEndpoint) in
|
||||
guard let resolvedEndpoint = resolvedEndpoint else { return true }
|
||||
switch (resolvedEndpoint.host) {
|
||||
case .ipv4: return true
|
||||
case .ipv6: return true
|
||||
case .name: return false
|
||||
}
|
||||
})
|
||||
|
||||
os_log("startActivation: Entering", log: OSLog.default, type: .debug)
|
||||
|
||||
guard (tunnelProvider.isEnabled) else {
|
||||
@@ -331,12 +362,12 @@ class TunnelContainer: NSObject {
|
||||
tunnelProvider.saveToPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -345,41 +376,35 @@ class TunnelContainer: NSObject {
|
||||
startObservingTunnelStatus()
|
||||
let session = (tunnelProvider.connection as! NETunnelProviderSession)
|
||||
do {
|
||||
os_log("startActivation: Generating options", log: OSLog.default, type: .debug)
|
||||
let tunnelOptions = PacketTunnelOptionsGenerator.generateOptions(
|
||||
from: tunnelConfiguration, withResolvedEndpoints: resolvedEndpoints)
|
||||
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
|
||||
try session.startTunnel(options: tunnelOptions)
|
||||
try session.startTunnel()
|
||||
os_log("startActivation: Success", log: OSLog.default, type: .debug)
|
||||
completionHandler(nil)
|
||||
} catch (let error) {
|
||||
os_log("startActivation: Error starting tunnel. Examining error", log: OSLog.default, type: .debug)
|
||||
guard let vpnError = error as? NEVPNError else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
os_log("Failed to activate tunnel: Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
os_log("Failed to activate tunnel: VPN Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
|
||||
status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
|
||||
os_log("startActivation: Error says: %{public}@", log: OSLog.default, type: .debug,
|
||||
vpnError.code == NEVPNError.configurationInvalid ? "Configuration invalid" : "Configuration stale")
|
||||
os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
|
||||
tunnelProvider.loadFromPreferences { [weak self] (error) in
|
||||
if (error != nil) {
|
||||
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
|
||||
os_log("startActivation: Error reloading tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
|
||||
self?.status = .inactive
|
||||
completionHandler(error)
|
||||
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
|
||||
return
|
||||
}
|
||||
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
|
||||
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
|
||||
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,6 +432,18 @@ class TunnelContainer: NSObject {
|
||||
object: connection,
|
||||
queue: nil) { [weak self] (_) in
|
||||
guard let s = self else { return }
|
||||
if (s.isAttemptingActivation) {
|
||||
if (connection.status == .connecting || connection.status == .connected) {
|
||||
// We tried to start the tunnel, and that attempt is on track to become succeessful
|
||||
s.onActivationCommitted?(true)
|
||||
s.onActivationCommitted = nil
|
||||
} else if (connection.status == .disconnecting || connection.status == .disconnected) {
|
||||
// We tried to start the tunnel, but that attempt didn't succeed
|
||||
s.onActivationCommitted?(false)
|
||||
s.onActivationCommitted = nil
|
||||
}
|
||||
s.isAttemptingActivation = false
|
||||
}
|
||||
if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
|
||||
// Don't change s.status when disconnecting for a restart
|
||||
if (connection.status == .disconnected) {
|
||||
@@ -414,13 +451,13 @@ class TunnelContainer: NSObject {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (s.status == .resolvingEndpointDomains && connection.status == .disconnected) {
|
||||
// Don't change to .inactive if we're still resolving endpoints
|
||||
return
|
||||
}
|
||||
s.status = TunnelStatus(from: connection.status)
|
||||
if (s.status == .inactive) {
|
||||
s.statusObservationToken = nil
|
||||
s.onDeactivationComplete?()
|
||||
s.onDeactivationComplete = nil
|
||||
if (!s.isActivateOnDemandEnabled) {
|
||||
s.statusObservationToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +471,7 @@ class TunnelContainer: NSObject {
|
||||
case reasserting // Not a possible state at present
|
||||
|
||||
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
|
||||
case resolvingEndpointDomains // DNS resolution in progress
|
||||
case waiting // Waiting for another tunnel to be brought down
|
||||
|
||||
init(from vpnStatus: NEVPNStatus) {
|
||||
switch (vpnStatus) {
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol WireGuardAppError: Error {
|
||||
func alertText() -> (/* title */ String, /* message */ String)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
enum WireGuardResult<T> {
|
||||
case success(T)
|
||||
case failure(WireGuardAppError)
|
||||
|
||||
var value: T? {
|
||||
switch (self) {
|
||||
case .success(let v): return v
|
||||
case .failure(_): return nil
|
||||
}
|
||||
}
|
||||
|
||||
var error: WireGuardAppError? {
|
||||
switch (self) {
|
||||
case .success(_): return nil
|
||||
case .failure(let e): return e
|
||||
}
|
||||
}
|
||||
|
||||
var isSuccess: Bool {
|
||||
switch (self) {
|
||||
case .success(_): return true
|
||||
case .failure(_): return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,21 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ZipArchiveError: Error {
|
||||
enum ZipArchiveError: WireGuardAppError {
|
||||
case cantOpenInputZipFile
|
||||
case cantOpenOutputZipFileForWriting
|
||||
case badArchive
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .cantOpenInputZipFile:
|
||||
return ("Unable to read zip archive", "The zip archive could not be read.")
|
||||
case .cantOpenOutputZipFileForWriting:
|
||||
return ("Unable to create zip archive", "Could not open zip file for writing.")
|
||||
case .badArchive:
|
||||
return ("Unable to read zip archive", "Bad or corrupt zip archive.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipArchive {
|
||||
@@ -48,7 +59,7 @@ class ZipArchive {
|
||||
throw ZipArchiveError.badArchive
|
||||
}
|
||||
|
||||
let bufferSize = 1024
|
||||
let bufferSize = 16384 // 16 kb
|
||||
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipExporterError: WireGuardAppError {
|
||||
case noTunnelsToExport
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .noTunnelsToExport:
|
||||
return ("Nothing to export", "There are no tunnels to export")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipExporter {
|
||||
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL,
|
||||
completion: @escaping (WireGuardAppError?) -> Void) {
|
||||
|
||||
guard (!tunnelConfigurations.isEmpty) else {
|
||||
completion(ZipExporterError.noTunnelsToExport)
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var inputsToArchiver: [(fileName: String, contents: Data)] = []
|
||||
var lastTunnelName: String = ""
|
||||
for tunnelConfiguration in tunnelConfigurations {
|
||||
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
|
||||
let name = tunnelConfiguration.interface.name
|
||||
if (name.isEmpty || name == lastTunnelName) { continue }
|
||||
inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
|
||||
lastTunnelName = name
|
||||
}
|
||||
}
|
||||
do {
|
||||
try ZipArchive.archive(inputs: inputsToArchiver, to: url)
|
||||
} catch (let error as WireGuardAppError) {
|
||||
DispatchQueue.main.async { completion(error) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum ZipImporterError: WireGuardAppError {
|
||||
case noTunnelsInZipArchive
|
||||
|
||||
func alertText() -> (String, String) {
|
||||
switch (self) {
|
||||
case .noTunnelsInZipArchive:
|
||||
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZipImporter {
|
||||
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var unarchivedFiles: [(fileName: String, contents: Data)]
|
||||
do {
|
||||
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
|
||||
|
||||
for (i, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
|
||||
let fileBaseName = URL(string: unarchivedFile.fileName)?.deletingPathExtension().lastPathComponent
|
||||
if let trimmedName = fileBaseName?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmedName.isEmpty {
|
||||
unarchivedFiles[i].fileName = trimmedName
|
||||
} else {
|
||||
unarchivedFiles.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
if (unarchivedFiles.isEmpty) {
|
||||
throw ZipImporterError.noTunnelsInZipArchive
|
||||
}
|
||||
} catch (let error as WireGuardAppError) {
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
return
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
unarchivedFiles.sort { $0.fileName < $1.fileName }
|
||||
var configs = Array<TunnelConfiguration?>(repeating: nil, count: unarchivedFiles.count)
|
||||
for (i, file) in unarchivedFiles.enumerated() {
|
||||
if (i > 0 && file == unarchivedFiles[i - 1]) {
|
||||
continue
|
||||
}
|
||||
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
|
||||
continue
|
||||
}
|
||||
configs[i] = tunnelConfig
|
||||
}
|
||||
DispatchQueue.main.async { completion(.success(configs)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-58
@@ -4,86 +4,61 @@
|
||||
import Network
|
||||
import Foundation
|
||||
|
||||
enum DNSResolverError: Error {
|
||||
case dnsResolutionFailed(hostnames: [String])
|
||||
}
|
||||
|
||||
class DNSResolver {
|
||||
let endpoints: [Endpoint?]
|
||||
let dispatchGroup: DispatchGroup
|
||||
var dispatchWorkItems: [DispatchWorkItem]
|
||||
static var cache = NSCache<NSString, NSString>()
|
||||
|
||||
init(endpoints: [Endpoint?]) {
|
||||
self.endpoints = endpoints
|
||||
self.dispatchWorkItems = []
|
||||
self.dispatchGroup = DispatchGroup()
|
||||
}
|
||||
|
||||
func resolveWithoutNetworkRequests() -> [Endpoint?]? {
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
|
||||
for endpoint in endpoints {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if (endpoint.hasHostAsIPAddress()) {
|
||||
resolvedEndpoints[i] = endpoint
|
||||
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
return nil
|
||||
if (!endpoint.hasHostAsIPAddress()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return resolvedEndpoints
|
||||
return true
|
||||
}
|
||||
|
||||
func resolve(completionHandler: @escaping ([Endpoint?]?) -> Void) {
|
||||
let endpoints = self.endpoints
|
||||
let dispatchGroup = self.dispatchGroup
|
||||
dispatchWorkItems = []
|
||||
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
|
||||
let dispatchGroup: DispatchGroup = DispatchGroup()
|
||||
|
||||
if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
|
||||
var isResolvedByDNSRequest: [Bool] = Array<Bool>(repeating: false, count: endpoints.count)
|
||||
for (i, endpoint) in self.endpoints.enumerated() {
|
||||
for (i, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint else { continue }
|
||||
if (endpoint.hasHostAsIPAddress()) {
|
||||
resolvedEndpoints[i] = endpoint
|
||||
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
|
||||
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
|
||||
resolvedEndpoints[i] = resolvedEndpointInCache
|
||||
} else {
|
||||
let workItem = DispatchWorkItem {
|
||||
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
|
||||
isResolvedByDNSRequest[i] = true
|
||||
}
|
||||
dispatchWorkItems.append(workItem)
|
||||
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for (i, endpoint) in endpoints.enumerated() {
|
||||
guard let endpoint = endpoint, let resolvedEndpoint = resolvedEndpoints[i] else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
if (isResolvedByDNSRequest[i]) {
|
||||
DNSResolver.cache.setObject(resolvedEndpoint.stringRepresentation() as NSString,
|
||||
forKey: endpoint.stringRepresentation() as NSString)
|
||||
|
||||
dispatchGroup.wait() // TODO: Timeout?
|
||||
|
||||
var hostnamesWithDnsResolutionFailure: [String] = []
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
for tuple in zip(endpoints, resolvedEndpoints) {
|
||||
let endpoint = tuple.0
|
||||
let resolvedEndpoint = tuple.1
|
||||
if let endpoint = endpoint {
|
||||
if (resolvedEndpoint == nil) {
|
||||
// DNS resolution failed
|
||||
guard let hostname = endpoint.hostname() else { fatalError() }
|
||||
hostnamesWithDnsResolutionFailure.append(hostname)
|
||||
}
|
||||
}
|
||||
let numberOfEndpointsToResolve = endpoints.compactMap { $0 }.count
|
||||
let numberOfResolvedEndpoints = resolvedEndpoints.compactMap { $0 }.count
|
||||
if (numberOfResolvedEndpoints < numberOfEndpointsToResolve) {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(resolvedEndpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
for workItem in dispatchWorkItems {
|
||||
workItem.cancel()
|
||||
if (!hostnamesWithDnsResolutionFailure.isEmpty) {
|
||||
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cancel()
|
||||
return resolvedEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
class ErrorNotifier {
|
||||
static func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
|
||||
switch (error) {
|
||||
case .savedProtocolConfigurationIsInvalid:
|
||||
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration")
|
||||
case .dnsResolutionFailure(_):
|
||||
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
|
||||
case .couldNotStartWireGuard:
|
||||
return ("Activation failure", "WireGuard backend could not be started")
|
||||
case .coultNotSetNetworkSettings:
|
||||
return ("Activation failure", "Error applying network settings on the tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
static func notify(_ error: PacketTunnelProviderError, from tunnelProvider: NEPacketTunnelProvider) {
|
||||
guard let (title, message) = ErrorNotifier.errorMessage(for: error) else { return }
|
||||
// displayMessage() is deprecated, but there's no better alternative to show the error to the user
|
||||
tunnelProvider.displayMessage("\(title): \(message)", completionHandler: { (_) in })
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -27,5 +29,7 @@
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||
</dict>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
enum PacketTunnelProviderError: Error {
|
||||
case invalidOptions
|
||||
case savedProtocolConfigurationIsInvalid
|
||||
case dnsResolutionFailure(hostnames: [String])
|
||||
case couldNotStartWireGuard
|
||||
case coultNotSetNetworkSettings
|
||||
}
|
||||
|
||||
private var logFileHandle: FileHandle?
|
||||
|
||||
/// A packet tunnel provider object.
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
@@ -22,110 +26,81 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
/// Begin the process of establishing the tunnel.
|
||||
override func startTunnel(options: [String: NSObject]?,
|
||||
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
os_log("Starting tunnel", log: OSLog.default, type: .info)
|
||||
|
||||
guard let options = options else {
|
||||
os_log("Starting tunnel failed: No options passed. Possible connection request from preferences", log: OSLog.default, type: .error)
|
||||
// displayMessage is deprecated API
|
||||
displayMessage("Please use the WireGuard app to start WireGuard tunnels") { (_) in
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let interfaceName = options[.interfaceName] as? String,
|
||||
let wireguardSettings = options[.wireguardSettings] as? String,
|
||||
let remoteAddress = options[.remoteAddress] as? String,
|
||||
let dnsServers = options[.dnsServers] as? [String],
|
||||
let mtu = options[.mtu] as? NSNumber,
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Addresses = options[.ipv4Addresses] as? [String],
|
||||
let ipv4SubnetMasks = options[.ipv4SubnetMasks] as? [String],
|
||||
let ipv4IncludedRouteAddresses = options[.ipv4IncludedRouteAddresses] as? [String],
|
||||
let ipv4IncludedRouteSubnetMasks = options[.ipv4IncludedRouteSubnetMasks] as? [String],
|
||||
let ipv4ExcludedRouteAddresses = options[.ipv4ExcludedRouteAddresses] as? [String],
|
||||
let ipv4ExcludedRouteSubnetMasks = options[.ipv4ExcludedRouteSubnetMasks] as? [String],
|
||||
|
||||
// IPv6 settings
|
||||
let ipv6Addresses = options[.ipv6Addresses] as? [String],
|
||||
let ipv6NetworkPrefixLengths = options[.ipv6NetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6IncludedRouteAddresses = options[.ipv6IncludedRouteAddresses] as? [String],
|
||||
let ipv6IncludedRouteNetworkPrefixLengths = options[.ipv6IncludedRouteNetworkPrefixLengths] as? [NSNumber],
|
||||
let ipv6ExcludedRouteAddresses = options[.ipv6ExcludedRouteAddresses] as? [String],
|
||||
let ipv6ExcludedRouteNetworkPrefixLengths = options[.ipv6ExcludedRouteNetworkPrefixLengths] as? [NSNumber]
|
||||
|
||||
else {
|
||||
os_log("Starting tunnel failed: Invalid options passed", log: OSLog.default, type: .error)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
|
||||
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
|
||||
}
|
||||
|
||||
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
|
||||
|
||||
// Configure logging
|
||||
configureLogger()
|
||||
|
||||
wg_log(.info, message: "WireGuard for iOS version \(appVersion())")
|
||||
wg_log(.info, message: "WireGuard Go backend version \(goBackendVersion())")
|
||||
wg_log(.info, message: "Tunnel interface name: \(tunnelConfiguration.interface.name)")
|
||||
|
||||
wg_log(.info, staticMessage: "Starting tunnel")
|
||||
|
||||
// Resolve endpoint domains
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
|
||||
var resolvedEndpoints: [Endpoint?] = []
|
||||
do {
|
||||
resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints)
|
||||
} catch DNSResolverError.dnsResolutionFailed(let hostnames) {
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
|
||||
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames), from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
|
||||
return
|
||||
} catch {
|
||||
// There can be no other errors from DNSResolver.resolveSync()
|
||||
fatalError()
|
||||
}
|
||||
assert(endpoints.count == resolvedEndpoints.count)
|
||||
|
||||
// Setup packetTunnelSettingsGenerator
|
||||
|
||||
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration,
|
||||
resolvedEndpoints: resolvedEndpoints)
|
||||
|
||||
// Bring up wireguard-go backend
|
||||
|
||||
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
|
||||
if fd < 0 {
|
||||
os_log("Starting tunnel failed: Could not determine file descriptor", log: OSLog.default, type: .error)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
|
||||
return
|
||||
}
|
||||
let handle = connect(interfaceName: interfaceName, settings: wireguardSettings, fd: fd)
|
||||
|
||||
let wireguardSettings = packetTunnelSettingsGenerator.generateWireGuardSettings()
|
||||
let handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fd: fd)
|
||||
|
||||
if handle < 0 {
|
||||
os_log("Starting tunnel failed: Could not start WireGuard", log: OSLog.default, type: .error)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
|
||||
return
|
||||
}
|
||||
|
||||
wgHandle = handle
|
||||
|
||||
// Network settings
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
|
||||
|
||||
// IPv4 settings
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
|
||||
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
|
||||
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
|
||||
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
// IPv6 settings
|
||||
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
|
||||
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
// DNS
|
||||
networkSettings.dnsSettings = NEDNSSettings(servers: dnsServers)
|
||||
|
||||
// MTU
|
||||
if (mtu == 0) {
|
||||
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
|
||||
networkSettings.tunnelOverheadBytes = 80
|
||||
} else {
|
||||
networkSettings.mtu = mtu
|
||||
}
|
||||
// Apply network settings
|
||||
|
||||
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
|
||||
setTunnelNetworkSettings(networkSettings) { (error) in
|
||||
if let error = error {
|
||||
os_log("Starting tunnel failed: Error setting network settings: %s", log: OSLog.default, type: .error, error.localizedDescription)
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
|
||||
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
|
||||
ErrorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings, from: self)
|
||||
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
|
||||
} else {
|
||||
startTunnelCompletionHandler(nil /* No errors */)
|
||||
@@ -135,14 +110,34 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
/// Begin the process of stopping the tunnel.
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
os_log("Stopping tunnel", log: OSLog.default, type: .info)
|
||||
wg_log(.info, staticMessage: "Stopping tunnel")
|
||||
if let handle = wgHandle {
|
||||
wgTurnOff(handle)
|
||||
}
|
||||
if let fileHandle = logFileHandle {
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
private func configureLogger() {
|
||||
|
||||
// Setup writing the log to a file
|
||||
if let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL {
|
||||
let fileManager = FileManager.default
|
||||
let filePath = networkExtensionLogFileURL.path
|
||||
fileManager.createFile(atPath: filePath, contents: nil) // Create the file if it doesn't already exist
|
||||
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
|
||||
logFileHandle = fileHandle
|
||||
} else {
|
||||
os_log("Can't open log file for writing. Log is not saved to file.", log: OSLog.default, type: .error)
|
||||
logFileHandle = nil
|
||||
}
|
||||
} else {
|
||||
os_log("Can't obtain log file URL. Log is not saved to file.", log: OSLog.default, type: .error)
|
||||
}
|
||||
|
||||
// Setup WireGuard logger
|
||||
wgSetLogger { (level, msgCStr) in
|
||||
let logType: OSLogType
|
||||
switch level {
|
||||
@@ -156,7 +151,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
logType = .default
|
||||
}
|
||||
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
|
||||
os_log("%{public}s", log: OSLog.default, type: logType, msg)
|
||||
wg_log(logType, message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +160,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return wgTurnOn(nameGoStr, settingsGoStr, fd)
|
||||
}
|
||||
}
|
||||
|
||||
func appVersion() -> String {
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
return appVersion
|
||||
}
|
||||
|
||||
func goBackendVersion() -> String {
|
||||
return WIREGUARD_GO_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R {
|
||||
@@ -176,3 +183,34 @@ private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
|
||||
// Write to os log
|
||||
os_log(msg, log: OSLog.default, type: type)
|
||||
// Write to file log
|
||||
let msgString: String = msg.withUTF8Buffer { (ptr: UnsafeBufferPointer<UInt8>) -> String in
|
||||
return String(decoding: ptr, as: UTF8.self)
|
||||
}
|
||||
file_log(type: type, message: msgString)
|
||||
}
|
||||
|
||||
private func wg_log(_ type: OSLogType, message msg: String) {
|
||||
// Write to os log
|
||||
os_log("%{public}s", log: OSLog.default, type: type, msg)
|
||||
// Write to file log
|
||||
file_log(type: type, message: msg)
|
||||
}
|
||||
|
||||
private func file_log(type: OSLogType, message: String) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS: "
|
||||
var msgLine = formatter.string(from: Date()) + message
|
||||
if (msgLine.last! != "\n") {
|
||||
msgLine.append("\n")
|
||||
}
|
||||
let data = msgLine.data(using: .utf8)
|
||||
if let data = data, let logFileHandle = logFileHandle {
|
||||
logFileHandle.write(data)
|
||||
logFileHandle.synchronizeFile()
|
||||
}
|
||||
}
|
||||
|
||||
+65
-38
@@ -3,29 +3,30 @@
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
|
||||
class PacketTunnelOptionsGenerator {
|
||||
static func generateOptions(from tc: TunnelConfiguration,
|
||||
withResolvedEndpoints resolvedEndpoints: [Endpoint?]) -> [String: NSObject] {
|
||||
var options: [String: NSObject] = [:]
|
||||
class PacketTunnelSettingsGenerator {
|
||||
|
||||
// Interface name
|
||||
let tunnelConfiguration: TunnelConfiguration
|
||||
let resolvedEndpoints: [Endpoint?]
|
||||
|
||||
options[.interfaceName] = tc.interface.name as NSObject
|
||||
|
||||
// WireGuard settings
|
||||
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
|
||||
self.tunnelConfiguration = tunnelConfiguration
|
||||
self.resolvedEndpoints = resolvedEndpoints
|
||||
}
|
||||
|
||||
func generateWireGuardSettings() -> String {
|
||||
var wgSettings = ""
|
||||
let privateKey = tc.interface.privateKey.hexEncodedString()
|
||||
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
|
||||
wgSettings.append("private_key=\(privateKey)\n")
|
||||
if let listenPort = tc.interface.listenPort {
|
||||
if let listenPort = tunnelConfiguration.interface.listenPort {
|
||||
wgSettings.append("listen_port=\(listenPort)\n")
|
||||
}
|
||||
if (tc.peers.count > 0) {
|
||||
if (tunnelConfiguration.peers.count > 0) {
|
||||
wgSettings.append("replace_peers=true\n")
|
||||
}
|
||||
assert(tc.peers.count == resolvedEndpoints.count)
|
||||
for (i, peer) in tc.peers.enumerated() {
|
||||
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
|
||||
for (i, peer) in tunnelConfiguration.peers.enumerated() {
|
||||
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
|
||||
@@ -43,8 +44,10 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
return wgSettings
|
||||
}
|
||||
|
||||
options[.wireguardSettings] = wgSettings as NSObject
|
||||
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
|
||||
|
||||
// Remote address
|
||||
|
||||
@@ -67,15 +70,24 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.remoteAddress] = remoteAddress as NSObject
|
||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
|
||||
|
||||
// DNS
|
||||
|
||||
options[.dnsServers] = tc.interface.dns.map { $0.stringRepresentation() } as NSObject
|
||||
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() }
|
||||
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
|
||||
dnsSettings.matchDomains = [""] // All DNS queries must first go through the VPN's DNS
|
||||
networkSettings.dnsSettings = dnsSettings
|
||||
|
||||
// MTU
|
||||
|
||||
options[.mtu] = NSNumber(value: tc.interface.mtu ?? 0) // 0 implies auto-MTU
|
||||
let mtu = tunnelConfiguration.interface.mtu ?? 0
|
||||
if (mtu == 0) {
|
||||
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
|
||||
networkSettings.tunnelOverheadBytes = 80
|
||||
} else {
|
||||
networkSettings.mtu = NSNumber(value: mtu)
|
||||
}
|
||||
|
||||
// Addresses from interface addresses
|
||||
|
||||
@@ -85,22 +97,16 @@ class PacketTunnelOptionsGenerator {
|
||||
var ipv6Addresses: [String] = []
|
||||
var ipv6NetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for addressRange in tc.interface.addresses {
|
||||
for addressRange in tunnelConfiguration.interface.addresses {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4Addresses.append("\(addressRange.address)")
|
||||
ipv4SubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
ipv4SubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6Addresses.append("\(addressRange.address)")
|
||||
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4Addresses] = ipv4Addresses as NSObject
|
||||
options[.ipv4SubnetMasks] = ipv4SubnetMasks as NSObject
|
||||
|
||||
options[.ipv6Addresses] = ipv6Addresses as NSObject
|
||||
options[.ipv6NetworkPrefixLengths] = ipv6NetworkPrefixLengths as NSObject
|
||||
|
||||
// Included routes from AllowedIPs
|
||||
|
||||
var ipv4IncludedRouteAddresses: [String] = []
|
||||
@@ -109,11 +115,11 @@ class PacketTunnelOptionsGenerator {
|
||||
var ipv6IncludedRouteAddresses: [String] = []
|
||||
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
|
||||
|
||||
for peer in tc.peers {
|
||||
for peer in tunnelConfiguration.peers {
|
||||
for addressRange in peer.allowedIPs {
|
||||
if (addressRange.address is IPv4Address) {
|
||||
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv4IncludedRouteSubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
|
||||
ipv4IncludedRouteSubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
|
||||
} else if (addressRange.address is IPv6Address) {
|
||||
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
|
||||
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
|
||||
@@ -121,12 +127,6 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4IncludedRouteAddresses] = ipv4IncludedRouteAddresses as NSObject
|
||||
options[.ipv4IncludedRouteSubnetMasks] = ipv4IncludedRouteSubnetMasks as NSObject
|
||||
|
||||
options[.ipv6IncludedRouteAddresses] = ipv6IncludedRouteAddresses as NSObject
|
||||
options[.ipv6IncludedRouteNetworkPrefixLengths] = ipv6IncludedRouteNetworkPrefixLengths as NSObject
|
||||
|
||||
// Excluded routes from endpoints
|
||||
|
||||
var ipv4ExcludedRouteAddresses: [String] = []
|
||||
@@ -149,13 +149,40 @@ class PacketTunnelOptionsGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
options[.ipv4ExcludedRouteAddresses] = ipv4ExcludedRouteAddresses as NSObject
|
||||
options[.ipv4ExcludedRouteSubnetMasks] = ipv4ExcludedRouteSubnetMasks as NSObject
|
||||
// Apply IPv4 settings
|
||||
|
||||
options[.ipv6ExcludedRouteAddresses] = ipv6ExcludedRouteAddresses as NSObject
|
||||
options[.ipv6ExcludedRouteNetworkPrefixLengths] = ipv6ExcludedRouteNetworkPrefixLengths as NSObject
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
|
||||
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
|
||||
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
|
||||
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
|
||||
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
|
||||
}
|
||||
networkSettings.ipv4Settings = ipv4Settings
|
||||
|
||||
return options
|
||||
// Apply IPv6 settings
|
||||
|
||||
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
|
||||
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
|
||||
* very bad, if various network parameters were actually relying on that subnet being
|
||||
* intentionally small. TODO: talk about this with upstream iOS devs.
|
||||
*/
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
|
||||
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
|
||||
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
|
||||
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
|
||||
}
|
||||
networkSettings.ipv6Settings = ipv6Settings
|
||||
|
||||
// Done
|
||||
|
||||
return networkSettings
|
||||
}
|
||||
|
||||
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
|
||||
@@ -1 +1,2 @@
|
||||
#include "../../wireguard-go-bridge/wireguard.h"
|
||||
#include "wireguard-go-version.h"
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -46,12 +48,54 @@ func (l *CLogger) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var tunnelHandles map[int32]*Device
|
||||
type DeviceState struct {
|
||||
device *Device
|
||||
logger *Logger
|
||||
endpointsTimer *time.Timer
|
||||
endpointsSettings string
|
||||
}
|
||||
|
||||
var tunnelHandles map[int32]*DeviceState
|
||||
|
||||
func listenForRouteChanges() {
|
||||
//TODO: replace with NWPathMonitor
|
||||
data := make([]byte, os.Getpagesize())
|
||||
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
n, err := unix.Read(routeSocket, data)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if n < 4 {
|
||||
continue
|
||||
}
|
||||
for _, deviceState := range tunnelHandles {
|
||||
if deviceState.endpointsTimer == nil {
|
||||
deviceState.endpointsTimer = time.AfterFunc(time.Second, func() {
|
||||
deviceState.endpointsTimer = nil
|
||||
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(deviceState.endpointsSettings)), bufio.NewWriter(ioutil.Discard))
|
||||
deviceState.logger.Info.Println("Setting endpoints for re-resolution due to network change")
|
||||
err := ipcSetOperation(deviceState.device, bufferedSettings)
|
||||
if err != nil {
|
||||
deviceState.logger.Error.Println(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionString = C.CString(WireGuardGoVersion)
|
||||
roamingDisabled = true
|
||||
tunnelHandles = make(map[int32]*Device)
|
||||
tunnelHandles = make(map[int32]*DeviceState)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, unix.SIGUSR2)
|
||||
go func() {
|
||||
@@ -67,6 +111,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
go listenForRouteChanges()
|
||||
}
|
||||
|
||||
//export wgSetLogger
|
||||
@@ -74,6 +119,32 @@ func wgSetLogger(loggerFn uintptr) {
|
||||
loggerFunc = unsafe.Pointer(loggerFn)
|
||||
}
|
||||
|
||||
func extractEndpointFromSettings(settings string) string {
|
||||
var b strings.Builder
|
||||
pubkey := ""
|
||||
endpoint := ""
|
||||
listenPort := "listen_port=0"
|
||||
for _, line := range strings.Split(settings, "\n") {
|
||||
if strings.HasPrefix(line, "listen_port=") {
|
||||
listenPort = line
|
||||
} else if strings.HasPrefix(line, "public_key=") {
|
||||
if pubkey != "" && endpoint != "" {
|
||||
b.WriteString(pubkey + "\n" + endpoint + "\n")
|
||||
}
|
||||
pubkey = line
|
||||
} else if strings.HasPrefix(line, "endpoint=") {
|
||||
endpoint = line
|
||||
} else if line == "remove=true" {
|
||||
pubkey = ""
|
||||
endpoint = ""
|
||||
}
|
||||
}
|
||||
if pubkey != "" && endpoint != "" {
|
||||
b.WriteString(pubkey + "\n" + endpoint + "\n")
|
||||
}
|
||||
return listenPort + "\n" + b.String()
|
||||
}
|
||||
|
||||
//export wgTurnOn
|
||||
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
|
||||
interfaceName := string([]byte(ifnameRef))
|
||||
@@ -113,18 +184,27 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
|
||||
if i == math.MaxInt32 {
|
||||
return -1
|
||||
}
|
||||
tunnelHandles[i] = device
|
||||
tunnelHandles[i] = &DeviceState{
|
||||
device: device,
|
||||
logger: logger,
|
||||
endpointsSettings: extractEndpointFromSettings(settings),
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
//export wgTurnOff
|
||||
func wgTurnOff(tunnelHandle int32) {
|
||||
device, ok := tunnelHandles[tunnelHandle]
|
||||
deviceState, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(tunnelHandles, tunnelHandle)
|
||||
device.Close()
|
||||
t := deviceState.endpointsTimer
|
||||
if t != nil {
|
||||
deviceState.endpointsTimer = nil
|
||||
t.Stop()
|
||||
}
|
||||
deviceState.device.Close()
|
||||
}
|
||||
|
||||
//export wgVersion
|
||||
|
||||
Reference in New Issue
Block a user