Compare commits

..
Author SHA1 Message Date
Jason A. Donenfeld 2fa2f58aad Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 23:58:53 +01:00
Jason A. Donenfeld 1eddb2c86d PacketTunnelProvider: Show log timestamp
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 23:56:26 +01:00
Jason A. Donenfeld ddccd6da4c wireguard-go-bridge: account for network changes
Everytime the network changes, we need to recreate the UDP socket,
because the ephemeral listen port is tied to the old physical interface.
As well, we need to re-set the IP addresses for each endpoint, so that
they're passed to getaddrinfo and are then resolved using DNS46.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 21:50:19 +01:00
Roopesh Chander aa0b288436 Zip: Increase size of buffer used to read data from the archive
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:55 +05:30
Roopesh Chander 71568d1899 Settings: Export log: Perform file operations in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:55 +05:30
Roopesh Chander 06a783f50a On-Demand: TunnelViewModel: Make activate-on-demand methods static
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:23 +05:30
Roopesh Chander 465c22f769 On-Demand: Move detail text to TunnelViewModel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:15 +05:30
Jason A. Donenfeld b7e5638681 Plist: Handle crypto export
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 18:52:14 +01:00
Jason A. Donenfeld 66b4aedd08 Make strings consistent
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 18:52:14 +01:00
Roopesh Chander 105eca7adc State restoration: Restore tunnel detail view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 19:05:08 +05:30
Roopesh Chander e3801308cb Main VC: No need to refresh statuses if the tunnelsManager isn't initialized yet
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 15:52:47 +05:30
Roopesh Chander 05d0429c50 Tunnels list: Deselect rows correctly
Do it like UITableViewController.clearsSelectionOnViewWillAppear would.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:23 +05:30
Roopesh Chander fa9a4921a8 Settings: Exporting configs as zip should open document picker
Because:
- Exporting UI should be consistent with importing UI
- UIActivityVC takes a long time to open

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander c827a00307 Error handling: Use ErrorPresenter.showErrorAlert() instead of per-VC showErrorAlert() methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander dcfa9473e9 Error handling: Use WireGuardAppError and WireGuardResult throughout the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 782dd2ea4e Error handling: Introduce a WireGuardResult type to handle errors in callbacks across the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander c9267ba634 Error handling: Introduce a WireGuardAppError protocol to manage errors
The alert strings shall be located next to where the errors are declared.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 8d26a3c536 Error handling: Cleanup Tunnels Manager errors
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 7631844fbe Error presenter: Always handle the passed error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 046b540e53 Tunnel detail: Cell status switch should be toggled only after the alert presentation completes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander f6faffa4c1 Refactoring: Consolidate file deletion into a separate function
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 290bd192a0 NE: Logging: Log file should begin with version numbers and tunnel name
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander bf86731879 NE: Logging: Make it clear which calls to wg_log use String and which use StaticString
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-04 15:58:53 +05:30
Roopesh Chander 4e386e85e4 Settings: Add timestamp to exported log
And remove the exported log afterwards.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-04 14:12:53 +05:30
Roopesh Chander 046d1413ec Refactor out VPN-handling stuff from tunnels list VC to the main VC
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-03 18:51:51 +05:30
Roopesh Chander e1b258353c VPN: Error out when tunnel activation fails because there's no internet
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-03 18:51:42 +05:30
Roopesh Chander cdd132d7d0 Settings: Export log file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-02 13:26:53 +05:30
Roopesh Chander 679d63294d NE: Write log to file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-30 00:36:33 +05:30
Roopesh Chander d01d46fde8 Info.plist: Add app group id for accessing from both the app and the network extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-29 14:05:27 +05:30
Roopesh Chander a3bc306b6e Xcode: Add app groups capability
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-29 12:01:45 +05:30
Roopesh Chander 3e1772ccd0 It's 'WiFi', not 'Wifi'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-28 12:41:35 +05:30
Roopesh Chander b946cbc0f3 NE: All DNS queries must first go through the VPN's DNS servers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-23 13:04:55 +05:30
Roopesh Chander 8590a8804a Tunnel view model: Invalidate the configuration object when updating allowedIPs using the 'Exclude private IPs' switch
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-19 15:22:27 +05:30
Roopesh Chander a117e3ae3e Config file parser: Be case-insensitive to attribute keys in the config file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-19 12:37:54 +05:30
Roopesh Chander ef2012330f Config file parser: Fix typo
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-17 22:52:37 +05:30
Roopesh Chander 6bb25782c1 Exporting: Export to zip in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander 1dac181803 Exporting: Refactor out zip exporting into a separate class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander d556729705 Exporting: No need to check for duplicate names - we disallow it at creation time itself
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander b1ae13652c Importing: Import from zip in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander 203d6b4596 Importing: Refactor out zip importing into a separate class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-14 18:52:10 +05:30
Roopesh Chander 2676ee0169 Tunnels manager: After saving after activating on-demand, reload tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-14 13:02:53 +05:30
Roopesh Chander c60c29b93c Tunnels manager: Need to keep VPN-on-demand tunnels's status under observation
Because they can turn on automatically, even while the app is in the foreground.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander bdb2411ebc Tunnel detail: Show VPN-on-demand information
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander 923d039a78 Tunnels manager: Keep track of NETunnelProviderManager's isOnDemandEnabled property
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander abb0312d38 Tunnel edit: Update for VPN-on-demand changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander f1b8de7312 Tunnel view model: VPN-on-demand stuff shouldn't be part of the tunnel model
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander cc122d7463 Model, Tunnels manager: Rewrite the model for VPN-on-demand
The VPN-on-demand settings should not be part of the tunnel
configuration. Rather, the onDemandRules stored in the
tunnel provider configuration serve as the one place
where the VPN-on-demand settings are stored.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander 39a067cb96 TunnelsManager: Support for on-demand rules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:31:38 +05:30
Roopesh Chander c5e8b05e8b Tunnel edit, Tunnel view model: UI for providing On-Demand activation options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:18:36 +05:30
Roopesh Chander 4b7094d652 Model: Add activationType to tunnel configuration
We make sure existing tunnel serializations can be deserialized correctly.

We also bump up the tunnelConfigurationVersion, because the tunnel
configuration contents have changed.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:17:36 +05:30
Roopesh Chander 0f03ffc920 Model: ActivityType enum to represent VPN-on-demand options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 19:17:24 +05:30
Roopesh Chander 1502bd42d3 Model: TunnelConfiguration: Add explicit conformance to Decodable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 19:01:34 +05:30
Roopesh Chander 290f83d5ef Model: Ensure that a TunnelConfiguration always has a valid array of peers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 17:02:30 +05:30
Roopesh Chander e8d68396ca VPN: When activating while another tunnel is active, deactivate the other tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 8e7bfb15ed TunnelsManager: startDeactivation() need not take a completion handler
Because the completion handler pattern doesn't fit in this case.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 95456ec956 VPN: There are no DNS errors to handle in the app now
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 7485474c4c NE: Minor refactoring to enable calling startTunnel() with a tunnelConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 22:29:52 +05:30
Roopesh Chander 1d63509b92 VPN: Refresh tunnel statuses when app gets to the foreground
Because the tunnel could've be activated from iOS Settings now

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:29:34 +05:30
Roopesh Chander f3e32ab737 Remove unused code
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:20:33 +05:30
Roopesh Chander 59b9a6e5d2 TunnelsManager: Ability to refresh connection statuses
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:19:32 +05:30
Roopesh Chander 3136fe0e2c NE: When there's an error starting the tunnel, show it to the user using displayMessage()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 17:07:42 +05:30
Roopesh Chander a1070d2b29 Remove unused file PacketTunnelOptionKey.swift
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 18:59:16 +05:30
Roopesh Chander 5ee4d392b5 Move logic to extension: Bring up the tunnel from the stored providerConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 18:59:16 +05:30
Roopesh Chander c17e4a27a2 DNSResolver: Simplify
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 8409b7e929 DNSResolver: Let's not cache DNS resolution results anymore
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 651ffa0c51 DNSResolver: DNS resolution can now happen synchronously
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 4404bb2b7d Model: Endpoint.hostname()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander e66cf5264a Move logic to extension: NETunnelProviderProtocol extension code should be shared
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:01 +05:30
Roopesh Chander af58bfcb00 Move logic to extension: Refactor PacketTunnelOptionsGenerator into a PacketTunnelSettingsGenerator
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:44:13 +05:30
Roopesh Chander 2f7e437202 Move logic to extension: Move DNSResolver to extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:09:45 +05:30
Roopesh Chander 62573e2ad7 Move logic to extension: .resolvingEndpointDomains is not longer a valid status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:08:57 +05:30
Roopesh Chander a5f9dc4821 Move logic to extension: DNS resolution no longer happens in the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:04:12 +05:30
Roopesh Chander 7827147bc8 Move logic to extension: Include shared model code when building the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:50:05 +05:30
Roopesh Chander a473dfe4f8 Model: Move InterfaceConfiguration.publicKey to Curve25519.swift
The code for public key calculation need not be shared with the extension

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:48:25 +05:30
Roopesh Chander fb6a7f6007 Move logic to extension: Move PacketTunnelOptionsGenerator to the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:16:30 +05:30
Roopesh Chander f7b04b0b5d Move logic to extension: Invoke startTunnel() without any options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:15:01 +05:30
Roopesh Chander c88c660b51 Move logic to extension: Move model files to Shared
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 13:56:50 +05:30
Roopesh Chander ec2d67ea00 Tunnel edit: While preparing for reuse, should make onValueBeingEdited nil as well
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 12:25:36 +05:30
37 changed files with 1470 additions and 661 deletions
@@ -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
}
}
@@ -4,11 +4,18 @@
import Foundation import Foundation
@available(OSX 10.14, iOS 12.0, *) @available(OSX 10.14, iOS 12.0, *)
class TunnelConfiguration: Codable { final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration var interface: InterfaceConfiguration
var peers: [PeerConfiguration] = [] let peers: [PeerConfiguration]
init(interface: InterfaceConfiguration) { init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface 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 mtu: UInt16?
var dns: [DNSServer] = [] var dns: [DNSServer] = []
var publicKey: Data {
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
}
init(name: String, privateKey: Data) { init(name: String, privateKey: Data) {
self.name = name self.name = name
self.privateKey = privateKey self.privateKey = privateKey
@@ -87,4 +87,15 @@ extension Endpoint {
return true 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
}
}
}
+79 -14
View File
@@ -9,12 +9,12 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; }; 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.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 */; }; 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, ); }; }; 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 */; }; 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; }; 6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */; };
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; }; 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; }; 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.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 */; }; 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; }; 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.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 */; }; 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -80,14 +93,16 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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 /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionsGenerator.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -152,9 +173,12 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */, 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */,
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */,
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
6F5D0C1E218352EF000F85AD /* Info.plist */, 6F5D0C1E218352EF000F85AD /* Info.plist */,
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */, 6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */,
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */, 6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */,
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */,
); );
path = WireGuardNetworkExtension; path = WireGuardNetworkExtension;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -162,7 +186,9 @@
6F5D0C432183B4A4000F85AD /* Shared */ = { 6F5D0C432183B4A4000F85AD /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */, 6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -227,8 +253,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */, 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6F5D0C1421832391000F85AD /* DNSResolver.swift */, 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */, 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */,
); );
path = VPN; path = VPN;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -255,6 +281,8 @@
6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = { 6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6FE254FA219C10800028284D /* ZipImporter.swift */,
6FE254FE219C60290028284D /* ZipExporter.swift */,
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */, 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */,
6FDEF7F421863B6100D8FBF6 /* 3rdparty */, 6FDEF7F421863B6100D8FBF6 /* 3rdparty */,
); );
@@ -308,10 +336,11 @@
6F919ED3218C65C50023B400 /* Resources */, 6F919ED3218C65C50023B400 /* Resources */,
6F6899A32180445A0012E523 /* Crypto */, 6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */, 6F6899AA218099D00012E523 /* ConfigFile */,
6F7774E6217201E0006A79B3 /* Model */,
6F7774DD217181B1006A79B3 /* UI */, 6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* VPN */, 6F7774ED21722D0C006A79B3 /* VPN */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */, 6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */, 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
6FF4AC1E211EC472002C96EB /* Assets.xcassets */, 6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */, 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
@@ -355,6 +384,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */; buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
buildPhases = ( buildPhases = (
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
6F5D0C16218352EF000F85AD /* Sources */, 6F5D0C16218352EF000F85AD /* Sources */,
6F5D0C17218352EF000F85AD /* Frameworks */, 6F5D0C17218352EF000F85AD /* Frameworks */,
6F5D0C18218352EF000F85AD /* Resources */, 6F5D0C18218352EF000F85AD /* Resources */,
@@ -410,6 +440,9 @@
CreatedOnToolsVersion = 9.4.1; CreatedOnToolsVersion = 9.4.1;
LastSwiftMigration = 1000; LastSwiftMigration = 1000;
SystemCapabilities = { SystemCapabilities = {
com.apple.ApplicationGroups.iOS = {
enabled = 1;
};
com.apple.NetworkExtensions.iOS = { com.apple.NetworkExtensions.iOS = {
enabled = 1; enabled = 1;
}; };
@@ -480,6 +513,25 @@
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n"; shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
showEnvVarsInLog = 0; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -487,7 +539,15 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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 */, 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -499,30 +559,35 @@
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */, 6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */, 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */, 6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */, 6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */, 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */, 6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */, 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */, 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */, 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */, 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */, 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */, 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104 VERSION_NAME = 0.0.20181104
VERSION_ID = 4 VERSION_ID = 5
@@ -26,16 +26,16 @@ class WgQuickConfigFileParser {
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? { func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
// required wg fields // 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 } guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey) var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
// other wg fields // other wg fields
if let listenPortString = attributes["ListenPort"] { if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else { return nil } guard let listenPort = UInt16(listenPortString) else { return nil }
interface.listenPort = listenPort interface.listenPort = listenPort
} }
// wg-quick fields // wg-quick fields
if let addressesString = attributes["Address"] { if let addressesString = attributes["address"] {
var addresses: [IPAddressRange] = [] var addresses: [IPAddressRange] = []
for addressString in addressesString.split(separator: ",") { for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces) let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
@@ -44,7 +44,7 @@ class WgQuickConfigFileParser {
} }
interface.addresses = addresses interface.addresses = addresses
} }
if let dnsString = attributes["DNS"] { if let dnsString = attributes["dns"] {
var dnsServers: [DNSServer] = [] var dnsServers: [DNSServer] = []
for dnsServerString in dnsString.split(separator: ",") { for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces) let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
@@ -53,7 +53,7 @@ class WgQuickConfigFileParser {
} }
interface.dns = dnsServers interface.dns = dnsServers
} }
if let mtuString = attributes["MTU"] { if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else { return nil } guard let mtu = UInt16(mtuString) else { return nil }
interface.mtu = mtu interface.mtu = mtu
} }
@@ -62,15 +62,15 @@ class WgQuickConfigFileParser {
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? { func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields // 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 } guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { return nil }
var peer = PeerConfiguration(publicKey: publicKey) var peer = PeerConfiguration(publicKey: publicKey)
// wg fields // wg fields
if let preSharedKeyString = attributes["PreSharedKey"] { if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil } guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil }
peer.preSharedKey = preSharedKey peer.preSharedKey = preSharedKey
} }
if let allowedIPsString = attributes["AllowedIPs"] { if let allowedIPsString = attributes["allowedips"] {
var allowedIPs: [IPAddressRange] = [] var allowedIPs: [IPAddressRange] = []
for allowedIPString in allowedIPsString.split(separator: ",") { for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
@@ -79,11 +79,11 @@ class WgQuickConfigFileParser {
} }
peer.allowedIPs = allowedIPs peer.allowedIPs = allowedIPs
} }
if let endpointString = attributes["Endpoint"] { if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else { return nil } guard let endpoint = Endpoint(from: endpointString) else { return nil }
peer.endpoint = endpoint peer.endpoint = endpoint
} }
if let persistentKeepAliveString = attributes["PersistentKeepalive"] { if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil } guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
peer.persistentKeepAlive = persistentKeepAlive peer.persistentKeepAlive = persistentKeepAlive
} }
@@ -113,9 +113,9 @@ class WgQuickConfigFileParser {
if let equalsIndex = line.firstIndex(of: "=") { if let equalsIndex = line.firstIndex(of: "=") {
// Line contains an attribute // 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 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) { if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
attributes[key] = presentValue + "," + value attributes[key] = presentValue + "," + value
} else { } else {
@@ -157,8 +157,7 @@ class WgQuickConfigFileParser {
} }
if let interfaceConfiguration = interfaceConfiguration { if let interfaceConfiguration = interfaceConfiguration {
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration) let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
tunnelConfiguration.peers = peerConfigurations
return tunnelConfiguration return tunnelConfiguration
} else { } else {
throw ParseError.noInterface throw ParseError.noInterface
@@ -25,3 +25,9 @@ struct Curve25519 {
return publicKey return publicKey
} }
} }
extension InterfaceConfiguration {
var publicKey: Data {
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
}
}
+4
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -120,5 +122,7 @@
</dict> </dict>
</dict> </dict>
</array> </array>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID)</string>
</dict> </dict>
</plist> </plist>
+36 -2
View File
@@ -361,6 +361,7 @@ class TunnelViewModel {
[TunnelViewModel.PeerData.ipv4DefaultRouteString] [TunnelViewModel.PeerData.ipv4DefaultRouteString]
} }
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ") scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
excludePrivateIPsValue = isOn excludePrivateIPsValue = isOn
} }
} }
@@ -441,9 +442,42 @@ class TunnelViewModel {
return .error("Two or more peers cannot have the same public key") return .error("Two or more peers cannot have the same public key")
} }
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration) let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
tunnelConfiguration.peers = peerConfigurations
return .saved(tunnelConfiguration) 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
}
}
+41 -8
View File
@@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var mainVC: MainViewController? var mainVC: MainViewController?
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds) let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white window.backgroundColor = UIColor.white
@@ -27,14 +27,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 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) mainVC?.tunnelsListVC?.importFromFile(url: url)
_ = FileManager.deleteFile(at: url)
return true 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 import os.log
class ErrorPresenter { class ErrorPresenter {
static func errorMessage(for error: Error) -> (String, String)? { static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?,
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?,
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) { onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return } 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 let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
onDismissal?() onDismissal?()
} }
@@ -4,6 +4,10 @@
import UIKit import UIKit
class MainViewController: UISplitViewController { class MainViewController: UISplitViewController {
var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController? var tunnelsListVC: TunnelsListTableViewController?
init() { init() {
@@ -19,6 +23,11 @@ class MainViewController: UISplitViewController {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.viewControllers = [ masterNC, detailNC ] self.viewControllers = [ masterNC, detailNC ]
// State restoration
self.restorationIdentifier = "MainVC"
masterNC.restorationIdentifier = "MasterNC"
detailNC.restorationIdentifier = "DetailNC"
} }
required init?(coder aDecoder: NSCoder) { 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 // On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
self.preferredDisplayMode = .allVisible 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) { func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
tunnelsListVC?.openForEditing(configFileURL: configFileURL) 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 iosAppVersion = "WireGuard for iOS"
case goBackendVersion = "WireGuard Go Backend" case goBackendVersion = "WireGuard Go Backend"
case exportZipArchive = "Export zip archive" case exportZipArchive = "Export zip archive"
case exportLogFile = "Export log file"
} }
let settingsFieldsBySection: [[SettingsFields]] = [ let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion], [.iosAppVersion, .goBackendVersion],
[.exportZipArchive] [.exportZipArchive],
[.exportLogFile]
] ]
let tunnelsManager: TunnelsManager? let tunnelsManager: TunnelsManager?
@@ -63,55 +65,73 @@ class SettingsTableViewController: UITableViewController {
} }
func exportConfigurationsAsZipFile(sourceView: UIView) { func exportConfigurationsAsZipFile(sourceView: UIView) {
guard let tunnelsManager = tunnelsManager, tunnelsManager.numberOfTunnels() > 0 else { guard let tunnelsManager = tunnelsManager else { return }
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 destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return 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 { let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
try ZipArchive.archive(inputs: inputsToArchiver, to: destinationURL) _ = FileManager.deleteFile(at: destinationURL)
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
// popoverPresentationController shall be non-nil on the iPad let count = tunnelsManager.numberOfTunnels()
activityVC.popoverPresentationController?.sourceView = sourceView let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() }
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] (error) in
present(activityVC, animated: true) if let error = error {
} catch (let error) { ErrorPresenter.showErrorAlert(error: error, from: self)
showErrorAlert(title: "Unable to export", message: "There was an error exporting the tunnel configuration archive: \(String(describing: error))") return
}
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
self?.present(fileExportVC, animated: true, completion: nil)
} }
} }
func showErrorAlert(title: String, message: String) { func exportLogForLastActivatedTunnel(sourceView: UIView) {
let okAction = UIAlertAction(title: "OK", style: .default) guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) return
alert.addAction(okAction) }
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" return "About"
case 1: case 1:
return "Export configurations" return "Export configurations"
case 2:
return "Tunnel log"
default: default:
return nil return nil
} }
@@ -152,14 +174,21 @@ extension SettingsTableViewController {
cell.value = WIREGUARD_GO_VERSION cell.value = WIREGUARD_GO_VERSION
} }
return cell return cell
} else { } else if (field == .exportZipArchive) {
assert(field == .exportZipArchive)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
cell.buttonText = field.rawValue cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
} }
return cell 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(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id) self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.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() { @objc func editTapped() {
@@ -52,14 +56,6 @@ class TunnelDetailTableViewController: UITableViewController {
present(editNC, animated: true) 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, func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) { onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
@@ -95,7 +91,7 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController { extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { 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 { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -112,7 +108,11 @@ extension TunnelDetailTableViewController {
// Peer // Peer
let peerData = tunnelViewModel.peersData[section - 2] let peerData = tunnelViewModel.peersData[section - 2]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else if (section < (3 + numberOfPeerSections)) {
// Activate on demand
return 1
} else { } else {
assert(section == (3 + numberOfPeerSections))
// Delete tunnel // Delete tunnel
return 1 return 1
} }
@@ -130,6 +130,9 @@ extension TunnelDetailTableViewController {
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) { } else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer // Peer
return "Peer" return "Peer"
} else if (section < (3 + numberOfPeerSections)) {
// On-Demand Activation
return "On-Demand Activation"
} else { } else {
// Delete tunnel // Delete tunnel
return nil return nil
@@ -152,16 +155,15 @@ extension TunnelDetailTableViewController {
if (isOn) { if (isOn) {
s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: s) ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
DispatchQueue.main.async { DispatchQueue.main.async {
cell.statusSwitch.isOn = false cell.statusSwitch.isOn = false
} }
})
} }
} }
} else { } else {
s.tunnelsManager.startDeactivation(of: s.tunnel) { error in s.tunnelsManager.startDeactivation(of: s.tunnel)
print("Error while deactivating: \(String(describing: error))")
}
} }
} }
return cell return cell
@@ -183,8 +185,13 @@ extension TunnelDetailTableViewController {
cell.key = field.rawValue cell.key = field.rawValue
cell.value = peerData[field] cell.value = peerData[field]
return cell 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 { } else {
assert(section == (2 + numberOfPeerSections)) assert(section == (3 + numberOfPeerSections))
// Delete configuration // Delete configuration
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = "Delete tunnel" cell.buttonText = "Delete tunnel"
@@ -259,10 +266,10 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
text = "Deactivating" text = "Deactivating"
case .reasserting: case .reasserting:
text = "Reactivating" text = "Reactivating"
case .resolvingEndpointDomains:
text = "Resolving domains"
case .restarting: case .restarting:
text = "Restarting" text = "Restarting"
case .waiting:
text = "Waiting"
} }
textLabel?.text = text textLabel?.text = text
DispatchQueue.main.async { [weak statusSwitch] in DispatchQueue.main.async { [weak statusSwitch] in
@@ -394,3 +401,36 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
hasDestructiveAction = false 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 .deletePeer
] ]
let activateOnDemandOptions: [ActivateOnDemandOption] = [
.useOnDemandOverWiFiOrCellular,
.useOnDemandOverWiFiOnly,
.useOnDemandOverCellularOnly
]
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer? let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) { init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
// Use this initializer to edit an existing tunnel. // Use this initializer to edit an existing tunnel.
tunnelsManager = tm tunnelsManager = tm
tunnel = t tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration()) tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
activateOnDemandSetting = t.activateOnDemandSetting()
super.init(style: .grouped) super.init(style: .grouped)
} }
@@ -44,6 +52,7 @@ class TunnelEditTableViewController: UITableViewController {
tunnelsManager = tm tunnelsManager = tm
tunnel = nil tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
super.init(style: .grouped) super.init(style: .grouped)
} }
@@ -58,12 +67,12 @@ class TunnelEditTableViewController: UITableViewController {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
self.tableView.rowHeight = 44 self.tableView.rowHeight = 44
self.tableView.allowsSelection = false
self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id) self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id)
self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id) self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id)
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id) self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id) self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.id)
} }
@objc func saveTapped() { @objc func saveTapped() {
@@ -77,7 +86,9 @@ class TunnelEditTableViewController: UITableViewController {
case .saved(let tunnelConfiguration): case .saved(let tunnelConfiguration):
if let tunnel = tunnel { if let tunnel = tunnel {
// We're modifying an existing 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 { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { } else {
@@ -87,14 +98,14 @@ class TunnelEditTableViewController: UITableViewController {
} }
} else { } else {
// We're adding a new tunnel // We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] (tunnel, error) in tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
if let error = error { activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { } else {
let tunnel: TunnelContainer = result.value!
self?.dismiss(animated: true, completion: nil) 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) dismiss(animated: true, completion: nil)
self.delegate?.tunnelEditingCancelled() 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 // MARK: UITableViewDataSource
@@ -122,7 +125,7 @@ extension TunnelEditTableViewController {
let numberOfInterfaceSections = interfaceFieldsBySection.count let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.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 { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -138,9 +141,16 @@ extension TunnelEditTableViewController {
let peerData = tunnelViewModel.peersData[peerIndex] let peerData = tunnelViewModel.peersData[peerIndex]
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs } let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
return peerFieldsToShow.count return peerFieldsToShow.count
} else { } else if (section < (numberOfInterfaceSections + numberOfPeerSections + 1)) {
// Add peer // Add peer
return 1 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))) { } else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer // Peer
return "Peer" return "Peer"
} else { } else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
// Add peer // Add peer
return nil return nil
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
return "On-Demand Activation"
} }
} }
@@ -344,8 +357,7 @@ extension TunnelEditTableViewController {
} }
return cell return cell
} }
} else { } else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
assert(section == (numberOfInterfaceSections + numberOfPeerSections))
// Add peer // Add peer
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = "Add peer" cell.buttonText = "Add peer"
@@ -365,6 +377,37 @@ extension TunnelEditTableViewController {
}, completion: nil) }, completion: nil)
} }
return cell 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 { class TunnelEditTableViewKeyValueCell: UITableViewCell {
static let id: String = "TunnelEditTableViewKeyValueCell" static let id: String = "TunnelEditTableViewKeyValueCell"
var key: String { var key: String {
@@ -488,6 +567,7 @@ class TunnelEditTableViewKeyValueCell: UITableViewCell {
isValueValid = true isValueValid = true
keyboardType = .default keyboardType = .default
onValueChanged = nil onValueChanged = nil
onValueBeingEdited = nil
} }
} }
@@ -664,3 +744,30 @@ class TunnelEditTableViewSwitchCell: UITableViewCell {
isOn = false 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 UIKit
import MobileCoreServices import MobileCoreServices
import UserNotifications
class TunnelsListTableViewController: UIViewController { class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager? var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var busyIndicator: UIActivityIndicatorView? var busyIndicator: UIActivityIndicatorView?
var centeredAddButton: BorderedTextButton? var centeredAddButton: BorderedTextButton?
@@ -38,50 +38,66 @@ class TunnelsListTableViewController: UIViewController {
busyIndicator.startAnimating() busyIndicator.startAnimating()
self.busyIndicator = busyIndicator self.busyIndicator = busyIndicator
// Create the tunnels manager, and when it's ready, create the tableView // State restoration
TunnelsManager.create { [weak self] tunnelsManager in self.restorationIdentifier = "TunnelsListVC"
guard let tunnelsManager = tunnelsManager else { return } }
guard let s = self else { return }
let tableView = UITableView(frame: CGRect.zero, style: .plain) func setTunnelsManager(tunnelsManager: TunnelsManager) {
tableView.rowHeight = 60 if (self.tunnelsManager != nil) {
tableView.separatorStyle = .none // If a tunnels manager is already set, do nothing
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id) return
}
s.view.addSubview(tableView) // Create the table view
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
// Add an add button, centered let tableView = UITableView(frame: CGRect.zero, style: .plain)
let centeredAddButton = BorderedTextButton() tableView.rowHeight = 60
centeredAddButton.title = "Add a tunnel" tableView.separatorStyle = .none
centeredAddButton.isHidden = true tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
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
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0) self.view.addSubview(tableView)
busyIndicator.stopAnimating() 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 // Add button at the center
s.tunnelsManager = tunnelsManager
s.onTunnelsManagerReady?(tunnelsManager) let centeredAddButton = BorderedTextButton()
s.onTunnelsManagerReady = nil 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) 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?) { func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration) let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
@@ -168,77 +164,37 @@ class TunnelsListTableViewController: UIViewController {
self.present(scanQRCodeNC, animated: true) 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) { func importFromFile(url: URL) {
guard let tunnelsManager = tunnelsManager else { return }
if (url.pathExtension == "zip") { if (url.pathExtension == "zip") {
var unarchivedFiles: [(fileName: String, contents: Data)] = [] ZipImporter.importConfigFiles(from: url) { [weak self] result in
do { if let error = result.error {
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"]) ErrorPresenter.showErrorAlert(error: error, from: self)
} 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 {
return return
} }
self?.showErrorAlert(title: "Created \(numberSuccessful) tunnels", let configs: [TunnelConfiguration?] = result.value!
message: "Created \(numberSuccessful) of \(unarchivedFiles.count) tunnels from zip archive") 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 */ { } else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url), if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) { let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
if let error = error { if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} }
} }
} else { } 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 { extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) { completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
if let error = error { if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler) ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else { } else {
completionHandler?() completionHandler?()
@@ -296,9 +252,7 @@ extension TunnelsListTableViewController: UITableViewDataSource {
} }
} }
} else { } else {
tunnelsManager.startDeactivation(of: tunnel) { [weak s] error in tunnelsManager.startDeactivation(of: tunnel)
s?.showErrorAlert(title: "Deactivation error", message: "Error while bringing down tunnel: \(String(describing: error))")
}
} }
} }
} }
@@ -310,12 +264,12 @@ extension TunnelsListTableViewController: UITableViewDataSource {
extension TunnelsListTableViewController: UITableViewDelegate { extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel) tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
} }
@@ -339,7 +293,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
// MARK: TunnelsManagerDelegate // MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerDelegate { extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) { func tunnelAdded(at index: Int) {
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic) tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0) 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
}
}
+178 -141
View File
@@ -5,33 +5,61 @@ import Foundation
import NetworkExtension import NetworkExtension
import os.log import os.log
protocol TunnelsManagerDelegate: class { protocol TunnelsManagerListDelegate: class {
func tunnelAdded(at: Int) func tunnelAdded(at: Int)
func tunnelModified(at: Int) func tunnelModified(at: Int)
func tunnelMoved(at oldIndex: Int, to newIndex: Int) func tunnelMoved(at oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at: Int) func tunnelRemoved(at: Int)
} }
enum TunnelActivationError: Error { protocol TunnelsManagerActivationDelegate: class {
case dnsResolutionFailed func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError)
case tunnelActivationFailed
case attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: TunnelStatus)
case attemptingActivationWhenTunnelIsNotInactive
case attemptingDeactivationWhenTunnelIsInactive
} }
enum TunnelManagementError: Error { enum TunnelsManagerError: WireGuardAppError {
// Tunnels list management
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName case tunnelAlreadyExistsWithThatName
case tunnelInvalidName case vpnSystemErrorOnListingTunnels
case vpnSystemErrorOnAddTunnel case vpnSystemErrorOnAddTunnel
case vpnSystemErrorOnModifyTunnel case vpnSystemErrorOnModifyTunnel
case vpnSystemErrorOnRemoveTunnel 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 { class TunnelsManager {
private var tunnels: [TunnelContainer] private var tunnels: [TunnelContainer]
weak var delegate: TunnelsManagerDelegate? weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
private var isAddingTunnel: Bool = false private var isAddingTunnel: Bool = false
private var isModifyingTunnel: 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 } 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) #if targetEnvironment(simulator)
// NETunnelProviderManager APIs don't work on the simulator // NETunnelProviderManager APIs don't work on the simulator
completionHandler(TunnelsManager(tunnelProviders: [])) completionHandler(.success(TunnelsManager(tunnelProviders: [])))
#else #else
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if let error = error { if let error = error {
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)") os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnListingTunnels))
return return
} }
completionHandler(TunnelsManager(tunnelProviders: managers ?? [])) completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
} }
#endif #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 let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
return return
} }
if self.tunnels.contains(where: { $0.name == tunnelName }) { if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
return return
} }
@@ -74,19 +105,21 @@ class TunnelsManager {
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] (error) in tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isAddingTunnel = false } defer { self?.isAddingTunnel = false }
guard (error == nil) else { guard (error == nil) else {
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)") os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(nil, TunnelManagementError.vpnSystemErrorOnAddTunnel) completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnAddTunnel))
return return
} }
if let s = self { if let s = self {
let tunnel = TunnelContainer(tunnel: tunnelProviderManager) let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
s.tunnels.append(tunnel) s.tunnels.append(tunnel)
s.tunnels.sort { $0.name < $1.name } s.tunnels.sort { $0.name < $1.name }
s.delegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!) s.tunnelsListDelegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
completionHandler(tunnel, nil) completionHandler(.success(tunnel))
} }
} }
} }
@@ -101,17 +134,18 @@ class TunnelsManager {
return return
} }
let tail = tunnelConfigurations.dropFirst() 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 { 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 let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(TunnelsManagerError.tunnelNameEmpty)
return return
} }
@@ -122,7 +156,7 @@ class TunnelsManager {
var oldName: String? var oldName: String?
if (isNameChanged) { if (isNameChanged) {
if self.tunnels.contains(where: { $0.name == tunnelName }) { if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return return
} }
oldName = tunnel.name oldName = tunnel.name
@@ -132,11 +166,14 @@ class TunnelsManager {
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = (!tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled)
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] (error) in tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isModifyingTunnel = false } defer { self?.isModifyingTunnel = false }
guard (error == nil) else { guard (error == nil) else {
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)") os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelManagementError.vpnSystemErrorOnModifyTunnel) completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
return return
} }
if let s = self { if let s = self {
@@ -144,21 +181,35 @@ class TunnelsManager {
let oldIndex = s.tunnels.firstIndex(of: tunnel)! let oldIndex = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.sort { $0.name < $1.name } s.tunnels.sort { $0.name < $1.name }
let newIndex = s.tunnels.firstIndex(of: tunnel)! 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) { 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 // Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.beginRestart() 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 let tunnelProviderManager = tunnel.tunnelProvider
isDeletingTunnel = true isDeletingTunnel = true
@@ -167,13 +218,13 @@ class TunnelsManager {
defer { self?.isDeletingTunnel = false } defer { self?.isDeletingTunnel = false }
guard (error == nil) else { guard (error == nil) else {
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)") os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelManagementError.vpnSystemErrorOnRemoveTunnel) completionHandler(TunnelsManagerError.vpnSystemErrorOnRemoveTunnel)
return return
} }
if let s = self { if let s = self {
let index = s.tunnels.firstIndex(of: tunnel)! let index = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.remove(at: index) s.tunnels.remove(at: index)
s.delegate?.tunnelRemoved(at: index) s.tunnelsListDelegate?.tunnelRemoved(at: index)
} }
completionHandler(nil) completionHandler(nil)
} }
@@ -187,57 +238,49 @@ class TunnelsManager {
return tunnels[index] return tunnels[index]
} }
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) { func tunnel(named tunnelName: String) -> TunnelContainer? {
guard (tunnel.status == .inactive) else { return self.tunnels.first(where: { $0.name == tunnelName })
completionHandler(TunnelActivationError.attemptingActivationWhenTunnelIsNotInactive)
return
}
for t in tunnels {
if t.status != .inactive {
completionHandler(TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: t.status))
return
}
}
tunnel.startActivation(completionHandler: completionHandler)
} }
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) { if (tunnel.status == .inactive) {
completionHandler(TunnelActivationError.attemptingDeactivationWhenTunnelIsInactive)
return return
} }
tunnel.startDeactivation() tunnel.startDeactivation()
} }
}
extension NETunnelProviderProtocol { func refreshStatuses() {
convenience init?(tunnelConfiguration: TunnelConfiguration) { for t in tunnels {
assert(!tunnelConfiguration.interface.name.isEmpty) t.refreshStatus()
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)
} }
} }
@@ -245,18 +288,29 @@ class TunnelContainer: NSObject {
@objc dynamic var name: String @objc dynamic var name: String
@objc dynamic var status: TunnelStatus @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 fileprivate let tunnelProvider: NETunnelProviderManager
private var statusObservationToken: AnyObject? private var statusObservationToken: AnyObject?
private var dnsResolver: DNSResolver?
init(tunnel: NETunnelProviderManager) { init(tunnel: NETunnelProviderManager) {
self.name = tunnel.localizedDescription ?? "Unnamed" self.name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status) let status = TunnelStatus(from: tunnel.connection.status)
self.status = status self.status = status
self.isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
self.tunnelProvider = tunnel self.tunnelProvider = tunnel
super.init() super.init()
if (status != .inactive) { if (status != .inactive || isActivateOnDemandEnabled) {
startObservingTunnelStatus() startObservingTunnelStatus()
} }
} }
@@ -265,62 +319,39 @@ class TunnelContainer: NSObject {
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration() return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
} }
fileprivate func startActivation(completionHandler: @escaping (Error?) -> Void) { func activateOnDemandSetting() -> ActivateOnDemandSetting {
assert(status == .inactive || status == .restarting) return ActivateOnDemandSetting(from: tunnelProvider)
assert(self.dnsResolver == nil) }
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() } guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
// Resolve DNS and start the tunnel onDeactivationComplete = nil
let dnsResolver = DNSResolver(endpoints: endpoints) isAttemptingActivation = true
let resolvedEndpoints = dnsResolver.resolveWithoutNetworkRequests() startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
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)
}
}
} }
fileprivate func startActivation(recursionCount: UInt = 0, fileprivate func startActivation(recursionCount: UInt = 0,
lastError: Error? = nil, lastError: Error? = nil,
tunnelConfiguration: TunnelConfiguration, tunnelConfiguration: TunnelConfiguration,
resolvedEndpoints: [Endpoint?], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
completionHandler: @escaping (Error?) -> Void) {
if (recursionCount >= 8) { if (recursionCount >= 8) {
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)") os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
completionHandler(TunnelActivationError.tunnelActivationFailed) completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return 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) os_log("startActivation: Entering", log: OSLog.default, type: .debug)
guard (tunnelProvider.isEnabled) else { guard (tunnelProvider.isEnabled) else {
@@ -331,12 +362,12 @@ class TunnelContainer: NSObject {
tunnelProvider.saveToPreferences { [weak self] (error) in tunnelProvider.saveToPreferences { [weak self] (error) in
if (error != nil) { if (error != nil) {
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)") os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(error) completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return return
} }
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info) os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug) 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 return
} }
@@ -345,41 +376,35 @@ class TunnelContainer: NSObject {
startObservingTunnelStatus() startObservingTunnelStatus()
let session = (tunnelProvider.connection as! NETunnelProviderSession) let session = (tunnelProvider.connection as! NETunnelProviderSession)
do { 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) 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) os_log("startActivation: Success", log: OSLog.default, type: .debug)
completionHandler(nil) completionHandler(nil)
} catch (let error) { } catch (let error) {
os_log("startActivation: Error starting tunnel. Examining error", log: OSLog.default, type: .debug)
guard let vpnError = error as? NEVPNError else { 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 status = .inactive
completionHandler(error) completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return return
} }
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else { 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 status = .inactive
completionHandler(error) completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return return
} }
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) 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) os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
tunnelProvider.loadFromPreferences { [weak self] (error) in tunnelProvider.loadFromPreferences { [weak self] (error) in
if (error != nil) { 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 self?.status = .inactive
completionHandler(error) completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return return
} }
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info) os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug) 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, object: connection,
queue: nil) { [weak self] (_) in queue: nil) { [weak self] (_) in
guard let s = self else { return } 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)) { if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
// Don't change s.status when disconnecting for a restart // Don't change s.status when disconnecting for a restart
if (connection.status == .disconnected) { if (connection.status == .disconnected) {
@@ -414,13 +451,13 @@ class TunnelContainer: NSObject {
} }
return 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) s.status = TunnelStatus(from: connection.status)
if (s.status == .inactive) { 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 reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel) 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) { init(from vpnStatus: NEVPNStatus) {
switch (vpnStatus) { switch (vpnStatus) {
@@ -6,5 +6,9 @@
<array> <array>
<string>packet-tunnel-provider</string> <string>packet-tunnel-provider</string>
</array> </array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_ID)</string>
</array>
</dict> </dict>
</plist> </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)
}
+28
View File
@@ -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 import Foundation
enum ZipArchiveError: Error { enum ZipArchiveError: WireGuardAppError {
case cantOpenInputZipFile case cantOpenInputZipFile
case cantOpenOutputZipFileForWriting case cantOpenOutputZipFileForWriting
case badArchive 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 { class ZipArchive {
@@ -48,7 +59,7 @@ class ZipArchive {
throw ZipArchiveError.badArchive throw ZipArchiveError.badArchive
} }
let bufferSize = 1024 let bufferSize = 16384 // 16 kb
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize) var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
var dataBuffer = 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)) }
}
}
}
@@ -4,86 +4,61 @@
import Network import Network
import Foundation import Foundation
enum DNSResolverError: Error {
case dnsResolutionFailed(hostnames: [String])
}
class DNSResolver { class DNSResolver {
let endpoints: [Endpoint?]
let dispatchGroup: DispatchGroup
var dispatchWorkItems: [DispatchWorkItem]
static var cache = NSCache<NSString, NSString>()
init(endpoints: [Endpoint?]) { static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
self.endpoints = endpoints for endpoint in 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() {
guard let endpoint = endpoint else { continue } guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) { if (!endpoint.hasHostAsIPAddress()) {
resolvedEndpoints[i] = endpoint return false
} 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
} }
} }
return resolvedEndpoints return true
} }
func resolve(completionHandler: @escaping ([Endpoint?]?) -> Void) { static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
let endpoints = self.endpoints let dispatchGroup: DispatchGroup = DispatchGroup()
let dispatchGroup = self.dispatchGroup
dispatchWorkItems = [] if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) {
return endpoints
}
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count) var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
var isResolvedByDNSRequest: [Bool] = Array<Bool>(repeating: false, count: endpoints.count) for (i, endpoint) in endpoints.enumerated() {
for (i, endpoint) in self.endpoints.enumerated() {
guard let endpoint = endpoint else { continue } guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) { if (endpoint.hasHostAsIPAddress()) {
resolvedEndpoints[i] = endpoint 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 { } else {
let workItem = DispatchWorkItem { let workItem = DispatchWorkItem {
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint) resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
isResolvedByDNSRequest[i] = true
} }
dispatchWorkItems.append(workItem)
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem) DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
} }
} }
dispatchGroup.notify(queue: .main) {
assert(endpoints.count == resolvedEndpoints.count) dispatchGroup.wait() // TODO: Timeout?
for (i, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint, let resolvedEndpoint = resolvedEndpoints[i] else { var hostnamesWithDnsResolutionFailure: [String] = []
completionHandler(nil) assert(endpoints.count == resolvedEndpoints.count)
return for tuple in zip(endpoints, resolvedEndpoints) {
} let endpoint = tuple.0
if (isResolvedByDNSRequest[i]) { let resolvedEndpoint = tuple.1
DNSResolver.cache.setObject(resolvedEndpoint.stringRepresentation() as NSString, if let endpoint = endpoint {
forKey: endpoint.stringRepresentation() as NSString) 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)
}
} }
} if (!hostnamesWithDnsResolutionFailure.isEmpty) {
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
func cancel() {
for workItem in dispatchWorkItems {
workItem.cancel()
} }
} return resolvedEndpoints
deinit {
cancel()
} }
} }
@@ -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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -27,5 +29,7 @@
<key>NSExtensionPrincipalClass</key> <key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string> <string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict> </dict>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID)</string>
</dict> </dict>
</plist> </plist>
@@ -2,14 +2,18 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
import Foundation
import os.log import os.log
enum PacketTunnelProviderError: Error { enum PacketTunnelProviderError: Error {
case invalidOptions case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure(hostnames: [String])
case couldNotStartWireGuard case couldNotStartWireGuard
case coultNotSetNetworkSettings case coultNotSetNetworkSettings
} }
private var logFileHandle: FileHandle?
/// A packet tunnel provider object. /// A packet tunnel provider object.
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
@@ -22,110 +26,81 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
/// Begin the process of establishing the tunnel. /// Begin the process of establishing the tunnel.
override func startTunnel(options: [String: NSObject]?, override func startTunnel(options: [String: NSObject]?,
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
os_log("Starting tunnel", log: OSLog.default, type: .info)
guard let options = options else { guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
os_log("Starting tunnel failed: No options passed. Possible connection request from preferences", log: OSLog.default, type: .error) let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
// displayMessage is deprecated API ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self)
displayMessage("Please use the WireGuard app to start WireGuard tunnels") { (_) in startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
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)
return return
} }
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
}
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
// Configure logging
configureLogger() 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 let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
if fd < 0 { 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) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return 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 { 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) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return return
} }
wgHandle = handle wgHandle = handle
// Network settings // Apply 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
}
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
setTunnelNetworkSettings(networkSettings) { (error) in setTunnelNetworkSettings(networkSettings) { (error) in
if let error = error { 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) startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else { } else {
startTunnelCompletionHandler(nil /* No errors */) startTunnelCompletionHandler(nil /* No errors */)
@@ -135,14 +110,34 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
/// Begin the process of stopping the tunnel. /// Begin the process of stopping the tunnel.
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { 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 { if let handle = wgHandle {
wgTurnOff(handle) wgTurnOff(handle)
} }
if let fileHandle = logFileHandle {
fileHandle.closeFile()
}
completionHandler() completionHandler()
} }
private func configureLogger() { 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 wgSetLogger { (level, msgCStr) in
let logType: OSLogType let logType: OSLogType
switch level { switch level {
@@ -156,7 +151,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
logType = .default logType = .default
} }
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : "" 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) 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 { 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()
}
}
@@ -3,29 +3,30 @@
import Foundation import Foundation
import Network import Network
import NetworkExtension
class PacketTunnelOptionsGenerator { class PacketTunnelSettingsGenerator {
static func generateOptions(from tc: TunnelConfiguration,
withResolvedEndpoints resolvedEndpoints: [Endpoint?]) -> [String: NSObject] {
var options: [String: NSObject] = [:]
// Interface name let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?]
options[.interfaceName] = tc.interface.name as NSObject init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
self.tunnelConfiguration = tunnelConfiguration
// WireGuard settings self.resolvedEndpoints = resolvedEndpoints
}
func generateWireGuardSettings() -> String {
var wgSettings = "" var wgSettings = ""
let privateKey = tc.interface.privateKey.hexEncodedString() let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
wgSettings.append("private_key=\(privateKey)\n") wgSettings.append("private_key=\(privateKey)\n")
if let listenPort = tc.interface.listenPort { if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n") wgSettings.append("listen_port=\(listenPort)\n")
} }
if (tc.peers.count > 0) { if (tunnelConfiguration.peers.count > 0) {
wgSettings.append("replace_peers=true\n") wgSettings.append("replace_peers=true\n")
} }
assert(tc.peers.count == resolvedEndpoints.count) assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (i, peer) in tc.peers.enumerated() { for (i, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n") wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let preSharedKey = peer.preSharedKey { if let preSharedKey = peer.preSharedKey {
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n") 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 // Remote address
@@ -67,15 +70,24 @@ class PacketTunnelOptionsGenerator {
} }
} }
options[.remoteAddress] = remoteAddress as NSObject let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
// DNS // 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 // 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 // Addresses from interface addresses
@@ -85,22 +97,16 @@ class PacketTunnelOptionsGenerator {
var ipv6Addresses: [String] = [] var ipv6Addresses: [String] = []
var ipv6NetworkPrefixLengths: [NSNumber] = [] var ipv6NetworkPrefixLengths: [NSNumber] = []
for addressRange in tc.interface.addresses { for addressRange in tunnelConfiguration.interface.addresses {
if (addressRange.address is IPv4Address) { if (addressRange.address is IPv4Address) {
ipv4Addresses.append("\(addressRange.address)") ipv4Addresses.append("\(addressRange.address)")
ipv4SubnetMasks.append(ipv4SubnetMaskString(of: addressRange)) ipv4SubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) { } else if (addressRange.address is IPv6Address) {
ipv6Addresses.append("\(addressRange.address)") ipv6Addresses.append("\(addressRange.address)")
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength)) 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 // Included routes from AllowedIPs
var ipv4IncludedRouteAddresses: [String] = [] var ipv4IncludedRouteAddresses: [String] = []
@@ -109,11 +115,11 @@ class PacketTunnelOptionsGenerator {
var ipv6IncludedRouteAddresses: [String] = [] var ipv6IncludedRouteAddresses: [String] = []
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = [] var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
for peer in tc.peers { for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs { for addressRange in peer.allowedIPs {
if (addressRange.address is IPv4Address) { if (addressRange.address is IPv4Address) {
ipv4IncludedRouteAddresses.append("\(addressRange.address)") ipv4IncludedRouteAddresses.append("\(addressRange.address)")
ipv4IncludedRouteSubnetMasks.append(ipv4SubnetMaskString(of: addressRange)) ipv4IncludedRouteSubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) { } else if (addressRange.address is IPv6Address) {
ipv6IncludedRouteAddresses.append("\(addressRange.address)") ipv6IncludedRouteAddresses.append("\(addressRange.address)")
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength)) 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 // Excluded routes from endpoints
var ipv4ExcludedRouteAddresses: [String] = [] var ipv4ExcludedRouteAddresses: [String] = []
@@ -149,13 +149,40 @@ class PacketTunnelOptionsGenerator {
} }
} }
options[.ipv4ExcludedRouteAddresses] = ipv4ExcludedRouteAddresses as NSObject // Apply IPv4 settings
options[.ipv4ExcludedRouteSubnetMasks] = ipv4ExcludedRouteSubnetMasks as NSObject
options[.ipv6ExcludedRouteAddresses] = ipv6ExcludedRouteAddresses as NSObject let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
options[.ipv6ExcludedRouteNetworkPrefixLengths] = ipv6ExcludedRouteNetworkPrefixLengths as NSObject 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 { static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
@@ -1 +1,2 @@
#include "../../wireguard-go-bridge/wireguard.h" #include "../../wireguard-go-bridge/wireguard.h"
#include "wireguard-go-version.h"
@@ -6,5 +6,9 @@
<array> <array>
<string>packet-tunnel-provider</string> <string>packet-tunnel-provider</string>
</array> </array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_ID)</string>
</array>
</dict> </dict>
</plist> </plist>
+85 -5
View File
@@ -25,6 +25,8 @@ import (
"os/signal" "os/signal"
"runtime" "runtime"
"strings" "strings"
"syscall"
"time"
"unsafe" "unsafe"
) )
@@ -46,12 +48,54 @@ func (l *CLogger) Write(p []byte) (int, error) {
return len(p), nil 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() { func init() {
versionString = C.CString(WireGuardGoVersion) versionString = C.CString(WireGuardGoVersion)
roamingDisabled = true roamingDisabled = true
tunnelHandles = make(map[int32]*Device) tunnelHandles = make(map[int32]*DeviceState)
signals := make(chan os.Signal) signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2) signal.Notify(signals, unix.SIGUSR2)
go func() { go func() {
@@ -67,6 +111,7 @@ func init() {
} }
} }
}() }()
go listenForRouteChanges()
} }
//export wgSetLogger //export wgSetLogger
@@ -74,6 +119,32 @@ func wgSetLogger(loggerFn uintptr) {
loggerFunc = unsafe.Pointer(loggerFn) 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 //export wgTurnOn
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 { func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
interfaceName := string([]byte(ifnameRef)) interfaceName := string([]byte(ifnameRef))
@@ -113,18 +184,27 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
if i == math.MaxInt32 { if i == math.MaxInt32 {
return -1 return -1
} }
tunnelHandles[i] = device tunnelHandles[i] = &DeviceState{
device: device,
logger: logger,
endpointsSettings: extractEndpointFromSettings(settings),
}
return i return i
} }
//export wgTurnOff //export wgTurnOff
func wgTurnOff(tunnelHandle int32) { func wgTurnOff(tunnelHandle int32) {
device, ok := tunnelHandles[tunnelHandle] deviceState, ok := tunnelHandles[tunnelHandle]
if !ok { if !ok {
return return
} }
delete(tunnelHandles, tunnelHandle) delete(tunnelHandles, tunnelHandle)
device.Close() t := deviceState.endpointsTimer
if t != nil {
deviceState.endpointsTimer = nil
t.Stop()
}
deviceState.device.Close()
} }
//export wgVersion //export wgVersion