Compare commits

...

84 Commits

Author SHA1 Message Date
Jason A. Donenfeld 9556901a33 Version bump
This is our first release to the real app store.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-17 14:08:17 +01:00
Eric Kuck ed9b4c85ed Got TunnelsManager back under the max file length by splitting out NEVPNStatus+CustomStringConvertible
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-16 23:51:25 -06:00
Eric Kuck fc452753a7 Potential fix for insertRowAtIndexPath crash
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-16 21:11:33 -06:00
Jason A. Donenfeld d775682ef4 More proper way to get sdk root directory
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-17 00:23:11 +01:00
Jason A. Donenfeld ae339a000e Further generalize makefile
This should allow us to eventually build on macOS

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 03:51:43 +01:00
Jason A. Donenfeld a80cf6a0dc Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:55:04 +01:00
Jason A. Donenfeld 727992f5d2 Improve mock tunnels generation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:51:14 +01:00
Roopesh Chander 2a22c0f2d6 Provide mock tunnels for the Simulator
To help in generation of screenshots for the App Store

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-16 01:15:10 +05:30
Jason A. Donenfeld b3f5635f4e Nuke duplicate file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:21:49 +01:00
Jason A. Donenfeld 946524aa8b Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:14:05 +01:00
Jason A. Donenfeld 1450538846 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 05:08:31 +01:00
Eric Kuck 5a08c67f33 Fixed editable KeyValueCells being copyable
Fixed DNS servers not saving

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 21:48:48 -06:00
Jason A. Donenfeld 1e9c806614 Fix confusing indentation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 04:42:46 +01:00
Eric Kuck ccd8cfe478 KeyValueCells now share code
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 20:02:37 -06:00
Eric Kuck cb051f695d Reorganized project structure
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:27:11 -06:00
Eric Kuck 7a24f18eb7 Most similar views now shared between ViewControllers
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:15:22 -06:00
Jason A. Donenfeld 83c95dc26d Prettier log time format
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 00:08:54 +01:00
Jason A. Donenfeld e0bc5e12b3 Simplify logging tags
This was roop's initial idea, and it turns out to be the better one, now
that we can pass cstrings more easily.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-14 22:53:42 +01:00
Roopesh Chander c4263da231 Fix tunnel remaining in 'Activating' state
It uses to remain in 'Activating' state when we don't get a status
update notification, for example, when turning on the tunnel repeatedly
without Internet connectivity.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 17:33:56 +05:30
Roopesh Chander 1eb3fd4de0 Fix status switch weird state after an error occurs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 16:50:36 +05:30
Jason A. Donenfeld 73be704b01 Deduplicate functions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-14 00:01:50 +01:00
Jason A. Donenfeld 2699c613bd Simplify filemanager extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:25:18 +01:00
Jason A. Donenfeld 74e983ea6f Can't -> cannot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:17:05 +01:00
Roopesh Chander 48552d2663 NE: Communicate last error to app through a shared file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 02:24:53 +05:30
Roopesh Chander 501e412b84 TunnelsManager: startActivation() need not take a tunnelConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 00:40:18 +05:30
Jason A. Donenfeld 77a26e4cd2 Localize swiftlint
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 20:06:37 +01:00
Eric Kuck 05d750539b Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)
All swiftlint warnings except one fixed up

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-13 12:58:50 -06:00
Jason A. Donenfeld 7323a00612 Avoid escaping heap allocation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:43:12 +01:00
Jason A. Donenfeld a6912ca7a2 Tidy up str to gostr conversion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:36:51 +01:00
Roopesh Chander b256acc372 TunnelsManager: Remove mentions of 'internal error'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:53:17 +05:30
Roopesh Chander 7e093575a4 TunnelsManager: Ask to check Internet connectivity in error alert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:49:42 +05:30
Roopesh Chander 740ffd68b6 Remove unused code: InternetReachability
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:45:21 +05:30
Roopesh Chander f67e1d8fc4 TunnelsManager: Remove unused variable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:44:13 +05:30
Roopesh Chander 33af8845b6 TunnelsManager: Remove assert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:43:15 +05:30
Jason A. Donenfeld 154774ada2 Simplify C strings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:08:38 +01:00
Roopesh Chander 3bddab8a9e TunnelsManager: Fix race between multiple startActivation() calls
After startActivate() is called on a waiting tunnel, user might turn
on a different tunnel before the waiting tunnel's status gets updated.
This fix prevents that from happening.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:34:00 +05:30
Roopesh Chander f9239dae75 TunnelsManager: Reintroduce waiting for another tunnel to deactivate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:21:49 +05:30
Jason A. Donenfeld 642b627d27 Rewrite Logger
This reverts all of Roop's changes to the C code, and then rewrites the
logger logic to be cleaner.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 18:06:37 +01:00
Eric Kuck 38accad27d More reliable logo sizing
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-13 09:30:13 -06:00
Roopesh Chander bf58159d99 TunnelsManager: Report activation errors through the activationDelegate
Don't report activation errors through completion handlers

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:56:07 +05:30
Roopesh Chander efd4b28a0d Logging: Write versions from both app and extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:09:38 +05:30
Roopesh Chander ae565db371 Logging: file_log doesn't need the message type
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:06:57 +05:30
Roopesh Chander e199ed0d6c Logging: Tag the entries in the merged log
So we know which entry is from the app and which is from the network
extension.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:00:32 +05:30
Roopesh Chander ba1d0c05be Logging: Use ringlogger for logging from the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:20 +05:30
Roopesh Chander 12503ae51d Logging: ringlogger.c: Trim trailing newlines
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:14 +05:30
Roopesh Chander ae7fb7323f Logging: Use ringlogger for logging from the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:14 +05:30
Roopesh Chander 5ae9eec555 Avoid using 'VPN' in code where possible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:20:10 +05:30
Roopesh Chander 6528a581de mv WireGuard/WireGuard/VPN/ WireGuard/WireGuard/Tunnel/
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:14:21 +05:30
Jason A. Donenfeld e11224f394 Commit untested ringlogger code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 06:14:24 +01:00
Jason A. Donenfeld 5971c197bd Remove useless whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:26:04 +01:00
Jason A. Donenfeld ecbab37e0e Settings: better padding calculation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:22:13 +01:00
Eric Kuck 4eec53d6d3 Fixed hacky logo display for settings
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:16:28 -06:00
Eric Kuck 8a916beb38 More formatting nits and cyclomatic complexity fixes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:09:52 -06:00
Eric Kuck e4ac48bc75 More linter warnings fixed, enabled more swiftlint rules, project cleanup
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 15:33:14 -06:00
Eric Kuck d06cff2a36 Tons more swiftlint warnings fixed. Still a few remaining.
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 12:28:27 -06:00
Eric Kuck de14b76b4d Added swiftlint and fixed all errors (and a bunch, but not all, warnings)
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 11:40:57 -06:00
Roopesh Chander af78fa9a1c Zip importing: importFromFile should take a completionHandler
Deletion of the being-imported file should be done in the
completionHandler.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 19:24:18 +05:30
Roopesh Chander 7ef12d93a7 ErrorPresenter: Support onPresented for showErrorAlert(title:,message:)
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 19:17:06 +05:30
Roopesh Chander 8259145f85 Zip importing: Handle spaces in filenames correctly
Previously, if a filename of a .conf file inside the zip file
contained spaces, it was not imported.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 16:57:17 +05:30
Jason A. Donenfeld 034a1a12f7 Supply missing pieces of path change
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-12 01:11:43 +01:00
Eric Kuck 9bc7e58487 Fixed a potential race condition, better naming on PacketTunnelSettingsGenerator methods
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-11 16:59:15 -06:00
Eric Kuck 27265fc222 Added an (unfinished) NWPathMonitor implementation for reconnecting on network changes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-11 16:12:04 -06:00
Jason A. Donenfeld 63e67a5e13 Revert pure-go network monitoring and add wgSetConfig
This reverts commit 99f0e457c34480f25582d7b4ed509404712c648c and adds a
function too.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 22:25:54 +01:00
Roopesh Chander bde984625c State restoration: Don't create duplicate mainVC and tunnelsListVC
This creates a duplicate tunnels manager, leading to problems tracking
tunnel statuses.

To reproduce the bug that this commit fixes, you can do the following:
1. Remove all tunnels
2. Run in Xcode
3. Import zip with ~10 tunnels
4. Stop app in Xcode
5. Run in Xcode
6. Turn on 1st tunnel, after it's on, turn off
Turn on 2nd tunnel, after it's on, turn off
...
After 6-8 tunnels, the spinner doesn't show up, indicating that the
status is not being tracked.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 17:52:54 +05:30
Roopesh Chander 1ded24f0e0 TunnelsManager: Error out only on no-internet scenario
The other scenario happens even during reloading of a tunnel for activation.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 03:48:28 +05:30
Roopesh Chander 1fd0c56f08 Remove the feature of waiting for another tunnel to deactivate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 03:47:23 +05:30
Roopesh Chander e59dbe6364 TunnelsManager: Deactivate only when the status becomes 'connected'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:42:21 +05:30
Roopesh Chander 4d63a3e9bd Allow turning off the status switch of a waiting tunnel
It just means the waiting should be cancelled

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:01:54 +05:30
Roopesh Chander 9946d8f989 TunnelsManager: Handle status change in TunnelsManager
Rather than in TunnelContainer.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:01:49 +05:30
Roopesh Chander 15b6cf5412 Error handling: alertText() can be nil
Indicating that no alert is to be shown for that error.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 00:34:22 +05:30
Roopesh Chander 851bd8102d TunnelsManager: Don't act on status change on tunnelProviders we don't have
That causes errors we don't want, and duplicate notifications.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 17:03:23 +05:30
Roopesh Chander 0d7a585bf7 TunnelsManager: Always call the completion handler before returning
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 17:01:53 +05:30
Roopesh Chander 663bb02c68 TunnelsManager: Debugging helpers for tunnel status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:31:36 +05:30
Roopesh Chander b491b9c371 TunnelsManager: Handle deactivation of a waiting tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:28:41 +05:30
Roopesh Chander 707f292e4e Tunnels list: Fix AutoLayout error during deletion of a tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 12:59:31 +05:30
Roopesh Chander aa41bd7d6c Settings: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander ec5be76d06 Tunnel edit: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander 692b0c6519 Tunnel detail: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander bb836b8fdc Tunnels list: Dynamic Type support for the add button at the center
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 18:13:37 +05:30
Roopesh Chander 8eda4641ca Tunnels list: Dynamic Type support for the table view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 18:13:04 +05:30
Roopesh Chander 60e13ddbf6 Model: Declare keyLength constant and use that wherever applicable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 14:07:03 +05:30
Roopesh Chander 0dcb285b67 TunnelsManager: Observe status for all tunnels in one block
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 18:43:24 +05:30
Roopesh Chander 6838dc3a74 TunnelsManager: Remove unused variables
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 15:00:26 +05:30
Roopesh Chander 46f4be6087 Zip: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 13:43:00 +05:30
63 changed files with 3435 additions and 3356 deletions
+3
View File
@@ -38,3 +38,6 @@ fastlane/screenshots
fastlane/test_output
Preview.html
output
# Wireguard specific
WireGuard/WireGuard/Config/Developer.xcconfig
-12
View File
@@ -1,12 +0,0 @@
disabled_rules:
- line_length
- trailing_comma
excluded:
- Pods
file_length:
warning: 500
type_name:
min_length: 2 # only warning
max_length: # warning and error
warning: 50
error: 60
+6
View File
@@ -23,6 +23,12 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
```
- Install swiftlint:
```
$ brew install swiftlint
```
- Open project in XCode:
```
+14
View File
@@ -0,0 +1,14 @@
disabled_rules:
- line_length
- trailing_whitespace
- todo
opt_in_rules:
- unneeded_parentheses_in_closure_argument
# - trailing_closure
file_length:
warning: 500
cyclomatic_complexity:
warning: 10
error: 25
function_body_length:
warning: 45
+21 -6
View File
@@ -5,23 +5,38 @@ import Foundation
import os.log
extension FileManager {
static var networkExtensionLogFileURL: URL? {
private static var sharedFolderURL: 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)
os_log("Cannot 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)
wg_log(.error, message: "Cannot obtain shared folder URL")
return nil
}
return sharedFolderURL.appendingPathComponent("last-activated-tunnel-log.txt")
return sharedFolderURL
}
static var networkExtensionLogFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
}
static var networkExtensionLastErrorFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("last-error.txt")
}
static var appLogFileURL: URL? {
guard let documentDirURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
wg_log(.error, message: "Cannot obtain app documents folder URL")
return nil
}
return documentDirURL.appendingPathComponent("app-log.bin")
}
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)
} catch {
return false
}
return true
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import os.log
public class Logger {
enum LoggerError: Error {
case openFailure
}
static var global: Logger?
var log: OpaquePointer
init(withFilePath filePath: String) throws {
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
self.log = log
}
deinit {
close_log(self.log)
}
func log(message: String) {
write_msg_to_log(log, message.trimmingCharacters(in: .newlines))
}
func writeLog(called ourTag: String, mergedWith otherLogFile: String, called otherTag: String, to targetFile: String) -> Bool {
guard let other = open_log(otherLogFile) else { return false }
let ret = write_logs_to_file(targetFile, log, ourTag, other, otherTag)
close_log(other)
return ret == 0
}
static func configureGlobal(withFilePath filePath: String?) {
if Logger.global != nil {
return
}
guard let filePath = filePath else {
os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error)
return
}
do {
try Logger.global = Logger(withFilePath: filePath)
} catch {
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
return
}
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
let goBackendVersion = WIREGUARD_GO_VERSION
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
}
}
func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
os_log(msg, log: OSLog.default, type: type)
Logger.global?.log(message: "\(msg)")
}
func wg_log(_ type: OSLogType, message msg: String) {
os_log("%{public}s", log: OSLog.default, type: type, msg)
Logger.global?.log(message: msg)
}
+147
View File
@@ -0,0 +1,147 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
*/
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/mman.h>
#include "ringlogger.h"
enum {
MAX_LOG_LINE_LENGTH = 512,
MAX_LINES = 1024,
MAGIC = 0xbeefbabeU
};
struct log_line {
struct timeval tv;
char line[MAX_LOG_LINE_LENGTH];
};
struct log {
struct { uint32_t first, len; } header;
struct log_line lines[MAX_LINES];
uint32_t magic;
};
void write_msg_to_log(struct log *log, const char *msg)
{
struct log_line *line = &log->lines[(log->header.first + log->header.len) % MAX_LINES];
if (log->header.len == MAX_LINES)
log->header.first = (log->header.first + 1) % MAX_LINES;
else
++log->header.len;
gettimeofday(&line->tv, NULL);
strncpy(line->line, msg, MAX_LOG_LINE_LENGTH - 1);
line->line[MAX_LOG_LINE_LENGTH - 1] = '\0';
msync(&log->header, sizeof(log->header), MS_ASYNC);
msync(line, sizeof(*line), MS_ASYNC);
}
static bool first_before_second(const struct log_line *line1, const struct log_line *line2)
{
if (line1->tv.tv_sec <= line2->tv.tv_sec)
return true;
if (line1->tv.tv_sec == line2->tv.tv_sec)
return line1->tv.tv_usec <= line2->tv.tv_usec;
return false;
}
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2)
{
uint32_t i1, i2, len1 = log1->header.len, len2 = log2->header.len;
FILE *file;
int ret;
if (len1 > MAX_LINES)
len1 = MAX_LINES;
if (len2 > MAX_LINES)
len2 = MAX_LINES;
file = fopen(file_name, "w");
if (!file)
return -errno;
for (i1 = 0, i2 = 0;;) {
struct tm tm;
char buf[MAX_LOG_LINE_LENGTH];
const struct log_line *line1 = &log1->lines[(log1->header.first + i1) % MAX_LINES];
const struct log_line *line2 = &log2->lines[(log2->header.first + i2) % MAX_LINES];
const struct log_line *line;
const char *tag;
if (i1 < len1 && (i2 >= len2 || first_before_second(line1, line2))) {
line = line1;
tag = tag1;
++i1;
} else if (i2 < len2 && (i1 >= len1 || first_before_second(line2, line1))) {
line = line2;
tag = tag2;
++i2;
} else {
break;
}
memcpy(buf, line->line, MAX_LOG_LINE_LENGTH);
buf[MAX_LOG_LINE_LENGTH - 1] = '\0';
if (!localtime_r(&line->tv.tv_sec, &tm))
goto err;
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: [%s] %s\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, line->tv.tv_usec,
tag, buf) < 0)
goto err;
}
errno = 0;
err:
ret = -errno;
fclose(file);
return ret;
}
struct log *open_log(const char *file_name)
{
int fd;
struct log *log;
fd = open(file_name, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd < 0)
return NULL;
if (ftruncate(fd, sizeof(*log)))
goto err;
log = mmap(NULL, sizeof(*log), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (log == MAP_FAILED)
goto err;
close(fd);
if (log->magic != MAGIC) {
memset(log, 0, sizeof(*log));
log->magic = MAGIC;
msync(log, sizeof(*log), MS_ASYNC);
}
return log;
err:
close(fd);
return NULL;
}
void close_log(struct log *log)
{
munmap(log, sizeof(*log));
}
+15
View File
@@ -0,0 +1,15 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
*/
#ifndef RINGLOGGER_H
#define RINGLOGGER_H
struct log;
void write_msg_to_log(struct log *log, const char *msg);
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2);
struct log *open_log(const char *file_name);
void close_log(struct log *log);
#endif
+19 -8
View File
@@ -7,13 +7,16 @@ import Foundation
final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength: Int = 32
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
@@ -23,16 +26,20 @@ final class TunnelConfiguration: Codable {
struct InterfaceConfiguration: Codable {
var name: String
var privateKey: Data
var addresses: [IPAddressRange] = []
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns: [DNSServer] = []
var dns = [DNSServer]()
init(name: String, privateKey: Data) {
self.name = name
self.privateKey = privateKey
if (name.isEmpty) { fatalError("Empty name") }
if (privateKey.count != 32) { fatalError("Invalid private key") }
if name.isEmpty {
fatalError("Empty name")
}
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
}
}
@@ -42,16 +49,20 @@ struct PeerConfiguration: Codable {
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if (value.count != 32) { fatalError("Invalid preshared key") }
if value.count != TunnelConfiguration.keyLength {
fatalError("Invalid preshared key")
}
}
}
}
var allowedIPs: [IPAddressRange] = []
var allowedIPs = [IPAddressRange]()
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
init(publicKey: Data) {
self.publicKey = publicKey
if (publicKey.count != 32) { fatalError("Invalid public key") }
if publicKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid public key")
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ extension DNSServer: Codable {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch (data.count) {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
+15 -15
View File
@@ -17,15 +17,15 @@ extension Endpoint {
init?(from string: String) {
// Separation of host and port is based on 'parse_endpoint' function in
// https://git.zx2c4.com/WireGuard/tree/src/tools/config.c
guard (!string.isEmpty) else { return nil }
guard !string.isEmpty else { return nil }
let startOfPort: String.Index
let hostString: String
if (string.first! == "[") {
if string.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = string.index(after: string.startIndex)
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
let afterEndOfHost = string.index(after: endOfHost)
guard (string[afterEndOfHost] == ":") else { return nil }
guard string[afterEndOfHost] == ":" else { return nil }
startOfPort = string.index(after: afterEndOfHost)
hostString = String(string[startOfHost ..< endOfHost])
} else {
@@ -35,15 +35,15 @@ extension Endpoint {
hostString = String(string[string.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { (c) -> Bool in
return !CharacterSet.urlHostAllowed.contains(c)
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard (invalidCharacterIndex == nil) else { return nil }
guard invalidCharacterIndex == nil else { return nil }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
func stringRepresentation() -> String {
switch (host) {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
@@ -61,7 +61,7 @@ extension Endpoint {
extension Endpoint: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.stringRepresentation())
try container.encode(stringRepresentation())
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -78,23 +78,23 @@ extension Endpoint: Codable {
extension Endpoint {
func hasHostAsIPAddress() -> Bool {
switch (host) {
case .name(_, _):
switch host {
case .name:
return false
case .ipv4(_):
case .ipv4:
return true
case .ipv6(_):
case .ipv6:
return true
}
}
func hostname() -> String? {
switch (host) {
switch host {
case .name(let hostname, _):
return hostname
case .ipv4(_):
case .ipv4:
return nil
case .ipv6(_):
case .ipv6:
return nil
}
}
+4 -6
View File
@@ -27,9 +27,9 @@ extension IPAddressRange {
}
let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128
var networkPrefixLength: UInt8
if (endOfIPAddress < string.endIndex) { // "/" was located
if endOfIPAddress < string.endIndex { // "/" was located
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
guard (indexOfNetworkPrefixLength < string.endIndex) else { return nil }
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
networkPrefixLength = min(npl, maxNetworkPrefixLength)
@@ -69,15 +69,13 @@ extension IPAddressRange: Codable {
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch (data.count) {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
address = ipAddress
}
enum DecodingError: Error {
@@ -17,7 +17,7 @@ extension NETunnelProviderProtocol {
"tunnelConfigurationVersion": 1
]
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
let endpoints = tunnelConfiguration.peers.compactMap {$0.endpoint}
if endpoints.count == 1 {
serverAddress = endpoints.first!.stringRepresentation()
} else if endpoints.isEmpty {
+129 -21
View File
@@ -7,8 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; };
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */; };
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418F21C2D53800994C13 /* SwitchCell.swift */; };
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419121C2D55800994C13 /* CheckmarkCell.swift */; };
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419721C2D60500994C13 /* KeyValueCell.swift */; };
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419F21C2D6B700994C13 /* TunnelListCell.swift */; };
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; };
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; };
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
5F4541AE21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.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 */; };
@@ -34,6 +42,7 @@
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; };
6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; };
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; };
6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; };
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; };
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; };
@@ -44,9 +53,12 @@
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 */; };
6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
6FF3527121C240160008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
6FF3527221C2616C0008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
6FF3527321C2616C0008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */; };
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
@@ -91,8 +103,16 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; };
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; };
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditKeyValueCell.swift; sourceTree = "<group>"; };
5F45418F21C2D53800994C13 /* SwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
5F45419121C2D55800994C13 /* CheckmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = "<group>"; };
5F45419721C2D60500994C13 /* KeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueCell.swift; sourceTree = "<group>"; };
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListCell.swift; sourceTree = "<group>"; };
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.swift; sourceTree = "<group>"; };
5F4541A521C4449E00994C13 /* ButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = "<group>"; };
5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = "<group>"; };
5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+CustomStringConvertible.swift"; sourceTree = "<group>"; };
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -124,6 +144,7 @@
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = "<group>"; };
6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; };
6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; };
6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; };
6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; };
@@ -137,6 +158,9 @@
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>"; };
6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; };
6FF3526C21C23F960008484E /* ringlogger.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ringlogger.c; sourceTree = "<group>"; };
6FF3526E21C23FA10008484E /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -144,7 +168,6 @@
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetReachability.swift; sourceTree = "<group>"; };
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
@@ -169,6 +192,33 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5F4541A721C44F5B00994C13 /* View */ = {
isa = PBXGroup;
children = (
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */,
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */,
5F45418F21C2D53800994C13 /* SwitchCell.swift */,
5F45419721C2D60500994C13 /* KeyValueCell.swift */,
5F4541A521C4449E00994C13 /* ButtonCell.swift */,
5F45419121C2D55800994C13 /* CheckmarkCell.swift */,
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
);
path = View;
sourceTree = "<group>";
};
5F4541AC21C4720B00994C13 /* ViewController */ = {
isa = PBXGroup;
children = (
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
);
path = ViewController;
sourceTree = "<group>";
};
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = {
isa = PBXGroup;
children = (
@@ -186,6 +236,7 @@
6F5D0C432183B4A4000F85AD /* Shared */ = {
isa = PBXGroup;
children = (
6FF3526A21C23F720008484E /* Logging */,
6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
@@ -224,16 +275,11 @@
6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup;
children = (
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
5F4541AC21C4720B00994C13 /* ViewController */,
5F4541A721C44F5B00994C13 /* View */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */,
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
);
path = iOS;
sourceTree = "<group>";
@@ -249,14 +295,16 @@
path = Model;
sourceTree = "<group>";
};
6F7774ED21722D0C006A79B3 /* VPN */ = {
6F7774ED21722D0C006A79B3 /* Tunnel */ = {
isa = PBXGroup;
children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */,
5F4541A821C451D100994C13 /* TunnelStatus.swift */,
6FB1017821C57DE600766195 /* MockTunnels.swift */,
5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift */,
);
path = VPN;
path = Tunnel;
sourceTree = "<group>";
};
6F919ED3218C65C50023B400 /* Resources */ = {
@@ -310,6 +358,16 @@
path = minizip;
sourceTree = "<group>";
};
6FF3526A21C23F720008484E /* Logging */ = {
isa = PBXGroup;
children = (
6FF3526C21C23F960008484E /* ringlogger.c */,
6FF3526B21C23F960008484E /* ringlogger.h */,
6FF3526E21C23FA10008484E /* Logger.swift */,
);
path = Logging;
sourceTree = "<group>";
};
6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup;
children = (
@@ -337,7 +395,7 @@
6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */,
6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* VPN */,
6F7774ED21722D0C006A79B3 /* Tunnel */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
@@ -384,6 +442,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
buildPhases = (
5F45417B21C0906F00994C13 /* Swiftlint */,
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
6F5D0C16218352EF000F85AD /* Sources */,
6F5D0C17218352EF000F85AD /* Frameworks */,
@@ -403,6 +462,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
buildPhases = (
5F45417A21C0902400994C13 /* Swiftlint */,
6B87860E2189532500C099FB /* Extract wireguard-go Version */,
6FF4AC10211EC46F002C96EB /* Sources */,
6FF4AC11211EC46F002C96EB /* Frameworks */,
@@ -494,6 +554,42 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
5F45417A21C0902400994C13 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
5F45417B21C0906F00994C13 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -539,6 +635,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6FF3527021C240160008484E /* ringlogger.c in Sources */,
6FF3527121C240160008484E /* Logger.swift in Sources */,
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
@@ -556,15 +654,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
@@ -572,22 +673,29 @@
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
5F4541AE21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */,
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104
VERSION_ID = 5
VERSION_ID = 7
@@ -20,83 +20,17 @@ class WgQuickConfigFileParser {
case invalidPeer
}
//swiftlint:disable:next cyclomatic_complexity function_body_length
static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
assert(!name.isEmpty)
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
// required wg fields
guard let privateKeyString = attributes["privatekey"] else { return nil }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
// other wg fields
if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else { return nil }
interface.listenPort = listenPort
}
// wg-quick fields
if let addressesString = attributes["address"] {
var addresses: [IPAddressRange] = []
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
guard let address = IPAddressRange(from: trimmedString) else { return nil }
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers: [DNSServer] = []
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
dnsServers.append(dnsServer)
}
interface.dns = dnsServers
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else { return nil }
interface.mtu = mtu
}
return interface
}
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields
guard let publicKeyString = attributes["publickey"] else { return nil }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { return nil }
var peer = PeerConfiguration(publicKey: publicKey)
// wg fields
if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil }
peer.preSharedKey = preSharedKey
}
if let allowedIPsString = attributes["allowedips"] {
var allowedIPs: [IPAddressRange] = []
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else { return nil }
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations: [PeerConfiguration] = []
var peerConfigurations = [PeerConfiguration]()
let lines = text.split(separator: "\n")
var parserState: ParserState = .notInASection
var attributes: [String: String] = [:]
var parserState = ParserState.notInASection
var attributes = [String: String]()
for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String
@@ -121,30 +55,28 @@ class WgQuickConfigFileParser {
} else {
attributes[key] = value
}
} else {
if (lowercasedLine != "[interface]" && lowercasedLine != "[peer]") {
throw ParseError.invalidLine(line)
}
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
throw ParseError.invalidLine(line)
}
let isLastLine: Bool = (lineIndex == lines.count - 1)
let isLastLine = lineIndex == lines.count - 1
if (isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]") {
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
// Previous section has ended; process the attributes collected so far
if (parserState == .inInterfaceSection) {
guard let interface = collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
guard (interfaceConfiguration == nil) else { throw ParseError.multipleInterfaces }
if parserState == .inInterfaceSection {
guard let interface = collate(interfaceAttributes: attributes, name: name) else { throw ParseError.invalidInterface }
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
} else if (parserState == .inPeerSection) {
} else if parserState == .inPeerSection {
guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer)
}
}
if (lowercasedLine == "[interface]") {
if lowercasedLine == "[interface]" {
parserState = .inInterfaceSection
attributes.removeAll()
} else if (lowercasedLine == "[peer]") {
} else if lowercasedLine == "[peer]" {
parserState = .inPeerSection
attributes.removeAll()
}
@@ -152,7 +84,7 @@ class WgQuickConfigFileParser {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey
}
@@ -163,4 +95,73 @@ class WgQuickConfigFileParser {
throw ParseError.noInterface
}
}
//swiftlint:disable:next cyclomatic_complexity
private static func collate(interfaceAttributes attributes: [String: String], name: String) -> InterfaceConfiguration? {
// required wg fields
guard let privateKeyString = attributes["privatekey"] else { return nil }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
// other wg fields
if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else { return nil }
interface.listenPort = listenPort
}
// wg-quick fields
if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
guard let address = IPAddressRange(from: trimmedString) else { return nil }
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
dnsServers.append(dnsServer)
}
interface.dns = dnsServers
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else { return nil }
interface.mtu = mtu
}
return interface
}
//swiftlint:disable:next cyclomatic_complexity
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields
guard let publicKeyString = attributes["publickey"] else { return nil }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { return nil }
var peer = PeerConfiguration(publicKey: publicKey)
// wg fields
if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { return nil }
peer.preSharedKey = preSharedKey
}
if let allowedIPsString = attributes["allowedips"] {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else { return nil }
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
}
@@ -4,18 +4,18 @@
import UIKit
class WgQuickConfigFileWriter {
static func writeConfigFile(from tc: TunnelConfiguration) -> Data? {
let interface = tc.interface
static func writeConfigFile(from configuration: TunnelConfiguration) -> Data? {
let interface = configuration.interface
var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n")
}
if (!interface.addresses.isEmpty) {
if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if (!interface.dns.isEmpty) {
if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
@@ -23,13 +23,13 @@ class WgQuickConfigFileWriter {
output.append("MTU = \(mtu)\n")
}
for peer in tc.peers {
for peer in configuration.peers {
output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
if let preSharedKey = peer.preSharedKey {
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
}
if (!peer.allowedIPs.isEmpty) {
if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
+11 -8
View File
@@ -4,24 +4,27 @@
import UIKit
struct Curve25519 {
static let keyLength: Int = 32
static func generatePrivateKey() -> Data {
var privateKey = Data(repeating: 0, count: 32)
privateKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeMutableBytes { bytes in
curve25519_generate_private_key(bytes)
}
assert(privateKey.count == 32)
assert(privateKey.count == TunnelConfiguration.keyLength)
return privateKey
}
static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data {
assert(privateKey.count == 32)
var publicKey = Data(repeating: 0, count: 32)
privateKey.withUnsafeBytes { (privateKeyBytes: UnsafePointer<UInt8>) in
publicKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
assert(privateKey.count == TunnelConfiguration.keyLength)
var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeBytes { privateKeyBytes in
publicKey.withUnsafeMutableBytes { bytes in
curve25519_derive_public_key(bytes, privateKeyBytes)
}
}
assert(publicKey.count == 32)
assert(publicKey.count == TunnelConfiguration.keyLength)
return publicKey
}
}
@@ -21,7 +21,7 @@ extension ActivateOnDemandSetting {
let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect()
switch (activateOnDemandOption) {
switch activateOnDemandOption {
case .none:
rules = nil
case .useOnDemandOverWiFiOrCellular:
@@ -41,7 +41,7 @@ extension ActivateOnDemandSetting {
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption
switch (rules.count) {
switch rules.count {
case 0:
activateOnDemandOption = .none
case 1:
@@ -51,9 +51,9 @@ extension ActivateOnDemandSetting {
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) {
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
activateOnDemandOption = .useOnDemandOverWiFiOnly
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) {
} else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .useOnDemandOverCellularOnly
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
@@ -62,10 +62,10 @@ extension ActivateOnDemandSetting {
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self.activateOnDemandOption = activateOnDemandOption
if (activateOnDemandOption == .none) {
self.isActivateOnDemandEnabled = false
if activateOnDemandOption == .none {
isActivateOnDemandEnabled = false
} else {
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
}
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
// Creates mock tunnels for the iOS Simulator.
#if targetEnvironment(simulator)
class MockTunnels {
static let tunnelNames = [
"demo",
"edgesecurity",
"home",
"office",
"infra-fr",
"infra-us",
"krantz",
"metheny",
"frisell"
]
static let address = "192.168.%d.%d/32"
static let dnsServers = ["8.8.8.8", "8.8.4.4"]
static let endpoint = "demo.wireguard.com:51820"
static let allowedIPs = "0.0.0.0/0"
static func createMockTunnels() -> [NETunnelProviderManager] {
return tunnelNames.map { tunnelName -> NETunnelProviderManager in
var interface = InterfaceConfiguration(name: tunnelName, privateKey: Curve25519.generatePrivateKey())
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
interface.dns = dnsServers.map { DNSServer(from: $0)! }
var peer = PeerConfiguration(publicKey: Curve25519.generatePublicKey(fromPrivateKey: Curve25519.generatePrivateKey()))
peer.endpoint = Endpoint(from: endpoint)
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
let tunnelConfiguration = TunnelConfiguration(interface: interface, peers: [peer])
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
return tunnelProviderManager
}
}
}
#endif
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
extension NEVPNStatus: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .connected: return "connected"
case .connecting: return "connecting"
case .disconnected: return "disconnected"
case .disconnecting: return "disconnecting"
case .reasserting: return "reasserting"
case .invalid: return "invalid"
}
}
}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
@objc enum TunnelStatus: Int {
case inactive
case activating
case active
case deactivating
case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case waiting // Waiting for another tunnel to be brought down
init(from systemStatus: NEVPNStatus) {
switch systemStatus {
case .connected:
self = .active
case .connecting:
self = .activating
case .disconnected:
self = .inactive
case .disconnecting:
self = .deactivating
case .reasserting:
self = .reasserting
case .invalid:
self = .inactive
}
}
}
extension TunnelStatus: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .inactive: return "inactive"
case .activating: return "activating"
case .active: return "active"
case .deactivating: return "deactivating"
case .reasserting: return "reasserting"
case .restarting: return "restarting"
case .waiting: return "waiting"
}
}
}
@@ -0,0 +1,487 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os.log
protocol TunnelsManagerListDelegate: class {
func tunnelAdded(at index: Int)
func tunnelModified(at index: Int)
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at index: Int)
}
protocol TunnelsManagerActivationDelegate: class {
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
}
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
case tunnelIsNotInactive
case anotherTunnelIsOperational(otherTunnelName: String)
case failedWhileStarting // startTunnel() throwed
case failedWhileSaving // save config after re-enabling throwed
case failedWhileLoading // reloading config throwed
case failedBecauseOfTooManyErrors // recursion limit reached
var alertText: AlertText {
switch self {
case .tunnelIsNotInactive:
return ("Activation failure", "The tunnel is already active or in the process of being activated")
case .anotherTunnelIsOperational(let otherTunnelName):
return ("Activation failure", "Please disconnect '\(otherTunnelName)' before enabling this tunnel.")
case .failedWhileStarting, .failedWhileSaving, .failedWhileLoading, .failedBecauseOfTooManyErrors:
return ("Activation failure", "The tunnel could not be activated.")
}
}
}
enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed
case activationFailedWithExtensionError(title: String, message: String)
var alertText: AlertText {
switch self {
case .activationFailed:
return ("Activation failure", "The tunnel could not be activated. Please ensure that you are connected to the Internet.")
case .activationFailedWithExtensionError(let title, let message):
return (title, message)
}
}
}
enum TunnelsManagerError: WireGuardAppError {
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case systemErrorOnListingTunnels
case systemErrorOnAddTunnel
case systemErrorOnModifyTunnel
case systemErrorOnRemoveTunnel
var alertText: AlertText {
switch self {
case .tunnelNameEmpty:
return ("No name provided", "Cannot create tunnel with an empty name")
case .tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists")
case .systemErrorOnListingTunnels:
return ("Unable to list tunnels", "")
case .systemErrorOnAddTunnel:
return ("Unable to create tunnel", "")
case .systemErrorOnModifyTunnel:
return ("Unable to modify tunnel", "")
case .systemErrorOnRemoveTunnel:
return ("Unable to remove tunnel", "")
}
}
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject?
init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
startObservingTunnelStatuses()
}
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
#if targetEnvironment(simulator)
completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
#else
NETunnelProviderManager.loadAllFromPreferences { managers, error in
if let error = error {
wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels))
return
}
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
}
#endif
}
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
return
}
if tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
return
}
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel))
return
}
guard let self = self else { return }
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel))
}
}
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
}
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful)
return
}
let tail = tunnelConfigurations.dropFirst()
add(tunnelConfiguration: head) { [weak self, tail] result in
DispatchQueue.main.async {
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
}
}
}
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty)
return
}
let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription
if isNameChanged {
guard !tunnels.contains(where: { $0.name == tunnelName }) else {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return
}
tunnel.name = tunnelName
}
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
return
}
guard let self = self else { return }
if isNameChanged {
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { $0.name < $1.name }
let newIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
}
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
// Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.status = .restarting
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
if isActivatingOnDemand {
// Reload tunnel after saving.
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
tunnelProviderManager.loadFromPreferences { error in
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
guard error == nil else {
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
return
}
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider
tunnelProviderManager.removeFromPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel)
return
}
if let self = self {
let index = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.remove(at: index)
self.tunnelsListDelegate?.tunnelRemoved(at: index)
}
completionHandler(nil)
}
}
func numberOfTunnels() -> Int {
return tunnels.count
}
func tunnel(at index: Int) -> TunnelContainer {
return tunnels[index]
}
func tunnel(named tunnelName: String) -> TunnelContainer? {
return tunnels.first { $0.name == tunnelName }
}
func startActivation(of tunnel: TunnelContainer) {
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
guard tunnel.status == .inactive else {
activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
return
}
if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
alreadyWaitingTunnel.status = .inactive
}
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
tunnel.status = .waiting
if tunnelInOperation.status != .deactivating {
startDeactivation(of: tunnelInOperation)
}
return
}
#if targetEnvironment(simulator)
tunnel.status = .active
#else
tunnel.startActivation(activationDelegate: activationDelegate)
#endif
}
func startDeactivation(of tunnel: TunnelContainer) {
tunnel.isAttemptingActivation = false
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
#if targetEnvironment(simulator)
tunnel.status = .inactive
#else
tunnel.startDeactivation()
#endif
}
func refreshStatuses() {
tunnels.forEach { $0.refreshStatus() }
}
private func startObservingTunnelStatuses() {
guard statusObservationToken == nil else { return }
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager,
let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
// Track what happened to our attempt to start the tunnel
if tunnel.isAttemptingActivation {
if session.status == .connected {
tunnel.isAttemptingActivation = false
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
} else if session.status == .disconnected {
tunnel.isAttemptingActivation = false
if let (title, message) = self.lastErrorTextFromNetworkExtension(for: tunnel) {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message))
} else {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed)
}
}
}
// In case we're restarting the tunnel
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
// Don't change tunnel.status when disconnecting for a restart
if session.status == .disconnected {
tunnel.startActivation(activationDelegate: self.activationDelegate)
}
return
}
tunnel.refreshStatus()
// In case some other tunnel is waiting for this tunnel to get deactivated
if session.status == .disconnected || session.status == .invalid {
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
}
}
}
}
func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
guard let lastErrorText = String(data: lastErrorData, encoding: .utf8) else { return nil }
let lastErrorStrings = lastErrorText.split(separator: "\n").map { String($0) }
guard lastErrorStrings.count == 3 else { return nil }
let attemptIdInDisk = lastErrorStrings[0]
if let attemptIdForTunnel = tunnel.activationAttemptId, attemptIdInDisk == attemptIdForTunnel {
return (title: lastErrorStrings[1], message: lastErrorStrings[2])
}
return nil
}
deinit {
if let statusObservationToken = statusObservationToken {
NotificationCenter.default.removeObserver(statusObservationToken)
}
}
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool
var isAttemptingActivation = false {
didSet {
if isAttemptingActivation {
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.refreshStatus()
if self.status == .inactive || self.status == .active {
self.isAttemptingActivation = false // This also invalidates the timer
}
}
self.activationTimer = activationTimer
RunLoop.main.add(activationTimer, forMode: .default)
} else {
activationTimer?.invalidate()
activationTimer = nil
}
}
}
var activationAttemptId: String?
var activationTimer: Timer?
fileprivate let tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus?
init(tunnel: NETunnelProviderManager) {
name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status)
self.status = status
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
tunnelProvider = tunnel
super.init()
}
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.tunnelConfiguration()
}
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshStatus() {
let status = TunnelStatus(from: tunnelProvider.connection.status)
self.status = status
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
}
//swiftlint:disable:next function_body_length
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
if recursionCount >= 8 {
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors)
return
}
wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
guard tunnelProvider.isEnabled else {
// In case the tunnel had gotten disabled, re-enable and save it,
// then call this function again.
wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
tunnelProvider.isEnabled = true
tunnelProvider.saveToPreferences { [weak self] error in
guard let self = self else { return }
if error != nil {
wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving)
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
}
return
}
// Start the tunnel
do {
wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
isAttemptingActivation = true
let activationAttemptId = UUID().uuidString
self.activationAttemptId = activationAttemptId
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
wg_log(.debug, staticMessage: "startActivation: Success")
activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
} catch let error {
isAttemptingActivation = false
guard let systemError = error as? NEVPNError else {
wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
return
}
guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
return
}
wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
tunnelProvider.loadFromPreferences { [weak self] error in
guard let self = self else { return }
if error != nil {
wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
self.status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading)
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
}
}
}
fileprivate func startDeactivation() {
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
}
+63 -66
View File
@@ -3,6 +3,7 @@
import UIKit
//swiftlint:disable:next type_body_length
class TunnelViewModel {
enum InterfaceField: String {
@@ -37,13 +38,13 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44
class InterfaceData {
var scratchpad: [InterfaceField: String] = [:]
var fieldsWithError: Set<InterfaceField> = []
var scratchpad = [InterfaceField: String]()
var fieldsWithError = Set<InterfaceField>()
var validatedConfiguration: InterfaceConfiguration?
subscript(field: InterfaceField) -> String {
get {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad()
@@ -51,21 +52,19 @@ class TunnelViewModel {
return scratchpad[field] ?? ""
}
set(stringValue) {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
if stringValue.isEmpty {
scratchpad.removeValue(forKey: field)
} else {
scratchpad[field] = stringValue
}
if (field == .privateKey) {
if (stringValue.count == TunnelViewModel.keyLengthInBase64),
let privateKey = Data(base64Encoded: stringValue),
privateKey.count == 32 {
if field == .privateKey {
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
scratchpad[.publicKey] = publicKey.base64EncodedString()
} else {
@@ -81,7 +80,7 @@ class TunnelViewModel {
scratchpad[.name] = config.name
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if (!config.addresses.isEmpty) {
if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
}
if let listenPort = config.listenPort {
@@ -90,11 +89,12 @@ class TunnelViewModel {
if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu)
}
if (!config.dns.isEmpty) {
if !config.dns.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
}
}
//swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
@@ -109,14 +109,14 @@ class TunnelViewModel {
fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required")
}
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else {
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.privateKey)
return .error("Interface's private key must be a 32-byte key in base64 encoding")
}
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
var errorMessages: [String] = []
var errorMessages = [String]()
if let addressesString = scratchpad[.addresses] {
var addresses: [IPAddressRange] = []
var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let address = IPAddressRange(from: trimmedString) {
@@ -145,7 +145,7 @@ class TunnelViewModel {
}
}
if let dnsString = scratchpad[.dns] {
var dnsServers: [DNSServer] = []
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let dnsServer = DNSServer(from: trimmedString) {
@@ -158,16 +158,15 @@ class TunnelViewModel {
config.dns = dnsServers
}
guard (errorMessages.isEmpty) else {
return .error(errorMessages.first!)
}
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config
return .saved(config)
}
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
return interfaceFields.filter { (field) -> Bool in
if (TunnelViewModel.interfaceFieldsWithControl.contains(field)) {
return interfaceFields.filter { field in
if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
return true
}
return (!self[field].isEmpty)
@@ -178,14 +177,14 @@ class TunnelViewModel {
class PeerData {
var index: Int
var scratchpad: [PeerField: String] = [:]
var fieldsWithError: Set<PeerField> = []
var scratchpad = [PeerField: String]()
var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration?
// For exclude private IPs
var shouldAllowExcludePrivateIPsControl: Bool = false /* Read-only from the VC's point of view */
var excludePrivateIPsValue: Bool = false /* Read-only from the VC's point of view */
fileprivate var numberOfPeers: Int = 0
private(set) var shouldAllowExcludePrivateIPsControl = false
private(set) var excludePrivateIPsValue = false
fileprivate var numberOfPeers = 0
init(index: Int) {
self.index = index
@@ -193,7 +192,7 @@ class TunnelViewModel {
subscript(field: PeerField) -> String {
get {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad()
@@ -201,18 +200,18 @@ class TunnelViewModel {
return scratchpad[field] ?? ""
}
set(stringValue) {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
if stringValue.isEmpty {
scratchpad.removeValue(forKey: field)
} else {
scratchpad[field] = stringValue
}
if (field == .allowedIPs) {
if field == .allowedIPs {
updateExcludePrivateIPsFieldState()
}
}
@@ -225,7 +224,7 @@ class TunnelViewModel {
if let preSharedKey = config.preSharedKey {
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
}
if (!config.allowedIPs.isEmpty) {
if !config.allowedIPs.isEmpty {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
}
if let endpoint = config.endpoint {
@@ -237,6 +236,7 @@ class TunnelViewModel {
updateExcludePrivateIPsFieldState()
}
//swiftlint:disable:next cyclomatic_complexity
func save() -> SaveResult<PeerConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
@@ -247,14 +247,14 @@ class TunnelViewModel {
fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required")
}
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else {
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.publicKey)
return .error("Peer's public key must be a 32-byte key in base64 encoding")
}
var config = PeerConfiguration(publicKey: publicKey)
var errorMessages: [String] = []
var errorMessages = [String]()
if let preSharedKeyString = scratchpad[.preSharedKey] {
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 {
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
config.preSharedKey = preSharedKey
} else {
fieldsWithError.insert(.preSharedKey)
@@ -262,7 +262,7 @@ class TunnelViewModel {
}
}
if let allowedIPsString = scratchpad[.allowedIPs] {
var allowedIPs: [IPAddressRange] = []
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let allowedIP = IPAddressRange(from: trimmedString) {
@@ -291,16 +291,15 @@ class TunnelViewModel {
}
}
guard (errorMessages.isEmpty) else {
return .error(errorMessages.first!)
}
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config
return .saved(config)
}
func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
return peerFields.filter { (field) -> Bool in
if (TunnelViewModel.peerFieldsWithControl.contains(field)) {
return peerFields.filter { field in
if TunnelViewModel.peerFieldsWithControl.contains(field) {
return true
}
return (!self[field].isEmpty)
@@ -319,12 +318,12 @@ class TunnelViewModel {
]
func updateExcludePrivateIPsFieldState() {
guard (numberOfPeers == 1) else {
guard numberOfPeers == 1 else {
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false
return
}
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(
@@ -332,10 +331,10 @@ class TunnelViewModel {
.split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
)
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) {
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = false
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) {
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = true
} else {
@@ -353,12 +352,10 @@ class TunnelViewModel {
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
let modifiedAllowedIPStrings: [String]
if (isOn) {
modifiedAllowedIPStrings = ipv6Addresses +
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
if isOn {
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
} else {
modifiedAllowedIPStrings = ipv6Addresses +
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
}
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
@@ -376,11 +373,11 @@ class TunnelViewModel {
init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData: InterfaceData = InterfaceData()
var peersData: [PeerData] = []
var peersData = [PeerData]()
if let tunnelConfiguration = tunnelConfiguration {
interfaceData.validatedConfiguration = tunnelConfiguration.interface
for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: i)
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: index)
peerData.validatedConfiguration = peerConfiguration
peersData.append(peerData)
}
@@ -397,22 +394,22 @@ class TunnelViewModel {
func appendEmptyPeer() {
let peer = PeerData(index: peersData.count)
peersData.append(peer)
for p in peersData {
p.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState()
for peer in peersData {
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
}
func deletePeer(peer: PeerData) {
let removedPeer = peersData.remove(at: peer.index)
assert(removedPeer.index == peer.index)
for p in peersData[peer.index ..< peersData.count] {
assert(p.index > 0)
p.index = p.index - 1
for peer in peersData[peer.index ..< peersData.count] {
assert(peer.index > 0)
peer.index -= 1
}
for p in peersData {
p.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState()
for peer in peersData {
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
}
@@ -421,14 +418,14 @@ class TunnelViewModel {
let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() }
// Collate the results
switch (interfaceSaveResult) {
switch interfaceSaveResult {
case .error(let errorMessage):
return .error(errorMessage)
case .saved(let interfaceConfiguration):
var peerConfigurations: [PeerConfiguration] = []
var peerConfigurations = [PeerConfiguration]()
peerConfigurations.reserveCapacity(peerSaveResults.count)
for peerSaveResult in peerSaveResults {
switch (peerSaveResult) {
switch peerSaveResult {
case .error(let errorMessage):
return .error(errorMessage)
case .saved(let peerConfiguration):
@@ -438,7 +435,7 @@ class TunnelViewModel {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
if peerPublicKeysArray.count != peerPublicKeysSet.count {
return .error("Two or more peers cannot have the same public key")
}
@@ -452,7 +449,7 @@ class TunnelViewModel {
extension TunnelViewModel {
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch (activateOnDemandOption) {
switch activateOnDemandOption {
case .none:
return "Off"
case .useOnDemandOverWiFiOrCellular:
@@ -466,7 +463,7 @@ extension TunnelViewModel {
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting {
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
if activateOnDemandSetting.isActivateOnDemandEnabled {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
+6 -9
View File
@@ -12,9 +12,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white
window.backgroundColor = .white
self.window = window
let mainVC = MainViewController()
@@ -27,8 +28,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
mainVC?.tunnelsListVC?.importFromFile(url: url)
_ = FileManager.deleteFile(at: url)
mainVC?.tunnelsListVC?.importFromFile(url: url) {
_ = FileManager.deleteFile(at: url)
}
return true
}
@@ -52,11 +54,7 @@ extension AppDelegate {
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:")) {
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) {
@@ -66,7 +64,6 @@ extension AppDelegate {
// Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
}
}
return nil
}
@@ -1,55 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class CopyableLabelTableViewCell: UITableViewCell {
var copyableGesture = true
var textToCopy: String? {
fatalError("textToCopy must be implemented by subclass")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
self.addGestureRecognizer(gestureRecognizer)
self.isUserInteractionEnabled = true
}
// MARK: - UIGestureRecognizer
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !self.copyableGesture {
return
}
guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared
menuController.setTargetRect(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.detailTextLabel?.superview ?? recognizerSuperView)
menuController.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = textToCopy
}
override func prepareForReuse() {
super.prepareForReuse()
self.copyableGesture = true
}
}
@@ -5,11 +5,15 @@ import UIKit
import os.log
class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?,
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
let (title, message) = error.alertText
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
}
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
let (title, message) = error.alertText()
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
onDismissal?()
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
@@ -17,15 +21,4 @@ class ErrorPresenter {
sourceVC.present(alert, animated: true, completion: onPresented)
}
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
onDismissal?()
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
sourceVC.present(alert, animated: true)
}
}
@@ -1,50 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class ScrollableLabel: UIScrollView {
var text: String {
get { return label.text ?? "" }
set(value) { label.text = value }
}
var textColor: UIColor {
get { return label.textColor }
set(value) { label.textColor = value }
}
private let label: UILabel
init() {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
self.label = label
super.init(frame: CGRect.zero)
self.isDirectionalLockEnabled = true
self.showsHorizontalScrollIndicator = false
self.showsVerticalScrollIndicator = false
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.leftAnchor.constraint(equalTo: self.contentLayoutGuide.leftAnchor),
label.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor),
label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor),
label.heightAnchor.constraint(equalTo: self.heightAnchor),
])
// If label has less content, it should expand to fit the scrollView,
// so that right-alignment works in the label.
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal,
toItem: self, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -1,257 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
enum SettingsFields: String {
case iosAppVersion = "WireGuard for iOS"
case goBackendVersion = "WireGuard Go Backend"
case exportZipArchive = "Export zip archive"
case exportLogFile = "Export log file"
}
let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion],
[.exportZipArchive],
[.exportLogFile]
]
let tunnelsManager: TunnelsManager?
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
init(tunnelsManager: TunnelsManager?) {
self.tunnelsManager = tunnelsManager
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Settings"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
self.tableView.rowHeight = 44
self.tableView.allowsSelection = false
self.tableView.register(TunnelSettingsTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
self.tableView.register(TunnelSettingsTableViewButtonCell.self, forCellReuseIdentifier: TunnelSettingsTableViewButtonCell.id)
let logo = UIImageView(image: UIImage(named: "wireguard.pdf", in: Bundle.main, compatibleWith: nil)!)
logo.contentMode = .scaleAspectFit
let height = self.tableView.rowHeight * 1.5
let width = height * logo.image!.size.width / logo.image!.size.height
logo.frame = CGRect(x: 0, y: 0, width: width, height: height)
logo.bounds = logo.frame.insetBy(dx: 2, dy: 2)
self.tableView.tableFooterView = logo
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let logo = self.tableView.tableFooterView else { return }
let bottomPadding = max(self.tableView.layoutMargins.bottom, CGFloat(10))
let fullHeight = max(self.tableView.contentSize.height, self.tableView.bounds.size.height - self.tableView.layoutMargins.top - bottomPadding)
let e = logo.frame
logo.frame = CGRect(x: e.minX, y: fullHeight - e.height, width: e.width, height: e.height)
}
@objc func doneTapped() {
dismiss(animated: true, completion: nil)
}
func exportConfigurationsAsZipFile(sourceView: UIView) {
guard let tunnelsManager = tunnelsManager else { return }
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
_ = FileManager.deleteFile(at: destinationURL)
let count = tunnelsManager.numberOfTunnels()
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() }
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] (error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
self?.present(fileExportVC, animated: true, completion: nil)
}
}
func exportLogForLastActivatedTunnel(sourceView: UIView) {
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
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)
}
}
}
}
// MARK: UITableViewDataSource
extension SettingsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return settingsFieldsBySection.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settingsFieldsBySection[section].count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch (section) {
case 0:
return "About"
case 1:
return "Export configurations"
case 2:
return "Tunnel log"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
if (field == .iosAppVersion || field == .goBackendVersion) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewKeyValueCell.id, for: indexPath) as! TunnelSettingsTableViewKeyValueCell
cell.key = field.rawValue
if (field == .iosAppVersion) {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
cell.value = appVersion
} else if (field == .goBackendVersion) {
cell.value = WIREGUARD_GO_VERSION
}
return cell
} else if (field == .exportZipArchive) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
}
return cell
} else {
assert(field == .exportLogFile)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
}
return cell
}
}
}
class TunnelSettingsTableViewKeyValueCell: UITableViewCell {
static let id: String = "TunnelSettingsTableViewKeyValueCell"
var key: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var value: String {
get { return detailTextLabel?.text ?? "" }
set(value) { detailTextLabel?.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelSettingsTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelSettingsTableViewButtonCell"
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
let button: UIButton
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
}
}
@@ -1,436 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController {
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = tunnelViewModel.interfaceData[.name]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
self.tableView.rowHeight = 44
self.tableView.allowsSelection = false
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id)
self.tableView.register(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.id)
// State restoration
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
@objc func editTapped() {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
self.present(alert, animated: true, completion: nil)
}
}
// MARK: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
self.title = tunnel.name
self.tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 4 + tunnelViewModel.peersData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
return 1
} else if (section == 1) {
// Interface
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerData = tunnelViewModel.peersData[section - 2]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else if (section < (3 + numberOfPeerSections)) {
// Activate on demand
return 1
} else {
assert(section == (3 + numberOfPeerSections))
// Delete tunnel
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
return "Status"
} else if (section == 1) {
// Interface
return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
return "Peer"
} else if (section < (3 + numberOfPeerSections)) {
// On-Demand Activation
return "On-Demand Activation"
} else {
// Delete tunnel
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let interfaceData = tunnelViewModel.interfaceData
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section == 0) {
// Status
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewStatusCell.id, for: indexPath) as! TunnelDetailTableViewStatusCell
cell.tunnel = self.tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let s = self else { return }
if (isOn) {
s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
DispatchQueue.main.async {
cell.statusSwitch.isOn = false
}
})
}
}
} else {
s.tunnelsManager.startDeactivation(of: s.tunnel)
}
}
return cell
} else if (section == 1) {
// Interface
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
// Set key and value
cell.key = field.rawValue
cell.value = interfaceData[field]
return cell
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerData = tunnelViewModel.peersData[section - 2]
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
// Set key and value
cell.key = field.rawValue
cell.value = peerData[field]
return cell
} else if (section < (3 + numberOfPeerSections)) {
// On-Demand Activation
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewActivateOnDemandCell.id, for: indexPath) as! TunnelDetailTableViewActivateOnDemandCell
cell.tunnel = self.tunnel
return cell
} else {
assert(section == (3 + numberOfPeerSections))
// Delete configuration
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let s = self else { return }
s.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak s] in
guard let tunnelsManager = s?.tunnelsManager, let tunnel = s?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { (error) in
if (error != nil) {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
s?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
}
}
}
class TunnelDetailTableViewStatusCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewStatusCell"
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
self?.update(from: tunnel.status)
}
}
}
var isSwitchInteractionEnabled: Bool {
get { return statusSwitch.isUserInteractionEnabled }
set(value) { statusSwitch.isUserInteractionEnabled = value }
}
var onSwitchToggled: ((Bool) -> Void)?
private var isOnSwitchToggledHandlerEnabled: Bool = true
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
statusSwitch = UISwitch()
super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
accessoryView = statusSwitch
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
if (isOnSwitchToggledHandlerEnabled) {
onSwitchToggled?(statusSwitch.isOn)
}
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
let text: String
switch (status) {
case .inactive:
text = "Inactive"
case .activating:
text = "Activating"
case .active:
text = "Active"
case .deactivating:
text = "Deactivating"
case .reasserting:
text = "Reactivating"
case .restarting:
text = "Restarting"
case .waiting:
text = "Waiting"
}
textLabel?.text = text
DispatchQueue.main.async { [weak statusSwitch] in
guard let statusSwitch = statusSwitch else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
}
textLabel?.textColor = (status == .active || status == .inactive) ? UIColor.black : UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
textLabel?.text = "Invalid"
statusSwitch.isOn = false
textLabel?.textColor = UIColor.gray
statusSwitch.isUserInteractionEnabled = false
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
override var textToCopy: String? {
return self.valueLabel.text
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueLabel = ScrollableLabel()
keyLabel.textColor = UIColor.black
valueLabel.textColor = UIColor.gray
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .left
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 8)
])
// Key label should never appear truncated
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
// Key label should hug it's content; value label should not.
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelDetailTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewButtonCell"
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
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 = ""
}
}
@@ -1,773 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
protocol TunnelEditTableViewControllerDelegate: class {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}
// MARK: TunnelEditTableViewController
class TunnelEditTableViewController: UITableViewController {
weak var delegate: TunnelEditTableViewControllerDelegate?
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
[.name],
[.privateKey, .publicKey, .generateKeyPair],
[.addresses, .listenPort, .mtu, .dns]
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
.deletePeer
]
let activateOnDemandOptions: [ActivateOnDemandOption] = [
.useOnDemandOverWiFiOrCellular,
.useOnDemandOverWiFiOnly,
.useOnDemandOverCellularOnly
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
activateOnDemandSetting = t.activateOnDemandSetting()
super.init(style: .grouped)
}
init(tunnelsManager tm: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
// Use this initializer to create a new tunnel.
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
tunnelsManager = tm
tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = (tunnel == nil) ? "New configuration" : "Edit configuration"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
self.tableView.rowHeight = 44
self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id)
self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id)
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.id)
}
@objc func saveTapped() {
self.tableView.endEditing(false)
let tunnelSaveResult = tunnelViewModel.save()
switch (tunnelSaveResult) {
case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer"
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self)
self.tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration):
if let tunnel = tunnel {
// We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel,
tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] (error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
} else {
// We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
let tunnel: TunnelContainer = result.value!
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
}
}
}
@objc func cancelTapped() {
dismiss(animated: true, completion: nil)
self.delegate?.tunnelEditingCancelled()
}
}
// MARK: UITableViewDataSource
extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
return numberOfInterfaceSections + numberOfPeerSections + 1 /* Add Peer */ + 1 /* On-Demand */
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) {
// Interface
return interfaceFieldsBySection[section].count
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
return peerFieldsToShow.count
} else if (section < (numberOfInterfaceSections + numberOfPeerSections + 1)) {
// Add peer
return 1
} else {
// On-Demand Rules
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
return 4
} else {
return 1
}
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) {
// Interface
return (section == 0) ? "Interface" : nil
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
return "Peer"
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
// Add peer
return nil
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
return "On-Demand Activation"
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section < numberOfInterfaceSections) {
// Interface
let interfaceData = tunnelViewModel.interfaceData
let field = interfaceFieldsBySection[section][row]
if (field == .generateKeyPair) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self, weak interfaceData] in
if let interfaceData = interfaceData, let s = self {
interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
if let privateKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .privateKey),
let publicKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: section)
let publicKeyIndex = IndexPath(row: publicKeyRow, section: section)
s.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .automatic)
}
}
}
return cell
} else if (field == .publicKey) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id, for: indexPath) as! TunnelEditTableViewReadOnlyKeyValueCell
cell.key = field.rawValue
cell.value = interfaceData[field]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue
// Set placeholder text
switch (field) {
case .name:
cell.placeholderText = "Required"
case .privateKey:
cell.placeholderText = "Required"
case .addresses:
cell.placeholderText = "Optional"
case .listenPort:
cell.placeholderText = "Automatic"
case .mtu:
cell.placeholderText = "Automatic"
case .dns:
cell.placeholderText = "Optional"
case .publicKey: break
case .generateKeyPair: break
}
// Set keyboardType
if (field == .mtu || field == .listenPort) {
cell.keyboardType = .numberPad
} else if (field == .addresses || field == .dns) {
cell.keyboardType = .numbersAndPunctuation
}
// Show erroring fields
cell.isValueValid = (!interfaceData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = interfaceData[field]
if (field == .dns) { // While editing DNS, you might directly set exclude private IPs
cell.onValueBeingEdited = { [weak interfaceData] value in
interfaceData?[field] = value
}
} else {
cell.onValueChanged = { [weak interfaceData] value in
interfaceData?[field] = value
}
}
// Compute public key live
if (field == .privateKey) {
cell.onValueBeingEdited = { [weak self, weak interfaceData] value in
if let interfaceData = interfaceData, let s = self {
interfaceData[.privateKey] = value
if let row = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
}
}
}
}
return cell
}
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
let field = peerFieldsToShow[row]
if (field == .deletePeer) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = field.rawValue
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return }
guard let s = self else { return }
s.showConfirmationAlert(message: "Delete this peer?",
buttonTitle: "Delete", from: cell,
onConfirmed: { [weak s] in
guard let s = s else { return }
let removedSectionIndices = s.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
tableView.performBatchUpdates({
s.tableView.deleteSections(removedSectionIndices, with: .automatic)
if (shouldShowExcludePrivateIPs) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
s.tableView.insertRows(at: [rowIndexPath], with: .automatic)
}
}
})
})
}
return cell
} else if (field == .excludePrivateIPs) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] (isOn) in
guard let s = self else { return }
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: s.tunnelViewModel.interfaceData[.dns])
if let row = s.peerFields.firstIndex(of: .allowedIPs) {
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
}
}
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue
// Set placeholder text
switch (field) {
case .publicKey:
cell.placeholderText = "Required"
case .preSharedKey:
cell.placeholderText = "Optional"
case .endpoint:
cell.placeholderText = "Optional"
case .allowedIPs:
cell.placeholderText = "Optional"
case .persistentKeepAlive:
cell.placeholderText = "Off"
case .excludePrivateIPs: break
case .deletePeer: break
}
// Set keyboardType
if (field == .persistentKeepAlive) {
cell.keyboardType = .numberPad
} else if (field == .allowedIPs) {
cell.keyboardType = .numbersAndPunctuation
}
// Show erroring fields
cell.isValueValid = (!peerData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = peerData[field]
if (field != .allowedIPs) {
cell.onValueChanged = { [weak peerData] value in
peerData?[field] = value
}
}
// Compute state of exclude private IPs live
if (field == .allowedIPs) {
cell.onValueBeingEdited = { [weak self, weak peerData] value in
if let peerData = peerData, let s = self {
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
peerData[.allowedIPs] = value
if (oldValue != peerData.shouldAllowExcludePrivateIPsControl) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
if (peerData.shouldAllowExcludePrivateIPsControl) {
s.tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .automatic)
} else {
s.tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .automatic)
}
}
}
}
}
}
return cell
}
} else if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
// Add peer
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in
guard let s = self else { return }
let shouldHideExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
let addedSectionIndices = s.appendEmptyPeer()
tableView.performBatchUpdates({
tableView.insertSections(addedSectionIndices, with: .automatic)
if (shouldHideExcludePrivateIPs) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
s.tableView.deleteRows(at: [rowIndexPath], with: .automatic)
}
}
}, completion: nil)
}
return cell
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
if (row == 0) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] (isOn) in
guard let s = self else { return }
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
if (isOn) {
s.activateOnDemandSetting.isActivateOnDemandEnabled = true
if (s.activateOnDemandSetting.activateOnDemandOption == .none) {
s.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
}
s.tableView.insertRows(at: indexPaths, with: .automatic)
} else {
s.activateOnDemandSetting.isActivateOnDemandEnabled = false
s.tableView.deleteRows(at: indexPaths, with: .automatic)
}
}
return cell
} else {
assert(row < 4)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSelectionListCell.id, for: indexPath) as! TunnelEditTableViewSelectionListCell
let rowOption = activateOnDemandOptions[row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = (selectedOption == rowOption)
return cell
}
}
}
func appendEmptyPeer() -> IndexSet {
let numberOfInterfaceSections = interfaceFieldsBySection.count
tunnelViewModel.appendEmptyPeer()
let addedPeerIndex = tunnelViewModel.peersData.count - 1
let addedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + addedPeerIndex))
return addedSectionIndices
}
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
let numberOfInterfaceSections = interfaceFieldsBySection.count
assert(peer.index < tunnelViewModel.peersData.count)
tunnelViewModel.deletePeer(peer: peer)
let removedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + peer.index))
return removedSectionIndices
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
self.present(alert, animated: true, completion: nil)
}
}
// MARK: UITableViewDelegate
extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section == (numberOfInterfaceSections + numberOfPeerSections + 1)) {
return (row > 0) ? indexPath : nil
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
assert(row > 0)
let option = activateOnDemandOptions[row - 1]
assert(option != .none)
activateOnDemandSetting.activateOnDemandOption = option
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
tableView.reloadRows(at: indexPaths, with: .automatic)
}
}
class TunnelEditTableViewKeyValueCell: UITableViewCell {
static let id: String = "TunnelEditTableViewKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var isValueValid: Bool = true {
didSet {
if (isValueValid) {
keyLabel.textColor = UIColor.black
} else {
keyLabel.textColor = UIColor.red
}
}
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
let keyLabel: UILabel
let valueTextField: UITextField
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueTextField = UITextField()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueTextField.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
])
valueTextField.delegate = self
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
onValueBeingEdited = nil
}
}
extension TunnelEditTableViewKeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
guard (isModified) else { return }
if let onValueChanged = onValueChanged {
onValueChanged(textField.text ?? "")
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}
class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelEditTableViewReadOnlyKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueLabel = ScrollableLabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textColor = UIColor.gray
valueLabel.textColor = UIColor.gray
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
])
}
override var textToCopy: String? {
return self.valueLabel.text
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelEditTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelEditTableViewButtonCell"
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}
class TunnelEditTableViewSwitchCell: UITableViewCell {
static let id: String = "TunnelEditTableViewSwitchCell"
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? UIColor.black : UIColor.gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView: UISwitch
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
switchView = UISwitch()
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
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
}
}
@@ -1,445 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
import UserNotifications
class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
var busyIndicator: UIActivityIndicatorView?
var centeredAddButton: BorderedTextButton?
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
// Set up the navigation bar
self.title = "WireGuard"
let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
self.navigationItem.rightBarButtonItem = addButtonItem
let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
self.navigationItem.leftBarButtonItem = settingsButtonItem
// Set up the busy indicator
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
// Add the busyIndicator, centered
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
busyIndicator.startAnimating()
self.busyIndicator = busyIndicator
// State restoration
self.restorationIdentifier = "TunnelsListVC"
}
func setTunnelsManager(tunnelsManager: TunnelsManager) {
if (self.tunnelsManager != nil) {
// If a tunnels manager is already set, do nothing
return
}
// Create the table view
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.rowHeight = 60
tableView.separatorStyle = .none
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
tableView.dataSource = self
tableView.delegate = self
self.tableView = tableView
// Add button at the center
let centeredAddButton = BorderedTextButton()
centeredAddButton.title = "Add a tunnel"
centeredAddButton.isHidden = true
self.view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
self?.addButtonTapped(sender: centeredAddButton)
}
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
self.centeredAddButton = centeredAddButton
// Hide the busy indicator
self.busyIndicator?.stopAnimating()
// Keep track of the tunnels manager
self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self
}
override func viewWillAppear(_: Bool) {
// Remove selection when getting back to the list view on iPhone
if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
}
}
@objc func addButtonTapped(sender: AnyObject) {
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] (_) in
self?.presentViewControllerForFileImport()
}
alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] (_) in
self?.presentViewControllerForScanningQRCode()
}
alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] (_) in
if let s = self, let tunnelsManager = s.tunnelsManager {
s.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
}
}
alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
if let sender = sender as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sender
} else if let sender = sender as? UIView {
alert.popoverPresentationController?.sourceView = sender
alert.popoverPresentationController?.sourceRect = sender.bounds
}
self.present(alert, animated: true, completion: nil)
}
@objc func settingsButtonTapped(sender: UIBarButtonItem!) {
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
let settingsNC = UINavigationController(rootViewController: settingsVC)
settingsNC.modalPresentationStyle = .formSheet
self.present(settingsNC, animated: true)
}
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
self.present(editNC, animated: true)
}
func presentViewControllerForFileImport() {
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
filePicker.delegate = self
self.present(filePicker, animated: true)
}
func presentViewControllerForScanningQRCode() {
let scanQRCodeVC = QRScanViewController()
scanQRCodeVC.delegate = self
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
scanQRCodeNC.modalPresentationStyle = .fullScreen
self.present(scanQRCodeNC, animated: true)
}
func importFromFile(url: URL) {
guard let tunnelsManager = tunnelsManager else { return }
if (url.pathExtension == "zip") {
ZipImporter.importConfigFiles(from: url) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let configs: [TunnelConfiguration?] = result.value!
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] (numberSuccessful) in
if numberSuccessful == configs.count {
return
}
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
from: self)
}
}
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
}
} else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
message: "An error occured when importing the tunnel configuration.",
from: self)
}
}
}
}
// MARK: UIDocumentPickerDelegate
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach(importFromFile)
}
}
// MARK: QRScanViewControllerDelegate
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else {
completionHandler?()
}
}
}
}
// MARK: UITableViewDataSource
extension TunnelsListTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (tunnelsManager?.numberOfTunnels() ?? 0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsListTableViewCell.id, for: indexPath) as! TunnelsListTableViewCell
if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let s = self, let tunnelsManager = s.tunnelsManager else { return }
if (isOn) {
tunnelsManager.startActivation(of: tunnel) { [weak s] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
DispatchQueue.main.async {
cell.statusSwitch.isOn = false
}
})
}
}
} else {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}
return cell
}
}
// MARK: UITableViewDelegate
extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete", handler: { [weak self] (_, _, completionHandler) in
guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel, completionHandler: { (error) in
if (error != nil) {
ErrorPresenter.showErrorAlert(error: error!, from: self)
completionHandler(false)
} else {
completionHandler(true)
}
})
})
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
// MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) {
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
func tunnelModified(at index: Int) {
tableView?.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
func tunnelMoved(at oldIndex: Int, to newIndex: Int) {
tableView?.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
}
func tunnelRemoved(at index: Int) {
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
}
class TunnelsListTableViewCell: UITableViewCell {
static let id: String = "TunnelsListTableViewCell"
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] (tunnel, _) in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
self?.update(from: tunnel.status)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel
let busyIndicator: UIActivityIndicatorView
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
nameLabel = UILabel()
busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
statusSwitch = UISwitch()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
busyIndicator.rightAnchor.constraint(equalTo: statusSwitch.leftAnchor, constant: -8)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16),
nameLabel.rightAnchor.constraint(equalTo: busyIndicator.leftAnchor)
])
self.accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if (status == .inactive || status == .active) {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
class BorderedTextButton: UIView {
let button: UIButton
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
button = UIButton(type: .system)
super.init(frame: CGRect.zero)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
extension UITableViewCell {
static var reuseIdentifier: String {
return NSStringFromClass(self)
}
}
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) {
register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
}
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
//swiftlint:disable:next force_cast
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
}
}
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class BorderedTextButton: UIView {
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
super.init(frame: CGRect.zero)
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: centerXAnchor),
button.centerYAnchor.constraint(equalTo: centerYAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func buttonTapped() {
onTapped?()
}
}
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class ButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == .red }
set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class CheckmarkCell: UITableViewCell {
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
}
}
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class KeyValueCell: UITableViewCell {
let keyLabel: UILabel = {
let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
keyLabel.textColor = .black
keyLabel.textAlignment = .left
return keyLabel
}()
let valueLabelScrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.isDirectionalLockEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
return scrollView
}()
let valueTextField: UITextField = {
let valueTextField = UITextField()
valueTextField.textAlignment = .right
valueTextField.isEnabled = false
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
valueTextField.adjustsFontForContentSizeCategory = true
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
valueTextField.textColor = .gray
return valueTextField
}()
var copyableGesture = true
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var isValueValid = true {
didSet {
if isValueValid {
keyLabel.textColor = .black
} else {
keyLabel.textColor = .red
}
}
}
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
])
valueTextField.delegate = self
valueLabelScrollView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.leftAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leftAnchor),
valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
valueTextField.rightAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.rightAnchor),
valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor)
])
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
contentView.addSubview(valueLabelScrollView)
contentView.addSubview(valueLabelScrollView)
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabelScrollView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5)
])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
addGestureRecognizer(gestureRecognizer)
isUserInteractionEnabled = true
configureForContentSize()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
contentSizeBasedConstraints = constraints
}
}
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !copyableGesture {
return
}
guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared
menuController.setTargetRect(detailTextLabel?.frame ?? recognizerView.frame, in: detailTextLabel?.superview ?? recognizerSuperView)
menuController.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = valueTextField.text
}
override func prepareForReuse() {
super.prepareForReuse()
copyableGesture = true
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
onValueBeingEdited = nil
key = ""
value = ""
configureForContentSize()
}
}
extension KeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
guard isModified else { return }
onValueChanged?(textField.text ?? "")
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class SwitchCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? .black : .gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView = UISwitch()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
override func prepareForReuse() {
super.prepareForReuse()
isEnabled = true
message = ""
isOn = false
}
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditKeyValueCell: KeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textAlignment = .right
valueTextField.textAlignment = .left
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
widthRatioConstraint.isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
copyableGesture = false
valueTextField.textColor = .black
valueTextField.isEnabled = true
valueLabelScrollView.isScrollEnabled = false
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelListCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel = {
let nameLabel = UILabel()
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
nameLabel.adjustsFontForContentSizeCategory = true
nameLabel.numberOfLines = 0
return nameLabel
}()
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
let statusSwitch = UISwitch()
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
bottomAnchorConstraint
])
accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
@@ -12,20 +12,19 @@ class MainViewController: UISplitViewController {
init() {
let detailVC = UIViewController()
detailVC.view.backgroundColor = UIColor.white
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController()
let masterNC = UINavigationController(rootViewController: masterVC)
self.tunnelsListVC = masterVC
tunnelsListVC = masterVC
super.init(nibName: nil, bundle: nil)
self.viewControllers = [ masterNC, detailNC ]
viewControllers = [ masterNC, detailNC ]
// State restoration
self.restorationIdentifier = "MainVC"
restorationIdentifier = "MainVC"
masterNC.restorationIdentifier = "MasterNC"
detailNC.restorationIdentifier = "DetailNC"
}
@@ -35,35 +34,48 @@ class MainViewController: UISplitViewController {
}
override func viewDidLoad() {
self.delegate = self
delegate = self
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
self.preferredDisplayMode = .allVisible
preferredDisplayMode = .allVisible
// Create the tunnels manager, and when it's ready, inform tunnelsListVC
TunnelsManager.create { [weak self] result in
guard let self = self else { return }
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)
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = s
tunnelsManager.activationDelegate = self
s.onTunnelsManagerReady?(tunnelsManager)
s.onTunnelsManagerReady = nil
self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
}
}
}
extension MainViewController: TunnelsManagerActivationDelegate {
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError) {
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
// Nothing to do
}
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
func tunnelActivationSucceeded(tunnel: TunnelContainer) {
// Nothing to do
}
}
extension MainViewController {
@@ -74,13 +86,13 @@ extension MainViewController {
}
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] (tunnelsManager) in
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) {
if animated {
self.showDetailViewController(tunnelDetailNC, sender: self)
} else {
UIView.performWithoutAnimation {
@@ -2,7 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import AVFoundation
import CoreData
import UIKit
protocol QRScanViewControllerDelegate: class {
@@ -18,13 +17,13 @@ class QRScanViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Scan QR code"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
title = "Scan QR code"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = UIColor.lightGray
tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center
view.addSubview(tipLabel)
@@ -32,8 +31,8 @@ class QRScanViewController: UIViewController {
NSLayoutConstraint.activate([
tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32),
])
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
])
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
@@ -102,7 +101,7 @@ class QRScanViewController: UIViewController {
}
}
previewLayer?.frame = self.view.bounds
previewLayer?.frame = view.bounds
}
func scanDidComplete(withCode code: String) {
@@ -114,27 +113,26 @@ class QRScanViewController: UIViewController {
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { [weak self] _ in
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default, handler: { [weak self] _ in
let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if (title.isEmpty) { return }
})
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
tunnelConfiguration.interface.name = title
if let s = self {
s.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: s) {
s.dismiss(animated: true, completion: nil)
if let self = self {
self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
self.dismiss(animated: true, completion: nil)
}
}
}))
})
present(alert, animated: true)
}
func scanDidEncounterError(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in
alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
})
present(alertController, animated: true)
captureSession = nil
}
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
enum SettingsFields: String {
case iosAppVersion = "WireGuard for iOS"
case goBackendVersion = "WireGuard Go Backend"
case exportZipArchive = "Export zip archive"
case exportLogFile = "Export log file"
}
let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion],
[.exportZipArchive],
[.exportLogFile]
]
let tunnelsManager: TunnelsManager?
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
init(tunnelsManager: TunnelsManager?) {
self.tunnelsManager = tunnelsManager
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self)
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let logo = tableView.tableFooterView else { return }
let bottomPadding = max(tableView.layoutMargins.bottom, 10)
let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding)
let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height
var height = tableView.estimatedRowHeight * 1.5
var width = height * imageAspectRatio
let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20)
if width > maxWidth {
width = maxWidth
height = width / imageAspectRatio
}
let needsReload = height != logo.frame.height
logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height)
if needsReload {
tableView.tableFooterView = logo
}
}
@objc func doneTapped() {
dismiss(animated: true, completion: nil)
}
func exportConfigurationsAsZipFile(sourceView: UIView) {
guard let tunnelsManager = tunnelsManager else { return }
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
_ = FileManager.deleteFile(at: destinationURL)
let count = tunnelsManager.numberOfTunnels()
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() }
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
self?.present(fileExportVC, animated: true, completion: nil)
}
}
func exportLogForLastActivatedTunnel(sourceView: UIView) {
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
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: "Log export failed", message: "The pre-existing log could not be cleared", from: self)
return
}
}
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self)
return
}
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", 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)
}
}
}
}
// MARK: UITableViewDataSource
extension SettingsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return settingsFieldsBySection.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settingsFieldsBySection[section].count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "About"
case 1:
return "Export configurations"
case 2:
return "Tunnel log"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
if field == .iosAppVersion || field == .goBackendVersion {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.copyableGesture = false
cell.key = field.rawValue
if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
cell.value = appVersion
} else if field == .goBackendVersion {
cell.value = WIREGUARD_GO_VERSION
}
return cell
} else if field == .exportZipArchive {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
}
return cell
} else {
assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
}
return cell
}
}
}
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
private var onDemandStatusObservervationToken: AnyObject?
private var statusObservervationToken: AnyObject?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
onDemandStatusObservervationToken = nil
statusObservervationToken = nil
}
override func viewDidLoad() {
super.viewDidLoad()
title = tunnelViewModel.interfaceData[.name]
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(SwitchCell.self)
tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self)
// State restoration
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
private func loadSections() {
sections.removeAll()
sections.append(.status)
sections.append(.interface)
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.onDemand)
sections.append(.delete)
}
@objc func editTapped() {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
// MARK: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
loadSections()
title = tunnel.name
tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .status:
return 1
case .interface:
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
case .peer(let peerData):
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
case .onDemand:
return 1
case .delete:
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .status:
return "Status"
case .interface:
return "Interface"
case .peer:
return "Peer"
case .onDemand:
return "On-Demand Activation"
case .delete:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .status:
return statusCell(for: tableView, at: indexPath)
case .interface:
return interfaceCell(for: tableView, at: indexPath)
case .peer(let peer):
return peerCell(for: tableView, at: indexPath, with: peer)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
case .delete:
return deleteConfigurationCell(for: tableView, at: indexPath)
}
}
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
let text: String
switch status {
case .inactive:
text = "Inactive"
case .activating:
text = "Activating"
case .active:
text = "Active"
case .deactivating:
text = "Deactivating"
case .reasserting:
text = "Reactivating"
case .restarting:
text = "Restarting"
case .waiting:
text = "Waiting"
}
cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
}
cell.isEnabled = status == .active || status == .inactive
}
statusUpdate(cell, tunnel.status)
statusObservervationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return }
statusUpdate(cell, tunnel.status)
}
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
if isOn {
self.tunnelsManager.startActivation(of: self.tunnel)
} else {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
return cell
}
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = peerData[field]
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = "Activate on demand"
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
onDemandStatusObservervationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
}
return cell
}
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
self?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
}
}
@@ -0,0 +1,486 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
protocol TunnelEditTableViewControllerDelegate: class {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}
// MARK: TunnelEditTableViewController
class TunnelEditTableViewController: UITableViewController {
private enum Section {
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case addPeer
case onDemand
}
weak var delegate: TunnelEditTableViewControllerDelegate?
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
[.name],
[.privateKey, .publicKey, .generateKeyPair],
[.addresses, .listenPort, .mtu, .dns]
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
.deletePeer
]
let activateOnDemandOptions: [ActivateOnDemandOption] = [
.useOnDemandOverWiFiOrCellular,
.useOnDemandOverWiFiOnly,
.useOnDemandOverCellularOnly
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting
private var sections = [Section]()
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
activateOnDemandSetting = tunnel.activateOnDemandSetting()
super.init(style: .grouped)
loadSections()
}
init(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
// Use this initializer to create a new tunnel.
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
self.tunnelsManager = tunnelsManager
tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = tunnel == nil ? "New configuration" : "Edit configuration"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.register(TunnelEditKeyValueCell.self)
tableView.register(TunnelEditEditableKeyValueCell.self)
tableView.register(ButtonCell.self)
tableView.register(SwitchCell.self)
tableView.register(CheckmarkCell.self)
}
private func loadSections() {
sections.removeAll()
interfaceFieldsBySection.forEach { _ in sections.append(.interface) }
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.addPeer)
sections.append(.onDemand)
}
@objc func saveTapped() {
tableView.endEditing(false)
let tunnelSaveResult = tunnelViewModel.save()
switch tunnelSaveResult {
case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer"
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self)
tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration):
if let tunnel = tunnel {
// We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel,
tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
} else {
// We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
let tunnel: TunnelContainer = result.value!
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
}
}
}
@objc func cancelTapped() {
dismiss(animated: true, completion: nil)
delegate?.tunnelEditingCancelled()
}
}
// MARK: UITableViewDataSource
extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .interface:
return interfaceFieldsBySection[section].count
case .peer(let peerData):
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
return peerFieldsToShow.count
case .addPeer:
return 1
case .onDemand:
if activateOnDemandSetting.isActivateOnDemandEnabled {
return 4
} else {
return 1
}
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .interface:
return section == 0 ? "Interface" : nil
case .peer:
return "Peer"
case .addPeer:
return nil
case .onDemand:
return "On-Demand Activation"
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .interface:
return interfaceFieldCell(for: tableView, at: indexPath)
case .peer(let peerData):
return peerCell(for: tableView, at: indexPath, with: peerData)
case .addPeer:
return addPeerCell(for: tableView, at: indexPath)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
}
}
private func interfaceFieldCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = interfaceFieldsBySection[indexPath.section][indexPath.row]
switch field {
case .generateKeyPair:
return generateKeyPairCell(for: tableView, at: indexPath, with: field)
case .publicKey:
return publicKeyCell(for: tableView, at: indexPath, with: field)
default:
return interfaceFieldKeyValueCell(for: tableView, at: indexPath, with: field)
}
}
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section)
self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .fade)
}
}
return cell
}
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
case .name, .privateKey:
cell.placeholderText = "Required"
cell.keyboardType = .default
case .addresses, .dns:
cell.placeholderText = "Optional"
cell.keyboardType = .numbersAndPunctuation
case .listenPort, .mtu:
cell.placeholderText = "Automatic"
cell.keyboardType = .numberPad
case .publicKey, .generateKeyPair:
cell.keyboardType = .default
}
cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = tunnelViewModel.interfaceData[field]
if field == .dns { // While editing DNS, you might directly set exclude private IPs
cell.onValueBeingEdited = { [weak self] value in
self?.tunnelViewModel.interfaceData[field] = value
}
} else {
cell.onValueChanged = { [weak self] value in
self?.tunnelViewModel.interfaceData[field] = value
}
}
// Compute public key live
if field == .privateKey {
cell.onValueBeingEdited = { [weak self] value in
guard let self = self else { return }
self.tunnelViewModel.interfaceData[.privateKey] = value
if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
}
}
}
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
let field = peerFieldsToShow[indexPath.row]
switch field {
case .deletePeer:
return deletePeerCell(for: tableView, at: indexPath, peerData: peerData, field: field)
case .excludePrivateIPs:
return excludePrivateIPsCell(for: tableView, at: indexPath, peerData: peerData, field: field)
default:
return peerFieldKeyValueCell(for: tableView, at: indexPath, peerData: peerData, field: field)
}
}
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return }
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let self = self else { return }
let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
tableView.performBatchUpdates({
self.tableView.deleteSections(removedSectionIndices, with: .fade)
if shouldShowExcludePrivateIPs {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
self.tableView.insertRows(at: [rowIndexPath], with: .fade)
}
}
})
}
}
return cell
}
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns])
if let row = self.peerFields.firstIndex(of: .allowedIPs) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
}
}
return cell
}
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
case .publicKey:
cell.placeholderText = "Required"
cell.keyboardType = .default
case .preSharedKey, .endpoint:
cell.placeholderText = "Optional"
cell.keyboardType = .default
case .allowedIPs:
cell.placeholderText = "Optional"
cell.keyboardType = .numbersAndPunctuation
case .persistentKeepAlive:
cell.placeholderText = "Off"
cell.keyboardType = .numberPad
case .excludePrivateIPs, .deletePeer:
cell.keyboardType = .default
}
cell.isValueValid = !peerData.fieldsWithError.contains(field)
cell.value = peerData[field]
if field == .allowedIPs {
cell.onValueBeingEdited = { [weak self, weak peerData] value in
guard let self = self, let peerData = peerData else { return }
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
peerData[.allowedIPs] = value
if oldValue != peerData.shouldAllowExcludePrivateIPsControl {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
if peerData.shouldAllowExcludePrivateIPsControl {
self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
} else {
self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
}
}
}
}
} else {
cell.onValueChanged = { [weak peerData] value in
peerData?[field] = value
}
}
return cell
}
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in
guard let self = self else { return }
let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
let addedSectionIndices = self.appendEmptyPeer()
tableView.performBatchUpdates({
tableView.insertSections(addedSectionIndices, with: .fade)
if shouldHideExcludePrivateIPs {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
self.tableView.deleteRows(at: [rowIndexPath], with: .fade)
}
}
}, completion: nil)
}
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return }
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
if isOn {
self.activateOnDemandSetting.isActivateOnDemandEnabled = true
if self.activateOnDemandSetting.activateOnDemandOption == .none {
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
}
self.loadSections()
self.tableView.insertRows(at: indexPaths, with: .fade)
} else {
self.activateOnDemandSetting.isActivateOnDemandEnabled = false
self.loadSections()
self.tableView.deleteRows(at: indexPaths, with: .fade)
}
}
return cell
} else {
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
let rowOption = activateOnDemandOptions[indexPath.row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = (selectedOption == rowOption)
return cell
}
}
func appendEmptyPeer() -> IndexSet {
tunnelViewModel.appendEmptyPeer()
loadSections()
let addedPeerIndex = tunnelViewModel.peersData.count - 1
return IndexSet(integer: interfaceFieldsBySection.count + addedPeerIndex)
}
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
tunnelViewModel.deletePeer(peer: peer)
loadSections()
return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
// MARK: UITableViewDelegate
extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section], indexPath.row > 0 {
return indexPath
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch sections[indexPath.section] {
case .onDemand:
let option = activateOnDemandOptions[indexPath.row - 1]
assert(option != .none)
activateOnDemandSetting.activateOnDemandOption = option
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
UIView.performWithoutAnimation {
tableView.reloadRows(at: indexPaths, with: .none)
}
default:
assertionFailure()
}
}
}
@@ -0,0 +1,306 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
import UserNotifications
class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
let tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.estimatedRowHeight = 60
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.register(TunnelListCell.self)
return tableView
}()
let centeredAddButton: BorderedTextButton = {
let button = BorderedTextButton()
button.title = "Add a tunnel"
button.isHidden = true
return button
}()
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
override func loadView() {
view = UIView()
view.backgroundColor = .white
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
guard let self = self else { return }
self.addButtonTapped(sender: self.centeredAddButton)
}
busyIndicator.startAnimating()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "WireGuard"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC"
}
func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self
busyIndicator.stopAnimating()
tableView.reloadData()
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
}
override func viewWillAppear(_: Bool) {
if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
}
}
@objc func addButtonTapped(sender: AnyObject) {
guard tunnelsManager != nil else { return }
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in
self?.presentViewControllerForFileImport()
}
alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in
self?.presentViewControllerForScanningQRCode()
}
alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in
if let self = self, let tunnelsManager = self.tunnelsManager {
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
}
}
alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
if let sender = sender as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sender
} else if let sender = sender as? UIView {
alert.popoverPresentationController?.sourceView = sender
alert.popoverPresentationController?.sourceRect = sender.bounds
}
present(alert, animated: true, completion: nil)
}
@objc func settingsButtonTapped(sender: UIBarButtonItem!) {
guard tunnelsManager != nil else { return }
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
let settingsNC = UINavigationController(rootViewController: settingsVC)
settingsNC.modalPresentationStyle = .formSheet
present(settingsNC, animated: true)
}
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func presentViewControllerForFileImport() {
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
filePicker.delegate = self
present(filePicker, animated: true)
}
func presentViewControllerForScanningQRCode() {
let scanQRCodeVC = QRScanViewController()
scanQRCodeVC.delegate = self
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
scanQRCodeNC.modalPresentationStyle = .fullScreen
present(scanQRCodeNC, animated: true)
}
func importFromFile(url: URL, completionHandler: (() -> Void)?) {
guard let tunnelsManager = tunnelsManager else { return }
if url.pathExtension == "zip" {
ZipImporter.importConfigFiles(from: url) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let configs = result.value!
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in
if numberSuccessful == configs.count {
completionHandler?()
return
}
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
from: self, onPresented: completionHandler)
}
}
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
} else {
completionHandler?()
}
}
} else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
message: "An error occured when importing the tunnel configuration.",
from: self, onPresented: completionHandler)
}
}
}
}
// MARK: UIDocumentPickerDelegate
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach {
importFromFile(url: $0, completionHandler: nil)
}
}
}
// MARK: QRScanViewControllerDelegate
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else {
completionHandler?()
}
}
}
}
// MARK: UITableViewDataSource
extension TunnelsListTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (tunnelsManager?.numberOfTunnels() ?? 0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
if isOn {
tunnelsManager.startActivation(of: tunnel)
} else {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}
return cell
}
}
// MARK: UITableViewDelegate
extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in
guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
ErrorPresenter.showErrorAlert(error: error!, from: self)
completionHandler(false)
} else {
completionHandler(true)
}
}
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
// MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) {
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
func tunnelModified(at index: Int) {
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
}
func tunnelRemoved(at index: Int) {
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
}
}
@@ -1,51 +0,0 @@
// 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
}
}
@@ -1,492 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os.log
protocol TunnelsManagerListDelegate: class {
func tunnelAdded(at: Int)
func tunnelModified(at: Int)
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at: Int)
}
protocol TunnelsManagerActivationDelegate: class {
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError)
}
enum TunnelsManagerError: WireGuardAppError {
// Tunnels list management
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case vpnSystemErrorOnListingTunnels
case vpnSystemErrorOnAddTunnel
case vpnSystemErrorOnModifyTunnel
case vpnSystemErrorOnRemoveTunnel
// Tunnel activation
case tunnelActivationAttemptFailed // startTunnel() throwed
case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed
case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet
func alertText() -> (String, String) {
switch (self) {
case .tunnelNameEmpty:
return ("No name provided", "Can't create tunnel with an empty name")
case .tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists")
case .vpnSystemErrorOnListingTunnels:
return ("Unable to list tunnels", "Internal error")
case .vpnSystemErrorOnAddTunnel:
return ("Unable to create tunnel", "Internal error")
case .vpnSystemErrorOnModifyTunnel:
return ("Unable to modify tunnel", "Internal error")
case .vpnSystemErrorOnRemoveTunnel:
return ("Unable to remove tunnel", "Internal error")
case .tunnelActivationAttemptFailed:
return ("Activation failure", "The tunnel could not be activated due to an internal error")
case .tunnelActivationFailedInternalError:
return ("Activation failure", "The tunnel could not be activated due to an internal error")
case .tunnelActivationFailedNoInternetConnection:
return ("Activation failure", "No internet connection")
}
}
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
private var isAddingTunnel: Bool = false
private var isModifyingTunnel: Bool = false
private var isDeletingTunnel: Bool = false
init(tunnelProviders: [NETunnelProviderManager]) {
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
}
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
#if targetEnvironment(simulator)
// NETunnelProviderManager APIs don't work on the simulator
completionHandler(.success(TunnelsManager(tunnelProviders: [])))
#else
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if let error = error {
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnListingTunnels))
return
}
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
}
#endif
}
func add(tunnelConfiguration: TunnelConfiguration,
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
return
}
if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
return
}
isAddingTunnel = true
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isAddingTunnel = false }
guard (error == nil) else {
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnAddTunnel))
return
}
if let s = self {
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
s.tunnels.append(tunnel)
s.tunnels.sort { $0.name < $1.name }
s.tunnelsListDelegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel))
}
}
}
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
}
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful)
return
}
let tail = tunnelConfigurations.dropFirst()
self.add(tunnelConfiguration: head) { [weak self, tail] (result) in
DispatchQueue.main.async {
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
}
}
}
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty)
return
}
isModifyingTunnel = true
let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = (tunnelName != tunnelProviderManager.localizedDescription)
var oldName: String?
if (isNameChanged) {
if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return
}
oldName = tunnel.name
tunnel.name = tunnelName
}
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = (!tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled)
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isModifyingTunnel = false }
guard (error == nil) else {
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
return
}
if let s = self {
if (isNameChanged) {
let oldIndex = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.sort { $0.name < $1.name }
let newIndex = s.tunnels.firstIndex(of: tunnel)!
s.tunnelsListDelegate?.tunnelMoved(at: oldIndex, to: newIndex)
}
s.tunnelsListDelegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
if (tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting) {
// Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.beginRestart()
}
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 (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider
isDeletingTunnel = true
tunnelProviderManager.removeFromPreferences { [weak self] (error) in
defer { self?.isDeletingTunnel = false }
guard (error == nil) else {
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelsManagerError.vpnSystemErrorOnRemoveTunnel)
return
}
if let s = self {
let index = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.remove(at: index)
s.tunnelsListDelegate?.tunnelRemoved(at: index)
}
completionHandler(nil)
}
}
func numberOfTunnels() -> Int {
return tunnels.count
}
func tunnel(at index: Int) -> TunnelContainer {
return tunnels[index]
}
func tunnel(named tunnelName: String) -> TunnelContainer? {
return self.tunnels.first(where: { $0.name == tunnelName })
}
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) {
return
}
tunnel.startDeactivation()
}
func refreshStatuses() {
for t in tunnels {
t.refreshStatus()
}
}
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool {
didSet {
if (isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
}
var isAttemptingActivation: Bool = false
var onActivationCommitted: ((Bool) -> Void)?
var onDeactivationComplete: (() -> Void)?
fileprivate let tunnelProvider: NETunnelProviderManager
private var statusObservationToken: AnyObject?
init(tunnel: NETunnelProviderManager) {
self.name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status)
self.status = status
self.isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
self.tunnelProvider = tunnel
super.init()
if (status != .inactive || isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
}
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshStatus() {
let status = TunnelStatus(from: self.tunnelProvider.connection.status)
self.status = status
self.isActivateOnDemandEnabled = self.tunnelProvider.isOnDemandEnabled
if (status != .inactive || isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
fileprivate func startActivation(completionHandler: @escaping (TunnelsManagerError?) -> Void) {
assert(status == .inactive || status == .restarting || status == .waiting)
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
onDeactivationComplete = nil
isAttemptingActivation = true
startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
}
fileprivate func startActivation(recursionCount: UInt = 0,
lastError: Error? = nil,
tunnelConfiguration: TunnelConfiguration,
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
if (recursionCount >= 8) {
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
os_log("startActivation: Entering", log: OSLog.default, type: .debug)
guard (tunnelProvider.isEnabled) else {
// In case the tunnel had gotten disabled, re-enable and save it,
// then call this function again.
os_log("startActivation: Tunnel is disabled. Re-enabling and saving", log: OSLog.default, type: .info)
tunnelProvider.isEnabled = true
tunnelProvider.saveToPreferences { [weak self] (error) in
if (error != nil) {
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
}
return
}
// Start the tunnel
startObservingTunnelStatus()
let session = (tunnelProvider.connection as! NETunnelProviderSession)
do {
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
try session.startTunnel()
os_log("startActivation: Success", log: OSLog.default, type: .debug)
completionHandler(nil)
} catch (let error) {
guard let vpnError = error as? NEVPNError else {
os_log("Failed to activate tunnel: Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
os_log("Failed to activate tunnel: VPN Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
tunnelProvider.loadFromPreferences { [weak self] (error) in
if (error != nil) {
os_log("startActivation: Error reloading tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
self?.status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
}
}
}
fileprivate func startDeactivation() {
assert(status == .active)
assert(statusObservationToken != nil)
let session = (tunnelProvider.connection as! NETunnelProviderSession)
session.stopTunnel()
}
fileprivate func beginRestart() {
assert(status == .active || status == .activating || status == .reasserting)
assert(statusObservationToken != nil)
status = .restarting
let session = (tunnelProvider.connection as! NETunnelProviderSession)
session.stopTunnel()
}
private func startObservingTunnelStatus() {
if (statusObservationToken != nil) { return }
let connection = tunnelProvider.connection
statusObservationToken = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: connection,
queue: nil) { [weak self] (_) in
guard let s = self else { return }
if (s.isAttemptingActivation) {
if (connection.status == .connecting || connection.status == .connected) {
// We tried to start the tunnel, and that attempt is on track to become succeessful
s.onActivationCommitted?(true)
s.onActivationCommitted = nil
} else if (connection.status == .disconnecting || connection.status == .disconnected) {
// We tried to start the tunnel, but that attempt didn't succeed
s.onActivationCommitted?(false)
s.onActivationCommitted = nil
}
s.isAttemptingActivation = false
}
if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
// Don't change s.status when disconnecting for a restart
if (connection.status == .disconnected) {
self?.startActivation(completionHandler: { _ in })
}
return
}
s.status = TunnelStatus(from: connection.status)
if (s.status == .inactive) {
s.onDeactivationComplete?()
s.onDeactivationComplete = nil
if (!s.isActivateOnDemandEnabled) {
s.statusObservationToken = nil
}
}
}
}
}
@objc enum TunnelStatus: Int {
case inactive
case activating
case active
case deactivating
case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case waiting // Waiting for another tunnel to be brought down
init(from vpnStatus: NEVPNStatus) {
switch (vpnStatus) {
case .connected:
self = .active
case .connecting:
self = .activating
case .disconnected:
self = .inactive
case .disconnecting:
self = .deactivating
case .reasserting:
self = .reasserting
case .invalid:
self = .inactive
}
}
}
@@ -2,3 +2,4 @@
#include "unzip.h"
#include "zip.h"
#include "wireguard-go-version.h"
#include "ringlogger.h"
+2 -1
View File
@@ -2,5 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error {
func alertText() -> (/* title */ String, /* message */ String)
typealias AlertText = (title: String, message: String)
var alertText: AlertText { get }
}
+11 -11
View File
@@ -2,27 +2,27 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> {
case success(T)
case failure(WireGuardAppError)
case success(_ value: T)
case failure(_ error: WireGuardAppError)
var value: T? {
switch (self) {
case .success(let v): return v
case .failure(_): return nil
switch self {
case .success(let value): return value
case .failure: return nil
}
}
var error: WireGuardAppError? {
switch (self) {
case .success(_): return nil
case .failure(let e): return e
switch self {
case .success: return nil
case .failure(let error): return error
}
}
var isSuccess: Bool {
switch (self) {
case .success(_): return true
case .failure(_): return false
switch self {
case .success: return true
case .failure: return false
}
}
}
+19 -27
View File
@@ -8,8 +8,8 @@ enum ZipArchiveError: WireGuardAppError {
case cantOpenOutputZipFileForWriting
case badArchive
func alertText() -> (String, String) {
switch (self) {
var alertText: AlertText {
switch self {
case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.")
case .cantOpenOutputZipFileForWriting:
@@ -39,9 +39,9 @@ class ZipArchive {
zipClose(zipFile, nil)
}
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileName: String, contents: Data)] {
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
var results: [(fileName: String, contents: Data)] = []
var results = [(fileBaseName: String, contents: Data)]()
guard let zipFile = unzOpen64(url.path) else {
throw ZipArchiveError.cantOpenInputZipFile
@@ -49,17 +49,13 @@ class ZipArchive {
defer {
unzClose(zipFile)
}
guard (unzGoToFirstFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzGoToFirstFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
var resultOfGoToNextFile: Int32
repeat {
guard (unzOpenCurrentFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzOpenCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
let bufferSize = 16384 // 16 kb
let bufferSize = 16384 // 16 KiB
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
@@ -68,37 +64,33 @@ class ZipArchive {
dataBuffer.deallocate()
}
guard (unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK else { throw ZipArchiveError.badArchive }
if let fileURL = URL(string: String(cString: fileNameBuffer)),
!fileURL.hasDirectoryPath,
requiredFileExtensions.contains(fileURL.pathExtension) {
let lastChar = String(cString: fileNameBuffer).suffix(1)
let isDirectory = (lastChar == "/" || lastChar == "\\")
let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) {
var unzippedData = Data()
var bytesRead: Int32 = 0
repeat {
bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize))
if (bytesRead > 0) {
if bytesRead > 0 {
let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) {
(buf: UnsafeMutablePointer<UInt8>) -> Data in
return Data(bytes: buf, count: Int(bytesRead))
return Data(bytes: $0, count: Int(bytesRead))
}
unzippedData.append(dataRead)
}
} while (bytesRead > 0)
results.append((fileName: fileURL.lastPathComponent, contents: unzippedData))
} while bytesRead > 0
results.append((fileBaseName: fileURL.deletingPathExtension().lastPathComponent, contents: unzippedData))
}
guard (unzCloseCurrentFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzCloseCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
resultOfGoToNextFile = unzGoToNextFile(zipFile)
} while (resultOfGoToNextFile == UNZ_OK)
} while resultOfGoToNextFile == UNZ_OK
if (resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE) {
if resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE {
return results
} else {
throw ZipArchiveError.badArchive
@@ -6,19 +6,15 @@ 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")
}
var alertText: AlertText {
return ("Nothing to export", "There are no tunnels to export")
}
}
class ZipExporter {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL,
completion: @escaping (WireGuardAppError?) -> Void) {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
guard (!tunnelConfigurations.isEmpty) else {
guard !tunnelConfigurations.isEmpty else {
completion(ZipExporterError.noTunnelsToExport)
return
}
@@ -28,14 +24,14 @@ class ZipExporter {
for tunnelConfiguration in tunnelConfigurations {
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
let name = tunnelConfiguration.interface.name
if (name.isEmpty || name == lastTunnelName) { continue }
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) {
} catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(error) }
return
} catch {
@@ -6,53 +6,50 @@ 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.")
}
var alertText: AlertText {
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)]
var unarchivedFiles: [(fileBaseName: 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
for (index, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
let fileBaseName = unarchivedFile.fileBaseName
let trimmedName = fileBaseName.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedName.isEmpty {
unarchivedFiles[index].fileBaseName = trimmedName
} else {
unarchivedFiles.remove(at: i)
unarchivedFiles.remove(at: index)
}
}
if (unarchivedFiles.isEmpty) {
if unarchivedFiles.isEmpty {
throw ZipImporterError.noTunnelsInZipArchive
}
} catch (let error as WireGuardAppError) {
} 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]) {
unarchivedFiles.sort { $0.fileBaseName < $1.fileBaseName }
var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
for (index, file) in unarchivedFiles.enumerated() {
if index > 0 && file == unarchivedFiles[index - 1] {
continue
}
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
continue
}
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileBaseName) else {
continue
}
configs[i] = tunnelConfig
configs[index] = tunnelConfig
}
DispatchQueue.main.async { completion(.success(configs)) }
}
@@ -13,7 +13,7 @@ class DNSResolver {
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
for endpoint in endpoints {
guard let endpoint = endpoint else { continue }
if (!endpoint.hasHostAsIPAddress()) {
if !endpoint.hasHostAsIPAddress() {
return false
}
}
@@ -23,18 +23,18 @@ class DNSResolver {
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
let dispatchGroup: DispatchGroup = DispatchGroup()
if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) {
if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints
}
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
for (i, endpoint) in endpoints.enumerated() {
var resolvedEndpoints: [Endpoint?] = Array(repeating: nil, count: endpoints.count)
for (index, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) {
resolvedEndpoints[i] = endpoint
if endpoint.hasHostAsIPAddress() {
resolvedEndpoints[index] = endpoint
} else {
let workItem = DispatchWorkItem {
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
}
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
}
@@ -42,20 +42,20 @@ class DNSResolver {
dispatchGroup.wait() // TODO: Timeout?
var hostnamesWithDnsResolutionFailure: [String] = []
var hostnamesWithDnsResolutionFailure = [String]()
assert(endpoints.count == resolvedEndpoints.count)
for tuple in zip(endpoints, resolvedEndpoints) {
let endpoint = tuple.0
let resolvedEndpoint = tuple.1
if let endpoint = endpoint {
if (resolvedEndpoint == nil) {
if resolvedEndpoint == nil {
// DNS resolution failed
guard let hostname = endpoint.hostname() else { fatalError() }
hostnamesWithDnsResolutionFailure.append(hostname)
}
}
}
if (!hostnamesWithDnsResolutionFailure.isEmpty) {
if !hostnamesWithDnsResolutionFailure.isEmpty {
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
}
return resolvedEndpoints
@@ -65,48 +65,37 @@ class DNSResolver {
extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
//swiftlint:disable:next cyclomatic_complexity
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
var hints = addrinfo(
ai_flags: 0,
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
switch (endpoint.host) {
switch endpoint.host {
case .name(let name, _):
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
// The endpoint is a hostname and needs DNS resolution
let returnValue = getaddrinfo(
name.cString(using: .utf8), // Hostname
"\(endpoint.port)".cString(using: .utf8), // Port
&hints,
&resultPointer)
if (returnValue == 0) {
if addressInfo(for: name, port: endpoint.port, resultPointer: &resultPointer) == 0 {
// getaddrinfo succeeded
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN))
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN))
var ipv4AddressString: String?
var ipv6AddressString: String?
while (resultPointer != nil) {
while resultPointer != nil {
let result = resultPointer!.pointee
resultPointer = result.ai_next
if (result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size) {
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
if (inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil) {
if inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil {
ipv4AddressString = String(cString: ipv4Buffer)
// If we found an IPv4 address, we can stop
break
}
} else if (result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size) {
if (ipv6AddressString != nil) {
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
if ipv6AddressString != nil {
// If we already have an IPv6 address, we can skip this one
continue
}
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
if (inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil) {
if inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil {
ipv6AddressString = String(cString: ipv6Buffer)
}
}
@@ -115,9 +104,9 @@ extension DNSResolver {
ipv6Buffer.deallocate()
// We prefer an IPv4 address over an IPv6 address
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) {
return Endpoint(host: NWEndpoint.Host.ipv4(ipv4Address), port: endpoint.port)
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6AddressString = ipv6AddressString, let ipv6Address = IPv6Address(ipv6AddressString) {
return Endpoint(host: NWEndpoint.Host.ipv6(ipv6Address), port: endpoint.port)
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
return nil
}
@@ -130,4 +119,22 @@ extension DNSResolver {
return endpoint
}
}
private static func addressInfo(for name: String, port: NWEndpoint.Port, resultPointer: inout UnsafeMutablePointer<addrinfo>?) -> Int32 {
var hints = addrinfo(
ai_flags: 0,
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
return getaddrinfo(
name.cString(using: .utf8), // Hostname
"\(port)".cString(using: .utf8), // Port
&hints,
&resultPointer)
}
}
@@ -4,11 +4,21 @@
import NetworkExtension
class ErrorNotifier {
static func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
switch (error) {
let activationAttemptId: String?
weak var tunnelProvider: NEPacketTunnelProvider?
init(activationAttemptId: String?, tunnelProvider: NEPacketTunnelProvider) {
self.activationAttemptId = activationAttemptId
self.tunnelProvider = tunnelProvider
ErrorNotifier.removeLastErrorFile()
}
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(_):
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")
@@ -17,9 +27,24 @@ class ErrorNotifier {
}
}
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 })
func notify(_ error: PacketTunnelProviderError) {
guard let (title, message) = errorMessage(for: error) else { return }
if let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path {
// The tunnel was started from the app
let errorMessageData = "\(activationAttemptId)\n\(title)\n\(message)".data(using: .utf8)
FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
} else {
// The tunnel was probably started from iOS Settings app
if let tunnelProvider = self.tunnelProvider {
// displayMessage() is deprecated, but there's no better alternative if invoked from iOS Settings
tunnelProvider.displayMessage("\(title): \(message)") { _ in }
}
}
}
static func removeLastErrorFile() {
if let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL {
_ = FileManager.deleteFile(at: lastErrorFileURL)
}
}
}
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
import Foundation
import Network
import NetworkExtension
import os.log
enum PacketTunnelProviderError: Error {
@@ -12,52 +13,45 @@ enum PacketTunnelProviderError: Error {
case coultNotSetNetworkSettings
}
private var logFileHandle: FileHandle?
/// A packet tunnel provider object.
class PacketTunnelProvider: NEPacketTunnelProvider {
// MARK: Properties
private var wgHandle: Int32?
// MARK: NEPacketTunnelProvider
private var networkMonitor: NWPathMonitor?
/// Begin the process of establishing the tunnel.
override func startTunnel(options: [String: NSObject]?,
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
deinit {
networkMonitor?.cancel()
}
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId, tunnelProvider: self)
guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self)
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
startTunnel(with: tunnelConfiguration, errorNotifier: errorNotifier, completionHandler: startTunnelCompletionHandler)
}
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
// Configure logging
//swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, errorNotifier: ErrorNotifier, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
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
wg_log(.info, message: "Starting tunnel '\(tunnelConfiguration.interface.name)'")
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
var resolvedEndpoints: [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)
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
return
} catch {
@@ -66,79 +60,78 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
assert(endpoints.count == resolvedEndpoints.count)
// Setup packetTunnelSettingsGenerator
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration,
resolvedEndpoints: resolvedEndpoints)
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
// Bring up wireguard-go backend
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
if fd < 0 {
let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast
if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
let wireguardSettings = packetTunnelSettingsGenerator.generateWireGuardSettings()
let handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fd: fd)
let wireguardSettings = packetTunnelSettingsGenerator.uapiConfiguration()
var handle: Int32 = -1
networkMonitor = NWPathMonitor()
networkMonitor?.pathUpdateHandler = { path in
guard handle >= 0 else { return }
if path.status == .satisfied {
wg_log(.debug, message: "Network change detected, re-establishing sockets and IPs: \(path.availableInterfaces)")
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: wgGetListenPort(handle))
let err = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
if err == -EADDRINUSE {
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: 0)
_ = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
}
}
}
networkMonitor?.start(queue: DispatchQueue(label: "NetworkMonitor"))
handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fileDescriptor: fileDescriptor)
if handle < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
wgHandle = handle
// Apply network settings
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
setTunnelNetworkSettings(networkSettings) { (error) in
setTunnelNetworkSettings(networkSettings) { error in
if let error = error {
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)
errorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else {
startTunnelCompletionHandler(nil /* No errors */)
startTunnelCompletionHandler(nil)
}
}
}
/// Begin the process of stopping the tunnel.
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
networkMonitor?.cancel()
networkMonitor = nil
ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle {
wgTurnOff(handle)
}
if let fileHandle = logFileHandle {
fileHandle.closeFile()
}
completionHandler()
}
private func configureLogger() {
// Setup writing the log to a file
if let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL {
let fileManager = FileManager.default
let filePath = networkExtensionLogFileURL.path
fileManager.createFile(atPath: filePath, contents: nil) // Create the file if it doesn't already exist
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
logFileHandle = fileHandle
} else {
os_log("Can't open log file for writing. Log is not saved to file.", log: OSLog.default, type: .error)
logFileHandle = nil
}
} else {
os_log("Can't obtain log file URL. Log is not saved to file.", log: OSLog.default, type: .error)
}
// Setup WireGuard logger
wgSetLogger { (level, msgCStr) in
Logger.configureGlobal(withFilePath: FileManager.networkExtensionLogFileURL?.path)
wgSetLogger { level, msgC in
guard let msgC = msgC else { return }
let logType: OSLogType
switch level {
case 0:
@@ -150,67 +143,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
default:
logType = .default
}
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
wg_log(logType, message: msg)
wg_log(logType, message: String(cString: msgC))
}
}
private func connect(interfaceName: String, settings: String, fd: Int32) -> Int32 { // swiftlint:disable:this cyclomatic_complexity
return withStringsAsGoStrings(interfaceName, settings) { (nameGoStr, settingsGoStr) -> Int32 in
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 connect(interfaceName: String, settings: String, fileDescriptor: Int32) -> Int32 {
return withStringsAsGoStrings(interfaceName, settings) { return wgTurnOn($0.0, $0.1, fileDescriptor) }
}
}
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R {
return str1.withCString { (s1cStr) -> R in
let gstr1 = gostring_t(p: s1cStr, n: str1.utf8.count)
return str2.withCString { (s2cStr) -> R in
let gstr2 = gostring_t(p: s2cStr, n: str2.utf8.count)
return closure(gstr1, gstr2)
}
}
}
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()
// swiftlint:disable:next large_tuple identifier_name
func withStringsAsGoStrings<R>(_ s1: String, _ s2: String? = nil, _ s3: String? = nil, _ s4: String? = nil, call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
// swiftlint:disable:next large_tuple identifier_name
func helper(_ p1: UnsafePointer<Int8>?, _ p2: UnsafePointer<Int8>?, _ p3: UnsafePointer<Int8>?, _ p4: UnsafePointer<Int8>?, _ call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
return call((gostring_t(p: p1, n: s1.utf8.count), gostring_t(p: p2, n: s2?.utf8.count ?? 0), gostring_t(p: p3, n: s3?.utf8.count ?? 0), gostring_t(p: p4, n: s4?.utf8.count ?? 0)))
}
return helper(s1, s2, s3, s4, call)
}
@@ -15,52 +15,61 @@ class PacketTunnelSettingsGenerator {
self.resolvedEndpoints = resolvedEndpoints
}
func generateWireGuardSettings() -> String {
func endpointUapiConfiguration(currentListenPort: UInt16) -> String {
var wgSettings = "listen_port=\(tunnelConfiguration.interface.listenPort ?? currentListenPort)\n"
for (index, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let endpoint = resolvedEndpoints[index] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
}
}
return wgSettings
}
func uapiConfiguration() -> String {
var wgSettings = ""
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
wgSettings.append("private_key=\(privateKey)\n")
if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n")
}
if (tunnelConfiguration.peers.count > 0) {
if tunnelConfiguration.peers.count > 0 {
wgSettings.append("replace_peers=true\n")
}
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (i, peer) in tunnelConfiguration.peers.enumerated() {
for (index, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let preSharedKey = peer.preSharedKey {
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
}
if let endpoint = resolvedEndpoints[i] {
if let endpoint = resolvedEndpoints[index] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
}
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if (!peer.allowedIPs.isEmpty) {
if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n")
for ip in peer.allowedIPs {
wgSettings.append("allowed_ip=\(ip.stringRepresentation())\n")
}
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation())\n") }
}
}
return wgSettings
}
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
// Remote address
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
var remoteAddress: String = "0.0.0.0"
let endpointsCompact = resolvedEndpoints.compactMap({ $0 })
var remoteAddress = "0.0.0.0"
let endpointsCompact = resolvedEndpoints.compactMap { $0 }
if endpointsCompact.count == 1 {
switch (endpointsCompact.first!.host) {
switch endpointsCompact.first!.host {
case .ipv4(let address):
remoteAddress = "\(address)"
case .ipv6(let address):
@@ -72,130 +81,98 @@ class PacketTunnelSettingsGenerator {
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
// DNS
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
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
networkSettings.dnsSettings = dnsSettings
// MTU
let mtu = tunnelConfiguration.interface.mtu ?? 0
if (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
let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let (ipv4ExcludedRoutes, ipv6ExcludedRoutes) = excludedRoutes()
var ipv4Addresses: [String] = []
var ipv4SubnetMasks: [String] = []
var ipv6Addresses: [String] = []
var ipv6NetworkPrefixLengths: [NSNumber] = []
for addressRange in tunnelConfiguration.interface.addresses {
if (addressRange.address is IPv4Address) {
ipv4Addresses.append("\(addressRange.address)")
ipv4SubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6Addresses.append("\(addressRange.address)")
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
// Included routes from AllowedIPs
var ipv4IncludedRouteAddresses: [String] = []
var ipv4IncludedRouteSubnetMasks: [String] = []
var ipv6IncludedRouteAddresses: [String] = []
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if (addressRange.address is IPv4Address) {
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
ipv4IncludedRouteSubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
}
// Excluded routes from endpoints
var ipv4ExcludedRouteAddresses: [String] = []
var ipv4ExcludedRouteSubnetMasks: [String] = []
var ipv6ExcludedRouteAddresses: [String] = []
var ipv6ExcludedRouteNetworkPrefixLengths: [NSNumber] = []
for endpoint in resolvedEndpoints {
guard let endpoint = endpoint else { continue }
switch (endpoint.host) {
case .ipv4(let address):
ipv4ExcludedRouteAddresses.append("\(address)")
ipv4ExcludedRouteSubnetMasks.append("255.255.255.255") // A single IPv4 address
case .ipv6(let address):
ipv6ExcludedRouteAddresses.append("\(address)")
ipv6ExcludedRouteNetworkPrefixLengths.append(NSNumber(value: UInt8(128))) // A single IPv6 address
default:
fatalError()
}
}
// Apply 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)
}
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes
ipv4Settings.excludedRoutes = ipv4ExcludedRoutes
networkSettings.ipv4Settings = ipv4Settings
// 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)
}
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
ipv6Settings.excludedRoutes = ipv6ExcludedRoutes
networkSettings.ipv6Settings = ipv6Settings
// Done
return networkSettings
}
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let n: UInt8 = addressRange.networkPrefixLength
assert(n <= 32)
private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let length: UInt8 = addressRange.networkPrefixLength
assert(length <= 32)
var octets: [UInt8] = [0, 0, 0, 0]
let subnetMask: UInt32 = n > 0 ? ~UInt32(0) << (32 - n) : UInt32(0)
let subnetMask: UInt32 = length > 0 ? ~UInt32(0) << (32 - length) : UInt32(0)
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
return octets.map { String($0) }.joined(separator: ".")
}
private func routes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4Routes = [NEIPv4Route]()
var ipv6Routes = [NEIPv6Route]()
for addressRange in tunnelConfiguration.interface.addresses {
if addressRange.address is IPv4Address {
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
/* 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.
*/
ipv6Routes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: min(120, addressRange.networkPrefixLength))))
}
}
return (ipv4Routes, ipv6Routes)
}
private func includedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if addressRange.address is IPv4Address {
ipv4IncludedRoutes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
ipv6IncludedRoutes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength)))
}
}
}
return (ipv4IncludedRoutes, ipv6IncludedRoutes)
}
private func excludedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv6ExcludedRoutes = [NEIPv6Route]()
for endpoint in resolvedEndpoints {
guard let endpoint = endpoint else { continue }
switch endpoint.host {
case .ipv4(let address):
ipv4ExcludedRoutes.append(NEIPv4Route(destinationAddress: "\(address)", subnetMask: "255.255.255.255"))
case .ipv6(let address):
ipv6ExcludedRoutes.append(NEIPv6Route(destinationAddress: "\(address)", networkPrefixLength: NSNumber(value: UInt8(128))))
default:
fatalError()
}
}
return (ipv4ExcludedRoutes, ipv6ExcludedRoutes)
}
}
private extension Data {
@@ -1,2 +1,3 @@
#include "../../wireguard-go-bridge/wireguard.h"
#include "wireguard-go-version.h"
#include "ringlogger.h"
+20 -26
View File
@@ -2,24 +2,24 @@
#
# Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
# These are generally passed to us by xcode, but we set working defaults for standalone compilation too.
ARCHS ?= arm64 armv7
SDK_NAME ?= iphoneos
SDK_DIR ?= $(shell xcrun --sdk $(SDK_NAME) --show-sdk-path)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
export CC ?= clang
LIPO ?= lipo
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum
DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go)
ARCHS ?= arm64 armv7 x86_64
CFLAGS_PREFIX := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)) -isysroot $(SDK_DIR) -arch
GOARCH_arm64 := arm64
GOARCH_armv7 := arm
GOARCH_x86_64 := amd64
DEPLOYMENT_CFLAGS := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME))
CGO_FLAGS_x86_64 := -arch x86_64 -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CGO_FLAGS_arm64 := -arch arm64 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CGO_FLAGS_armv7 := -arch armv7 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CC_x86_64 := $(shell xcrun --sdk iphonesimulator --find clang)
CC_arm64 := $(shell xcrun --sdk iphoneos --find clang)
CC_armv7 := $(shell xcrun --sdk iphoneos --find clang)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
export GOOS := darwin
export CGO_ENABLED := 1
@@ -28,7 +28,7 @@ version-header: $(DESTDIR)/wireguard-go-version.h
GOBUILDARCH := $(GOARCH_$(shell uname -m))
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
GOBUILDVERSION := 1.11.2
GOBUILDVERSION := 1.11.4
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
export GOROOT := $(BUILDDIR)/goroot
@@ -57,16 +57,15 @@ $(foreach FILE,$(UPSTREAM_FILES),$(eval $(call copy-src-to-build,../wireguard-go
$(foreach FILE,$(DOWNSTREAM_FILES),$(eval $(call copy-src-to-build,src/,$(FILE))))
$(BUILDDIR)/.prepared: $(GOROOT)/bin/go
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CC="$(CC_$(ARCH))" CGO_CFLAGS="$(CGO_FLAGS_$(ARCH))" CGO_LDFLAGS="$(CGO_FLAGS_$(ARCH))" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
chmod -fR +w "$(GOPATH)/pkg/mod"
touch "$@"
define libwg-go-a
$(BUILDDIR)/libwg-go-$(1).a: $(BUILDDIR)/.prepared
cd "$(BUILDDIR)" || exit $$$$?; \
CC="$(CC_$(1))" \
CGO_CFLAGS="$(CGO_FLAGS_$(1))" \
CGO_LDFLAGS="$(CGO_FLAGS_$(1))" \
CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
GOARCH="$(GOARCH_$(1))" \
go build -tags ios -ldflags=-w -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive && go version > "$(BUILDDIR)/.gobuildversion"; \
ret=$$$$?; \
@@ -80,16 +79,11 @@ $(DESTDIR)/wireguard-go-version.h: ../wireguard-go/version.go
$(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
@mkdir -vp "$(DESTDIR)"
xcrun --sdk iphoneos lipo -create -output "$@" $^
$(DESTDIR)/example: example.c $(DESTDIR)/libwg-go.a
xcrun --sdk iphoneos clang -framework CoreFoundation -isysroot "$(shell xcrun --sdk iphoneos --show-sdk-path)" -arch arm64 -arch armv7 -L"$(DESTDIR)" -lwg-go -o "$@" "$<"
example: $(DESTDIR)/example
$(LIPO) -create -output "$@" $^
clean:
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/example" "$(DESTDIR)/wireguard-go-version.h"
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h"
install: build
.PHONY: clean build example version-header install
.PHONY: clean build version-header install
-46
View File
@@ -1,46 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#include "wireguard.h"
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
static struct {
int something;
} ctx;
static bool is_closed = false;
ssize_t do_read(void *ctx, unsigned char *buf, size_t len)
{
printf("Reading from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
sleep(1);
return is_closed ? -1 : 0;
}
ssize_t do_write(void *ctx, unsigned char *buf, size_t len)
{
printf("Writing from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
return len;
}
void do_log(int level, const char *msg)
{
printf("Log level %d: %s", level, msg);
}
int main(int argc, char *argv[])
{
int handle;
printf("WireGuard Go Version %s\n", wgVersion());
wgSetLogger(do_log);
handle = wgTurnOn((gostring_t){ .p = "test", .n = 4 }, (gostring_t){ .p = "", .n = 0 }, 0, do_read, do_write, &ctx);
sleep(5);
is_closed = true;
wgTurnOff(handle);
return 0;
}
+28 -84
View File
@@ -25,8 +25,6 @@ import (
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"unsafe"
)
@@ -48,54 +46,12 @@ func (l *CLogger) Write(p []byte) (int, error) {
return len(p), nil
}
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)
}
})
}
}
}
}
var tunnelHandles map[int32]*Device
func init() {
versionString = C.CString(WireGuardGoVersion)
roamingDisabled = true
tunnelHandles = make(map[int32]*DeviceState)
tunnelHandles = make(map[int32]*Device)
signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2)
go func() {
@@ -111,7 +67,6 @@ func init() {
}
}
}()
go listenForRouteChanges()
}
//export wgSetLogger
@@ -119,32 +74,6 @@ func wgSetLogger(loggerFn uintptr) {
loggerFunc = unsafe.Pointer(loggerFn)
}
func extractEndpointFromSettings(settings string) string {
var b strings.Builder
pubkey := ""
endpoint := ""
listenPort := "listen_port=0"
for _, line := range strings.Split(settings, "\n") {
if strings.HasPrefix(line, "listen_port=") {
listenPort = line
} else if strings.HasPrefix(line, "public_key=") {
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
pubkey = line
} else if strings.HasPrefix(line, "endpoint=") {
endpoint = line
} else if line == "remove=true" {
pubkey = ""
endpoint = ""
}
}
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
return listenPort + "\n" + b.String()
}
//export wgTurnOn
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
interfaceName := string([]byte(ifnameRef))
@@ -184,27 +113,42 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
if i == math.MaxInt32 {
return -1
}
tunnelHandles[i] = &DeviceState{
device: device,
logger: logger,
endpointsSettings: extractEndpointFromSettings(settings),
}
tunnelHandles[i] = device
return i
}
//export wgTurnOff
func wgTurnOff(tunnelHandle int32) {
deviceState, ok := tunnelHandles[tunnelHandle]
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return
}
delete(tunnelHandles, tunnelHandle)
t := deviceState.endpointsTimer
if t != nil {
deviceState.endpointsTimer = nil
t.Stop()
device.Close()
}
//export wgSetConfig
func wgSetConfig(tunnelHandle int32, settings string) int64 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return 0
}
deviceState.device.Close()
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
err := ipcSetOperation(device, bufferedSettings)
if err != nil {
device.log.Error.Println(err)
return err.Code
}
return 0
}
//export wgGetListenPort
func wgGetListenPort(tunnelHandle int32) uint16 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return 0
}
return device.net.port
}
//export wgVersion
+2
View File
@@ -14,6 +14,8 @@ typedef void(*logger_fn_t)(int level, const char *msg);
extern void wgSetLogger(logger_fn_t logger_fn);
extern int wgTurnOn(gostring_t ifname, gostring_t settings, int32_t tun_fd);
extern void wgTurnOff(int handle);
extern int64_t wgSetConfig(int handle, gostring_t settings);
extern uint16_t wgGetListenPort(int handle);
extern char *wgVersion();
#endif