Skip to content

Commit

Permalink
Rework trusted networks to be generic on-demand
Browse files Browse the repository at this point in the history
Drop onDemand.disconnectsIfNotMatching on the way.
  • Loading branch information
keeshux committed Jul 22, 2023
1 parent 0c0a9d0 commit 51adf9a
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions Passepartout/App/L10n/Core+L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ extension Profile.WireGuardSettings: StyledOptionalLocalizableEntity {
}
}

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 {
Expand Down
52 changes: 37 additions & 15 deletions Passepartout/App/Views/OnDemandView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -68,17 +67,47 @@ 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
var mainView: some View {
// FIXME: l10n, on-demand
if Utils.hasCellularData() {
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)
Text(L10n.Global.Strings.networks)
}
Section {
SSIDList(withSSIDs: $onDemand.withSSIDs)
Expand All @@ -87,8 +116,7 @@ private extension OnDemandView {
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)
Text(L10n.Global.Strings.networks)
}
Section {
SSIDList(withSSIDs: $onDemand.withSSIDs)
Expand All @@ -97,15 +125,9 @@ private extension OnDemandView {
Section {
SSIDList(withSSIDs: $onDemand.withSSIDs)
} header: {
// TODO: on-demand, restore when "trusted networks" -> "on-demand"
// Text(L10n.Profile.Sections.Trusted.header)
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 {
Expand Down
20 changes: 14 additions & 6 deletions Passepartout/App/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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" = "on any network";
"on_demand.sections.policy.footer.matching" = "%@ when connected to the networks below";
"on_demand.sections.policy.footer.including" = "only";
"on_demand.sections.policy.footer.excluding" = "except";
"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 */

Expand Down
48 changes: 33 additions & 15 deletions Passepartout/AppShared/Constants/SwiftGen+Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
/// on any network
internal static let any = L10n.tr("Localizable", "on_demand.sections.policy.footer.any", fallback: "on any network")
/// except
internal static let excluding = L10n.tr("Localizable", "on_demand.sections.policy.footer.excluding", fallback: "except")
/// only
internal static let including = L10n.tr("Localizable", "on_demand.sections.policy.footer.including", fallback: "only")
/// %@ when connected to the networks below
internal static func matching(_ p1: Any) -> String {
return L10n.tr("Localizable", "on_demand.sections.policy.footer.matching", String(describing: p1), fallback: "%@ when connected to the networks below")
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -54,44 +54,76 @@ 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)")
}
}
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)

// 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)
}

var policyRule: NEOnDemandRule {
disconnectsIfNotMatching ? NEOnDemandRuleDisconnect() : NEOnDemandRuleIgnore()
func wifiRule(SSIDs: [String]) -> NEOnDemandRule {
let rule = networkRule(matchingInterface: .wiFi)
rule.ssidMatch = SSIDs
return rule
}
}

0 comments on commit 51adf9a

Please sign in to comment.