Compare commits

..
Author SHA1 Message Date
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
59 changed files with 3526 additions and 2966 deletions
+3
View File
@@ -38,3 +38,6 @@ fastlane/screenshots
fastlane/test_output fastlane/test_output
Preview.html Preview.html
output 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 $ vim WireGuard/WireGuard/Config/Developer.xcconfig
``` ```
- Install swiftlint:
```
$ brew install swiftlint
```
- Open project in XCode: - 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 import os.log
extension FileManager { 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 { 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 return nil
} }
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else { 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 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 { static func deleteFile(at url: URL) -> Bool {
do { do {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} catch(let e) { } catch {
os_log("Failed to delete file '%{public}@': %{public}@", log: OSLog.default, type: .debug, url.absoluteString, e.localizedDescription)
return false return false
} }
return true 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 { final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration var interface: InterfaceConfiguration
let peers: [PeerConfiguration] let peers: [PeerConfiguration]
static let keyLength: Int = 32
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) { init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface self.interface = interface
self.peers = peers self.peers = peers
let peerPublicKeysArray = peers.map { $0.publicKey } let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) 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") fatalError("Two or more peers cannot have the same public key")
} }
} }
@@ -23,16 +26,20 @@ final class TunnelConfiguration: Codable {
struct InterfaceConfiguration: Codable { struct InterfaceConfiguration: Codable {
var name: String var name: String
var privateKey: Data var privateKey: Data
var addresses: [IPAddressRange] = [] var addresses = [IPAddressRange]()
var listenPort: UInt16? var listenPort: UInt16?
var mtu: UInt16? var mtu: UInt16?
var dns: [DNSServer] = [] var dns = [DNSServer]()
init(name: String, privateKey: Data) { init(name: String, privateKey: Data) {
self.name = name self.name = name
self.privateKey = privateKey self.privateKey = privateKey
if (name.isEmpty) { fatalError("Empty name") } if name.isEmpty {
if (privateKey.count != 32) { fatalError("Invalid private key") } fatalError("Empty name")
}
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
} }
} }
@@ -42,16 +49,20 @@ struct PeerConfiguration: Codable {
var preSharedKey: Data? { var preSharedKey: Data? {
didSet(value) { didSet(value) {
if let value = 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 endpoint: Endpoint?
var persistentKeepAlive: UInt16? var persistentKeepAlive: UInt16?
init(publicKey: Data) { init(publicKey: Data) {
self.publicKey = publicKey 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() let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self) var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = { let ipAddressFromData: IPAddress? = {
switch (data.count) { switch data.count {
case 4: return IPv4Address(data) case 4: return IPv4Address(data)
case 16: return IPv6Address(data) case 16: return IPv6Address(data)
default: return nil default: return nil
+15 -15
View File
@@ -17,15 +17,15 @@ extension Endpoint {
init?(from string: String) { init?(from string: String) {
// Separation of host and port is based on 'parse_endpoint' function in // Separation of host and port is based on 'parse_endpoint' function in
// https://git.zx2c4.com/WireGuard/tree/src/tools/config.c // 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 startOfPort: String.Index
let hostString: String let hostString: String
if (string.first! == "[") { if string.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80 // Look for IPv6-style endpoint, like [::1]:80
let startOfHost = string.index(after: string.startIndex) let startOfHost = string.index(after: string.startIndex)
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil } guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
let afterEndOfHost = string.index(after: endOfHost) let afterEndOfHost = string.index(after: endOfHost)
guard (string[afterEndOfHost] == ":") else { return nil } guard string[afterEndOfHost] == ":" else { return nil }
startOfPort = string.index(after: afterEndOfHost) startOfPort = string.index(after: afterEndOfHost)
hostString = String(string[startOfHost ..< endOfHost]) hostString = String(string[startOfHost ..< endOfHost])
} else { } else {
@@ -35,15 +35,15 @@ extension Endpoint {
hostString = String(string[string.startIndex ..< endOfHost]) hostString = String(string[string.startIndex ..< endOfHost])
} }
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil } guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { (c) -> Bool in let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(c) return !CharacterSet.urlHostAllowed.contains(char)
} }
guard (invalidCharacterIndex == nil) else { return nil } guard invalidCharacterIndex == nil else { return nil }
host = NWEndpoint.Host(hostString) host = NWEndpoint.Host(hostString)
port = endpointPort port = endpointPort
} }
func stringRepresentation() -> String { func stringRepresentation() -> String {
switch (host) { switch host {
case .name(let hostname, _): case .name(let hostname, _):
return "\(hostname):\(port)" return "\(hostname):\(port)"
case .ipv4(let address): case .ipv4(let address):
@@ -61,7 +61,7 @@ extension Endpoint {
extension Endpoint: Codable { extension Endpoint: Codable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(self.stringRepresentation()) try container.encode(stringRepresentation())
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
@@ -78,23 +78,23 @@ extension Endpoint: Codable {
extension Endpoint { extension Endpoint {
func hasHostAsIPAddress() -> Bool { func hasHostAsIPAddress() -> Bool {
switch (host) { switch host {
case .name(_, _): case .name:
return false return false
case .ipv4(_): case .ipv4:
return true return true
case .ipv6(_): case .ipv6:
return true return true
} }
} }
func hostname() -> String? { func hostname() -> String? {
switch (host) { switch host {
case .name(let hostname, _): case .name(let hostname, _):
return hostname return hostname
case .ipv4(_): case .ipv4:
return nil return nil
case .ipv6(_): case .ipv6:
return nil return nil
} }
} }
+4 -6
View File
@@ -27,9 +27,9 @@ extension IPAddressRange {
} }
let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128 let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128
var networkPrefixLength: UInt8 var networkPrefixLength: UInt8
if (endOfIPAddress < string.endIndex) { // "/" was located if endOfIPAddress < string.endIndex { // "/" was located
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress) 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] let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil } guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
networkPrefixLength = min(npl, maxNetworkPrefixLength) networkPrefixLength = min(npl, maxNetworkPrefixLength)
@@ -69,15 +69,13 @@ extension IPAddressRange: Codable {
var data = try container.decode(Data.self) var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast() networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = { let ipAddressFromData: IPAddress? = {
switch (data.count) { switch data.count {
case 4: return IPv4Address(data) case 4: return IPv4Address(data)
case 16: return IPv6Address(data) case 16: return IPv6Address(data)
default: return nil default: return nil
} }
}() }()
guard let ipAddress = ipAddressFromData else { guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
throw DecodingError.invalidData
}
address = ipAddress address = ipAddress
} }
enum DecodingError: Error { enum DecodingError: Error {
@@ -17,7 +17,7 @@ extension NETunnelProviderProtocol {
"tunnelConfigurationVersion": 1 "tunnelConfigurationVersion": 1
] ]
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint}) let endpoints = tunnelConfiguration.peers.compactMap {$0.endpoint}
if endpoints.count == 1 { if endpoints.count == 1 {
serverAddress = endpoints.first!.stringRepresentation() serverAddress = endpoints.first!.stringRepresentation()
} else if endpoints.isEmpty { } else if endpoints.isEmpty {
+121 -21
View File
@@ -7,8 +7,15 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; }; 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; };
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.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 */; };
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.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 */; }; 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; }; 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
@@ -44,9 +51,12 @@
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; }; 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; }; 6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.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 */; }; 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; }; 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */; };
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; }; 6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; }; 6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; }; 6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
@@ -91,8 +101,15 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; }; 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; };
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.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>"; };
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; }; 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; }; 6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -137,6 +154,9 @@
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; }; 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; }; 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>"; }; 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; }; 6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -144,7 +164,6 @@
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; }; 6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; }; 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetReachability.swift; sourceTree = "<group>"; };
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
@@ -169,6 +188,33 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup 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 */ = { 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -186,6 +232,7 @@
6F5D0C432183B4A4000F85AD /* Shared */ = { 6F5D0C432183B4A4000F85AD /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6FF3526A21C23F720008484E /* Logging */,
6F7774E6217201E0006A79B3 /* Model */, 6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */, 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */, 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
@@ -224,16 +271,11 @@
6F7774DE217181B1006A79B3 /* iOS */ = { 6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */, 5F4541AC21C4720B00994C13 /* ViewController */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */, 5F4541A721C44F5B00994C13 /* View */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */, 6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */, 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */, 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
); );
path = iOS; path = iOS;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -249,14 +291,14 @@
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6F7774ED21722D0C006A79B3 /* VPN */ = { 6F7774ED21722D0C006A79B3 /* Tunnel */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */, 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */, 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */, 5F4541A821C451D100994C13 /* TunnelStatus.swift */,
); );
path = VPN; path = Tunnel;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6F919ED3218C65C50023B400 /* Resources */ = { 6F919ED3218C65C50023B400 /* Resources */ = {
@@ -310,6 +352,16 @@
path = minizip; path = minizip;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6FF3526A21C23F720008484E /* Logging */ = {
isa = PBXGroup;
children = (
6FF3526C21C23F960008484E /* ringlogger.c */,
6FF3526B21C23F960008484E /* ringlogger.h */,
6FF3526E21C23FA10008484E /* Logger.swift */,
);
path = Logging;
sourceTree = "<group>";
};
6FF4AC0B211EC46F002C96EB = { 6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -337,7 +389,7 @@
6F6899A32180445A0012E523 /* Crypto */, 6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */, 6F6899AA218099D00012E523 /* ConfigFile */,
6F7774DD217181B1006A79B3 /* UI */, 6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* VPN */, 6F7774ED21722D0C006A79B3 /* Tunnel */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */, 6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */, 6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */, 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
@@ -384,6 +436,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */; buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
buildPhases = ( buildPhases = (
5F45417B21C0906F00994C13 /* Swiftlint */,
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */, 6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
6F5D0C16218352EF000F85AD /* Sources */, 6F5D0C16218352EF000F85AD /* Sources */,
6F5D0C17218352EF000F85AD /* Frameworks */, 6F5D0C17218352EF000F85AD /* Frameworks */,
@@ -403,6 +456,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */; buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
buildPhases = ( buildPhases = (
5F45417A21C0902400994C13 /* Swiftlint */,
6B87860E2189532500C099FB /* Extract wireguard-go Version */, 6B87860E2189532500C099FB /* Extract wireguard-go Version */,
6FF4AC10211EC46F002C96EB /* Sources */, 6FF4AC10211EC46F002C96EB /* Sources */,
6FF4AC11211EC46F002C96EB /* Frameworks */, 6FF4AC11211EC46F002C96EB /* Frameworks */,
@@ -494,6 +548,42 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase 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 */ = { 6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -539,6 +629,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FF3527021C240160008484E /* ringlogger.c in Sources */,
6FF3527121C240160008484E /* Logger.swift in Sources */,
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */, 6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */, 6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
@@ -556,15 +648,18 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */, 6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */, 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */, 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */, 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */, 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
@@ -572,22 +667,27 @@
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */, 6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */, 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */, 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */, 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */, 6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */, 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */, 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */, 6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */, 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */, 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */, 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104 VERSION_NAME = 0.0.20181104
VERSION_ID = 5 VERSION_ID = 6
@@ -20,83 +20,17 @@ class WgQuickConfigFileParser {
case invalidPeer case invalidPeer
} }
//swiftlint:disable:next cyclomatic_complexity function_body_length
static func parse(_ text: String, name: String) throws -> TunnelConfiguration { static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
assert(!name.isEmpty) 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 interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations: [PeerConfiguration] = [] var peerConfigurations = [PeerConfiguration]()
let lines = text.split(separator: "\n") let lines = text.split(separator: "\n")
var parserState: ParserState = .notInASection var parserState = ParserState.notInASection
var attributes: [String: String] = [:] var attributes = [String: String]()
for (lineIndex, line) in lines.enumerated() { for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String var trimmedLine: String
@@ -121,30 +55,28 @@ class WgQuickConfigFileParser {
} else { } else {
attributes[key] = value attributes[key] = value
} }
} else { } else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
if (lowercasedLine != "[interface]" && lowercasedLine != "[peer]") { throw ParseError.invalidLine(line)
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 // Previous section has ended; process the attributes collected so far
if (parserState == .inInterfaceSection) { if parserState == .inInterfaceSection {
guard let interface = collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface } guard let interface = collate(interfaceAttributes: attributes, name: name) else { throw ParseError.invalidInterface }
guard (interfaceConfiguration == nil) else { throw ParseError.multipleInterfaces } guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface interfaceConfiguration = interface
} else if (parserState == .inPeerSection) { } else if parserState == .inPeerSection {
guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer } guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer) peerConfigurations.append(peer)
} }
} }
if (lowercasedLine == "[interface]") { if lowercasedLine == "[interface]" {
parserState = .inInterfaceSection parserState = .inInterfaceSection
attributes.removeAll() attributes.removeAll()
} else if (lowercasedLine == "[peer]") { } else if lowercasedLine == "[peer]" {
parserState = .inPeerSection parserState = .inPeerSection
attributes.removeAll() attributes.removeAll()
} }
@@ -152,7 +84,7 @@ class WgQuickConfigFileParser {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) { if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey throw ParseError.multiplePeersWithSamePublicKey
} }
@@ -163,4 +95,73 @@ class WgQuickConfigFileParser {
throw ParseError.noInterface 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 import UIKit
class WgQuickConfigFileWriter { class WgQuickConfigFileWriter {
static func writeConfigFile(from tc: TunnelConfiguration) -> Data? { static func writeConfigFile(from configuration: TunnelConfiguration) -> Data? {
let interface = tc.interface let interface = configuration.interface
var output = "[Interface]\n" var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n") output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
if let listenPort = interface.listenPort { if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n") output.append("ListenPort = \(listenPort)\n")
} }
if (!interface.addresses.isEmpty) { if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ") let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("Address = \(addressString)\n") output.append("Address = \(addressString)\n")
} }
if (!interface.dns.isEmpty) { if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ") let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n") output.append("DNS = \(dnsString)\n")
} }
@@ -23,13 +23,13 @@ class WgQuickConfigFileWriter {
output.append("MTU = \(mtu)\n") output.append("MTU = \(mtu)\n")
} }
for peer in tc.peers { for peer in configuration.peers {
output.append("\n[Peer]\n") output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n") output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
if let preSharedKey = peer.preSharedKey { if let preSharedKey = peer.preSharedKey {
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n") output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
} }
if (!peer.allowedIPs.isEmpty) { if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ") let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n") output.append("AllowedIPs = \(allowedIPsString)\n")
} }
+11 -8
View File
@@ -4,24 +4,27 @@
import UIKit import UIKit
struct Curve25519 { struct Curve25519 {
static let keyLength: Int = 32
static func generatePrivateKey() -> Data { static func generatePrivateKey() -> Data {
var privateKey = Data(repeating: 0, count: 32) var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in privateKey.withUnsafeMutableBytes { bytes in
curve25519_generate_private_key(bytes) curve25519_generate_private_key(bytes)
} }
assert(privateKey.count == 32) assert(privateKey.count == TunnelConfiguration.keyLength)
return privateKey return privateKey
} }
static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data { static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data {
assert(privateKey.count == 32) assert(privateKey.count == TunnelConfiguration.keyLength)
var publicKey = Data(repeating: 0, count: 32) var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeBytes { (privateKeyBytes: UnsafePointer<UInt8>) in privateKey.withUnsafeBytes { privateKeyBytes in
publicKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in publicKey.withUnsafeMutableBytes { bytes in
curve25519_derive_public_key(bytes, privateKeyBytes) curve25519_derive_public_key(bytes, privateKeyBytes)
} }
} }
assert(publicKey.count == 32) assert(publicKey.count == TunnelConfiguration.keyLength)
return publicKey return publicKey
} }
} }
@@ -21,7 +21,7 @@ extension ActivateOnDemandSetting {
let rules: [NEOnDemandRule]? let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect() let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect() let disconnectRule = NEOnDemandRuleDisconnect()
switch (activateOnDemandOption) { switch activateOnDemandOption {
case .none: case .none:
rules = nil rules = nil
case .useOnDemandOverWiFiOrCellular: case .useOnDemandOverWiFiOrCellular:
@@ -41,7 +41,7 @@ extension ActivateOnDemandSetting {
init(from tunnelProviderManager: NETunnelProviderManager) { init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? [] let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption let activateOnDemandOption: ActivateOnDemandOption
switch (rules.count) { switch rules.count {
case 0: case 0:
activateOnDemandOption = .none activateOnDemandOption = .none
case 1: case 1:
@@ -51,9 +51,9 @@ extension ActivateOnDemandSetting {
case 2: case 2:
let connectRule = rules.first(where: { $0.action == .connect })! let connectRule = rules.first(where: { $0.action == .connect })!
let disconnectRule = rules.first(where: { $0.action == .disconnect })! let disconnectRule = rules.first(where: { $0.action == .disconnect })!
if (connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular) { if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
activateOnDemandOption = .useOnDemandOverWiFiOnly activateOnDemandOption = .useOnDemandOverWiFiOnly
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) { } else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .useOnDemandOverCellularOnly activateOnDemandOption = .useOnDemandOverCellularOnly
} else { } else {
fatalError("Unexpected onDemandRules set on tunnel provider manager") fatalError("Unexpected onDemandRules set on tunnel provider manager")
@@ -62,10 +62,10 @@ extension ActivateOnDemandSetting {
fatalError("Unexpected number of onDemandRules set on tunnel provider manager") fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
} }
self.activateOnDemandOption = activateOnDemandOption self.activateOnDemandOption = activateOnDemandOption
if (activateOnDemandOption == .none) { if activateOnDemandOption == .none {
self.isActivateOnDemandEnabled = false isActivateOnDemandEnabled = false
} else { } else {
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
} }
} }
} }
@@ -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,495 @@
// 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: [])))
#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 {
if tunnels.contains(where: { $0.name == tunnelName }) {
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
}
tunnel.startActivation(activationDelegate: activationDelegate)
}
func startDeactivation(of tunnel: TunnelContainer) {
tunnel.isAttemptingActivation = false
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
tunnel.startDeactivation()
}
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()
}
}
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"
}
}
}
+63 -66
View File
@@ -3,6 +3,7 @@
import UIKit import UIKit
//swiftlint:disable:next type_body_length
class TunnelViewModel { class TunnelViewModel {
enum InterfaceField: String { enum InterfaceField: String {
@@ -37,13 +38,13 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44 static let keyLengthInBase64 = 44
class InterfaceData { class InterfaceData {
var scratchpad: [InterfaceField: String] = [:] var scratchpad = [InterfaceField: String]()
var fieldsWithError: Set<InterfaceField> = [] var fieldsWithError = Set<InterfaceField>()
var validatedConfiguration: InterfaceConfiguration? var validatedConfiguration: InterfaceConfiguration?
subscript(field: InterfaceField) -> String { subscript(field: InterfaceField) -> String {
get { get {
if (scratchpad.isEmpty) { if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad. // 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. // The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad() populateScratchpad()
@@ -51,21 +52,19 @@ class TunnelViewModel {
return scratchpad[field] ?? "" return scratchpad[field] ?? ""
} }
set(stringValue) { set(stringValue) {
if (scratchpad.isEmpty) { if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration. // 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. // The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad() populateScratchpad()
} }
validatedConfiguration = nil validatedConfiguration = nil
if (stringValue.isEmpty) { if stringValue.isEmpty {
scratchpad.removeValue(forKey: field) scratchpad.removeValue(forKey: field)
} else { } else {
scratchpad[field] = stringValue scratchpad[field] = stringValue
} }
if (field == .privateKey) { if field == .privateKey {
if (stringValue.count == TunnelViewModel.keyLengthInBase64), if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
let privateKey = Data(base64Encoded: stringValue),
privateKey.count == 32 {
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey) let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
scratchpad[.publicKey] = publicKey.base64EncodedString() scratchpad[.publicKey] = publicKey.base64EncodedString()
} else { } else {
@@ -81,7 +80,7 @@ class TunnelViewModel {
scratchpad[.name] = config.name scratchpad[.name] = config.name
scratchpad[.privateKey] = config.privateKey.base64EncodedString() scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString() scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if (!config.addresses.isEmpty) { if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ") scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
} }
if let listenPort = config.listenPort { if let listenPort = config.listenPort {
@@ -90,11 +89,12 @@ class TunnelViewModel {
if let mtu = config.mtu { if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu) scratchpad[.mtu] = String(mtu)
} }
if (!config.dns.isEmpty) { if !config.dns.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ") scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
} }
} }
//swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<InterfaceConfiguration> { func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration { if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved // It's already validated and saved
@@ -109,14 +109,14 @@ class TunnelViewModel {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required") 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) fieldsWithError.insert(.privateKey)
return .error("Interface's private key must be a 32-byte key in base64 encoding") return .error("Interface's private key must be a 32-byte key in base64 encoding")
} }
var config = InterfaceConfiguration(name: name, privateKey: privateKey) var config = InterfaceConfiguration(name: name, privateKey: privateKey)
var errorMessages: [String] = [] var errorMessages = [String]()
if let addressesString = scratchpad[.addresses] { if let addressesString = scratchpad[.addresses] {
var addresses: [IPAddressRange] = [] var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") { for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let address = IPAddressRange(from: trimmedString) { if let address = IPAddressRange(from: trimmedString) {
@@ -145,7 +145,7 @@ class TunnelViewModel {
} }
} }
if let dnsString = scratchpad[.dns] { if let dnsString = scratchpad[.dns] {
var dnsServers: [DNSServer] = [] var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") { for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let dnsServer = DNSServer(from: trimmedString) { if let dnsServer = DNSServer(from: trimmedString) {
@@ -158,16 +158,15 @@ class TunnelViewModel {
config.dns = dnsServers config.dns = dnsServers
} }
guard (errorMessages.isEmpty) else { guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
return .error(errorMessages.first!)
}
validatedConfiguration = config validatedConfiguration = config
return .saved(config) return .saved(config)
} }
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] { func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
return interfaceFields.filter { (field) -> Bool in return interfaceFields.filter { field in
if (TunnelViewModel.interfaceFieldsWithControl.contains(field)) { if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
return true return true
} }
return (!self[field].isEmpty) return (!self[field].isEmpty)
@@ -178,14 +177,14 @@ class TunnelViewModel {
class PeerData { class PeerData {
var index: Int var index: Int
var scratchpad: [PeerField: String] = [:] var scratchpad = [PeerField: String]()
var fieldsWithError: Set<PeerField> = [] var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration? var validatedConfiguration: PeerConfiguration?
// For exclude private IPs // For exclude private IPs
var shouldAllowExcludePrivateIPsControl: Bool = false /* Read-only from the VC's point of view */ private(set) var shouldAllowExcludePrivateIPsControl = false
var excludePrivateIPsValue: Bool = false /* Read-only from the VC's point of view */ private(set) var excludePrivateIPsValue = false
fileprivate var numberOfPeers: Int = 0 fileprivate var numberOfPeers = 0
init(index: Int) { init(index: Int) {
self.index = index self.index = index
@@ -193,7 +192,7 @@ class TunnelViewModel {
subscript(field: PeerField) -> String { subscript(field: PeerField) -> String {
get { get {
if (scratchpad.isEmpty) { if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad. // 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. // The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad() populateScratchpad()
@@ -201,18 +200,18 @@ class TunnelViewModel {
return scratchpad[field] ?? "" return scratchpad[field] ?? ""
} }
set(stringValue) { set(stringValue) {
if (scratchpad.isEmpty) { if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration. // 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. // The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad() populateScratchpad()
} }
validatedConfiguration = nil validatedConfiguration = nil
if (stringValue.isEmpty) { if stringValue.isEmpty {
scratchpad.removeValue(forKey: field) scratchpad.removeValue(forKey: field)
} else { } else {
scratchpad[field] = stringValue scratchpad[field] = stringValue
} }
if (field == .allowedIPs) { if field == .allowedIPs {
updateExcludePrivateIPsFieldState() updateExcludePrivateIPsFieldState()
} }
} }
@@ -225,7 +224,7 @@ class TunnelViewModel {
if let preSharedKey = config.preSharedKey { if let preSharedKey = config.preSharedKey {
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString() scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
} }
if (!config.allowedIPs.isEmpty) { if !config.allowedIPs.isEmpty {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ") scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
} }
if let endpoint = config.endpoint { if let endpoint = config.endpoint {
@@ -237,6 +236,7 @@ class TunnelViewModel {
updateExcludePrivateIPsFieldState() updateExcludePrivateIPsFieldState()
} }
//swiftlint:disable:next cyclomatic_complexity
func save() -> SaveResult<PeerConfiguration> { func save() -> SaveResult<PeerConfiguration> {
if let validatedConfiguration = validatedConfiguration { if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved // It's already validated and saved
@@ -247,14 +247,14 @@ class TunnelViewModel {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required") 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) fieldsWithError.insert(.publicKey)
return .error("Peer's public key must be a 32-byte key in base64 encoding") return .error("Peer's public key must be a 32-byte key in base64 encoding")
} }
var config = PeerConfiguration(publicKey: publicKey) var config = PeerConfiguration(publicKey: publicKey)
var errorMessages: [String] = [] var errorMessages = [String]()
if let preSharedKeyString = scratchpad[.preSharedKey] { 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 config.preSharedKey = preSharedKey
} else { } else {
fieldsWithError.insert(.preSharedKey) fieldsWithError.insert(.preSharedKey)
@@ -262,7 +262,7 @@ class TunnelViewModel {
} }
} }
if let allowedIPsString = scratchpad[.allowedIPs] { if let allowedIPsString = scratchpad[.allowedIPs] {
var allowedIPs: [IPAddressRange] = [] var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") { for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let allowedIP = IPAddressRange(from: trimmedString) { if let allowedIP = IPAddressRange(from: trimmedString) {
@@ -291,16 +291,15 @@ class TunnelViewModel {
} }
} }
guard (errorMessages.isEmpty) else { guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
return .error(errorMessages.first!)
}
validatedConfiguration = config validatedConfiguration = config
return .saved(config) return .saved(config)
} }
func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] { func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
return peerFields.filter { (field) -> Bool in return peerFields.filter { field in
if (TunnelViewModel.peerFieldsWithControl.contains(field)) { if TunnelViewModel.peerFieldsWithControl.contains(field) {
return true return true
} }
return (!self[field].isEmpty) return (!self[field].isEmpty)
@@ -319,12 +318,12 @@ class TunnelViewModel {
] ]
func updateExcludePrivateIPsFieldState() { func updateExcludePrivateIPsFieldState() {
guard (numberOfPeers == 1) else { guard numberOfPeers == 1 else {
shouldAllowExcludePrivateIPsControl = false shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false excludePrivateIPsValue = false
return return
} }
if (scratchpad.isEmpty) { if scratchpad.isEmpty {
populateScratchpad() populateScratchpad()
} }
let allowedIPStrings = Set<String>( let allowedIPStrings = Set<String>(
@@ -332,10 +331,10 @@ class TunnelViewModel {
.split(separator: ",") .split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } .map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
) )
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) { if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
shouldAllowExcludePrivateIPsControl = true shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = false excludePrivateIPsValue = false
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) { } else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
shouldAllowExcludePrivateIPsControl = true shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = true excludePrivateIPsValue = true
} else { } else {
@@ -353,12 +352,10 @@ class TunnelViewModel {
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } .map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") } let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
let modifiedAllowedIPStrings: [String] let modifiedAllowedIPStrings: [String]
if (isOn) { if isOn {
modifiedAllowedIPStrings = ipv6Addresses + modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
} else { } else {
modifiedAllowedIPStrings = ipv6Addresses + modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
} }
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ") scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil // The configuration has been modified, and needs to be saved validatedConfiguration = nil // The configuration has been modified, and needs to be saved
@@ -376,11 +373,11 @@ class TunnelViewModel {
init(tunnelConfiguration: TunnelConfiguration?) { init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData: InterfaceData = InterfaceData() let interfaceData: InterfaceData = InterfaceData()
var peersData: [PeerData] = [] var peersData = [PeerData]()
if let tunnelConfiguration = tunnelConfiguration { if let tunnelConfiguration = tunnelConfiguration {
interfaceData.validatedConfiguration = tunnelConfiguration.interface interfaceData.validatedConfiguration = tunnelConfiguration.interface
for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() { for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: i) let peerData = PeerData(index: index)
peerData.validatedConfiguration = peerConfiguration peerData.validatedConfiguration = peerConfiguration
peersData.append(peerData) peersData.append(peerData)
} }
@@ -397,22 +394,22 @@ class TunnelViewModel {
func appendEmptyPeer() { func appendEmptyPeer() {
let peer = PeerData(index: peersData.count) let peer = PeerData(index: peersData.count)
peersData.append(peer) peersData.append(peer)
for p in peersData { for peer in peersData {
p.numberOfPeers = peersData.count peer.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState() peer.updateExcludePrivateIPsFieldState()
} }
} }
func deletePeer(peer: PeerData) { func deletePeer(peer: PeerData) {
let removedPeer = peersData.remove(at: peer.index) let removedPeer = peersData.remove(at: peer.index)
assert(removedPeer.index == peer.index) assert(removedPeer.index == peer.index)
for p in peersData[peer.index ..< peersData.count] { for peer in peersData[peer.index ..< peersData.count] {
assert(p.index > 0) assert(peer.index > 0)
p.index = p.index - 1 peer.index -= 1
} }
for p in peersData { for peer in peersData {
p.numberOfPeers = peersData.count peer.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState() peer.updateExcludePrivateIPsFieldState()
} }
} }
@@ -421,14 +418,14 @@ class TunnelViewModel {
let interfaceSaveResult = interfaceData.save() let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() } let peerSaveResults = peersData.map { $0.save() }
// Collate the results // Collate the results
switch (interfaceSaveResult) { switch interfaceSaveResult {
case .error(let errorMessage): case .error(let errorMessage):
return .error(errorMessage) return .error(errorMessage)
case .saved(let interfaceConfiguration): case .saved(let interfaceConfiguration):
var peerConfigurations: [PeerConfiguration] = [] var peerConfigurations = [PeerConfiguration]()
peerConfigurations.reserveCapacity(peerSaveResults.count) peerConfigurations.reserveCapacity(peerSaveResults.count)
for peerSaveResult in peerSaveResults { for peerSaveResult in peerSaveResults {
switch (peerSaveResult) { switch peerSaveResult {
case .error(let errorMessage): case .error(let errorMessage):
return .error(errorMessage) return .error(errorMessage)
case .saved(let peerConfiguration): case .saved(let peerConfiguration):
@@ -438,7 +435,7 @@ class TunnelViewModel {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) 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") return .error("Two or more peers cannot have the same public key")
} }
@@ -452,7 +449,7 @@ class TunnelViewModel {
extension TunnelViewModel { extension TunnelViewModel {
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String { static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch (activateOnDemandOption) { switch activateOnDemandOption {
case .none: case .none:
return "Off" return "Off"
case .useOnDemandOverWiFiOrCellular: case .useOnDemandOverWiFiOrCellular:
@@ -466,7 +463,7 @@ extension TunnelViewModel {
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String { static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting { if let activateOnDemandSetting = activateOnDemandSetting {
if (activateOnDemandSetting.isActivateOnDemandEnabled) { if activateOnDemandSetting.isActivateOnDemandEnabled {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption) return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else { } else {
return TunnelViewModel.activateOnDemandOptionText(for: .none) return TunnelViewModel.activateOnDemandOptionText(for: .none)
+6 -9
View File
@@ -12,9 +12,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
let window = UIWindow(frame: UIScreen.main.bounds) let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white window.backgroundColor = .white
self.window = window self.window = window
let mainVC = MainViewController() let mainVC = MainViewController()
@@ -27,8 +28,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
mainVC?.tunnelsListVC?.importFromFile(url: url) mainVC?.tunnelsListVC?.importFromFile(url: url) {
_ = FileManager.deleteFile(at: url) _ = FileManager.deleteFile(at: url)
}
return true return true
} }
@@ -52,11 +54,7 @@ extension AppDelegate {
viewControllerWithRestorationIdentifierPath identifierComponents: [String], viewControllerWithRestorationIdentifierPath identifierComponents: [String],
coder: NSCoder) -> UIViewController? { coder: NSCoder) -> UIViewController? {
guard let vcIdentifier = identifierComponents.last else { return nil } guard let vcIdentifier = identifierComponents.last else { return nil }
if (vcIdentifier == "MainVC") { if vcIdentifier.hasPrefix("TunnelDetailVC:") {
return MainViewController()
} else if (vcIdentifier == "TunnelsListVC") {
return TunnelsListTableViewController()
} else if (vcIdentifier.hasPrefix("TunnelDetailVC:")) {
let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count)) let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
if let tunnelsManager = mainVC?.tunnelsManager { if let tunnelsManager = mainVC?.tunnelsManager {
if let tunnel = tunnelsManager.tunnel(named: tunnelName) { if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
@@ -66,7 +64,6 @@ extension AppDelegate {
// Show it when tunnelsManager is available // Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false) mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
} }
} }
return nil 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 import os.log
class ErrorPresenter { class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
onDismissal: (() -> Void)? = nil, onPresented: (() -> 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 } 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?() onDismissal?()
} }
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
@@ -17,15 +21,4 @@ class ErrorPresenter {
sourceVC.present(alert, animated: true, completion: onPresented) 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
}
}
@@ -7,6 +7,14 @@ import UIKit
class TunnelDetailTableViewController: UITableViewController { class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
let interfaceFields: [TunnelViewModel.InterfaceField] = [ let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses, .name, .publicKey, .addresses,
.listenPort, .mtu, .dns .listenPort, .mtu, .dns
@@ -20,12 +28,14 @@ class TunnelDetailTableViewController: UITableViewController {
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
tunnelsManager = tm self.tunnelsManager = tunnelsManager
tunnel = t self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration()) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
super.init(style: .grouped) super.init(style: .grouped)
loadSections()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@@ -37,17 +47,27 @@ class TunnelDetailTableViewController: UITableViewController {
self.title = tunnelViewModel.interfaceData[.name] self.title = tunnelViewModel.interfaceData[.name]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
self.tableView.rowHeight = 44 self.tableView.estimatedRowHeight = 44
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.allowsSelection = false self.tableView.allowsSelection = false
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id) self.tableView.register(StatusCell.self)
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id) self.tableView.register(KeyValueCell.self)
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id) self.tableView.register(ButtonCell.self)
self.tableView.register(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.id) self.tableView.register(ActivateOnDemandCell.self)
// State restoration // State restoration
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" self.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() { @objc func editTapped() {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
editVC.delegate = self editVC.delegate = self
@@ -58,7 +78,7 @@ class TunnelDetailTableViewController: UITableViewController {
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) { onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed() onConfirmed()
} }
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
@@ -79,6 +99,7 @@ class TunnelDetailTableViewController: UITableViewController {
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate { extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) { func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
loadSections()
self.title = tunnel.name self.title = tunnel.name
self.tableView.reloadData() self.tableView.reloadData()
} }
@@ -91,136 +112,117 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController { extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
return 4 + tunnelViewModel.peersData.count return sections.count
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData switch sections[section] {
let numberOfPeerSections = tunnelViewModel.peersData.count case .status:
if (section == 0) {
// Status
return 1 return 1
} else if (section == 1) { case .interface:
// Interface return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count case .peer(let peerData):
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerData = tunnelViewModel.peersData[section - 2]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else if (section < (3 + numberOfPeerSections)) { case .onDemand:
// Activate on demand
return 1 return 1
} else { case .delete:
assert(section == (3 + numberOfPeerSections))
// Delete tunnel
return 1 return 1
} }
} }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfPeerSections = tunnelViewModel.peersData.count switch sections[section] {
case .status:
if (section == 0) {
// Status
return "Status" return "Status"
} else if (section == 1) { case .interface:
// Interface
return "Interface" return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) { case .peer:
// Peer
return "Peer" return "Peer"
} else if (section < (3 + numberOfPeerSections)) { case .onDemand:
// On-Demand Activation
return "On-Demand Activation" return "On-Demand Activation"
} else { case .delete:
// Delete tunnel
return nil return nil
} }
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let interfaceData = tunnelViewModel.interfaceData switch sections[indexPath.section] {
let numberOfPeerSections = tunnelViewModel.peersData.count case .status:
return statusCell(for: tableView, at: indexPath)
let section = indexPath.section case .interface:
let row = indexPath.row return interfaceCell(for: tableView, at: indexPath)
case .peer(let peer):
if (section == 0) { return peerCell(for: tableView, at: indexPath, with: peer)
// Status case .onDemand:
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewStatusCell.id, for: indexPath) as! TunnelDetailTableViewStatusCell return onDemandCell(for: tableView, at: indexPath)
cell.tunnel = self.tunnel case .delete:
cell.onSwitchToggled = { [weak self] isOn in return deleteConfigurationCell(for: tableView, at: indexPath)
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
} }
} }
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: StatusCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
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: ActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
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
}
} }
class TunnelDetailTableViewStatusCell: UITableViewCell { private class StatusCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewStatusCell"
var tunnel: TunnelContainer? { var tunnel: TunnelContainer? {
didSet(value) { didSet(value) {
update(from: tunnel?.status) update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status) self?.update(from: tunnel.status)
} }
} }
@@ -237,14 +239,14 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
statusSwitch = UISwitch() statusSwitch = UISwitch()
super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.id) super.init(style: .default, reuseIdentifier: KeyValueCell.reuseIdentifier)
accessoryView = statusSwitch accessoryView = statusSwitch
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged) statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
} }
@objc func switchToggled() { @objc func switchToggled() {
if (isOnSwitchToggledHandlerEnabled) { if isOnSwitchToggledHandlerEnabled {
onSwitchToggled?(statusSwitch.isOn) onSwitchToggled?(statusSwitch.isOn)
} }
} }
@@ -255,7 +257,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
return return
} }
let text: String let text: String
switch (status) { switch status {
case .inactive: case .inactive:
text = "Inactive" text = "Inactive"
case .activating: case .activating:
@@ -297,8 +299,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
} }
} }
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell { private class KeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell"
var key: String { var key: String {
get { return keyLabel.text ?? "" } get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value } set(value) { keyLabel.text = value }
@@ -315,9 +316,17 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
let keyLabel: UILabel let keyLabel: UILabel
let valueLabel: ScrollableLabel let valueLabel: ScrollableLabel
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel() keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
valueLabel = ScrollableLabel() valueLabel = ScrollableLabel()
valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
valueLabel.label.adjustsFontForContentSizeCategory = true
keyLabel.textColor = UIColor.black keyLabel.textColor = UIColor.black
valueLabel.textColor = UIColor.gray valueLabel.textColor = UIColor.gray
@@ -329,22 +338,55 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
keyLabel.textAlignment = .left keyLabel.textAlignment = .left
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]) ])
contentView.addSubview(valueLabel) contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabel.bottomAnchor, multiplier: 0.5)
valueLabel.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 8) ])
])
// Key label should never appear truncated // Key label should never appear truncated
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
// Key label should hug it's content; value label should not. // Key label should hug it's content; value label should not.
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
configureForContentSize()
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabel.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),
valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
self.contentSizeBasedConstraints = constraints
}
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@@ -355,11 +397,11 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
super.prepareForReuse() super.prepareForReuse()
key = "" key = ""
value = "" value = ""
configureForContentSize()
} }
} }
class TunnelDetailTableViewButtonCell: UITableViewCell { private class ButtonCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewButtonCell"
var buttonText: String { var buttonText: String {
get { return button.title(for: .normal) ?? "" } get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) } set(value) { button.setTitle(value, for: .normal) }
@@ -375,14 +417,17 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system) button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
buttonStandardTintColor = button.tintColor buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button) contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
]) ])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
} }
@@ -402,13 +447,11 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
} }
} }
class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell { private class ActivateOnDemandCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewActivateOnDemandCell"
var tunnel: TunnelContainer? { var tunnel: TunnelContainer? {
didSet(value) { didSet(value) {
update(from: tunnel?.activateOnDemandSetting()) update(from: tunnel?.activateOnDemandSetting())
onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] (tunnel, _) in onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
self?.update(from: tunnel.activateOnDemandSetting()) self?.update(from: tunnel.activateOnDemandSetting())
} }
} }
@@ -419,6 +462,10 @@ class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier) super.init(style: .value1, reuseIdentifier: reuseIdentifier)
textLabel?.text = "Activate on demand" textLabel?.text = "Activate on demand"
textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
textLabel?.adjustsFontForContentSizeCategory = true
detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
detailTextLabel?.adjustsFontForContentSizeCategory = true
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@@ -431,6 +478,7 @@ class TunnelDetailTableViewActivateOnDemandCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
textLabel?.text = "Activate on demand"
detailTextLabel?.text = "" 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,20 @@ class MainViewController: UISplitViewController {
init() { init() {
let detailVC = UIViewController() let detailVC = UIViewController()
detailVC.view.backgroundColor = UIColor.white detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC) let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController() let masterVC = TunnelsListTableViewController()
let masterNC = UINavigationController(rootViewController: masterVC) let masterNC = UINavigationController(rootViewController: masterVC)
self.tunnelsListVC = masterVC tunnelsListVC = masterVC
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.viewControllers = [ masterNC, detailNC ] viewControllers = [ masterNC, detailNC ]
// State restoration // State restoration
self.restorationIdentifier = "MainVC" restorationIdentifier = "MainVC"
masterNC.restorationIdentifier = "MasterNC" masterNC.restorationIdentifier = "MasterNC"
detailNC.restorationIdentifier = "DetailNC" detailNC.restorationIdentifier = "DetailNC"
} }
@@ -35,35 +35,48 @@ class MainViewController: UISplitViewController {
} }
override func viewDidLoad() { override func viewDidLoad() {
self.delegate = self delegate = self
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app // On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
self.preferredDisplayMode = .allVisible preferredDisplayMode = .allVisible
// Create the tunnels manager, and when it's ready, inform tunnelsListVC // Create the tunnels manager, and when it's ready, inform tunnelsListVC
TunnelsManager.create { [weak self] result in TunnelsManager.create { [weak self] result in
guard let self = self else { return }
if let error = result.error { if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
return return
} }
let tunnelsManager: TunnelsManager = result.value! let tunnelsManager: TunnelsManager = result.value!
guard let s = self else { return }
s.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
s.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager) self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = s tunnelsManager.activationDelegate = self
s.onTunnelsManagerReady?(tunnelsManager) self.onTunnelsManagerReady?(tunnelsManager)
s.onTunnelsManagerReady = nil self.onTunnelsManagerReady = nil
} }
} }
} }
extension MainViewController: TunnelsManagerActivationDelegate { extension MainViewController: TunnelsManagerActivationDelegate {
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError) { func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
ErrorPresenter.showErrorAlert(error: error, from: self) 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 { extension MainViewController {
@@ -74,13 +87,13 @@ extension MainViewController {
} }
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) { 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) { if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC" tunnelDetailNC.restorationIdentifier = "DetailNC"
if let self = self { if let self = self {
if (animated) { if animated {
self.showDetailViewController(tunnelDetailNC, sender: self) self.showDetailViewController(tunnelDetailNC, sender: self)
} else { } else {
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
@@ -2,7 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018 WireGuard LLC. All Rights Reserved.
import AVFoundation import AVFoundation
import CoreData
import UIKit import UIKit
protocol QRScanViewControllerDelegate: class { protocol QRScanViewControllerDelegate: class {
@@ -18,13 +17,13 @@ class QRScanViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.title = "Scan QR code" title = "Scan QR code"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
let tipLabel = UILabel() let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`" tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.adjustsFontSizeToFitWidth = true tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = UIColor.lightGray tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center tipLabel.textAlignment = .center
view.addSubview(tipLabel) view.addSubview(tipLabel)
@@ -32,8 +31,8 @@ class QRScanViewController: UIViewController {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), 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), guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), 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) { 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) let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: nil) 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) self?.dismiss(animated: true, completion: nil)
})) })
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default, handler: { [weak self] _ in alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default) { [weak self] _ in
let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
if (title.isEmpty) { return }
tunnelConfiguration.interface.name = title tunnelConfiguration.interface.name = title
if let s = self { if let self = self {
s.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: s) { self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
s.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
} }
} }
})) })
present(alert, animated: true) present(alert, animated: true)
} }
func scanDidEncounterError(title: String, message: String) { func scanDidEncounterError(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 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) self?.dismiss(animated: true, completion: nil)
})) })
present(alertController, animated: true) present(alertController, animated: true)
captureSession = nil 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,484 @@
// 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 }
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,307 @@
// 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) {
// Remove selection when getting back to the list view on iPhone
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 "unzip.h"
#include "zip.h" #include "zip.h"
#include "wireguard-go-version.h" #include "wireguard-go-version.h"
#include "ringlogger.h"
+2 -1
View File
@@ -2,5 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error { 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. // Copyright © 2018 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> { enum WireGuardResult<T> {
case success(T) case success(_ value: T)
case failure(WireGuardAppError) case failure(_ error: WireGuardAppError)
var value: T? { var value: T? {
switch (self) { switch self {
case .success(let v): return v case .success(let value): return value
case .failure(_): return nil case .failure: return nil
} }
} }
var error: WireGuardAppError? { var error: WireGuardAppError? {
switch (self) { switch self {
case .success(_): return nil case .success: return nil
case .failure(let e): return e case .failure(let error): return error
} }
} }
var isSuccess: Bool { var isSuccess: Bool {
switch (self) { switch self {
case .success(_): return true case .success: return true
case .failure(_): return false case .failure: return false
} }
} }
} }
+19 -27
View File
@@ -8,8 +8,8 @@ enum ZipArchiveError: WireGuardAppError {
case cantOpenOutputZipFileForWriting case cantOpenOutputZipFileForWriting
case badArchive case badArchive
func alertText() -> (String, String) { var alertText: AlertText {
switch (self) { switch self {
case .cantOpenInputZipFile: case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.") return ("Unable to read zip archive", "The zip archive could not be read.")
case .cantOpenOutputZipFileForWriting: case .cantOpenOutputZipFileForWriting:
@@ -39,9 +39,9 @@ class ZipArchive {
zipClose(zipFile, nil) 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 { guard let zipFile = unzOpen64(url.path) else {
throw ZipArchiveError.cantOpenInputZipFile throw ZipArchiveError.cantOpenInputZipFile
@@ -49,17 +49,13 @@ class ZipArchive {
defer { defer {
unzClose(zipFile) unzClose(zipFile)
} }
guard (unzGoToFirstFile(zipFile) == UNZ_OK) else { guard unzGoToFirstFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
throw ZipArchiveError.badArchive
}
var resultOfGoToNextFile: Int32 var resultOfGoToNextFile: Int32
repeat { repeat {
guard (unzOpenCurrentFile(zipFile) == UNZ_OK) else { guard unzOpenCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
throw ZipArchiveError.badArchive
}
let bufferSize = 16384 // 16 kb let bufferSize = 16384 // 16 KiB
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize) var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize) var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
@@ -68,37 +64,33 @@ class ZipArchive {
dataBuffer.deallocate() dataBuffer.deallocate()
} }
guard (unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK) else { guard unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK else { throw ZipArchiveError.badArchive }
throw ZipArchiveError.badArchive
}
if let fileURL = URL(string: String(cString: fileNameBuffer)), let lastChar = String(cString: fileNameBuffer).suffix(1)
!fileURL.hasDirectoryPath, let isDirectory = (lastChar == "/" || lastChar == "\\")
requiredFileExtensions.contains(fileURL.pathExtension) { let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) {
var unzippedData = Data() var unzippedData = Data()
var bytesRead: Int32 = 0 var bytesRead: Int32 = 0
repeat { repeat {
bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize)) bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize))
if (bytesRead > 0) { if bytesRead > 0 {
let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) { let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) {
(buf: UnsafeMutablePointer<UInt8>) -> Data in return Data(bytes: $0, count: Int(bytesRead))
return Data(bytes: buf, count: Int(bytesRead))
} }
unzippedData.append(dataRead) unzippedData.append(dataRead)
} }
} while (bytesRead > 0) } while bytesRead > 0
results.append((fileName: fileURL.lastPathComponent, contents: unzippedData)) results.append((fileBaseName: fileURL.deletingPathExtension().lastPathComponent, contents: unzippedData))
} }
guard (unzCloseCurrentFile(zipFile) == UNZ_OK) else { guard unzCloseCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
throw ZipArchiveError.badArchive
}
resultOfGoToNextFile = unzGoToNextFile(zipFile) 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 return results
} else { } else {
throw ZipArchiveError.badArchive throw ZipArchiveError.badArchive
@@ -6,19 +6,15 @@ import UIKit
enum ZipExporterError: WireGuardAppError { enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport case noTunnelsToExport
func alertText() -> (String, String) { var alertText: AlertText {
switch (self) { return ("Nothing to export", "There are no tunnels to export")
case .noTunnelsToExport:
return ("Nothing to export", "There are no tunnels to export")
}
} }
} }
class ZipExporter { class ZipExporter {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
completion: @escaping (WireGuardAppError?) -> Void) {
guard (!tunnelConfigurations.isEmpty) else { guard !tunnelConfigurations.isEmpty else {
completion(ZipExporterError.noTunnelsToExport) completion(ZipExporterError.noTunnelsToExport)
return return
} }
@@ -28,14 +24,14 @@ class ZipExporter {
for tunnelConfiguration in tunnelConfigurations { for tunnelConfiguration in tunnelConfigurations {
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) { if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
let name = tunnelConfiguration.interface.name let name = tunnelConfiguration.interface.name
if (name.isEmpty || name == lastTunnelName) { continue } if name.isEmpty || name == lastTunnelName { continue }
inputsToArchiver.append((fileName: "\(name).conf", contents: contents)) inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
lastTunnelName = name lastTunnelName = name
} }
} }
do { do {
try ZipArchive.archive(inputs: inputsToArchiver, to: url) try ZipArchive.archive(inputs: inputsToArchiver, to: url)
} catch (let error as WireGuardAppError) { } catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(error) } DispatchQueue.main.async { completion(error) }
return return
} catch { } catch {
@@ -6,53 +6,50 @@ import UIKit
enum ZipImporterError: WireGuardAppError { enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive case noTunnelsInZipArchive
func alertText() -> (String, String) { var alertText: AlertText {
switch (self) { return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
case .noTunnelsInZipArchive:
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
}
} }
} }
class ZipImporter { class ZipImporter {
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) { static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
var unarchivedFiles: [(fileName: String, contents: Data)] var unarchivedFiles: [(fileBaseName: String, contents: Data)]
do { do {
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"]) unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
for (index, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
for (i, unarchivedFile) in unarchivedFiles.enumerated().reversed() { let fileBaseName = unarchivedFile.fileBaseName
let fileBaseName = URL(string: unarchivedFile.fileName)?.deletingPathExtension().lastPathComponent let trimmedName = fileBaseName.trimmingCharacters(in: .whitespacesAndNewlines)
if let trimmedName = fileBaseName?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmedName.isEmpty { if !trimmedName.isEmpty {
unarchivedFiles[i].fileName = trimmedName unarchivedFiles[index].fileBaseName = trimmedName
} else { } else {
unarchivedFiles.remove(at: i) unarchivedFiles.remove(at: index)
} }
} }
if (unarchivedFiles.isEmpty) { if unarchivedFiles.isEmpty {
throw ZipImporterError.noTunnelsInZipArchive throw ZipImporterError.noTunnelsInZipArchive
} }
} catch (let error as WireGuardAppError) { } catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(.failure(error)) } DispatchQueue.main.async { completion(.failure(error)) }
return return
} catch { } catch {
fatalError() fatalError()
} }
unarchivedFiles.sort { $0.fileName < $1.fileName } unarchivedFiles.sort { $0.fileBaseName < $1.fileBaseName }
var configs = Array<TunnelConfiguration?>(repeating: nil, count: unarchivedFiles.count) var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
for (i, file) in unarchivedFiles.enumerated() { for (index, file) in unarchivedFiles.enumerated() {
if (i > 0 && file == unarchivedFiles[i - 1]) { if index > 0 && file == unarchivedFiles[index - 1] {
continue continue
} }
guard let fileContents = String(data: file.contents, encoding: .utf8) else { guard let fileContents = String(data: file.contents, encoding: .utf8) else {
continue continue
} }
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else { guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileBaseName) else {
continue continue
} }
configs[i] = tunnelConfig configs[index] = tunnelConfig
} }
DispatchQueue.main.async { completion(.success(configs)) } DispatchQueue.main.async { completion(.success(configs)) }
} }
@@ -13,7 +13,7 @@ class DNSResolver {
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool { static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
for endpoint in endpoints { for endpoint in endpoints {
guard let endpoint = endpoint else { continue } guard let endpoint = endpoint else { continue }
if (!endpoint.hasHostAsIPAddress()) { if !endpoint.hasHostAsIPAddress() {
return false return false
} }
} }
@@ -23,18 +23,18 @@ class DNSResolver {
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] { static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
let dispatchGroup: DispatchGroup = DispatchGroup() let dispatchGroup: DispatchGroup = DispatchGroup()
if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) { if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints return endpoints
} }
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count) var resolvedEndpoints: [Endpoint?] = Array(repeating: nil, count: endpoints.count)
for (i, endpoint) in endpoints.enumerated() { for (index, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint else { continue } guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) { if endpoint.hasHostAsIPAddress() {
resolvedEndpoints[i] = endpoint resolvedEndpoints[index] = endpoint
} else { } else {
let workItem = DispatchWorkItem { let workItem = DispatchWorkItem {
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint) resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
} }
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem) DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
} }
@@ -42,20 +42,20 @@ class DNSResolver {
dispatchGroup.wait() // TODO: Timeout? dispatchGroup.wait() // TODO: Timeout?
var hostnamesWithDnsResolutionFailure: [String] = [] var hostnamesWithDnsResolutionFailure = [String]()
assert(endpoints.count == resolvedEndpoints.count) assert(endpoints.count == resolvedEndpoints.count)
for tuple in zip(endpoints, resolvedEndpoints) { for tuple in zip(endpoints, resolvedEndpoints) {
let endpoint = tuple.0 let endpoint = tuple.0
let resolvedEndpoint = tuple.1 let resolvedEndpoint = tuple.1
if let endpoint = endpoint { if let endpoint = endpoint {
if (resolvedEndpoint == nil) { if resolvedEndpoint == nil {
// DNS resolution failed // DNS resolution failed
guard let hostname = endpoint.hostname() else { fatalError() } guard let hostname = endpoint.hostname() else { fatalError() }
hostnamesWithDnsResolutionFailure.append(hostname) hostnamesWithDnsResolutionFailure.append(hostname)
} }
} }
} }
if (!hostnamesWithDnsResolutionFailure.isEmpty) { if !hostnamesWithDnsResolutionFailure.isEmpty {
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure) throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
} }
return resolvedEndpoints return resolvedEndpoints
@@ -65,48 +65,37 @@ class DNSResolver {
extension DNSResolver { extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com> // Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase // in parse_endpoint() in src/tools/config.c in the WireGuard codebase
//swiftlint:disable:next cyclomatic_complexity
private static func resolveSync(endpoint: Endpoint) -> Endpoint? { private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
var hints = addrinfo( switch endpoint.host {
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) {
case .name(let name, _): case .name(let name, _):
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
// The endpoint is a hostname and needs DNS resolution // The endpoint is a hostname and needs DNS resolution
let returnValue = getaddrinfo( if addressInfo(for: name, port: endpoint.port, resultPointer: &resultPointer) == 0 {
name.cString(using: .utf8), // Hostname
"\(endpoint.port)".cString(using: .utf8), // Port
&hints,
&resultPointer)
if (returnValue == 0) {
// getaddrinfo succeeded // getaddrinfo succeeded
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN)) let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN))
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN)) let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN))
var ipv4AddressString: String? var ipv4AddressString: String?
var ipv6AddressString: String? var ipv6AddressString: String?
while (resultPointer != nil) { while resultPointer != nil {
let result = resultPointer!.pointee let result = resultPointer!.pointee
resultPointer = result.ai_next 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 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) ipv4AddressString = String(cString: ipv4Buffer)
// If we found an IPv4 address, we can stop // If we found an IPv4 address, we can stop
break break
} }
} else if (result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size) { } else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
if (ipv6AddressString != nil) { if ipv6AddressString != nil {
// If we already have an IPv6 address, we can skip this one // If we already have an IPv6 address, we can skip this one
continue continue
} }
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee 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) ipv6AddressString = String(cString: ipv6Buffer)
} }
} }
@@ -115,9 +104,9 @@ extension DNSResolver {
ipv6Buffer.deallocate() ipv6Buffer.deallocate()
// We prefer an IPv4 address over an IPv6 address // We prefer an IPv4 address over an IPv6 address
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) { 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) { } 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 { } else {
return nil return nil
} }
@@ -130,4 +119,22 @@ extension DNSResolver {
return endpoint 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 import NetworkExtension
class ErrorNotifier { 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: case .savedProtocolConfigurationIsInvalid:
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration") 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") return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
case .couldNotStartWireGuard: case .couldNotStartWireGuard:
return ("Activation failure", "WireGuard backend could not be started") return ("Activation failure", "WireGuard backend could not be started")
@@ -17,9 +27,24 @@ class ErrorNotifier {
} }
} }
static func notify(_ error: PacketTunnelProviderError, from tunnelProvider: NEPacketTunnelProvider) { func notify(_ error: PacketTunnelProviderError) {
guard let (title, message) = ErrorNotifier.errorMessage(for: error) else { return } guard let (title, message) = errorMessage(for: error) else { return }
// displayMessage() is deprecated, but there's no better alternative to show the error to the user if let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path {
tunnelProvider.displayMessage("\(title): \(message)", completionHandler: { (_) in }) // 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 // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
import Foundation import Foundation
import Network
import NetworkExtension
import os.log import os.log
enum PacketTunnelProviderError: Error { enum PacketTunnelProviderError: Error {
@@ -12,8 +13,6 @@ enum PacketTunnelProviderError: Error {
case coultNotSetNetworkSettings case coultNotSetNetworkSettings
} }
private var logFileHandle: FileHandle?
/// A packet tunnel provider object. /// A packet tunnel provider object.
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
@@ -21,43 +20,44 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var wgHandle: Int32? private var wgHandle: Int32?
private var networkMonitor: NWPathMonitor?
// MARK: NEPacketTunnelProvider // MARK: NEPacketTunnelProvider
/// Begin the process of establishing the tunnel. deinit {
override func startTunnel(options: [String: NSObject]?, networkMonitor?.cancel()
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { }
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 { let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self) errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return return
} }
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler) startTunnel(with: tunnelConfiguration, errorNotifier: errorNotifier, completionHandler: startTunnelCompletionHandler)
} }
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { //swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, errorNotifier: ErrorNotifier, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
// Configure logging
configureLogger() configureLogger()
wg_log(.info, message: "WireGuard for iOS version \(appVersion())") wg_log(.info, message: "Starting tunnel '\(tunnelConfiguration.interface.name)'")
wg_log(.info, message: "WireGuard Go backend version \(goBackendVersion())")
wg_log(.info, message: "Tunnel interface name: \(tunnelConfiguration.interface.name)")
wg_log(.info, staticMessage: "Starting tunnel")
// Resolve endpoint domains
let endpoints = tunnelConfiguration.peers.map { $0.endpoint } let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
var resolvedEndpoints: [Endpoint?] = [] var resolvedEndpoints = [Endpoint?]()
do { do {
resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints) resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints)
} catch DNSResolverError.dnsResolutionFailed(let hostnames) { } catch DNSResolverError.dnsResolutionFailed(let hostnames) {
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure") wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))") 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)) startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
return return
} catch { } catch {
@@ -68,25 +68,42 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Setup packetTunnelSettingsGenerator // Setup packetTunnelSettingsGenerator
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
resolvedEndpoints: resolvedEndpoints)
// Bring up wireguard-go backend // Bring up wireguard-go backend
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast
if fd < 0 { if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor") 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) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return return
} }
let wireguardSettings = packetTunnelSettingsGenerator.generateWireGuardSettings() let wireguardSettings = packetTunnelSettingsGenerator.uapiConfiguration()
let handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fd: fd)
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 { if handle < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard") wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self) errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return return
} }
@@ -96,11 +113,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Apply network settings // Apply network settings
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings() let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
setTunnelNetworkSettings(networkSettings) { (error) in setTunnelNetworkSettings(networkSettings) { error in
if let error = error { if let error = error {
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.") wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)") wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
ErrorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings, from: self) errorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings) startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else { } else {
startTunnelCompletionHandler(nil /* No errors */) startTunnelCompletionHandler(nil /* No errors */)
@@ -110,35 +127,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
/// Begin the process of stopping the tunnel. /// Begin the process of stopping the tunnel.
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
networkMonitor?.cancel()
networkMonitor = nil
ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel") wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle { if let handle = wgHandle {
wgTurnOff(handle) wgTurnOff(handle)
} }
if let fileHandle = logFileHandle {
fileHandle.closeFile()
}
completionHandler() completionHandler()
} }
private func configureLogger() { private func configureLogger() {
Logger.configureGlobal(withFilePath: FileManager.networkExtensionLogFileURL?.path)
// Setup writing the log to a file wgSetLogger { level, msgC in
if let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL { guard let msgC = msgC else { return }
let fileManager = FileManager.default
let filePath = networkExtensionLogFileURL.path
fileManager.createFile(atPath: filePath, contents: nil) // Create the file if it doesn't already exist
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
logFileHandle = fileHandle
} else {
os_log("Can't open log file for writing. Log is not saved to file.", log: OSLog.default, type: .error)
logFileHandle = nil
}
} else {
os_log("Can't obtain log file URL. Log is not saved to file.", log: OSLog.default, type: .error)
}
// Setup WireGuard logger
wgSetLogger { (level, msgCStr) in
let logType: OSLogType let logType: OSLogType
switch level { switch level {
case 0: case 0:
@@ -150,67 +154,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
default: default:
logType = .default logType = .default
} }
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : "" wg_log(logType, message: String(cString: msgC))
wg_log(logType, message: msg)
} }
} }
private func connect(interfaceName: String, settings: String, fd: Int32) -> Int32 { // swiftlint:disable:this cyclomatic_complexity private func connect(interfaceName: String, settings: String, fileDescriptor: Int32) -> Int32 {
return withStringsAsGoStrings(interfaceName, settings) { (nameGoStr, settingsGoStr) -> Int32 in return withStringsAsGoStrings(interfaceName, settings) { return wgTurnOn($0.0, $0.1, fileDescriptor) }
return wgTurnOn(nameGoStr, settingsGoStr, fd)
}
}
func appVersion() -> String {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
return appVersion
}
func goBackendVersion() -> String {
return WIREGUARD_GO_VERSION
} }
} }
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R { // swiftlint:disable:next large_tuple identifier_name
return str1.withCString { (s1cStr) -> R in 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 {
let gstr1 = gostring_t(p: s1cStr, n: str1.utf8.count) // swiftlint:disable:next large_tuple identifier_name
return str2.withCString { (s2cStr) -> R in 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 {
let gstr2 = gostring_t(p: s2cStr, n: str2.utf8.count) 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 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()
} }
return helper(s1, s2, s3, s4, call)
} }
@@ -15,52 +15,61 @@ class PacketTunnelSettingsGenerator {
self.resolvedEndpoints = resolvedEndpoints 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 = "" var wgSettings = ""
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString() let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
wgSettings.append("private_key=\(privateKey)\n") wgSettings.append("private_key=\(privateKey)\n")
if let listenPort = tunnelConfiguration.interface.listenPort { if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n") wgSettings.append("listen_port=\(listenPort)\n")
} }
if (tunnelConfiguration.peers.count > 0) { if tunnelConfiguration.peers.count > 0 {
wgSettings.append("replace_peers=true\n") wgSettings.append("replace_peers=true\n")
} }
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count) 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") wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let preSharedKey = peer.preSharedKey { if let preSharedKey = peer.preSharedKey {
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n") wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
} }
if let endpoint = resolvedEndpoints[i] { if let endpoint = resolvedEndpoints[index] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") } if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n") wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
} }
let persistentKeepAlive = peer.persistentKeepAlive ?? 0 let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n") wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if (!peer.allowedIPs.isEmpty) { if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n") wgSettings.append("replace_allowed_ips=true\n")
for ip in peer.allowedIPs { peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation())\n") }
wgSettings.append("allowed_ip=\(ip.stringRepresentation())\n")
}
} }
} }
return wgSettings return wgSettings
} }
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings { func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
// Remote address
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for /* 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 * 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 * which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not * make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet. * a valid IP address that will actually route over the Internet.
*/ */
var remoteAddress: String = "0.0.0.0" var remoteAddress = "0.0.0.0"
let endpointsCompact = resolvedEndpoints.compactMap({ $0 }) let endpointsCompact = resolvedEndpoints.compactMap { $0 }
if endpointsCompact.count == 1 { if endpointsCompact.count == 1 {
switch (endpointsCompact.first!.host) { switch endpointsCompact.first!.host {
case .ipv4(let address): case .ipv4(let address):
remoteAddress = "\(address)" remoteAddress = "\(address)"
case .ipv6(let address): case .ipv6(let address):
@@ -72,130 +81,98 @@ class PacketTunnelSettingsGenerator {
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress) let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
// DNS
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() } let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() }
let dnsSettings = NEDNSSettings(servers: dnsServerStrings) 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 networkSettings.dnsSettings = dnsSettings
// MTU
let mtu = tunnelConfiguration.interface.mtu ?? 0 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 // 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
networkSettings.tunnelOverheadBytes = 80 networkSettings.tunnelOverheadBytes = 80
} else { } else {
networkSettings.mtu = NSNumber(value: mtu) networkSettings.mtu = NSNumber(value: mtu)
} }
// Addresses from interface addresses let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let (ipv4ExcludedRoutes, ipv6ExcludedRoutes) = excludedRoutes()
var ipv4Addresses: [String] = [] let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
var ipv4SubnetMasks: [String] = [] ipv4Settings.includedRoutes = ipv4IncludedRoutes
ipv4Settings.excludedRoutes = ipv4ExcludedRoutes
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)
}
networkSettings.ipv4Settings = ipv4Settings networkSettings.ipv4Settings = ipv4Settings
// Apply IPv6 settings let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have ipv6Settings.excludedRoutes = ipv6ExcludedRoutes
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
* very bad, if various network parameters were actually relying on that subnet being
* intentionally small. TODO: talk about this with upstream iOS devs.
*/
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
}
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
}
networkSettings.ipv6Settings = ipv6Settings networkSettings.ipv6Settings = ipv6Settings
// Done
return networkSettings return networkSettings
} }
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String { private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let n: UInt8 = addressRange.networkPrefixLength let length: UInt8 = addressRange.networkPrefixLength
assert(n <= 32) assert(length <= 32)
var octets: [UInt8] = [0, 0, 0, 0] 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[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16) octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8) octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
octets[3] = UInt8(truncatingIfNeeded: subnetMask) octets[3] = UInt8(truncatingIfNeeded: subnetMask)
return octets.map { String($0) }.joined(separator: ".") 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 { private extension Data {
@@ -1,2 +1,3 @@
#include "../../wireguard-go-bridge/wireguard.h" #include "../../wireguard-go-bridge/wireguard.h"
#include "wireguard-go-version.h" #include "wireguard-go-version.h"
#include "ringlogger.h"
+28 -84
View File
@@ -25,8 +25,6 @@ import (
"os/signal" "os/signal"
"runtime" "runtime"
"strings" "strings"
"syscall"
"time"
"unsafe" "unsafe"
) )
@@ -48,54 +46,12 @@ func (l *CLogger) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
type DeviceState struct { var tunnelHandles map[int32]*Device
device *Device
logger *Logger
endpointsTimer *time.Timer
endpointsSettings string
}
var tunnelHandles map[int32]*DeviceState
func listenForRouteChanges() {
//TODO: replace with NWPathMonitor
data := make([]byte, os.Getpagesize())
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
if err != nil {
return
}
for {
n, err := unix.Read(routeSocket, data)
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINTR {
continue
}
return
}
if n < 4 {
continue
}
for _, deviceState := range tunnelHandles {
if deviceState.endpointsTimer == nil {
deviceState.endpointsTimer = time.AfterFunc(time.Second, func() {
deviceState.endpointsTimer = nil
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(deviceState.endpointsSettings)), bufio.NewWriter(ioutil.Discard))
deviceState.logger.Info.Println("Setting endpoints for re-resolution due to network change")
err := ipcSetOperation(deviceState.device, bufferedSettings)
if err != nil {
deviceState.logger.Error.Println(err)
}
})
}
}
}
}
func init() { func init() {
versionString = C.CString(WireGuardGoVersion) versionString = C.CString(WireGuardGoVersion)
roamingDisabled = true roamingDisabled = true
tunnelHandles = make(map[int32]*DeviceState) tunnelHandles = make(map[int32]*Device)
signals := make(chan os.Signal) signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2) signal.Notify(signals, unix.SIGUSR2)
go func() { go func() {
@@ -111,7 +67,6 @@ func init() {
} }
} }
}() }()
go listenForRouteChanges()
} }
//export wgSetLogger //export wgSetLogger
@@ -119,32 +74,6 @@ func wgSetLogger(loggerFn uintptr) {
loggerFunc = unsafe.Pointer(loggerFn) loggerFunc = unsafe.Pointer(loggerFn)
} }
func extractEndpointFromSettings(settings string) string {
var b strings.Builder
pubkey := ""
endpoint := ""
listenPort := "listen_port=0"
for _, line := range strings.Split(settings, "\n") {
if strings.HasPrefix(line, "listen_port=") {
listenPort = line
} else if strings.HasPrefix(line, "public_key=") {
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
pubkey = line
} else if strings.HasPrefix(line, "endpoint=") {
endpoint = line
} else if line == "remove=true" {
pubkey = ""
endpoint = ""
}
}
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
return listenPort + "\n" + b.String()
}
//export wgTurnOn //export wgTurnOn
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 { func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
interfaceName := string([]byte(ifnameRef)) interfaceName := string([]byte(ifnameRef))
@@ -184,27 +113,42 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
if i == math.MaxInt32 { if i == math.MaxInt32 {
return -1 return -1
} }
tunnelHandles[i] = &DeviceState{ tunnelHandles[i] = device
device: device,
logger: logger,
endpointsSettings: extractEndpointFromSettings(settings),
}
return i return i
} }
//export wgTurnOff //export wgTurnOff
func wgTurnOff(tunnelHandle int32) { func wgTurnOff(tunnelHandle int32) {
deviceState, ok := tunnelHandles[tunnelHandle] device, ok := tunnelHandles[tunnelHandle]
if !ok { if !ok {
return return
} }
delete(tunnelHandles, tunnelHandle) delete(tunnelHandles, tunnelHandle)
t := deviceState.endpointsTimer device.Close()
if t != nil { }
deviceState.endpointsTimer = nil
t.Stop() //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 //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 void wgSetLogger(logger_fn_t logger_fn);
extern int wgTurnOn(gostring_t ifname, gostring_t settings, int32_t tun_fd); extern int wgTurnOn(gostring_t ifname, gostring_t settings, int32_t tun_fd);
extern void wgTurnOff(int handle); extern void wgTurnOff(int handle);
extern int64_t wgSetConfig(int handle, gostring_t settings);
extern uint16_t wgGetListenPort(int handle);
extern char *wgVersion(); extern char *wgVersion();
#endif #endif