Compare commits

...

67 Commits

Author SHA1 Message Date
Jason A. Donenfeld 5f15b664fc Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-08 02:13:44 +01:00
Jason A. Donenfeld 49f287439e PacketTunnelSettingsGenerator: use 127.0.0.1 as dummy address
It turns out that using 0.0.0.0 somehow conflicts with DNS lookups when
CLAT is in use.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-08 01:51:12 +01:00
Jason A. Donenfeld 150cd119c7 Avoid dynamic MTU calculations for now
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:23:39 -05:00
Jason A. Donenfeld e2384e143c Update copyright
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:23:39 -05:00
Jason A. Donenfeld 52c59704de Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-28 19:43:11 +01:00
Jason A. Donenfeld 0b828f9b96 Rework DNS and routes in network extension
The DNS resolver prior had useless comments, awful nesting, converted
bytes into strings and back into bytes, and generally made no sense.
That's been rewritten now.

But more fundumentally, this commit made the DNS resolver actually
accomplish its objective, by passing AI_ALL to it. It turns out, though,
that the Go library isn't actually using GAI in the way we need for
parsing IP addresses, so we actually need to do another round, this time
with hints flag as zero, so that we get the DNS64 address.

Additionally, since we're now binding sockets to interfaces, we can
entirely remove the excludedRoutes logic.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-28 19:38:03 +01:00
Jason A. Donenfeld 51a3e5c0b4 Version bump
A Christmas Special, for TestFlight, and possibly for release if things
go well there.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-26 01:41:22 +01:00
Jason A. Donenfeld c9c343cde2 NetworkExtension: rescope socket instead of tearing down socket
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-26 01:17:55 +01:00
Roopesh Chander c563a24348 minizip: Remove zip encryption code
We can now remove -DNOCRYPT cflag while compiling

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-25 12:48:04 +05:30
Roopesh Chander 808852c547 Tunnel edit: Fix crash
This fixes a crash that happens when you:

1. Scroll to the end of the Edit screen
2. Delete a peer
3. Toggle the Activate On Demand switch

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-24 13:01:21 +05:30
Eric Kuck 035055ef0a SwitchCell nits
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 15:32:29 -06:00
Eric Kuck 508ba44576 Fix typo for simulator builds
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 14:32:39 -06:00
Roopesh Chander 999b761ed0 Remove more comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-23 00:31:44 +05:30
Jason A. Donenfeld 129f94dccd Rely on availability of fd only after setting network settings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 18:29:54 +01:00
Jason A. Donenfeld aaee3c5cbe Bump to latest wireguard-go release
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 17:38:25 +01:00
Jason A. Donenfeld dddbf3b370 Retain aggressive socket reestablishment for now
This can be reverted once we've done more testing.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 15:45:09 +01:00
Roopesh Chander d29f47fc9b Don't set username on NETunnelProviderProtocol
The username corresponds to the Account field in iOS system VPN UI,
but if we don't set it, the field is not shown, so setting it isn't
really required.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 16:13:31 +05:30
Roopesh Chander e6e1795d08 TunnelErrors: Add alert text for PacketTunnelProviderError
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 16:05:43 +05:30
Roopesh Chander fd29cf3402 TunnelStatus: Absorb NEVPNStatus+CustomStringConvertible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 15:46:28 +05:30
Jason A. Donenfeld 56ad5f74e9 Also refresh status
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 07:07:53 +01:00
Jason A. Donenfeld 49bf55021f Reassign tunnelProvider if it changes from outside the app
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 06:56:12 +01:00
Eric Kuck 0bec5b04b0 All models now Equatable
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 22:57:17 -06:00
Jason A. Donenfeld d36e7e27ff Clean up trailing whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 05:30:35 +01:00
Jason A. Donenfeld b0b6866c51 Do not crash if we can't get socket.fileDescriptor
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 05:13:04 +01:00
Eric Kuck 9098cd1161 Removing a tunnel from iOS's settings is now immediately reflected in app
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 21:59:43 -06:00
Jason A. Donenfeld 8365adf435 Localize remaining strings in network extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 03:42:01 +01:00
Eric Kuck 9d9859248e RTL support
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 20:37:22 -06:00
Eric Kuck f7e9f4d631 Strongly recommended now appears as placeholder for DNS when needed
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 19:52:51 -06:00
Jason A. Donenfeld f2000aa1da Combine double log invocations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 02:21:07 +01:00
Jason A. Donenfeld 82de3e3090 Bump go bridge
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:58:02 +01:00
Jason A. Donenfeld 41a4c6362a Attempt to strongly recommend things
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:55:42 +01:00
Jason A. Donenfeld aede9f6e45 Move model helpers to model directory
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:36:42 +01:00
Eric Kuck 1eeed89174 Fixes mock tunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 18:35:01 -06:00
Jason A. Donenfeld c1c5f7a7c7 Do not set copyable back to true on reuse
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:31:59 +01:00
Jason A. Donenfeld 4ed646973e Move name from interface to tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 00:28:18 +01:00
Jason A. Donenfeld 9295895e3a Fix paren typo
"I am very anti-paren." --Eric

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 23:45:20 +01:00
Jason A. Donenfeld 7b9d4cb9e3 Nuke trailing spaces
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 23:34:56 +01:00
Eric Kuck 1fecd8eb6c providerConfiguration is now a WgQuickConfig
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:32:08 -06:00
Jason A. Donenfeld accf60b82f Do not require NetworkExtension to know its own name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 22:05:47 +01:00
Eric Kuck f6af9d9ffb All migration stuff moved to one gross file
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 12:51:14 -06:00
Jason A. Donenfeld 78b38a4eba Simplify versioning of stored data
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:58:06 +01:00
Jason A. Donenfeld ec031b1f19 Get rid of superflous isActivateOnDemandEnabled key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:50:32 +01:00
Eric Kuck 8553723e04 Updated NETunnelProvider save format
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:42:16 +01:00
Jason A. Donenfeld 38445114e0 NE: simplify logic
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 15:56:03 +01:00
Roopesh Chander a21c569e9f NE: Simplify DNS resolution
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 19:24:22 +05:30
Roopesh Chander 0552d75aa1 Localize all the things
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 18:34:09 +05:30
Roopesh Chander e47a8232d8 Tunnel detail: iPad: Handle deletion of tunnel correctly
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 18:02:18 +05:30
Roopesh Chander f818cdd963 NE: Update listen port only when first interface changes
When handling network path changes, change the listen port
only when the first interface has changed.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 17:32:44 +05:30
Roopesh Chander 28ce4d5164 NE: Change handling of bad domain names and Activate On Demand
The solution implemented in commit b8c331c causes the tunnel to
remain in 'Activating' state, without the ability to cancel that.

So, in this commit, instead of retrying DNS silently on
Activated-On-Demand tunnels, we fail the startTunnel() silently.

To summarize, if activate-on-demand is on:
- If started from the WireGuard app, show error using lastErrorFile
mechanism, suggesting a way to turn off Activate On Demand
- If not started from WireGuard app, don't call displayMessage()
(don't show error to user) and silently fail starting the tunnel

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 15:52:47 +05:30
Eric Kuck c2131cb757 Added missing param in MockTunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 13:51:44 -06:00
Jason A. Donenfeld b23578dc7c wireguard-go-bridge: SDK_DIR is not defined for simulator
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-20 20:47:49 +01:00
Eric Kuck a89ad95901 Enabled more swiftlint rules
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 11:22:37 -06:00
Eric Kuck 5618c465a2 Added a String->[String] helper
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 10:46:26 -06:00
Roopesh Chander de08978a80 TunnelErrors: Remove unused error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-20 19:44:57 +05:30
Roopesh Chander 9268c0c4bc Tunnel edit: init() need not take a tunnelConfiguration argument
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander 5c501ac9a6 NE: Log whether tunnel was activated from the app or not
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander 35450bf407 Remove non-helpful comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander f93c9797ea Tunnel edit: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander bba6d2f919 TunnelsManager: If only Activate On Demand has changed, don't restart tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander fa51e3f1d1 NE: Handle bad domain names and Activate On Demand
This combination causes iOS to keep trying to bring up the tunnel,
leading to a lot of displayMessage() alerts.

In this fix, if we get a DNS resolution error in an Activate On Demand
enabled tunnel, we silently retry 9 times (with a 4-second delay before
each retry) and then show the displayMessage() alert.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 15:38:00 +05:30
Roopesh Chander 04a8c2ff5a NE: No need for two startTunnel() methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 13:10:42 +05:30
Roopesh Chander 4e516d6769 TunnelsManager: Handle waiting on a stale tunnel
If we have a stale tunnel on which we don't get status updates we rely
on a timer to update the status (see commit 34a7e5b).  Previously, if
the user tries to activate another tunnel, that resulted in both tunnels
waiting indefinitely. This commit fixes that.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 12:48:10 +05:30
Roopesh Chander fab7af6f38 Remove buttons and text from LaunchScreen.storyboard
With state restoration, we're not guaranteed that the
list view will get shown immediately after the launch screen.
So, generalize the launch screen as much as possible.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 23:00:27 +05:30
Roopesh Chander 3ae9fb538d s/Observervation/Observation/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 23:00:27 +05:30
Roopesh Chander 78eaab8b5b Tunnel detail: Update restorationIdentifier when tunnel name changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 19:27:31 +05:30
Roopesh Chander 20f8abdf04 TunnelsManager: Add periods to end the system error messages
Because they can be part of a multi-sentence message when displayed
in the alert.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 15:47:20 +05:30
Roopesh Chander 2582ddd6f6 Error handling: Add info on the underlying system error to error alerts
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-17 19:04:17 +05:30
68 changed files with 1739 additions and 1280 deletions
+1
View File
@@ -19,6 +19,7 @@ DerivedData
*.perspectivev3 *.perspectivev3
!default.perspectivev3 !default.perspectivev3
xcuserdata xcuserdata
*.xcscheme
## Other ## Other
*.xccheckout *.xccheckout
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright © 2018 WireGuard LLC. All Rights Reserved. Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
+10
View File
@@ -3,7 +3,17 @@ disabled_rules:
- trailing_whitespace - trailing_whitespace
- todo - todo
opt_in_rules: opt_in_rules:
- empty_count
- empty_string
- implicitly_unwrapped_optional
- legacy_random
- let_var_whitespace
- literal_expression_end_indentation
- override_in_extension
- redundant_type_annotation
- toggle_bool
- unneeded_parentheses_in_closure_argument - unneeded_parentheses_in_closure_argument
- unused_import
# - trailing_closure # - trailing_closure
file_length: file_length:
warning: 500 warning: 500
+1 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import os.log import os.log
+2 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import os.log import os.log
@@ -8,6 +8,7 @@ public class Logger {
enum LoggerError: Error { enum LoggerError: Error {
case openFailure case openFailure
} }
static var global: Logger? static var global: Logger?
var log: OpaquePointer var log: OpaquePointer
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright © 2018 WireGuard LLC. All Rights Reserved. * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
*/ */
#include <string.h> #include <string.h>
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright © 2018 WireGuard LLC. All Rights Reserved. * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
*/ */
#ifndef RINGLOGGER_H #ifndef RINGLOGGER_H
@@ -1,68 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
@available(OSX 10.14, iOS 12.0, *)
final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength: Int = 32
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
}
@available(OSX 10.14, iOS 12.0, *)
struct InterfaceConfiguration: Codable {
var name: String
var privateKey: Data
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns = [DNSServer]()
init(name: String, privateKey: Data) {
self.name = name
self.privateKey = privateKey
if name.isEmpty {
fatalError("Empty name")
}
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
}
}
@available(OSX 10.14, iOS 12.0, *)
struct PeerConfiguration: Codable {
var publicKey: Data
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if value.count != TunnelConfiguration.keyLength {
fatalError("Invalid preshared key")
}
}
}
}
var allowedIPs = [IPAddressRange]()
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
init(publicKey: Data) {
self.publicKey = publicKey
if publicKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid public key")
}
}
}
+14 -36
View File
@@ -1,18 +1,28 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Network import Network
@available(OSX 10.14, iOS 12.0, *)
struct DNSServer { struct DNSServer {
let address: IPAddress let address: IPAddress
init(address: IPAddress) {
self.address = address
}
} }
// MARK: Converting to and from String extension DNSServer: Equatable {
// For use in the UI static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue
}
}
extension DNSServer { extension DNSServer {
var stringRepresentation: String {
return "\(address)"
}
init?(from addressString: String) { init?(from addressString: String) {
if let addr = IPv4Address(addressString) { if let addr = IPv4Address(addressString) {
address = addr address = addr
@@ -22,36 +32,4 @@ extension DNSServer {
return nil return nil
} }
} }
func stringRepresentation() -> String {
return "\(address)"
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension DNSServer: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(address.rawValue)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
} }
+29 -36
View File
@@ -1,19 +1,44 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Network import Network
@available(OSX 10.14, iOS 12.0, *)
struct Endpoint { struct Endpoint {
let host: NWEndpoint.Host let host: NWEndpoint.Host
let port: NWEndpoint.Port let port: NWEndpoint.Port
init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
self.host = host
self.port = port
}
} }
// MARK: Converting to and from String extension Endpoint: Equatable {
// For use in the UI static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
return lhs.host == rhs.host && lhs.port == rhs.port
}
}
extension Endpoint: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(host)
hasher.combine(port)
}
}
extension Endpoint { extension Endpoint {
var stringRepresentation: String {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
}
}
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
@@ -42,38 +67,6 @@ extension Endpoint {
host = NWEndpoint.Host(hostString) host = NWEndpoint.Host(hostString)
port = endpointPort port = endpointPort
} }
func stringRepresentation() -> String {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
}
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension Endpoint: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(stringRepresentation())
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let endpointString = try container.decode(String.self)
guard let endpoint = Endpoint(from: endpointString) else {
throw DecodingError.invalidData
}
self = endpoint
}
enum DecodingError: Error {
case invalidData
}
} }
extension Endpoint { extension Endpoint {
+32 -49
View File
@@ -1,20 +1,44 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Network import Network
@available(OSX 10.14, iOS 12.0, *)
struct IPAddressRange { struct IPAddressRange {
let address: IPAddress let address: IPAddress
var networkPrefixLength: UInt8 var networkPrefixLength: UInt8
init(address: IPAddress, networkPrefixLength: UInt8) {
self.address = address
self.networkPrefixLength = networkPrefixLength
}
} }
// MARK: Converting to and from String extension IPAddressRange: Equatable {
// For use in the UI static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
}
}
extension IPAddressRange: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(address.rawValue)
hasher.combine(networkPrefixLength)
}
}
extension IPAddressRange { extension IPAddressRange {
var stringRepresentation: String {
return "\(address)/\(networkPrefixLength)"
}
init?(from string: String) { init?(from string: String) {
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
address = parsed.0
networkPrefixLength = parsed.1
}
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
let addressString = String(string[string.startIndex ..< endOfIPAddress]) let addressString = String(string[string.startIndex ..< endOfIPAddress])
let address: IPAddress let address: IPAddress
@@ -25,7 +49,8 @@ extension IPAddressRange {
} else { } else {
return nil return nil
} }
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)
@@ -36,49 +61,7 @@ extension IPAddressRange {
} else { } else {
networkPrefixLength = maxNetworkPrefixLength networkPrefixLength = maxNetworkPrefixLength
} }
self.address = address
self.networkPrefixLength = networkPrefixLength return (address, networkPrefixLength)
}
func stringRepresentation() -> String {
return "\(address)/\(networkPrefixLength)"
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension IPAddressRange: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let addressDataLength: Int
if address is IPv4Address {
addressDataLength = 4
} else if address is IPv6Address {
addressDataLength = 16
} else {
fatalError()
}
var data = Data(capacity: addressDataLength + 1)
data.append(address.rawValue)
data.append(networkPrefixLength)
try container.encode(data)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
address = ipAddress
}
enum DecodingError: Error {
case invalidData
} }
} }
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct InterfaceConfiguration {
var privateKey: Data
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns = [DNSServer]()
init(privateKey: Data) {
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
self.privateKey = privateKey
}
}
extension InterfaceConfiguration: Equatable {
static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
return lhs.privateKey == rhs.privateKey &&
lhsAddresses == rhsAddresses &&
lhs.listenPort == rhs.listenPort &&
lhs.mtu == rhs.mtu &&
lhs.dns == rhs.dns
}
}
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
protocol LegacyModel: Decodable {
associatedtype Model
var migrated: Model { get }
}
struct LegacyDNSServer: LegacyModel {
let address: IPAddress
var migrated: DNSServer {
return DNSServer(address: address)
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
}
extension Array where Element == LegacyDNSServer {
var migrated: [DNSServer] {
return map { $0.migrated }
}
}
struct LegacyEndpoint: LegacyModel {
let host: Network.NWEndpoint.Host
let port: Network.NWEndpoint.Port
var migrated: Endpoint {
return Endpoint(host: host, port: port)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let endpointString = try container.decode(String.self)
guard !endpointString.isEmpty else { throw DecodingError.invalidData }
let startOfPort: String.Index
let hostString: String
if endpointString.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = endpointString.index(after: endpointString.startIndex)
guard let endOfHost = endpointString.dropFirst().firstIndex(of: "]") else { throw DecodingError.invalidData }
let afterEndOfHost = endpointString.index(after: endOfHost)
guard endpointString[afterEndOfHost] == ":" else { throw DecodingError.invalidData }
startOfPort = endpointString.index(after: afterEndOfHost)
hostString = String(endpointString[startOfHost ..< endOfHost])
} else {
// Look for an IPv4-style endpoint, like 127.0.0.1:80
guard let endOfHost = endpointString.firstIndex(of: ":") else { throw DecodingError.invalidData }
startOfPort = endpointString.index(after: endOfHost)
hostString = String(endpointString[endpointString.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(endpointString[startOfPort ..< endpointString.endIndex])) else { throw DecodingError.invalidData }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard invalidCharacterIndex == nil else { throw DecodingError.invalidData }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
enum DecodingError: Error {
case invalidData
}
}
struct LegacyInterfaceConfiguration: LegacyModel {
let name: String
let privateKey: Data
let addresses: [LegacyIPAddressRange]
let listenPort: UInt16?
let mtu: UInt16?
let dns: [LegacyDNSServer]
var migrated: InterfaceConfiguration {
var interface = InterfaceConfiguration(privateKey: privateKey)
interface.addresses = addresses.migrated
interface.listenPort = listenPort
interface.mtu = mtu
interface.dns = dns.migrated
return interface
}
}
struct LegacyIPAddressRange: LegacyModel {
let address: IPAddress
let networkPrefixLength: UInt8
var migrated: IPAddressRange {
return IPAddressRange(address: address, networkPrefixLength: networkPrefixLength)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
}
extension Array where Element == LegacyIPAddressRange {
var migrated: [IPAddressRange] {
return map { $0.migrated }
}
}
struct LegacyPeerConfiguration: LegacyModel {
let publicKey: Data
let preSharedKey: Data?
let allowedIPs: [LegacyIPAddressRange]
let endpoint: LegacyEndpoint?
let persistentKeepAlive: UInt16?
var migrated: PeerConfiguration {
var configuration = PeerConfiguration(publicKey: publicKey)
configuration.preSharedKey = preSharedKey
configuration.allowedIPs = allowedIPs.migrated
configuration.endpoint = endpoint?.migrated
configuration.persistentKeepAlive = persistentKeepAlive
return configuration
}
}
extension Array where Element == LegacyPeerConfiguration {
var migrated: [PeerConfiguration] {
return map { $0.migrated }
}
}
final class LegacyTunnelConfiguration: LegacyModel {
let interface: LegacyInterfaceConfiguration
let peers: [LegacyPeerConfiguration]
var migrated: TunnelConfiguration {
return TunnelConfiguration(name: interface.name, interface: interface.migrated, peers: peers.migrated)
}
}
extension NETunnelProviderProtocol {
@discardableResult
func migrateConfigurationIfNeeded() -> Bool {
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
if configurationVersion == 1 {
migrateFromConfigurationV1()
} else {
fatalError("No migration from configuration version \(configurationVersion) exists.")
}
return true
}
private func migrateFromConfigurationV1() {
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return }
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return }
providerConfiguration = [Keys.wgQuickConfig.rawValue: configuration.migrated.asWgQuickConfig()]
}
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum PacketTunnelProviderError: String, Error {
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure
case couldNotStartBackend
case couldNotDetermineFileDescriptor
case couldNotSetNetworkSettings
}
extension NETunnelProviderProtocol {
enum Keys: String {
case wgQuickConfig = "WgQuickConfig"
}
convenience init?(tunnelConfiguration: TunnelConfiguration) {
self.init()
let appId = Bundle.main.bundleIdentifier!
providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [Keys.wgQuickConfig.rawValue: tunnelConfiguration.asWgQuickConfig()]
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
if endpoints.count == 1 {
serverAddress = endpoints[0].stringRepresentation
} else if endpoints.isEmpty {
serverAddress = "Unspecified"
} else {
serverAddress = "Multiple endpoints"
}
}
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
migrateConfigurationIfNeeded()
guard let serializedConfig = providerConfiguration?[Keys.wgQuickConfig.rawValue] as? String else { return nil }
return try? TunnelConfiguration(fromWgQuickConfig: serializedConfig, called: name)
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
struct PeerConfiguration {
var publicKey: Data
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if value.count != TunnelConfiguration.keyLength {
fatalError("Invalid preshared key")
}
}
}
}
var allowedIPs = [IPAddressRange]()
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
init(publicKey: Data) {
self.publicKey = publicKey
if publicKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid public key")
}
}
}
extension PeerConfiguration: Equatable {
static func == (lhs: PeerConfiguration, rhs: PeerConfiguration) -> Bool {
return lhs.publicKey == rhs.publicKey &&
lhs.preSharedKey == rhs.preSharedKey &&
Set(lhs.allowedIPs) == Set(rhs.allowedIPs) &&
lhs.endpoint == rhs.endpoint &&
lhs.persistentKeepAlive == rhs.persistentKeepAlive
}
}
extension PeerConfiguration: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(publicKey)
hasher.combine(preSharedKey)
hasher.combine(Set(allowedIPs))
hasher.combine(endpoint)
hasher.combine(persistentKeepAlive)
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
extension String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
return split(separator: separator)
.map {
if let charSet = trimmingCharacters {
return $0.trimmingCharacters(in: charSet)
} else {
return String($0)
}
}
}
}
extension Optional where Wrapped == String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
switch self {
case .none:
return []
case .some(let wrapped):
return wrapped.splitToArray(separator: separator, trimmingCharacters: trimmingCharacters)
}
}
}
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
class WgQuickConfigFileParser { extension TunnelConfiguration {
enum ParserState { enum ParserState {
case inInterfaceSection case inInterfaceSection
@@ -20,14 +20,12 @@ class WgQuickConfigFileParser {
case invalidPeer case invalidPeer
} }
//swiftlint:disable:next cyclomatic_complexity function_body_length //swiftlint:disable:next function_body_length cyclomatic_complexity
static func parse(_ text: String, name: String) throws -> TunnelConfiguration { convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
assert(!name.isEmpty)
var interfaceConfiguration: InterfaceConfiguration? var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]() var peerConfigurations = [PeerConfiguration]()
let lines = text.split(separator: "\n") let lines = wgQuickConfig.split(separator: "\n")
var parserState = ParserState.notInASection var parserState = ParserState.notInASection
var attributes = [String: String]() var attributes = [String: String]()
@@ -42,7 +40,7 @@ class WgQuickConfigFileParser {
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces) trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
guard trimmedLine.count > 0 else { continue } guard !trimmedLine.isEmpty else { continue }
let lowercasedLine = line.lowercased() let lowercasedLine = line.lowercased()
if let equalsIndex = line.firstIndex(of: "=") { if let equalsIndex = line.firstIndex(of: "=") {
@@ -64,11 +62,11 @@ class WgQuickConfigFileParser {
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, name: name) else { throw ParseError.invalidInterface } guard let interface = TunnelConfiguration.collate(interfaceAttributes: attributes) 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 = TunnelConfiguration.collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer) peerConfigurations.append(peer)
} }
} }
@@ -89,19 +87,57 @@ class WgQuickConfigFileParser {
} }
if let interfaceConfiguration = interfaceConfiguration { if let interfaceConfiguration = interfaceConfiguration {
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations) self.init(name: name, interface: interfaceConfiguration, peers: peerConfigurations)
return tunnelConfiguration
} else { } else {
throw ParseError.noInterface throw ParseError.noInterface
} }
} }
func asWgQuickConfig() -> String {
var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n")
}
if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
if let mtu = interface.mtu {
output.append("MTU = \(mtu)\n")
}
for peer in peers {
output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
if let preSharedKey = peer.preSharedKey {
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
}
if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
if let endpoint = peer.endpoint {
output.append("Endpoint = \(endpoint.stringRepresentation)\n")
}
if let persistentKeepAlive = peer.persistentKeepAlive {
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
}
}
return output
}
//swiftlint:disable:next cyclomatic_complexity //swiftlint:disable:next cyclomatic_complexity
private static func collate(interfaceAttributes attributes: [String: String], name: String) -> InterfaceConfiguration? { private static func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
// required wg fields // required wg fields
guard let privateKeyString = attributes["privatekey"] else { return nil } guard let privateKeyString = attributes["privatekey"] else { return nil }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil } guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey) var interface = InterfaceConfiguration(privateKey: privateKey)
// other wg fields // other wg fields
if let listenPortString = attributes["listenport"] { if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else { return nil } guard let listenPort = UInt16(listenPortString) else { return nil }
@@ -110,18 +146,16 @@ class WgQuickConfigFileParser {
// wg-quick fields // wg-quick fields
if let addressesString = attributes["address"] { if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]() var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") { for addressString in addressesString.splitToArray(trimmingCharacters: .whitespaces) {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces) guard let address = IPAddressRange(from: addressString) else { return nil }
guard let address = IPAddressRange(from: trimmedString) else { return nil }
addresses.append(address) addresses.append(address)
} }
interface.addresses = addresses interface.addresses = addresses
} }
if let dnsString = attributes["dns"] { if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]() var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") { for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespaces) {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces) guard let dnsServer = DNSServer(from: dnsServerString) else { return nil }
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
dnsServers.append(dnsServer) dnsServers.append(dnsServer)
} }
interface.dns = dnsServers interface.dns = dnsServers
@@ -146,9 +180,8 @@ class WgQuickConfigFileParser {
} }
if let allowedIPsString = attributes["allowedips"] { if let allowedIPsString = attributes["allowedips"] {
var allowedIPs = [IPAddressRange]() var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") { for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) guard let allowedIP = IPAddressRange(from: allowedIPString) else { return nil }
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
allowedIPs.append(allowedIP) allowedIPs.append(allowedIP)
} }
peer.allowedIPs = allowedIPs peer.allowedIPs = allowedIPs
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
final class TunnelConfiguration {
var name: String?
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength = 32
init(name: String?, interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
self.name = name
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
}
extension TunnelConfiguration: Equatable {
static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
return lhs.name == rhs.name &&
lhs.interface == rhs.interface &&
Set(lhs.peers) == Set(rhs.peers)
}
}
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
extension NETunnelProviderProtocol {
convenience init?(tunnelConfiguration: TunnelConfiguration) {
assert(!tunnelConfiguration.interface.name.isEmpty)
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
self.init()
let appId = Bundle.main.bundleIdentifier!
providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [
"tunnelConfiguration": serializedTunnelConfiguration,
"tunnelConfigurationVersion": 1
]
let endpoints = tunnelConfiguration.peers.compactMap {$0.endpoint}
if endpoints.count == 1 {
serverAddress = endpoints.first!.stringRepresentation()
} else if endpoints.isEmpty {
serverAddress = "Unspecified"
} else {
serverAddress = "Multiple endpoints"
}
username = tunnelConfiguration.interface.name
}
func tunnelConfiguration() -> TunnelConfiguration? {
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
}
}
+76 -29
View File
@@ -16,7 +16,16 @@
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; }; 5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; };
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; }; 5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; };
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; }; 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
5F4541AE21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift */; }; 5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; };
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.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 */; };
@@ -28,15 +37,15 @@
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; }; 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; }; 6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; }; 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */; };
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; }; 6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; }; 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; }; 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; }; 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; };
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; }; 6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */; };
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; }; 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; }; 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; }; 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; }; 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; }; 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; }; 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
@@ -50,7 +59,8 @@
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */; }; 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */; };
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; }; 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; }; 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; }; 6FE1765621C90BBE002690EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6FE1765421C90BBE002690EA /* Localizable.strings */; };
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1765921C90E87002690EA /* LocalizationHelper.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 */; }; 6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
@@ -60,7 +70,7 @@
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 */; };
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 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */; };
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; }; 6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; }; 6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; }; 6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
@@ -112,7 +122,11 @@
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.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>"; }; 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>"; }; 5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = "<group>"; };
5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+CustomStringConvertible.swift"; sourceTree = "<group>"; }; 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyConfigMigration.swift; sourceTree = "<group>"; };
5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConfiguration.swift; sourceTree = "<group>"; };
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConfiguration.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; };
@@ -130,15 +144,15 @@
6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; }; 6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; };
6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; }; 6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; };
6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; }; 6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; };
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileParser.swift; sourceTree = "<group>"; };
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; }; 6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; }; 6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; }; 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
6F7774E72172020C006A79B3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; }; 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; };
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; }; 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; };
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; }; 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; }; 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; }; 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; }; 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; }; 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
@@ -155,7 +169,8 @@
6FDEF7FF21863C0100D8FBF6 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = "<group>"; }; 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = "<group>"; };
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; }; 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; }; 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; }; 6FE1765521C90BBE002690EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/Localizable.strings; sourceTree = "<group>"; };
6FE1765921C90E87002690EA /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.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>"; }; 6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; };
@@ -238,7 +253,6 @@
children = ( children = (
6FF3526A21C23F720008484E /* Logging */, 6FF3526A21C23F720008484E /* Logging */,
6F7774E6217201E0006A79B3 /* Model */, 6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */, 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
); );
path = Shared; path = Shared;
@@ -254,15 +268,6 @@
path = Crypto; path = Crypto;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6F6899AA218099D00012E523 /* ConfigFile */ = {
isa = PBXGroup;
children = (
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */,
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */,
);
path = ConfigFile;
sourceTree = "<group>";
};
6F7774DD217181B1006A79B3 /* UI */ = { 6F7774DD217181B1006A79B3 /* UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -287,10 +292,16 @@
6F7774E6217201E0006A79B3 /* Model */ = { 6F7774E6217201E0006A79B3 /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F7774E72172020C006A79B3 /* Configuration.swift */, 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */,
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */,
6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */,
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */, 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */,
6F693A552179E556008551C1 /* Endpoint.swift */, 6F693A552179E556008551C1 /* Endpoint.swift */,
6F628C3E217F3413003482A3 /* DNSServer.swift */, 6F628C3E217F3413003482A3 /* DNSServer.swift */,
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */,
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -302,7 +313,7 @@
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */, 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
5F4541A821C451D100994C13 /* TunnelStatus.swift */, 5F4541A821C451D100994C13 /* TunnelStatus.swift */,
6FB1017821C57DE600766195 /* MockTunnels.swift */, 6FB1017821C57DE600766195 /* MockTunnels.swift */,
5F4541AD21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift */, 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */,
); );
path = Tunnel; path = Tunnel;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -371,6 +382,7 @@
6FF4AC0B211EC46F002C96EB = { 6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6FE1765421C90BBE002690EA /* Localizable.strings */,
6F5D0C432183B4A4000F85AD /* Shared */, 6F5D0C432183B4A4000F85AD /* Shared */,
6FF4AC16211EC46F002C96EB /* WireGuard */, 6FF4AC16211EC46F002C96EB /* WireGuard */,
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */, 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */,
@@ -393,12 +405,12 @@
children = ( children = (
6F919ED3218C65C50023B400 /* Resources */, 6F919ED3218C65C50023B400 /* Resources */,
6F6899A32180445A0012E523 /* Crypto */, 6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */,
6F7774DD217181B1006A79B3 /* UI */, 6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* Tunnel */, 6F7774ED21722D0C006A79B3 /* Tunnel */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */, 6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */, 6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */, 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
6FE1765921C90E87002690EA /* LocalizationHelper.swift */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */, 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
6FF4AC1E211EC472002C96EB /* Assets.xcassets */, 6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */, 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
@@ -462,6 +474,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */; buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
buildPhases = ( buildPhases = (
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */,
5F45417A21C0902400994C13 /* Swiftlint */, 5F45417A21C0902400994C13 /* Swiftlint */,
6B87860E2189532500C099FB /* Extract wireguard-go Version */, 6B87860E2189532500C099FB /* Extract wireguard-go Version */,
6FF4AC10211EC46F002C96EB /* Sources */, 6FF4AC10211EC46F002C96EB /* Sources */,
@@ -543,6 +556,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */, 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */, 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */, 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */, 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */,
@@ -590,6 +604,24 @@
shellPath = /bin/sh; 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"; 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";
}; };
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Strip Trailing Whitespace";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "find . -name '*.swift' -exec sed -i '' -E 's/[[:space:]]+$//g' {} +\n";
};
6B87860E2189532500C099FB /* Extract wireguard-go Version */ = { 6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -639,12 +671,17 @@
6FF3527121C240160008484E /* Logger.swift 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 */,
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */, 6FFA5D8E2194370D0001E2F7 /* TunnelConfiguration.swift in Sources */,
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */,
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */, 6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */, 6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */, 6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */, 6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */, 6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */, 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
); );
@@ -654,9 +691,9 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */,
6FF3527221C2616C0008484E /* ringlogger.c in Sources */, 6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */, 6FF3527321C2616C0008484E /* Logger.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 */,
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */, 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
@@ -665,27 +702,29 @@
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.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 */,
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.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 */,
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */, 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */,
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */, 6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */, 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */, 5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */, 5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */, 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */, 5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.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 */,
5F4541AE21C7704300994C13 /* NEVPNStatus+CustomStringConvertible.swift in Sources */, 6F7774E82172020C006A79B3 /* TunnelConfiguration.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 */, 5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.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 */,
@@ -694,8 +733,10 @@
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */, 6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */, 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */, 5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */, 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -715,6 +756,14 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
6FE1765421C90BBE002690EA /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
6FE1765521C90BBE002690EA /* Base */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = { 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@@ -928,7 +977,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
@@ -948,7 +996,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
@@ -4,6 +4,6 @@
<dict> <dict>
<key>FILEHEADER</key> <key>FILEHEADER</key>
<string> SPDX-License-Identifier: MIT <string> SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.</string> // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
</dict> </dict>
</plist> </plist>
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
<device id="ipad9_7" orientation="landscape"> <device id="ipad9_7" orientation="landscape">
<adaptation id="fullscreen"/> <adaptation id="fullscreen"/>
</device> </device>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@@ -23,7 +23,7 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="XJD-RE-ATd" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="XJD-RE-ATd" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
</scene> </scene>
<!--WireGuard--> <!--Table View Controller-->
<scene sceneID="pXL-Um-fWR"> <scene sceneID="pXL-Um-fWR">
<objects> <objects>
<tableViewController clearsSelectionOnViewWillAppear="NO" id="9s0-Gz-xEQ" sceneMemberID="viewController"> <tableViewController clearsSelectionOnViewWillAppear="NO" id="9s0-Gz-xEQ" sceneMemberID="viewController">
@@ -46,10 +46,7 @@
<outlet property="delegate" destination="9s0-Gz-xEQ" id="Frz-TZ-bD1"/> <outlet property="delegate" destination="9s0-Gz-xEQ" id="Frz-TZ-bD1"/>
</connections> </connections>
</tableView> </tableView>
<navigationItem key="navigationItem" title="WireGuard" id="0ZE-eq-OPY"> <navigationItem key="navigationItem" id="0ZE-eq-OPY"/>
<barButtonItem key="leftBarButtonItem" title="Settings" id="a9Y-E3-hac"/>
<barButtonItem key="rightBarButtonItem" systemItem="add" id="ZSm-4E-cJx"/>
</navigationItem>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="YJ1-t3-sLe" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="YJ1-t3-sLe" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
// Generic alert action names
"actionOK" = "OK";
"actionCancel" = "Cancel";
"actionSave" = "Save";
// Tunnels list UI
"tunnelsListTitle" = "WireGuard";
"tunnelsListSettingsButtonTitle" = "Settings";
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
"tunnelsListSwipeDeleteButtonTitle" = "Delete";
// Tunnels list menu
"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
"addTunnelMenuImportFile" = "Create from file or archive";
"addTunnelMenuQRCode" = "Create from QR code";
"addTunnelMenuFromScratch" = "Create from scratch";
// Tunnels list alerts
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
"alertUnableToImportTitle" = "Unable to import tunnel";
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration.";
// Tunnel detail and edit UI
"newTunnelViewTitle" = "New configuration";
"editTunnelViewTitle" = "Edit configuration";
"tunnelSectionTitleStatus" = "Status";
"tunnelStatusInactive" = "Inactive";
"tunnelStatusActivating" = "Activating";
"tunnelStatusActive" = "Active";
"tunnelStatusDeactivating" = "Deactivating";
"tunnelStatusReasserting" = "Reactivating";
"tunnelStatusRestarting" = "Restarting";
"tunnelStatusWaiting" = "Waiting";
"tunnelSectionTitleInterface" = "Interface";
"tunnelInterfaceName" = "Name";
"tunnelInterfacePrivateKey" = "Private key";
"tunnelInterfacePublicKey" = "Public key";
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
"tunnelInterfaceAddresses" = "Addresses";
"tunnelInterfaceListenPort" = "Listen port";
"tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS servers";
"tunnelSectionTitlePeer" = "Peer";
"tunnelPeerPublicKey" = "Public key";
"tunnelPeerPreSharedKey" = "Preshared key";
"tunnelPeerEndpoint" = "Endpoint";
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
"tunnelPeerAllowedIPs" = "Allowed IPs";
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
"tunnelOnDemandKey" = "Activate on demand";
"tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
"addPeerButtonTitle" = "Add peer";
"deletePeerButtonTitle" = "Delete peer";
"deletePeerConfirmationAlertButtonTitle" = "Delete";
"deletePeerConfirmationAlertMessage" = "Delete this peer?";
"deleteTunnelButtonTitle" = "Delete tunnel";
"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
"tunnelEditPlaceholderTextRequired" = "Required";
"tunnelEditPlaceholderTextOptional" = "Optional";
"tunnelEditPlaceholderTextAutomatic" = "Automatic";
"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
"tunnelEditPlaceholderTextOff" = "Off";
// Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */
"alertInvalidInterfaceTitle" = "Invalid interface";
/* Any one of the following alert messages can go with the above title */
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface's private key is required";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface's private key must be a 32-byte key in base64 encoding";
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface's listen port must be between 0 and 65535, or unspecified";
"alertInvalidInterfaceMessageMTUInvalid" = "Interface's MTU must be between 576 and 65535, or unspecified";
"alertInvalidInterfaceMessageDNSInvalid" = "Interface's DNS servers must be a list of comma-separated IP addresses";
/* Alert title for error in the peer data */
"alertInvalidPeerTitle" = "Invalid peer";
/* Any one of the following alert messages can go with the above title */
"alertInvalidPeerMessagePublicKeyRequired" = "Peer's public key is required";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer's public key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer's preshared key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidPeerMessageEndpointInvalid" = "Peer's endpoint must be of the form 'host:port' or '[host]:port'";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer's persistent keepalive must be between 0 to 65535, or unspecified";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
// Scanning QR code UI
"scanQRCodeViewTitle" = "Scan QR code";
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
// Scanning QR code alerts
"alertScanQRCodeCameraUnsupportedTitle" = "Camera Unsupported";
"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
"alertScanQRCodeInvalidQRCodeTitle" = "Invalid QR Code";
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
"alertScanQRCodeUnreadableQRCodeTitle" = "Invalid Code";
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
// Settings UI
"settingsViewTitle" = "Settings";
"settingsSectionTitleAbout" = "About";
"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"settingsExportZipButtonTitle" = "Export zip archive";
"settingsSectionTitleTunnelLog" = "Tunnel log";
"settingsExportLogFileButtonTitle" = "Export log file";
// Settings alerts
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
"alertUnableToWriteLogTitle" = "Log export failed";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
// Zip import / export error alerts
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"alertBadArchiveTitle" = "Unable to read zip archive";
"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
"alertNoTunnelsToExportTitle" = "Nothing to export";
"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
// Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Activation failure";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertTunnelNameEmptyTitle" = "No name provided";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
// Tunnel management error alerts on system error
/* The alert message that goes with the following titles would be
one of the alertSystemErrorMessage* listed further down */
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
/* The alert message for this alert shall include
one of the alertSystemErrorMessage* listed further down */
"alertTunnelActivationSystemErrorTitle" = "Activation failure";
"alertTunnelActivationSystemErrorMessage (%@)" = "The tunnel could not be activated. %@";
/* alertSystemErrorMessage* messages */
"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104 VERSION_NAME = 0.0.20190107
VERSION_ID = 7 VERSION_ID = 1
@@ -1,46 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class WgQuickConfigFileWriter {
static func writeConfigFile(from configuration: TunnelConfiguration) -> Data? {
let interface = configuration.interface
var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n")
}
if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
if let mtu = interface.mtu {
output.append("MTU = \(mtu)\n")
}
for peer in configuration.peers {
output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
if let preSharedKey = peer.preSharedKey {
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
}
if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
if let endpoint = peer.endpoint {
output.append("Endpoint = \(endpoint.stringRepresentation())\n")
}
if let persistentKeepAlive = peer.persistentKeepAlive {
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
}
}
return output.data(using: .utf8)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0+ /* SPDX-License-Identifier: GPL-2.0+
* *
* Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* *
* Curve25519 ECDH functions, based on TweetNaCl but cleaned up. * Curve25519 ECDH functions, based on TweetNaCl but cleaned up.
*/ */
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
func tr(_ key: String) -> String {
return NSLocalizedString(key, comment: "")
}
func tr(format: String, _ arguments: CVarArg...) -> String {
return String(format: NSLocalizedString(format, comment: ""), arguments: arguments)
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
+4 -4
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
@@ -26,7 +26,7 @@ class MockTunnels {
static func createMockTunnels() -> [NETunnelProviderManager] { static func createMockTunnels() -> [NETunnelProviderManager] {
return tunnelNames.map { tunnelName -> NETunnelProviderManager in return tunnelNames.map { tunnelName -> NETunnelProviderManager in
var interface = InterfaceConfiguration(name: tunnelName, privateKey: Curve25519.generatePrivateKey()) var interface = InterfaceConfiguration(privateKey: Curve25519.generatePrivateKey())
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!] interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
interface.dns = dnsServers.map { DNSServer(from: $0)! } interface.dns = dnsServers.map { DNSServer(from: $0)! }
@@ -34,11 +34,11 @@ class MockTunnels {
peer.endpoint = Endpoint(from: endpoint) peer.endpoint = Endpoint(from: endpoint)
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!] peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
let tunnelConfiguration = TunnelConfiguration(interface: interface, peers: [peer]) let tunnelConfiguration = TunnelConfiguration(name: tunnelName, interface: interface, peers: [peer])
let tunnelProviderManager = NETunnelProviderManager() let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration) tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelConfiguration.name
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
return tunnelProviderManager return tunnelProviderManager
@@ -1,18 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
extension NEVPNStatus: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .connected: return "connected"
case .connecting: return "connecting"
case .disconnected: return "disconnected"
case .disconnecting: return "disconnecting"
case .reasserting: return "reasserting"
case .invalid: return "invalid"
}
}
}
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum TunnelsManagerError: WireGuardAppError {
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case systemErrorOnListingTunnels(systemError: Error)
case systemErrorOnAddTunnel(systemError: Error)
case systemErrorOnModifyTunnel(systemError: Error)
case systemErrorOnRemoveTunnel(systemError: Error)
var alertText: AlertText {
switch self {
case .tunnelNameEmpty:
return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
case .tunnelAlreadyExistsWithThatName:
return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
case .systemErrorOnListingTunnels(let systemError):
return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
case .systemErrorOnAddTunnel(let systemError):
return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnModifyTunnel(let systemError):
return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnRemoveTunnel(let systemError):
return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
}
}
}
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
case tunnelIsNotInactive
case failedWhileStarting(systemError: Error) // startTunnel() throwed
case failedWhileSaving(systemError: Error) // save config after re-enabling throwed
case failedWhileLoading(systemError: Error) // reloading config throwed
case failedBecauseOfTooManyErrors(lastSystemError: Error) // recursion limit reached
var alertText: AlertText {
switch self {
case .tunnelIsNotInactive:
return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
case .failedWhileStarting(let systemError),
.failedWhileSaving(let systemError),
.failedWhileLoading(let systemError),
.failedBecauseOfTooManyErrors(let systemError):
return (tr("alertTunnelActivationSystemErrorTitle"),
tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
}
}
}
enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed(wasOnDemandEnabled: Bool)
case activationFailedWithExtensionError(title: String, message: String, wasOnDemandEnabled: Bool)
var alertText: AlertText {
switch self {
case .activationFailed(let wasOnDemandEnabled):
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
}
}
}
extension PacketTunnelProviderError: WireGuardAppError {
var alertText: AlertText {
switch self {
case .savedProtocolConfigurationIsInvalid:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSavedConfigFailureMessage"))
case .dnsResolutionFailure:
return (tr("alertTunnelDNSFailureTitle"), tr("alertTunnelDNSFailureMessage"))
case .couldNotStartBackend:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationBackendFailureMessage"))
case .couldNotDetermineFileDescriptor:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFileDescriptorFailureMessage"))
case .couldNotSetNetworkSettings:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSetNetworkSettingsMessage"))
}
}
}
extension Error {
var localizedUIString: String {
if let systemError = self as? NEVPNError {
switch systemError {
case NEVPNError.configurationInvalid:
return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
case NEVPNError.configurationDisabled:
return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
case NEVPNError.connectionFailed:
return tr("alertSystemErrorMessageTunnelConnectionFailed")
case NEVPNError.configurationStale:
return tr("alertSystemErrorMessageTunnelConfigurationStale")
case NEVPNError.configurationReadWriteFailed:
return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
case NEVPNError.configurationUnknown:
return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
default:
return ""
}
} else {
return localizedDescription
}
}
}
+15 -2
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import NetworkExtension import NetworkExtension
@@ -12,7 +12,7 @@ import NetworkExtension
case reasserting // Not a possible state at present case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel) case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case waiting // Waiting for another tunnel to be brought down case waiting // Waiting for another tunnel to be brought down
init(from systemStatus: NEVPNStatus) { init(from systemStatus: NEVPNStatus) {
switch systemStatus { switch systemStatus {
case .connected: case .connected:
@@ -44,3 +44,16 @@ extension TunnelStatus: CustomDebugStringConvertible {
} }
} }
} }
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"
}
}
}
+104 -131
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import NetworkExtension import NetworkExtension
@@ -19,70 +19,12 @@ protocol TunnelsManagerActivationDelegate: class {
func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed 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 { class TunnelsManager {
private var tunnels: [TunnelContainer] private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate? weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate? weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject? private var statusObservationToken: AnyObject?
private var waiteeObservationToken: AnyObject?
init(tunnelProviders: [NETunnelProviderManager]) { init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name } tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
@@ -96,16 +38,45 @@ class TunnelsManager {
NETunnelProviderManager.loadAllFromPreferences { managers, error in NETunnelProviderManager.loadAllFromPreferences { managers, error in
if let error = error { if let error = error {
wg_log(.error, message: "Failed to load tunnel provider managers: \(error)") wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels)) completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels(systemError: error)))
return return
} }
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
let tunnelManagers = managers ?? []
tunnelManagers.forEach { tunnelManager in
if (tunnelManager.protocolConfiguration as? NETunnelProviderProtocol)?.migrateConfigurationIfNeeded() == true {
tunnelManager.saveToPreferences { _ in }
}
}
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
}
#endif
}
func reload(completionHandler: @escaping (Bool) -> Void) {
#if targetEnvironment(simulator)
completionHandler(false)
#else
NETunnelProviderManager.loadAllFromPreferences { managers, _ in
guard let managers = managers else {
completionHandler(false)
return
}
let newTunnels = managers.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
let hasChanges = self.tunnels.map { $0.tunnelConfiguration } != newTunnels.map { $0.tunnelConfiguration }
if hasChanges {
self.tunnels = newTunnels
completionHandler(true)
} else {
completionHandler(false)
}
} }
#endif #endif
} }
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) { func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.name ?? ""
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty)) completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
return return
@@ -118,7 +89,7 @@ class TunnelsManager {
let tunnelProviderManager = NETunnelProviderManager() let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration) tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelConfiguration.name
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager) activateOnDemandSetting.apply(on: tunnelProviderManager)
@@ -126,12 +97,12 @@ class TunnelsManager {
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
wg_log(.error, message: "Add: Saving configuration failed: \(error!)") wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel)) completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
return return
} }
guard let self = self else { return } guard let self = self else { return }
let tunnel = TunnelContainer(tunnel: tunnelProviderManager) let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnels.append(tunnel) self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name } self.tunnels.sort { $0.name < $1.name }
@@ -158,7 +129,7 @@ class TunnelsManager {
} }
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.name ?? ""
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty) completionHandler(TunnelsManagerError.tunnelNameEmpty)
return return
@@ -173,8 +144,9 @@ class TunnelsManager {
} }
tunnel.name = tunnelName tunnel.name = tunnelName
} }
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration) tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelConfiguration.name
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled
@@ -183,11 +155,11 @@ class TunnelsManager {
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)") wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel) completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
return return
} }
guard let self = self else { return } guard let self = self else { return }
if isNameChanged { if isNameChanged {
let oldIndex = self.tunnels.firstIndex(of: tunnel)! let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { $0.name < $1.name } self.tunnels.sort { $0.name < $1.name }
@@ -195,13 +167,13 @@ class TunnelsManager {
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex) self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
} }
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting { if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
// Turn off the tunnel, and then turn it back on, so the changes are made effective // Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.status = .restarting tunnel.status = .restarting
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel() (tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
} }
if isActivatingOnDemand { if isActivatingOnDemand {
// Reload tunnel after saving. // Reload tunnel after saving.
// Without this, the tunnel stopes getting updates on the tunnel status from iOS. // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
@@ -209,7 +181,7 @@ class TunnelsManager {
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
guard error == nil else { guard error == nil else {
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)") wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel) completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
return return
} }
completionHandler(nil) completionHandler(nil)
@@ -226,7 +198,7 @@ class TunnelsManager {
tunnelProviderManager.removeFromPreferences { [weak self] error in tunnelProviderManager.removeFromPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)") wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel) completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
return return
} }
if let self = self { if let self = self {
@@ -264,6 +236,7 @@ class TunnelsManager {
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) { if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'") wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
tunnel.status = .waiting tunnel.status = .waiting
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
if tunnelInOperation.status != .deactivating { if tunnelInOperation.status != .deactivating {
startDeactivation(of: tunnelInOperation) startDeactivation(of: tunnelInOperation)
} }
@@ -291,35 +264,47 @@ class TunnelsManager {
tunnels.forEach { $0.refreshStatus() } tunnels.forEach { $0.refreshStatus() }
} }
private func startObservingTunnelStatuses() { private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
guard statusObservationToken == nil else { return } waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
guard let self = self else { return }
if tunnel.status == .inactive {
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
}
self.waiteeObservationToken = nil
}
}
}
private func startObservingTunnelStatuses() {
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
guard let self = self, guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession, let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager, let tunnelProvider = session.manager as? NETunnelProviderManager,
let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return } let tunnelConfiguration = TunnelContainer(tunnel: tunnelProvider).tunnelConfiguration,
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
if tunnel.tunnelProvider != tunnelProvider {
tunnel.tunnelProvider = tunnelProvider
tunnel.refreshStatus()
}
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'") 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 tunnel.isAttemptingActivation {
if session.status == .connected { if session.status == .connected {
tunnel.isAttemptingActivation = false tunnel.isAttemptingActivation = false
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel) self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
} else if session.status == .disconnected { } else if session.status == .disconnected {
tunnel.isAttemptingActivation = false tunnel.isAttemptingActivation = false
if let (title, message) = self.lastErrorTextFromNetworkExtension(for: tunnel) { if let (title, message) = lastErrorTextFromNetworkExtension(for: tunnel) {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message)) self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
} else { } else {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed) self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
} }
} }
} }
// In case we're restarting the tunnel
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) { if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
// Don't change tunnel.status when disconnecting for a restart
if session.status == .disconnected { if session.status == .disconnected {
tunnel.startActivation(activationDelegate: self.activationDelegate) tunnel.startActivation(activationDelegate: self.activationDelegate)
} }
@@ -327,35 +312,22 @@ class TunnelsManager {
} }
tunnel.refreshStatus() 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 private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
guard let lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
return extensionError.alertText
} }
deinit { return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
if let statusObservationToken = statusObservationToken {
NotificationCenter.default.removeObserver(statusObservationToken)
}
}
} }
class TunnelContainer: NSObject { class TunnelContainer: NSObject {
@@ -367,27 +339,38 @@ class TunnelContainer: NSObject {
var isAttemptingActivation = false { var isAttemptingActivation = false {
didSet { didSet {
if isAttemptingActivation { if isAttemptingActivation {
self.activationTimer?.invalidate()
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.refreshStatus() wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
if self.status == .inactive || self.status == .active { switch self.tunnelProvider.connection.status {
self.isAttemptingActivation = false // This also invalidates the timer case .connected, .disconnected, .invalid:
self.activationTimer?.invalidate()
self.activationTimer = nil
default:
break
} }
self.refreshStatus()
} }
self.activationTimer = activationTimer self.activationTimer = activationTimer
RunLoop.main.add(activationTimer, forMode: .default) RunLoop.main.add(activationTimer, forMode: .default)
} else {
activationTimer?.invalidate()
activationTimer = nil
} }
} }
} }
var activationAttemptId: String? var activationAttemptId: String?
var activationTimer: Timer? var activationTimer: Timer?
fileprivate let tunnelProvider: NETunnelProviderManager fileprivate var tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus? private var lastTunnelConnectionStatus: NEVPNStatus?
var tunnelConfiguration: TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: tunnelProvider.localizedDescription)
}
var activateOnDemandSetting: ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
init(tunnel: NETunnelProviderManager) { init(tunnel: NETunnelProviderManager) {
name = tunnel.localizedDescription ?? "Unnamed" name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status) let status = TunnelStatus(from: tunnel.connection.status)
@@ -397,14 +380,6 @@ class TunnelContainer: NSObject {
super.init() super.init()
} }
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.tunnelConfiguration()
}
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshStatus() { func refreshStatus() {
let status = TunnelStatus(from: tunnelProvider.connection.status) let status = TunnelStatus(from: tunnelProvider.connection.status)
self.status = status self.status = status
@@ -415,7 +390,7 @@ class TunnelContainer: NSObject {
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) { fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
if recursionCount >= 8 { if recursionCount >= 8 {
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)") wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors(lastSystemError: lastError!))
return return
} }
@@ -432,11 +407,10 @@ class TunnelContainer: NSObject {
guard let self = self else { return } guard let self = self else { return }
if error != nil { if error != nil {
wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)") wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving(systemError: error!))
return return
} }
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling") wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, invoking startActivation")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate) self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
} }
return return
@@ -456,13 +430,13 @@ class TunnelContainer: NSObject {
guard let systemError = error as? NEVPNError else { guard let systemError = error as? NEVPNError else {
wg_log(.error, message: "Failed to activate tunnel: Error: \(error)") wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
status = .inactive status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: error))
return return
} }
guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else { guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)") wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
status = .inactive status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: systemError))
return return
} }
wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.") wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
@@ -471,11 +445,10 @@ class TunnelContainer: NSObject {
if error != nil { if error != nil {
wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)") wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
self.status = .inactive self.status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading) activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading(systemError: systemError))
return return
} }
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded") wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate) self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
} }
} }
+96 -95
View File
@@ -1,34 +1,59 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
//swiftlint:disable:next type_body_length //swiftlint:disable:next type_body_length
class TunnelViewModel { class TunnelViewModel {
enum InterfaceField: String { enum InterfaceField {
case name = "Name" case name
case privateKey = "Private key" case privateKey
case publicKey = "Public key" case publicKey
case generateKeyPair = "Generate keypair" case generateKeyPair
case addresses = "Addresses" case addresses
case listenPort = "Listen port" case listenPort
case mtu = "MTU" case mtu
case dns = "DNS servers" case dns
var localizedUIString: String {
switch self {
case .name: return tr("tunnelInterfaceName")
case .privateKey: return tr("tunnelInterfacePrivateKey")
case .publicKey: return tr("tunnelInterfacePublicKey")
case .generateKeyPair: return tr("tunnelInterfaceGenerateKeypair")
case .addresses: return tr("tunnelInterfaceAddresses")
case .listenPort: return tr("tunnelInterfaceListenPort")
case .mtu: return tr("tunnelInterfaceMTU")
case .dns: return tr("tunnelInterfaceDNS")
}
}
} }
static let interfaceFieldsWithControl: Set<InterfaceField> = [ static let interfaceFieldsWithControl: Set<InterfaceField> = [
.generateKeyPair .generateKeyPair
] ]
enum PeerField: String { enum PeerField {
case publicKey = "Public key" case publicKey
case preSharedKey = "Preshared key" case preSharedKey
case endpoint = "Endpoint" case endpoint
case persistentKeepAlive = "Persistent keepalive" case persistentKeepAlive
case allowedIPs = "Allowed IPs" case allowedIPs
case excludePrivateIPs = "Exclude private IPs" case excludePrivateIPs
case deletePeer = "Delete peer" case deletePeer
var localizedUIString: String {
switch self {
case .publicKey: return tr("tunnelPeerPublicKey")
case .preSharedKey: return tr("tunnelPeerPreSharedKey")
case .endpoint: return tr("tunnelPeerEndpoint")
case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
case .allowedIPs: return tr("tunnelPeerAllowedIPs")
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
case .deletePeer: return tr("deletePeerButtonTitle")
}
}
} }
static let peerFieldsWithControl: Set<PeerField> = [ static let peerFieldsWithControl: Set<PeerField> = [
@@ -41,23 +66,21 @@ class TunnelViewModel {
var scratchpad = [InterfaceField: String]() var scratchpad = [InterfaceField: String]()
var fieldsWithError = Set<InterfaceField>() var fieldsWithError = Set<InterfaceField>()
var validatedConfiguration: InterfaceConfiguration? var validatedConfiguration: InterfaceConfiguration?
var validatedName: String?
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.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad() populateScratchpad()
} }
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.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad() populateScratchpad()
} }
validatedConfiguration = nil validatedConfiguration = nil
validatedName = nil
if stringValue.isEmpty { if stringValue.isEmpty {
scratchpad.removeValue(forKey: field) scratchpad.removeValue(forKey: field)
} else { } else {
@@ -75,13 +98,13 @@ class TunnelViewModel {
} }
func populateScratchpad() { func populateScratchpad() {
// Populate the scratchpad from the configuration object
guard let config = validatedConfiguration else { return } guard let config = validatedConfiguration else { return }
scratchpad[.name] = config.name guard let name = validatedName else { return }
scratchpad[.name] = 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 {
scratchpad[.listenPort] = String(listenPort) scratchpad[.listenPort] = String(listenPort)
@@ -90,40 +113,38 @@ class TunnelViewModel {
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 //swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<InterfaceConfiguration> { func save() -> SaveResult<(String, InterfaceConfiguration)> {
if let validatedConfiguration = validatedConfiguration { if let config = validatedConfiguration, let name = validatedName {
// It's already validated and saved return .saved((name, config))
return .saved(validatedConfiguration)
} }
fieldsWithError.removeAll() fieldsWithError.removeAll()
guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else { guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
fieldsWithError.insert(.name) fieldsWithError.insert(.name)
return .error("Interface name is required") return .error(tr("alertInvalidInterfaceMessageNameRequired"))
} }
guard let privateKeyString = scratchpad[.privateKey] else { guard let privateKeyString = scratchpad[.privateKey] else {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required") return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
} }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength 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(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
} }
var config = InterfaceConfiguration(name: name, privateKey: privateKey) var config = InterfaceConfiguration(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.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if let address = IPAddressRange(from: addressString) {
if let address = IPAddressRange(from: trimmedString) {
addresses.append(address) addresses.append(address)
} else { } else {
fieldsWithError.insert(.addresses) fieldsWithError.insert(.addresses)
errorMessages.append("Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation") errorMessages.append(tr("alertInvalidInterfaceMessageAddressInvalid"))
} }
} }
config.addresses = addresses config.addresses = addresses
@@ -133,7 +154,7 @@ class TunnelViewModel {
config.listenPort = listenPort config.listenPort = listenPort
} else { } else {
fieldsWithError.insert(.listenPort) fieldsWithError.insert(.listenPort)
errorMessages.append("Interface's listen port must be between 0 and 65535, or unspecified") errorMessages.append(tr("alertInvalidInterfaceMessageListenPortInvalid"))
} }
} }
if let mtuString = scratchpad[.mtu] { if let mtuString = scratchpad[.mtu] {
@@ -141,18 +162,17 @@ class TunnelViewModel {
config.mtu = mtu config.mtu = mtu
} else { } else {
fieldsWithError.insert(.mtu) fieldsWithError.insert(.mtu)
errorMessages.append("Interface's MTU must be between 576 and 65535, or unspecified") errorMessages.append(tr("alertInvalidInterfaceMessageMTUInvalid"))
} }
} }
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.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if let dnsServer = DNSServer(from: dnsServerString) {
if let dnsServer = DNSServer(from: trimmedString) {
dnsServers.append(dnsServer) dnsServers.append(dnsServer)
} else { } else {
fieldsWithError.insert(.dns) fieldsWithError.insert(.dns)
errorMessages.append("Interface's DNS servers must be a list of comma-separated IP addresses") errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
} }
} }
config.dns = dnsServers config.dns = dnsServers
@@ -161,7 +181,8 @@ class TunnelViewModel {
guard errorMessages.isEmpty else { return .error(errorMessages.first!) } guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config validatedConfiguration = config
return .saved(config) validatedName = name
return .saved((name, config))
} }
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] { func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
@@ -169,9 +190,8 @@ class TunnelViewModel {
if TunnelViewModel.interfaceFieldsWithControl.contains(field) { if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
return true return true
} }
return (!self[field].isEmpty) return !self[field].isEmpty
} }
// TODO: Cache this to avoid recomputing
} }
} }
@@ -181,8 +201,8 @@ class TunnelViewModel {
var fieldsWithError = Set<PeerField>() var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration? var validatedConfiguration: PeerConfiguration?
// For exclude private IPs
private(set) var shouldAllowExcludePrivateIPsControl = false private(set) var shouldAllowExcludePrivateIPsControl = false
private(set) var shouldStronglyRecommendDNS = false
private(set) var excludePrivateIPsValue = false private(set) var excludePrivateIPsValue = false
fileprivate var numberOfPeers = 0 fileprivate var numberOfPeers = 0
@@ -193,16 +213,12 @@ 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.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad() populateScratchpad()
} }
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.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad() populateScratchpad()
} }
validatedConfiguration = nil validatedConfiguration = nil
@@ -218,17 +234,16 @@ class TunnelViewModel {
} }
func populateScratchpad() { func populateScratchpad() {
// Populate the scratchpad from the configuration object
guard let config = validatedConfiguration else { return } guard let config = validatedConfiguration else { return }
scratchpad[.publicKey] = config.publicKey.base64EncodedString() scratchpad[.publicKey] = config.publicKey.base64EncodedString()
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 {
scratchpad[.endpoint] = endpoint.stringRepresentation() scratchpad[.endpoint] = endpoint.stringRepresentation
} }
if let persistentKeepAlive = config.persistentKeepAlive { if let persistentKeepAlive = config.persistentKeepAlive {
scratchpad[.persistentKeepAlive] = String(persistentKeepAlive) scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
@@ -239,17 +254,16 @@ class TunnelViewModel {
//swiftlint:disable:next cyclomatic_complexity //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
return .saved(validatedConfiguration) return .saved(validatedConfiguration)
} }
fieldsWithError.removeAll() fieldsWithError.removeAll()
guard let publicKeyString = scratchpad[.publicKey] else { guard let publicKeyString = scratchpad[.publicKey] else {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required") return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
} }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength 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(tr("alertInvalidPeerMessagePublicKeyInvalid"))
} }
var config = PeerConfiguration(publicKey: publicKey) var config = PeerConfiguration(publicKey: publicKey)
var errorMessages = [String]() var errorMessages = [String]()
@@ -258,18 +272,17 @@ class TunnelViewModel {
config.preSharedKey = preSharedKey config.preSharedKey = preSharedKey
} else { } else {
fieldsWithError.insert(.preSharedKey) fieldsWithError.insert(.preSharedKey)
errorMessages.append("Peer's preshared key must be a 32-byte key in base64 encoding") errorMessages.append(tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
} }
} }
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.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if let allowedIP = IPAddressRange(from: allowedIPString) {
if let allowedIP = IPAddressRange(from: trimmedString) {
allowedIPs.append(allowedIP) allowedIPs.append(allowedIP)
} else { } else {
fieldsWithError.insert(.allowedIPs) fieldsWithError.insert(.allowedIPs)
errorMessages.append("Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation") errorMessages.append(tr("alertInvalidPeerMessageAllowedIPsInvalid"))
} }
} }
config.allowedIPs = allowedIPs config.allowedIPs = allowedIPs
@@ -279,7 +292,7 @@ class TunnelViewModel {
config.endpoint = endpoint config.endpoint = endpoint
} else { } else {
fieldsWithError.insert(.endpoint) fieldsWithError.insert(.endpoint)
errorMessages.append("Peer's endpoint must be of the form 'host:port' or '[host]:port'") errorMessages.append(tr("alertInvalidPeerMessageEndpointInvalid"))
} }
} }
if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] { if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
@@ -287,7 +300,7 @@ class TunnelViewModel {
config.persistentKeepAlive = persistentKeepAlive config.persistentKeepAlive = persistentKeepAlive
} else { } else {
fieldsWithError.insert(.persistentKeepAlive) fieldsWithError.insert(.persistentKeepAlive)
errorMessages.append("Peer's persistent keepalive must be between 0 to 65535, or unspecified") errorMessages.append(tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
} }
} }
@@ -304,7 +317,6 @@ class TunnelViewModel {
} }
return (!self[field].isEmpty) return (!self[field].isEmpty)
} }
// TODO: Cache this to avoid recomputing
} }
static let ipv4DefaultRouteString = "0.0.0.0/0" static let ipv4DefaultRouteString = "0.0.0.0/0"
@@ -318,19 +330,16 @@ class TunnelViewModel {
] ]
func updateExcludePrivateIPsFieldState() { func updateExcludePrivateIPsFieldState() {
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
guard numberOfPeers == 1 else { guard numberOfPeers == 1 else {
shouldAllowExcludePrivateIPsControl = false shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false excludePrivateIPsValue = false
return return
} }
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(
(scratchpad[.allowedIPs] ?? "")
.split(separator: ",")
.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
@@ -344,12 +353,8 @@ class TunnelViewModel {
} }
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) { func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) {
let allowedIPStrings = (scratchpad[.allowedIPs] ?? "") let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
.split(separator: ",") let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let dnsServerStrings = dnsServers
.split(separator: ",")
.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 {
@@ -358,24 +363,25 @@ class TunnelViewModel {
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString] modifiedAllowedIPStrings = ipv6Addresses + [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
excludePrivateIPsValue = isOn excludePrivateIPsValue = isOn
} }
} }
enum SaveResult<Configuration> { enum SaveResult<Configuration> {
case saved(Configuration) case saved(Configuration)
case error(String) // TODO: Localize error messages case error(String)
} }
var interfaceData: InterfaceData var interfaceData: InterfaceData
var peersData: [PeerData] var peersData: [PeerData]
init(tunnelConfiguration: TunnelConfiguration?) { init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData: InterfaceData = InterfaceData() let interfaceData = InterfaceData()
var peersData = [PeerData]() var peersData = [PeerData]()
if let tunnelConfiguration = tunnelConfiguration { if let tunnelConfiguration = tunnelConfiguration {
interfaceData.validatedConfiguration = tunnelConfiguration.interface interfaceData.validatedConfiguration = tunnelConfiguration.interface
interfaceData.validatedName = tunnelConfiguration.name
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() { for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: index) let peerData = PeerData(index: index)
peerData.validatedConfiguration = peerConfiguration peerData.validatedConfiguration = peerConfiguration
@@ -414,10 +420,8 @@ class TunnelViewModel {
} }
func save() -> SaveResult<TunnelConfiguration> { func save() -> SaveResult<TunnelConfiguration> {
// Attempt to save the interface and all peers, so that all erroring fields are collected
let interfaceSaveResult = interfaceData.save() let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() } let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
// Collate the results
switch interfaceSaveResult { switch interfaceSaveResult {
case .error(let errorMessage): case .error(let errorMessage):
return .error(errorMessage) return .error(errorMessage)
@@ -436,28 +440,26 @@ 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(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
} }
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations) let tunnelConfiguration = TunnelConfiguration(name: interfaceConfiguration.0, interface: interfaceConfiguration.1, peers: peerConfigurations)
return .saved(tunnelConfiguration) return .saved(tunnelConfiguration)
} }
} }
} }
// MARK: Activate on demand
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 tr("tunnelOnDemandOptionOff")
case .useOnDemandOverWiFiOrCellular: case .useOnDemandOverWiFiOrCellular:
return "Wi-Fi or cellular" return tr("tunnelOnDemandOptionWiFiOrCellular")
case .useOnDemandOverWiFiOnly: case .useOnDemandOverWiFiOnly:
return "Wi-Fi only" return tr("tunnelOnDemandOptionWiFiOnly")
case .useOnDemandOverCellularOnly: case .useOnDemandOverCellularOnly:
return "Cellular only" return tr("tunnelOnDemandOptionCellularOnly")
} }
} }
@@ -476,5 +478,4 @@ extension TunnelViewModel {
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption { static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
return .useOnDemandOverWiFiOrCellular return .useOnDemandOverWiFiOrCellular
} }
} }
+3 -8
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
@@ -10,8 +10,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var mainVC: MainViewController? var mainVC: MainViewController?
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) Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
let window = UIWindow(frame: UIScreen.main.bounds) let window = UIWindow(frame: UIScreen.main.bounds)
@@ -39,8 +38,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
} }
// MARK: State restoration
extension AppDelegate { extension AppDelegate {
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true return true
@@ -50,9 +47,7 @@ extension AppDelegate {
return true return true
} }
func application(_ application: UIApplication, func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
viewControllerWithRestorationIdentifierPath identifierComponents: [String],
coder: NSCoder) -> UIViewController? {
guard let vcIdentifier = identifierComponents.last else { return nil } guard let vcIdentifier = identifierComponents.last else { return nil }
if vcIdentifier.hasPrefix("TunnelDetailVC:") { if vcIdentifier.hasPrefix("TunnelDetailVC:") {
let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count)) let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -10,42 +10,42 @@ class BorderedTextButton: UIView {
button.titleLabel?.adjustsFontForContentSizeCategory = true button.titleLabel?.adjustsFontForContentSizeCategory = true
return button return button
}() }()
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16) return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
} }
var title: String { var title: 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) }
} }
var onTapped: (() -> Void)? var onTapped: (() -> Void)?
init() { init() {
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
layer.borderWidth = 1 layer.borderWidth = 1
layer.cornerRadius = 5 layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor layer.borderColor = button.tintColor.cgColor
addSubview(button) addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: centerXAnchor), button.centerXAnchor.constraint(equalTo: centerXAnchor),
button.centerYAnchor.constraint(equalTo: centerYAnchor) button.centerYAnchor.constraint(equalTo: centerYAnchor)
]) ])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func buttonTapped() { @objc func buttonTapped() {
onTapped?() onTapped?()
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -13,39 +13,39 @@ class ButtonCell: UITableViewCell {
set(value) { button.tintColor = value ? .red : buttonStandardTintColor } set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
} }
var onTapped: (() -> Void)? var onTapped: (() -> Void)?
let button: UIButton = { let button: UIButton = {
let button = UIButton(type: .system) let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true button.titleLabel?.adjustsFontForContentSizeCategory = true
return button return button
}() }()
var buttonStandardTintColor: UIColor var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
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.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor), 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)
} }
@objc func buttonTapped() { @objc func buttonTapped() {
onTapped?() onTapped?()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
buttonText = "" buttonText = ""
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -13,16 +13,16 @@ class CheckmarkCell: UITableViewCell {
accessoryType = isChecked ? .checkmark : .none accessoryType = isChecked ? .checkmark : .none
} }
} }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
isChecked = false isChecked = false
super.init(style: .default, reuseIdentifier: reuseIdentifier) super.init(style: .default, reuseIdentifier: reuseIdentifier)
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
message = "" message = ""
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
class KeyValueCell: UITableViewCell { class KeyValueCell: UITableViewCell {
let keyLabel: UILabel = { let keyLabel: UILabel = {
let keyLabel = UILabel() let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body) keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
@@ -13,7 +13,7 @@ class KeyValueCell: UITableViewCell {
keyLabel.textAlignment = .left keyLabel.textAlignment = .left
return keyLabel return keyLabel
}() }()
let valueLabelScrollView: UIScrollView = { let valueLabelScrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero) let scrollView = UIScrollView(frame: .zero)
scrollView.isDirectionalLockEnabled = true scrollView.isDirectionalLockEnabled = true
@@ -21,7 +21,7 @@ class KeyValueCell: UITableViewCell {
scrollView.showsVerticalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false
return scrollView return scrollView
}() }()
let valueTextField: UITextField = { let valueTextField: UITextField = {
let valueTextField = UITextField() let valueTextField = UITextField()
valueTextField.textAlignment = .right valueTextField.textAlignment = .right
@@ -34,7 +34,7 @@ class KeyValueCell: UITableViewCell {
valueTextField.textColor = .gray valueTextField.textColor = .gray
return valueTextField return valueTextField
}() }()
var copyableGesture = true var copyableGesture = true
var key: String { var key: String {
@@ -53,7 +53,7 @@ class KeyValueCell: UITableViewCell {
get { return valueTextField.keyboardType } get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value } set(value) { valueTextField.keyboardType = value }
} }
var isValueValid = true { var isValueValid = true {
didSet { didSet {
if isValueValid { if isValueValid {
@@ -63,64 +63,64 @@ class KeyValueCell: UITableViewCell {
} }
} }
} }
var isStackedHorizontally = false var isStackedHorizontally = false
var isStackedVertically = false var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]() var contentSizeBasedConstraints = [NSLayoutConstraint]()
var onValueChanged: ((String) -> Void)? var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)? var onValueBeingEdited: ((String) -> Void)?
private var textFieldValueOnBeginEditing: String = "" private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel) contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false keyLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), keyLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]) ])
valueTextField.delegate = self valueTextField.delegate = self
valueLabelScrollView.addSubview(valueTextField) valueLabelScrollView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
valueTextField.leftAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leftAnchor), valueTextField.leadingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leadingAnchor),
valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor), valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor), valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
valueTextField.rightAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.rightAnchor), valueTextField.trailingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.trailingAnchor),
valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor) valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor)
]) ])
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0) let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1 expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true expandToFitValueLabelConstraint.isActive = true
contentView.addSubview(valueLabelScrollView) contentView.addSubview(valueLabelScrollView)
contentView.addSubview(valueLabelScrollView) contentView.addSubview(valueLabelScrollView)
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
valueLabelScrollView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), valueLabelScrollView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5) contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5)
]) ])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal) valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
addGestureRecognizer(gestureRecognizer) addGestureRecognizer(gestureRecognizer)
isUserInteractionEnabled = true isUserInteractionEnabled = true
configureForContentSize() configureForContentSize()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func configureForContentSize() { func configureForContentSize() {
var constraints = [NSLayoutConstraint]() var constraints = [NSLayoutConstraint]()
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory { if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
@@ -128,8 +128,8 @@ class KeyValueCell: UITableViewCell {
if !isStackedVertically { if !isStackedVertically {
constraints = [ constraints = [
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), valueLabelScrollView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor) keyLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor)
] ]
isStackedVertically = true isStackedVertically = true
isStackedHorizontally = false isStackedHorizontally = false
@@ -139,7 +139,7 @@ class KeyValueCell: UITableViewCell {
if !isStackedHorizontally { if !isStackedHorizontally {
constraints = [ constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5), contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1), valueLabelScrollView.leadingAnchor.constraint(equalToSystemSpacingAfter: keyLabel.trailingAnchor, multiplier: 1),
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5) valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
] ]
isStackedHorizontally = true isStackedHorizontally = true
@@ -152,13 +152,13 @@ class KeyValueCell: UITableViewCell {
contentSizeBasedConstraints = constraints contentSizeBasedConstraints = constraints
} }
} }
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) { @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !copyableGesture { if !copyableGesture {
return return
} }
guard recognizer.state == .recognized else { return } guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view, if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() { let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared let menuController = UIMenuController.shared
@@ -166,19 +166,19 @@ class KeyValueCell: UITableViewCell {
menuController.setMenuVisible(true, animated: true) menuController.setMenuVisible(true, animated: true)
} }
} }
override var canBecomeFirstResponder: Bool { override var canBecomeFirstResponder: Bool {
return true return true
} }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:))) return (action == #selector(UIResponderStandardEditActions.copy(_:)))
} }
override func copy(_ sender: Any?) { override func copy(_ sender: Any?) {
UIPasteboard.general.string = valueTextField.text UIPasteboard.general.string = valueTextField.text
} }
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
copyableGesture = true copyableGesture = true
@@ -194,18 +194,18 @@ class KeyValueCell: UITableViewCell {
} }
extension KeyValueCell: UITextFieldDelegate { extension KeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) { func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? "" textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true isValueValid = true
} }
func textFieldDidEndEditing(_ textField: UITextField) { func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
guard isModified else { return } guard isModified else { return }
onValueChanged?(textField.text ?? "") onValueChanged?(textField.text ?? "")
} }
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited { if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
@@ -213,5 +213,5 @@ extension KeyValueCell: UITextFieldDelegate {
} }
return true return true
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -19,14 +19,14 @@ class SwitchCell: UITableViewCell {
textLabel?.textColor = value ? .black : .gray textLabel?.textColor = value ? .black : .gray
} }
} }
var onSwitchToggled: ((Bool) -> Void)? var onSwitchToggled: ((Bool) -> Void)?
let switchView = UISwitch() let switchView = UISwitch()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier) super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged) switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
} }
@@ -34,13 +34,14 @@ class SwitchCell: UITableViewCell {
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func switchToggled() { @objc func switchToggled() {
onSwitchToggled?(switchView.isOn) onSwitchToggled?(switchView.isOn)
} }
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
onSwitchToggled = nil
isEnabled = true isEnabled = true
message = "" message = ""
isOn = false isOn = false
@@ -1,43 +1,48 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
class TunnelEditKeyValueCell: KeyValueCell { class TunnelEditKeyValueCell: KeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textAlignment = .right keyLabel.textAlignment = .right
valueTextField.textAlignment = .left valueTextField.textAlignment = .left
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0) 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, // In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority. // set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1 widthRatioConstraint.priority = .defaultHigh + 1
widthRatioConstraint.isActive = true widthRatioConstraint.isActive = true
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
} }
class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell { class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
copyableGesture = false copyableGesture = false
valueTextField.textColor = .black valueTextField.textColor = .black
valueTextField.isEnabled = true valueTextField.isEnabled = true
valueLabelScrollView.isScrollEnabled = false valueLabelScrollView.isScrollEnabled = false
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func prepareForReuse() {
super.prepareForReuse()
copyableGesture = false
}
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -8,18 +8,18 @@ class TunnelListCell: UITableViewCell {
didSet(value) { didSet(value) {
// Bind to the tunnel's name // Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? "" nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name self?.nameLabel.text = tunnel.name
} }
// Bind to the tunnel's status // Bind to the tunnel's status
update(from: tunnel?.status) update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status) self?.update(from: tunnel.status)
} }
} }
} }
var onSwitchToggled: ((Bool) -> Void)? var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel = { let nameLabel: UILabel = {
let nameLabel = UILabel() let nameLabel = UILabel()
nameLabel.font = UIFont.preferredFont(forTextStyle: .body) nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
@@ -27,35 +27,35 @@ class TunnelListCell: UITableViewCell {
nameLabel.numberOfLines = 0 nameLabel.numberOfLines = 0
return nameLabel return nameLabel
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true busyIndicator.hidesWhenStopped = true
return busyIndicator return busyIndicator
}() }()
let statusSwitch = UISwitch() let statusSwitch = UISwitch()
private var statusObservervationToken: AnyObject? private var statusObservationToken: AnyObject?
private var nameObservervationToken: AnyObject? private var nameObservationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch) contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor) contentView.trailingAnchor.constraint(equalTo: statusSwitch.trailingAnchor)
]) ])
contentView.addSubview(busyIndicator) contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1) statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1)
]) ])
contentView.addSubview(nameLabel) contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@@ -63,20 +63,20 @@ class TunnelListCell: UITableViewCell {
bottomAnchorConstraint.priority = .defaultLow bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1), nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1), nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1), busyIndicator.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1),
bottomAnchorConstraint bottomAnchorConstraint
]) ])
accessoryType = .disclosureIndicator accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged) statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
} }
@objc func switchToggled() { @objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn) onSwitchToggled?(statusSwitch.isOn)
} }
private func update(from status: TunnelStatus?) { private func update(from status: TunnelStatus?) {
guard let status = status else { guard let status = status else {
reset() reset()
@@ -93,17 +93,17 @@ class TunnelListCell: UITableViewCell {
} }
} }
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private func reset() { private func reset() {
statusSwitch.isOn = false statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating() busyIndicator.stopAnimating()
} }
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
reset() reset()
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -7,8 +7,8 @@ class MainViewController: UISplitViewController {
var tunnelsManager: TunnelsManager? var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)? var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController? var tunnelsListVC: TunnelsListTableViewController?
private var foregroundObservationToken: AnyObject?
init() { init() {
let detailVC = UIViewController() let detailVC = UIViewController()
@@ -57,7 +57,31 @@ class MainViewController: UISplitViewController {
self.onTunnelsManagerReady?(tunnelsManager) self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil self.onTunnelsManagerReady = nil
} }
foregroundObservationToken = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
self.tunnelsManager?.reload { [weak self] hasChanges in
guard let self = self, let tunnelsManager = self.tunnelsManager, hasChanges else { return }
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
if self.isCollapsed {
(self.viewControllers[0] as? UINavigationController)?.popViewController(animated: false)
} else {
let detailVC = UIViewController()
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
self.showDetailViewController(detailNC, sender: self)
}
if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
}
} }
} }
extension MainViewController: TunnelsManagerActivationDelegate { extension MainViewController: TunnelsManagerActivationDelegate {
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import AVFoundation import AVFoundation
import UIKit import UIKit
@@ -17,11 +17,11 @@ class QRScanViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Scan QR code" title = tr("scanQRCodeViewTitle")
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 = tr("scanQRCodeTipText")
tipLabel.adjustsFontSizeToFitWidth = true tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = .lightGray tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center tipLabel.textAlignment = .center
@@ -29,8 +29,8 @@ class QRScanViewController: UIViewController {
view.addSubview(tipLabel) view.addSubview(tipLabel)
tipLabel.translatesAutoresizingMaskIntoConstraints = false tipLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), tipLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), tipLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32) tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
]) ])
@@ -39,7 +39,7 @@ class QRScanViewController: UIViewController {
let captureSession = captureSession, let captureSession = captureSession,
captureSession.canAddInput(videoInput), captureSession.canAddInput(videoInput),
captureSession.canAddOutput(metadataOutput) else { captureSession.canAddOutput(metadataOutput) else {
scanDidEncounterError(title: "Camera Unsupported", message: "This device is not able to scan QR codes") scanDidEncounterError(title: tr("alertScanQRCodeCameraUnsupportedTitle"), message: tr("alertScanQRCodeCameraUnsupportedMessage"))
return return
} }
@@ -76,15 +76,11 @@ class QRScanViewController: UIViewController {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
if let connection = previewLayer?.connection { if let connection = previewLayer?.connection {
let currentDevice = UIDevice.current
let currentDevice: UIDevice = UIDevice.current let orientation = currentDevice.orientation
let previewLayerConnection = connection
let orientation: UIDeviceOrientation = currentDevice.orientation
let previewLayerConnection: AVCaptureConnection = connection
if previewLayerConnection.isVideoOrientationSupported { if previewLayerConnection.isVideoOrientationSupported {
switch orientation { switch orientation {
case .portrait: case .portrait:
previewLayerConnection.videoOrientation = .portrait previewLayerConnection.videoOrientation = .portrait
@@ -105,20 +101,20 @@ class QRScanViewController: UIViewController {
} }
func scanDidComplete(withCode code: String) { func scanDidComplete(withCode code: String) {
let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned") let scannedTunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: code, called: "Scanned")
guard let tunnelConfiguration = scannedTunnelConfiguration else { guard let tunnelConfiguration = scannedTunnelConfiguration else {
scanDidEncounterError(title: "Invalid QR Code", message: "The scanned QR code is not a valid WireGuard configuration") scanDidEncounterError(title: tr("alertScanQRCodeInvalidQRCodeTitle"), message: tr("alertScanQRCodeInvalidQRCodeMessage"))
return return
} }
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert) let alert = UIAlertController(title: tr("alertScanQRCodeNamePromptTitle"), message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: nil) alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { [weak self] _ in alert.addAction(UIAlertAction(title: tr("actionCancel"), 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) { [weak self] _ in alert.addAction(UIAlertAction(title: tr("actionSave"), style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return } guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
tunnelConfiguration.interface.name = title tunnelConfiguration.name = title
if let self = self { if let self = self {
self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) { self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
self.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
@@ -130,7 +126,7 @@ class QRScanViewController: UIViewController {
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) { [weak self] _ in alertController.addAction(UIAlertAction(title: tr("actionOK"), 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)
@@ -149,7 +145,7 @@ extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate {
guard let metadataObject = metadataObjects.first, guard let metadataObject = metadataObjects.first,
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else { let stringValue = readableObject.stringValue else {
scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read") scanDidEncounterError(title: tr("alertScanQRCodeUnreadableQRCodeTitle"), message: tr("alertScanQRCodeUnreadableQRCodeMessage"))
return return
} }
@@ -1,16 +1,25 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
class SettingsTableViewController: UITableViewController { class SettingsTableViewController: UITableViewController {
enum SettingsFields: String { enum SettingsFields {
case iosAppVersion = "WireGuard for iOS" case iosAppVersion
case goBackendVersion = "WireGuard Go Backend" case goBackendVersion
case exportZipArchive = "Export zip archive" case exportZipArchive
case exportLogFile = "Export log file" case exportLogFile
var localizedUIString: String {
switch self {
case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
case .exportZipArchive: return tr("settingsExportZipButtonTitle")
case .exportLogFile: return tr("settingsExportLogFileButtonTitle")
}
}
} }
let settingsFieldsBySection: [[SettingsFields]] = [ let settingsFieldsBySection: [[SettingsFields]] = [
@@ -33,7 +42,7 @@ class SettingsTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Settings" title = tr("settingsViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
tableView.estimatedRowHeight = 44 tableView.estimatedRowHeight = 44
@@ -49,12 +58,12 @@ class SettingsTableViewController: UITableViewController {
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
guard let logo = tableView.tableFooterView else { return } guard let logo = tableView.tableFooterView else { return }
let bottomPadding = max(tableView.layoutMargins.bottom, 10) let bottomPadding = max(tableView.layoutMargins.bottom, 10)
let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding) let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding)
let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height
var height = tableView.estimatedRowHeight * 1.5 var height = tableView.estimatedRowHeight * 1.5
var width = height * imageAspectRatio var width = height * imageAspectRatio
let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20) let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20)
@@ -62,11 +71,11 @@ class SettingsTableViewController: UITableViewController {
width = maxWidth width = maxWidth
height = width / imageAspectRatio height = width / imageAspectRatio
} }
let needsReload = height != logo.frame.height let needsReload = height != logo.frame.height
logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height) logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height)
if needsReload { if needsReload {
tableView.tableFooterView = logo tableView.tableFooterView = logo
} }
@@ -84,7 +93,7 @@ class SettingsTableViewController: UITableViewController {
_ = FileManager.deleteFile(at: destinationURL) _ = FileManager.deleteFile(at: destinationURL)
let count = tunnelsManager.numberOfTunnels() let count = tunnelsManager.numberOfTunnels()
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() } let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
@@ -109,25 +118,24 @@ class SettingsTableViewController: UITableViewController {
if FileManager.default.fileExists(atPath: destinationURL.path) { if FileManager.default.fileExists(atPath: destinationURL.path) {
let isDeleted = FileManager.deleteFile(at: destinationURL) let isDeleted = FileManager.deleteFile(at: destinationURL)
if !isDeleted { if !isDeleted {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
return return
} }
} }
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
return return
} }
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else { guard isWritten else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
// popoverPresentationController shall be non-nil on the iPad
activityVC.popoverPresentationController?.sourceView = sourceView activityVC.popoverPresentationController?.sourceView = sourceView
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
activityVC.completionWithItemsHandler = { _, _, _, _ in activityVC.completionWithItemsHandler = { _, _, _, _ in
@@ -140,8 +148,6 @@ class SettingsTableViewController: UITableViewController {
} }
} }
// MARK: UITableViewDataSource
extension SettingsTableViewController { extension SettingsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
return settingsFieldsBySection.count return settingsFieldsBySection.count
@@ -154,11 +160,11 @@ extension SettingsTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section { switch section {
case 0: case 0:
return "About" return tr("settingsSectionTitleAbout")
case 1: case 1:
return "Export configurations" return tr("settingsSectionTitleExportConfigurations")
case 2: case 2:
return "Tunnel log" return tr("settingsSectionTitleTunnelLog")
default: default:
return nil return nil
} }
@@ -169,7 +175,7 @@ extension SettingsTableViewController {
if field == .iosAppVersion || field == .goBackendVersion { if field == .iosAppVersion || field == .goBackendVersion {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.copyableGesture = false cell.copyableGesture = false
cell.key = field.rawValue cell.key = field.localizedUIString
if field == .iosAppVersion { if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
@@ -182,7 +188,7 @@ extension SettingsTableViewController {
return cell return cell
} else if field == .exportZipArchive { } else if field == .exportZipArchive {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
} }
@@ -190,7 +196,7 @@ extension SettingsTableViewController {
} else { } else {
assert(field == .exportLogFile) assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button) self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
} }
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController { class TunnelDetailTableViewController: UITableViewController {
private enum Section { private enum Section {
@@ -29,13 +27,13 @@ class TunnelDetailTableViewController: UITableViewController {
let tunnel: TunnelContainer let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel var tunnelViewModel: TunnelViewModel
private var sections = [Section]() private var sections = [Section]()
private var onDemandStatusObservervationToken: AnyObject? private var onDemandStatusObservationToken: AnyObject?
private var statusObservervationToken: AnyObject? private var statusObservationToken: AnyObject?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
} }
@@ -43,11 +41,6 @@ class TunnelDetailTableViewController: UITableViewController {
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
onDemandStatusObservervationToken = nil
statusObservervationToken = nil
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@@ -61,7 +54,6 @@ class TunnelDetailTableViewController: UITableViewController {
tableView.register(KeyValueCell.self) tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self) tableView.register(ButtonCell.self)
// State restoration
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
} }
@@ -86,12 +78,11 @@ class TunnelDetailTableViewController: UITableViewController {
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: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction) alert.addAction(destroyAction)
alert.addAction(cancelAction) alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds alert.popoverPresentationController?.sourceRect = sourceView.bounds
@@ -99,13 +90,12 @@ class TunnelDetailTableViewController: UITableViewController {
} }
} }
// MARK: TunnelEditTableViewControllerDelegate
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() loadSections()
title = tunnel.name title = tunnel.name
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
tableView.reloadData() tableView.reloadData()
} }
func tunnelEditingCancelled() { func tunnelEditingCancelled() {
@@ -113,8 +103,6 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
} }
} }
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController { extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count return sections.count
@@ -138,13 +126,13 @@ extension TunnelDetailTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] { switch sections[section] {
case .status: case .status:
return "Status" return tr("tunnelSectionTitleStatus")
case .interface: case .interface:
return "Interface" return tr("tunnelSectionTitleInterface")
case .peer: case .peer:
return "Peer" return tr("tunnelSectionTitlePeer")
case .onDemand: case .onDemand:
return "On-Demand Activation" return tr("tunnelSectionTitleOnDemand")
case .delete: case .delete:
return nil return nil
} }
@@ -167,24 +155,24 @@ extension TunnelDetailTableViewController {
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
let text: String let text: String
switch status { switch status {
case .inactive: case .inactive:
text = "Inactive" text = tr("tunnelStatusInactive")
case .activating: case .activating:
text = "Activating" text = tr("tunnelStatusActivating")
case .active: case .active:
text = "Active" text = tr("tunnelStatusActive")
case .deactivating: case .deactivating:
text = "Deactivating" text = tr("tunnelStatusDeactivating")
case .reasserting: case .reasserting:
text = "Reactivating" text = tr("tunnelStatusReasserting")
case .restarting: case .restarting:
text = "Restarting" text = tr("tunnelStatusRestarting")
case .waiting: case .waiting:
text = "Waiting" text = tr("tunnelStatusWaiting")
} }
cell.textLabel?.text = text cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
@@ -193,13 +181,13 @@ extension TunnelDetailTableViewController {
} }
cell.isEnabled = status == .active || status == .inactive cell.isEnabled = status == .active || status == .inactive
} }
statusUpdate(cell, tunnel.status) statusUpdate(cell, tunnel.status)
statusObservervationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return } guard let cell = cell else { return }
statusUpdate(cell, tunnel.status) statusUpdate(cell, tunnel.status)
} }
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return } guard let self = self else { return }
if isOn { if isOn {
@@ -214,7 +202,7 @@ extension TunnelDetailTableViewController {
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row] let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = tunnelViewModel.interfaceData[field] cell.value = tunnelViewModel.interfaceData[field]
return cell return cell
} }
@@ -222,36 +210,43 @@ extension TunnelDetailTableViewController {
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell { private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row] let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = peerData[field] cell.value = peerData[field]
return cell return cell
} }
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = "Activate on demand" cell.key = tr("tunnelOnDemandKey")
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
onDemandStatusObservervationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
} }
return cell return cell
} }
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel" cell.buttonText = tr("deleteTunnelButtonTitle")
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in
guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return } guard let self = self else { return }
tunnelsManager.remove(tunnel: tunnel) { error in self.tunnelsManager.remove(tunnel: self.tunnel) { error in
if error != nil { if error != nil {
print("Error removing tunnel: \(String(describing: error))") print("Error removing tunnel: \(String(describing: error))")
return return
} }
} }
self?.navigationController?.navigationController?.popToRootViewController(animated: true) if self.splitViewController?.isCollapsed != false {
self.navigationController?.navigationController?.popToRootViewController(animated: true)
} else {
let detailVC = UIViewController()
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
self.showDetailViewController(detailNC, sender: self)
}
} }
} }
return cell return cell
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -8,15 +8,25 @@ protocol TunnelEditTableViewControllerDelegate: class {
func tunnelEditingCancelled() func tunnelEditingCancelled()
} }
// MARK: TunnelEditTableViewController
class TunnelEditTableViewController: UITableViewController { class TunnelEditTableViewController: UITableViewController {
private enum Section { private enum Section {
case interface case interface
case peer(_ peer: TunnelViewModel.PeerData) case peer(_ peer: TunnelViewModel.PeerData)
case addPeer case addPeer
case onDemand case onDemand
static func == (lhs: Section, rhs: Section) -> Bool {
switch (lhs, rhs) {
case (.interface, .interface),
(.addPeer, .addPeer),
(.onDemand, .onDemand):
return true
case let (.peer(peerA), .peer(peerB)):
return peerA.index == peerB.index
default:
return false
}
}
} }
weak var delegate: TunnelEditTableViewControllerDelegate? weak var delegate: TunnelEditTableViewControllerDelegate?
@@ -45,22 +55,21 @@ class TunnelEditTableViewController: UITableViewController {
var activateOnDemandSetting: ActivateOnDemandSetting var activateOnDemandSetting: ActivateOnDemandSetting
private var sections = [Section]() private var sections = [Section]()
// Use this initializer to edit an existing tunnel.
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration()) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
activateOnDemandSetting = tunnel.activateOnDemandSetting() activateOnDemandSetting = tunnel.activateOnDemandSetting
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
} }
init(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) { // Use this initializer to create a new tunnel.
// Use this initializer to create a new tunnel. init(tunnelsManager: TunnelsManager) {
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnel = nil tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
@@ -72,7 +81,7 @@ class TunnelEditTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = tunnel == nil ? "New configuration" : "Edit configuration" title = tunnel == nil ? tr("newTunnelViewTitle") : tr("editTunnelViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
@@ -99,15 +108,14 @@ class TunnelEditTableViewController: UITableViewController {
let tunnelSaveResult = tunnelViewModel.save() let tunnelSaveResult = tunnelViewModel.save()
switch tunnelSaveResult { switch tunnelSaveResult {
case .error(let errorMessage): case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer" let alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil || tunnelViewModel.interfaceData.validatedName == nil) ?
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self) tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle")
ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
tableView.reloadData() // Highlight erroring fields tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration): case .saved(let tunnelConfiguration):
if let tunnel = tunnel { if let tunnel = tunnel {
// We're modifying an existing tunnel // We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel, tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { } else {
@@ -117,8 +125,7 @@ class TunnelEditTableViewController: UITableViewController {
} }
} else { } else {
// We're adding a new tunnel // We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
if let error = result.error { if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { } else {
@@ -165,13 +172,13 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] { switch sections[section] {
case .interface: case .interface:
return section == 0 ? "Interface" : nil return section == 0 ? tr("tunnelSectionTitleInterface") : nil
case .peer: case .peer:
return "Peer" return tr("tunnelSectionTitlePeer")
case .addPeer: case .addPeer:
return nil return nil
case .onDemand: case .onDemand:
return "On-Demand Activation" return tr("tunnelSectionTitleOnDemand")
} }
} }
@@ -202,7 +209,7 @@ extension TunnelEditTableViewController {
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@@ -219,24 +226,27 @@ extension TunnelEditTableViewController {
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = tunnelViewModel.interfaceData[field] cell.value = tunnelViewModel.interfaceData[field]
return cell return cell
} }
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
switch field { switch field {
case .name, .privateKey: case .name, .privateKey:
cell.placeholderText = "Required" cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default cell.keyboardType = .default
case .addresses, .dns: case .addresses:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextStronglyRecommended")
cell.keyboardType = .numbersAndPunctuation
case .dns:
cell.placeholderText = tunnelViewModel.peersData.contains(where: { return $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation cell.keyboardType = .numbersAndPunctuation
case .listenPort, .mtu: case .listenPort, .mtu:
cell.placeholderText = "Automatic" cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
cell.keyboardType = .numberPad cell.keyboardType = .numberPad
case .publicKey, .generateKeyPair: case .publicKey, .generateKeyPair:
cell.keyboardType = .default cell.keyboardType = .default
@@ -284,12 +294,11 @@ extension TunnelEditTableViewController {
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return } guard let self = self, let peerData = peerData else { return }
guard let self = self else { return } self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), from: cell) { [weak self] in
self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let removedSectionIndices = self.deletePeer(peer: peerData) let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
@@ -310,7 +319,7 @@ extension TunnelEditTableViewController {
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue cell.message = field.localizedUIString
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
@@ -325,20 +334,20 @@ extension TunnelEditTableViewController {
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
switch field { switch field {
case .publicKey: case .publicKey:
cell.placeholderText = "Required" cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default cell.keyboardType = .default
case .preSharedKey, .endpoint: case .preSharedKey, .endpoint:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .default cell.keyboardType = .default
case .allowedIPs: case .allowedIPs:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation cell.keyboardType = .numbersAndPunctuation
case .persistentKeepAlive: case .persistentKeepAlive:
cell.placeholderText = "Off" cell.placeholderText = tr("tunnelEditPlaceholderTextOff")
cell.keyboardType = .numberPad cell.keyboardType = .numberPad
case .excludePrivateIPs, .deletePeer: case .excludePrivateIPs, .deletePeer:
cell.keyboardType = .default cell.keyboardType = .default
@@ -348,20 +357,24 @@ extension TunnelEditTableViewController {
cell.value = peerData[field] cell.value = peerData[field]
if field == .allowedIPs { if field == .allowedIPs {
let firstInterfaceSection = sections.firstIndex(where: { $0 == .interface })!
let interfaceSubSection = interfaceFieldsBySection.firstIndex(where: { $0.contains(.dns) })!
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex(where: { $0 == .dns })!
cell.onValueBeingEdited = { [weak self, weak peerData] value in cell.onValueBeingEdited = { [weak self, weak peerData] value in
guard let self = self, let peerData = peerData else { return } guard let self = self, let peerData = peerData else { return }
let oldValue = peerData.shouldAllowExcludePrivateIPsControl let oldValue = peerData.shouldAllowExcludePrivateIPsControl
peerData[.allowedIPs] = value peerData[.allowedIPs] = value
if oldValue != peerData.shouldAllowExcludePrivateIPsControl { if oldValue != peerData.shouldAllowExcludePrivateIPsControl, let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) { if peerData.shouldAllowExcludePrivateIPsControl {
if peerData.shouldAllowExcludePrivateIPsControl { self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade) } else {
} else { self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
}
} }
} }
tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
} }
} else { } else {
cell.onValueChanged = { [weak peerData] value in cell.onValueChanged = { [weak peerData] value in
@@ -374,7 +387,7 @@ extension TunnelEditTableViewController {
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer" cell.buttonText = tr("addPeerButtonTitle")
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
@@ -395,23 +408,23 @@ extension TunnelEditTableViewController {
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 { if indexPath.row == 0 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand" cell.message = tr("tunnelOnDemandKey")
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return } guard let self = self else { return }
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return } guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return }
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) } self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn
self.loadSections()
let section = self.sections.firstIndex(where: { $0 == .onDemand })!
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) }
if isOn { if isOn {
self.activateOnDemandSetting.isActivateOnDemandEnabled = true
if self.activateOnDemandSetting.activateOnDemandOption == .none { if self.activateOnDemandSetting.activateOnDemandOption == .none {
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption() self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
} }
self.loadSections()
self.tableView.insertRows(at: indexPaths, with: .fade) self.tableView.insertRows(at: indexPaths, with: .fade)
} else { } else {
self.activateOnDemandSetting.isActivateOnDemandEnabled = false
self.loadSections()
self.tableView.deleteRows(at: indexPaths, with: .fade) self.tableView.deleteRows(at: indexPaths, with: .fade)
} }
} }
@@ -422,7 +435,7 @@ extension TunnelEditTableViewController {
let selectedOption = activateOnDemandSetting.activateOnDemandOption let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none) assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption) cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = (selectedOption == rowOption) cell.isChecked = selectedOption == rowOption
return cell return cell
} }
} }
@@ -444,12 +457,11 @@ extension TunnelEditTableViewController {
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: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction) alert.addAction(destroyAction)
alert.addAction(cancelAction) alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds alert.popoverPresentationController?.sourceRect = sourceView.bounds
@@ -457,8 +469,6 @@ extension TunnelEditTableViewController {
} }
} }
// MARK: UITableViewDelegate
extension TunnelEditTableViewController { extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section], indexPath.row > 0 { if case .onDemand = sections[indexPath.section], indexPath.row > 0 {
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import MobileCoreServices import MobileCoreServices
@@ -17,32 +17,32 @@ class TunnelsListTableViewController: UIViewController {
tableView.register(TunnelListCell.self) tableView.register(TunnelListCell.self)
return tableView return tableView
}() }()
let centeredAddButton: BorderedTextButton = { let centeredAddButton: BorderedTextButton = {
let button = BorderedTextButton() let button = BorderedTextButton()
button.title = "Add a tunnel" button.title = tr("tunnelsListCenteredAddTunnelButtonTitle")
button.isHidden = true button.isHidden = true
return button return button
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true busyIndicator.hidesWhenStopped = true
return busyIndicator return busyIndicator
}() }()
override func loadView() { override func loadView() {
view = UIView() view = UIView()
view.backgroundColor = .white view.backgroundColor = .white
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
view.addSubview(tableView) view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]) ])
@@ -53,28 +53,28 @@ class TunnelsListTableViewController: UIViewController {
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]) ])
view.addSubview(centeredAddButton) view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor) centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]) ])
centeredAddButton.onTapped = { [weak self] in centeredAddButton.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.addButtonTapped(sender: self.centeredAddButton) self.addButtonTapped(sender: self.centeredAddButton)
} }
busyIndicator.startAnimating() busyIndicator.startAnimating()
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "WireGuard" title = tr("tunnelsListTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:))) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:))) navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC" restorationIdentifier = "TunnelsListVC"
} }
@@ -82,7 +82,7 @@ class TunnelsListTableViewController: UIViewController {
func setTunnelsManager(tunnelsManager: TunnelsManager) { func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self tunnelsManager.tunnelsListDelegate = self
busyIndicator.stopAnimating() busyIndicator.stopAnimating()
tableView.reloadData() tableView.reloadData()
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0 centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
@@ -96,29 +96,28 @@ class TunnelsListTableViewController: UIViewController {
@objc func addButtonTapped(sender: AnyObject) { @objc func addButtonTapped(sender: AnyObject) {
guard tunnelsManager != nil else { return } guard tunnelsManager != nil else { return }
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in
self?.presentViewControllerForFileImport() self?.presentViewControllerForFileImport()
} }
alert.addAction(importFileAction) alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in
self?.presentViewControllerForScanningQRCode() self?.presentViewControllerForScanningQRCode()
} }
alert.addAction(scanQRCodeAction) alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in
if let self = self, let tunnelsManager = self.tunnelsManager { if let self = self, let tunnelsManager = self.tunnelsManager {
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil) self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager)
} }
} }
alert.addAction(createFromScratchAction) alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
alert.addAction(cancelAction) alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
if let sender = sender as? UIBarButtonItem { if let sender = sender as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sender alert.popoverPresentationController?.barButtonItem = sender
} else if let sender = sender as? UIView { } else if let sender = sender as? UIView {
@@ -128,17 +127,17 @@ class TunnelsListTableViewController: UIViewController {
present(alert, animated: true, completion: nil) present(alert, animated: true, completion: nil)
} }
@objc func settingsButtonTapped(sender: UIBarButtonItem!) { @objc func settingsButtonTapped(sender: UIBarButtonItem) {
guard tunnelsManager != nil else { return } guard tunnelsManager != nil else { return }
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager) let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
let settingsNC = UINavigationController(rootViewController: settingsVC) let settingsNC = UINavigationController(rootViewController: settingsVC)
settingsNC.modalPresentationStyle = .formSheet settingsNC.modalPresentationStyle = .formSheet
present(settingsNC, animated: true) present(settingsNC, animated: true)
} }
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) { func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration) let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true) present(editNC, animated: true)
@@ -173,15 +172,15 @@ class TunnelsListTableViewController: UIViewController {
completionHandler?() completionHandler?()
return return
} }
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels", let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful)
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive", let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count)
from: self, onPresented: completionHandler) ErrorPresenter.showErrorAlert(title: title, message: message, from: self, onPresented: completionHandler)
} }
} }
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ { } else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url), if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) { let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName) {
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
if let error = result.error { if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler) ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
@@ -190,16 +189,13 @@ class TunnelsListTableViewController: UIViewController {
} }
} }
} else { } else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel", ErrorPresenter.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"),
message: "An error occured when importing the tunnel configuration.",
from: self, onPresented: completionHandler) from: self, onPresented: completionHandler)
} }
} }
} }
} }
// MARK: UIDocumentPickerDelegate
extension TunnelsListTableViewController: UIDocumentPickerDelegate { extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach { urls.forEach {
@@ -208,8 +204,6 @@ extension TunnelsListTableViewController: UIDocumentPickerDelegate {
} }
} }
// MARK: QRScanViewControllerDelegate
extension TunnelsListTableViewController: QRScanViewControllerDelegate { extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) { completionHandler: (() -> Void)?) {
@@ -223,8 +217,6 @@ extension TunnelsListTableViewController: QRScanViewControllerDelegate {
} }
} }
// MARK: UITableViewDataSource
extension TunnelsListTableViewController: UITableViewDataSource { extension TunnelsListTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { func numberOfSections(in tableView: UITableView) -> Int {
return 1 return 1
@@ -252,8 +244,6 @@ extension TunnelsListTableViewController: UITableViewDataSource {
} }
} }
// MARK: UITableViewDelegate
extension TunnelsListTableViewController: UITableViewDelegate { extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
@@ -267,7 +257,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in
guard let tunnelsManager = self?.tunnelsManager else { return } guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel) { error in tunnelsManager.remove(tunnel: tunnel) { error in
@@ -283,8 +273,6 @@ extension TunnelsListTableViewController: UITableViewDelegate {
} }
} }
// MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerListDelegate { extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) { func tunnelAdded(at index: Int) {
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic) tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+2 -1
View File
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error { protocol WireGuardAppError: Error {
typealias AlertText = (title: String, message: String) typealias AlertText = (title: String, message: String)
var alertText: AlertText { get } var alertText: AlertText { get }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> { enum WireGuardResult<T> {
case success(_ value: T) case success(_ value: T)
-54
View File
@@ -68,10 +68,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#ifndef NOUNCRYPT
#define NOUNCRYPT
#endif
#include "zlib.h" #include "zlib.h"
#include "unzip.h" #include "unzip.h"
@@ -185,18 +181,8 @@ typedef struct
int encrypted; int encrypted;
int isZip64; int isZip64;
# ifndef NOUNCRYPT
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
const z_crc_t* pcrc_32_tab;
# endif
} unz64_s; } unz64_s;
#ifndef NOUNCRYPT
#include "crypt.h"
#endif
/* =========================================================================== /* ===========================================================================
Read a byte from a gz_stream; update next_in and avail_in. Return EOF Read a byte from a gz_stream; update next_in and avail_in. Return EOF
for end of file. for end of file.
@@ -1478,12 +1464,8 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
file_in_zip64_read_info_s* pfile_in_zip_read_info; file_in_zip64_read_info_s* pfile_in_zip_read_info;
ZPOS64_T offset_local_extrafield; /* offset of the local extra field */ ZPOS64_T offset_local_extrafield; /* offset of the local extra field */
uInt size_local_extrafield; /* size of the local extra field */ uInt size_local_extrafield; /* size of the local extra field */
# ifndef NOUNCRYPT
char source[12];
# else
if (password != NULL) if (password != NULL)
return UNZ_PARAMERROR; return UNZ_PARAMERROR;
# endif
if (file==NULL) if (file==NULL)
return UNZ_PARAMERROR; return UNZ_PARAMERROR;
@@ -1612,29 +1594,6 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
s->pfile_in_zip_read = pfile_in_zip_read_info; s->pfile_in_zip_read = pfile_in_zip_read_info;
s->encrypted = 0; s->encrypted = 0;
# ifndef NOUNCRYPT
if (password != NULL)
{
int i;
s->pcrc_32_tab = get_crc_table();
init_keys(password,s->keys,s->pcrc_32_tab);
if (ZSEEK64(s->z_filefunc, s->filestream,
s->pfile_in_zip_read->pos_in_zipfile +
s->pfile_in_zip_read->byte_before_the_zipfile,
SEEK_SET)!=0)
return UNZ_INTERNALERROR;
if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12)
return UNZ_INTERNALERROR;
for (i = 0; i<12; i++)
zdecode(s->keys,s->pcrc_32_tab,source[i]);
s->pfile_in_zip_read->pos_in_zipfile+=12;
s->encrypted=1;
}
# endif
return UNZ_OK; return UNZ_OK;
} }
@@ -1739,19 +1698,6 @@ extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len)
uReadThis)!=uReadThis) uReadThis)!=uReadThis)
return UNZ_ERRNO; return UNZ_ERRNO;
# ifndef NOUNCRYPT
if(s->encrypted)
{
uInt i;
for(i=0;i<uReadThis;i++)
pfile_in_zip_read_info->read_buffer[i] =
zdecode(s->keys,s->pcrc_32_tab,
pfile_in_zip_read_info->read_buffer[i]);
}
# endif
pfile_in_zip_read_info->pos_in_zipfile += uReadThis; pfile_in_zip_read_info->pos_in_zipfile += uReadThis;
pfile_in_zip_read_info->rest_read_compressed-=uReadThis; pfile_in_zip_read_info->rest_read_compressed-=uReadThis;
-49
View File
@@ -153,11 +153,6 @@ typedef struct
ZPOS64_T pos_zip64extrainfo; ZPOS64_T pos_zip64extrainfo;
ZPOS64_T totalCompressedData; ZPOS64_T totalCompressedData;
ZPOS64_T totalUncompressedData; ZPOS64_T totalUncompressedData;
#ifndef NOCRYPT
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
const z_crc_t* pcrc_32_tab;
int crypt_header_size;
#endif
} curfile64_info; } curfile64_info;
typedef struct typedef struct
@@ -178,12 +173,6 @@ typedef struct
} zip64_internal; } zip64_internal;
#ifndef NOCRYPT
#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
#include "crypt.h"
#endif
local linkedlist_datablock_internal* allocate_new_datablock() local linkedlist_datablock_internal* allocate_new_datablock()
{ {
linkedlist_datablock_internal* ldi; linkedlist_datablock_internal* ldi;
@@ -1064,12 +1053,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
uInt i; uInt i;
int err = ZIP_OK; int err = ZIP_OK;
# ifdef NOCRYPT
(void)(crcForCrypting);
if (password != NULL)
return ZIP_PARAMERROR;
# endif
if (file == NULL) if (file == NULL)
return ZIP_PARAMERROR; return ZIP_PARAMERROR;
@@ -1237,24 +1220,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
} }
# ifndef NOCRYPT
zi->ci.crypt_header_size = 0;
if ((err==Z_OK) && (password != NULL))
{
unsigned char bufHead[RAND_HEAD_LEN];
unsigned int sizeHead;
zi->ci.encrypt = 1;
zi->ci.pcrc_32_tab = get_crc_table();
/*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
zi->ci.crypt_header_size = sizeHead;
if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
err = ZIP_ERRNO;
}
# endif
if (err==Z_OK) if (err==Z_OK)
zi->in_opened_file_inzip = 1; zi->in_opened_file_inzip = 1;
return err; return err;
@@ -1362,16 +1327,6 @@ local int zip64FlushWriteBuffer(zip64_internal* zi)
{ {
int err=ZIP_OK; int err=ZIP_OK;
if (zi->ci.encrypt != 0)
{
#ifndef NOCRYPT
uInt i;
int t;
for (i=0;i<zi->ci.pos_in_buffered_data;i++)
zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
#endif
}
if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data) if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
err = ZIP_ERRNO; err = ZIP_ERRNO;
@@ -1602,10 +1557,6 @@ extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_s
} }
compressed_size = zi->ci.totalCompressedData; compressed_size = zi->ci.totalCompressedData;
# ifndef NOCRYPT
compressed_size += zi->ci.crypt_header_size;
# endif
// update Current Item crc and sizes, // update Current Item crc and sizes,
if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff) if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
{ {
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -11,11 +11,11 @@ enum ZipArchiveError: WireGuardAppError {
var alertText: AlertText { 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 (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage"))
case .cantOpenOutputZipFileForWriting: case .cantOpenOutputZipFileForWriting:
return ("Unable to create zip archive", "Could not open zip file for writing.") return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
case .badArchive: case .badArchive:
return ("Unable to read zip archive", "Bad or corrupt zip archive.") return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
} }
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -7,7 +7,7 @@ enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport case noTunnelsToExport
var alertText: AlertText { var alertText: AlertText {
return ("Nothing to export", "There are no tunnels to export") return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage"))
} }
} }
@@ -22,8 +22,8 @@ class ZipExporter {
var inputsToArchiver: [(fileName: String, contents: Data)] = [] var inputsToArchiver: [(fileName: String, contents: Data)] = []
var lastTunnelName: String = "" var lastTunnelName: String = ""
for tunnelConfiguration in tunnelConfigurations { for tunnelConfiguration in tunnelConfigurations {
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) { if let contents = tunnelConfiguration.asWgQuickConfig().data(using: .utf8) {
let name = tunnelConfiguration.interface.name let name = tunnelConfiguration.name ?? "untitled"
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
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -7,7 +7,7 @@ enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive case noTunnelsInZipArchive
var alertText: AlertText { var alertText: AlertText {
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.") return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
} }
} }
@@ -43,12 +43,8 @@ class ZipImporter {
if index > 0 && file == unarchivedFiles[index - 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? TunnelConfiguration(fromWgQuickConfig: fileContents, called: file.fileBaseName) else { continue }
}
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileBaseName) else {
continue
}
configs[index] = tunnelConfig configs[index] = tunnelConfig
} }
DispatchQueue.main.async { completion(.success(configs)) } DispatchQueue.main.async { completion(.success(configs)) }
@@ -1,13 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Network import Network
import Foundation import Foundation
enum DNSResolverError: Error {
case dnsResolutionFailed(hostnames: [String])
}
class DNSResolver { class DNSResolver {
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool { static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
@@ -20,8 +16,8 @@ class DNSResolver {
return true return true
} }
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] { static func resolveSync(endpoints: [Endpoint?]) -> [Endpoint?]? {
let dispatchGroup: DispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
if isAllEndpointsAlreadyResolved(endpoints: endpoints) { if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints return endpoints
@@ -49,92 +45,108 @@ class DNSResolver {
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
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) wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure.joined(separator: ", "))")
return nil
} }
return resolvedEndpoints return resolvedEndpoints
} }
}
extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
//swiftlint:disable:next cyclomatic_complexity
private static func resolveSync(endpoint: Endpoint) -> Endpoint? { private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
switch endpoint.host { switch endpoint.host {
case .name(let name, _): case .name(let name, _):
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0)) var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo(
ai_flags: AI_ALL, // We set this to ALL so that we get v4 addresses even on DNS64 networks
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
if getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer) != 0 {
return nil
}
var next = resultPointer
var ipv4Address: IPv4Address?
var ipv6Address: IPv6Address?
while next != nil {
let result = next!.pointee
next = result.ai_next
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
ipv4Address = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
break // If we found an IPv4 address, we can stop
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
ipv6Address = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
continue // If we already have an IPv6 address, we can skip this one
}
}
freeaddrinfo(resultPointer)
// The endpoint is a hostname and needs DNS resolution // We prefer an IPv4 address over an IPv6 address
if addressInfo(for: name, port: endpoint.port, resultPointer: &resultPointer) == 0 { if let ipv4Address = ipv4Address {
// getaddrinfo succeeded return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN)) } else if let ipv6Address = ipv6Address {
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN)) return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
var ipv4AddressString: String?
var ipv6AddressString: String?
while resultPointer != nil {
let result = resultPointer!.pointee
resultPointer = result.ai_next
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
if inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil {
ipv4AddressString = String(cString: ipv4Buffer)
// If we found an IPv4 address, we can stop
break
}
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
if ipv6AddressString != nil {
// If we already have an IPv6 address, we can skip this one
continue
}
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
if inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil {
ipv6AddressString = String(cString: ipv6Buffer)
}
}
}
ipv4Buffer.deallocate()
ipv6Buffer.deallocate()
// We prefer an IPv4 address over an IPv6 address
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) {
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6AddressString = ipv6AddressString, let ipv6Address = IPv6Address(ipv6AddressString) {
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
return nil
}
} else { } else {
// getaddrinfo failed
return nil return nil
} }
default: default:
// The endpoint is already resolved
return endpoint return endpoint
} }
} }
}
private static func addressInfo(for name: String, port: NWEndpoint.Port, resultPointer: inout UnsafeMutablePointer<addrinfo>?) -> Int32 { extension Endpoint {
func withReresolvedIP() -> Endpoint {
var ret = self
let hostname: String
switch host {
case .name(let name, _):
hostname = name
case .ipv4(let address):
hostname = "\(address)"
case .ipv6(let address):
hostname = "\(address)"
}
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo( var hints = addrinfo(
ai_flags: 0, ai_flags: 0, // We set this to zero so that we actually resolve this using DNS64
ai_family: AF_UNSPEC, ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only ai_protocol: IPPROTO_UDP,
ai_addrlen: 0, ai_addrlen: 0,
ai_canonname: nil, ai_canonname: nil,
ai_addr: nil, ai_addr: nil,
ai_next: nil) ai_next: nil)
if getaddrinfo(hostname, "\(port)", &hints, &resultPointer) != 0 || resultPointer == nil {
return getaddrinfo( return ret
name.cString(using: .utf8), // Hostname }
"\(port)".cString(using: .utf8), // Port let result = resultPointer!.pointee
&hints, if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
&resultPointer) var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
let addr = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
ret = Endpoint(host: .ipv4(addr!), port: port)
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
let addr = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
ret = Endpoint(host: .ipv6(addr!), port: port)
}
freeaddrinfo(resultPointer)
if ret.host != host {
wg_log(.debug, message: "DNS64: mapped \(host) to \(ret.host)")
} else {
wg_log(.debug, message: "DNS64: mapped \(host) to itself.")
}
return ret
} }
} }
@@ -1,45 +1,20 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
class ErrorNotifier { class ErrorNotifier {
let activationAttemptId: String? let activationAttemptId: String?
weak var tunnelProvider: NEPacketTunnelProvider?
init(activationAttemptId: String?, tunnelProvider: NEPacketTunnelProvider) { init(activationAttemptId: String?) {
self.activationAttemptId = activationAttemptId self.activationAttemptId = activationAttemptId
self.tunnelProvider = tunnelProvider
ErrorNotifier.removeLastErrorFile() ErrorNotifier.removeLastErrorFile()
} }
func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
switch error {
case .savedProtocolConfigurationIsInvalid:
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration")
case .dnsResolutionFailure:
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
case .couldNotStartWireGuard:
return ("Activation failure", "WireGuard backend could not be started")
case .coultNotSetNetworkSettings:
return ("Activation failure", "Error applying network settings on the tunnel")
}
}
func notify(_ error: PacketTunnelProviderError) { func notify(_ error: PacketTunnelProviderError) {
guard let (title, message) = errorMessage(for: error) else { return } guard let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path else { return }
if let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path { let errorMessageData = "\(activationAttemptId)\n\(error)".data(using: .utf8)
// The tunnel was started from the app FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
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() { static func removeLastErrorFile() {
@@ -1,115 +1,81 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Network import Network
import NetworkExtension import NetworkExtension
import os.log import os.log
enum PacketTunnelProviderError: Error {
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure(hostnames: [String])
case couldNotStartWireGuard
case coultNotSetNetworkSettings
}
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
private var wgHandle: Int32?
private var handle: Int32?
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
private var ifname: String?
private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator?
deinit { deinit {
networkMonitor?.cancel() networkMonitor?.cancel()
} }
//swiftlint:disable:next function_body_length
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) { override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
let activationAttemptId = options?["activationAttemptId"] as? String let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId, tunnelProvider: self) let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol, guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else { let tunnelConfiguration = tunnelProviderProtocol.asTunnelConfiguration() else {
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid) startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return return
} }
startTunnel(with: tunnelConfiguration, errorNotifier: errorNotifier, completionHandler: startTunnelCompletionHandler)
}
//swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, errorNotifier: ErrorNotifier, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
configureLogger() configureLogger()
wg_log(.info, message: "Starting tunnel '\(tunnelConfiguration.interface.name)'") wg_log(.info, message: "Starting tunnel from the " + (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
let endpoints = tunnelConfiguration.peers.map { $0.endpoint } let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
var resolvedEndpoints = [Endpoint?]() guard let resolvedEndpoints = DNSResolver.resolveSync(endpoints: endpoints) else {
do { errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints) startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure)
} catch DNSResolverError.dnsResolutionFailed(let hostnames) {
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
return return
} catch {
// There can be no other errors from DNSResolver.resolveSync()
fatalError()
} }
assert(endpoints.count == resolvedEndpoints.count) assert(endpoints.count == resolvedEndpoints.count)
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints) packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
// Bring up wireguard-go backend setTunnelNetworkSettings(packetTunnelSettingsGenerator!.generateNetworkSettings()) { error in
let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast
if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
let wireguardSettings = packetTunnelSettingsGenerator.uapiConfiguration()
var handle: Int32 = -1
networkMonitor = NWPathMonitor()
networkMonitor?.pathUpdateHandler = { path in
guard handle >= 0 else { return }
if path.status == .satisfied {
wg_log(.debug, message: "Network change detected, re-establishing sockets and IPs: \(path.availableInterfaces)")
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: wgGetListenPort(handle))
let err = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
if err == -EADDRINUSE {
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: 0)
_ = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
}
}
}
networkMonitor?.start(queue: DispatchQueue(label: "NetworkMonitor"))
handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fileDescriptor: fileDescriptor)
if handle < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
wgHandle = handle
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
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, message: "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)") errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
errorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings) startTunnelCompletionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else { } else {
self.networkMonitor = NWPathMonitor()
self.networkMonitor!.pathUpdateHandler = self.pathUpdate
self.networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
let fileDescriptor = (self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32) ?? -1
if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
return
}
var ifnameSize = socklen_t(IFNAMSIZ)
let ifnamePtr = UnsafeMutablePointer<CChar>.allocate(capacity: Int(ifnameSize))
ifnamePtr.initialize(repeating: 0, count: Int(ifnameSize))
if getsockopt(fileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, ifnamePtr, &ifnameSize) == 0 {
self.ifname = String(cString: ifnamePtr)
}
ifnamePtr.deallocate()
wg_log(.info, message: "Tunnel interface is \(self.ifname ?? "unknown")")
let handle = self.packetTunnelSettingsGenerator!.uapiConfiguration().withGoString { return wgTurnOn($0, fileDescriptor) }
if handle < 0 {
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(handle)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartBackend)
return
}
self.handle = handle
startTunnelCompletionHandler(nil) startTunnelCompletionHandler(nil)
} }
} }
@@ -122,7 +88,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
ErrorNotifier.removeLastErrorFile() ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel") wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle { if let handle = handle {
wgTurnOff(handle) wgTurnOff(handle)
} }
completionHandler() completionHandler()
@@ -147,16 +113,25 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
} }
private func connect(interfaceName: String, settings: String, fileDescriptor: Int32) -> Int32 { private func pathUpdate(path: Network.NWPath) {
return withStringsAsGoStrings(interfaceName, settings) { return wgTurnOn($0.0, $0.1, fileDescriptor) } guard let handle = handle, let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator else { return }
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
var interfaces = path.availableInterfaces
if let ifname = ifname {
interfaces = interfaces.filter { $0.name != ifname }
}
if let ifscope = interfaces.first?.index {
wgBindInterfaceScope(handle, Int32(ifscope))
}
} }
} }
// swiftlint:disable:next large_tuple identifier_name extension String {
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 { func withGoString<R>(_ call: (gostring_t) -> R) -> R {
// swiftlint:disable:next large_tuple identifier_name func helper(_ pointer: UnsafePointer<Int8>?, _ call: (gostring_t) -> R) -> R {
func helper(_ p1: UnsafePointer<Int8>?, _ p2: UnsafePointer<Int8>?, _ p3: UnsafePointer<Int8>?, _ p4: UnsafePointer<Int8>?, _ call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R { return call(gostring_t(p: pointer, n: 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 helper(self, call)
} }
return helper(s1, s2, s3, s4, call)
} }
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Network import Network
import NetworkExtension import NetworkExtension
class PacketTunnelSettingsGenerator { class PacketTunnelSettingsGenerator {
let tunnelConfiguration: TunnelConfiguration let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?] let resolvedEndpoints: [Endpoint?]
@@ -15,17 +14,15 @@ class PacketTunnelSettingsGenerator {
self.resolvedEndpoints = resolvedEndpoints self.resolvedEndpoints = resolvedEndpoints
} }
func endpointUapiConfiguration(currentListenPort: UInt16) -> String { func endpointUapiConfiguration() -> String {
var wgSettings = "listen_port=\(tunnelConfiguration.interface.listenPort ?? currentListenPort)\n" var wgSettings = ""
for (index, 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 endpoint = resolvedEndpoints[index] { if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
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")
} }
} }
return wgSettings return wgSettings
} }
@@ -36,7 +33,7 @@ class PacketTunnelSettingsGenerator {
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.isEmpty {
wgSettings.append("replace_peers=true\n") wgSettings.append("replace_peers=true\n")
} }
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count) assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
@@ -45,15 +42,15 @@ class PacketTunnelSettingsGenerator {
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[index] { if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
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")
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation())\n") } peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation)\n") }
} }
} }
return wgSettings return wgSettings
@@ -66,46 +63,36 @@ class PacketTunnelSettingsGenerator {
* 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 = "0.0.0.0" let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let endpointsCompact = resolvedEndpoints.compactMap { $0 }
if endpointsCompact.count == 1 {
switch endpointsCompact.first!.host {
case .ipv4(let address):
remoteAddress = "\(address)"
case .ipv6(let address):
remoteAddress = "\(address)"
default:
break
}
}
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress) 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 tunnel's DNS dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
networkSettings.dnsSettings = dnsSettings networkSettings.dnsSettings = dnsSettings
let mtu = tunnelConfiguration.interface.mtu ?? 0 var mtu = tunnelConfiguration.interface.mtu ?? 0
/* 0 means automatic MTU. In theory, we should just do
* `networkSettings.tunnelOverheadBytes = 80` but in
* practice there are too many broken networks out there.
* Instead set it to 1280. Boohoo. Maybe someday we'll
* add a nob, maybe, or iOS will do probing for us.
*/
if mtu == 0 { if mtu == 0 {
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard mtu = 1280
networkSettings.tunnelOverheadBytes = 80
} else {
networkSettings.mtu = NSNumber(value: mtu)
} }
networkSettings.mtu = NSNumber(value: mtu)
let (ipv4Routes, ipv6Routes) = routes() let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes() let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let (ipv4ExcludedRoutes, ipv6ExcludedRoutes) = excludedRoutes()
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask }) let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes ipv4Settings.includedRoutes = ipv4IncludedRoutes
ipv4Settings.excludedRoutes = ipv4ExcludedRoutes
networkSettings.ipv4Settings = ipv4Settings networkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength }) let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes ipv6Settings.includedRoutes = ipv6IncludedRoutes
ipv6Settings.excludedRoutes = ipv6ExcludedRoutes
networkSettings.ipv6Settings = ipv6Settings networkSettings.ipv6Settings = ipv6Settings
return networkSettings return networkSettings
@@ -155,24 +142,6 @@ class PacketTunnelSettingsGenerator {
} }
return (ipv4IncludedRoutes, ipv6IncludedRoutes) 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 {
+3 -3
View File
@@ -1,11 +1,11 @@
# SPDX-License-Identifier: GPL-2.0 # SPDX-License-Identifier: GPL-2.0
# #
# Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. # Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
# These are generally passed to us by xcode, but we set working defaults for standalone compilation too. # These are generally passed to us by xcode, but we set working defaults for standalone compilation too.
ARCHS ?= arm64 armv7 ARCHS ?= arm64 armv7
SDK_NAME ?= iphoneos SDK_NAME ?= iphoneos
SDK_DIR ?= $(shell xcrun --sdk $(SDK_NAME) --show-sdk-path) SDKROOT ?= $(shell xcrun --sdk $(SDK_NAME) --show-sdk-path)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
@@ -16,7 +16,7 @@ BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum
DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go) DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go)
CFLAGS_PREFIX := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)) -isysroot $(SDK_DIR) -arch CFLAGS_PREFIX := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)) -isysroot $(SDKROOT) -arch
GOARCH_arm64 := arm64 GOARCH_arm64 := arm64
GOARCH_armv7 := arm GOARCH_armv7 := arm
GOARCH_x86_64 := amd64 GOARCH_x86_64 := amd64
+47 -16
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0 /* SPDX-License-Identifier: GPL-2.0
* *
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. * Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/ */
package main package main
@@ -32,15 +32,14 @@ var loggerFunc unsafe.Pointer
var versionString *C.char var versionString *C.char
type CLogger struct { type CLogger struct {
level C.int level C.int
interfaceName string
} }
func (l *CLogger) Write(p []byte) (int, error) { func (l *CLogger) Write(p []byte) (int, error) {
if uintptr(loggerFunc) == 0 { if uintptr(loggerFunc) == 0 {
return 0, errors.New("No logger initialized") return 0, errors.New("No logger initialized")
} }
message := C.CString(l.interfaceName + ": " + string(p)) message := C.CString(string(p))
C.callLogger(loggerFunc, l.level, message) C.callLogger(loggerFunc, l.level, message)
C.free(unsafe.Pointer(message)) C.free(unsafe.Pointer(message))
return len(p), nil return len(p), nil
@@ -75,17 +74,13 @@ func wgSetLogger(loggerFn uintptr) {
} }
//export wgTurnOn //export wgTurnOn
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 { func wgTurnOn(settings string, tunFd int32) int32 {
interfaceName := string([]byte(ifnameRef))
logger := &Logger{ logger := &Logger{
Debug: log.New(&CLogger{level: 0, interfaceName: interfaceName}, "", 0), Debug: log.New(&CLogger{level: 0}, "", 0),
Info: log.New(&CLogger{level: 1, interfaceName: interfaceName}, "", 0), Info: log.New(&CLogger{level: 1}, "", 0),
Error: log.New(&CLogger{level: 2, interfaceName: interfaceName}, "", 0), Error: log.New(&CLogger{level: 2}, "", 0),
} }
logger.Debug.Println("Debug log enabled")
tun, _, err := tun.CreateTUNFromFD(int(tunFd)) tun, _, err := tun.CreateTUNFromFD(int(tunFd))
if err != nil { if err != nil {
logger.Error.Println(err) logger.Error.Println(err)
@@ -142,13 +137,49 @@ func wgSetConfig(tunnelHandle int32, settings string) int64 {
return 0 return 0
} }
//export wgGetListenPort //export wgBindInterfaceScope
func wgGetListenPort(tunnelHandle int32) uint16 { func wgBindInterfaceScope(tunnelHandle int32, ifscope int32) {
var operr error
device, ok := tunnelHandles[tunnelHandle] device, ok := tunnelHandles[tunnelHandle]
if !ok { if !ok {
return 0 return
}
device.log.Info.Printf("Binding sockets to interface %d\n", ifscope)
bind := device.net.bind.(*NativeBind)
for bind.ipv4 != nil {
fd, err := bind.ipv4.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
}
break
}
for bind.ipv6 != nil {
fd, err := bind.ipv6.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
}
break
} }
return device.net.port
} }
//export wgVersion //export wgVersion
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0 /* SPDX-License-Identifier: GPL-2.0
* *
* Copyright (C) 2017-2018 WireGuard LLC. All Rights Reserved. * Copyright (C) 2017-2019 WireGuard LLC. All Rights Reserved.
*/ */
package main package main
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0 /* SPDX-License-Identifier: GPL-2.0
* *
* Copyright (C) 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/ */
package tun package tun
+3 -3
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-2.0 /* SPDX-License-Identifier: GPL-2.0
* *
* Copyright (C) 2018 WireGuard LLC. All Rights Reserved. * Copyright (C) 2018-2019 WireGuard LLC. All Rights Reserved.
*/ */
#ifndef WIREGUARD_H #ifndef WIREGUARD_H
@@ -12,10 +12,10 @@
typedef struct { const char *p; size_t n; } gostring_t; typedef struct { const char *p; size_t n; } gostring_t;
typedef void(*logger_fn_t)(int level, const char *msg); 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 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 int64_t wgSetConfig(int handle, gostring_t settings);
extern uint16_t wgGetListenPort(int handle); extern void wgBindInterfaceScope(int handle, int32_t ifscope);
extern char *wgVersion(); extern char *wgVersion();
#endif #endif