2018-10-24 01:37:28 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2018-10-30 02:57:35 +00:00
|
|
|
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
2018-10-23 09:53:18 +00:00
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
2018-12-14 23:27:11 +00:00
|
|
|
//swiftlint:disable:next type_body_length
|
2018-10-23 09:53:18 +00:00
|
|
|
class TunnelViewModel {
|
|
|
|
|
2018-10-23 12:18:07 +00:00
|
|
|
enum InterfaceField: String {
|
2018-10-23 09:53:18 +00:00
|
|
|
case name = "Name"
|
|
|
|
case privateKey = "Private key"
|
|
|
|
case publicKey = "Public key"
|
|
|
|
case generateKeyPair = "Generate keypair"
|
|
|
|
case addresses = "Addresses"
|
|
|
|
case listenPort = "Listen port"
|
|
|
|
case mtu = "MTU"
|
|
|
|
case dns = "DNS servers"
|
|
|
|
}
|
|
|
|
|
2018-10-24 09:55:30 +00:00
|
|
|
static let interfaceFieldsWithControl: Set<InterfaceField> = [
|
2018-10-31 00:00:27 +00:00
|
|
|
.generateKeyPair
|
2018-10-24 09:55:30 +00:00
|
|
|
]
|
|
|
|
|
2018-10-23 12:18:07 +00:00
|
|
|
enum PeerField: String {
|
2018-10-23 09:53:18 +00:00
|
|
|
case publicKey = "Public key"
|
2018-11-01 17:59:58 +00:00
|
|
|
case preSharedKey = "Preshared key"
|
2018-10-23 09:53:18 +00:00
|
|
|
case endpoint = "Endpoint"
|
2018-11-01 17:59:58 +00:00
|
|
|
case persistentKeepAlive = "Persistent keepalive"
|
2018-10-23 09:53:18 +00:00
|
|
|
case allowedIPs = "Allowed IPs"
|
|
|
|
case excludePrivateIPs = "Exclude private IPs"
|
|
|
|
case deletePeer = "Delete peer"
|
|
|
|
}
|
|
|
|
|
2018-10-24 09:55:30 +00:00
|
|
|
static let peerFieldsWithControl: Set<PeerField> = [
|
|
|
|
.excludePrivateIPs, .deletePeer
|
|
|
|
]
|
|
|
|
|
2018-10-24 08:41:34 +00:00
|
|
|
static let keyLengthInBase64 = 44
|
|
|
|
|
2018-10-23 09:53:18 +00:00
|
|
|
class InterfaceData {
|
2018-12-13 03:09:52 +00:00
|
|
|
var scratchpad = [InterfaceField: String]()
|
|
|
|
var fieldsWithError = Set<InterfaceField>()
|
2018-11-03 18:35:25 +00:00
|
|
|
var validatedConfiguration: InterfaceConfiguration?
|
2018-10-23 09:53:18 +00:00
|
|
|
|
2018-10-23 12:18:07 +00:00
|
|
|
subscript(field: InterfaceField) -> String {
|
2018-10-23 09:53:18 +00:00
|
|
|
get {
|
2018-12-12 18:28:27 +00:00
|
|
|
if scratchpad.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
return scratchpad[field] ?? ""
|
|
|
|
}
|
|
|
|
set(stringValue) {
|
2018-12-12 18:28:27 +00:00
|
|
|
if scratchpad.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
// When starting to edit a config, setup the scratchpad and remove the configuration.
|
|
|
|
// The scratchpad shall be the sole source of the being-edited configuration.
|
|
|
|
populateScratchpad()
|
|
|
|
}
|
|
|
|
validatedConfiguration = nil
|
2018-12-12 18:28:27 +00:00
|
|
|
if stringValue.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
scratchpad.removeValue(forKey: field)
|
|
|
|
} else {
|
|
|
|
scratchpad[field] = stringValue
|
|
|
|
}
|
2018-12-12 18:28:27 +00:00
|
|
|
if field == .privateKey {
|
2018-12-15 03:42:11 +00:00
|
|
|
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
2018-10-24 08:41:34 +00:00
|
|
|
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
|
|
|
scratchpad[.publicKey] = publicKey.base64EncodedString()
|
|
|
|
} else {
|
|
|
|
scratchpad.removeValue(forKey: .publicKey)
|
|
|
|
}
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func populateScratchpad() {
|
|
|
|
// Populate the scratchpad from the configuration object
|
|
|
|
guard let config = validatedConfiguration else { return }
|
|
|
|
scratchpad[.name] = config.name
|
|
|
|
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
|
2018-10-24 06:56:21 +00:00
|
|
|
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
2018-12-12 18:28:27 +00:00
|
|
|
if !config.addresses.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
|
|
|
|
}
|
|
|
|
if let listenPort = config.listenPort {
|
|
|
|
scratchpad[.listenPort] = String(listenPort)
|
|
|
|
}
|
|
|
|
if let mtu = config.mtu {
|
|
|
|
scratchpad[.mtu] = String(mtu)
|
|
|
|
}
|
2018-12-12 18:28:27 +00:00
|
|
|
if !config.dns.isEmpty {
|
2018-10-23 10:58:24 +00:00
|
|
|
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-13 18:58:50 +00:00
|
|
|
//swiftlint:disable:next cyclomatic_complexity function_body_length
|
2018-10-23 09:53:18 +00:00
|
|
|
func save() -> SaveResult<InterfaceConfiguration> {
|
2018-11-05 17:25:09 +00:00
|
|
|
if let validatedConfiguration = validatedConfiguration {
|
|
|
|
// It's already validated and saved
|
|
|
|
return .saved(validatedConfiguration)
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
fieldsWithError.removeAll()
|
2018-11-03 10:15:29 +00:00
|
|
|
guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
|
2018-10-23 09:53:18 +00:00
|
|
|
fieldsWithError.insert(.name)
|
|
|
|
return .error("Interface name is required")
|
|
|
|
}
|
|
|
|
guard let privateKeyString = scratchpad[.privateKey] else {
|
|
|
|
fieldsWithError.insert(.privateKey)
|
|
|
|
return .error("Interface's private key is required")
|
|
|
|
}
|
2018-12-08 13:22:11 +00:00
|
|
|
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
2018-10-23 09:53:18 +00:00
|
|
|
fieldsWithError.insert(.privateKey)
|
2018-11-01 17:59:58 +00:00
|
|
|
return .error("Interface's private key must be a 32-byte key in base64 encoding")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
|
2018-12-13 03:09:52 +00:00
|
|
|
var errorMessages = [String]()
|
2018-10-23 09:53:18 +00:00
|
|
|
if let addressesString = scratchpad[.addresses] {
|
2018-12-13 03:09:52 +00:00
|
|
|
var addresses = [IPAddressRange]()
|
2018-10-23 09:53:18 +00:00
|
|
|
for addressString in addressesString.split(separator: ",") {
|
|
|
|
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
|
|
if let address = IPAddressRange(from: trimmedString) {
|
|
|
|
addresses.append(address)
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.addresses)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
config.addresses = addresses
|
|
|
|
}
|
|
|
|
if let listenPortString = scratchpad[.listenPort] {
|
2018-10-23 10:21:19 +00:00
|
|
|
if let listenPort = UInt16(listenPortString) {
|
2018-10-23 09:53:18 +00:00
|
|
|
config.listenPort = listenPort
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.listenPort)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Interface's listen port must be between 0 and 65535, or unspecified")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let mtuString = scratchpad[.mtu] {
|
2018-11-01 17:59:58 +00:00
|
|
|
if let mtu = UInt16(mtuString), mtu >= 576 {
|
2018-10-23 09:53:18 +00:00
|
|
|
config.mtu = mtu
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.mtu)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Interface's MTU must be between 576 and 65535, or unspecified")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let dnsString = scratchpad[.dns] {
|
2018-12-13 03:09:52 +00:00
|
|
|
var dnsServers = [DNSServer]()
|
2018-10-23 10:58:24 +00:00
|
|
|
for dnsServerString in dnsString.split(separator: ",") {
|
|
|
|
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
|
|
if let dnsServer = DNSServer(from: trimmedString) {
|
|
|
|
dnsServers.append(dnsServer)
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.dns)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Interface's DNS servers must be a list of comma-separated IP addresses")
|
2018-10-23 10:58:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
config.dns = dnsServers
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
2018-12-12 18:28:27 +00:00
|
|
|
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
|
|
|
|
2018-10-23 09:53:18 +00:00
|
|
|
validatedConfiguration = config
|
|
|
|
return .saved(config)
|
|
|
|
}
|
2018-10-24 09:55:30 +00:00
|
|
|
|
|
|
|
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
|
2018-12-12 21:33:14 +00:00
|
|
|
return interfaceFields.filter { field in
|
2018-12-12 18:28:27 +00:00
|
|
|
if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
|
2018-10-24 09:55:30 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return (!self[field].isEmpty)
|
|
|
|
}
|
|
|
|
// TODO: Cache this to avoid recomputing
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class PeerData {
|
|
|
|
var index: Int
|
2018-12-13 03:09:52 +00:00
|
|
|
var scratchpad = [PeerField: String]()
|
|
|
|
var fieldsWithError = Set<PeerField>()
|
2018-11-03 18:35:25 +00:00
|
|
|
var validatedConfiguration: PeerConfiguration?
|
2018-10-23 09:53:18 +00:00
|
|
|
|
2018-10-29 09:38:26 +00:00
|
|
|
// For exclude private IPs
|
2018-12-13 03:09:52 +00:00
|
|
|
private(set) var shouldAllowExcludePrivateIPsControl = false
|
|
|
|
private(set) var excludePrivateIPsValue = false
|
|
|
|
fileprivate var numberOfPeers = 0
|
2018-10-29 09:38:26 +00:00
|
|
|
|
2018-10-23 09:53:18 +00:00
|
|
|
init(index: Int) {
|
|
|
|
self.index = index
|
|
|
|
}
|
|
|
|
|
2018-10-23 12:18:07 +00:00
|
|
|
subscript(field: PeerField) -> String {
|
2018-10-23 09:53:18 +00:00
|
|
|
get {
|
2018-12-12 18:28:27 +00:00
|
|
|
if scratchpad.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
return scratchpad[field] ?? ""
|
|
|
|
}
|
|
|
|
set(stringValue) {
|
2018-12-12 18:28:27 +00:00
|
|
|
if scratchpad.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
// When starting to edit a config, setup the scratchpad and remove the configuration.
|
|
|
|
// The scratchpad shall be the sole source of the being-edited configuration.
|
|
|
|
populateScratchpad()
|
|
|
|
}
|
|
|
|
validatedConfiguration = nil
|
2018-12-12 18:28:27 +00:00
|
|
|
if stringValue.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
scratchpad.removeValue(forKey: field)
|
|
|
|
} else {
|
|
|
|
scratchpad[field] = stringValue
|
|
|
|
}
|
2018-12-12 18:28:27 +00:00
|
|
|
if field == .allowedIPs {
|
2018-10-29 09:38:26 +00:00
|
|
|
updateExcludePrivateIPsFieldState()
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func populateScratchpad() {
|
|
|
|
// Populate the scratchpad from the configuration object
|
|
|
|
guard let config = validatedConfiguration else { return }
|
|
|
|
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
|
|
|
if let preSharedKey = config.preSharedKey {
|
|
|
|
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
|
|
|
|
}
|
2018-12-12 18:28:27 +00:00
|
|
|
if !config.allowedIPs.isEmpty {
|
2018-10-23 09:53:18 +00:00
|
|
|
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
|
|
|
|
}
|
|
|
|
if let endpoint = config.endpoint {
|
|
|
|
scratchpad[.endpoint] = endpoint.stringRepresentation()
|
|
|
|
}
|
|
|
|
if let persistentKeepAlive = config.persistentKeepAlive {
|
|
|
|
scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
|
|
|
|
}
|
2018-10-29 09:38:26 +00:00
|
|
|
updateExcludePrivateIPsFieldState()
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
2018-12-13 18:58:50 +00:00
|
|
|
//swiftlint:disable:next cyclomatic_complexity
|
2018-10-23 09:53:18 +00:00
|
|
|
func save() -> SaveResult<PeerConfiguration> {
|
2018-11-05 17:25:09 +00:00
|
|
|
if let validatedConfiguration = validatedConfiguration {
|
|
|
|
// It's already validated and saved
|
|
|
|
return .saved(validatedConfiguration)
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
fieldsWithError.removeAll()
|
|
|
|
guard let publicKeyString = scratchpad[.publicKey] else {
|
|
|
|
fieldsWithError.insert(.publicKey)
|
|
|
|
return .error("Peer's public key is required")
|
|
|
|
}
|
2018-12-08 13:22:11 +00:00
|
|
|
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
2018-10-23 09:53:18 +00:00
|
|
|
fieldsWithError.insert(.publicKey)
|
2018-11-01 17:59:58 +00:00
|
|
|
return .error("Peer's public key must be a 32-byte key in base64 encoding")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
var config = PeerConfiguration(publicKey: publicKey)
|
2018-12-13 03:09:52 +00:00
|
|
|
var errorMessages = [String]()
|
2018-10-23 09:53:18 +00:00
|
|
|
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
2018-12-08 13:22:11 +00:00
|
|
|
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
2018-10-23 09:53:18 +00:00
|
|
|
config.preSharedKey = preSharedKey
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.preSharedKey)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Peer's preshared key must be a 32-byte key in base64 encoding")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let allowedIPsString = scratchpad[.allowedIPs] {
|
2018-12-13 03:09:52 +00:00
|
|
|
var allowedIPs = [IPAddressRange]()
|
2018-10-23 09:53:18 +00:00
|
|
|
for allowedIPString in allowedIPsString.split(separator: ",") {
|
|
|
|
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
|
|
if let allowedIP = IPAddressRange(from: trimmedString) {
|
|
|
|
allowedIPs.append(allowedIP)
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.allowedIPs)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
config.allowedIPs = allowedIPs
|
|
|
|
}
|
|
|
|
if let endpointString = scratchpad[.endpoint] {
|
|
|
|
if let endpoint = Endpoint(from: endpointString) {
|
|
|
|
config.endpoint = endpoint
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.endpoint)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Peer's endpoint must be of the form 'host:port' or '[host]:port'")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
|
2018-10-23 10:21:19 +00:00
|
|
|
if let persistentKeepAlive = UInt16(persistentKeepAliveString) {
|
2018-10-23 09:53:18 +00:00
|
|
|
config.persistentKeepAlive = persistentKeepAlive
|
|
|
|
} else {
|
|
|
|
fieldsWithError.insert(.persistentKeepAlive)
|
2018-11-01 17:59:58 +00:00
|
|
|
errorMessages.append("Peer's persistent keepalive must be between 0 to 65535, or unspecified")
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-12 18:28:27 +00:00
|
|
|
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
2018-12-12 21:33:14 +00:00
|
|
|
|
2018-10-23 09:53:18 +00:00
|
|
|
validatedConfiguration = config
|
|
|
|
return .saved(config)
|
|
|
|
}
|
2018-10-24 09:55:30 +00:00
|
|
|
|
|
|
|
func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
|
2018-12-12 21:33:14 +00:00
|
|
|
return peerFields.filter { field in
|
2018-12-12 18:28:27 +00:00
|
|
|
if TunnelViewModel.peerFieldsWithControl.contains(field) {
|
2018-10-24 09:55:30 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return (!self[field].isEmpty)
|
|
|
|
}
|
|
|
|
// TODO: Cache this to avoid recomputing
|
|
|
|
}
|
2018-10-29 09:38:26 +00:00
|
|
|
|
|
|
|
static let ipv4DefaultRouteString = "0.0.0.0/0"
|
|
|
|
static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs
|
|
|
|
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
|
|
|
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
|
|
|
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
|
|
|
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
|
|
|
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
|
|
|
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
|
|
|
]
|
|
|
|
|
|
|
|
func updateExcludePrivateIPsFieldState() {
|
2018-12-12 18:28:27 +00:00
|
|
|
guard numberOfPeers == 1 else {
|
2018-10-29 09:38:26 +00:00
|
|
|
shouldAllowExcludePrivateIPsControl = false
|
|
|
|
excludePrivateIPsValue = false
|
|
|
|
return
|
|
|
|
}
|
2018-12-12 18:28:27 +00:00
|
|
|
if scratchpad.isEmpty {
|
2018-11-02 07:39:45 +00:00
|
|
|
populateScratchpad()
|
|
|
|
}
|
2018-10-29 09:38:26 +00:00
|
|
|
let allowedIPStrings = Set<String>(
|
|
|
|
(scratchpad[.allowedIPs] ?? "")
|
|
|
|
.split(separator: ",")
|
|
|
|
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
|
|
|
)
|
2018-12-12 18:28:27 +00:00
|
|
|
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
|
2018-10-29 09:38:26 +00:00
|
|
|
shouldAllowExcludePrivateIPsControl = true
|
|
|
|
excludePrivateIPsValue = false
|
2018-12-12 18:28:27 +00:00
|
|
|
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
|
2018-10-29 09:38:26 +00:00
|
|
|
shouldAllowExcludePrivateIPsControl = true
|
|
|
|
excludePrivateIPsValue = true
|
|
|
|
} else {
|
|
|
|
shouldAllowExcludePrivateIPsControl = false
|
|
|
|
excludePrivateIPsValue = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) {
|
|
|
|
let allowedIPStrings = (scratchpad[.allowedIPs] ?? "")
|
|
|
|
.split(separator: ",")
|
|
|
|
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
|
|
|
let dnsServerStrings = dnsServers
|
|
|
|
.split(separator: ",")
|
|
|
|
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
|
|
|
|
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
|
|
|
|
let modifiedAllowedIPStrings: [String]
|
2018-12-12 18:28:27 +00:00
|
|
|
if isOn {
|
2018-12-13 03:09:52 +00:00
|
|
|
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
|
2018-10-29 09:38:26 +00:00
|
|
|
} else {
|
2018-12-13 03:09:52 +00:00
|
|
|
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
2018-10-29 09:38:26 +00:00
|
|
|
}
|
|
|
|
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
2018-11-19 09:52:21 +00:00
|
|
|
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
|
2018-10-29 09:38:26 +00:00
|
|
|
excludePrivateIPsValue = isOn
|
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enum SaveResult<Configuration> {
|
|
|
|
case saved(Configuration)
|
|
|
|
case error(String) // TODO: Localize error messages
|
|
|
|
}
|
|
|
|
|
|
|
|
var interfaceData: InterfaceData
|
|
|
|
var peersData: [PeerData]
|
|
|
|
|
|
|
|
init(tunnelConfiguration: TunnelConfiguration?) {
|
2018-11-02 07:39:45 +00:00
|
|
|
let interfaceData: InterfaceData = InterfaceData()
|
2018-12-13 03:09:52 +00:00
|
|
|
var peersData = [PeerData]()
|
2018-10-23 09:53:18 +00:00
|
|
|
if let tunnelConfiguration = tunnelConfiguration {
|
|
|
|
interfaceData.validatedConfiguration = tunnelConfiguration.interface
|
2018-12-12 17:40:57 +00:00
|
|
|
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
|
|
|
|
let peerData = PeerData(index: index)
|
2018-10-23 09:53:18 +00:00
|
|
|
peerData.validatedConfiguration = peerConfiguration
|
|
|
|
peersData.append(peerData)
|
|
|
|
}
|
|
|
|
}
|
2018-11-02 06:35:40 +00:00
|
|
|
let numberOfPeers = peersData.count
|
|
|
|
for peerData in peersData {
|
|
|
|
peerData.numberOfPeers = numberOfPeers
|
2018-11-02 07:39:45 +00:00
|
|
|
peerData.updateExcludePrivateIPsFieldState()
|
2018-11-02 06:35:40 +00:00
|
|
|
}
|
2018-11-02 07:39:45 +00:00
|
|
|
self.interfaceData = interfaceData
|
|
|
|
self.peersData = peersData
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func appendEmptyPeer() {
|
|
|
|
let peer = PeerData(index: peersData.count)
|
|
|
|
peersData.append(peer)
|
2018-12-12 17:40:57 +00:00
|
|
|
for peer in peersData {
|
|
|
|
peer.numberOfPeers = peersData.count
|
|
|
|
peer.updateExcludePrivateIPsFieldState()
|
2018-10-29 09:38:26 +00:00
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func deletePeer(peer: PeerData) {
|
|
|
|
let removedPeer = peersData.remove(at: peer.index)
|
|
|
|
assert(removedPeer.index == peer.index)
|
2018-12-12 17:40:57 +00:00
|
|
|
for peer in peersData[peer.index ..< peersData.count] {
|
|
|
|
assert(peer.index > 0)
|
|
|
|
peer.index -= 1
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
2018-12-12 17:40:57 +00:00
|
|
|
for peer in peersData {
|
|
|
|
peer.numberOfPeers = peersData.count
|
|
|
|
peer.updateExcludePrivateIPsFieldState()
|
2018-10-29 09:38:26 +00:00
|
|
|
}
|
2018-10-23 09:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func save() -> SaveResult<TunnelConfiguration> {
|
|
|
|
// Attempt to save the interface and all peers, so that all erroring fields are collected
|
|
|
|
let interfaceSaveResult = interfaceData.save()
|
|
|
|
let peerSaveResults = peersData.map { $0.save() }
|
|
|
|
// Collate the results
|
2018-12-12 18:28:27 +00:00
|
|
|
switch interfaceSaveResult {
|
2018-10-23 09:53:18 +00:00
|
|
|
case .error(let errorMessage):
|
|
|
|
return .error(errorMessage)
|
|
|
|
case .saved(let interfaceConfiguration):
|
2018-12-13 03:09:52 +00:00
|
|
|
var peerConfigurations = [PeerConfiguration]()
|
2018-10-23 09:53:18 +00:00
|
|
|
peerConfigurations.reserveCapacity(peerSaveResults.count)
|
|
|
|
for peerSaveResult in peerSaveResults {
|
2018-12-12 18:28:27 +00:00
|
|
|
switch peerSaveResult {
|
2018-10-23 09:53:18 +00:00
|
|
|
case .error(let errorMessage):
|
|
|
|
return .error(errorMessage)
|
|
|
|
case .saved(let peerConfiguration):
|
|
|
|
peerConfigurations.append(peerConfiguration)
|
|
|
|
}
|
|
|
|
}
|
2018-11-06 02:47:19 +00:00
|
|
|
|
|
|
|
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
|
|
|
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
2018-12-12 18:28:27 +00:00
|
|
|
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
2018-11-06 02:47:19 +00:00
|
|
|
return .error("Two or more peers cannot have the same public key")
|
|
|
|
}
|
|
|
|
|
2018-11-10 11:32:30 +00:00
|
|
|
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
|
2018-10-23 09:53:18 +00:00
|
|
|
return .saved(tunnelConfiguration)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-11-12 10:22:28 +00:00
|
|
|
|
|
|
|
// MARK: Activate on demand
|
|
|
|
|
|
|
|
extension TunnelViewModel {
|
2018-12-07 19:24:58 +00:00
|
|
|
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
|
2018-12-12 18:28:27 +00:00
|
|
|
switch activateOnDemandOption {
|
2018-11-12 10:22:28 +00:00
|
|
|
case .none:
|
2018-12-07 17:35:06 +00:00
|
|
|
return "Off"
|
2018-11-28 07:11:35 +00:00
|
|
|
case .useOnDemandOverWiFiOrCellular:
|
2018-12-07 17:35:06 +00:00
|
|
|
return "Wi-Fi or cellular"
|
2018-11-28 07:11:35 +00:00
|
|
|
case .useOnDemandOverWiFiOnly:
|
2018-12-07 17:35:06 +00:00
|
|
|
return "Wi-Fi only"
|
2018-11-12 10:22:28 +00:00
|
|
|
case .useOnDemandOverCellularOnly:
|
2018-12-07 17:35:06 +00:00
|
|
|
return "Cellular only"
|
2018-11-12 10:22:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-07 20:24:18 +00:00
|
|
|
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
|
|
|
|
if let activateOnDemandSetting = activateOnDemandSetting {
|
2018-12-12 18:28:27 +00:00
|
|
|
if activateOnDemandSetting.isActivateOnDemandEnabled {
|
2018-12-07 20:24:18 +00:00
|
|
|
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
|
|
|
|
} else {
|
|
|
|
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-07 19:24:58 +00:00
|
|
|
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
|
2018-11-28 07:11:35 +00:00
|
|
|
return .useOnDemandOverWiFiOrCellular
|
2018-11-12 10:22:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|