summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Debug/DebugViewModel.swift
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2026-02-27 16:25:03 +0100
committerJon Petersson <jon.petersson@mullvad.net>2026-02-27 16:25:03 +0100
commit6676acf301c498a8dcb71a35f409b89968b3a750 (patch)
treed59d5398b6a3d03f2e2b04498e02d2546610ea3f /ios/MullvadVPN/Debug/DebugViewModel.swift
parentfe84f9bad91e222ed768ede172fce1e2ada29bea (diff)
downloadmullvadvpn-debug-view.tar.xz
mullvadvpn-debug-view.zip
Add debug viewdebug-view
Diffstat (limited to 'ios/MullvadVPN/Debug/DebugViewModel.swift')
-rw-r--r--ios/MullvadVPN/Debug/DebugViewModel.swift301
1 files changed, 301 insertions, 0 deletions
diff --git a/ios/MullvadVPN/Debug/DebugViewModel.swift b/ios/MullvadVPN/Debug/DebugViewModel.swift
new file mode 100644
index 0000000000..e6f4ebd1db
--- /dev/null
+++ b/ios/MullvadVPN/Debug/DebugViewModel.swift
@@ -0,0 +1,301 @@
+//
+// DebugViewModel.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2026-02-27.
+// Copyright © 2026 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import Network
+import PacketTunnelCore
+import SwiftUI
+
+@MainActor
+protocol DebugViewModel: ObservableObject {
+ typealias Item = (title: String, data: [String])
+
+ var tunnelSettings: LatestTunnelSettings { get }
+ var nwPathStatus: NWPath.Status { get }
+
+ var connection: [Item] { get }
+ var settings: [Item] { get }
+}
+
+class DebugViewModelImpl: DebugViewModel {
+ var tunnelManager: TunnelManager
+ var nwPathMonitor: NWPathMonitor
+ var appPreferences: AppPreferencesDataSource
+ var tunnelObserver: TunnelBlockObserver!
+
+ var tunnelSettings: LatestTunnelSettings
+ var tunnelStatus: TunnelStatus
+ var nwPathStatus = NWPath.Status.unsatisfied
+
+ @Published var connection = [Item]()
+ @Published var settings = [Item]()
+
+ init(
+ tunnelManager: TunnelManager,
+ nwPathMonitor: NWPathMonitor,
+ appPreferences: AppPreferencesDataSource
+ ) {
+ self.tunnelManager = tunnelManager
+ self.nwPathMonitor = nwPathMonitor
+ self.appPreferences = appPreferences
+
+ tunnelSettings = tunnelManager.settings
+ tunnelStatus = tunnelManager.tunnelStatus
+
+ refreshData()
+
+ tunnelObserver = TunnelBlockObserver(
+ didUpdateTunnelStatus: { _, status in
+ self.tunnelStatus = status
+ self.refreshData()
+ },
+ didUpdateDeviceState: { _, _, _ in },
+ didUpdateTunnelSettings: { _, settings in
+ self.tunnelSettings = settings
+ self.refreshData()
+ }
+ )
+ self.tunnelManager.addObserver(tunnelObserver)
+ }
+
+ // Arrange these functions to get the desired section order in the view.
+ private func refreshData() {
+ // Connection
+ setRelays()
+ setObfuscation()
+
+ // Settings
+ setRelaySettings()
+ setMultihopSettings()
+ setDaitaSettings()
+ setObfuscationSettings()
+ setQuantumResistanceSettings()
+ setIncludeAllNetworksSettings()
+ setMullvadDnsBlockers()
+ setCustomDnsBlockers()
+ }
+
+ private func update(item: Item, in list: inout [Item]) {
+ guard let index = (list.firstIndex { $0.title == item.title }) else {
+ list.append(item)
+ return
+ }
+
+ list.remove(at: index)
+ list.insert(item, at: index)
+ }
+}
+
+// MARK: Connection
+
+extension DebugViewModelImpl {
+ private func setRelays() {
+ let entry = tunnelStatus.state.relays?.entry?.debugDescription ?? "-"
+ let exit = tunnelStatus.state.relays?.exit.debugDescription ?? "-"
+
+ update(
+ item: (
+ title: "Relays",
+ data: [
+ "Entry: \(entry)",
+ "Exit: \(exit)",
+ ]
+ ), in: &connection
+ )
+ }
+
+ private func setObfuscation() {
+ update(
+ item: (
+ title: "Obfuscation",
+ data: [
+ tunnelStatus.observedState.connectionState?.obfuscationMethod.description ?? "-"
+ ]
+ ), in: &connection
+ )
+ }
+}
+
+// MARK: Settings
+
+extension DebugViewModelImpl {
+ func setRelaySettings() {
+ let entry = tunnelSettings.relayConstraints.entryLocations.value?.locations.first?.stringRepresentation ?? "-"
+ let exit = tunnelSettings.relayConstraints.exitLocations.value?.locations.first?.stringRepresentation ?? "-"
+
+ update(
+ item: (
+ title: "Relays",
+ data: [
+ "Entry: \(entry)",
+ "Exit: \(exit)",
+ ]
+ ), in: &settings
+ )
+ }
+
+ func setMullvadDnsBlockers() {
+ let blockingOptions = tunnelSettings.dnsSettings.blockingOptions
+ var dnsBlockers = [String]()
+
+ if blockingOptions.contains(.blockAdvertising) {
+ dnsBlockers.append("Advertising (\(DNSBlockingOptions.blockAdvertising.serverAddress!))")
+ }
+ if blockingOptions.contains(.blockTracking) {
+ dnsBlockers.append("Tracking (\(DNSBlockingOptions.blockTracking.serverAddress!))")
+ }
+ if blockingOptions.contains(.blockMalware) {
+ dnsBlockers.append("Malware (\(DNSBlockingOptions.blockMalware.serverAddress!))")
+ }
+ if blockingOptions.contains(.blockAdultContent) {
+ dnsBlockers.append("Adult content (\(DNSBlockingOptions.blockAdultContent.serverAddress!))")
+ }
+ if blockingOptions.contains(.blockGambling) {
+ dnsBlockers.append("Gambling (\(DNSBlockingOptions.blockGambling.serverAddress!))")
+ }
+ if blockingOptions.contains(.blockSocialMedia) {
+ dnsBlockers.append("Social media (\(DNSBlockingOptions.blockSocialMedia.serverAddress!))")
+ }
+
+ update(
+ item: (
+ title: "Mullvad DNS blockers",
+ data: dnsBlockers.isEmpty ? ["-"] : dnsBlockers
+ ), in: &settings
+ )
+ }
+
+ func setCustomDnsBlockers() {
+ let customAddresses = tunnelSettings.dnsSettings.customDNSDomains.map { $0.debugDescription }
+
+ update(
+ item: (
+ title: "Custom DNS blockers",
+ data: customAddresses.isEmpty ? ["-"] : customAddresses
+ ), in: &settings
+ )
+ }
+
+ func setObfuscationSettings() {
+ let method = tunnelSettings.wireGuardObfuscation.state.description
+ let udpTcpPort = tunnelSettings.wireGuardObfuscation.udpOverTcpPort.description
+ let shadowSocksPort = tunnelSettings.wireGuardObfuscation.shadowsocksPort.description
+
+ update(
+ item: (
+ title: "Obfuscation",
+ data: [
+ "Method: \(method)",
+ "UDP over TCP port: \(udpTcpPort)",
+ "Shadowsocks port: \(shadowSocksPort)",
+ ]
+ ), in: &settings
+ )
+ }
+
+ func setQuantumResistanceSettings() {
+ update(
+ item: (
+ title: "Quantum resistance",
+ data: [
+ tunnelSettings.tunnelQuantumResistance.isEnabled ? "Enabled" : "Disabled"
+ ]
+ ), in: &settings
+ )
+ }
+
+ func setMultihopSettings() {
+ update(
+ item: (
+ title: "Multihop",
+ data: [
+ tunnelSettings.tunnelMultihopState.isEnabled ? "Enabled" : "Disabled"
+ ]
+ ), in: &settings
+ )
+ }
+
+ func setDaitaSettings() {
+ let daitaIsEnabled = tunnelSettings.daita.daitaState.isEnabled ? "Enabled" : "Disabled"
+ let directOnlyIsEnabled = tunnelSettings.daita.directOnlyState.isEnabled ? "Enabled" : "Disabled"
+
+ update(
+ item: (
+ title: "DAITA",
+ data: [
+ "DAITA: \(daitaIsEnabled)",
+ "Direct only: \(directOnlyIsEnabled)",
+ ]
+ ), in: &settings
+ )
+ }
+
+ func setIncludeAllNetworksSettings() {
+ let includeAllNetworksIsEnabled =
+ tunnelSettings.includeAllNetworks.includeAllNetworksIsEnabled ? "Enabled" : "Disabled"
+ let localNetworkSharingIsEnabled =
+ tunnelSettings.includeAllNetworks.localNetworkSharingIsEnabled ? "Enabled" : "Disabled"
+ let consent = appPreferences.includeAllNetworksConsent ? "True" : "False"
+
+ update(
+ item: (
+ title: "Force all apps",
+ data: [
+ "Force all apps: \(includeAllNetworksIsEnabled)",
+ "Local network sharing: \(localNetworkSharingIsEnabled)",
+ "Consent: \(consent)",
+ ]
+ ), in: &settings
+ )
+ }
+}
+
+// MARK: Mock
+
+class MockDebugViewModel: DebugViewModel {
+ var tunnelSettings: LatestTunnelSettings = LatestTunnelSettings()
+ var nwPathStatus: NWPath.Status = NWPath.Status.unsatisfied
+
+ // Connection
+ var connection = [
+ (
+ title: "Relays",
+ data: [
+ "Entry: Sweden, Gothenburg, se-got-001",
+ "Exit: Sweden, Gothenburg, se-got-002",
+ ]
+ ),
+ (title: "Obfuscation", data: [""]),
+ ]
+
+ // Settings
+ var settings = [
+ (title: "Relays", data: ["Entry: se-se-got-se-got-001", "Exit: se-se-got-se-got-001"]),
+ (
+ title: "Mullvad DNS blockers",
+ data: [
+ "Advertising (100.64.0.1)",
+ "Tracking (100.64.0.2)",
+ "Social Media (100.64.0.3)",
+ ]
+ ),
+ (title: "Custom DNS blockers", data: ["192.168.1.1", "192.168.1.2"]),
+ (title: "Obfuscation", data: ["QUIC", "UDP over TCP port: 53", "Shadowsocks port: 53"]),
+ (title: "Quantum resistance", data: ["Enabled"]),
+ (title: "Multihop", data: ["Disabled"]),
+ (title: "DAITA", data: ["DAITA: Enabled", "Direct only: Disabled"]),
+ (
+ title: "Force all apps",
+ data: [
+ "Force all apps: Disabled",
+ "Local network sharing: Disabled",
+ "Consent: False",
+ ]
+ ),
+ ]
+}