diff --git a/Passepartout/App/Mac/Models/DefaultLightProfileManager.swift b/Passepartout/App/Mac/Models/DefaultLightProfileManager.swift index 780c4b10..fb8c56bd 100644 --- a/Passepartout/App/Mac/Models/DefaultLightProfileManager.swift +++ b/Passepartout/App/Mac/Models/DefaultLightProfileManager.swift @@ -77,7 +77,7 @@ class DefaultLightProfileManager: LightProfileManager { $0.header < $1.header }.map { let server: ProviderServer? - if let serverId = $0.providerServerId() { + if let serverId = $0.providerServerId { server = providerManager.server(withId: serverId) } else { server = nil diff --git a/Passepartout/App/Views/EndpointView+OpenVPN.swift b/Passepartout/App/Views/EndpointView+OpenVPN.swift index 2bcb788b..13e7ddb9 100644 --- a/Passepartout/App/Views/EndpointView+OpenVPN.swift +++ b/Passepartout/App/Views/EndpointView+OpenVPN.swift @@ -92,13 +92,13 @@ extension EndpointView { } _customEndpoint = .init { if currentProfile.value.isProvider { - return currentProfile.value.providerCustomEndpoint() + return currentProfile.value.providerCustomEndpoint } else { return currentProfile.value.hostOpenVPNSettings?.customEndpoint } } set: { if currentProfile.value.isProvider { - currentProfile.value.setProviderCustomEndpoint($0) + currentProfile.value.providerCustomEndpoint = $0 } else { currentProfile.value.hostOpenVPNSettings?.customEndpoint = $0 } diff --git a/Passepartout/App/Views/ProfileView+Extra.swift b/Passepartout/App/Views/ProfileView+Extra.swift index 28fc8daa..18346f06 100644 --- a/Passepartout/App/Views/ProfileView+Extra.swift +++ b/Passepartout/App/Views/ProfileView+Extra.swift @@ -35,16 +35,6 @@ extension ProfileView { } var body: some View { - if currentProfile.value.isProvider { - Section { - Toggle( - L10n.Profile.Items.VpnResolvesHostname.caption, - isOn: $currentProfile.value.networkSettings.resolvesHostname - ) - } footer: { - Text(L10n.Profile.Sections.VpnResolvesHostname.footer) - } - } Section { Toggle( L10n.Profile.Items.VpnSurvivesSleep.caption, diff --git a/Passepartout/App/Views/ProfileView+Provider.swift b/Passepartout/App/Views/ProfileView+Provider.swift index 3f3ed40f..9252aa54 100644 --- a/Passepartout/App/Views/ProfileView+Provider.swift +++ b/Passepartout/App/Views/ProfileView+Provider.swift @@ -77,6 +77,18 @@ extension ProfileView { } footer: { currentProviderServerDescription.map(Text.init) } + Section { + Toggle( + L10n.Profile.Items.RandomizesServer.caption, + isOn: $currentProfile.value.providerRandomizesServer ?? false + ) + Toggle( + L10n.Profile.Items.VpnResolvesHostname.caption, + isOn: $currentProfile.value.networkSettings.resolvesHostname + ) + } footer: { + Text(L10n.Profile.Sections.VpnResolvesHostname.footer) + } Section { NavigationLink { ProviderPresetView(currentProfile: currentProfile) @@ -107,7 +119,14 @@ extension ProfileView { } private var currentProviderServerDescription: String? { - profile.providerServer(providerManager)?.localizedLongDescription(withCategory: true) + guard let server = profile.providerServer(providerManager) else { + return nil + } + if currentProfile.value.providerRandomizesServer ?? false { + return server.localizedCountry(withCategory: true) + } else { + return server.localizedLongDescription(withCategory: true) + } } private var currentProviderCountryImage: Image? { diff --git a/Passepartout/App/Views/ProviderLocationView.swift b/Passepartout/App/Views/ProviderLocationView.swift index 80ff1515..0c114ecb 100644 --- a/Passepartout/App/Views/ProviderLocationView.swift +++ b/Passepartout/App/Views/ProviderLocationView.swift @@ -71,7 +71,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability { self.isEditable = isEditable _selectedServer = .init { - guard let serverId = currentProfile.value.providerServerId() else { + guard let serverId = currentProfile.value.providerServerId else { return nil } return providerManager.server(withId: serverId) @@ -84,9 +84,9 @@ struct ProviderLocationView: View, ProviderProfileAvailability { isPresented.wrappedValue = false } _favoriteLocationIds = .init { - currentProfile.value.providerFavoriteLocationIds() + currentProfile.value.providerFavoriteLocationIds } set: { - currentProfile.value.setProviderFavoriteLocationIds($0) + currentProfile.value.providerFavoriteLocationIds = $0 } } @@ -151,6 +151,8 @@ struct ProviderLocationView: View, ProviderProfileAvailability { private func locationRow(_ location: ProviderLocation) -> some View { if let onlyServer = location.onlyServer { singleServerRow(location, onlyServer) + } else if profile.providerRandomizesServer ?? false { + singleServerRow(location, nil) } else { multipleServersRow(location) } @@ -170,9 +172,9 @@ struct ProviderLocationView: View, ProviderProfileAvailability { }) } - private func singleServerRow(_ location: ProviderLocation, _ server: ProviderServer) -> some View { + private func singleServerRow(_ location: ProviderLocation, _ server: ProviderServer?) -> some View { Button { - selectedServer = server + selectedServer = server ?? location.servers?.randomElement() } label: { LocationRow( location: location, diff --git a/Passepartout/App/Views/ProviderPresetView.swift b/Passepartout/App/Views/ProviderPresetView.swift index 50496f1d..400e14e9 100644 --- a/Passepartout/App/Views/ProviderPresetView.swift +++ b/Passepartout/App/Views/ProviderPresetView.swift @@ -46,7 +46,7 @@ struct ProviderPresetView: View { server = currentProfile.value.providerServer(providerManager) _selectedPreset = .init { - guard let serverId = currentProfile.value.providerServerId() else { + guard let serverId = currentProfile.value.providerServerId else { return nil } guard let server = providerManager.server(withId: serverId) else { diff --git a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift index fecda60c..d377e0b2 100644 --- a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift +++ b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift @@ -795,6 +795,10 @@ internal enum L10n { internal static let caption = L10n.tr("Localizable", "profile.items.provider.refresh.caption", fallback: "Refresh infrastructure") } } + internal enum RandomizesServer { + /// Randomize server + internal static let caption = L10n.tr("Localizable", "profile.items.randomizes_server.caption", fallback: "Randomize server") + } internal enum UseProfile { /// Use this profile internal static let caption = L10n.tr("Localizable", "profile.items.use_profile.caption", fallback: "Use this profile") diff --git a/Passepartout/AppShared/L10n/Providers+L10n.swift b/Passepartout/AppShared/L10n/Providers+L10n.swift index 6301d9fe..06e975f0 100644 --- a/Passepartout/AppShared/L10n/Providers+L10n.swift +++ b/Passepartout/AppShared/L10n/Providers+L10n.swift @@ -70,6 +70,14 @@ extension ProviderServer { countryCode.localizedAsCountryCode } + func localizedCountry(withCategory: Bool) -> String { + let desc = localizedCountry + if withCategory, !categoryName.isEmpty { + return "\(categoryName.uppercased()): \(desc)" + } + return desc + } + var localizedShortDescription: String? { var comps = localizedName.map { [$0] } ?? [] if let serverIndex = serverIndex { diff --git a/Passepartout/AppShared/en.lproj/Localizable.strings b/Passepartout/AppShared/en.lproj/Localizable.strings index 95615d16..affcba1d 100644 --- a/Passepartout/AppShared/en.lproj/Localizable.strings +++ b/Passepartout/AppShared/en.lproj/Localizable.strings @@ -152,6 +152,7 @@ "profile.items.vpn.turn_off.caption" = "Disable VPN"; "profile.items.connection_status.caption" = "Status"; "profile.items.data_count.caption" = "Exchanged data"; +"profile.items.randomizes_server.caption" = "Randomize server"; "profile.items.provider.refresh.caption" = "Refresh infrastructure"; "profile.items.category.caption" = "Category"; "profile.items.only_shows_favorites.caption" = "Only show favorite locations"; diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Host+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Host+Extensions.swift index 1909a8eb..10a7d8aa 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Host+Extensions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Host+Extensions.swift @@ -27,23 +27,24 @@ import Foundation import TunnelKitCore extension Profile { - public func hostAccount() -> Profile.Account? { - switch currentVPNProtocol { - case .openVPN: - return host?.ovpnSettings?.account + public var hostAccount: Profile.Account? { + get { + switch currentVPNProtocol { + case .openVPN: + return host?.ovpnSettings?.account - case .wireGuard: - return nil + case .wireGuard: + return nil + } } - } + set { + switch currentVPNProtocol { + case .openVPN: + host?.ovpnSettings?.account = newValue - public mutating func setHostAccount(_ account: Profile.Account?) { - switch currentVPNProtocol { - case .openVPN: - host?.ovpnSettings?.account = account - - case .wireGuard: - break + case .wireGuard: + break + } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Profile+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Profile+Extensions.swift index bca32108..41f517f1 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Profile+Extensions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Profile+Extensions.swift @@ -41,16 +41,16 @@ extension Profile { public var account: Profile.Account { get { if isProvider { - return providerAccount() ?? .init() + return providerAccount ?? .init() } else { - return hostAccount() ?? .init() + return hostAccount ?? .init() } } set { if isProvider { - setProviderAccount(newValue) + providerAccount = newValue } else { - setHostAccount(newValue) + hostAccount = newValue } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Provider+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Provider+Extensions.swift index a1ae74b1..ff7f5768 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Provider+Extensions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Extensions/Provider+Extensions.swift @@ -45,7 +45,7 @@ extension Profile { provider?.name } - public func providerServerId() -> String? { + public var providerServerId: String? { provider?.vpnSettings[currentVPNProtocol]?.serverId } @@ -71,28 +71,40 @@ extension Profile { provider?.vpnSettings[currentVPNProtocol]?.presetId = preset.id } - public func providerFavoriteLocationIds() -> Set? { - provider?.vpnSettings[currentVPNProtocol]?.favoriteLocationIds + public var providerFavoriteLocationIds: Set? { + get { + provider?.vpnSettings[currentVPNProtocol]?.favoriteLocationIds + } + set { + provider?.vpnSettings[currentVPNProtocol]?.favoriteLocationIds = newValue + } } - public mutating func setProviderFavoriteLocationIds(_ ids: Set?) { - provider?.vpnSettings[currentVPNProtocol]?.favoriteLocationIds = ids + public var providerCustomEndpoint: Endpoint? { + get { + provider?.vpnSettings[currentVPNProtocol]?.customEndpoint + } + set { + provider?.vpnSettings[currentVPNProtocol]?.customEndpoint = newValue + } } - public func providerCustomEndpoint() -> Endpoint? { - provider?.vpnSettings[currentVPNProtocol]?.customEndpoint + public var providerAccount: Profile.Account? { + get { + provider?.vpnSettings[currentVPNProtocol]?.account + } + set { + provider?.vpnSettings[currentVPNProtocol]?.account = newValue + } } - public mutating func setProviderCustomEndpoint(_ endpoint: Endpoint?) { - provider?.vpnSettings[currentVPNProtocol]?.customEndpoint = endpoint - } - - public func providerAccount() -> Profile.Account? { - provider?.vpnSettings[currentVPNProtocol]?.account - } - - public mutating func setProviderAccount(_ account: Profile.Account?) { - provider?.vpnSettings[currentVPNProtocol]?.account = account + public var providerRandomizesServer: Bool? { + get { + provider?.randomizesServer + } + set { + provider?.randomizesServer = newValue + } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Models/Profile+Provider.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Models/Profile+Provider.swift index b7877ccc..2634f302 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Models/Profile+Provider.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Models/Profile+Provider.swift @@ -47,6 +47,8 @@ extension Profile { public var vpnSettings: [VPNProtocolType: Settings] = [:] + public var randomizesServer: Bool? + public init(_ name: ProviderName) { self.name = name } diff --git a/PassepartoutLibrary/Sources/PassepartoutProfiles/Extensions/PassepartoutProfiles+Subtype.swift b/PassepartoutLibrary/Sources/PassepartoutProfiles/Extensions/PassepartoutProfiles+Subtype.swift index ee124291..620e3063 100644 --- a/PassepartoutLibrary/Sources/PassepartoutProfiles/Extensions/PassepartoutProfiles+Subtype.swift +++ b/PassepartoutLibrary/Sources/PassepartoutProfiles/Extensions/PassepartoutProfiles+Subtype.swift @@ -39,7 +39,7 @@ extension Profile { extension Profile { public func providerServer(_ providerManager: ProviderManager) -> ProviderServer? { - guard let serverId = providerServerId() else { + guard let serverId = providerServerId else { return nil } return providerManager.server(withId: serverId) @@ -51,9 +51,21 @@ extension Profile { } // infer remotes from preset + server - guard let server = providerServer(providerManager) else { + guard let selectedServer = providerServer(providerManager) else { throw PassepartoutError.missingProviderServer } + let server: ProviderServer + if providerRandomizesServer ?? false { + let location = selectedServer.location(withVPNProtocol: currentVPNProtocol) + let servers = providerManager.servers(forLocation: location) + guard let randomServerId = servers.randomElement()?.id, + let randomServer = providerManager.server(withId: randomServerId) else { + throw PassepartoutError.missingProviderServer + } + server = randomServer + } else { + server = selectedServer + } guard let preset = providerPreset(server) else { throw PassepartoutError.missingProviderPreset } @@ -68,8 +80,8 @@ extension Profile { // apply provider settings (username, custom endpoint) let cfg = builder.build() var settings = OpenVPNSettings(configuration: cfg) - settings.account = providerAccount() - settings.customEndpoint = providerCustomEndpoint() + settings.account = providerAccount + settings.customEndpoint = providerCustomEndpoint return settings } diff --git a/PassepartoutLibrary/Sources/PassepartoutProviders/Extensions/PassepartoutProviders+Identifiable.swift b/PassepartoutLibrary/Sources/PassepartoutProviders/Extensions/PassepartoutProviders+Identifiable.swift index cbaafc37..b761e839 100644 --- a/PassepartoutLibrary/Sources/PassepartoutProviders/Extensions/PassepartoutProviders+Identifiable.swift +++ b/PassepartoutLibrary/Sources/PassepartoutProviders/Extensions/PassepartoutProviders+Identifiable.swift @@ -45,6 +45,16 @@ extension ProviderServer { public var locationId: String { "\(providerMetadata.name):\(categoryName):\(countryCode)" } + + public func location(withVPNProtocol vpnProtocol: VPNProtocolType) -> ProviderLocation { + ProviderLocation( + providerMetadata: providerMetadata, + vpnProtocol: vpnProtocol, + categoryName: categoryName, + countryCode: countryCode, + servers: nil + ) + } } extension ProviderServer { diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager+Actions.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager+Actions.swift index fd706044..b961e3d1 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager+Actions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager+Actions.swift @@ -85,7 +85,7 @@ extension VPNManager { try await profileManager.makeProfileReady(profile) } - let oldServerId = profile.providerServerId() + let oldServerId = profile.providerServerId guard let newServer = providerManager.server(withId: newServerId) else { pp_log.warning("Server \(newServerId) not found") throw PassepartoutError.missingProviderServer diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift index 99b1299f..99218f4e 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift @@ -206,15 +206,15 @@ extension VPNManager { if newProfile.isProvider { // server changed? - if newProfile.providerServerId() != lastProfile.providerServerId() { - pp_log.info("Provider server changed: \(newProfile.providerServerId()?.description ?? "nil")") + if newProfile.providerServerId != lastProfile.providerServerId { + pp_log.info("Provider server changed: \(newProfile.providerServerId?.description ?? "nil")") isHandled = true shouldReconnect = notDisconnected } // endpoint changed? - else if newProfile.providerCustomEndpoint() != lastProfile.providerCustomEndpoint() { - pp_log.info("Provider endpoint changed: \(newProfile.providerCustomEndpoint()?.description ?? "automatic")") + else if newProfile.providerCustomEndpoint != lastProfile.providerCustomEndpoint { + pp_log.info("Provider endpoint changed: \(newProfile.providerCustomEndpoint?.description ?? "automatic")") isHandled = true shouldReconnect = notDisconnected }