summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorSteffen Ernst <steffen.ernst@mullvad.net>2025-02-06 17:02:58 +0100
committerBug Magnet <marco.nikic@mullvad.net>2025-02-18 11:50:42 +0100
commit235a3b54088bc99b26f9519d3b337e040bb38bd8 (patch)
treed2d2e72febfd10a7ab265f012756e2bd188f8659 /ios
parent0f629c6d1158301f6f0314630e1d4f80ecc100ae (diff)
downloadmullvadvpn-235a3b54088bc99b26f9519d3b337e040bb38bd8.tar.xz
mullvadvpn-235a3b54088bc99b26f9519d3b337e040bb38bd8.zip
Add local network sharing to settings and hook up
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadSettings/TunnelSettings.swift2
-rw-r--r--ios/MullvadSettings/TunnelSettingsStrategy.swift27
-rw-r--r--ios/MullvadSettings/TunnelSettingsUpdate.swift4
-rw-r--r--ios/MullvadSettings/TunnelSettingsV6.swift16
-rw-r--r--ios/MullvadSettings/TunnelSettingsV7.swift1
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift1
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift22
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift4
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift17
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift50
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift20
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift25
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift1
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift20
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift52
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift8
16 files changed, 229 insertions, 41 deletions
diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift
index 17670bc032..d950ce39a4 100644
--- a/ios/MullvadSettings/TunnelSettings.swift
+++ b/ios/MullvadSettings/TunnelSettings.swift
@@ -58,7 +58,7 @@ public enum SchemaVersion: Int, Equatable, Sendable {
case .v3: return .v4
case .v4: return .v5
case .v5: return .v6
- case .v6: return .v6
+ case .v6: return .v7
case .v7: return .v7
}
}
diff --git a/ios/MullvadSettings/TunnelSettingsStrategy.swift b/ios/MullvadSettings/TunnelSettingsStrategy.swift
index 830573e4f0..05db9132a4 100644
--- a/ios/MullvadSettings/TunnelSettingsStrategy.swift
+++ b/ios/MullvadSettings/TunnelSettingsStrategy.swift
@@ -9,19 +9,42 @@
import Foundation
public protocol TunnelSettingsStrategyProtocol: Sendable {
func shouldReconnectToNewRelay(oldSettings: LatestTunnelSettings, newSettings: LatestTunnelSettings) -> Bool
+ func getReconnectionStrategy(
+ oldSettings: LatestTunnelSettings,
+ newSettings: LatestTunnelSettings
+ ) -> TunnelSettingsReconnectionStrategy
}
public struct TunnelSettingsStrategy: TunnelSettingsStrategyProtocol, Sendable {
public init() {}
+
public func shouldReconnectToNewRelay(
oldSettings: LatestTunnelSettings,
newSettings: LatestTunnelSettings
) -> Bool {
+ getReconnectionStrategy(oldSettings: oldSettings, newSettings: newSettings) != .noReconnect
+ }
+
+ public func getReconnectionStrategy(
+ oldSettings: LatestTunnelSettings,
+ newSettings: LatestTunnelSettings
+ ) -> TunnelSettingsReconnectionStrategy {
+ if oldSettings.localNetworkSharing != newSettings.localNetworkSharing {
+ return .hardReconnect
+ }
switch (oldSettings, newSettings) {
case let (old, new) where old != new:
- true
+ return .softReconnect
default:
- false
+ return .noReconnect
}
}
}
+
+public enum TunnelSettingsReconnectionStrategy {
+ case noReconnect
+ case softReconnect
+// This will fully disconnect and start a new connection
+// Attention: This will leak traffic!!!
+ case hardReconnect
+}
diff --git a/ios/MullvadSettings/TunnelSettingsUpdate.swift b/ios/MullvadSettings/TunnelSettingsUpdate.swift
index c6091453de..237e914617 100644
--- a/ios/MullvadSettings/TunnelSettingsUpdate.swift
+++ b/ios/MullvadSettings/TunnelSettingsUpdate.swift
@@ -10,6 +10,7 @@ import Foundation
import MullvadTypes
public enum TunnelSettingsUpdate: Sendable {
+ case localNetworkSharing(Bool)
case dnsSettings(DNSSettings)
case obfuscation(WireGuardObfuscationSettings)
case relayConstraints(RelayConstraints)
@@ -21,6 +22,8 @@ public enum TunnelSettingsUpdate: Sendable {
extension TunnelSettingsUpdate {
public func apply(to settings: inout LatestTunnelSettings) {
switch self {
+ case let .localNetworkSharing(enabled):
+ settings.localNetworkSharing = enabled
case let .dnsSettings(newDNSSettings):
settings.dnsSettings = newDNSSettings
case let .obfuscation(newObfuscationSettings):
@@ -38,6 +41,7 @@ extension TunnelSettingsUpdate {
public var subjectName: String {
switch self {
+ case .localNetworkSharing: "Local network sharing"
case .dnsSettings: "DNS settings"
case .obfuscation: "obfuscation settings"
case .relayConstraints: "relay constraints"
diff --git a/ios/MullvadSettings/TunnelSettingsV6.swift b/ios/MullvadSettings/TunnelSettingsV6.swift
index 9bfccce2df..3ec50d5658 100644
--- a/ios/MullvadSettings/TunnelSettingsV6.swift
+++ b/ios/MullvadSettings/TunnelSettingsV6.swift
@@ -45,12 +45,14 @@ public struct TunnelSettingsV6: Codable, Equatable, TunnelSettings, Sendable {
}
public func upgradeToNextVersion() -> any TunnelSettings {
- TunnelSettingsV7(relayConstraints: relayConstraints,
- dnsSettings: dnsSettings,
- wireGuardObfuscation: wireGuardObfuscation,
- tunnelQuantumResistance: tunnelQuantumResistance,
- tunnelMultihopState: tunnelMultihopState,
- daita: daita,
- localNetworkSharing: false)
+ TunnelSettingsV7(
+ relayConstraints: relayConstraints,
+ dnsSettings: dnsSettings,
+ wireGuardObfuscation: wireGuardObfuscation,
+ tunnelQuantumResistance: tunnelQuantumResistance,
+ tunnelMultihopState: tunnelMultihopState,
+ daita: daita,
+ localNetworkSharing: false
+ )
}
}
diff --git a/ios/MullvadSettings/TunnelSettingsV7.swift b/ios/MullvadSettings/TunnelSettingsV7.swift
index 09314d3014..bdd23f707b 100644
--- a/ios/MullvadSettings/TunnelSettingsV7.swift
+++ b/ios/MullvadSettings/TunnelSettingsV7.swift
@@ -6,7 +6,6 @@
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
-
import Foundation
import MullvadTypes
diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
index ddd384e44a..27e68b1b67 100644
--- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
+++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
@@ -175,6 +175,7 @@ public enum AccessibilityIdentifier: Equatable {
case statusImageView
// DNS settings
+ case localNetworkSharing
case dnsSettings
case ipOverrides
case wireGuardCustomPort
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 6c2b97ac06..d3c88eb0a3 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -9,6 +9,7 @@
import Foundation
import MullvadLogging
import MullvadREST
+import MullvadSettings
import NetworkExtension
import Operations
import PacketTunnelCore
@@ -48,7 +49,7 @@ class StartTunnelOperation: ResultOperation<Void>, @unchecked Sendable {
finish(result: .success(()))
- case .disconnected, .pendingReconnect:
+ case .disconnected, .pendingReconnect, .waitingForConnectivity:
makeTunnelProviderAndStartTunnel { error in
self.finish(result: error.map { .failure($0) } ?? .success(()))
}
@@ -106,28 +107,11 @@ class StartTunnelOperation: ResultOperation<Void>, @unchecked Sendable {
) {
let persistentTunnels = interactor.getPersistentTunnels()
let tunnel = persistentTunnels.first ?? interactor.createNewTunnel()
- let configuration = Self.makeTunnelConfiguration()
+ let configuration = TunnelConfiguration(excludeLocalNetworks: interactor.settings.localNetworkSharing)
tunnel.setConfiguration(configuration)
tunnel.saveToPreferences { error in
completionHandler(error.map { .failure($0) } ?? .success(tunnel))
}
}
-
- private class func makeTunnelConfiguration() -> TunnelConfiguration {
- let protocolConfig = NETunnelProviderProtocol()
- protocolConfig.providerBundleIdentifier = ApplicationTarget.packetTunnel.bundleIdentifier
- protocolConfig.serverAddress = ""
-
- let alwaysOnRule = NEOnDemandRuleConnect()
- alwaysOnRule.interfaceTypeMatch = .any
-
- return TunnelConfiguration(
- isEnabled: true,
- localizedDescription: "WireGuard",
- protocolConfiguration: protocolConfig,
- onDemandRules: [alwaysOnRule],
- isOnDemandEnabled: true
- )
- }
}
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
index c4d727fe3e..bed05b1d1f 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -11,6 +11,7 @@ import Operations
class StopTunnelOperation: ResultOperation<Void>, @unchecked Sendable {
private let interactor: TunnelInteractor
+ var isOnDemandEnabled = false
init(
dispatchQueue: DispatchQueue,
@@ -50,8 +51,7 @@ class StopTunnelOperation: ResultOperation<Void>, @unchecked Sendable {
return
}
- // Disable on-demand when stopping the tunnel to prevent it from coming back up
- tunnel.isOnDemandEnabled = false
+ tunnel.isOnDemandEnabled = isOnDemandEnabled
tunnel.saveToPreferences { error in
self.dispatchQueue.async {
diff --git a/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift b/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift
index 38c7c3b56b..274e9439be 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift
@@ -16,6 +16,23 @@ struct TunnelConfiguration {
var onDemandRules: [NEOnDemandRule]
var isOnDemandEnabled: Bool
+ init(excludeLocalNetworks: Bool, isOnDemandEnabled: Bool = true) {
+ let protocolConfig = NETunnelProviderProtocol()
+ protocolConfig.providerBundleIdentifier = ApplicationTarget.packetTunnel.bundleIdentifier
+ protocolConfig.serverAddress = ""
+ protocolConfig.includeAllNetworks = true
+ protocolConfig.excludeLocalNetworks = excludeLocalNetworks
+
+ let alwaysOnRule = NEOnDemandRuleConnect()
+ alwaysOnRule.interfaceTypeMatch = .any
+
+ isEnabled = true
+ localizedDescription = "WireGuard"
+ protocolConfiguration = protocolConfig
+ onDemandRules = [alwaysOnRule]
+ self.isOnDemandEnabled = isOnDemandEnabled
+ }
+
func apply(to manager: TunnelProviderManagerType) {
manager.isEnabled = isEnabled
manager.localizedDescription = localizedDescription
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 2039d1297e..d12be3b424 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -78,6 +78,8 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
/// Last processed device check.
private var lastPacketTunnelKeyRotation: Date?
+ private var observer: TunnelObserver?
+
// MARK: - Initialization
init(
@@ -250,7 +252,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
operationQueue.addOperation(operation)
}
- func stopTunnel(completionHandler: ((Error?) -> Void)? = nil) {
+ func stopTunnel(isOnDemandEnabled: Bool = false, completionHandler: ((Error?) -> Void)? = nil) {
let operation = StopTunnelOperation(
dispatchQueue: internalQueue,
interactor: TunnelInteractorProxy(self)
@@ -272,7 +274,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
completionHandler?(result.error)
}
-
+ operation.isOnDemandEnabled = isOnDemandEnabled
operation.addObserver(BackgroundObserver(
backgroundTaskProvider: backgroundTaskProvider,
name: "Stop tunnel",
@@ -320,6 +322,35 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
operationQueue.addOperation(operation)
}
+ func reapplyTunnelConfiguration() {
+ guard let tunnel else { return }
+ if self.tunnelStatus.state.isSecured {
+ let observer = TunnelBlockObserver(
+ didUpdateTunnelStatus: { _, status in
+ if case .disconnected = status.state {
+ if let observer = self.observer {
+ self.removeObserver(observer)
+ self.observer = nil
+ }
+ self.startTunnel()
+ }
+ }
+ )
+ addObserver(observer)
+ self.observer = observer
+
+ let configuration = TunnelConfiguration(
+ includeAllNetworks: settings.includeAllNetworks,
+ excludeLocalNetworks: settings.localNetworkSharing
+ )
+
+ tunnel.setConfiguration(configuration)
+ tunnel.saveToPreferences { _ in
+ self.stopTunnel(isOnDemandEnabled: true)
+ }
+ }
+ }
+
func setNewAccount() async throws -> StoredAccountData {
try await setAccount(action: .new)!
}
@@ -940,11 +971,18 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable {
modificationBlock(&updatedSettings)
self.setSettings(updatedSettings, persist: true)
- self.reconnectTunnel(
- selectNewRelay: settingsStrategy
- .shouldReconnectToNewRelay(oldSettings: currentSettings, newSettings: updatedSettings),
- completionHandler: nil
+ let reconnectionStrategy = settingsStrategy.getReconnectionStrategy(
+ oldSettings: currentSettings,
+ newSettings: updatedSettings
)
+ switch reconnectionStrategy {
+ case .noReconnect:
+ self.reconnectTunnel(selectNewRelay: false)
+ case .softReconnect:
+ self.reconnectTunnel(selectNewRelay: true)
+ case .hardReconnect:
+ self.reapplyTunnelConfiguration()
+ }
}
operation.completionBlock = {
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
index fff545a07e..faed3382ac 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
@@ -16,6 +16,7 @@ protocol VPNSettingsCellEventHandler {
func selectCustomPortEntry(_ port: UInt16) -> Bool
func selectObfuscationState(_ state: WireGuardObfuscationState)
func switchMultihop(_ state: MultihopState)
+ func setLocalNetworkSettings(_ enabled: Bool, onCancel: @escaping () -> Void)
}
@MainActor
@@ -32,7 +33,6 @@ final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol {
func makeCell(for item: VPNSettingsDataSource.Item, indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: item.reuseIdentifier.rawValue, for: indexPath)
-
configureCell(cell, item: item, indexPath: indexPath)
return cell
@@ -42,6 +42,24 @@ final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol {
func configureCell(_ cell: UITableViewCell, item: VPNSettingsDataSource.Item, indexPath: IndexPath) {
(cell as? SettingsCell)?.detailTitleLabel.accessibilityIdentifier = nil
switch item {
+ case .localNetworkSharing:
+ guard let cell = cell as? SettingsSwitchCell else { return }
+ cell.infoButtonHandler = { self.delegate?.showInfo(for: .localNetworkSharing) }
+ cell.action = { enable in
+ self.delegate?.setLocalNetworkSettings(enable) {
+ cell.setOn(self.viewModel.localNetworkSharing, animated: true)
+ }
+ }
+
+ cell.titleLabel.text = NSLocalizedString(
+ "LOCAL_NETWORK_SHARING_CELL_LABEL",
+ tableName: "VPNSettings",
+ value: "Local network sharing",
+ comment: ""
+ )
+ cell.setAccessibilityIdentifier(item.accessibilityIdentifier)
+ cell.setOn(viewModel.localNetworkSharing, animated: true)
+
case .dnsSettings:
guard let cell = cell as? SettingsCell else { return }
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
index 236ecddb62..50b7aeb558 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
@@ -16,6 +16,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
typealias InfoButtonHandler = (Item) -> Void
enum CellReuseIdentifiers: String, CaseIterable {
+ case localNetworkSharing
case dnsSettings
case ipOverrides
case wireGuardPort
@@ -27,6 +28,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
var reusableViewClass: AnyClass {
switch self {
+ case .localNetworkSharing:
+ return SettingsSwitchCell.self
case .dnsSettings:
return SettingsCell.self
case .ipOverrides:
@@ -56,6 +59,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
}
enum Section: String, Hashable, CaseIterable {
+ case localNetworkSharing
case dnsSettings
case ipOverrides
case wireGuardPorts
@@ -65,6 +69,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
}
enum Item: Hashable {
+ case localNetworkSharing(_ enabled: Bool)
case dnsSettings
case ipOverrides
case wireGuardPort(_ port: UInt16?)
@@ -108,6 +113,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
var accessibilityIdentifier: AccessibilityIdentifier {
switch self {
+ case .localNetworkSharing:
+ return .localNetworkSharing
case .dnsSettings:
return .dnsSettings
case .ipOverrides:
@@ -137,6 +144,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
var reuseIdentifier: CellReuseIdentifiers {
switch self {
+ case .localNetworkSharing:
+ return .localNetworkSharing
case .dnsSettings:
return .dnsSettings
case .ipOverrides:
@@ -378,7 +387,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
let sectionIdentifier = snapshot().sectionIdentifiers[section]
switch sectionIdentifier {
- case .dnsSettings, .ipOverrides, .privacyAndSecurity:
+ case .dnsSettings, .ipOverrides, .privacyAndSecurity, .localNetworkSharing:
return .leastNonzeroMagnitude
default:
return tableView.estimatedRowHeight
@@ -391,7 +400,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
return switch sectionIdentifier {
// 0 due to there already being a separator between .dnsSettings and .ipOverrides.
case .dnsSettings: 0
- case .ipOverrides, .quantumResistance: UITableView.automaticDimension
+ case .ipOverrides, .quantumResistance, .localNetworkSharing: UITableView.automaticDimension
default: 0.5
}
}
@@ -445,6 +454,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
snapshot.appendSections(Section.allCases)
snapshot.appendItems([.dnsSettings], toSection: .dnsSettings)
snapshot.appendItems([.ipOverrides], toSection: .ipOverrides)
+ snapshot.appendItems([.localNetworkSharing(viewModel.localNetworkSharing)], toSection: .localNetworkSharing)
applySnapshot(snapshot, animated: animated, completion: completion)
}
@@ -601,6 +611,17 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
}
extension VPNSettingsDataSource: @preconcurrency VPNSettingsCellEventHandler {
+ func setLocalNetworkSettings(_ enabled: Bool, onCancel: @escaping () -> Void) {
+ delegate?.showLocalNetworkSharingWarning(enabled) { accepted in
+ if accepted {
+ self.delegate?.didUpdateTunnelSettings(.localNetworkSharing(enabled))
+ self.viewModel.setLocalNetworkSharing(enabled)
+ } else {
+ onCancel()
+ }
+ }
+ }
+
func showInfo(for button: VPNSettingsInfoButtonItem) {
delegate?.showInfo(for: button)
}
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift
index bf3856b464..45194c8582 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift
@@ -22,4 +22,5 @@ protocol VPNSettingsDataSourceDelegate: AnyObject {
func showIPOverrides()
func didSelectWireGuardPort(_ port: UInt16?)
func humanReadablePortRepresentation() -> String
+ func showLocalNetworkSharingWarning(_ enable: Bool, completion: @escaping (Bool) -> Void)
}
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
index 2b87f5b67c..9df6ed2a41 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift
@@ -9,6 +9,7 @@
import Foundation
enum VPNSettingsInfoButtonItem: CustomStringConvertible {
+ case localNetworkSharing
case contentBlockers
case blockMalware
case wireGuardPorts(String)
@@ -19,6 +20,25 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible {
var description: String {
switch self {
+ case .localNetworkSharing:
+ NSLocalizedString(
+ "VPN_SETTINGS_LOCAL_NETWORK_SHARING",
+ tableName: "LocalNetworkSharing",
+ value: """
+ This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc.
+
+ It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:
+ 10.0.0.0/8
+ 172.16.0.0/12
+ 192.168.0.0/16
+ 169.254.0.0/16
+ fe80::/10
+ fc00::/7
+
+ Attention: toggling “Local network sharing” requires restarting the VPN connection.
+ """,
+ comment: ""
+ )
case .contentBlockers:
NSLocalizedString(
"VPN_SETTINGS_CONTENT_BLOCKERS_GENERAL",
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
index 4b55c94f46..df8f8c2981 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
@@ -159,4 +159,56 @@ extension VPNSettingsViewController: @preconcurrency VPNSettingsDataSourceDelega
func didSelectWireGuardPort(_ port: UInt16?) {
interactor.setPort(port)
}
+
+ func showLocalNetworkSharingWarning(_ enable: Bool, completion: @escaping (Bool) -> Void) {
+ if interactor.tunnelManager.tunnelStatus.state.isSecured {
+ let description = NSLocalizedString(
+ "VPN_SETTINGS_LOCAL_NETWORK_SHARING_WARNING",
+ tableName: "LocalNetworkSharing",
+ value: """
+ \(
+ enable
+ ? "Enabling"
+ : "Disabling"
+ ) “Local network sharing” requires restarting the VPN connection, which will disconnect you and briefly expose your traffic.
+ To prevent this, manually enable Airplane Mode and turn off Wi-Fi before continuing.
+
+ Would you like to continue to enable “Local network sharing”?
+ """,
+ comment: ""
+ )
+
+ let presentation = AlertPresentation(
+ id: "vpn-settings-local-network-sharing-warning",
+ icon: .info,
+ message: description,
+ buttons: [
+ AlertAction(
+ title: NSLocalizedString(
+ "VPN_SETTINGS_LOCAL_NETWORK_SHARING_OK_ACTION",
+ tableName: "ContentBlockers",
+ value: "Yes, continue",
+ comment: ""
+ ),
+ style: .destructive,
+ handler: { completion(true) }
+ ),
+ AlertAction(
+ title: NSLocalizedString(
+ "VPN_SETTINGS_LOCAL_NETWORK_SHARING_CANCEL_ACTION",
+ tableName: "ContentBlockers",
+ value: "Cancel",
+ comment: ""
+ ),
+ style: .default,
+ handler: { completion(false) }
+ ),
+ ]
+ )
+
+ alertPresenter.showAlert(presentation: presentation, animated: true)
+ } else {
+ completion(true)
+ }
+ }
}
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift
index 05f0b2652d..cf791e0e29 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift
@@ -103,6 +103,8 @@ struct VPNSettingsViewModel: Equatable {
private(set) var quantumResistance: TunnelQuantumResistance
private(set) var multihopState: MultihopState
+ private(set) var localNetworkSharing: Bool
+
static let defaultWireGuardPorts: [UInt16] = [51820, 53]
var enabledBlockersCount: Int {
@@ -195,6 +197,10 @@ struct VPNSettingsViewModel: Equatable {
multihopState = newState
}
+ mutating func setLocalNetworkSharing(_ newState: Bool) {
+ localNetworkSharing = newState
+ }
+
/// Precondition for enabling Custom DNS.
var customDNSPrecondition: CustomDNSPrecondition {
if blockAdvertising || blockTracking || blockMalware ||
@@ -252,6 +258,8 @@ struct VPNSettingsViewModel: Equatable {
quantumResistance = tunnelSettings.tunnelQuantumResistance
multihopState = tunnelSettings.tunnelMultihopState
+
+ localNetworkSharing = tunnelSettings.localNetworkSharing
}
/// Produce merged view model, keeping entry `identifier` for matching DNS entries and