Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52c59704de | |||
| 0b828f9b96 | |||
| 51a3e5c0b4 | |||
| c9c343cde2 | |||
| c563a24348 | |||
| 808852c547 | |||
| 035055ef0a | |||
| 508ba44576 | |||
| 999b761ed0 | |||
| 129f94dccd | |||
| aaee3c5cbe | |||
| dddbf3b370 | |||
| d29f47fc9b | |||
| e6e1795d08 | |||
| fd29cf3402 | |||
| 56ad5f74e9 | |||
| 49bf55021f | |||
| 0bec5b04b0 | |||
| d36e7e27ff | |||
| b0b6866c51 | |||
| 9098cd1161 | |||
| 8365adf435 | |||
| 9d9859248e | |||
| f7e9f4d631 | |||
| f2000aa1da | |||
| 82de3e3090 | |||
| 41a4c6362a | |||
| aede9f6e45 | |||
| 1eeed89174 | |||
| c1c5f7a7c7 | |||
| 4ed646973e | |||
| 9295895e3a | |||
| 7b9d4cb9e3 | |||
| 1fecd8eb6c | |||
| accf60b82f | |||
| f6af9d9ffb | |||
| 78b38a4eba | |||
| ec031b1f19 | |||
| 8553723e04 | |||
| 38445114e0 | |||
| a21c569e9f | |||
| 0552d75aa1 | |||
| e47a8232d8 | |||
| f818cdd963 | |||
| 28ce4d5164 | |||
| c2131cb757 | |||
| b23578dc7c | |||
| a89ad95901 | |||
| 5618c465a2 | |||
| de08978a80 | |||
| 9268c0c4bc | |||
| 5c501ac9a6 | |||
| 35450bf407 | |||
| f93c9797ea | |||
| bba6d2f919 | |||
| fa51e3f1d1 | |||
| 04a8c2ff5a | |||
| 4e516d6769 | |||
| fab7af6f38 | |||
| 3ae9fb538d | |||
| 78eaab8b5b | |||
| 20f8abdf04 | |||
| 2582ddd6f6 |
@@ -19,6 +19,7 @@ DerivedData
|
|||||||
*.perspectivev3
|
*.perspectivev3
|
||||||
!default.perspectivev3
|
!default.perspectivev3
|
||||||
xcuserdata
|
xcuserdata
|
||||||
|
*.xcscheme
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,25 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,41 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -4,17 +4,41 @@
|
|||||||
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 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 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 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 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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+55
-22
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 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.";
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME = 0.0.20181104
|
VERSION_NAME = 0.0.20181225
|
||||||
VERSION_ID = 7
|
VERSION_ID = 2
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright © 2018 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)
|
||||||
|
}
|
||||||
@@ -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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,29 +6,54 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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?()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,40 +4,45 @@
|
|||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
|
|
||||||
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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)) }
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,42 +4,17 @@
|
|||||||
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() {
|
||||||
|
|||||||
@@ -6,110 +6,76 @@ 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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,22 +63,11 @@ 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 remoteAddress = "0.0.0.0"
|
||||||
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 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
|
||||||
@@ -96,16 +82,13 @@ class PacketTunnelSettingsGenerator {
|
|||||||
|
|
||||||
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 +138,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 {
|
||||||
|
|||||||
+1
-1
Submodule wireguard-go updated: 276bf973e8...8fde8334dc
@@ -5,7 +5,7 @@
|
|||||||
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user