From 0ee41873d10f9d6cf3f845566f6bff3b466b5d09 Mon Sep 17 00:00:00 2001 From: Bug Magnet Date: Fri, 24 Nov 2023 16:05:46 +0100 Subject: [PATCH] Reflect obfuscation transport layer in the UI --- ios/MullvadTypes/TransportLayer.swift | 14 ++ ios/MullvadVPN.xcodeproj/project.pbxproj | 23 +++ .../SimulatorTunnelProviderHost.swift | 4 +- .../Preferences/PreferencesCellFactory.swift | 2 - .../Preferences/PreferencesDataSource.swift | 27 +--- .../Tunnel/TunnelControlView.swift | 64 ++++---- .../Tunnel/TunnelControlViewModel.swift | 70 +++++++++ .../Tunnel/TunnelViewController.swift | 28 ++-- .../Actor/ObservedState.swift | 8 + .../Actor/PacketTunnelActor.swift | 142 ++++++++---------- .../Actor/ProtocolObfuscator.swift | 10 ++ ios/PacketTunnelCore/Actor/State.swift | 8 + .../Mocks/ProtocolObfuscationStub.swift | 4 + .../Mocks/TunnelObfuscationStub.swift | 3 + .../UDPOverTCPObfuscator.swift | 8 + 15 files changed, 266 insertions(+), 149 deletions(-) create mode 100644 ios/MullvadTypes/TransportLayer.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift diff --git a/ios/MullvadTypes/TransportLayer.swift b/ios/MullvadTypes/TransportLayer.swift new file mode 100644 index 000000000000..b4a7e6c3cdd2 --- /dev/null +++ b/ios/MullvadTypes/TransportLayer.swift @@ -0,0 +1,14 @@ +// +// TransportLayer.swift +// MullvadTypes +// +// Created by Marco Nikic on 2023-11-24. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public enum TransportLayer: Codable { + case udp + case tcp +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 0f5a62ca1c33..d009ee6201e8 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -505,6 +505,9 @@ A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */; }; A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */; }; A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */; }; + A91614D12B108D1B00F416EB /* TransportLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91614D02B108D1B00F416EB /* TransportLayer.swift */; }; + A91614D42B108F5600F416EB /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; + A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */; }; A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; }; A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; }; A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; @@ -985,6 +988,13 @@ remoteGlobalIDString = 7A88DCCD2A8FABBE00D2FF0E; remoteInfo = Routing; }; + A91614D22B108F4D00F416EB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; + remoteInfo = MullvadTypes; + }; A91D78E12B03BDE500FCD5D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -1620,6 +1630,8 @@ A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DevicesProxy+Stubs.swift"; sourceTree = ""; }; A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = ""; }; A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = ""; }; + A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = ""; }; + A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlViewModel.swift; sourceTree = ""; }; A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = ""; }; A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = ""; }; @@ -1724,6 +1736,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A91614D42B108F5600F416EB /* MullvadTypes.framework in Frameworks */, 584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2005,6 +2018,7 @@ 581DA2722A1E227D0046ED47 /* RESTTypes.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 58E511E028DDB7F100B0BCDE /* WrappingError.swift */, + A91614D02B108D1B00F416EB /* TransportLayer.swift */, ); path = MullvadTypes; sourceTree = ""; @@ -2186,6 +2200,7 @@ 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */, 58CCA00F224249A1004F3011 /* TunnelViewController.swift */, 5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */, + A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */, ); path = Tunnel; sourceTree = ""; @@ -3307,6 +3322,7 @@ buildRules = ( ); dependencies = ( + A91614D32B108F4D00F416EB /* PBXTargetDependency */, ); name = TunnelObfuscation; productName = TunnelObfuscator; @@ -4413,6 +4429,7 @@ 58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */, + A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 7AF9BE992A4E0FE900DBFEDB /* MarkdownStylingOptions.swift in Sources */, 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, @@ -4685,6 +4702,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A91614D12B108D1B00F416EB /* TransportLayer.swift in Sources */, 58D22406294C90210029F5F8 /* IPv4Endpoint.swift in Sources */, 7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */, 58D22407294C90210029F5F8 /* IPv6Endpoint.swift in Sources */, @@ -4999,6 +5017,11 @@ target = 7A88DCCD2A8FABBE00D2FF0E /* Routing */; targetProxy = 7ABCA5B52A9349F20044A708 /* PBXContainerItemProxy */; }; + A91614D32B108F4D00F416EB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; + targetProxy = A91614D22B108F4D00F416EB /* PBXContainerItemProxy */; + }; A91D78E22B03BDE500FCD5D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */; diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index ecd2082d9502..86e4d83030b8 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -184,7 +184,9 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { selectedRelay: selectedRelay, relayConstraints: try SettingsManager.readSettings().relayConstraints, networkReachability: .reachable, - connectionAttemptCount: 0 + connectionAttemptCount: 0, + transportLayer: .udp, + remotePort: selectedRelay.endpoint.ipv4Relay.port ) ) } catch { diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift index 636f16547c76..00cfc5a30453 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift @@ -108,7 +108,6 @@ final class PreferencesCellFactory: CellFactoryProtocol { } } - #if DEBUG case .wireGuardObfuscationAutomatic: guard let cell = cell as? SelectableSettingsCell else { return } @@ -157,7 +156,6 @@ final class PreferencesCellFactory: CellFactoryProtocol { ) cell.accessibilityHint = nil cell.applySubCellStyling() - #endif } } } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift index f712df58e2de..176c843aff9b 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift @@ -19,10 +19,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case dnsSettings case wireGuardPort case wireGuardCustomPort - #if DEBUG case wireGuardObfuscation case wireGuardObfuscationPort - #endif var reusableViewClass: AnyClass { switch self { case .dnsSettings: @@ -31,12 +29,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .wireGuardCustomPort: return SettingsInputCell.self - #if DEBUG case .wireGuardObfuscation: return SelectableSettingsCell.self case .wireGuardObfuscationPort: return SelectableSettingsCell.self - #endif } } } @@ -52,22 +48,18 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< enum Section: String, Hashable, CaseIterable { case dnsSettings case wireGuardPorts - #if DEBUG case wireGuardObfuscation case wireGuardObfuscationPort - #endif } enum Item: Hashable { case dnsSettings case wireGuardPort(_ port: UInt16?) case wireGuardCustomPort - #if DEBUG case wireGuardObfuscationAutomatic case wireGuardObfuscationOn case wireGuardObfuscationOff case wireGuardObfuscationPort(_ port: UInt16) - #endif static var wireGuardPorts: [Item] { let defaultPorts = PreferencesViewModel.defaultWireGuardPorts.map { @@ -76,7 +68,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return [.wireGuardPort(nil)] + defaultPorts + [.wireGuardCustomPort] } - #if DEBUG static var wireGuardObfuscation: [Item] { [.wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, wireGuardObfuscationOff] } @@ -84,7 +75,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< static var wireGuardObfuscationPort: [Item] { [.wireGuardObfuscationPort(0), wireGuardObfuscationPort(80), wireGuardObfuscationPort(5001)] } - #endif + var accessibilityIdentifier: String { switch self { case .dnsSettings: @@ -97,7 +88,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } case .wireGuardCustomPort: return "wireGuardCustomPort" - #if DEBUG case .wireGuardObfuscationAutomatic: return "Automatic" case .wireGuardObfuscationOn: @@ -109,7 +99,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return "Automatic" } return "\(port)" - #endif } } @@ -121,12 +110,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return .wireGuardPort case .wireGuardCustomPort: return .wireGuardCustomPort - #if DEBUG case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, .wireGuardObfuscationOff: return .wireGuardObfuscation case .wireGuardObfuscationPort: return .wireGuardObfuscationPort - #endif } } } @@ -144,7 +131,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< ? .wireGuardPort(viewModel.wireGuardPort) : .wireGuardCustomPort - #if DEBUG let obfuscationStateItem: Item = switch viewModel.obfuscationState { case .automatic: .wireGuardObfuscationAutomatic case .off: .wireGuardObfuscationOff @@ -158,11 +144,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< indexPath(for: obfuscationStateItem), indexPath(for: obfuscationPortItem), ].compactMap { $0 } - #else - return [ - indexPath(for: wireGuardPortItem), - ].compactMap { $0 } - #endif } init(tableView: UITableView) { @@ -254,7 +235,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case .wireGuardCustomPort: getCustomPortCell()?.textField.becomeFirstResponder() - #if DEBUG case .wireGuardObfuscationAutomatic: selectObfuscationState(.automatic) delegate?.didChangeViewModel(viewModel) @@ -267,7 +247,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case let .wireGuardObfuscationPort(port): selectObfuscationPort(port) delegate?.didChangeViewModel(viewModel) - #endif default: break } @@ -299,14 +278,12 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< configureWireguardPortsHeader(view) return view - #if DEBUG case .wireGuardObfuscation: configureObfuscationHeader(view) return view case .wireGuardObfuscationPort: configureObfuscationPortHeader(view) return view - #endif default: return nil } @@ -433,7 +410,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } } - #if DEBUG private func configureObfuscationHeader(_ header: SettingsHeaderView) { let title = NSLocalizedString( "OBFUSCATION_HEADER_LABEL", @@ -489,7 +465,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< self.map { $0.delegate?.showInfo(for: .wireGuardObfuscationPort) } } } - #endif private func selectRow(at indexPath: IndexPath?, animated: Bool = false) { tableView?.selectRow(at: indexPath, animated: animated, scrollPosition: .none) diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 132a9729111c..e1493f27fae7 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -8,6 +8,7 @@ import MapKit import MullvadTypes +import PacketTunnelCore import UIKit enum TunnelControlAction { @@ -99,7 +100,7 @@ final class TunnelControlView: UIView { }() private var traitConstraints = [NSLayoutConstraint]() - private var tunnelState: TunnelState = .disconnected + private var viewModel: TunnelControlViewModel? var actionHandler: ((TunnelControlAction) -> Void)? @@ -135,24 +136,30 @@ final class TunnelControlView: UIView { if previousTraitCollection?.userInterfaceIdiom != traitCollection.userInterfaceIdiom || previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass { - updateActionButtons() + if let viewModel { + updateActionButtons(tunnelState: viewModel.tunnelStatus.state) + } } } - func update(from tunnelState: TunnelState, animated: Bool) { - self.tunnelState = tunnelState - - updateSecureLabel() - updateActionButtons() - updateTunnelRelay() - } - - func update(from outgoingConnectionInfo: OutgoingConnectionInfo) { - if let tunnelRelay = tunnelState.relay { - connectionPanel.dataSource = ConnectionPanelData( - inAddress: "\(tunnelRelay.endpoint.ipv4Relay) UDP", - outAddress: outgoingConnectionInfo.outAddress - ) + func update(with model: TunnelControlViewModel) { + viewModel = model + let tunnelState = model.tunnelStatus.state + secureLabel.text = model.secureLabelText + secureLabel.textColor = tunnelState.textColorForSecureLabel + selectLocationButtonBlurView.isEnabled = model.enableButtons + connectButtonBlurView.isEnabled = model.enableButtons + cityLabel.attributedText = attributedStringForLocation(string: model.city) + countryLabel.attributedText = attributedStringForLocation(string: model.country) + connectionPanel.connectedRelayName = model.connectedRelayName + connectionPanel.dataSource = model.connectionPanel + + updateSecureLabel(tunnelState: tunnelState) + updateActionButtons(tunnelState: tunnelState) + if tunnelState.isSecured { + updateTunnelRelay(tunnelRelay: tunnelState.relay) + } else { + updateTunnelRelay(tunnelRelay: nil) } } @@ -164,21 +171,21 @@ final class TunnelControlView: UIView { } } - private func updateActionButtons() { + private func updateActionButtons(tunnelState: TunnelState) { let actionButtons = tunnelState.actionButtons(traitCollection: traitCollection) let views = actionButtons.map { self.view(forActionButton: $0) } - updateButtonTitles() - updateButtonEnabledStates() + updateButtonTitles(tunnelState: tunnelState) + updateButtonEnabledStates(shouldEnableButtons: tunnelState.shouldEnableButtons) setArrangedButtons(views) } - private func updateSecureLabel() { + private func updateSecureLabel(tunnelState: TunnelState) { secureLabel.text = tunnelState.localizedTitleForSecureLabel.uppercased() secureLabel.textColor = tunnelState.textColorForSecureLabel } - private func updateButtonTitles() { + private func updateButtonTitles(tunnelState: TunnelState) { connectButton.setTitle( NSLocalizedString( "CONNECT_BUTTON_TITLE", @@ -215,15 +222,13 @@ final class TunnelControlView: UIView { ) } - private func updateButtonEnabledStates() { - let shouldEnableButtons = tunnelState.shouldEnableButtons - + private func updateButtonEnabledStates(shouldEnableButtons: Bool) { selectLocationButtonBlurView.isEnabled = shouldEnableButtons connectButtonBlurView.isEnabled = shouldEnableButtons } - private func updateTunnelRelay() { - if let tunnelRelay = tunnelState.relay { + private func updateTunnelRelay(tunnelRelay: SelectedRelay?) { + if let tunnelRelay { cityLabel.attributedText = attributedStringForLocation( string: tunnelRelay.location.city ) @@ -231,11 +236,6 @@ final class TunnelControlView: UIView { string: tunnelRelay.location.country ) - connectionPanel.dataSource = ConnectionPanelData( - // TODO: - UDP shouldn't be hardcoded after tunnel obfuscation - inAddress: "\(tunnelRelay.endpoint.ipv4Relay) UDP", - outAddress: nil - ) connectionPanel.isHidden = false connectionPanel.connectedRelayName = tunnelRelay.hostname } else { @@ -245,7 +245,7 @@ final class TunnelControlView: UIView { connectionPanel.isHidden = true } - locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel + locationContainerView.accessibilityLabel = viewModel?.tunnelStatus.state.localizedAccessibilityLabel } // MARK: - Private diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift new file mode 100644 index 000000000000..f1fb518efac0 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift @@ -0,0 +1,70 @@ +// +// TunnelControlViewModel.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-11-24. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +struct TunnelControlViewModel { + let tunnelStatus: TunnelStatus + let secureLabelText: String + let connectionPanel: ConnectionPanelData + let enableButtons: Bool + let city: String + let country: String + let connectedRelayName: String + + func update(status: TunnelStatus) -> TunnelControlViewModel { + TunnelControlViewModel( + tunnelStatus: status, + secureLabelText: secureLabelText, + connectionPanel: connectionPanel, + enableButtons: enableButtons, + city: city, + country: country, + connectedRelayName: connectedRelayName + ) + } + + func update(outgoingConnectionInfo: OutgoingConnectionInfo) -> TunnelControlViewModel { + let inPort = tunnelStatus.observedState.connectionState?.remotePort ?? 0 + + var connectionPanelData = ConnectionPanelData(inAddress: "") + if let tunnelRelay = tunnelStatus.state.relay { + var protocolLayer = "" + if case let .connected(state) = tunnelStatus.observedState { + protocolLayer = state.transportLayer == .tcp ? "TCP" : "UDP" + } + + connectionPanelData = ConnectionPanelData( + inAddress: "\(tunnelRelay.endpoint.ipv4Relay.ip):\(inPort) \(protocolLayer)", + outAddress: outgoingConnectionInfo.outAddress + ) + } + + return TunnelControlViewModel( + tunnelStatus: tunnelStatus, + secureLabelText: secureLabelText, + connectionPanel: connectionPanelData, + enableButtons: enableButtons, + city: city, + country: country, + connectedRelayName: connectedRelayName + ) + } + + static var empty: Self { + TunnelControlViewModel( + tunnelStatus: TunnelStatus(), + secureLabelText: "", + connectionPanel: ConnectionPanelData(inAddress: ""), + enableButtons: true, + city: "", + country: "", + connectedRelayName: "" + ) + } +} diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 3c6cc11ea603..d5ca5caf2ec4 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -16,6 +16,7 @@ class TunnelViewController: UIViewController, RootContainment { private let interactor: TunnelViewControllerInteractor private let contentView = TunnelControlView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) private var tunnelState: TunnelState = .disconnected + private var viewModel = TunnelControlViewModel.empty var shouldShowSelectLocationPicker: (() -> Void)? var shouldShowCancelTunnelAlert: (() -> Void)? @@ -61,10 +62,11 @@ class TunnelViewController: UIViewController, RootContainment { interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in self?.setTunnelState(tunnelStatus.state, animated: true) + self?.updateViewModel(tunnelStatus: tunnelStatus) } interactor.didGetOutGoingAddress = { [weak self] outgoingConnectionInfo in - self?.contentView.update(from: outgoingConnectionInfo) + self?.updateViewModel(outgoingConnectionInfo: outgoingConnectionInfo) } contentView.actionHandler = { [weak self] action in @@ -94,8 +96,21 @@ class TunnelViewController: UIViewController, RootContainment { addContentView() tunnelState = interactor.tunnelStatus.state - updateContentView(animated: false) updateMap(animated: false) + updateViewModel(tunnelStatus: interactor.tunnelStatus) + } + + func updateViewModel( + tunnelStatus: TunnelStatus? = nil, + outgoingConnectionInfo: OutgoingConnectionInfo? = nil + ) { + if let tunnelStatus { + viewModel = viewModel.update(status: tunnelStatus) + } + if let outgoingConnectionInfo { + viewModel = viewModel.update(outgoingConnectionInfo: outgoingConnectionInfo) + } + contentView.update(with: viewModel) } override func viewWillTransition( @@ -104,9 +119,7 @@ class TunnelViewController: UIViewController, RootContainment { ) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: nil, completion: { context in - self.updateContentView(animated: context.isAnimated) - }) + contentView.update(with: viewModel) } func setMainContentHidden(_ isHidden: Bool, animated: Bool) { @@ -129,7 +142,6 @@ class TunnelViewController: UIViewController, RootContainment { guard isViewLoaded else { return } - updateContentView(animated: animated) updateMap(animated: animated) } @@ -167,10 +179,6 @@ class TunnelViewController: UIViewController, RootContainment { } } - private func updateContentView(animated: Bool) { - contentView.update(from: tunnelState, animated: animated) - } - private func addMapController() { let mapView = mapViewController.view! mapView.translatesAutoresizingMaskIntoConstraints = false diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index 01f31e9abd3a..a6b7d741bc72 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -28,6 +28,8 @@ public struct ObservedConnectionState: Equatable, Codable { public var relayConstraints: RelayConstraints public var networkReachability: NetworkReachability public var connectionAttemptCount: UInt + public var transportLayer: TransportLayer + public var remotePort: UInt16 public var lastKeyRotation: Date? public var isNetworkReachable: Bool { @@ -39,12 +41,16 @@ public struct ObservedConnectionState: Equatable, Codable { relayConstraints: RelayConstraints, networkReachability: NetworkReachability, connectionAttemptCount: UInt, + transportLayer: TransportLayer, + remotePort: UInt16, lastKeyRotation: Date? = nil ) { self.selectedRelay = selectedRelay self.relayConstraints = relayConstraints self.networkReachability = networkReachability self.connectionAttemptCount = connectionAttemptCount + self.transportLayer = transportLayer + self.remotePort = remotePort self.lastKeyRotation = lastKeyRotation } } @@ -85,6 +91,8 @@ extension ConnectionState { relayConstraints: relayConstraints, networkReachability: networkReachability, connectionAttemptCount: connectionAttemptCount, + transportLayer: transportLayer, + remotePort: remotePort, lastKeyRotation: lastKeyRotation ) } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index b624b29cd227..8d3372c6beb9 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -228,7 +228,7 @@ extension PacketTunnelActor { ) async throws { let settings: Settings = try settingsReader.read() - guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason), + guard let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason), let targetState = state.targetStateForReconnect else { return } let activeKey: PrivateKey @@ -246,18 +246,11 @@ extension PacketTunnelActor { state = .reconnecting(connectionState) } - var endpoint = connectionState.selectedRelay.endpoint - endpoint = protocolObfuscator.obfuscate( - endpoint, - settings: settings, - retryAttempts: connectionState.selectedRelay.retryAttempts - ) - let configurationBuilder = ConfigurationBuilder( privateKey: activeKey, interfaceAddresses: settings.interfaceAddresses, dns: settings.dnsServers, - endpoint: endpoint + endpoint: connectionState.connectedEndpoint ) /* @@ -276,7 +269,7 @@ extension PacketTunnelActor { try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) // Resume tunnel monitoring and use IPv4 gateway as a probe address. - tunnelMonitor.start(probeAddress: endpoint.ipv4Gateway) + tunnelMonitor.start(probeAddress: connectionState.selectedRelay.endpoint.ipv4Gateway) } /** @@ -294,90 +287,84 @@ extension PacketTunnelActor { settings: Settings, reason: ReconnectReason ) throws -> ConnectionState? { - switch state { - case .initial: - return try makeConnectionStateInner( + var keyPolicy: KeyPolicy = .useCurrent + var networkReachability = defaultPathObserver.defaultPath?.networkReachability ?? .undetermined + var lastKeyRotation: Date? + + let callRelaySelector = { [self] maybeCurrentRelay, connectionCount in + try self.selectRelay( nextRelay: nextRelay, - settings: settings, - keyPolicy: .useCurrent, - networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined, - lastKeyRotation: nil + relayConstraints: settings.relayConstraints, + currentRelay: maybeCurrentRelay, + connectionAttemptCount: connectionCount ) + } - case var .connecting(connState), var .reconnecting(connState): - switch reason { - case .connectionLoss: - // Increment attempt counter when reconnection is requested due to connectivity loss. - connState.incrementAttemptCount() - case .userInitiated: - break + switch state { + case .initial: + break + case var .connecting(connectionState), var .reconnecting(connectionState): + if reason == .connectionLoss { + connectionState.incrementAttemptCount() } - // Explicit fallthrough fallthrough - - case var .connected(connState): - let relayConstraints = settings.relayConstraints - - connState.selectedRelay = try selectRelay( - nextRelay: nextRelay, - relayConstraints: relayConstraints, - currentRelay: connState.selectedRelay, - connectionAttemptCount: connState.connectionAttemptCount + case var .connected(connectionState): + let selectedRelay = try callRelaySelector( + connectionState.selectedRelay, + connectionState.connectionAttemptCount ) - connState.relayConstraints = relayConstraints - connState.currentKey = settings.privateKey - - return connState - + connectionState.selectedRelay = selectedRelay + connectionState.relayConstraints = settings.relayConstraints + connectionState.currentKey = settings.privateKey + return connectionState case let .error(blockedState): - return try makeConnectionStateInner( - nextRelay: nextRelay, - settings: settings, - keyPolicy: blockedState.keyPolicy, - networkReachability: blockedState.networkReachability, - lastKeyRotation: blockedState.lastKeyRotation - ) - + keyPolicy = blockedState.keyPolicy + lastKeyRotation = blockedState.lastKeyRotation + networkReachability = blockedState.networkReachability case .disconnecting, .disconnected: return nil } + let selectedRelay = try callRelaySelector(nil, 0) + return ConnectionState( + selectedRelay: selectedRelay, + relayConstraints: settings.relayConstraints, + currentKey: settings.privateKey, + keyPolicy: keyPolicy, + networkReachability: networkReachability, + connectionAttemptCount: 0, + lastKeyRotation: lastKeyRotation, + connectedEndpoint: selectedRelay.endpoint, + transportLayer: .udp, + remotePort: selectedRelay.endpoint.ipv4Relay.port + ) } - /** - Create a connection state when `State` is either `.inital` or `.error`. - - - Parameters: - - nextRelay: Next relay to connect to. - - settings: Current settings. - - keyPolicy: Current key that should be used by the tunnel. - - networkReachability: Network connectivity outside of tunnel. - - lastKeyRotation: Last time packet tunnel rotated the key. - - - Returns: New connection state, or `nil` if new relay cannot be selected. - */ - private func makeConnectionStateInner( + private func obfuscateConnection( nextRelay: NextRelay, settings: Settings, - keyPolicy: KeyPolicy, - networkReachability: NetworkReachability, - lastKeyRotation: Date? + reason: ReconnectReason ) throws -> ConnectionState? { - let relayConstraints = settings.relayConstraints - let privateKey = settings.privateKey + guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason) + else { return nil } + + let obfuscatedEndpoint = protocolObfuscator.obfuscate( + connectionState.selectedRelay.endpoint, + settings: settings, + retryAttempts: connectionState.selectedRelay.retryAttempts + ) + let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp return ConnectionState( - selectedRelay: try selectRelay( - nextRelay: nextRelay, - relayConstraints: relayConstraints, - currentRelay: nil, - connectionAttemptCount: 0 - ), - relayConstraints: relayConstraints, - currentKey: privateKey, - keyPolicy: keyPolicy, - networkReachability: networkReachability, - connectionAttemptCount: 0, - lastKeyRotation: lastKeyRotation + selectedRelay: connectionState.selectedRelay, + relayConstraints: connectionState.relayConstraints, + currentKey: settings.privateKey, + keyPolicy: connectionState.keyPolicy, + networkReachability: connectionState.networkReachability, + connectionAttemptCount: connectionState.connectionAttemptCount, + lastKeyRotation: connectionState.lastKeyRotation, + connectedEndpoint: obfuscatedEndpoint, + transportLayer: transportLayer, + remotePort: protocolObfuscator.remotePort ) } @@ -420,5 +407,4 @@ extension PacketTunnelActor { } extension PacketTunnelActor: PacketTunnelActorProtocol {} - // swiftlint:disable:this file_length diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift index 467dd4c9df63..0b59e7a23ac2 100644 --- a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift +++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift @@ -12,6 +12,8 @@ import TunnelObfuscation public protocol ProtocolObfuscation { func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint + var transportLayer: TransportLayer? { get } + var remotePort: UInt16 { get } } public class ProtocolObfuscator: ProtocolObfuscation { @@ -28,8 +30,15 @@ public class ProtocolObfuscator: ProtocolObfuscat /// - settings: Whether obfuscation should be used or not. /// - retryAttempts: The number of times a connection was attempted to `endpoint` /// - Returns: `endpoint` if obfuscation is disabled, or an obfuscated endpoint otherwise. + public var transportLayer: TransportLayer? { + return tunnelObfuscator?.transportLayer + } + + private(set) public var remotePort: UInt16 = 0 + public func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt = 0) -> MullvadEndpoint { var obfuscatedEndpoint = endpoint + remotePort = endpoint.ipv4Relay.port let shouldObfuscate = switch settings.obfuscation.state { case .automatic: retryAttempts % 4 == 2 || retryAttempts % 4 == 3 @@ -51,6 +60,7 @@ public class ProtocolObfuscator: ProtocolObfuscat remoteAddress: obfuscatedEndpoint.ipv4Relay.ip, tcpPort: tcpPort.portValue ) + remotePort = tcpPort.portValue obfuscator.start() tunnelObfuscator = obfuscator diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 03149081374d..3cca82d865d7 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -127,6 +127,14 @@ struct ConnectionState { let (value, isOverflow) = connectionAttemptCount.addingReportingOverflow(1) connectionAttemptCount = isOverflow ? 0 : value } + + /// The actual endpoint fed to WireGuard, can be a local endpoint if obfuscation is used. + public let connectedEndpoint: MullvadEndpoint + /// Via which transport protocol was the connection made to the relay + public let transportLayer: TransportLayer + + /// The remote port that was chosen to connect to `connectedEndpoint` + public let remotePort: UInt16 } /// Data associated with error state. diff --git a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift index 7426075b61d8..acb69753f1ea 100644 --- a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift @@ -11,7 +11,11 @@ import Foundation @testable import PacketTunnelCore struct ProtocolObfuscationStub: ProtocolObfuscation { + var remotePort: UInt16 { 42 } + func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint { endpoint } + + var transportLayer: TransportLayer? { .udp } } diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift index e07011598659..8c7fdc83f077 100644 --- a/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift @@ -7,10 +7,13 @@ // import Foundation +@testable import MullvadTypes import Network @testable import TunnelObfuscation struct TunnelObfuscationStub: TunnelObfuscation { + var transportLayer: TransportLayer { .udp } + let remotePort: UInt16 init(remoteAddress: IPAddress, tcpPort: UInt16) { remotePort = tcpPort diff --git a/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift b/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift index 9af841056134..580c4937d785 100644 --- a/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift +++ b/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadTypes import Network import TunnelObfuscatorProxy @@ -15,6 +16,9 @@ public protocol TunnelObfuscation { func start() func stop() var localUdpPort: UInt16 { get } + var remotePort: UInt16 { get } + + var transportLayer: TransportLayer { get } } /// Class that implements UDP over TCP obfuscation by accepting traffic on a local UDP port and proxying it over TCP to the remote endpoint. @@ -32,6 +36,10 @@ public final class UDPOverTCPObfuscator: TunnelObfuscation { return stateLock.withLock { proxyHandle.port } } + public var remotePort: UInt16 { tcpPort } + + public var transportLayer: TransportLayer { .tcp } + /// Initialize tunnel obfuscator with remote server address and TCP port where udp2tcp is running. public init(remoteAddress: IPAddress, tcpPort: UInt16) { self.remoteAddress = remoteAddress