Model, Tunnels manager: Rewrite the model for VPN-on-demand

The VPN-on-demand settings should not be part of the tunnel
configuration. Rather, the onDemandRules stored in the
tunnel provider configuration serve as the one place
where the VPN-on-demand settings are stored.

Signed-off-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
Roopesh Chander 2018-11-12 14:02:09 +05:30
parent 39a067cb96
commit cc122d7463
6 changed files with 92 additions and 130 deletions

View File

@ -1,66 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
enum ActivationType {
case activateManually
case useOnDemandOverWifiAndCellular
case useOnDemandOverWifiOnly
case useOnDemandOverCellularOnly
}
extension ActivationType: Codable {
// We use separate coding keys in case we might have a enum with associated values in the future
enum CodingKeys: CodingKey {
case activateManually
case useOnDemandOverWifiAndCellular
case useOnDemandOverWifiOnly
case useOnDemandOverCellularOnly
}
// Decoding error
enum DecodingError: Error {
case invalidInput
}
// Encoding
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .activateManually:
try container.encode(true, forKey: CodingKeys.activateManually)
case .useOnDemandOverWifiAndCellular:
try container.encode(true, forKey: CodingKeys.useOnDemandOverWifiAndCellular)
case .useOnDemandOverWifiOnly:
try container.encode(true, forKey: CodingKeys.useOnDemandOverWifiOnly)
case .useOnDemandOverCellularOnly:
try container.encode(true, forKey: CodingKeys.useOnDemandOverCellularOnly)
}
}
// Decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let isValid = try? container.decode(Bool.self, forKey: CodingKeys.activateManually), isValid {
self = .activateManually
return
}
if let isValid = try? container.decode(Bool.self, forKey: CodingKeys.useOnDemandOverWifiAndCellular), isValid {
self = .useOnDemandOverWifiAndCellular
return
}
if let isValid = try? container.decode(Bool.self, forKey: CodingKeys.useOnDemandOverWifiOnly), isValid {
self = .useOnDemandOverWifiOnly
return
}
if let isValid = try? container.decode(Bool.self, forKey: CodingKeys.useOnDemandOverCellularOnly), isValid {
self = .useOnDemandOverCellularOnly
return
}
throw DecodingError.invalidInput
}
}

View File

@ -4,14 +4,12 @@
import Foundation import Foundation
@available(OSX 10.14, iOS 12.0, *) @available(OSX 10.14, iOS 12.0, *)
final class TunnelConfiguration { final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration var interface: InterfaceConfiguration
let peers: [PeerConfiguration] let peers: [PeerConfiguration]
var activationType: ActivationType
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) { init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface self.interface = interface
self.peers = peers self.peers = peers
self.activationType = .activateManually
let peerPublicKeysArray = peers.map { $0.publicKey } let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
@ -57,21 +55,3 @@ struct PeerConfiguration: Codable {
if (publicKey.count != 32) { fatalError("Invalid public key") } if (publicKey.count != 32) { fatalError("Invalid public key") }
} }
} }
extension TunnelConfiguration: Encodable { }
extension TunnelConfiguration: Decodable {
enum CodingKeys: CodingKey {
case interface
case peers
case activationType
}
convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let interface = try values.decode(InterfaceConfiguration.self, forKey: .interface)
let peers = try values.decode([PeerConfiguration].self, forKey: .peers)
let activationType = (try? values.decode(ActivationType.self, forKey: .activationType)) ?? .activateManually
self.init(interface: interface, peers: peers)
self.activationType = activationType
}
}

View File

@ -14,7 +14,7 @@ extension NETunnelProviderProtocol {
providerBundleIdentifier = "\(appId).network-extension" providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [ providerConfiguration = [
"tunnelConfiguration": serializedTunnelConfiguration, "tunnelConfiguration": serializedTunnelConfiguration,
"tunnelConfigurationVersion": 2 "tunnelConfigurationVersion": 1
] ]
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint}) let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})

View File

@ -49,8 +49,7 @@
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; }; 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; }; 6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; }; 6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
6FFA5DA42197085D0001E2F7 /* ActivationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivationType.swift */; }; 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; };
6FFA5DA521970B370001E2F7 /* ActivationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivationType.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -135,7 +134,7 @@
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; }; 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; }; 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; }; 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
6FFA5DA32197085D0001E2F7 /* ActivationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationType.swift; sourceTree = "<group>"; }; 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -232,7 +231,6 @@
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */, 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */,
6F693A552179E556008551C1 /* Endpoint.swift */, 6F693A552179E556008551C1 /* Endpoint.swift */,
6F628C3E217F3413003482A3 /* DNSServer.swift */, 6F628C3E217F3413003482A3 /* DNSServer.swift */,
6FFA5DA32197085D0001E2F7 /* ActivationType.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -241,6 +239,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */, 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
); );
path = VPN; path = VPN;
sourceTree = "<group>"; sourceTree = "<group>";
@ -498,7 +497,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FFA5DA521970B370001E2F7 /* ActivationType.swift in Sources */,
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */, 6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */, 6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */,
@ -540,7 +538,7 @@
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */, 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */, 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivationType.swift in Sources */, 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
struct ActivateOnDemandSetting {
var isActivateOnDemandEnabled: Bool
var activateOnDemandOption: ActivateOnDemandOption
}
enum ActivateOnDemandOption {
case none // Valid only when isActivateOnDemandEnabled is false
case useOnDemandOverWifiOrCellular
case useOnDemandOverWifiOnly
case useOnDemandOverCellularOnly
}
extension ActivateOnDemandSetting {
func apply(on tunnelProviderManager: NETunnelProviderManager) {
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect()
switch (activateOnDemandOption) {
case .none:
rules = nil
case .useOnDemandOverWifiOrCellular:
rules = [connectRule]
case .useOnDemandOverWifiOnly:
connectRule.interfaceTypeMatch = .wiFi
disconnectRule.interfaceTypeMatch = .cellular
rules = [connectRule, disconnectRule]
case .useOnDemandOverCellularOnly:
connectRule.interfaceTypeMatch = .cellular
disconnectRule.interfaceTypeMatch = .wiFi
rules = [connectRule, disconnectRule]
}
tunnelProviderManager.onDemandRules = rules
}
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption
switch (rules.count) {
case 0:
activateOnDemandOption = .none
case 1:
let rule = rules[0]
precondition(rule.action == .connect)
activateOnDemandOption = .useOnDemandOverWifiOrCellular
case 2:
let connectRule = rules.first(where: { $0.action == .connect })!
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
if (connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular) {
activateOnDemandOption = .useOnDemandOverWifiOnly
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) {
activateOnDemandOption = .useOnDemandOverCellularOnly
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
}
default:
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self.activateOnDemandOption = activateOnDemandOption
if (activateOnDemandOption == .none) {
self.isActivateOnDemandEnabled = false
} else {
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
}
}
}
extension ActivateOnDemandSetting {
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
}

View File

@ -54,7 +54,9 @@ class TunnelsManager {
#endif #endif
} }
func add(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) { func add(tunnelConfiguration: TunnelConfiguration,
activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting,
completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
@ -72,13 +74,7 @@ class TunnelsManager {
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
if (tunnelConfiguration.activationType == .activateManually) { activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.onDemandRules = []
tunnelProviderManager.isOnDemandEnabled = false
} else {
tunnelProviderManager.onDemandRules = onDemandRules(for: tunnelConfiguration.activationType)
tunnelProviderManager.isOnDemandEnabled = true
}
tunnelProviderManager.saveToPreferences { [weak self] (error) in tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isAddingTunnel = false } defer { self?.isAddingTunnel = false }
@ -114,7 +110,8 @@ class TunnelsManager {
} }
} }
func modify(tunnel: TunnelContainer, with tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelManagementError?) -> Void) { func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelManagementError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName) completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
@ -138,13 +135,7 @@ class TunnelsManager {
tunnelProviderManager.localizedDescription = tunnelName tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
if (tunnelConfiguration.activationType == .activateManually) { activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.onDemandRules = []
tunnelProviderManager.isOnDemandEnabled = false
} else {
tunnelProviderManager.onDemandRules = onDemandRules(for: tunnelConfiguration.activationType)
tunnelProviderManager.isOnDemandEnabled = true
}
tunnelProviderManager.saveToPreferences { [weak self] (error) in tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isModifyingTunnel = false } defer { self?.isModifyingTunnel = false }
@ -229,26 +220,6 @@ class TunnelsManager {
t.refreshConnectionStatus() t.refreshConnectionStatus()
} }
} }
func onDemandRules(for activationType: ActivationType) -> [NEOnDemandRule] {
switch (activationType) {
case .activateManually: return []
case .useOnDemandOverWifiAndCellular:
return [NEOnDemandRuleConnect()]
case .useOnDemandOverWifiOnly:
let connectOnWifiRule = NEOnDemandRuleConnect()
connectOnWifiRule.interfaceTypeMatch = .wiFi
let disconnectOnCellularRule = NEOnDemandRuleDisconnect()
disconnectOnCellularRule.interfaceTypeMatch = .cellular
return [connectOnWifiRule, disconnectOnCellularRule]
case .useOnDemandOverCellularOnly:
let connectOnCellularRule = NEOnDemandRuleConnect()
connectOnCellularRule.interfaceTypeMatch = .cellular
let disconnectOnWifiRule = NEOnDemandRuleDisconnect()
disconnectOnWifiRule.interfaceTypeMatch = .wiFi
return [connectOnCellularRule, disconnectOnWifiRule]
}
}
} }
class TunnelContainer: NSObject { class TunnelContainer: NSObject {
@ -275,6 +246,10 @@ class TunnelContainer: NSObject {
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration() return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
} }
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshConnectionStatus() { func refreshConnectionStatus() {
let status = TunnelStatus(from: self.tunnelProvider.connection.status) let status = TunnelStatus(from: self.tunnelProvider.connection.status)
self.status = status self.status = status