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
|
||||
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -64,18 +64,56 @@ extension Profile.Header: Comparable {
|
|||
}
|
||||
}
|
||||
|
||||
extension Profile.OpenVPNSettings {
|
||||
var endpointDescription: String? {
|
||||
extension Profile.OpenVPNSettings: StyledOptionalLocalizableEntity {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
extension Profile.WireGuardSettings {
|
||||
var endpointDescription: String? {
|
||||
extension Profile.WireGuardSettings: StyledOptionalLocalizableEntity {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
public var localizedDescription: String {
|
||||
switch self {
|
||||
|
|
|
@ -41,11 +41,10 @@ struct OnDemandView: View {
|
|||
var body: some View {
|
||||
debugChanges()
|
||||
return List {
|
||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
||||
// enabledView
|
||||
// if onDemand.isEnabled {
|
||||
enabledView
|
||||
if onDemand.isEnabled && onDemand.policy != .any {
|
||||
mainView
|
||||
// }
|
||||
}
|
||||
}.navigationTitle(L10n.OnDemand.title)
|
||||
.toolbar {
|
||||
CopySavingButton(
|
||||
|
@ -68,7 +67,37 @@ private extension OnDemandView {
|
|||
var enabledView: some View {
|
||||
Section {
|
||||
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
|
||||
|
@ -77,35 +106,24 @@ private extension OnDemandView {
|
|||
Section {
|
||||
Toggle(L10n.OnDemand.Items.Mobile.caption, isOn: $onDemand.withMobileNetwork)
|
||||
} header: {
|
||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
||||
// Text(L10n.Profile.Sections.Trusted.header)
|
||||
}
|
||||
Section {
|
||||
SSIDList(withSSIDs: $onDemand.withSSIDs)
|
||||
// FIXME: l10n, on-demand
|
||||
Text(L10n.Global.Strings.networks)
|
||||
}
|
||||
} else if Utils.hasEthernet() {
|
||||
Section {
|
||||
Toggle(L10n.OnDemand.Items.Ethernet.caption, isOn: $onDemand.withEthernetNetwork)
|
||||
} header: {
|
||||
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
|
||||
// Text(L10n.Profile.Sections.Trusted.header)
|
||||
// FIXME: l10n, on-demand
|
||||
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)
|
||||
if !Utils.hasCellularData() && !Utils.hasEthernet() {
|
||||
Text(L10n.Global.Strings.networks)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
Toggle(L10n.OnDemand.Items.Policy.caption, isOn: $onDemand.disconnectsIfNotMatching)
|
||||
} footer: {
|
||||
Text(L10n.OnDemand.Sections.Policy.footer)
|
||||
}
|
||||
}
|
||||
|
||||
var isEligibleForSiri: Bool {
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
"global.strings.disconnect" = "Disconnect";
|
||||
"global.strings.download" = "Download";
|
||||
"global.strings.authentication" = "Authentication";
|
||||
"global.strings.policy" = "Policy";
|
||||
"global.strings.networks" = "Networks";
|
||||
"global.messages.unlock_app" = "Passepartout is locked";
|
||||
"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";
|
||||
|
@ -251,14 +253,20 @@
|
|||
|
||||
/* MARK: ProfileView -> OnDemandView */
|
||||
|
||||
"on_demand.title" = "Trusted networks";
|
||||
"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.title" = "On-demand";
|
||||
"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.active.caption" = "Trust";
|
||||
"on_demand.items.mobile.caption" = "Cellular network";
|
||||
"on_demand.items.ethernet.caption" = "Trust wired connections";
|
||||
"on_demand.items.ethernet.description" = "Check to trust any wired cable connection.";
|
||||
"on_demand.items.policy.caption" = "Trust disables VPN";
|
||||
"on_demand.items.ethernet.caption" = "Wired connections";
|
||||
"on_demand.items.ethernet.description" = "Check to match any wired cable connection.";
|
||||
|
||||
"on_demand.policy.any" = "All networks";
|
||||
"on_demand.policy.including" = "Include";
|
||||
"on_demand.policy.excluding" = "Exclude";
|
||||
|
||||
/* MARK: ProfileView -> DiagnosticsView */
|
||||
|
||||
|
|
|
@ -506,12 +506,16 @@ internal enum L10n {
|
|||
internal static let manual = L10n.tr("Localizable", "global.strings.manual", fallback: "Manual")
|
||||
/// 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
|
||||
internal static let next = L10n.tr("Localizable", "global.strings.next", fallback: "Next")
|
||||
/// None
|
||||
internal static let `none` = L10n.tr("Localizable", "global.strings.none", fallback: "None")
|
||||
/// MARK: Global
|
||||
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
|
||||
internal static let port = L10n.tr("Localizable", "global.strings.port", fallback: "Port")
|
||||
/// Private key
|
||||
|
@ -640,35 +644,49 @@ internal enum L10n {
|
|||
}
|
||||
internal enum OnDemand {
|
||||
/// 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 Active {
|
||||
/// Trust
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.active.caption", fallback: "Trust")
|
||||
}
|
||||
internal enum AddSsid {
|
||||
/// Add Wi-Fi
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.add_ssid.caption", fallback: "Add Wi-Fi")
|
||||
}
|
||||
internal enum Ethernet {
|
||||
/// Trust wired connections
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.ethernet.caption", fallback: "Trust wired connections")
|
||||
/// Check to trust any wired cable connection.
|
||||
internal static let description = L10n.tr("Localizable", "on_demand.items.ethernet.description", fallback: "Check to trust any wired cable connection.")
|
||||
/// Wired connections
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.ethernet.caption", fallback: "Wired connections")
|
||||
/// Check to match 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 {
|
||||
/// Cellular network
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.mobile.caption", fallback: "Cellular network")
|
||||
}
|
||||
internal enum Policy {
|
||||
/// Trust disables VPN
|
||||
internal static let caption = L10n.tr("Localizable", "on_demand.items.policy.caption", fallback: "Trust disables VPN")
|
||||
}
|
||||
internal enum Policy {
|
||||
/// 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 Policy {
|
||||
/// When entering a trusted network, the VPN is normally shut down and kept disconnected. Disable this option to not enforce such behavior.
|
||||
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.")
|
||||
/// Activate the VPN %@.
|
||||
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
|
||||
}
|
||||
|
||||
// hardcode this to keep "Trusted networks" semantics
|
||||
public var isEnabled = true
|
||||
|
||||
// hardcode this to keep "Trusted networks" semantics
|
||||
public var policy: Policy = .excluding
|
||||
|
||||
public var withSSIDs: [String: Bool] = [:]
|
||||
|
||||
public var withOtherNetworks: Set<OtherNetwork> = []
|
||||
|
||||
public var disconnectsIfNotMatching = true
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import PassepartoutVPN
|
|||
extension NEOnDemandRuleInterfaceType {
|
||||
static var compatibleEthernet: NEOnDemandRuleInterfaceType? {
|
||||
#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
|
||||
NEOnDemandRuleInterfaceType(rawValue: 1)
|
||||
#elseif os(macOS)
|
||||
|
@ -54,24 +54,17 @@ private extension Profile.OnDemand {
|
|||
return []
|
||||
}
|
||||
|
||||
// TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand"
|
||||
// isEnabled = true
|
||||
// policy = .excluding
|
||||
assert(policy == .excluding)
|
||||
|
||||
var rules: [NEOnDemandRule] = []
|
||||
if withCustomRules {
|
||||
#if os(iOS)
|
||||
|
||||
// apply exceptions (unless .any)
|
||||
if withCustomRules && policy != .any {
|
||||
#if os(iOS)
|
||||
if Utils.hasCellularData() && withMobileNetwork {
|
||||
let rule = policyRule
|
||||
rule.interfaceTypeMatch = .cellular
|
||||
rules.append(rule)
|
||||
rules.append(cellularRule())
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
if Utils.hasEthernet() && withEthernetNetwork {
|
||||
if let compatibleEthernet = NEOnDemandRuleInterfaceType.compatibleEthernet {
|
||||
let rule = policyRule
|
||||
rule.interfaceTypeMatch = compatibleEthernet
|
||||
if let rule = ethernetRule() {
|
||||
rules.append(rule)
|
||||
} else {
|
||||
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)
|
||||
if !SSIDs.isEmpty {
|
||||
let rule = policyRule
|
||||
rule.interfaceTypeMatch = .wiFi
|
||||
rule.ssidMatch = SSIDs
|
||||
rules.append(rule)
|
||||
rules.append(wifiRule(SSIDs: SSIDs))
|
||||
}
|
||||
}
|
||||
let connection = NEOnDemandRuleConnect()
|
||||
connection.interfaceTypeMatch = .any
|
||||
rules.append(connection)
|
||||
return rules
|
||||
}
|
||||
|
||||
var policyRule: NEOnDemandRule {
|
||||
disconnectsIfNotMatching ? NEOnDemandRuleDisconnect() : NEOnDemandRuleIgnore()
|
||||
// IMPORTANT: append fallback rule last
|
||||
rules.append(globalRule())
|
||||
|
||||
return rules
|
||||
}
|
||||
}
|
||||
|
||||
private extension Profile.OnDemand {
|
||||
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