diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2026-02-27 16:25:03 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2026-02-27 16:25:03 +0100 |
| commit | 6676acf301c498a8dcb71a35f409b89968b3a750 (patch) | |
| tree | d59d5398b6a3d03f2e2b04498e02d2546610ea3f /ios/MullvadVPN/Debug | |
| parent | fe84f9bad91e222ed768ede172fce1e2ada29bea (diff) | |
| download | mullvadvpn-debug-view.tar.xz mullvadvpn-debug-view.zip | |
Add debug viewdebug-view
Diffstat (limited to 'ios/MullvadVPN/Debug')
| -rw-r--r-- | ios/MullvadVPN/Debug/DebugCoordinator.swift | 45 | ||||
| -rw-r--r-- | ios/MullvadVPN/Debug/DebugView.swift | 109 | ||||
| -rw-r--r-- | ios/MullvadVPN/Debug/DebugViewModel.swift | 301 |
3 files changed, 455 insertions, 0 deletions
diff --git a/ios/MullvadVPN/Debug/DebugCoordinator.swift b/ios/MullvadVPN/Debug/DebugCoordinator.swift new file mode 100644 index 0000000000..30299b3fc2 --- /dev/null +++ b/ios/MullvadVPN/Debug/DebugCoordinator.swift @@ -0,0 +1,45 @@ +// +// DebugCoordinator.swift +// MullvadVPN +// +// Created by Jon Petersson on 2026-02-27. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import Routing +import SwiftUI + +class DebugCoordinator: Coordinator, Presentable, Presenting { + private let navigationController: UINavigationController + private let viewModel: DebugViewModelImpl + // private let alertPresenter: AlertPresenter + + var presentedViewController: UIViewController { + navigationController + } + + var didFinish: ((DebugCoordinator) -> Void)? + + init( + navigationController: UINavigationController, + viewModel: DebugViewModelImpl + ) { + self.navigationController = navigationController + self.viewModel = viewModel + + super.init() + + // alertPresenter = AlertPresenter(context: self) + } + + func start(animated: Bool) { + let view = DebugView(viewModel: viewModel) + + let host = UIHostingController(rootView: view) + host.title = NSLocalizedString("Debug", comment: "") + host.view.setAccessibilityIdentifier(.debugView) + host.view.backgroundColor = .secondaryColor + + navigationController.pushViewController(host, animated: animated) + } +} diff --git a/ios/MullvadVPN/Debug/DebugView.swift b/ios/MullvadVPN/Debug/DebugView.swift new file mode 100644 index 0000000000..8de0299ca8 --- /dev/null +++ b/ios/MullvadVPN/Debug/DebugView.swift @@ -0,0 +1,109 @@ +// +// DebugView.swift +// MullvadVPN +// +// Created by Jon Petersson on 2026-02-27. +// Copyright © 2026 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import SwiftUI + +@MainActor +struct DebugView<ViewModel: DebugViewModel>: View { + @ObservedObject var viewModel: ViewModel + @State private var alert: MullvadAlert? + + @State private var showRelays: Bool = false + @State private var showSettings: Bool = false + + var body: some View { + ZStack { + ScrollView { + VStack(alignment: .leading) { + HStack { + Spacer() + } + + connection() + settings() + } + .background(Color.mullvadBackground) + Spacer() + } + .padding(EdgeInsets(UIMetrics.contentLayoutMargins)) + } + .mullvadAlert(item: $alert) + .font(.mullvadTiny) + .foregroundStyle(Color.mullvadTextPrimary) + .background(Color.mullvadBackground) + + Spacer() + } +} + +extension DebugView { + private func connection() -> some View { + VStack { + Button { + withAnimation { + showRelays.toggle() + } + } label: { + Text("Connection".excludeLocalization) + .font(.mullvadMedium) + .foregroundStyle(Color.mullvadTextSecondary) + Spacer() + } + + RowSeparator() + + VStack(alignment: .leading) { + ForEach(Array(viewModel.connection), id: \.title) { item in + createSection(title: item.title, rows: item.data) + } + } + .showIf(showRelays) + } + } + + private func settings() -> some View { + VStack { + Button { + withAnimation { + showSettings.toggle() + } + } label: { + Text("Settings".excludeLocalization) + .font(.mullvadMedium) + .foregroundStyle(Color.mullvadTextSecondary) + Spacer() + } + + RowSeparator() + + VStack(alignment: .leading) { + ForEach(Array(viewModel.settings), id: \.title) { item in + createSection(title: item.title, rows: item.data) + } + }.showIf(showSettings) + } + } + + private func createSection(title: String, rows: [String]) -> some View { + VStack(alignment: .leading) { + Text(title) + .font(.mullvadSmallSemiBold) + .foregroundStyle(Color.mullvadTextSecondary) + ForEach(rows, id: \.self) { + Text($0) + } + + RowSeparator() + } + } +} + +#Preview { + DebugView(viewModel: MockDebugViewModel()) +} 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", + ] + ), + ] +} |
