summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-12-06 11:58:37 +0100
committerBug Magnet <marco.nikic@mullvad.net>2023-12-06 11:58:37 +0100
commitac3e222d031b0f599561c4c30504de5cd3f871a2 (patch)
tree2b0f1abb1941436a115c12dc643fd7fd100188f5
parentdfd6436010b1eeebb5a33812f3a8055aa4ad6323 (diff)
parent0ee41873d10f9d6cf3f845566f6bff3b466b5d09 (diff)
downloadmullvadvpn-ac3e222d031b0f599561c4c30504de5cd3f871a2.tar.xz
mullvadvpn-ac3e222d031b0f599561c4c30504de5cd3f871a2.zip
Merge branch 'reflect-obfuscation-protocol-in-ui-ios-398'
-rw-r--r--ios/MullvadTypes/TransportLayer.swift14
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj23
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift27
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift62
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift70
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift28
-rw-r--r--ios/PacketTunnelCore/Actor/ObservedState.swift8
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift142
-rw-r--r--ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift10
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift8
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift4
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift3
-rw-r--r--ios/TunnelObfuscation/UDPOverTCPObfuscator.swift8
15 files changed, 265 insertions, 148 deletions
diff --git a/ios/MullvadTypes/TransportLayer.swift b/ios/MullvadTypes/TransportLayer.swift
new file mode 100644
index 0000000000..b4a7e6c3cd
--- /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 0f5a62ca1c..d009ee6201 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 = "<group>"; };
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = "<group>"; };
A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = "<group>"; };
+ A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = "<group>"; };
+ A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlViewModel.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -2186,6 +2200,7 @@
58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */,
58CCA00F224249A1004F3011 /* TunnelViewController.swift */,
5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */,
+ A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */,
);
path = Tunnel;
sourceTree = "<group>";
@@ -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 ecd2082d95..86e4d83030 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 636f16547c..00cfc5a304 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 f712df58e2..176c843aff 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 132a972911..e1493f27fa 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(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
- func update(from outgoingConnectionInfo: OutgoingConnectionInfo) {
- if let tunnelRelay = tunnelState.relay {
- connectionPanel.dataSource = ConnectionPanelData(
- inAddress: "\(tunnelRelay.endpoint.ipv4Relay) UDP",
- outAddress: outgoingConnectionInfo.outAddress
- )
+ 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 0000000000..f1fb518efa
--- /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 3c6cc11ea6..d5ca5caf2e 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 01f31e9abd..a6b7d741bc 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 b624b29cd2..8d3372c6be 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 467dd4c9df..0b59e7a23a 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<Obfuscator: TunnelObfuscation>: ProtocolObfuscation {
@@ -28,8 +30,15 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: 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<Obfuscator: TunnelObfuscation>: 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 0314908137..3cca82d865 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 7426075b61..acb69753f1 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 e070115986..8c7fdc83f0 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 9af8410561..580c4937d7 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