Rework "Trusted networks" to be a generic "On-demand" (#333)
Extend the feature by also providing a complementary "include" policy, i.e. activate the VPN _only_ on the specified networks. "Trusted networks" was only providing the "exclude" counterpart, i.e. _except_ the specified networks. Closes #119
This commit is contained in:
parent
1c3cbe02e5
commit
e0dbca224f
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- OpenVPN: Endpoint UX. [#332](https://github.com/passepartoutvpn/passepartout-apple/pull/332)
|
- OpenVPN: Endpoint UX. [#332](https://github.com/passepartoutvpn/passepartout-apple/pull/332)
|
||||||
|
- Convert trusted networks to on-demand activation. [#119](https://github.com/passepartoutvpn/passepartout-apple/issues/119)
|
||||||
|
|
||||||
## 2.1.2 (2023-07-06)
|
## 2.1.2 (2023-07-06)
|
||||||
|
|
||||||
|
|
|
@ -64,18 +64,56 @@ extension Profile.Header: Comparable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Profile.OpenVPNSettings {
|
extension Profile.OpenVPNSettings: StyledOptionalLocalizableEntity {
|
||||||
var endpointDescription: String? {
|
public enum OptionalStyle {
|
||||||
|
case endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
public func localizedDescription(optionalStyle: OptionalStyle) -> String? {
|
||||||
|
switch optionalStyle {
|
||||||
|
case .endpoint:
|
||||||
|
return endpointDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var endpointDescription: String? {
|
||||||
customEndpoint?.address ?? configuration.remotes?.first?.address
|
customEndpoint?.address ?? configuration.remotes?.first?.address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Profile.WireGuardSettings {
|
extension Profile.WireGuardSettings: StyledOptionalLocalizableEntity {
|
||||||
var endpointDescription: String? {
|
public enum OptionalStyle {
|
||||||
|
case endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
public func localizedDescription(optionalStyle: OptionalStyle) -> String? {
|
||||||
|
switch optionalStyle {
|
||||||
|
case .endpoint:
|
||||||
|
return endpointDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var endpointDescription: String? {
|
||||||
configuration.tunnelConfiguration.peers.first?.endpoint?.stringRepresentation
|
configuration.tunnelConfiguration.peers.first?.endpoint?.stringRepresentation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Profile.OnDemand.Policy: LocalizableEntity {
|
||||||
|
public var localizedDescription: String {
|
||||||
|
// FIXME: l10n, on-demand
|
||||||
|
switch self {
|
||||||
|
case .any:
|
||||||
|
return L10n.OnDemand.Policy.any
|
||||||
|
|
||||||
|
case .including:
|
||||||
|
return L10n.OnDemand.Policy.including
|
||||||
|
|
||||||
|
case .excluding:
|
||||||
|
return L10n.OnDemand.Policy.excluding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Network.Choice: LocalizableEntity {
|
extension Network.Choice: LocalizableEntity {
|
||||||
public var localizedDescription: String {
|
public var localizedDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
|
@ -41,11 +41,10 @@ struct OnDemandView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return List {
|
return List {
|
||||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
enabledView
|
||||||
// enabledView
|
if onDemand.isEnabled && onDemand.policy != .any {
|
||||||
// if onDemand.isEnabled {
|
|
||||||
mainView
|
mainView
|
||||||
// }
|
}
|
||||||
}.navigationTitle(L10n.OnDemand.title)
|
}.navigationTitle(L10n.OnDemand.title)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
CopySavingButton(
|
CopySavingButton(
|
||||||
|
@ -68,43 +67,62 @@ private extension OnDemandView {
|
||||||
var enabledView: some View {
|
var enabledView: some View {
|
||||||
Section {
|
Section {
|
||||||
Toggle(L10n.Global.Strings.enabled, isOn: $onDemand.isEnabled.themeAnimation())
|
Toggle(L10n.Global.Strings.enabled, isOn: $onDemand.isEnabled.themeAnimation())
|
||||||
|
if onDemand.isEnabled {
|
||||||
|
themeTextPicker(
|
||||||
|
// FIXME: l10n, on-demand
|
||||||
|
L10n.Global.Strings.policy,
|
||||||
|
selection: $onDemand.policy,
|
||||||
|
values: [.any, .including, .excluding],
|
||||||
|
description: \.localizedDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text(policyFooterDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: l10n, on-demand
|
||||||
|
var policyFooterDescription: String {
|
||||||
|
let suffix: String
|
||||||
|
switch onDemand.policy {
|
||||||
|
case .any:
|
||||||
|
suffix = L10n.OnDemand.Sections.Policy.Footer.any
|
||||||
|
|
||||||
|
case .including, .excluding:
|
||||||
|
let arg: String
|
||||||
|
if onDemand.policy == .including {
|
||||||
|
arg = L10n.OnDemand.Sections.Policy.Footer.including
|
||||||
|
} else {
|
||||||
|
arg = L10n.OnDemand.Sections.Policy.Footer.excluding
|
||||||
|
}
|
||||||
|
suffix = L10n.OnDemand.Sections.Policy.Footer.matching(arg)
|
||||||
|
}
|
||||||
|
return L10n.OnDemand.Sections.Policy.footer(suffix)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var mainView: some View {
|
var mainView: some View {
|
||||||
if Utils.hasCellularData() {
|
if Utils.hasCellularData() {
|
||||||
Section {
|
Section {
|
||||||
Toggle(L10n.OnDemand.Items.Mobile.caption, isOn: $onDemand.withMobileNetwork)
|
Toggle(L10n.OnDemand.Items.Mobile.caption, isOn: $onDemand.withMobileNetwork)
|
||||||
} header: {
|
} header: {
|
||||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
// FIXME: l10n, on-demand
|
||||||
// Text(L10n.Profile.Sections.Trusted.header)
|
Text(L10n.Global.Strings.networks)
|
||||||
}
|
|
||||||
Section {
|
|
||||||
SSIDList(withSSIDs: $onDemand.withSSIDs)
|
|
||||||
}
|
}
|
||||||
} else if Utils.hasEthernet() {
|
} else if Utils.hasEthernet() {
|
||||||
Section {
|
Section {
|
||||||
Toggle(L10n.OnDemand.Items.Ethernet.caption, isOn: $onDemand.withEthernetNetwork)
|
Toggle(L10n.OnDemand.Items.Ethernet.caption, isOn: $onDemand.withEthernetNetwork)
|
||||||
} header: {
|
} header: {
|
||||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
// FIXME: l10n, on-demand
|
||||||
// Text(L10n.Profile.Sections.Trusted.header)
|
Text(L10n.Global.Strings.networks)
|
||||||
}
|
|
||||||
Section {
|
|
||||||
SSIDList(withSSIDs: $onDemand.withSSIDs)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Section {
|
|
||||||
SSIDList(withSSIDs: $onDemand.withSSIDs)
|
|
||||||
} header: {
|
|
||||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
|
||||||
// Text(L10n.Profile.Sections.Trusted.header)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section {
|
Section {
|
||||||
Toggle(L10n.OnDemand.Items.Policy.caption, isOn: $onDemand.disconnectsIfNotMatching)
|
SSIDList(withSSIDs: $onDemand.withSSIDs)
|
||||||
} footer: {
|
} header: {
|
||||||
Text(L10n.OnDemand.Sections.Policy.footer)
|
if !Utils.hasCellularData() && !Utils.hasEthernet() {
|
||||||
|
Text(L10n.Global.Strings.networks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@
|
||||||
"global.strings.disconnect" = "Disconnect";
|
"global.strings.disconnect" = "Disconnect";
|
||||||
"global.strings.download" = "Download";
|
"global.strings.download" = "Download";
|
||||||
"global.strings.authentication" = "Authentication";
|
"global.strings.authentication" = "Authentication";
|
||||||
|
"global.strings.policy" = "Policy";
|
||||||
|
"global.strings.networks" = "Networks";
|
||||||
"global.messages.unlock_app" = "Passepartout is locked";
|
"global.messages.unlock_app" = "Passepartout is locked";
|
||||||
"global.messages.email_not_configured" = "No e-mail account is configured.";
|
"global.messages.email_not_configured" = "No e-mail account is configured.";
|
||||||
"global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS";
|
"global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS";
|
||||||
|
@ -251,14 +253,20 @@
|
||||||
|
|
||||||
/* MARK: ProfileView -> OnDemandView */
|
/* MARK: ProfileView -> OnDemandView */
|
||||||
|
|
||||||
"on_demand.title" = "Trusted networks";
|
"on_demand.title" = "On-demand";
|
||||||
"on_demand.sections.policy.footer" = "When entering a trusted network, the VPN is normally shut down and kept disconnected. Disable this option to not enforce such behavior.";
|
"on_demand.sections.policy.footer" = "Activate the VPN %@.";
|
||||||
|
"on_demand.sections.policy.footer.any" = "in any network";
|
||||||
|
"on_demand.sections.policy.footer.matching" = "%@ the networks below";
|
||||||
|
"on_demand.sections.policy.footer.including" = "only in";
|
||||||
|
"on_demand.sections.policy.footer.excluding" = "except in";
|
||||||
"on_demand.items.add_ssid.caption" = "Add Wi-Fi";
|
"on_demand.items.add_ssid.caption" = "Add Wi-Fi";
|
||||||
"on_demand.items.active.caption" = "Trust";
|
|
||||||
"on_demand.items.mobile.caption" = "Cellular network";
|
"on_demand.items.mobile.caption" = "Cellular network";
|
||||||
"on_demand.items.ethernet.caption" = "Trust wired connections";
|
"on_demand.items.ethernet.caption" = "Wired connections";
|
||||||
"on_demand.items.ethernet.description" = "Check to trust any wired cable connection.";
|
"on_demand.items.ethernet.description" = "Check to match any wired cable connection.";
|
||||||
"on_demand.items.policy.caption" = "Trust disables VPN";
|
|
||||||
|
"on_demand.policy.any" = "All networks";
|
||||||
|
"on_demand.policy.including" = "Include";
|
||||||
|
"on_demand.policy.excluding" = "Exclude";
|
||||||
|
|
||||||
/* MARK: ProfileView -> DiagnosticsView */
|
/* MARK: ProfileView -> DiagnosticsView */
|
||||||
|
|
||||||
|
|
|
@ -506,12 +506,16 @@ internal enum L10n {
|
||||||
internal static let manual = L10n.tr("Localizable", "global.strings.manual", fallback: "Manual")
|
internal static let manual = L10n.tr("Localizable", "global.strings.manual", fallback: "Manual")
|
||||||
/// Name
|
/// Name
|
||||||
internal static let name = L10n.tr("Localizable", "global.strings.name", fallback: "Name")
|
internal static let name = L10n.tr("Localizable", "global.strings.name", fallback: "Name")
|
||||||
|
/// Networks
|
||||||
|
internal static let networks = L10n.tr("Localizable", "global.strings.networks", fallback: "Networks")
|
||||||
/// Next
|
/// Next
|
||||||
internal static let next = L10n.tr("Localizable", "global.strings.next", fallback: "Next")
|
internal static let next = L10n.tr("Localizable", "global.strings.next", fallback: "Next")
|
||||||
/// None
|
/// None
|
||||||
internal static let `none` = L10n.tr("Localizable", "global.strings.none", fallback: "None")
|
internal static let `none` = L10n.tr("Localizable", "global.strings.none", fallback: "None")
|
||||||
/// MARK: Global
|
/// MARK: Global
|
||||||
internal static let ok = L10n.tr("Localizable", "global.strings.ok", fallback: "OK")
|
internal static let ok = L10n.tr("Localizable", "global.strings.ok", fallback: "OK")
|
||||||
|
/// Policy
|
||||||
|
internal static let policy = L10n.tr("Localizable", "global.strings.policy", fallback: "Policy")
|
||||||
/// Port
|
/// Port
|
||||||
internal static let port = L10n.tr("Localizable", "global.strings.port", fallback: "Port")
|
internal static let port = L10n.tr("Localizable", "global.strings.port", fallback: "Port")
|
||||||
/// Private key
|
/// Private key
|
||||||
|
@ -640,35 +644,49 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
internal enum OnDemand {
|
internal enum OnDemand {
|
||||||
/// MARK: ProfileView -> OnDemandView
|
/// MARK: ProfileView -> OnDemandView
|
||||||
internal static let title = L10n.tr("Localizable", "on_demand.title", fallback: "Trusted networks")
|
internal static let title = L10n.tr("Localizable", "on_demand.title", fallback: "On-demand")
|
||||||
internal enum Items {
|
internal enum Items {
|
||||||
internal enum Active {
|
|
||||||
/// Trust
|
|
||||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.active.caption", fallback: "Trust")
|
|
||||||
}
|
|
||||||
internal enum AddSsid {
|
internal enum AddSsid {
|
||||||
/// Add Wi-Fi
|
/// Add Wi-Fi
|
||||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.add_ssid.caption", fallback: "Add Wi-Fi")
|
internal static let caption = L10n.tr("Localizable", "on_demand.items.add_ssid.caption", fallback: "Add Wi-Fi")
|
||||||
}
|
}
|
||||||
internal enum Ethernet {
|
internal enum Ethernet {
|
||||||
/// Trust wired connections
|
/// Wired connections
|
||||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.ethernet.caption", fallback: "Trust wired connections")
|
internal static let caption = L10n.tr("Localizable", "on_demand.items.ethernet.caption", fallback: "Wired connections")
|
||||||
/// Check to trust any wired cable connection.
|
/// Check to match any wired cable connection.
|
||||||
internal static let description = L10n.tr("Localizable", "on_demand.items.ethernet.description", fallback: "Check to trust any wired cable connection.")
|
internal static let description = L10n.tr("Localizable", "on_demand.items.ethernet.description", fallback: "Check to match any wired cable connection.")
|
||||||
}
|
}
|
||||||
internal enum Mobile {
|
internal enum Mobile {
|
||||||
/// Cellular network
|
/// Cellular network
|
||||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.mobile.caption", fallback: "Cellular network")
|
internal static let caption = L10n.tr("Localizable", "on_demand.items.mobile.caption", fallback: "Cellular network")
|
||||||
}
|
}
|
||||||
internal enum Policy {
|
}
|
||||||
/// Trust disables VPN
|
internal enum Policy {
|
||||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.policy.caption", fallback: "Trust disables VPN")
|
/// All networks
|
||||||
}
|
internal static let any = L10n.tr("Localizable", "on_demand.policy.any", fallback: "All networks")
|
||||||
|
/// Exclude
|
||||||
|
internal static let excluding = L10n.tr("Localizable", "on_demand.policy.excluding", fallback: "Exclude")
|
||||||
|
/// Include
|
||||||
|
internal static let including = L10n.tr("Localizable", "on_demand.policy.including", fallback: "Include")
|
||||||
}
|
}
|
||||||
internal enum Sections {
|
internal enum Sections {
|
||||||
internal enum Policy {
|
internal enum Policy {
|
||||||
/// When entering a trusted network, the VPN is normally shut down and kept disconnected. Disable this option to not enforce such behavior.
|
/// Activate the VPN %@.
|
||||||
internal static let footer = L10n.tr("Localizable", "on_demand.sections.policy.footer", fallback: "When entering a trusted network, the VPN is normally shut down and kept disconnected. Disable this option to not enforce such behavior.")
|
internal static func footer(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "on_demand.sections.policy.footer", String(describing: p1), fallback: "Activate the VPN %@.")
|
||||||
|
}
|
||||||
|
internal enum Footer {
|
||||||
|
/// in any network
|
||||||
|
internal static let any = L10n.tr("Localizable", "on_demand.sections.policy.footer.any", fallback: "in any network")
|
||||||
|
/// except in
|
||||||
|
internal static let excluding = L10n.tr("Localizable", "on_demand.sections.policy.footer.excluding", fallback: "except in")
|
||||||
|
/// only in
|
||||||
|
internal static let including = L10n.tr("Localizable", "on_demand.sections.policy.footer.including", fallback: "only in")
|
||||||
|
/// %@ the networks below
|
||||||
|
internal static func matching(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "on_demand.sections.policy.footer.matching", String(describing: p1), fallback: "%@ the networks below")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,18 +41,14 @@ extension Profile {
|
||||||
case ethernet
|
case ethernet
|
||||||
}
|
}
|
||||||
|
|
||||||
// hardcode this to keep "Trusted networks" semantics
|
|
||||||
public var isEnabled = true
|
public var isEnabled = true
|
||||||
|
|
||||||
// hardcode this to keep "Trusted networks" semantics
|
|
||||||
public var policy: Policy = .excluding
|
public var policy: Policy = .excluding
|
||||||
|
|
||||||
public var withSSIDs: [String: Bool] = [:]
|
public var withSSIDs: [String: Bool] = [:]
|
||||||
|
|
||||||
public var withOtherNetworks: Set<OtherNetwork> = []
|
public var withOtherNetworks: Set<OtherNetwork> = []
|
||||||
|
|
||||||
public var disconnectsIfNotMatching = true
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import PassepartoutVPN
|
||||||
extension NEOnDemandRuleInterfaceType {
|
extension NEOnDemandRuleInterfaceType {
|
||||||
static var compatibleEthernet: NEOnDemandRuleInterfaceType? {
|
static var compatibleEthernet: NEOnDemandRuleInterfaceType? {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
// FIXME: Catalyst, missing enum case, try hardcoding
|
// XXX: Catalyst, missing enum case, try hardcoding
|
||||||
// https://developer.apple.com/documentation/networkextension/neondemandruleinterfacetype/ethernet
|
// https://developer.apple.com/documentation/networkextension/neondemandruleinterfacetype/ethernet
|
||||||
NEOnDemandRuleInterfaceType(rawValue: 1)
|
NEOnDemandRuleInterfaceType(rawValue: 1)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
|
@ -54,24 +54,17 @@ private extension Profile.OnDemand {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand"
|
|
||||||
// isEnabled = true
|
|
||||||
// policy = .excluding
|
|
||||||
assert(policy == .excluding)
|
|
||||||
|
|
||||||
var rules: [NEOnDemandRule] = []
|
var rules: [NEOnDemandRule] = []
|
||||||
if withCustomRules {
|
|
||||||
#if os(iOS)
|
// apply exceptions (unless .any)
|
||||||
|
if withCustomRules && policy != .any {
|
||||||
|
#if os(iOS)
|
||||||
if Utils.hasCellularData() && withMobileNetwork {
|
if Utils.hasCellularData() && withMobileNetwork {
|
||||||
let rule = policyRule
|
rules.append(cellularRule())
|
||||||
rule.interfaceTypeMatch = .cellular
|
|
||||||
rules.append(rule)
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if Utils.hasEthernet() && withEthernetNetwork {
|
if Utils.hasEthernet() && withEthernetNetwork {
|
||||||
if let compatibleEthernet = NEOnDemandRuleInterfaceType.compatibleEthernet {
|
if let rule = ethernetRule() {
|
||||||
let rule = policyRule
|
|
||||||
rule.interfaceTypeMatch = compatibleEthernet
|
|
||||||
rules.append(rule)
|
rules.append(rule)
|
||||||
} else {
|
} else {
|
||||||
pp_log.warning("Unable to add rule for NEOnDemandRuleInterfaceType.ethernet (not compatible)")
|
pp_log.warning("Unable to add rule for NEOnDemandRuleInterfaceType.ethernet (not compatible)")
|
||||||
|
@ -79,19 +72,58 @@ private extension Profile.OnDemand {
|
||||||
}
|
}
|
||||||
let SSIDs = Array(withSSIDs.filter { $1 }.keys)
|
let SSIDs = Array(withSSIDs.filter { $1 }.keys)
|
||||||
if !SSIDs.isEmpty {
|
if !SSIDs.isEmpty {
|
||||||
let rule = policyRule
|
rules.append(wifiRule(SSIDs: SSIDs))
|
||||||
rule.interfaceTypeMatch = .wiFi
|
|
||||||
rule.ssidMatch = SSIDs
|
|
||||||
rules.append(rule)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let connection = NEOnDemandRuleConnect()
|
|
||||||
connection.interfaceTypeMatch = .any
|
// IMPORTANT: append fallback rule last
|
||||||
rules.append(connection)
|
rules.append(globalRule())
|
||||||
|
|
||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var policyRule: NEOnDemandRule {
|
private extension Profile.OnDemand {
|
||||||
disconnectsIfNotMatching ? NEOnDemandRuleDisconnect() : NEOnDemandRuleIgnore()
|
func globalRule() -> NEOnDemandRule {
|
||||||
|
let rule: NEOnDemandRule
|
||||||
|
switch policy {
|
||||||
|
case .any, .excluding:
|
||||||
|
rule = NEOnDemandRuleConnect()
|
||||||
|
|
||||||
|
case .including:
|
||||||
|
rule = NEOnDemandRuleDisconnect()
|
||||||
|
}
|
||||||
|
rule.interfaceTypeMatch = .any
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkRule(matchingInterface interfaceType: NEOnDemandRuleInterfaceType) -> NEOnDemandRule {
|
||||||
|
let rule: NEOnDemandRule
|
||||||
|
switch policy {
|
||||||
|
case .any, .excluding:
|
||||||
|
rule = NEOnDemandRuleDisconnect()
|
||||||
|
|
||||||
|
case .including:
|
||||||
|
rule = NEOnDemandRuleConnect()
|
||||||
|
}
|
||||||
|
rule.interfaceTypeMatch = interfaceType
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellularRule() -> NEOnDemandRule {
|
||||||
|
networkRule(matchingInterface: .cellular)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ethernetRule() -> NEOnDemandRule? {
|
||||||
|
guard let compatibleEthernet = NEOnDemandRuleInterfaceType.compatibleEthernet else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return networkRule(matchingInterface: compatibleEthernet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wifiRule(SSIDs: [String]) -> NEOnDemandRule {
|
||||||
|
let rule = networkRule(matchingInterface: .wiFi)
|
||||||
|
rule.ssidMatch = SSIDs
|
||||||
|
return rule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue